├── .gitignore ├── README.md ├── app ├── app.install ├── app.js ├── app.png └── emoji.js /.gitignore: -------------------------------------------------------------------------------- 1 | app.cookies 2 | app.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Desktop Client for Twitter Mobile 2 | ... if that makes any sense to you. 3 | 4 | ### WAT!?! 5 | Twitter released a new https://mobile.twitter.com/ which looks pretty nice. 6 | You can finally have it as stand-alone application on Linux Desktop too, 7 | through a simple wrapper written in few hours of JavaScript writing and testing. 8 | 9 | ![jsGtk Twitter screenshot](http://webreflection.github.io/jsgtk-twitter/img/sc02.png?360) 10 | 11 | This is the beauty of [writing native App with JavaScript](https://www.webreflection.co.uk/blog/2015/12/08/writing-native-apps-with-javascript). 12 | 13 | 14 | ## How To Install on Mac / OSX 15 | 16 | **Note**: Highly experimental, it takes long time to install due lack of pre-built WebKit2GTK quartz package via MacPorts. 17 | 18 | If you don't have Command Line Tools already installed, please write this on your terminal: 19 | ```sh 20 | # content in https://github.com/WebReflection/jsgtk/blob/gh-pages/clt 21 | sh -c "$(curl -fsSL https://webreflection.github.io/jsgtk/clt)" 22 | ``` 23 | 24 | After that, the only current working packages manager is MacPorts. 25 | 26 | Please download and install it [from the official page](https://www.macports.org/install.php). 27 | 28 | The last step is to install [jsgtk]() and all dependencies, including WebKit2 GTK. 29 | Please write this in console. 30 | ```sh 31 | # content in https://github.com/WebReflection/jsgtk/blob/gh-pages/install 32 | WEBKIT=true sh -c "$(curl -fsSL https://webreflection.github.io/jsgtk/install)" 33 | ``` 34 | 35 | Please note it might take very long time to fully build all dependencies. 36 | 37 | In case you have/want X11 instead of quartz as UI backend, please export `X11=true` too. 38 | If you don't want any `gstreamer1-gst-plugin-bad`, neither `x11` nor `gtk2`, you can export `PURE_QUARTZ=true`. 39 | 40 | 41 | ## How To Install on Linux 42 | The dependency number 1 is [jsgtk](https://github.com/WebReflection/jsgtk). 43 | There are [few ways to install it](https://github.com/WebReflection/jsgtk#how-to-install), just pick your favorite. 44 | 45 | The easiest way is to write this on a terminal: 46 | ```sh 47 | # content in https://github.com/WebReflection/jsgtk/blob/gh-pages/install 48 | WEBKIT=true sh -c "$(curl -fsSL https://webreflection.github.io/jsgtk/install)" 49 | ``` 50 | 51 | 52 | ## How to run 53 | I haven't yet created a proper [AUR](https://wiki.archlinux.org/index.php/Arch_User_Repository) or [npm](https://www.npmjs.com/) package yet, but I eventually will. 54 | The simplest way to use this app is to clone this repository and then launch `./app`. 55 | 56 | The first time only you'll need to login through the website. 57 | This app doesn't hold, send, use, or analyze anything about you, your account, your twitter activity, or your credentials. 58 | 59 | 60 | ### How to install on Linux as Desktop App 61 | There is an `app.install` which, if executed, should make the app available through the main Desktop environment, at least in ArchLinux and GNOME, [which is my primary OS of choice](http://archibold.io/). 62 | 63 | 64 | ### How to test stuff ? 65 | Remember to launch the app via `./app --debug` to get notified about all the things and have no conflicts with the live web app. 66 | 67 | ![jsGtk Twitter screenshot](http://webreflection.github.io/jsgtk-twitter/img/sc01.png?360) 68 | -------------------------------------------------------------------------------- /app: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env jsgtk 2 | 3 | /*! 4 | * Copyright (c) 2016 Andrea Giammarchi - @WebReflection 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), 8 | * to deal in the Software without restriction, including without limitation 9 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | * and/or sell copies of the Software, and to permit persons to whom the Software 11 | * is furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included 14 | * in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 20 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 22 | * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | !*/ 24 | 25 | const 26 | DEBUG = imports.jsgtk.constants.DEBUG, 27 | GLib = require('GLib'), 28 | Gtk = require('Gtk'), 29 | Gio = require('Gio'), 30 | Gdk = require('Gdk'), 31 | GdkPixbuf = require('GdkPixbuf'), 32 | WebKit2 = require('WebKit2'), 33 | fs = require('fs'), 34 | os = require('os'), 35 | path = require('path'), 36 | spawn = require('child_process').spawn, 37 | Notify = os.platform() === 'darwin' ? 38 | { 39 | init: Object, 40 | isInitted() { return true; }, 41 | Notification(notification) { 42 | this.show = function show() { 43 | spawn('osascript', [ 44 | '-e', 45 | `display notification "${ 46 | notification.body 47 | }" with title "${ 48 | notification.summary 49 | }"` 50 | ]); 51 | }; 52 | } 53 | } : 54 | require('Notify') 55 | ; 56 | 57 | ({ 58 | title: 'jsGtk Twitter', 59 | channel: 'jsgtk' + String(Math.random()).slice(2), 60 | cookies: path.join(__dirname, 'app.cookies'), 61 | json: path.join(__dirname, 'app.json'), 62 | icon: path.join(__dirname, 'app.png'), 63 | javascript: fs.readFileSync(path.join(__dirname, 'app.js')).toString(), 64 | defaults: { 65 | showNotification: true, 66 | windowPosition: Gtk.WindowPosition.NONE, 67 | uri: 'https://mobile.twitter.com/' 68 | }, 69 | run(...args) { 70 | GLib.setPrgname(this.title); 71 | this.initInfo(); 72 | this.app = new Gtk.Application() 73 | .once('startup', () => this.initUI()) 74 | .once('shutdown', () => this.saveInfo()) 75 | .on('activate', () => { 76 | this.window.on('configure-event', () => this.updateInfo()); 77 | this.window.move(this.info.x, this.info.y); 78 | this.window.showAll(); 79 | }); 80 | this.app.run(args); 81 | }, 82 | initInfo() { 83 | try { 84 | this.info = JSON.parse(fs.readFileSync(this.json)); 85 | } catch(meh) { 86 | this.info = Object.assign({}, this.defaults); 87 | } 88 | }, 89 | updateInfo() { 90 | let [x, y] = this.window.getPosition(); 91 | let [width, height] = this.window.getSize(); 92 | this.info.x = x; 93 | this.info.y = y; 94 | this.info.defaultWidth = width; 95 | this.info.defaultHeight = height; 96 | }, 97 | saveInfo() { 98 | this.info.uri = this.webView.uri; 99 | fs.writeFileSync(this.json, JSON.stringify(this.info, null, ' ')); 100 | this.cleanUp(); 101 | }, 102 | initUI() { 103 | const 104 | screen = Gdk.Screen.getDefault(), 105 | margin = 160, 106 | config = { 107 | application: this.app, 108 | defaultWidth: this.info.defaultWidth || 360, 109 | defaultHeight: this.info.defaultHeight || 110 | Math.max(margin, screen.getHeight() - margin), 111 | windowPosition: this.info.windowPosition 112 | } 113 | ; 114 | this.window = new Gtk.ApplicationWindow(config); 115 | this.window.setIconFromFile(this.icon); 116 | this.window.on('delete_event', () => false); 117 | this.window.setTitlebar(this.header); 118 | this.window.add(this.webView); 119 | if (!this.info.hasOwnProperty('x')) { 120 | this.info.x = screen.getWidth() - 121 | Math.floor(margin / 4 + config.defaultWidth); 122 | } 123 | if (!this.info.hasOwnProperty('y')) { 124 | this.info.y = Math.ceil( 125 | (screen.getHeight() - config.defaultHeight) / 2 126 | ); 127 | } 128 | this.cleanUp(); 129 | }, 130 | cleanUp(files) { 131 | let cb = (files) => files.forEach(fileName => { 132 | if (fileName.slice(-5) === ':orig') { 133 | fs.unlink(fileName, Object); 134 | } 135 | }); 136 | if (files) cb(files); 137 | else fs.readdir(__dirname, (err, files) => cb(files)); 138 | }, 139 | get webView() { 140 | if (!this._webView) { 141 | const 142 | webView = new WebKit2.WebView(), 143 | context = webView.getContext() 144 | ; 145 | if (DEBUG) { 146 | webView.getSettings().setEnableWriteConsoleMessagesToStdout(true); 147 | [ 148 | 'insecure-content-detected', 149 | 'load-failed', 150 | 'load-failed-with-tls-errors' 151 | ].forEach((type) => { 152 | webView.connect(type, () => { 153 | console.warn(type); 154 | }); 155 | }); 156 | } 157 | context.getCookieManager().setPersistentStorage( 158 | this.cookies, 159 | WebKit2.CookiePersistentStorage.TEXT 160 | ); 161 | webView.loadUri(this.info.uri || this.defaults.uri); 162 | webView.on('decide-policy', (webView, policy, type) => { 163 | switch(type) { 164 | case WebKit2.PolicyDecisionType.NAVIGATION_ACTION: 165 | let 166 | uri = policy.getRequest().getUri(), 167 | channel = this.channel 168 | ; 169 | if (uri.indexOf(channel + ':') === 0) { 170 | this.jsAction(uri.slice(channel.length + 1)); 171 | policy.ignore(); 172 | } else if (DEBUG) console.info(uri); 173 | break; 174 | } 175 | }); 176 | webView.on('permission-request', (webView, request, data) => { 177 | switch (true) { 178 | case request instanceof WebKit2.NotificationPermissionRequest && 179 | this.info.showNotification: 180 | request.allow(); 181 | break; 182 | default: 183 | request.deny(); 184 | break; 185 | } 186 | }); 187 | webView.on('load-changed', (webView, loadEvent, data) => { 188 | switch (loadEvent) { 189 | case 2: // FIGUREITOUT: where the hell is WEBKIT_LOAD_COMMITTED constant? 190 | let 191 | stringified = 'JSON.stringify(Array.prototype.slice.call(arguments, 0))', 192 | JSGTK = Object.keys(this.actions).map( 193 | key => typeof this.actions[key] === 'function' ? 194 | `${key}: function ${key}() { 195 | location.href = '${this.channel}:${key}(' + 196 | encodeURIComponent(${stringified}) + 197 | ')'; 198 | }` : 199 | `${key}: ${JSON.stringify(this.actions[key])}` 200 | ).join(',\n') 201 | ; 202 | this.JSGTK = JSGTK; 203 | webView.runJavaScript( 204 | `(function(window, JSGTK){'use strict'; 205 | ${this.javascript} 206 | ;window.addEventListener('${this.channel}', gtkHandler); 207 | }(this, {\nshowNotification: ${this.info.showNotification},\n${JSGTK}\n}));`, 208 | null, 209 | (webView, result) => { 210 | webView.runJavaScriptFinish(result); 211 | } 212 | ); 213 | break; 214 | } 215 | }); 216 | this._webView = webView; 217 | } 218 | return this._webView; 219 | }, 220 | get menu() { 221 | if (!this._menu) { 222 | const 223 | MenuButton = new Gtk.MenuButton({ 224 | image: new Gtk.Image({ 225 | iconName: 'open-menu-symbolic', 226 | iconSize: Gtk.IconSize.SMALL_TOOLBAR 227 | }) 228 | }), 229 | popMenu = new Gtk.Popover(), 230 | menu = new Gio.Menu() 231 | ; 232 | 233 | // group them via [{ ... }, { ... }] instead of { ... }, { ... } 234 | [ 235 | { 236 | name: 'Show notifications', 237 | action: 'notifications', 238 | extras: { 239 | state: new GLib.Variant('b', this.info.showNotification) 240 | }, 241 | callback: (action) => { 242 | let state = !action.getState().getBoolean(); 243 | action.setState(new GLib.Variant('b', state)); 244 | this.info.showNotification = state; 245 | this.runJavaScript(`{showNotification: ${state}}`); 246 | } 247 | }, 248 | [{ 249 | name: 'Show emoji', 250 | action: 'show-emoji', 251 | callback: () => { 252 | this.runJavaScript(`{showEmoji: true}`); 253 | } 254 | },{ 255 | name: 'Grab emoji', 256 | action: 'grab-emoji', 257 | callback: () => { 258 | if (this.emojiShown) { 259 | this.emojiShown = false; 260 | this.window.remove(this.emoji); 261 | this.window.add(this.webView); 262 | } else { 263 | this.emojiShown = true; 264 | this.window.remove(this.webView); 265 | this.window.add(this.emoji); 266 | } 267 | } 268 | },{ 269 | name: 'Grab current page uri', 270 | action: 'grab-page-uri', 271 | callback: () => { 272 | Gtk.Clipboard.getDefault( 273 | Gdk.Display.getDefault() 274 | ).setText( 275 | this.webView.uri.replace('mobile.', ''), 276 | -1 277 | ); 278 | } 279 | }], 280 | { 281 | name: 'Close', 282 | action: 'close', 283 | callback: () => this.window.close() 284 | } 285 | ].forEach((group) => { 286 | let section = new Gio.Menu(); 287 | [].concat(group).forEach((item) => { 288 | section.append(item.name, 'app.' + item.action); 289 | this.app.addAction( 290 | new Gio.SimpleAction(Object.assign( 291 | {}, 292 | item.extras || {}, 293 | {name: item.action} 294 | )) 295 | .on('activate', item.callback || Object) 296 | ); 297 | }); 298 | menu.appendSection(null, section); 299 | }); 300 | 301 | MenuButton.setPopover(popMenu); 302 | popMenu.setSizeRequest(-1, -1); 303 | MenuButton.setMenuModel(menu); 304 | this._menu = MenuButton; 305 | 306 | } 307 | return this._menu; 308 | }, 309 | get emoji() { 310 | if (!this._emoji) { 311 | const 312 | emoji = new WebKit2.WebView(), 313 | settings = emoji.getSettings() 314 | ; 315 | settings.setEnableJavascript(false); 316 | emoji.loadUri('https://twemoji.maxcdn.com/2/test/preview.html'); 317 | emoji.on('decide-policy', (webView, policy, type) => { 318 | switch(type) { 319 | case WebKit2.PolicyDecisionType.NAVIGATION_ACTION: 320 | let 321 | uri = policy.getRequest().getUri(), 322 | channel = this.channel 323 | ; 324 | if (uri.indexOf(channel + ':') === 0) { 325 | this.jsAction(uri.slice(channel.length + 1)); 326 | policy.ignore(); 327 | } else if (DEBUG) console.info(uri); 328 | break; 329 | } 330 | }); 331 | emoji.on('load-changed', (webView, loadEvent, data) => { 332 | switch (loadEvent) { 333 | case 3: 334 | settings.setEnableJavascript(true); 335 | webView.runJavaScript( 336 | `(function(window, JSGTK){'use strict'; 337 | ${fs.readFileSync(path.join(__dirname, 'emoji.js')).toString()} 338 | }(this, {\n${this.JSGTK}\n}));`, 339 | null, 340 | (webView, result) => { 341 | webView.runJavaScriptFinish(result); 342 | } 343 | ); 344 | break; 345 | } 346 | }); 347 | emoji.show(); 348 | this._emoji = emoji; 349 | } 350 | return this._emoji; 351 | }, 352 | get button() { 353 | if (!this._button) { 354 | const button = { 355 | back: Gtk.ToolButton.newFromStock(Gtk.STOCK_GO_BACK), 356 | refresh: Gtk.ToolButton.newFromStock(Gtk.STOCK_REFRESH), 357 | menu: this.menu 358 | }; 359 | button.back.on('clicked', () => this.webView.goBack()); 360 | button.refresh.on('clicked', () => this.webView.reload()); 361 | this._button = button; 362 | } 363 | return this._button; 364 | }, 365 | get header() { 366 | if (!this._header) { 367 | this._header = new Gtk.HeaderBar({ 368 | title: this.title, 369 | showCloseButton: false 370 | }); 371 | this._header.packStart(this.button.back); 372 | this._header.packStart(this.button.refresh); 373 | this._header.packEnd(this.button.menu); 374 | } 375 | return this._header; 376 | }, 377 | runJavaScript(detail) { 378 | this.webView.runJavaScript( 379 | `window.dispatchEvent( 380 | new CustomEvent( 381 | '${this.channel}', 382 | {detail: ${detail}} 383 | ) 384 | );`, 385 | null, 386 | (webView, result, error) => { 387 | webView.runJavaScriptFinish(result); 388 | } 389 | ); 390 | }, 391 | jsAction(which) { 392 | const 393 | i = which.indexOf('('), 394 | method = which.slice(0, i), 395 | args = JSON.parse(decodeURIComponent(which.slice(i + 1, -1))) 396 | ; 397 | this.actions[method].apply(this, args); 398 | }, 399 | calucalteSize(outer, inner, gap) { 400 | let 401 | mw = outer.getWidth() - gap, 402 | mh = outer.getHeight() - gap, 403 | cw = inner.getWidth(), 404 | ch = inner.getHeight() 405 | ; 406 | if (mw < cw) { 407 | ch = mw * ch / cw; 408 | cw = mw; 409 | } 410 | if (mh < ch) { 411 | cw = mh * cw / ch; 412 | ch = mh; 413 | } 414 | return [cw, ch]; 415 | }, 416 | actions: { 417 | debug: DEBUG, 418 | b64emoji(encode) { 419 | var result = {}; 420 | function grab(icon) { 421 | spawn( 422 | 'curl', 423 | ['-L', '-O', 'http://twemoji.maxcdn.com/2/svg/' + icon + '.svg'], 424 | {cwd: __dirname} 425 | ).once('close', () => { 426 | var chunks = []; 427 | spawn('base64', icon + '.svg') 428 | .once('close', () => { 429 | GLib.unlink(icon + '.svg'); 430 | result[icon] = 'data:image/svg+xml;base64,' + chunks.join(''); 431 | next(); 432 | }) 433 | .stdout.on('data', (data) => { 434 | chunks.push(data); 435 | }); 436 | }) 437 | } 438 | const next = () => { 439 | if (encode.length) { 440 | grab(encode.shift()); 441 | } else { 442 | this.runJavaScript(JSON.stringify({ 443 | b64: result 444 | })); 445 | } 446 | }; 447 | next(); 448 | }, 449 | grabEmoji(text) { 450 | this.emojiShown = false; 451 | this.window.remove(this.emoji); 452 | this.window.add(this.webView); 453 | if (DEBUG) print(text); 454 | Gtk.Clipboard.getDefault( 455 | Gdk.Display.getDefault() 456 | ).setText(text, -1); 457 | }, 458 | error() { 459 | console.error.apply(console, arguments); 460 | }, 461 | notify(notifications, messages) { 462 | if (this.info.showNotification) { 463 | if (!Notify.isInitted()) 464 | Notify.init(this.title); 465 | new Notify.Notification({ 466 | summary: `You have ${notifications + messages} updates`, 467 | body: ( 468 | (notifications ? `${notifications} notifications` : '') + 469 | (messages ? 470 | ((notifications ? ' and ' : '') + `${messages} messages`) : '') 471 | ), 472 | iconName: this.icon 473 | }).show(); 474 | } 475 | }, 476 | /* TODO: finish properly this gallery 477 | gallery(images) { 478 | let 479 | i = 0, 480 | screen = Gdk.Screen.getDefault(), 481 | files = images.map(src => path.join(__dirname, GLib.basename(src))) 482 | ; 483 | Promise.all(images.map(img => new Promise((res, rej) => { 484 | spawn('curl', ['-L', '-O', img], {cwd: __dirname}).once('close', res); 485 | }))).then(() => { 486 | new Promise((res, rej) => { 487 | spawn('sync', [], {}).once('close', res); 488 | }).then(() => { 489 | let 490 | margin = 0, 491 | window = new Gtk.Dialog({ 492 | defaultWidth: screen.getWidth() - margin, 493 | defaultHeight: screen.getHeight() - margin, 494 | modal: true, 495 | useHeaderBar: false, 496 | decorated: false, 497 | // opacity: 0.5, 498 | transientFor: this.window 499 | }) 500 | ,pixbuf = GdkPixbuf.Pixbuf.newFromFile(files[i]) 501 | ; 502 | pixbuf = Gtk.Image.newFromPixbuf(pixbuf.scaleSimple.apply( 503 | pixbuf, 504 | this.calucalteSize( 505 | screen, 506 | pixbuf, 507 | margin 508 | ).concat( 509 | GdkPixbuf.InterpType.BILINEAR 510 | ) 511 | )); 512 | window.once('delete_event', () => { 513 | this.cleanUp(files); 514 | window.destroy(); 515 | window = null; 516 | pixbuf = null; 517 | }); 518 | window.getContentArea().add(pixbuf); 519 | window.showAll(); 520 | }); 521 | }); 522 | }, 523 | //*/ 524 | open(uri) { 525 | // problems with this operation on OSX 526 | if (os.platform() === 'darwin') { 527 | // fallback to a system call 528 | spawn('open', [uri]); 529 | } else { 530 | Gio.AppInfo.launchDefaultForUri(uri, null); 531 | } 532 | } 533 | } 534 | }).run(); 535 | -------------------------------------------------------------------------------- /app.install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | APP="app" 4 | ICON="app.png" 5 | 6 | echo "#!/usr/bin/env bash 7 | cd $(pwd) 8 | ./$APP \"\$@\" > /dev/null 2>&1 & 9 | ">"$APP.tmp" 10 | 11 | mkdir -p /tmp/create-desktop-app 12 | mv "$APP.tmp" /tmp/create-desktop-app/$APP 13 | cp $ICON /tmp/create-desktop-app 14 | cd /tmp/create-desktop-app 15 | chmod +x "$APP" 16 | echo '#!/bin/sh 17 | set -e 18 | test -n "$srcdir" || srcdir=`dirname "$0"` 19 | test -n "$srcdir" || srcdir=. 20 | olddir=`pwd` 21 | cd "$srcdir" 22 | autoreconf --force --install 23 | cd "$olddir" 24 | if test -z "$NOCONFIGURE"; then 25 | "$srcdir"/configure "$@" 26 | fi'>autogen.sh 27 | chmod +x autogen.sh 28 | echo "[Desktop Entry] 29 | Version=1.0 30 | Encoding=UTF-8 31 | Name=jsGtk Twitter 32 | Comment=Mobile Twitter Desktop Wrapper 33 | Exec=@prefix@/bin/$APP 34 | Icon=@prefix@/bin/$ICON 35 | Terminal=false 36 | Type=Application 37 | StartupNotify=true 38 | Categories=GNOME;Application;">"${APP}.desktop.in" 39 | echo "bin_SCRIPTS = ${APP}">Makefile.am 40 | echo 'EXTRA_DIST = $(bin_SCRIPTS)'>>Makefile.am 41 | echo 'desktopdir = $(datadir)/applications'>>Makefile.am 42 | echo "desktop_DATA = ${APP}.desktop">>Makefile.am 43 | echo "AC_INIT([$2], 1.0) 44 | AM_INIT_AUTOMAKE([1.10 no-define foreign dist-xz no-dist-gzip]) 45 | AC_CONFIG_FILES([Makefile ${APP}.desktop]) 46 | AC_OUTPUT">configure.ac 47 | ./autogen.sh 48 | sudo make install 49 | sudo cp $ICON "$(dirname $(which $APP))" 50 | rm -rf /tmp/create-desktop-app -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // intercepts and shows possible webView errors 2 | addEventListener('error', function (e) { 3 | JSGTK.error(e.message); 4 | }, true); 5 | 6 | // generic handler triggered fromthe GTK world 7 | // the currentTarget is always the window 8 | var emoji = Object.create(null); 9 | function gtkHandler(e) { 10 | var detail = e.detail; 11 | switch (true) { 12 | case detail.hasOwnProperty('showNotification'): 13 | JSGTK.showNotification = detail.showNotification; 14 | break; 15 | case detail.hasOwnProperty('showEmoji'): 16 | var encode = []; 17 | twemoji.parse(document.body, { 18 | callback: function(icon) { 19 | if (!(icon in emoji) && encode.indexOf(icon) < 0) { 20 | encode.push(icon); 21 | } 22 | return false; 23 | } 24 | }); 25 | window.addEventListener(e.type, function wait(e) { 26 | if (e.detail.hasOwnProperty('b64')) { 27 | window.removeEventListener(e.type, wait); 28 | Object.keys(e.detail.b64).forEach(function (icon) { 29 | emoji[icon] = e.detail.b64[icon]; 30 | }); 31 | twemoji.parse(document.body, { 32 | onload: function () { 33 | this.style.cssText = 'height:1em;width: 1em;margin: 0 .05em 0 .1em;vertical-align: -0.1em;'; 34 | }, 35 | callback: function (icon) { 36 | return emoji[icon] || false; 37 | } 38 | }); 39 | } 40 | }); 41 | JSGTK.b64emoji(encode); 42 | break; 43 | } 44 | } 45 | 46 | // will enable them by default 47 | // and fix the deprecated API 48 | if (!Notification.requestPermission()) 49 | Object.defineProperty( 50 | window, 51 | 'Notification', 52 | { 53 | value: (function (_Notification) { 54 | function Notification(title, options) { 55 | switch (arguments.length) { 56 | case 0: return new _Notification(); 57 | case 1: return new _Notification(title); 58 | default: return new _Notification(title, options); 59 | } 60 | } 61 | Notification.permission = _Notification.permission; 62 | Notification.requestPermission = function requestPermission() { 63 | return new Promise(function (resolve, reject) { 64 | setTimeout(function () { 65 | resolve(_Notification.permission); 66 | }); 67 | }); 68 | }; 69 | Notification.prototype = _Notification.prototype; 70 | return Notification; 71 | }(Notification)) 72 | } 73 | ); 74 | 75 | // prevent remote webapp notifications 76 | // when it's actually me doing nasty things 77 | // to test this stuff 78 | if (JSGTK.debug) { 79 | Object.defineProperties( 80 | window, 81 | { 82 | addEventListener: { 83 | value: (function (_addEventListener) { 84 | return function addEventListener(type) { 85 | if (type != 'error') 86 | _addEventListener.apply(window, arguments); 87 | }; 88 | }(addEventListener)) 89 | }, 90 | onerror: { 91 | get: function () { return Object; }, 92 | set: function (niceTry) {} 93 | } 94 | } 95 | ); 96 | } 97 | 98 | document.addEventListener('click', function (e) { 99 | var target = e.target; 100 | if (target.nodeType === 1) { 101 | if (target.nodeName === 'IMG' && JSGTK.gallery) { 102 | target = e.target.closest('[role=article]'); 103 | if (target) { 104 | var images = Array.prototype.map.call( 105 | target.querySelectorAll('img'), 106 | function (img) { return img.src + ':orig'; } 107 | ).filter(function (src) { 108 | return -1 < src.indexOf('/media/'); 109 | }); 110 | if (images.length) { 111 | nuf(e); 112 | JSGTK.gallery(images); 113 | } 114 | } 115 | } else { 116 | target = e.target.closest('a[target=_blank]'); 117 | if (target) { 118 | nuf(e); 119 | // provided automagically 120 | // by the loader, injected upfront. 121 | // It uses a private UID as protocol channel 122 | JSGTK.open(target.href); 123 | } 124 | } 125 | } 126 | }, true); 127 | 128 | // only if the method is exposed 129 | if (JSGTK.notify) { 130 | JSGTK.ni = setInterval(function (data) { 131 | // and only if notifications are enabled 132 | if (!JSGTK.showNotification) return; 133 | var 134 | notifications = document.querySelector('[href="/notifications"] span'), 135 | messages = document.querySelector('[href="/messages"] span'), 136 | length = !!notifications + !!messages 137 | ; 138 | if (length > data.length || !length) { 139 | data.length = length; 140 | if (length) { 141 | JSGTK.notify( 142 | parseFloat(notifications ? notifications.textContent : 0), 143 | parseFloat(messages ? messages.textContent : 0) 144 | ); 145 | } 146 | } 147 | }, 2000, {length: 0}); 148 | } 149 | 150 | // little helper to stop everything 151 | function nuf(e) { 152 | e.preventDefault(); 153 | e.stopPropagation(); 154 | } 155 | 156 | /*! Copyright Twitter Inc. and other contributors. Licensed under MIT */ 157 | var twemoji=function(){"use strict";var twemoji={base:"https://twemoji.maxcdn.com/2/",ext:".png",size:"72x72",className:"emoji",convert:{fromCodePoint:fromCodePoint,toCodePoint:toCodePoint},onerror:function onerror(){if(this.parentNode){this.parentNode.replaceChild(createText(this.alt),this)}},parse:parse,replace:replace,test:test},escaper={"&":"&","<":"<",">":">","'":"'",'"':"""},re=/\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc41\u200d\ud83d\udde8|(?:[\u0023\u002a\u0030-\u0039])\ufe0f?\u20e3|(?:(?:[\u261d\u270c])(?:\ufe0f|(?!\ufe0e))|\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca\udfcb]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd75\udd90\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0]|\ud83e\udd18|[\u26f9\u270a\u270b\u270d])(?:\ud83c[\udffb-\udfff]|)|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf21\udf24-\udf84\udf86-\udf93\udf96\udf97\udf99-\udf9b\udf9e-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcc-\udff0\udff3-\udff5\udff7-\udfff]|\ud83d[\udc00-\udc41\udc44\udc45\udc51-\udc65\udc6a-\udc6d\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfd\udcff-\udd3d\udd49-\udd4e\udd50-\udd67\udd6f\udd70\udd73\udd74\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\udecb-\uded0\udee0-\udee5\udee9\udeeb\udeec\udef0\udef3]|\ud83e[\udd10-\udd17\udd80-\udd84\uddc0]|[\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u2602-\u2604\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638\u2692\u2694\u2696\u2697\u2699\u269b\u269c\u26b0\u26b1\u26c8\u26ce\u26cf\u26d1\u26d3\u26e9\u26f0\u26f1\u26f4\u26f7\u26f8\u2705\u271d\u2721\u2728\u274c\u274e\u2753-\u2755\u2763\u2795-\u2797\u27b0\u27bf\ue50a]|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37]|[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600\u2601\u260e\u2611\u2614\u2615\u2639\u263a\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2693\u26a0\u26a1\u26aa\u26ab\u26bd\u26be\u26c4\u26c5\u26d4\u26ea\u26f2\u26f3\u26f5\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u2733\u2734\u2744\u2747\u2757\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))/g,UFE0Fg=/\uFE0F/g,U200D=String.fromCharCode(8205),rescaper=/[&<>'"]/g,shouldntBeParsed=/IFRAME|NOFRAMES|NOSCRIPT|SCRIPT|SELECT|STYLE|TEXTAREA|[a-z]/,fromCharCode=String.fromCharCode;return twemoji;function createText(text){return document.createTextNode(text)}function escapeHTML(s){return s.replace(rescaper,replacer)}function defaultImageSrcGenerator(icon,options){return"".concat(options.base,options.size,"/",icon,options.ext)}function grabAllTextNodes(node,allText){var childNodes=node.childNodes,length=childNodes.length,subnode,nodeType;while(length--){subnode=childNodes[length];nodeType=subnode.nodeType;if(nodeType===3){allText.push(subnode)}else if(nodeType===1&&!shouldntBeParsed.test(subnode.nodeName)){grabAllTextNodes(subnode,allText)}}return allText}function grabTheRightIcon(rawText){return toCodePoint(rawText.indexOf(U200D)<0?rawText.replace(UFE0Fg,""):rawText)}function parseNode(node,options){var allText=grabAllTextNodes(node,[]),length=allText.length,attrib,attrname,modified,fragment,subnode,text,match,i,index,img,rawText,iconId,src;while(length--){modified=false;fragment=document.createDocumentFragment();subnode=allText[length];text=subnode.nodeValue;i=0;while(match=re.exec(text)){index=match.index;if(index!==i){fragment.appendChild(createText(text.slice(i,index)))}rawText=match[0];iconId=grabTheRightIcon(rawText);i=index+rawText.length;src=options.callback(iconId,options);if(src){img=new Image;img.onload=options.onload;img.onerror=options.onerror;img.setAttribute("draggable","false");attrib=options.attributes(rawText,iconId);for(attrname in attrib){if(attrib.hasOwnProperty(attrname)&&attrname.indexOf("on")!==0&&!img.hasAttribute(attrname)){img.setAttribute(attrname,attrib[attrname])}}img.className=options.className;img.alt=rawText;img.src=src;modified=true;fragment.appendChild(img)}if(!img)fragment.appendChild(createText(rawText));img=null}if(modified){if(i")}return ret})}function replacer(m){return escaper[m]}function returnNull(){return null}function toSizeSquaredAsset(value){return typeof value==="number"?value+"x"+value:value}function fromCodePoint(codepoint){var code=typeof codepoint==="string"?parseInt(codepoint,16):codepoint;if(code<65536){return fromCharCode(code)}code-=65536;return fromCharCode(55296+(code>>10),56320+(code&1023))}function parse(what,how){if(!how||typeof how==="function"){how={callback:how}}return(typeof what==="string"?parseString:parseNode)(what,{callback:how.callback||defaultImageSrcGenerator,attributes:typeof how.attributes==="function"?how.attributes:returnNull,base:typeof how.base==="string"?how.base:twemoji.base,ext:how.ext||twemoji.ext,size:how.folder||toSizeSquaredAsset(how.size||twemoji.size),className:how.className||twemoji.className,onload:how.onload||Object,onerror:how.onerror||twemoji.onerror})}function replace(text,callback){return String(text).replace(re,callback)}function test(text){re.lastIndex=0;var result=re.test(text);re.lastIndex=0;return result}function toCodePoint(unicodeSurrogates,sep){var r=[],c=0,p=0,i=0;while(i