Merge branch 'master' into gh-pages

gh-pages
Lorenz Hübschle-Schneider 7 years ago
commit e5eb728bcf
  1. 4
      .gitignore
  2. 3
      .travis.yml
  3. 8
      3rdparty/favico-0.3.10.min.js
  4. 7
      3rdparty/favico-0.3.5.min.js
  5. 49
      README.md
  6. BIN
      assets/img/badge_firefoxos.png
  7. BIN
      assets/img/glowing-bear.icns
  8. 32
      bower.json
  9. 100
      css/glowingbear.css
  10. 1452
      css/themes/base16-default.css
  11. 20
      css/themes/base16-light.css
  12. 20
      css/themes/base16-mocha.css
  13. 20
      css/themes/base16-ocean-dark.css
  14. 20
      css/themes/base16-solarized-dark.css
  15. 20
      css/themes/base16-solarized-light.css
  16. 188
      css/themes/blue.css
  17. 5
      css/themes/dark-spacious.css
  18. 23
      css/themes/dark.css
  19. 12
      css/themes/light.css
  20. 3
      directives/input.html
  21. 19
      electron-globals.js
  22. 258
      electron-main.js
  23. 41
      electron-start.html
  24. 27
      electron.makefile
  25. 272
      index.html
  26. 56
      js/bufferResume.js
  27. 151
      js/connection.js
  28. 34
      js/filters.js
  29. 347
      js/glowingbear.js
  30. 110
      js/handlers.js
  31. 2
      js/imgur.js
  32. 166
      js/inputbar.js
  33. 17
      js/irc-utils.js
  34. 45
      js/localstorage.js
  35. 121
      js/models.js
  36. 63
      js/notifications.js
  37. 134
      js/plugins.js
  38. 30
      js/utils.js
  39. 6
      manifest.json
  40. 12
      manifest.webapp
  41. 36
      package.json
  42. 2
      test/karma.conf.js
  43. 19
      test/unit/plugins.js
  44. 21
      webapp.manifest.json

4
.gitignore vendored

@ -2,3 +2,7 @@ bower_components/
node_modules/ node_modules/
min.js min.js
min.map min.map
# Electron stuff
fonts/
Glowing\ Bear-*/

@ -1,6 +1,7 @@
language: node_js language: node_js
node_js: node_js:
- "0.10" - "8"
dist: trusty
install: "npm install" install: "npm install"
script: "sh -e run_tests.sh" script: "sh -e run_tests.sh"
notifications: notifications:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,24 +1,30 @@
#A web client for WeeChat [![Build Status](https://api.travis-ci.org/glowing-bear/glowing-bear.png)](https://travis-ci.org/glowing-bear/glowing-bear?branch=master) # A web client for WeeChat [![Build Status](https://api.travis-ci.org/glowing-bear/glowing-bear.png)](https://travis-ci.org/glowing-bear/glowing-bear?branch=master)
Glowing Bear is a web frontend for the [WeeChat](http://weechat.org) IRC client and strives to be a modern interface. It relies on WeeChat to do all the heavy lifting and then provides some nice features on top of that, like embedding images, videos, and other content. The best part, however, is that you can use it from any modern internet device -- whether it's a computer, tablet, or smart phone -- and all your stuff is there, whereever you are. You don't have to deal with the messy technical details, and all you need to have installed is a browser or our app. Glowing Bear is a web frontend for the [WeeChat](https://weechat.org) IRC client and strives to be a modern interface. It relies on WeeChat to do all the heavy lifting and then provides some nice features on top of that, like embedding images, videos, and other content. The best part, however, is that you can use it from any modern internet device -- whether it's a computer, tablet, or smart phone -- and all your stuff is there, wherever you are. You don't have to deal with the messy technical details, and all you need to have installed is a browser or our app.
##Getting Started ## Getting Started
Glowing Bear connects to the WeeChat instance you're already running (version 0.4.2 or later is required), and you need to be able to establish a connection to the WeeChat host from your device. It makes use of the relay plugin, and therefore you need to set up a relay. If you want to get started as quickly as possible, use these commands in WeeChat: Glowing Bear connects to the WeeChat instance you're already running (version 0.4.2 or later is required), and you need to be able to establish a connection to the WeeChat host from your device. It makes use of the relay plugin, and therefore you need to set up a relay. If you want to try this out with a local WeeChat instance, use these commands in WeeChat to create an **unencrypted relay** (see the note below):
/relay add weechat 9001 /relay add weechat 9001
/set relay.network.password YOURPASSWORD /set relay.network.password YOURPASSWORD
Now point your browser to the [Glowing Bear](http://www.glowing-bear.org)! If you're having trouble connecting, check that the host and port of your WeeChat host are entered correctly, and that your server's firewall permits incoming connections on the relay port. Now point your browser to the [Glowing Bear](http://www.glowing-bear.org)! If you're having trouble connecting, check that the host and port of your WeeChat host are entered correctly, and that your server's firewall permits incoming connections on the relay port (9001 in this example).
Please note that the above instructions set up an *unencrypted* relay, and all your data will be transmitted in clear. Therefore, we strongly recommend that you set up encryption if you want to keep using Glowing Bear. We've written [a detailed guide on how to set up a trusted secure relay](https://4z2.de/2014/07/06/weechat-trusted-relay) for you. **Please note that the above instructions set up an unencrypted relay, and all your data will be transmitted in clear.** You should not use this over the internet. We strongly recommend that you set up encryption if you want to keep using Glowing Bear. There's a guide on setting it up with Let's Encrypt on the landing page of the [next version of Glowing Bear](https://latest.glowing-bear.org), under "Getting Started". Ask us in `#glowing-bear` on freenode if something is unclear.
You can run Glowing Bear in many ways: use it like any other webpage, as an app in Firefox (choose "Install app" on the landing page) or Chrome ("Tools", then "Create application shortcuts"), or a full-screen Chrome app on Android ("Add to homescreen"). We also provide an [Android app](https://play.google.com/store/apps/details?id=com.glowing_bear) that you can install from the Google Play Store, and a [Firefox OS app](https://marketplace.firefox.com/app/glowing-bear/) in the Firefox Marketplace. You can run Glowing Bear in many ways:
<a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="/assets/img/badge_playstore.png" /></a><a href="https://marketplace.firefox.com/app/glowing-bear/"><img alt="Firefox OS app in the Firefox Marketplace" src="/assets/img/badge_firefoxos.png" /></a> * like any other webpage
* Chrome app ("Tools", then "Create application shortcuts")
* Android Chrome app, a full-screen experience ("Add to homescreen").
* [Android app](https://play.google.com/store/apps/details?id=com.glowing_bear) that you can install from the Google Play Store
* Electron app for Windows, Linux and macOS. ```npm install; npm install electron-packager; npm run build-electron-{windows, darwin, linux}``` (choose your platform from the list, e.g. `build-electron-darwin` for macOS)
##Screenshots <a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="/assets/img/badge_playstore.png" /></a>
## Screenshots
Running as Chrome application in a separate window on Windows and as Android app: Running as Chrome application in a separate window on Windows and as Android app:
@ -27,20 +33,21 @@ Running as Chrome application in a separate window on Windows and as Android app
Are you good with design? We'd love your help! Are you good with design? We'd love your help!
![Glowing Bear screenshot with lots of Comic Sans MS](https://4z2.de/glowing-bear3.png) ![Glowing Bear screenshot with lots of Comic Sans MS](https://4z2.de/glowing-bear3.png)
##How it Works ## How it Works
What follows is a more technical explanation of how Glowing Bear works, and you don't need to understand it to use it. What follows is a more technical explanation of how Glowing Bear works, and you don't need to understand it to use it.
Glowing Bear uses WeeChat directly as its backend through the relay plugin. This means that we can connect to WeeChat directly from the browser using WebSockets. Therefore, the client does not need a special "backend service", and you don't have to install anything. A connection is made from your browser to your WeeChat, with no services in between. Thus, Glowing Bear is written purely in client-side JavaScript with a bit of HTML and CSS. Glowing Bear uses WeeChat directly as its backend through the relay plugin. This means that we can connect to WeeChat directly from the browser using WebSockets. Therefore, the client does not need a special "backend service", and you don't have to install anything. A connection is made from your browser to your WeeChat, with no services in between. Thus, Glowing Bear is written purely in client-side JavaScript with a bit of HTML and CSS.
##FAQ ## FAQ
- *Can I use Glowing Bear to access a machine or port not exposed to the internet by passing the connection through my server?* No, that's not what Glowing Bear does. You can use a websocket proxy module for your webserver to forward `/weechat` to your WeeChat instance though. Here are some pointers you might find helpful for setting this up with [nginx](http://nginx.com/blog/websocket-nginx/) or [apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html). - *Can I use Glowing Bear to access a machine or port not exposed to the internet by passing the connection through my server?* No, that's not what Glowing Bear does. You can use a websocket proxy module for your webserver to forward `/weechat` to your WeeChat instance though. We've got instructions for setting this up [on our wiki](https://github.com/glowing-bear/glowing-bear/wiki/Proxying-WeeChat-relay-with-a-web-server).
- *How does the encryption work?* TLS is used for securing the connection if you enable encryption. This is handled by your browser, and we have no influence on certificate handling, etc. You can find more detailed instructions on how to communicate securely in the "encryption instructions" tab on the [landing page](http://www.glowing-bear.org). A detailed guide on setting up a trusted secure relay is available [here](https://4z2.de/2014/07/06/weechat-trusted-relay). - *How does the encryption work?* TLS is used for securing the connection if you enable encryption. This is handled by your browser, and we have no influence on certificate handling, etc. You can find more detailed instructions on how to communicate securely in the "Getting Started" tab on the [landing page of our development version](https://latest.glowing-bear.org).
- *Can I make it so that there are no requests to third party servers at all?* Sure, you'll have to hide embeds by default (it's in the settings dialog), and download the JavaScript files for which we use a CDN by default. For the second step, you have two options: a) use the Android or Electron app, or b) run `npm run make-local` to download the files and apply a patch to use them instead of the CDN. But remember to re-run this command whenever you update Glowing Bear!
##Development ## Development
###Setup ### Setup
Getting started with the development of Glowing Bear is really simple, partly because we don't have a build process (pure client-side JS, remember). All you have to do is clone the repository, fire up a webserver to host the files, and start fiddling around. You can try out your changes by reloading the page. Getting started with the development of Glowing Bear is really simple, partly because we don't have a build process (pure client-side JS, remember). All you have to do is clone the repository, fire up a webserver to host the files, and start fiddling around. You can try out your changes by reloading the page.
Here's a simple example using the python simple web server: Here's a simple example using the python simple web server:
@ -55,13 +62,11 @@ python -m http.server
Now you can point your browser to [http://localhost:8000](http://localhost:8000)! Now you can point your browser to [http://localhost:8000](http://localhost:8000)!
Remember that **you don't need to host Glowing Bear yourself to use it**, you can just use [our hosted version](http://www.glowing-bear.org) powered by GitHub pages, and we'll take care of updates for you. Your browser connects to WeeChat directly, so it does not matter where Glowing Bear is hosted. Remember that **you don't need to host Glowing Bear yourself to use it**, you can just use [our hosted version](https://www.glowing-bear.org) powered by GitHub pages, and we'll take care of updates for you. Your browser connects to WeeChat directly, so it does not matter where Glowing Bear is hosted.
If you'd prefer a version hosted with HTTPS, GitHub serves that as well with an undocumented, not officially supported (by GitHub) link. Be careful though, it might break any minute. Anyway, here's the link: [secret GitHub HTTPS link](https://glowing-bear.github.io/glowing-bear/).
You can also use the latest and greatest development version of Glowing Bear at [https://latest.glowing-bear.org/](https://latest.glowing-bear.org/). You can also use the latest and greatest development version of Glowing Bear at [https://latest.glowing-bear.org/](https://latest.glowing-bear.org/). Branches of this repository are available as [https://latest.glowing-bear.org/**branchname**/](https://latest.glowing-bear.org/branchname/), and pull requests as [https://latest.glowing-bear.org/pull/**123**/](https://latest.glowing-bear.org/pull/123/)—note the trailing slashes.
###Running the tests ### Running the tests
Glowing Bear uses Karma and Jasmine to run its unit tests. To run the tests locally, you will first need to install `npm` on your machine. Check out the wonderful [nvm](https://github.com/creationix/nvm) if you don't know it already, it's highly recommended. Glowing Bear uses Karma and Jasmine to run its unit tests. To run the tests locally, you will first need to install `npm` on your machine. Check out the wonderful [nvm](https://github.com/creationix/nvm) if you don't know it already, it's highly recommended.
Once this is done, you will need to retrieve the necessary packages for testing Glowing-Bear (first, you might want to use `npm link` on any packages you have already installed globally): Once this is done, you will need to retrieve the necessary packages for testing Glowing-Bear (first, you might want to use `npm link` on any packages you have already installed globally):
@ -77,7 +82,7 @@ Or the end to end tests:
**Note**: the end to end tests assume that a web server is hosting Glowing Bear on `localhost:8000` and that a WeeChat relay is configured on port 9001. **Note**: the end to end tests assume that a web server is hosting Glowing Bear on `localhost:8000` and that a WeeChat relay is configured on port 9001.
##Contributing ## Contributing
Whether you are interested in contributing or simply want to talk about the project, join us at **#glowing-bear** on **freenode**! Whether you are interested in contributing or simply want to talk about the project, join us at **#glowing-bear** on **freenode**!
@ -89,4 +94,4 @@ If you wish to submit code, we try to make the contribution process as simple as
We'd also like to ask you to join our IRC channel, #glowing-bear on freenode, so we can discuss your ideas and changes. We'd also like to ask you to join our IRC channel, #glowing-bear on freenode, so we can discuss your ideas and changes.
If you're curious about the projects we're using, here's a list: [AngularJS](https://angularjs.org/), [Bootstrap](http://getbootstrap.com/), [Underscore](http://underscorejs.org/), [favico.js](http://lab.ejci.net/favico.js/), Emoji provided free by [Emoji One](http://emojione.com/), and [zlib.js](https://github.com/imaya/zlib.js). Technology-wise, [WebSockets](http://en.wikipedia.org/wiki/WebSocket) are the most important part, but we also use [local storage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage), the [Notification Web API](https://developer.mozilla.org/en/docs/Web/API/notification), and last (but not least) [Apache Cordova](https://cordova.apache.org/) for our mobile app. If you're curious about the projects we're using, here's a list: [AngularJS](https://angularjs.org/), [Bootstrap](http://getbootstrap.com/), [Underscore](http://underscorejs.org/), [favico.js](http://lab.ejci.net/favico.js/), Emoji provided free by [Emoji One](http://emojione.com/), and [zlib.js](https://github.com/imaya/zlib.js). Technology-wise, [WebSockets](https://en.wikipedia.org/wiki/WebSocket) are the most important part, but we also use [local storage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage), the [Notification Web API](https://developer.mozilla.org/en/docs/Web/API/notification), and last (but not least) [Apache Cordova](https://cordova.apache.org/) for our mobile app.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

@ -1,17 +1,31 @@
{ {
"name": "glowing-bear", "name": "glowing-bear",
"description": "A webclient for WeeChat", "description": "A webclient for WeeChat",
"version": "0.6.0", "version": "0.7.0",
"homepage": "https://github.com/glowing-bear/glowing-bear", "homepage": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3", "license": "GPLv3",
"private": true, "private": true,
"dependencies": { "dependencies": {
"angular": "1.4.x", "angular": "1.7.x",
"angular-route": "1.4.x", "angular-route": "1.7.x",
"angular-sanitize": "1.4.x", "angular-sanitize": "1.7.x",
"angular-touch": "1.4.x", "angular-touch": "1.7.x",
"angular-loader": "1.4.x", "angular-loader": "1.7.x",
"angular-mocks": "1.4.x", "angular-mocks": "1.7.x",
"html5-boilerplate": "~4.3.0" "underscore": "~1.9"
} },
"devDependencies": {
"bootstrap": "~3.3",
"html5-boilerplate": "~4.3.0",
"emojione": "~2.2"
},
"keywords": [
"weechat",
"irc"
],
"ignore": [
"**/.*",
"node_modules",
"bower_components"
]
} }

@ -73,10 +73,29 @@ td.message {
border-bottom: 2px solid #555; border-bottom: 2px solid #555;
} }
.input-group-addon, .input-group-btn { .input-group-btn {
vertical-align: top; vertical-align: top;
} }
.input-group-addon {
background: none;
border: none;
color: #ccc;
}
#jump-addon {
width: 0px;
max-width: 0px;
padding: 0;
overflow: hidden;
transition: all ease-in-out 0.5s;
}
.showjumpkeys #jump-addon {
width: auto;
max-width: 70px;
padding: 6px 12px;
}
.footer button { .footer button {
border-radius: 0; border-radius: 0;
} }
@ -89,6 +108,12 @@ input[type=text], input[type=password], #sendMessage {
margin-bottom: 5px !important; margin-bottom: 5px !important;
} }
.btn-complete-nick {
position: relative;
overflow: hidden;
cursor: pointer;
}
.btn-send-image { .btn-send-image {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -155,6 +180,7 @@ input[type=text], input[type=password], #sendMessage {
position: fixed; position: fixed;
left: 145px; /* sidebar */ left: 145px; /* sidebar */
overflow: hidden; overflow: hidden;
width: 100%; /* for title modal click area */
} }
#topbar .actions { #topbar .actions {
@ -197,7 +223,9 @@ input[type=text], input[type=password], #sendMessage {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding-top: 35px; /* topbar */ padding-top: 35px; /* topbar */
padding-right: env(safe-area-inset-right);
padding-bottom: 1px; /* need to force a padding here */ padding-bottom: 1px; /* need to force a padding here */
padding-left: env(safe-area-inset-left);
font-size: smaller; font-size: smaller;
transition:0.2s ease-in-out; transition:0.2s ease-in-out;
z-index: 2; z-index: 2;
@ -216,7 +244,13 @@ input[type=text], input[type=password], #sendMessage {
display:block!important; display:block!important;
} }
#sidebar .badge {
#sidebar .buffer .badge {
display: none;
}
#sidebar .buffer.unread .badge, #sidebar .buffer.notification .badge {
display: inline-block;
border-radius: 0; border-radius: 0;
margin-right: -10px; margin-right: -10px;
padding: 4px 7px; padding: 4px 7px;
@ -239,9 +273,9 @@ input[type=text], input[type=password], #sendMessage {
overflow-x: hidden; overflow-x: hidden;
right: 0; right: 0;
top: 0; top: 0;
padding-top: 39px; margin-top: 39px;
padding-left: 5px; padding-left: 5px;
padding-bottom: 35px; padding-bottom: 44px;
z-index: 2; z-index: 2;
} }
#nicklist ul { #nicklist ul {
@ -285,6 +319,12 @@ input[type=text], input[type=password], #sendMessage {
color: #222; color: #222;
} }
.nav-pills > li.highlight > a, .nav-pills > li.highlight > a span {
text-decoration: none;
color: #fff;
background: #428BCA;
}
.nav-pills > li > a { .nav-pills > li > a {
display: block; display: block;
overflow: hidden; overflow: hidden;
@ -295,6 +335,7 @@ input[type=text], input[type=password], #sendMessage {
.content { .content {
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
} }
#bufferlines { #bufferlines {
@ -350,6 +391,7 @@ td.time {
bottom: 0; bottom: 0;
height: 35px; height: 35px;
width: 100%; width: 100%;
margin-bottom: env(safe-area-inset-bottom); /* margin for home-indicator on iPhone X */
-webkit-transition:0.2s ease-in-out all; -webkit-transition:0.2s ease-in-out all;
transition:0.2s ease-in-out all; transition:0.2s ease-in-out all;
z-index: 1; z-index: 1;
@ -424,14 +466,6 @@ div.colourbox {
} }
table.notimestamp td.time {
display: none !important;
}
table.notimestampseconds td.time span.seconds {
display: none !important;
}
#sidebar .showquickkeys .buffer .buffer-quick-key { #sidebar .showquickkeys .buffer .buffer-quick-key {
transition: all ease-in-out 0.5s; transition: all ease-in-out 0.5s;
-webkit-transition: all ease-in-out 0.5s; -webkit-transition: all ease-in-out 0.5s;
@ -456,6 +490,33 @@ table.notimestampseconds td.time span.seconds {
align: right; align: right;
} }
#sidebar .showjumpkeys .buffer .buffer-jump-key {
transition: all ease-in-out 0.2s;
-webkit-transition: all ease-in-out 0.2s;
transition-delay: 0s;
-webkit-transition-delay: 0s;
opacity: 0.7;
margin-left: -1.2em;
margin-right: 0.1em;
}
#sidebar .buffer .buffer-jump-key {
margin-left: -1.2em;
margin-right: -0.2em;
font-size: smaller;
transition: all ease-in-out 0.2s;
-webkit-transition: all ease-in-out 0.2s;
opacity: 0;
text-shadow: -1px 0px 4px rgba(255, 255, 255, 0.4),
0px -1px 4px rgba(255, 255, 255, 0.4),
1px 0px 4px rgba(255, 255, 255, 0.4),
0px 1px 4px rgba(255, 255, 255, 0.4);
vertical-align: baseline;
display: inline-block;
width: 1em;
align: right;
}
.gb-modal { .gb-modal {
z-index: 1000; z-index: 1000;
height: 100%; height: 100%;
@ -749,6 +810,7 @@ img.emojione {
right: 60px; right: 60px;
text-align: center; text-align: center;
font-size: 18px; font-size: 18px;
width: initial;
} }
#topbar .brand img { #topbar .brand img {
@ -777,6 +839,10 @@ img.emojione {
bottom: 0px; bottom: 0px;
} }
.content[sidebar-state=visible] #nicklist {
display: none;
}
.navbar-fixed-bottom { .navbar-fixed-bottom {
margin: 0; margin: 0;
} }
@ -789,6 +855,11 @@ img.emojione {
font-size: 14px; font-size: 14px;
} }
.nav-pills li.buffer {
min-height: 30px;
max-height: 30px;
}
.nav-pills li a { .nav-pills li a {
padding: 10px 15px; padding: 10px 15px;
} }
@ -850,3 +921,8 @@ img.emojione {
padding-right: 0px !important; padding-right: 0px !important;
} }
} }
/* ng-cloak hides elements until Angular loads, preventing flickering */
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,20 @@
@import "base16-default.css";
:root {
--base00: #f8f8f8; /* Default Background */
--base01: #e8e8e8; /* Lighter Background (Used for status bars) */
--base02: #d8d8d8; /* Selection Background */
--base03: #b8b8b8; /* Comments, Invisibles, Line Highlighting */
--base04: #585858; /* Dark Foreground (Used for status bars) */
--base05: #383838; /* Default Foreground, Caret, Delimiters, Operators */
--base06: #282828; /* Light Foreground (Not often used) */
--base07: #181818; /* Light Background (Not often used) */
--base08: #bd5a56; /* "Red": Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted */
--base09: #a96423; /* "Orange": Integers, Boolean, Constants, XML Attributes, Markup Link Url */
--base0A: #774b08; /* "Yellow": Classes, Markup Bold, Search Text Background */
--base0B: #7f9249; /* "Green": Strings, Inherited Class, Markup Code, Diff Inserted */
--base0C: #3e7971; /* "Cyan": Support, Regular Expressions, Escape Characters, Markup Quotes */
--base0D: #3e7184; /* "Blue": Functions, Methods, Attribute IDs, Headings */
--base0E: #734568; /* "Magenta": Keywords, Storage, Selector, Markup Italic, Diff Changed */
--base0F: #b9825f; /* "Brown": Deprecated, Opening/Closing Embedded Language Tags e.g. <?php ?> */
}

@ -0,0 +1,20 @@
@import "base16-default.css";
:root {
--base00: #3B3228;
--base01: #534636;
--base02: #645240;
--base03: #7e705a;
--base04: #b8afad;
--base05: #d0c8c6;
--base06: #e9e1dd;
--base07: #f5eeeb;
--base08: #cb6077;
--base09: #d28b71;
--base0A: #f4bc87;
--base0B: #beb55b;
--base0C: #7bbda4;
--base0D: #8ab3b5;
--base0E: #a89bb9;
--base0F: #bb9584;
}

@ -0,0 +1,20 @@
@import "base16-default.css";
:root {
--base00: #2b303b;
--base01: #343d46;
--base02: #4f5b66;
--base03: #65737e;
--base04: #a7adba;
--base05: #c0c5ce;
--base06: #dfe1e8;
--base07: #eff1f5;
--base08: #bf616a;
--base09: #d08770;
--base0A: #ebcb8b;
--base0B: #a3be8c;
--base0C: #96b5b4;
--base0D: #8fa1b3;
--base0E: #b48ead;
--base0F: #ab7967;
}

@ -0,0 +1,20 @@
@import "base16-default.css";
:root {
--base00: #002b36;
--base01: #002b36;
--base02: #073642;
--base03: #073642;
--base04: #586e75;
--base05: #839496;
--base06: #93a1a1;
--base07: #93a1a1;
--base08: #dc322f;
--base09: #cb4b16;
--base0A: #b58900;
--base0B: #859900;
--base0C: #2aa198;
--base0D: #268bd2;
--base0E: #6c71c4;
--base0F: #d33682;
}

@ -0,0 +1,20 @@
@import "base16-default.css";
:root {
--base00: #fdf6e3;
--base01: #fdf6e3;
--base02: #eee8d5;
--base03: #eee8d5;
--base04: #93a1a1;
--base05: #657b83;
--base06: #586e75;
--base07: #586e75;
--base08: #dc322f;
--base09: #cb4b16;
--base0A: #b58900;
--base0B: #859900;
--base0C: #2aa198;
--base0D: #268bd2;
--base0E: #6c71c4;
--base0F: #d33682;
}

@ -0,0 +1,188 @@
@import "dark.css";
body {
background-color: #1d222c;
color: #dfdfcf;
}
a {
color: #0787c1;
}
.btn-primary {
background: linear-gradient(#0f99d9,#0177af);
border-color: #0d7eb2;
}
.badge {
border-radius: 0;
}
.footer {
bottom: 5px;
}
#bufferlines {
bottom: 45px;
}
.form-control {
color: #ccc;
background: #141922;
border: 2px solid #4a5b6c;
}
.form-control option {
color: #eee;
background: #282828;
}
.nav-pills > li > a {
/* To prevent resize when they get border on active or hover */
border: 1px solid transparent;
margin-left: 5px;
margin-right: 5px;
}
.nav-pills > li.active > a {
background-color: rgba(27, 97, 161, 0.4);
border: 1px solid #1b61a1;
}
/* fix for mobile firefox which ignores :hover */
.nav-pills > li > a:active, .nav-pills > li > a:active span, .nav-pills > li.active > a:hover {
background-color: #031633;
color: #fff;
}
.nav-pills > li > a:hover, .nav-pills > li > a:hover span {
color: #fff;
}
.nav-pills > li > a:hover, .nav-pills > li > a:focus {
background-color: #282828;
color: white;
border: 1px solid #363943;
}
.nav-pills > li.highlight > a, .nav-pills > li.highlight > a span {
color: #fff;
background: #283244;
}
.bufferfilter {
margin-left: 5px;
margin-right: 5px;
}
tr.bufferline:hover {
background-color: #1b2737;
}
td.prefix {
border-right: 1px solid #464f5e;
}
td.message {
border-left: 1px solid #1b2737;
}
input[type=text], input[type=password], #sendMessage, .badge {
/*box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset; */
border: 1px solid #4a5b6c;
}
input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-send-image, .btn-complete-nick {
color: #ccc;
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3);
}
.btn-complete-nick:hover, .btn-complete-nick:focus,
.btn-send:hover, .btn-send:focus,
.btn-send-image:hover, .btn-send-image:focus {
background-color: #555;
color: white;
}
.nav-pills li:nth-child(2n) {
background: #283244;
}
.nav-pills > li > a {
color: #ddd;
}
#topbar .actions {
background: #283244;
color: #666;
}
#topbar, #sidebar, .panel, .dropdown-menu, .modal-content {
background: #283244;
}
#topbar, #topbar .actions, #sidebar, .panel, .dropdown-menu, .modal-content {
box-shadow: 0 1px #1d222c;
border-bottom: 1px solid #3a4657;
}
#sidebar {
padding-top: 40px;
border-right: 1px solid #1d1f29;
border-bottom: 0;
box-shadow: none;
}
#nicklist a {
border: 1px solid transparent;
}
#nicklist a:hover {
background: #282828;
border: 1px solid #363943;
}
.horizontal-line {
-webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0;
-moz-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0;
box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0;
border-bottom: 1px solid #1b2737;
}
.vertical-line {
-webkit-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0;
-moz-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0;
box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0;
border-right: 1px solid #1b2737;
}
.vertical-line-left {
-webkit-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0;
-moz-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0;
box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0;
border-left: 1px solid #1b2737;
}
.panel-group .panel-heading + .panel-collapse .panel-body, .modal-body, .modal-header, .modal-footer {
-webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0;
-moz-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0;
box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0;
border-top: 1px solid #1b2737;
}
#readmarker {
border-color: rgba(29, 94, 152, 0.5);
height: 0;
}
#sendMessage:focus, #sendMessage:active {
border: 1px solid;
border-color: #0d7eb2;
}
@media (min-width: 1400px) {
.content[sidebar-state="visible"] .footer {
padding-left: 205px;
}
}
@media (max-width: 968px) {
.footer {
padding-left: 5px !important;
}
.btn-complete-nick:hover,
.btn-send:hover,
.btn-send-image:hover {
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3);
color: #ccc;
}
}

@ -0,0 +1,5 @@
@import "dark.css";
tr.bufferline {
line-height: 1.4;
}

@ -35,6 +35,11 @@ html {
color: #222; color: #222;
} }
.nav-pills > li.highlight > a, .nav-pills > li.highlight > a span {
color: #fff;
background: #428BCA;
}
tr.bufferline:hover { tr.bufferline:hover {
background-color: #222222; background-color: #222222;
} }
@ -77,11 +82,12 @@ input[type=text], input[type=password], #sendMessage, .badge {
box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset; box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset;
} }
input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-send-image { input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-send-image, .btn-complete-nick {
color: #ccc; color: #ccc;
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3);
} }
.btn-complete-nick:hover, .btn-complete-nick:focus,
.btn-send:hover, .btn-send:focus, .btn-send:hover, .btn-send:focus,
.btn-send-image:hover, .btn-send-image:focus { .btn-send-image:hover, .btn-send-image:focus {
background-color: #555; background-color: #555;
@ -155,6 +161,15 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-se
border-bottom-color: #121212; border-bottom-color: #121212;
} }
button.close {
color: #ddd;
opacity: 1;
}
button.close:hover {
color: #ddd;
}
/****************************/ /****************************/
/* Weechat colors and style */ /* Weechat colors and style */
/****************************/ /****************************/
@ -2119,6 +2134,12 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-se
background: rgb(24,24,24); background: rgb(24,24,24);
} }
.btn-complete-nick:hover,
.btn-send:hover,
.btn-send-image:hover {
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3);
color: #ccc;
}
} }

@ -22,6 +22,12 @@ html {
background-color: #222; background-color: #222;
} }
.nav-pills > li.highlight > a, .nav-pills > li.highlight > a span {
color: #fff;
background: #428BCA;
}
.btn-complete-nick,
.btn-send, .btn-send,
.btn-send-image, { .btn-send-image, {
background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.3); background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.3);
@ -2099,4 +2105,10 @@ select.form-control, select option, input[type=text], input[type=password], #sen
background: rgb(230,230,230); background: rgb(230,230,230);
} }
.btn-complete-nick:hover,
.btn-send:hover,
.btn-send-image:hover {
color: #428BCA;
}
} }

@ -1,8 +1,9 @@
<form class="form form-horizontal" id="inputform" ng-submit="sendMessage()" imgur-drop> <form class="form form-horizontal" id="inputform" ng-submit="sendMessage()" imgur-drop>
<div class="input-group"> <div class="input-group">
<textarea id="{{inputId}}" class="form-control favorite-font" ng-trim="false" rows="1" ng-change="inputChanged()" autocomplete="on" ng-model="command" ng-focus="hideSidebar()"> <textarea id="{{inputId}}" class="form-control favorite-font" ng-trim="false" rows="1" ng-change="inputChanged()" autocomplete="on" ng-model="command" ng-focus="hideSidebar()" ng-paste="inputPasted($event)">
</textarea> </textarea>
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-complete-nick unselectable mobile" title="Complete nick" ng-click="handleCompleteNickButton($event)"><i class="glyphicon glyphicon-bullhorn"></i></button>
<label class="btn btn-send-image unselectable" for="imgur-upload" title="Send image"> <label class="btn btn-send-image unselectable" for="imgur-upload" title="Send image">
<i class="glyphicon glyphicon-picture"></i> <i class="glyphicon glyphicon-picture"></i>
<input type="file" accept="image/*" multiple title="Send image" id="imgur-upload" class="imgur-upload" file-change="uploadImage($event, files)"> <input type="file" accept="image/*" multiple title="Send image" id="imgur-upload" class="imgur-upload" file-change="uploadImage($event, files)">

@ -0,0 +1,19 @@
/**
* Global functions for electron app
*/
var ipc = require('electron').ipcRenderer;
// Set app bagde
var setElectronBadge = function(value) {
// Check ipc
if (ipc && typeof ipc.send === 'function') {
// Send new badge value
ipc.send('badge', value);
}
};
// Export global variables and functions
global.setElectronBadge = setElectronBadge;
// Let Glowing Bear know it's running as an electron app
window.is_electron = 1;

@ -0,0 +1,258 @@
(function() {
'use strict';
const electron = require('electron');
const app = electron.app; // Module to control application life.
const BrowserWindow = electron.BrowserWindow; // Module to create native browser window.
const ipcMain = require('electron').ipcMain;
const nativeImage = require('electron').nativeImage;
const Menu = require('electron').Menu;
// Node fs module
const fs = require("fs");
var template;
template = [
{
label: 'Edit',
submenu: [
{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
role: 'undo'
},
{
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
role: 'redo'
},
{
type: 'separator'
},
{
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
role: 'cut'
},
{
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
role: 'copy'
},
{
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
role: 'paste'
},
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall'
},
]
},
{
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click: function(item, focusedWindow) {
if (focusedWindow)
focusedWindow.reload();
}
},
{
label: 'Toggle Full Screen',
accelerator: (function() {
if (process.platform == 'darwin')
return 'Ctrl+Command+F';
else
return 'F11';
})(),
click: function(item, focusedWindow) {
if (focusedWindow)
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
},
{
label: 'Electron Developer Tools',
accelerator: (function() {
if (process.platform == 'darwin')
return 'Alt+Command+E';
else
return 'Ctrl+Shift+E';
})(),
click: function(item, focusedWindow) {
if (focusedWindow)
focusedWindow.toggleDevTools();
}
},
{
label: 'Web Developer Tools',
accelerator: (function() {
if (process.platform == 'darwin')
return 'Alt+Command+I';
else
return 'Ctrl+Shift+I';
})(),
click: function(item, focusedWindow) {
if ( focusedWindow ) {
focusedWindow.webContents.send( 'openDevTools' );
}
}
}
]
},
{
label: 'Window',
role: 'window',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
},
{
label: 'Close',
accelerator: 'CmdOrCtrl+Q',
role: 'close'
},
]
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Learn More',
click: function() { require('electron').shell.openExternal('https://github.com/glowing-bear/glowing-bear'); }
},
]
},
];
if (process.platform == 'darwin') {
var name = app.getName();
template.unshift({
label: name,
submenu: [
{
label: 'About ' + name,
role: 'about'
},
{
type: 'separator'
},
{
label: 'Services',
role: 'services',
submenu: []
},
{
type: 'separator'
},
{
label: 'Hide ' + name,
accelerator: 'Command+H',
role: 'hide'
},
{
label: 'Hide Others',
accelerator: 'Command+Alt+H',
role: 'hideothers'
},
{
label: 'Show All',
role: 'unhide'
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: function() { app.quit(); }
},
]
});
// Window menu.
template[3].submenu.push(
{
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
);
}
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
var mainWindow = null;
app.on('browser-window-focus', function(e, w) {
w.webContents.send('browser-window-focus');
});
app.on('ready', function() {
var menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
const initPath = __dirname + "/init.json";
var data;
// read saved state from file (e.g. window bounds)
try {
data = JSON.parse(fs.readFileSync(initPath, 'utf8'));
}
catch(e) {
console.log('Unable to read init.json: ', e);
}
const bounds = (data && data.bounds) ? data.bounds : {width: 1280, height:800 };
var bwdata = {width: bounds.width, height: bounds.height, 'min-width': 1024, 'min-height': 600, 'autoHideMenuBar': true, 'web-security': true, 'java': false, 'accept-first-mouse': true, defaultEncoding: 'UTF-8', 'icon':'file://'+__dirname + '/assets/img/favicon.png'};
// Remembe window position
if (data && data.bounds.x && data.bounds.y) {
bwdata.x = data.bounds.x;
bwdata.y = data.bounds.y;
}
mainWindow = new BrowserWindow(bwdata);
mainWindow.loadURL('file://' + __dirname + '/electron-start.html');
mainWindow.focus();
// Listen for badge changes
ipcMain.on('badge', function(event, arg) {
if (process.platform === "darwin") {
app.dock.setBadge(String(arg));
}
else if (process.platform === "win32") {
let n = parseInt(arg, 10);
// Only show notifications with number
if (isNaN(n)) {
return;
}
if (n > 0) {
mainWindow.setOverlayIcon(__dirname + '/assets/img/favicon.ico', String(arg));
} else {
mainWindow.setOverlayIcon(null, '');
}
}
});
mainWindow.on('devtools-opened', function() {
mainWindow.webContents.executeJavaScript("document.getElementById('glowingbear').openDevTools();");
});
mainWindow.on('close', function() {
// Save window bounds to disk
var data = {
bounds: mainWindow.getBounds()
};
fs.writeFileSync(initPath, JSON.stringify(data));
});
mainWindow.on('closed', function() {
app.quit();
});
});
})();

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
onload = function() {
const ipc= require('electron').ipcRenderer;
const remote = require('electron').remote;
const nativeImage = require('electron').nativeImage;
const shell = require('electron').shell;
var webview = document.getElementById("glowingbear");
var handleconsole = function(e) {
console.log("webview: " + e.message);
}
var handlenewwindow = function(e) {
shell.openExternal(e.url);
}
var handletitleset = function(e) {
document.title = e.title;
}
webview.addEventListener("console-message", handleconsole);
webview.addEventListener("new-window", handlenewwindow);
webview.addEventListener("page-title-set", handletitleset);
ipc.on('openDevTools', function() {
setTimeout(function() { webview.openDevTools(); }, 0 );
});
ipc.on('browser-window-focus', function() {
setTimeout(function() { webview.focus(); }, 0);
setTimeout(function() { webview.executeJavaScript("document.getElementById(\"sendMessage\").focus();") }, 0);
});
}
</script>
</head>
<body>
<webview preload="electron-globals.js" id="glowingbear" src="index.html" style="position:fixed; top:0; left:0; bottom:0; right:0;"></webview>
</body>
</html>

@ -0,0 +1,27 @@
# Common flags for electron-packager on all platforms
ELECTRON_COMMON=. "Glowing Bear" --overwrite --version-string.FileDescription="Glowing Bear" --ignore=node_modules --ignore=test --ignore=bower_components
# fetch dependencies for local installation
bower:
bower install
# copy dependencies from bower_components to the correct place
copylocal:
find bower_components \( -name "*min.js" -o -name "*min.css" \) -exec cp {} 3rdparty \;
cp -r bower_components/bootstrap/fonts .
cp bower_components/emojione/assets/sprites/emojione.sprites.svg 3rdparty
# modify index.html to use local files
uselocal: copylocal
sed -i.bak 's,https://cdnjs.cloudflare.com/ajax/libs/[^\"]*/,3rdparty/,g' index.html
sed -i.bak 's, integrity=\".*\" crossorigin=\"anonymous\",,' index.html
# build the electron app for various platforms
build-electron-windows: uselocal
electron-packager ${ELECTRON_COMMON} --platform=win32 --arch=ia32 --electron-version=3.0.6 --icon=assets/img/favicon.ico --asar=true
build-electron-darwin: uselocal
electron-packager ${ELECTRON_COMMON} --platform=darwin --arch=x64 --electron-version=3.0.6 --icon=assets/img/glowing-bear.icns
build-electron-linux: uselocal
electron-packager ${ELECTRON_COMMON} --platform=linux --arch=x64 --electron-version=3.0.6 --icon=assets/img/favicon.ico

@ -1,9 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html ng-app="weechat" ng-cloak> <html ng-app="weechat">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge"> <meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
@ -13,24 +13,24 @@
<!-- https://w3c.github.io/manifest/ && https://developer.mozilla.org/en-US/docs/Web/Manifest --> <!-- https://w3c.github.io/manifest/ && https://developer.mozilla.org/en-US/docs/Web/Manifest -->
<link rel="manifest" href="webapp.manifest.json"> <link rel="manifest" href="webapp.manifest.json">
<title ng-bind-template="{{ notificationStatus }}Glowing Bear {{ pageTitle}}"></title> <title ng-bind-template="{{ notificationStatus }}Glowing Bear {{ pageTitle}}"></title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen" integrity="sha384-7tY7Dc2Q8WQTKGz2Fa0vC4dWQo07N4mJjKvHfIGnxuC4vPqFGFQppd9b3NWpf18/" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=" crossorigin="anonymous" />
<link rel="shortcut icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png"> <link rel="shortcut icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
<link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png"> <link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
<link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" > <link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" >
<link href="css/glowingbear.css" rel="stylesheet" media="screen"> <link href="css/glowingbear.css" rel="stylesheet" media="screen">
<link href="css/themes/dark.css" rel="stylesheet" media="screen" id="themeCSS" /> <link href="css/themes/dark.css" rel="stylesheet" media="screen" id="themeCSS" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js" integrity="sha384-r1y8TJcloKTvouxnYsi4PJAx+nHNr90ibsEn3zznzDzWBN9X3o3kbHLSgcIPtzAp" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js" integrity="sha256-QRJz3b0/ZZC4ilKmBRRjY0MgnVhQ+RR1tpWLYaRRjSo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-route.min.js" integrity="sha384-fQQcs0/yvL0uyyzpXoTKfcQl5e9GYh7GKIft35qSjfKXSILYNI6YZOM0Ju94DY+/" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular-route.min.js" integrity="sha256-PQfkC+TI/HZv0O9JbmrLmPyhgOT2hry24vA5yAV59zY=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-sanitize.min.js" integrity="sha384-79uolbJAcWnfqb2Oi/w0fEz2NdE5lvY1p+TSew6D3XC7PlZY1OGuvGBiwjZhFvOg" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular-sanitize.min.js" integrity="sha256-LLlLr1XzKUXSFI9SiuEJOAn88Dwge+/zld523N2c8+8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular-touch.min.js" integrity="sha384-bnrVwYH8/uQCvK9n+xYQKdf1xtgSNHBYcy0djCofRUPvAt93iEhBfHlngRP/aXsg" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular-touch.min.js" integrity="sha256-4IS2pHNTST2Jl6dSzNsERpYleiQi1r4L2MLPElG8LZ4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js" integrity="sha384-nXjwhL1LfWUDVHxQ2R0rHpbr/E6lfCFXR4kfcPHp1eLGH1dH/mZohGINd44EzEya" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js" integrity="sha256-G7A4JrJjJlFqP0yamznwPjAApIKPkadeHfyIwiaa9e0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/emojione/2.1.0/lib/js/emojione.min.js" integrity="sha384-pJb7FFLYTcgO7KbgirAXNIHFIKzywqq4LIcWx9cavPapYWdCH5mcYptrkpHHEkH1" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.7/lib/js/emojione.min.js" integrity="sha256-9cBkVeU53NiJ9/BdcJta3HbERAmf5X9DE2WvL8V+gDs=" crossorigin="anonymous"></script>
<script type="text/javascript" src="3rdparty/inflate.min.js"></script> <script type="text/javascript" src="3rdparty/inflate.min.js"></script>
<script type="text/javascript" src="min.js"></script> <script type="text/javascript" src="min.js"></script>
<script type="text/javascript" src="3rdparty/favico-0.3.5.min.js"></script> <script type="text/javascript" src="3rdparty/favico-0.3.10.min.js"></script>
</head> </head>
<body ng-controller="WeechatCtrl" ng-keydown="handleKeyPress($event)" ng-keyup="handleKeyRelease($event)" ng-keypress="handleKeyPress($event)" ng-class="{'no-overflow': connected}" ng-init="init()" lang="en-US"> <body ng-controller="WeechatCtrl" ng-keydown="handleKeyPress($event)" ng-keyup="handleKeyRelease($event)" ng-keypress="handleKeyPress($event)" ng-class="{'no-overflow': connected}" ng-init="init()" lang="en-US">
<div class="alert alert-danger upload-error" ng-show="uploadError"> <div class="alert alert-danger upload-error" ng-show="uploadError" ng-cloak>
<p><strong>Upload error:</strong> Image upload failed.</p> <p><strong>Upload error:</strong> Image upload failed.</p>
</div> </div>
<div ng-hide="connected" class="container"> <div ng-hide="connected" class="container">
@ -39,20 +39,36 @@
<span>Glowing Bear</span> <span>Glowing Bear</span>
<small>WeeChat web frontend</small> <small>WeeChat web frontend</small>
</h2> </h2>
<div class="alert alert-info alert-dismissable" ng-show="!settings.hideTLSinfo"> <div class="alert alert-warning" ng-show="show_tls_warning" ng-cloak><strong>You're using Glowing Bear over an unencrypted connection (http://). This is not recommended!</strong> We recommend using our secure hosted version at <a href="https://www.glowing-bear.org/">https://www.glowing-bear.org/</a>, or <a href="https://latest.glowing-bear.org/">https://latest.glowing-bear.org</a> for the latest development version. If your relay is on your local network, that is unfortunately impossible, but be aware of the implications.</div>
<a href="#" class="close" ng-click="settings.hideTLSinfo=true;" aria-label="close">&times;</a> <div class="alert alert-danger" ng-show="errorMessage" ng-cloak>
<strong>We now support TLS!</strong> If you're using an encrypted relay, you should change your bookmarks to <a href="https://www.glowing-bear.org" style="">http<span style="color:#090; font-weight:bold">s</span>://www.glowing-bear.org</a>
</div>
<div class="alert alert-danger" ng-show="errorMessage">
<strong>Connection error</strong> The client was unable to connect to the WeeChat relay <strong>Connection error</strong> The client was unable to connect to the WeeChat relay
</div> </div>
<div class="alert alert-danger" ng-show="sslError"> <div class="alert alert-danger" ng-show="sslError" ng-cloak>
<strong>Secure connection error</strong> A secure connection with the WeeChat relay could not be initiated. This is most likely because your browser does not trust your relay's certificate. Please read the encryption instructions below! <strong>Secure connection error</strong> A secure connection with the WeeChat relay could not be initiated. This is most likely because your browser does not trust your relay's certificate. Please read the encryption instructions below!
</div> </div>
<div class="alert alert-danger" ng-show="securityError"> <div class="alert alert-danger" ng-show="securityError" ng-cloak>
<strong>Secure connection error</strong> Unable to connect to unencrypted relay when your are connecting to Glowing Bear over HTTPS. Please use an encrypted relay or load the page without using HTTPS. <strong>Secure connection error</strong> Unable to connect to unencrypted relay when you are connecting to Glowing Bear over HTTPS. Please use an encrypted relay or load the page without using HTTPS.
</div> </div>
<div class="panel-group accordion"> <div class="panel-group accordion">
<div class="panel" data-state="active" ng-show=false>
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle">
Important Note!
</a>
</h4>
</div>
<div class="panel-collapse collapse">
<div class="panel-body">
<div class="form-group">
<div class="alert alert-danger">
GlowingBear requires JavaScript support to function. Additionally, you must allow JS from <code>cdnjs.cloudflare.com</code>. Please check your script blocker or browser settings.
</div>
Glowing Bear is a web frontend for the WeeChat IRC client and strives to be a modern interface. It relies on WeeChat to do all the heavy lifting and then provides some nice features on top of that, like embedding images, videos, and other content. The best part, however, is that you can use it from any modern internet device -- whether it's a computer, tablet, or smart phone -- and all your stuff is there, wherever you are. You don't have to deal with the messy technical details, and all you need to have installed is a browser or our app.
</div>
</div>
</div>
</div>
<div class="panel" data-state="active"> <div class="panel" data-state="active">
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title"> <h4 class="panel-title">
@ -61,7 +77,7 @@
</a> </a>
</h4> </h4>
</div> </div>
<div id="collapseOne" class="panel-collapse collapse in"> <div id="collapseOne" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
<form class="form-signin" role="form"> <form class="form-signin" role="form">
<div class="form-group"> <div class="form-group">
@ -78,7 +94,7 @@
</div> </div>
<label class="control-label" for="password">WeeChat relay password</label> <label class="control-label" for="password">WeeChat relay password</label>
<input type="password" class="form-control favorite-font" id="password" ng-model="password" placeholder="Password"> <input type="password" class="form-control favorite-font" id="password" ng-model="password" placeholder="Password">
<div class="alert alert-danger" ng-show="passwordError"> <div class="alert alert-danger" ng-show="passwordError" ng-cloak>
Error: wrong password Error: wrong password
</div> </div>
@ -97,11 +113,11 @@
<div class="checkbox"> <div class="checkbox">
<label class="control-label" for="ssl"> <label class="control-label" for="ssl">
<input type="checkbox" id="ssl" ng-model="settings.ssl"> <input type="checkbox" id="ssl" ng-model="settings.ssl">
Encryption. Read instructions for help Encryption. <strong>Strongly recommended!</strong> Need help? Check below.
</label> </label>
</div> </div>
</div> </div>
<button class="btn btn-lg btn-primary" ng-click="connect()">{{ connectbutton }} <i ng-class="connectbuttonicon" class="glyphicon"></i></button> <button class="btn btn-lg btn-primary" ng-click="connect()" ng-cloak>{{ connectbutton }} <i ng-class="connectbuttonicon" class="glyphicon"></i></button>
</form> </form>
</div> </div>
</div> </div>
@ -110,68 +126,72 @@
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title"> <h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)"> <a class="accordion-toggle" ng-click="toggleAccordion($event)">
Usage instructions Getting Started
</a> </a>
</h4> </h4>
</div> </div>
<div id="collapseTwo" class="panel-collapse collapse in"> <div id="collapseTwo" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
<h3>Configuring the relay</h3> <p><span class="label label-danger">WeeChat version 0.4.2 or higher is required—we recommend at least 1.0.</p>
<div>To start using glowing bear, please enable the relay plugin in your WeeChat client: <p>To start using Glowing Bear, follow the instructions below to set up an encrypted relay. All communication goes directly between your browser and your WeeChat relay! This means that your server must be accessible. We never see any of your data or your password, and you don't need to trust a "cloud". All settings, including your password, are saved locally in your own browser between sessions.</p>
<div class="alert alert-warning" ng-show="show_tls_warning"><strong>You're using Glowing Bear over an unencrypted connection (http://). This is not recommended!</strong> We recommend using our secure hosted version at <a href="https://www.glowing-bear.org/">https://www.glowing-bear.org/</a>, or <a href="https://latest.glowing-bear.org/">https://latest.glowing-bear.org</a> for the latest and greatest development version. You can still follow the instructions below to set up an encrypted relay, though.</div>
<p>When using encryption, all communication between your browser and WeeChat will be securely encrypted with TLS. This means that you have to set up a certificate. While it's possible to use a self-signed cert, we recommend against it, because it's handled poorly in browsers, and may not work at all on mobile devices. If you don't already have a certificate for your domain (or you don't have a domain), we strongly encourage you to get a certificate from <a href="https://letsencrypt.org/">Let's Encrypt</a>—it's free and easy. We'll walk you through it.</p>
<p><strong>If you don't have a domain</strong> you can get a free subdomain from providers such as <a href="https://freedns.afraid.org/">afraid</a>. You'll want to set up an 'A' record to your server's IP address, and quite possibly an AAAA record to its IPv6 address. These might take a few hours to propagate, if the steps below don't work right away, try again in a few hours.</p>
<p><strong>Getting a certificate</strong> is easy. You'll need certbot—just follow the encryptions at <a href="https://certbot.eff.org/">https://certbot.eff.org</a>. If you're not serving webpages on the same server or are unsure, select "none of the above" (if you are, you can probably use that webserver to proxy your relay, and skip this—check out the <a href="https://github.com/glowing-bear/glowing-bear/wiki/Proxying-WeeChat-relay-with-a-web-server">instructions in our Wiki</a>). Next, get the certificate with <code>certbot certonly --standalone -d {{ settings.host || your.domain.com }}</code> and follow the instructions.</p>
<p>Nearly done! Now you just need to copy the files into place. To do that, use the following commands, replacing the <strong>username</strong> placeholder with your actual username:</p>
<pre>mkdir -p ~<strong>username</strong>/.weechat/ssl
cat /etc/letsencrypt/live/{{ settings.host || your.domain.com }}/{fullchain,privkey}.pem &gt; ~<strong>username</strong>/.weechat/ssl/relay.pem
chown -R <strong>username</strong>:<strong>username</strong> ~<strong>username</strong>/.weechat/ssl/</pre>
<p>Once you've got the certificate and moved it in place, you can set up an encrypted relay on port {{ settings.port || 9001 }} with these WeeChat commands:</p>
<pre> <pre>
/set relay.network.password yourpassword /set relay.network.password y0ur_StRonG-pa$sw0rd:of*choice
/relay add weechat {{ settings.port || 9001 }} /relay sslcertkey
/relay add ssl.weechat {{ settings.port || 9001 }}
</pre> </pre>
<span class="label label-danger">WeeChat version 0.4.2 or higher is required.</span><br> <p>Your certificate needs to be renewed every couple of months. Either follow the instructions for automatic renewal at <a href="https://certbot.eff.org/">https://certbot.eff.org</a>, or run <code>certbot renew</code> manually when renewal is due. <strong>Important:</strong> You'll need to follow the instructions for copying the certificate to the right place again, and re-run <code>/relay sslcertkey</code> in WeeChat.</p>
The communication goes directly between your browser and your WeeChat relay in plain text. Check the instructions below for help on setting up encrypted communication. </div>
Connection settings, including your password, are saved locally in your own browser between sessions. </div>
<br> </div>
<h3>Shortcuts</h3> <div class="panel" data-state="collapsed">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
Usage instructions
</a>
</h4>
</div>
<div id="collapseThree" class="panel-collapse collapse">
<div class="panel-body">
<h3 style="margin-top: 0px">Shortcuts</h3>
Glowing Bear has a few shortcuts: Glowing Bear has a few shortcuts:
<ul> <ul>
<li><kbd>ALT-n</kbd>: Toggle nicklist</li> <li><kbd>ALT-n</kbd>: Toggle nicklist</li>
<li><kbd>ALT-l</kbd>: Focus on input bar</li> <li><kbd>ALT-l</kbd>: Focus on input bar</li>
<li><kbd>ALT-[0-9]</kbd>: Switch to buffer number N</li> <li><kbd>ALT-[0-9]</kbd>: Switch to buffer number N</li>
<li><kbd>ALT-↑/↓</kbd>: Switch to buffer above/below</li>
<li><kbd>ALT-a</kbd>: Focus on next buffer with activity</li> <li><kbd>ALT-a</kbd>: Focus on next buffer with activity</li>
<li><kbd>ALT-&lt;</kbd>: Switch to previous active buffer</li> <li><kbd>ALT-&lt;</kbd>: Switch to previous active buffer</li>
<li><kbd>ALT-g</kbd>: Focus on buffer list filter</li> <li><kbd>ALT-g</kbd>: Focus on buffer list filter</li>
<li><kbd>ALT-h</kbd>: Clear unread counters in every buffer (locally)</li>
<li><kbd>Esc-Esc</kbd>: Disconnect (double-tap)</li> <li><kbd>Esc-Esc</kbd>: Disconnect (double-tap)</li>
<li>Arrow keys: Navigate history</li> <li>Arrow keys: Navigate history, or navigate quick search buffer results. Pressing <kbd></kbd> while writing a message pushes it onto the history for later re-use, without sending it.</li>
<li><kbd>Tab</kbd>: Complete nick</li> <li><kbd>Tab</kbd>: Complete nick</li>
<li>The following readline/emacs style keybindings can be enabled with a setting: <span title="Move cursor to beginning of line"><kbd>Ctrl-a</kbd></span>, <span title="Move cursor to te end of the line"><kbd>Ctrl-e</kbd></span>, <span title="Delete from cursor to beginning of the line"><kbd>Ctrl-u</kbd></span>, <span title="Delete from cursor to the end of the line"><kbd>Ctrl-k</kbd></span>, <span title="Delete from cursor to previous space"><kbd>Ctrl-w</kbd></span></li> <li>The following readline/emacs style keybindings can be enabled with a setting: <span title="Move cursor to beginning of line"><kbd>Ctrl-a</kbd></span>, <span title="Move cursor to te end of the line"><kbd>Ctrl-e</kbd></span>, <span title="Delete from cursor to beginning of the line"><kbd>Ctrl-u</kbd></span>, <span title="Delete from cursor to the end of the line"><kbd>Ctrl-k</kbd></span>, <span title="Delete from cursor to previous space"><kbd>Ctrl-w</kbd></span></li>
</ul> </ul>
</div> <h3>Pinning buffers</h3>
<p>
The option "Only show buffers with unread messages" is useful when you have a lot of buffers and can't meaningfully look at all of them at the same time. However, often you have a select few buffers that you use more frequently and would like to have displayed permanently.
</p>
<p>
To pin a buffer, type <code>/buffer set localvar_set_pinned true</code>. <strong>Note</strong>: Local variables on buffers are not persisted across WeeChat restarts, so either use script <code>buffer_autoset.py</code> to automatically apply that upon buffer creation or use a trigger if you want automatic repinning when buffers get recreated. To unpin, you can use the same command and set anything other than <code>true</code>.
</p>
<p>
Helpful trigger to automatically repin a buffer (in this instance, <var>irc.freenode.#weechat</var>): <pre><code>/trigger add autopin signal "buffer_opened" "${buffer[${tg_signal_data}].full_name} =~ <var>irc.freenode.#weechat</var>" "" "/command -buffer ${buffer[${tg_signal_data}].full_name} * /buffer set localvar_set_pinned true"</code></pre>
</p>
</div> </div>
</div> </div>
</div> </div>
<div class="panel" data-state="collapsed"> <div class="panel" data-state="collapsed">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
Encryption instructions
</a>
</h4>
</div>
<div id="collapseThree" class="panel-collapse collapse in">
<div class="panel-body">
<p>If you check the encryption box, the communication between browser and WeeChat will be encrypted with TLS.</p>
<p><strong>Note</strong>: If you are using a self-signed certificate, you have to visit <a href="https://{{ settings.host }}:{{ settings.port }}/weechat">https://{{ settings.host || 'weechathost' }}:{{ settings.port || 'relayport' }}/weechat</a> in your browser first to add a security exception. You can close that tab once you confirmed the certificate, no content will appear. The necessity of this process is a bug in <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=594502">Firefox</a> and other browsers.</p>
<p><strong>Setup</strong>: If you want to use an encrypted session you first have to set up the relay to use TLS. You basically have two options: a self-signed certificate is easier to set up, but requires manual security exceptions. Using a certificate that is trusted by your browser requires more setup, but offers greater convenience later on and does not require security exceptions. You can find a guide to set up WeeChat with a free trusted certificate from StartSSL <a href="https://4z2.de/2014/07/06/weechat-trusted-relay">here</a>. Should you wish to use a self-signed certificate instead, execute the following commands in a shell on the same host and as the user running WeeChat:</p>
<pre>
$ mkdir -p ~/.weechat/ssl
$ cd ~/.weechat/ssl
$ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out relay.pem -sha256 -subj "/CN={{settings.host || 'your weechat host'}}/"
</pre>
<p>If WeeChat is already running, you can reload the certificate and private key and set up an encrypted relay on port {{ settings.port || 9001 }} with these WeeChat commands:</p>
<pre>
/set relay.network.password yourpassword
/relay sslcertkey
/relay add ssl.weechat {{ settings.port || 9001 }}
</pre>
</div>
</div>
</div>
<div class="panel" data-state="collapsed" ng-hide="isinstalled">
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title"> <h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)"> <a class="accordion-toggle" ng-click="toggleAccordion($event)">
@ -179,16 +199,20 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</a> </a>
</h4> </h4>
</div> </div>
<div id="collapseFour" class="panel-collapse collapse in"> <div id="collapseFour" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
<p>You don't need to install anything to use this app, it should work with any modern browser. Start using it <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">right now</a>! However, there are a few ways to improve integration with your operating system.</p> <p>You don't need to install anything to use Glowing Bear, it works with any modern browser. Start using it <a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">right now</a> at the top of the page! However, there are a few ways to improve integration with your operating system.</p>
<h3>Mobile Applications</h3> <h3>Mobile Applications</h3>
<p>If you're running Android 4.4 or later, you can install our app from the Google Play Store! We also provide an optimized application for Firefox OS devices. If you're using the Firefox browser, keep on reading below -- the Firefox OS app won't work for you</p> <p>If you're running Android 4.4 or later, you can install our app from the Google Play Store! We can't distribute on iOS unfortunately, but if you're a developer, you can <a href="https://github.com/glowing-bear/glowing-bear-cordova">follow the sideloading instructions</a>.</p>
<p><a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="assets/img/badge_playstore.png" /></a> <a href="https://marketplace.firefox.com/app/glowing-bear/"><img alt="Firefox OS app in the Firefox Marketplace" src="assets/img/badge_firefoxos.png" /></a></p> <p><a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="assets/img/badge_playstore.png" /></a></p>
<h3>Electron</h3>
<p>Glowing Bear supports the electron shell. You'll have to build it yourself, though. Run the following commands, choosing your platform from the list in the last command: <pre>git clone https://github.com/glowing-bear/glowing-bear
cd glowing-bear
npm install
npm install electron-packager
npm run build-electron-{windows, darwin, linux}</pre>
<h3>Firefox Browser</h3> <h3>Firefox Browser</h3>
<p>If you have a recent version of Firefox you can install Glowing Bear as a Firefox app. Click the button to install.</p> <p>Firefox used to support apps, but this was removed from Firefox. There's nothing we can do about it. Sorry!</p>
<p><button class="btn btn-lg btn-primary" ng-click="install()">Install Firefox app <i class="glyphicon glyphicon-chevron-right"></i></button></p>
<p>Note for self-signed certificates: Firefox does not share a certificate storage with Firefox apps, so accepting self-signed certificates is a bit tricky.</p>
<h3>Chrome</h3> <h3>Chrome</h3>
<p>To install Glowing Bear as an app in Chrome for Android, select <kbd>Menu - Add to home screen</kbd>. In the desktop version of Chrome, click <kbd>Menu - More tools - Create application shortcuts</kbd>.</p> <p>To install Glowing Bear as an app in Chrome for Android, select <kbd>Menu - Add to home screen</kbd>. In the desktop version of Chrome, click <kbd>Menu - More tools - Create application shortcuts</kbd>.</p>
</div> </div>
@ -202,16 +226,16 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</a> </a>
</h4> </h4>
</div> </div>
<div id="collapseFive" class="panel-collapse collapse in"> <div id="collapseFive" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
<p>Glowing bear is built by a small group of developers in their free time. As we're always trying to improve it, we would love getting your feedback and help. If that sounds like something you might enjoy, check out our <a href="https://github.com/glowing-bear/glowing-bear">project page</a> on GitHub!</p> <p>Glowing bear is built by a small group of developers in their free time. As we're always trying to improve it, we would love getting your feedback and help. If that sounds like something you might enjoy, check out our <a href="https://github.com/glowing-bear/glowing-bear">project page</a> on GitHub!</p>
<p>If you're interested in contributing or simply want to say hello, head over to <strong>#glowing-bear</strong> on <strong>freenode!</strong> We won't bite, promise (-ish).</p> <p>If you're interested in contributing or simply want to say hello, head over to <strong>#glowing-bear</strong> on <strong>freenode!</strong> We won't bite, promise :)</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="content" id="content" sidebar-state="visible" ng-show="connected"> <div class="content" id="content" sidebar-state="visible" ng-show="connected" ng-cloak>
<div id="topbar"> <div id="topbar">
<div class="brand"> <div class="brand">
<a href="#" ng-click="toggleSidebar()"> <a href="#" ng-click="toggleSidebar()">
@ -223,9 +247,9 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button> <button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button>
</div> </div>
<div class="title" title="{{activeBuffer().rtitle}}"> <div class="title" ng-click="showModal('topicModal')">
<span class="desktop" ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></span> <span class="desktop" ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank':{rel:'noopener noreferrer'} | DOMfilter:'irclinky')"></span>
<span class="mobile" ng-click="showModal('topicModal')" ng-class="{'active': activeBuffer().active, 'channel': activeBuffer().type === 'channel', 'channel_hash': activeBuffer().prefix === '#', 'channel_plus': activeBuffer().prefix === '+', 'channel_ampersand': activeBuffer().prefix === '&'}">{{ activeBuffer().trimmedName || activeBuffer().fullName }}</span> <span class="mobile" ng-class="{'active': activeBuffer().active, 'channel': activeBuffer().type === 'channel', 'channel_hash': activeBuffer().prefix === '#', 'channel_plus': activeBuffer().prefix === '+', 'channel_ampersand': activeBuffer().prefix === '&'}">{{ activeBuffer().trimmedName || activeBuffer().fullName }}</span>
</div> </div>
<div class="actions pull-right vertical-line-left"> <div class="actions pull-right vertical-line-left">
@ -237,31 +261,45 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</a> </a>
</div> </div>
</div> </div>
<div id="sidebar" data-state="visible" ng-swipe-left="hideSidebar()" class="vertical-line"> <div id="sidebar" data-state="visible" ng-swipe-left="swipeLeft()" ng-swipe-disable-mouse class="vertical-line">
<ul class="nav nav-pills nav-stacked" ng-class="{'indented': (predicate === 'serverSortKey'), 'showquickkeys': showQuickKeys}"> <ul class="nav nav-pills nav-stacked" ng-class="{'indented': (predicate === 'serverSortKey'), 'showquickkeys': showQuickKeys, 'showjumpkeys': showJumpKeys}">
<li class="bufferfilter"> <li class="bufferfilter">
<form role="form"> <form role="form">
<input class="form-control favorite-font" type="text" id="bufferFilter" ng-model="search" ng-keydown="handleSearchBoxKey($event)" placeholder="Search" autocomplete="off"> <div class="input-group">
<span class="input-group-addon" id="jump-addon">Jump</span>
<input class="form-control favorite-font" type="text" id="bufferFilter" ng-model="search" ng-keydown="handleSearchBoxKey($event)" placeholder="{{ search_placeholder || 'Search' }}" autocomplete="off" aria-describedby="jump-addon">
</div>
</form> </form>
</li> </li>
<li class="buffer" ng-class="{'active': buffer.active, 'indent': buffer.indent, 'channel': buffer.type === 'channel', 'channel_hash': buffer.prefix === '#', 'channel_plus': buffer.prefix === '+', 'channel_ampersand': buffer.prefix === '&', 'private': buffer.type === 'private'}" ng-repeat="(key, buffer) in (filteredBuffers = (getBuffers() | toArray:'withidx' | filter:{fullName:search} | filter:hasUnread | orderBy:predicate | getBufferQuickKeys:this))"> <li class="buffer" ng-class="{'active': buffer.active,
'unread': buffer.unread,
'notification': buffer.notification,
'highlight': search && search_highlight_key === key,
'indent': buffer.indent,
'channel': buffer.type === 'channel',
'channel_hash': buffer.prefix === '#',
'channel_plus': buffer.prefix === '+',
'channel_ampersand': buffer.prefix === '&',
'private': buffer.type === 'private'}"
ng-repeat="(key, buffer) in (filteredBuffers = (getBuffers() | toArray:'withidx' | filter:bufferlistfilter | filter:hasUnread | orderBy:predicate | getBufferQuickKeys:this))">
<a ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}" href="#"> <a ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}" href="#">
<span class="badge pull-right" ng-class="{'danger': buffer.notification}" ng-if="buffer.notification || buffer.unread" ng-bind="buffer.notification || buffer.unread"></span> <span class="badge pull-right" ng-class="{'danger': buffer.notification}" ng-bind="buffer.notification || buffer.unread"></span>
<span class="buffer-quick-key">{{ buffer.$quickKey }}</span> <span class="buffer-quick-key">{{ buffer.$quickKey }}</span>
<span class="buffer-jump-key">{{ ("0" + buffer.$jumpKey).slice(-2) }}</span>
<span class="buffername">{{ buffer.trimmedName || buffer.fullName }}</span> <span class="buffername">{{ buffer.trimmedName || buffer.fullName }}</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<div id="bufferlines" class="favorite-font" ng-swipe-right="showSidebar()" ng-swipe-left="hideSidebar()" ng-class="{'withnicklist': showNicklist}" when-scrolled="infiniteScroll()" imgur-drop> <div id="nicklist" ng-if="showNicklist" ng-swipe-right="swipeRight()" ng-swipe-disable-mouse class="vertical-line-left">
<div id="nicklist" ng-if="showNicklist" ng-swipe-right="closeNick()" class="vertical-line-left"> <ul class="nicklistgroup list-unstyled" ng-repeat="group in nicklist">
<ul class="nicklistgroup list-unstyled" ng-repeat="group in nicklist"> <li ng-repeat="nick in group.nicks|orderBy:'name'">
<li ng-repeat="nick in group.nicks|orderBy:'name'"> <a ng-click="openBuffer(nick.name)"><span ng-class="::nick.prefixClasses" ng-bind="::nick.prefix"></span><span ng-class="::nick.nameClasses" ng-bind="::nick.name"></span></a>
<a ng-click="openBuffer(nick.name)"><span ng-class="::nick.prefixClasses" ng-bind="::nick.prefix"></span><span ng-class="::nick.nameClasses" ng-bind="::nick.name"></span></a> </li>
</li> </ul>
</ul> </div>
</div> <div id="bufferlines" class="favorite-font" ng-swipe-right="swipeRight()" ng-swipe-left="swipeLeft()" ng-swipe-disable-mouse ng-class="{'withnicklist': showNicklist}" when-scrolled="infiniteScroll()" imgur-drop>
<table ng-class="{'notimestamp':!settings.showtimestamp,'notimestampseconds':!settings.showtimestampSeconds}"> <table>
<tbody> <tbody>
<tr class="bufferline"> <tr class="bufferline">
<td ng-hide="activeBuffer().allLinesFetched" colspan="3"> <td ng-hide="activeBuffer().allLinesFetched" colspan="3">
@ -274,13 +312,13 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<tr class="bufferline"> <tr class="bufferline">
<td class="time"> <td class="time">
<span class="date" ng-class="::{'repeated-time': bufferline.shortTime==bufferlines[$index-1].shortTime}"> <span class="date" ng-class="::{'repeated-time': bufferline.shortTime==bufferlines[$index-1].shortTime}">
<span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind="::(bufferline.date|date:'HH')"></span><span class="cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters">:</span><span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind="::(bufferline.date|date:'mm')"></span><span class="seconds"><span class="cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters">:</span><span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind="::(bufferline.date|date:'ss')"></span></span> <span class="cof-chat_time cob-chat_time coa-chat_time" ng-bind-html="::bufferline.formattedTime"></span>
</span> </span>
</td> </td>
<td class="prefix"><a ng-click="addMention(bufferline.prefix)"><span class="hidden-bracket">&lt;</span><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text|prefixlimit:25"></span><span class="hidden-bracket">&gt;</span></a></td><!-- <td class="prefix"><span ng-class="::{'repeated-prefix': bufferline.prefixtext==bufferlines[$index-1].prefixtext}"><a ng-click="addMention(bufferline)"><span class="hidden-bracket" ng-if="::(bufferline.showHiddenBrackets)">&lt;</span><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text|prefixlimit:25"></span><span class="hidden-bracket" ng-if="::(bufferline.showHiddenBrackets)">&gt;</span></a></span></td><!--
--><td class="message"><!-- --><td class="message"><!--
--><div ng-repeat="metadata in ::bufferline.metadata" plugin data="::metadata"></div><!-- --><div ng-repeat="metadata in ::bufferline.metadata" plugin data="::metadata"></div><!--
--><span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes.concat(['line-' + part.$$hashKey.replace(':','_')])" ng-bind-html="::part.text | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'emojify':settings.enableJSEmoji | DOMfilter:'inlinecolour' | DOMfilter:'mathjax':('.line-' + part.$$hashKey.replace(':','_')):settings.enableMathjax"></span> --><span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes.concat(['line-' + part.$$hashKey.replace(':','_')])" ng-bind-html="::part.text | linky:'_blank':{rel:'noopener noreferrer'} | DOMfilter:'irclinky' | DOMfilter:'emojify':settings.enableJSEmoji | DOMfilter:'inlinecolour' | DOMfilter:'latexmath':('.line-' + part.$$hashKey.replace(':','_')):settings.enableMathjax"></span>
</td> </td>
</tr> </tr>
<tr class="readmarker" ng-if="activeBuffer().lastSeen==$index"> <tr class="readmarker" ng-if="activeBuffer().lastSeen==$index">
@ -296,7 +334,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</div> </div>
</div> </div>
<div id="soundNotification"></div> <div id="soundNotification"></div>
<div id="reconnect" class="alert alert-danger" ng-click="reconnect()" ng-show="reconnecting"> <div id="reconnect" class="alert alert-danger" ng-click="reconnect()" ng-show="reconnecting" ng-cloak>
<p><strong>Connection to WeeChat lost</strong></p> <p><strong>Connection to WeeChat lost</strong></p>
<i class="glyphicon glyphicon-refresh"></i> <i class="glyphicon glyphicon-refresh"></i>
Reconnecting... <i class="glyphicon glyphicon-spin glyphicon-refresh"></i> <a class="btn btn-xs" ng-click="reconnect()" href="#">Click to try to reconnect now</a> Reconnecting... <i class="glyphicon glyphicon-spin glyphicon-refresh"></i> <a class="btn btn-xs" ng-click="reconnect()" href="#">Click to try to reconnect now</a>
@ -307,7 +345,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button> <button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button>
<span class="pull-right version">Glowing Bear version 0.6.0</span> <span class="pull-right version">Glowing Bear version 0.7.0</span>
<h4 class="modal-title">Settings</h4> <h4 class="modal-title">Settings</h4>
<p>Settings will be stored in your browser.</p> <p>Settings will be stored in your browser.</p>
</div> </div>
@ -364,45 +402,33 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="settings.showtimestamp"> <input type="checkbox" ng-model="settings.noembed">
Show timestamps Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden regardless of this choice</span>
</label> </label>
</div> </div>
</form> </form>
<ul ng-show="settings.showtimestamp">
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.showtimestampSeconds">
Show seconds
</label>
</div>
</form>
</li>
</ul>
</li> </li>
<li> <li>
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="settings.noembed"> <input type="checkbox" ng-model="settings.hotlistsync">
Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden regardless of this choice</span> Mark messages as read in WeeChat
</label> </label>
</div> </div>
</form> </form>
</li> </li>
<li> <li class="mobile">
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="settings.hotlistsync"> <input type="checkbox" ng-model="settings.alwaysnicklist">
Mark messages as read in WeeChat Always show nicklist
</label> </label>
</div> </div>
</form> </form>
</li> </li>
<li> <li class="desktop">
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
@ -462,7 +488,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</div> </div>
</form> </form>
</li> </li>
<li> <li>
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
<label> <label>
@ -472,6 +498,16 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</div> </div>
</form> </form>
</li> </li>
<li class="desktop">
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.enableQuickKeys">
Use Alt+[0-9] to switch buffers
</label>
</div>
</form>
</li>
</ul> </ul>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -486,8 +522,8 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button> <button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button>
<h4 class="modal-title">Channel topic</h4> <h4 class="modal-title">Channel topic {{ activeBuffer().shortName || activeBuffer().fullName }}</h4>
<p ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></p> <p ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank':{rel:'noopener noreferrer'} | DOMfilter:'irclinky')"></p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="closeModal($event)">Close</button> <button type="button" class="btn btn-primary" ng-click="closeModal($event)">Close</button>

@ -0,0 +1,56 @@
/*
* This file contains the service used to record the last
* accessed buffer and return to it when reconnecting to
* the relay.
*/
(function() {
'use strict';
var bufferResume = angular.module('bufferResume', []);
bufferResume.service('bufferResume', ['settings', function(settings) {
var resumer = {};
var key = settings.host + ":" + settings.port;
// Hold the status that we were able to find the previously accessed buffer
// and reload it. If we cannot, we'll need to know so we can load the default
var hasResumed = false;
// Store the current buffer as having been accessed. We can later retrieve it and compare
// we recieve info from weechat to determine if we should switch to it.
resumer.record = function(activeBuffer) {
var subSetting = settings.currentlyViewedBuffers;
subSetting[key] = activeBuffer.id;
settings.currentlyViewedBuffers = subSetting;
};
// See if the requested buffer information matches the last recorded access. If so,
// the handler should switch to this buffer.
resumer.shouldResume = function(buffer) {
var savedBuffer = settings.currentlyViewedBuffers[key];
if (!savedBuffer) { return false; }
if (!hasResumed) {
if (savedBuffer === buffer.id) {
hasResumed = true;
return true;
}
return false;
}
};
// The handler will ask for this after loading all infos. If it was unable to find a buffer
// it will need to know so it can pick the default buffer.
resumer.wasAbleToResume = function() {
return hasResumed;
};
// Clear out the recorded info. Maybe we'll do this when the user chooses to disconnect?
resumer.reset = function() {
hasResumed = false;
};
return resumer;
}]);
})();

@ -15,6 +15,9 @@ weechat.factory('connection',
var connectionData = []; var connectionData = [];
var reconnectTimer; var reconnectTimer;
// Global connection lock to prevent multiple connections from being opened
var locked = false;
// Takes care of the connection and websocket hooks // Takes care of the connection and websocket hooks
var connect = function (host, port, passwd, ssl, noCompression, successCallback, failCallback) { var connect = function (host, port, passwd, ssl, noCompression, successCallback, failCallback) {
$rootScope.passwordError = false; $rootScope.passwordError = false;
@ -32,6 +35,8 @@ weechat.factory('connection',
// Helper methods for initialization commands // Helper methods for initialization commands
var _initializeConnection = function(passwd) { var _initializeConnection = function(passwd) {
// Escape comma in password (#937)
passwd = passwd.replace(',', '\\,');
// This is not the proper way to do this. // This is not the proper way to do this.
// WeeChat does not send a confirmation for the init. // WeeChat does not send a confirmation for the init.
// Until it does, We need to "assume" that formatInit // Until it does, We need to "assume" that formatInit
@ -74,6 +79,104 @@ weechat.factory('connection',
); );
}; };
var _parseWeechatTimeFormat = function() {
// helper function to get a custom delimiter span
var _timeDelimiter = function(delim) {
return "'<span class=\"cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters\">" + delim + "</span>'";
};
// Fetch the buffer time format from weechat
var timeFormat = models.wconfig['weechat.look.buffer_time_format'];
// Weechat uses strftime, with time specifiers such as %I:%M:%S for 12h time
// The time formatter we use, AngularJS' date filter, uses a different format
// Where %I:%M:%S would be represented as hh:mm:ss
// Here, we detect what format the user has set in Weechat and slot it into
// one of four formats, (short|long) (12|24)-hour time
var angularFormat = "";
var timeDelimiter = _timeDelimiter(":");
var left12 = "hh" + timeDelimiter + "mm";
var right12 = "'&nbsp;'a";
var short12 = left12 + right12;
var long12 = left12 + timeDelimiter + "ss" + right12;
var short24 = "HH" + timeDelimiter + "mm";
var long24 = short24 + timeDelimiter + "ss";
if (timeFormat.indexOf("%H") > -1 ||
timeFormat.indexOf("%k") > -1) {
// 24h time detected
if (timeFormat.indexOf("%S") > -1) {
// show seconds
angularFormat = long24;
} else {
// don't show seconds
angularFormat = short24;
}
} else if (timeFormat.indexOf("%I") > -1 ||
timeFormat.indexOf("%l") > -1 ||
timeFormat.indexOf("%p") > -1 ||
timeFormat.indexOf("%P") > -1) {
// 12h time detected
if (timeFormat.indexOf("%S") > -1) {
// show seconds
angularFormat = long12;
} else {
// don't show seconds
angularFormat = short12;
}
} else if (timeFormat.indexOf("%r") > -1) {
// strftime doesn't have an equivalent for short12???
angularFormat = long12;
} else if (timeFormat.indexOf("%T") > -1) {
angularFormat = long24;
} else if (timeFormat.indexOf("%R") > -1) {
angularFormat = short24;
} else {
angularFormat = short24;
}
// Assemble date format
var date_components = [];
// Check for day of month in time format
var day_pos = Math.max(timeFormat.indexOf("%d"),
timeFormat.indexOf("%e"));
date_components.push([day_pos, "dd"]);
// month of year?
var month_pos = timeFormat.indexOf("%m");
date_components.push([month_pos, "MM"]);
// year as well?
var year_pos = Math.max(timeFormat.indexOf("%y"),
timeFormat.indexOf("%Y"));
if (timeFormat.indexOf("%y") > -1) {
date_components.push([year_pos, "yy"]);
} else if (timeFormat.indexOf("%Y") > -1) {
date_components.push([year_pos, "yyyy"]);
}
// if there is a date, assemble it in the right order
date_components.sort();
var format_array = [];
for (var i = 0; i < date_components.length; i++) {
if (date_components[i][0] == -1) continue;
format_array.push(date_components[i][1]);
}
if (format_array.length > 0) {
// TODO: parse delimiter as well? For now, use '/' as it is
// more common internationally than '-'
var date_format = format_array.join(_timeDelimiter("/"));
angularFormat = date_format + _timeDelimiter("&nbsp;") + angularFormat;
}
$rootScope.angularTimeFormat = angularFormat;
};
// First command asks for the password and issues // First command asks for the password and issues
// a version command. If it fails, it means the we // a version command. If it fails, it means the we
@ -90,14 +193,37 @@ weechat.factory('connection',
_requestHotlist().then(function(hotlist) { _requestHotlist().then(function(hotlist) {
handlers.handleHotlistInfo(hotlist); handlers.handleHotlistInfo(hotlist);
if (successCallback) { });
successCallback(); // Schedule hotlist syncing every so often so that this
// client will have unread counts (mostly) in sync with
// other clients or terminal usage directly.
setInterval(function() {
if ($rootScope.connected) {
_requestHotlist().then(function(hotlist) {
handlers.handleHotlistInfo(hotlist);
});
} }
}, 60000); // Sync hotlist every 60 second
// Fetch weechat time format for displaying timestamps
fetchConfValue('weechat.look.buffer_time_format',
function() {
// Will set models.wconfig['weechat.look.buffer_time_format']
_parseWeechatTimeFormat();
}); });
// Fetch nick completion config
fetchConfValue('weechat.completion.nick_completer');
fetchConfValue('weechat.completion.nick_add_space');
_requestSync(); _requestSync();
$log.info("Connected to relay"); $log.info("Connected to relay");
$rootScope.connected = true; $rootScope.connected = true;
if (successCallback) {
successCallback();
}
}, },
function() { function() {
handleWrongPassword(); handleWrongPassword();
@ -120,6 +246,7 @@ weechat.factory('connection',
*/ */
$log.info("Disconnected from relay"); $log.info("Disconnected from relay");
$rootScope.$emit('relayDisconnect'); $rootScope.$emit('relayDisconnect');
locked = false;
if ($rootScope.userdisconnect || !$rootScope.waseverconnected) { if ($rootScope.userdisconnect || !$rootScope.waseverconnected) {
handleClose(evt); handleClose(evt);
$rootScope.userdisconnect = false; $rootScope.userdisconnect = false;
@ -154,6 +281,7 @@ weechat.factory('connection',
* the relay. * the relay.
*/ */
$log.error("Relay error", evt); $log.error("Relay error", evt);
locked = false; // release connection lock
$rootScope.lastError = Date.now(); $rootScope.lastError = Date.now();
if (evt.type === "error" && this.readyState !== 1) { if (evt.type === "error" && this.readyState !== 1) {
@ -162,6 +290,13 @@ weechat.factory('connection',
} }
}; };
if (locked) {
// We already have an open connection
$log.debug("Aborting connection (lock in use)");
}
// Kinda need a compare-and-swap here...
locked = true;
try { try {
ngWebsockets.connect(url, ngWebsockets.connect(url,
protocol, protocol,
@ -173,6 +308,7 @@ weechat.factory('connection',
'onerror': onerror 'onerror': onerror
}); });
} catch(e) { } catch(e) {
locked = false;
$log.debug("Websocket caught DOMException:", e); $log.debug("Websocket caught DOMException:", e);
$rootScope.lastError = Date.now(); $rootScope.lastError = Date.now();
$rootScope.errorMessage = true; $rootScope.errorMessage = true;
@ -250,6 +386,7 @@ weechat.factory('connection',
// The connection can time out on its own // The connection can time out on its own
ngWebsockets.failCallbacks('disconnection'); ngWebsockets.failCallbacks('disconnection');
$rootScope.connected = false; $rootScope.connected = false;
locked = false; // release the connection lock
$rootScope.$emit('relayDisconnect'); $rootScope.$emit('relayDisconnect');
$rootScope.$apply(); $rootScope.$apply();
}); });
@ -289,6 +426,10 @@ weechat.factory('connection',
} }
}; };
var sendHotlistClearAll = function() {
sendMessage("/input hotlist_clear");
};
var requestNicklist = function(bufferId, callback) { var requestNicklist = function(bufferId, callback) {
// Prevent requesting nicklist for all buffers if bufferId is invalid // Prevent requesting nicklist for all buffers if bufferId is invalid
if (!bufferId) { if (!bufferId) {
@ -306,7 +447,7 @@ weechat.factory('connection',
}); });
}; };
var fetchConfValue = function(name) { var fetchConfValue = function(name, callback) {
ngWebsockets.send( ngWebsockets.send(
weeChat.Protocol.formatInfolist({ weeChat.Protocol.formatInfolist({
name: "option", name: "option",
@ -315,6 +456,9 @@ weechat.factory('connection',
}) })
).then(function(i) { ).then(function(i) {
handlers.handleConfValue(i); handlers.handleConfValue(i);
if (callback !== undefined) {
callback();
}
}); });
}; };
@ -379,6 +523,7 @@ weechat.factory('connection',
sendMessage: sendMessage, sendMessage: sendMessage,
sendCoreCommand: sendCoreCommand, sendCoreCommand: sendCoreCommand,
sendHotlistClear: sendHotlistClear, sendHotlistClear: sendHotlistClear,
sendHotlistClearAll: sendHotlistClearAll,
fetchMoreLines: fetchMoreLines, fetchMoreLines: fetchMoreLines,
requestNicklist: requestNicklist, requestNicklist: requestNicklist,
attemptReconnect: attemptReconnect attemptReconnect: attemptReconnect

@ -135,6 +135,17 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
}; };
}]); }]);
// This is used by the cordova app to change link targets to "window.open(<url>, '_system')"
// so that they're opened in a browser window and don't navigate away from Glowing Bear
weechat.filter('linksForCordova', ['$sce', function($sce) {
return function(text) {
// XXX TODO this needs to be improved
text = text.replace(/<a (rel="[a-z ]+"\s+)?(?:target="_[a-z]+"\s+)?href="([^"]+)"/gi,
"<a $1 onClick=\"window.open('$2', '_system')\"");
return $sce.trustAsHtml(text);
};
}]);
weechat.filter('getBufferQuickKeys', function () { weechat.filter('getBufferQuickKeys', function () {
return function (obj, $scope) { return function (obj, $scope) {
if (!$scope) { return obj; } if (!$scope) { return obj; }
@ -151,6 +162,11 @@ weechat.filter('getBufferQuickKeys', function () {
return left[0] - right[0] || left[1] - right[1]; return left[0] - right[0] || left[1] - right[1];
}).forEach(function(info, keyIdx) { }).forEach(function(info, keyIdx) {
obj[ info[2] ].$quickKey = keyIdx < 10 ? (keyIdx + 1) % 10 : ''; obj[ info[2] ].$quickKey = keyIdx < 10 ? (keyIdx + 1) % 10 : '';
// Don't update jump key upon filtering
if (obj[ info[2] ].$jumpKey === undefined) {
// Only assign jump keys up to 99
obj[ info[2] ].$jumpKey = (keyIdx < 99) ? keyIdx + 1 : '';
}
}); });
} }
return obj; return obj;
@ -175,15 +191,23 @@ weechat.filter('emojify', function() {
}; };
}); });
weechat.filter('mathjax', function() { weechat.filter('latexmath', function() {
return function(text, selector, enabled) { return function(text, selector, enabled) {
if (!enabled || typeof(MathJax) === "undefined") { if (!enabled || typeof(katex) === "undefined") {
return text; return text;
} }
if (text.indexOf("$$") != -1 || text.indexOf("\\[") != -1 || text.indexOf("\\(") != -1) { if (text.indexOf("$$") != -1 || text.indexOf("\\[") != -1 || text.indexOf("\\(") != -1) {
// contains math // contains math -> delayed rendering
var math = document.querySelector(selector); setTimeout(function() {
MathJax.Hub.Queue(["Typeset",MathJax.Hub,math]); var math = document.querySelector(selector);
renderMathInElement(math, {
delimiters: [
{left: "$$", right: "$$", display: false},
{left: "\\[", right: "\\]", display: true},
{left: "\\(", right: "\\)", display: false}
]
});
});
} }
return text; return text;

@ -1,7 +1,14 @@
(function() { (function() {
'use strict'; 'use strict';
var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch'], ['$compileProvider', function($compileProvider) { // cordova splash screen
document.addEventListener("deviceready", function () {
if (navigator.splashscreen !== undefined) {
navigator.splashscreen.hide();
}
}, false);
var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'bufferResume', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch'], ['$compileProvider', function($compileProvider) {
// hacky way to be able to find out if we're in debug mode // hacky way to be able to find out if we're in debug mode
weechat.compileProvider = $compileProvider; weechat.compileProvider = $compileProvider;
}]); }]);
@ -12,8 +19,9 @@ weechat.config(['$compileProvider', function ($compileProvider) {
} }
}]); }]);
weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', 'settings', weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'bufferResume', 'connection', 'notifications', 'utils', 'settings',
function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils, settings) { function ($rootScope, $scope, $store, $timeout, $log, models, bufferResume, connection, notifications, utils, settings)
{
window.openBuffer = function(channel) { window.openBuffer = function(channel) {
$scope.openBuffer(channel); $scope.openBuffer(channel);
@ -21,7 +29,13 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}; };
$scope.command = ''; $scope.command = '';
$scope.themes = ['dark', 'light', 'black']; $scope.themes = ['dark', 'light', 'black', 'dark-spacious', 'blue', 'base16-default', 'base16-light', 'base16-mocha', 'base16-ocean-dark', 'base16-solarized-dark', 'base16-solarized-light'];
// Current swipe status. Values:
// +1: bufferlist open, nicklist closed
// 0: bufferlist closed, nicklist closed
// -1: bufferlist closed, nicklist open
$scope.swipeStatus = 1;
// Initialise all our settings, this needs to include all settings // Initialise all our settings, this needs to include all settings
// or else they won't be saved to the localStorage. // or else they won't be saved to the localStorage.
@ -33,21 +47,21 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
'savepassword': false, 'savepassword': false,
'autoconnect': false, 'autoconnect': false,
'nonicklist': utils.isMobileUi(), 'nonicklist': utils.isMobileUi(),
'alwaysnicklist': false, // only significant on mobile
'noembed': true, 'noembed': true,
'onlyUnread': false, 'onlyUnread': false,
'hotlistsync': true, 'hotlistsync': true,
'orderbyserver': true, 'orderbyserver': true,
'useFavico': true, 'useFavico': !utils.isCordova(),
'showtimestamp': true,
'showtimestampSeconds': false,
'soundnotification': true, 'soundnotification': true,
'fontsize': '14px', 'fontsize': '14px',
'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'), 'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'),
'readlineBindings': false, 'readlineBindings': false,
'enableJSEmoji': (utils.isMobileUi() ? false : true), 'enableJSEmoji': !utils.isMobileUi(),
'enableMathjax': false, 'enableMathjax': false,
'enableQuickKeys': true,
'customCSS': '', 'customCSS': '',
'hideTLSinfo': false, "currentlyViewedBuffers":{},
}); });
$scope.settings = settings; $scope.settings = settings;
@ -55,22 +69,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$log.debug($rootScope.$$watchersCount); $log.debug($rootScope.$$watchersCount);
}; };
$scope.isinstalled = (function() {
// Check for firefox & app installed
if (navigator.mozApps !== undefined) {
navigator.mozApps.getSelf().onsuccess = function _onAppReady(evt) {
var app = evt.target.result;
if (app) {
return true;
} else {
return false;
}
};
} else {
return false;
}
}());
// Detect page visibility attributes // Detect page visibility attributes
(function() { (function() {
@ -105,6 +103,18 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
} }
})(); })();
// Show a TLS warning if GB was loaded over an unencrypted connection,
// except for local instances (testing, cordova, or electron)
$scope.show_tls_warning = (window.location.protocol !== "https:") &&
(["localhost", "127.0.0.1", "::1"].indexOf(window.location.hostname) === -1) &&
!window.is_electron && !utils.isCordova();
if (window.is_electron) {
// Use packaged emojione sprite in the electron app
emojione.imageType = 'svg';
emojione.sprites = true;
emojione.imagePathSVGSprites = './3rdparty/emojione.sprites.svg';
}
$rootScope.isWindowFocused = function() { $rootScope.isWindowFocused = function() {
if (typeof $scope.documentHidden === "undefined") { if (typeof $scope.documentHidden === "undefined") {
@ -124,6 +134,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
var buffer = models.getActiveBuffer(); var buffer = models.getActiveBuffer();
// This can also be triggered before connecting to the relay, check for null (not undefined!) // This can also be triggered before connecting to the relay, check for null (not undefined!)
if (buffer !== null) { if (buffer !== null) {
var server = models.getServerForBuffer(buffer);
server.unread -= (buffer.unread + buffer.notification);
buffer.unread = 0; buffer.unread = 0;
buffer.notification = 0; buffer.notification = 0;
@ -141,11 +153,9 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.$on('activeBufferChanged', function(event, unreadSum) { $rootScope.$on('activeBufferChanged', function(event, unreadSum) {
var ab = models.getActiveBuffer(); var ab = models.getActiveBuffer();
// Discard surplus lines. This is done *before* lines are fetched because that saves us the effort of special handling for the // Discard unread lines above 2 screenfuls. We can click through to get more if needs be
// case where a buffer is opened for the first time ;) // This is to keep GB responsive when loading buffers which have seen a lot of traffic. See issue #859
var minRetainUnread = ab.lines.length - unreadSum + 5; // do not discard unread lines and keep 5 additional lines for context var linesToRemove = ab.lines.length - (2 * $scope.lines_per_screen + 10);
var surplusLines = ab.lines.length - (2 * $scope.lines_per_screen + 10); // retain up to 2*(screenful + 10) + 10 lines because magic numbers
var linesToRemove = Math.min(minRetainUnread, surplusLines);
if (linesToRemove > 0) { if (linesToRemove > 0) {
ab.lines.splice(0, linesToRemove); // remove the lines from the buffer ab.lines.splice(0, linesToRemove); // remove the lines from the buffer
@ -160,13 +170,13 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Send a request for the nicklist if it hasn't been loaded yet // Send a request for the nicklist if it hasn't been loaded yet
if (!ab.nicklistRequested()) { if (!ab.nicklistRequested()) {
connection.requestNicklist(ab.id, function() { connection.requestNicklist(ab.id, function() {
$scope.showNicklist = $scope.updateShowNicklist(); $scope.updateShowNicklist();
// Scroll after nicklist has been loaded, as it may break long lines // Scroll after nicklist has been loaded, as it may break long lines
$rootScope.scrollWithBuffer(true); $rootScope.scrollWithBuffer(true);
}); });
} else { } else {
// Check if we should show nicklist or not // Check if we should show nicklist or not
$scope.showNicklist = $scope.updateShowNicklist(); $scope.updateShowNicklist();
} }
if (ab.requestedLines < $scope.lines_per_screen) { if (ab.requestedLines < $scope.lines_per_screen) {
@ -216,6 +226,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Clear search term on buffer change // Clear search term on buffer change
$scope.search = ''; $scope.search = '';
$scope.search_placeholder = 'Search';
if (!utils.isMobileUi()) { if (!utils.isMobileUi()) {
// This needs to happen asynchronously to prevent the enter key handler // This needs to happen asynchronously to prevent the enter key handler
@ -232,7 +243,9 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
} }
}); });
$rootScope.favico = new Favico({animation: 'none'}); if (!utils.isCordova()) {
$rootScope.favico = new Favico({animation: 'none'});
}
$scope.notifications = notifications.unreadCount('notification'); $scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread'); $scope.unread = notifications.unreadCount('unread');
@ -241,7 +254,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$scope.notifications = notifications.unreadCount('notification'); $scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread'); $scope.unread = notifications.unreadCount('unread');
if (settings.useFavico && $rootScope.favico) { if (!utils.isCordova() && settings.useFavico && $rootScope.favico) {
notifications.updateFavico(); notifications.updateFavico();
} }
}); });
@ -250,12 +263,18 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Reset title // Reset title
$rootScope.pageTitle = ''; $rootScope.pageTitle = '';
$rootScope.notificationStatus = ''; $rootScope.notificationStatus = '';
// cancel outstanding notifications (incl cordova)
notifications.cancelAll(); notifications.cancelAll();
if (window.plugin !== undefined && window.plugin.notification !== undefined && window.plugin.notification.local !== undefined) {
window.plugin.notification.local.cancelAll();
}
models.reinitialize(); models.reinitialize();
$rootScope.$emit('notificationChanged'); $rootScope.$emit('notificationChanged');
$scope.connectbutton = 'Connect'; $scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right'; $scope.connectbuttonicon = 'glyphicon-chevron-right';
bufferResume.reset();
}); });
$scope.connectbutton = 'Connect'; $scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right'; $scope.connectbuttonicon = 'glyphicon-chevron-right';
@ -314,6 +333,44 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
return document.getElementById('content').getAttribute('sidebar-state') === 'visible'; return document.getElementById('content').getAttribute('sidebar-state') === 'visible';
}; };
$scope.swipeRight = function() {
// Depending on swipe state
if ($scope.swipeStatus === 1) {
/* do nothing */
} else if ($scope.swipeStatus === 0) {
$scope.showSidebar(); // updates swipe status to 1
} else if ($scope.swipeStatus === -1) {
// hide nicklist
$scope.swipeStatus = 0;
$scope.updateShowNicklist();
} else {
console.log("Weird swipe status:", $scope.swipeStatus);
$scope.swipeStatus = 0; // restore sanity
$scope.updateShowNicklist();
$scope.hideSidebar();
}
};
$rootScope.swipeLeft = function() {
// Depending on swipe state, ...
if ($scope.swipeStatus === 1) {
$scope.hideSidebar(); // updates swipe status to 0
} else if ($scope.swipeStatus === 0) {
// show nicklist
$scope.swipeStatus = -1;
if (!$scope.updateShowNicklist()) {
$scope.swipeStatus = 0;
}
} else if ($scope.swipeStatus === -1) {
/* do nothing */
} else {
console.log("Weird swipe status:", $scope.swipeStatus);
$scope.swipeStatus = 0; // restore sanity
$scope.updateShowNicklist();
$scope.hideSidebar();
}
};
$scope.showSidebar = function() { $scope.showSidebar = function() {
document.getElementById('sidebar').setAttribute('data-state', 'visible'); document.getElementById('sidebar').setAttribute('data-state', 'visible');
document.getElementById('content').setAttribute('sidebar-state', 'visible'); document.getElementById('content').setAttribute('sidebar-state', 'visible');
@ -323,14 +380,18 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$timeout(function(){elem.blur();}); $timeout(function(){elem.blur();});
}); });
} }
$scope.swipeStatus = 1;
}; };
$rootScope.hideSidebar = function() { $rootScope.hideSidebar = function() {
if (utils.isMobileUi()) { if (utils.isMobileUi()) {
// make sure nicklist is hidden
document.getElementById('sidebar').setAttribute('data-state', 'hidden'); document.getElementById('sidebar').setAttribute('data-state', 'hidden');
document.getElementById('content').setAttribute('sidebar-state', 'hidden'); document.getElementById('content').setAttribute('sidebar-state', 'hidden');
} }
$scope.swipeStatus = 0;
}; };
settings.addCallback('autoconnect', function(autoconnect) { settings.addCallback('autoconnect', function(autoconnect) {
if (autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) { if (autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) {
$scope.connect(); $scope.connect();
@ -348,61 +409,53 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
} }
}; };
// Open and close panels while on mobile devices through swiping // Watch model and update channel sorting when it changes
$scope.openNick = function() { var set_filter_predicate = function(orderbyserver) {
if (utils.isMobileUi()) { if ($rootScope.showJumpKeys) {
if (settings.nonicklist) { $rootScope.predicate = '$jumpKey';
settings.nonicklist = false; } else if (orderbyserver) {
} $rootScope.predicate = 'serverSortKey';
} else {
$rootScope.predicate = 'number';
} }
}; };
settings.addCallback('orderbyserver', set_filter_predicate);
$scope.closeNick = function() { // convenience wrapper for jump keys
if (utils.isMobileUi()) { $rootScope.refresh_filter_predicate = function() {
if (!settings.nonicklist) { set_filter_predicate(settings.orderbyserver);
settings.nonicklist = true;
}
}
}; };
// Watch model and update channel sorting when it changes
settings.addCallback('orderbyserver', function(orderbyserver) {
$rootScope.predicate = orderbyserver ? 'serverSortKey' : 'number';
});
settings.addCallback('useFavico', function(useFavico) { settings.addCallback('useFavico', function(useFavico) {
// this check is necessary as this is called on page load, too // this check is necessary as this is called on page load, too
if (!$rootScope.connected) { if (!$rootScope.connected) {
return; return;
} }
if (utils.isCordova()) {
return; // cordova doesn't have a favicon
}
if (useFavico) { if (useFavico) {
notifications.updateFavico(); notifications.updateFavico();
} else { } else {
$rootScope.favico.reset(); $rootScope.favico.reset();
notifications.updateBadge('');
} }
}); });
// To prevent unnecessary loading times for users who don't // To prevent unnecessary loading times for users who don't
// want MathJax, load it only if the setting is enabled. // want LaTeX math, load it only if the setting is enabled.
// This also fires when the page is loaded if enabled. // This also fires when the page is loaded if enabled.
// Note that this says MathJax but we switched to KaTeX
settings.addCallback('enableMathjax', function(enabled) { settings.addCallback('enableMathjax', function(enabled) {
if (enabled && !$rootScope.mathjax_init) { // no latex math support for cordova right now
if (!utils.isCordova() && enabled && !$rootScope.mathjax_init) {
// Load MathJax only once // Load MathJax only once
$rootScope.mathjax_init = true; $rootScope.mathjax_init = true;
(function () {
var head = document.getElementsByTagName("head")[0], script; utils.inject_css("https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css");
script = document.createElement("script"); utils.inject_script("https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js");
script.type = "text/x-mathjax-config"; utils.inject_script("https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/contrib/auto-render.min.js");
script[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({\n" +
" tex2jax: { inlineMath: [['$$','$$'], ['\\\\(','\\\\)']], displayMath: [['\\\\[','\\\\]']] },\n" +
"});";
head.appendChild(script);
script = document.createElement("script");
script.type = "text/javascript";
script.src = "//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML";
head.appendChild(script);
})();
} }
}); });
@ -416,14 +469,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
} }
// Load new theme // Load new theme
(function() { utils.inject_css("css/themes/" + theme + ".css", "themeCSS");
var elem = document.createElement("link");
elem.rel = "stylesheet";
elem.href = "css/themes/" + theme + ".css";
elem.media = "screen";
elem.id = "themeCSS";
document.getElementsByTagName("head")[0].appendChild(elem);
})();
}); });
settings.addCallback('customCSS', function(css) { settings.addCallback('customCSS', function(css) {
@ -451,6 +497,15 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}); });
// Update font size when changed // Update font size when changed
settings.addCallback('fontsize', function(fontsize) { settings.addCallback('fontsize', function(fontsize) {
if (typeof(fontsize) === "number") {
// settings module recognizes a fontsize without unit it as a number
// and converts, we need to convert back
fontsize = fontsize.toString();
}
// If no unit is specified, it should be pixels
if (fontsize.match(/^[0-9]+$/)) {
fontsize += 'px';
}
utils.changeClassStyle('favorite-font', 'fontSize', fontsize); utils.changeClassStyle('favorite-font', 'fontSize', fontsize);
}); });
@ -521,6 +576,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Wrap in a condition so we save ourselves the $apply if nothing changes (50ms or more) // Wrap in a condition so we save ourselves the $apply if nothing changes (50ms or more)
if ($scope.wasMobileUi && !utils.isMobileUi()) { if ($scope.wasMobileUi && !utils.isMobileUi()) {
$scope.showSidebar(); $scope.showSidebar();
$scope.updateShowNicklist();
} }
$scope.wasMobileUi = utils.isMobileUi(); $scope.wasMobileUi = utils.isMobileUi();
$scope.calculateNumLines(); $scope.calculateNumLines();
@ -611,6 +667,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$scope.disconnect = function() { $scope.disconnect = function() {
$scope.connectbutton = 'Connect'; $scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right'; $scope.connectbuttonicon = 'glyphicon-chevron-right';
bufferResume.reset();
connection.disconnect(); connection.disconnect();
}; };
$scope.reconnect = function() { $scope.reconnect = function() {
@ -618,30 +675,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
connection.attemptReconnect(bufferId, 3000); connection.attemptReconnect(bufferId, 3000);
}; };
//XXX this is a bit out of place here, either move up to the rest of the firefox install code or remove
$scope.install = function() {
if (navigator.mozApps !== undefined) {
// Find absolute url with trailing '/' or '/index.html' removed
var base_url = location.protocol + '//' + location.host +
location.pathname.replace(/\/(index\.html)?$/, '');
var request = navigator.mozApps.install(base_url + '/manifest.webapp');
request.onsuccess = function () {
$scope.isinstalled = true;
// Save the App object that is returned
var appRecord = this.result;
// Start the app.
appRecord.launch();
alert('Installation successful!');
};
request.onerror = function () {
// Display the error information from the DOMError object
alert('Install failed, error: ' + this.error.name);
};
} else {
alert('Sorry. Only supported in Firefox v26+');
}
};
$scope.showModal = function(elementId) { $scope.showModal = function(elementId) {
document.getElementById(elementId).setAttribute('data-state', 'visible'); document.getElementById(elementId).setAttribute('data-state', 'visible');
}; };
@ -685,18 +718,46 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
return true; return true;
} }
// Always show core buffer in the list (issue #438) // Always show core buffer in the list (issue #438)
// Also show server buffers in hierarchical view if (buffer.fullName === "core.weechat") {
if (buffer.fullName === "core.weechat" || (settings.orderbyserver && buffer.type === 'server')) {
return true; return true;
} }
return (buffer.unread > 0 || buffer.notification > 0) && !buffer.hidden;
// In hierarchical view, show server iff it has a buffer with unread messages
if (settings.orderbyserver && buffer.type === 'server') {
return models.getServerForBuffer(buffer).unread > 0;
}
// Always show pinned buffers
if (buffer.pinned) {
return true;
}
return (buffer.unread > 0 && !buffer.hidden) || buffer.notification > 0;
} }
return !buffer.hidden; return !buffer.hidden;
}; };
// filter bufferlist for search or jump key
$rootScope.bufferlistfilter = function(buffer) {
if ($rootScope.showJumpKeys) {
// filter by jump key
if ($rootScope.jumpDecimal === undefined) {
// no digit input yet, show all buffers
return true;
} else {
var min_jumpKey = 10 * $rootScope.jumpDecimal,
max_jumpKey = 10 * ($rootScope.jumpDecimal + 1);
return (min_jumpKey <= buffer.$jumpKey) &&
(buffer.$jumpKey < max_jumpKey);
}
} else {
// filter by buffer name
return buffer.fullName.toLowerCase().indexOf($scope.search.toLowerCase()) !== -1;
}
};
// Watch model and update show setting when it changes // Watch model and update show setting when it changes
settings.addCallback('nonicklist', function() { settings.addCallback('nonicklist', function() {
$scope.showNicklist = $scope.updateShowNicklist(); $scope.updateShowNicklist();
// restore bottom view // restore bottom view
if ($rootScope.connected && $rootScope.bufferBottom) { if ($rootScope.connected && $rootScope.bufferBottom) {
$timeout(function(){ $timeout(function(){
@ -704,23 +765,32 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}, 500); }, 500);
} }
}); });
settings.addCallback('alwaysnicklist', function() {
$scope.updateShowNicklist();
});
$scope.showNicklist = false; $scope.showNicklist = false;
// Utility function that template can use to check if nicklist should // Utility function that template can use to check if nicklist should be
// be displayed for current buffer or not // displayed for current buffer or not is called on buffer switch and
// is called on buffer switch // certain swipe actions. Sets $scope.showNicklist accordingly and returns
// whether the buffer even has a nicklist to show.
$scope.updateShowNicklist = function() { $scope.updateShowNicklist = function() {
var ab = models.getActiveBuffer(); var ab = models.getActiveBuffer();
if (!ab) { // Check whether buffer exists and nicklist is non-empty
if (!ab || ab.isNicklistEmpty()) {
$scope.showNicklist = false;
return false; return false;
} }
// Check if option no nicklist is set // Check if nicklist is disabled in settings (ignored on mobile)
if (settings.nonicklist) { if (!utils.isMobileUi() && settings.nonicklist) {
return false; $scope.showNicklist = false;
return true;
} }
// Check if nicklist is empty // mobile: hide nicklist unless overriden by setting or swipe action
if (ab.isNicklistEmpty()) { if (utils.isMobileUi() && !settings.alwaysnicklist && $scope.swipeStatus !== -1) {
return false; $scope.showNicklist = false;
return true;
} }
$scope.showNicklist = true;
return true; return true;
}; };
@ -740,7 +810,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// No notifications, find first buffer with unread lines instead // No notifications, find first buffer with unread lines instead
for (i in sortedBuffers) { for (i in sortedBuffers) {
buffer = sortedBuffers[i]; buffer = sortedBuffers[i];
if (buffer.unread > 0) { if (buffer.unread > 0 && !buffer.hidden) {
$scope.setActiveBuffer(buffer.id); $scope.setActiveBuffer(buffer.id);
return; return;
} }
@ -755,29 +825,57 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// direction is +1 for next buffer, -1 for previous buffer // direction is +1 for next buffer, -1 for previous buffer
var sortedBuffers = _.sortBy($scope.getBuffers(), $rootScope.predicate); var sortedBuffers = _.sortBy($scope.getBuffers(), $rootScope.predicate);
var activeBuffer = models.getActiveBuffer(); var activeBuffer = models.getActiveBuffer();
var index = sortedBuffers.indexOf(activeBuffer); var index = sortedBuffers.indexOf(activeBuffer) + direction;
if (index >= 0) { var newBuffer;
var newBuffer = sortedBuffers[index + direction];
if (newBuffer) { // look for next non-hidden buffer
$scope.setActiveBuffer(newBuffer.id); while (index >= 0 && index < sortedBuffers.length &&
} (!newBuffer || newBuffer.hidden)) {
newBuffer = sortedBuffers[index];
index += direction;
}
if (!!newBuffer) {
$scope.setActiveBuffer(newBuffer.id);
} }
}; };
$scope.handleSearchBoxKey = function($event) { $scope.handleSearchBoxKey = function($event) {
// Support different browser quirks // Support different browser quirks
var code = $event.keyCode ? $event.keyCode : $event.charCode; var code = $event.keyCode ? $event.keyCode : $event.charCode;
// Handle escape // Handle escape
if (code === 27) { if (code === 27) {
$event.preventDefault(); $event.preventDefault();
$scope.search = ''; $scope.search = '';
} // Handle enter } // Handle enter
else if (code === 13) { else if (code === 13) {
var index;
$event.preventDefault(); $event.preventDefault();
if ($scope.filteredBuffers.length > 0) { if ($scope.filteredBuffers.length > 0) {
$scope.setActiveBuffer($scope.filteredBuffers[0].id); // Go to highlighted buffer if available
// or first one
if ($scope.search_highlight_key) {
index = $scope.search_highlight_key;
} else {
index = 0;
}
$scope.setActiveBuffer($scope.filteredBuffers[index].id);
} }
$scope.search = ''; $scope.search = '';
} // Handle arrow up
else if (code === 38) {
$event.preventDefault();
if ($scope.search_highlight_key && $scope.search_highlight_key > 0) {
$scope.search_highlight_key = $scope.search_highlight_key - 1;
}
} // Handle arrow down and tab
else if (code === 40 || code === 9) {
$event.preventDefault();
$scope.search_highlight_key = $scope.search_highlight_key + 1;
} // Set highlight key to zero on all other keypress
else {
$scope.search_highlight_key = 0;
} }
}; };
@ -811,7 +909,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
if ($rootScope.connected) { if ($rootScope.connected) {
$scope.disconnect(); $scope.disconnect();
} }
$scope.favico.reset();
if (!utils.isCordova()) {
$scope.favico.reset();
}
} }
}; };

@ -3,7 +3,7 @@
var weechat = angular.module('weechat'); var weechat = angular.module('weechat');
weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) { weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', 'bufferResume', function($rootScope, $log, models, plugins, notifications, bufferResume) {
var handleVersionInfo = function(message) { var handleVersionInfo = function(message) {
var content = message.objects[0].content; var content = message.objects[0].content;
@ -161,13 +161,16 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
} }
if (!manually && (!buffer.active || !$rootScope.isWindowFocused())) { if (!manually && (!buffer.active || !$rootScope.isWindowFocused())) {
var server = models.getServerForBuffer(buffer);
if (buffer.notify > 1 && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) { if (buffer.notify > 1 && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) {
buffer.unread++; buffer.unread++;
server.unread++;
$rootScope.$emit('notificationChanged'); $rootScope.$emit('notificationChanged');
} }
if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) { if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) {
buffer.notification++; buffer.notification++;
server.unread++;
notifications.createHighlight(buffer, message); notifications.createHighlight(buffer, message);
$rootScope.$emit('notificationChanged'); $rootScope.$emit('notificationChanged');
} }
@ -186,13 +189,25 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
handleBufferUpdate(buffer, bufferInfos[i]); handleBufferUpdate(buffer, bufferInfos[i]);
} else { } else {
buffer = new models.Buffer(bufferInfos[i]); buffer = new models.Buffer(bufferInfos[i]);
if (buffer.type === 'server') {
models.registerServer(buffer);
} else {
var server = models.getServerForBuffer(buffer);
server.unread += buffer.unread + buffer.notification;
}
models.addBuffer(buffer); models.addBuffer(buffer);
// Switch to first buffer on startup // Switch to first buffer on startup
if (i === 0) { var shouldResume = bufferResume.shouldResume(buffer);
if(shouldResume){
models.setActiveBuffer(buffer.id); models.setActiveBuffer(buffer.id);
} }
} }
} }
// If there was no buffer to autmatically load, go to the first one.
if (!bufferResume.wasAbleToResume()) {
var first = bufferInfos[0].pointers[0];
models.setActiveBuffer(first);
}
}; };
var handleBufferUpdate = function(buffer, message) { var handleBufferUpdate = function(buffer, message) {
@ -208,7 +223,9 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
buffer.number = message.number; buffer.number = message.number;
buffer.hidden = message.hidden; buffer.hidden = message.hidden;
// reset these, hotlist info will arrive shortly // reset unread counts, hotlist info will arrive shortly
var server = models.getServerForBuffer(buffer);
server.unread -= (buffer.unread + buffer.notification);
buffer.notification = 0; buffer.notification = 0;
buffer.unread = 0; buffer.unread = 0;
buffer.lastSeen = -1; buffer.lastSeen = -1;
@ -232,6 +249,12 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
var handleBufferOpened = function(message) { var handleBufferOpened = function(message) {
var bufferMessage = message.objects[0].content[0]; var bufferMessage = message.objects[0].content[0];
var buffer = new models.Buffer(bufferMessage); var buffer = new models.Buffer(bufferMessage);
if (buffer.type === 'server') {
models.registerServer(buffer);
} else {
var server = models.getServerForBuffer(buffer);
server.unread += buffer.unread + buffer.notification;
}
models.addBuffer(buffer); models.addBuffer(buffer);
}; };
@ -271,6 +294,27 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
} }
}; };
var handleBufferMoved = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
var old_number = old.number;
var new_number = obj.number;
_.each(models.getBuffers(), function(buffer) {
if (buffer.number > old_number && buffer.number <= new_number) {
buffer.number -= 1;
}
if (buffer.number < old_number && buffer.number >= new_number) {
buffer.number += 1;
}
});
old.number = new_number;
};
var handleBufferHidden = function(message) { var handleBufferHidden = function(message) {
var obj = message.objects[0].content[0]; var obj = message.objects[0].content[0];
var buffer = obj.pointers[0]; var buffer = obj.pointers[0];
@ -300,13 +344,14 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
old.server = localvars.server; old.server = localvars.server;
old.serverSortKey = old.plugin + "." + old.server + old.serverSortKey = old.plugin + "." + old.server +
(old.type === "server" ? "" : ("." + old.shortName)); (old.type === "server" ? "" : ("." + old.shortName));
old.pinned = localvars.pinned === "true";
} }
}; };
var handleBufferTypeChanged = function(message) { var handleBufferTypeChanged = function(message) {
var obj = message.objects[0].content[0]; var obj = message.objects[0].content[0];
var buffer = obj.pointers[0]; var buffer = obj.pointers[0];
var old = models.getBuffer(buffer); //var old = models.getBuffer(buffer);
// 0 = formatted (normal); 1 = free // 0 = formatted (normal); 1 = free
buffer.bufferType = obj.type; buffer.bufferType = obj.type;
}; };
@ -340,23 +385,47 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
* Handle answers to hotlist request * Handle answers to hotlist request
*/ */
var handleHotlistInfo = function(message) { var handleHotlistInfo = function(message) {
if (message.objects.length === 0) { // Hotlist includes only buffers with unread counts so first we
return; // iterate all our buffers and resets the counts.
_.each(models.getBuffers(), function(buffer) {
buffer.unread = 0;
buffer.notification = 0;
});
_.each(models.getServers(), function(server) {
server.unread = 0;
});
if (message.objects.length > 0) {
var hotlist = message.objects[0].content;
hotlist.forEach(function(l) {
var buffer = models.getBuffer(l.buffer);
// If buffer is active in gb, but not active in WeeChat the
// hotlist in WeeChat will increase but we should ignore that
// in gb.
if (buffer.active) {
return;
}
// 1 is message
buffer.unread = l.count[1];
// 2 is private
// Use += so count[2] or count[3] doesn't overwrite each other
buffer.notification += l.count[2];
// 3 is highlight
// Use += so count[2] or count[3] doesn't overwrite each other
buffer.notification += l.count[3];
/* Since there is unread messages, we can guess
* what the last read line is and update it accordingly
*/
var unreadSum = _.reduce(l.count, function(memo, num) { return memo + num; }, 0);
buffer.lastSeen = buffer.lines.length - 1 - unreadSum;
// update server buffer. Don't incude index 0 -> not unreadSum
models.getServerForBuffer(buffer).unread += l.count[1] + l.count[2] + l.count[3];
});
} }
var hotlist = message.objects[0].content; // the unread badges in the bufferlist doesn't update if we don't do this
hotlist.forEach(function(l) { setTimeout(function() {
var buffer = models.getBuffer(l.buffer); $rootScope.$apply();
// 1 is message $rootScope.$emit('notificationChanged');
buffer.unread += l.count[1];
// 2 is private
buffer.notification += l.count[2];
// 3 is highlight
buffer.notification += l.count[3];
/* Since there is unread messages, we can guess
* what the last read line is and update it accordingly
*/
var unreadSum = _.reduce(l.count, function(memo, num) { return memo + num; }, 0);
buffer.lastSeen = buffer.lines.length - 1 - unreadSum;
}); });
}; };
@ -413,6 +482,7 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
_buffer_localvar_added: handleBufferLocalvarChanged, _buffer_localvar_added: handleBufferLocalvarChanged,
_buffer_localvar_removed: handleBufferLocalvarChanged, _buffer_localvar_removed: handleBufferLocalvarChanged,
_buffer_localvar_changed: handleBufferLocalvarChanged, _buffer_localvar_changed: handleBufferLocalvarChanged,
_buffer_moved: handleBufferMoved,
_buffer_opened: handleBufferOpened, _buffer_opened: handleBufferOpened,
_buffer_title_changed: handleBufferTitleChanged, _buffer_title_changed: handleBufferTitleChanged,
_buffer_type_changed: handleBufferTypeChanged, _buffer_type_changed: handleBufferTypeChanged,

@ -71,7 +71,7 @@ weechat.factory('imgur', ['$rootScope', function($rootScope) {
if( response.data && response.data.link ) { if( response.data && response.data.link ) {
if (callback && typeof(callback) === "function") { if (callback && typeof(callback) === "function") {
callback(response.data.link); callback(response.data.link.replace(/^http:/, "https:"));
} }
} else { } else {

@ -14,7 +14,7 @@ weechat.directive('inputBar', function() {
command: '=command' command: '=command'
}, },
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'imgur', 'models', 'IrcUtils', 'settings', function($rootScope, controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'imgur', 'models', 'IrcUtils', 'settings', 'utils', function($rootScope,
$scope, $scope,
$element, //XXX do we need this? don't seem to be using it $element, //XXX do we need this? don't seem to be using it
$log, $log,
@ -22,11 +22,46 @@ weechat.directive('inputBar', function() {
imgur, imgur,
models, models,
IrcUtils, IrcUtils,
settings) { settings,
utils) {
// E.g. Turn :smile: into the unicode equivalent // Expose utils to be able to check if we're on a mobile UI
$scope.utils = utils;
// Emojify input. E.g. Turn :smile: into the unicode equivalent, but
// don't do replacements in the middle of a word (e.g. std::io::foo)
$scope.inputChanged = function() { $scope.inputChanged = function() {
$scope.command = emojione.shortnameToUnicode($scope.command); var emojiRegex = /^(?:[\uD800-\uDBFF][\uDC00-\uDFFF])+$/, // *only* emoji
changed = false, // whether a segment was modified
inputNode = $scope.getInputNode(),
caretPos = inputNode.selectionStart,
position = 0; // current position in text
// use capturing group in regex to include whitespace in output array
var segments = $scope.command.split(/(\s+)/);
for (var i = 0; i < segments.length; i ++) {
if (/\s+/.test(segments[i]) || emojiRegex.test(segments[i])) {
// ignore whitespace and emoji-only segments
position += segments[i].length;
continue;
}
// emojify segment
var emojified = emojione.shortnameToUnicode(segments[i]);
if (emojiRegex.test(emojified)) {
// If result consists *only* of emoji, adjust caret
// position and replace segment with emojified version
caretPos = caretPos - segments[i].length + emojified.length;
segments[i] = emojified;
changed = true;
}
position += segments[i].length;
}
if (changed) { // Only re-assemble if something changed
$scope.command = segments.join('');
setTimeout(function() {
inputNode.setSelectionRange(caretPos, caretPos);
});
}
}; };
/* /*
@ -54,8 +89,11 @@ weechat.directive('inputBar', function() {
var input = $scope.command || ''; var input = $scope.command || '';
// complete nick // complete nick
var completion_suffix = models.wconfig['weechat.completion.nick_completer'];
var add_space = models.wconfig['weechat.completion.nick_add_space'];
var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate, var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate,
activeBuffer.getNicklistByTime(), ':'); activeBuffer.getNicklistByTime(),
completion_suffix, add_space);
// remember iteration candidate // remember iteration candidate
$scope.iterCandidate = nickComp.iterCandidate; $scope.iterCandidate = nickComp.iterCandidate;
@ -170,7 +208,12 @@ weechat.directive('inputBar', function() {
}; };
//XXX THIS DOES NOT BELONG HERE! //XXX THIS DOES NOT BELONG HERE!
$rootScope.addMention = function(prefix) { $rootScope.addMention = function(bufferline) {
if (!bufferline.showHiddenBrackets) {
// the line is a notice or action or something else that doesn't belong
return;
}
var prefix = bufferline.prefix;
// Extract nick from bufferline prefix // Extract nick from bufferline prefix
var nick = prefix[prefix.length - 1].text; var nick = prefix[prefix.length - 1].text;
@ -223,9 +266,12 @@ weechat.directive('inputBar', function() {
// Support different browser quirks // Support different browser quirks
var code = $event.keyCode ? $event.keyCode : $event.charCode; var code = $event.keyCode ? $event.keyCode : $event.charCode;
// A KeyboardEvent property representing the physical key that was pressed, ignoring the keyboard layout and ignoring whether any modifier keys were active.
// Not supported in Edge or Safari at the time of writing this, but supported in Firefox and Chrome.
var key = $event.code;
// Safari doesn't implement DOM 3 input events yet as of 8.0.6 // Safari doesn't implement DOM 3 input events yet as of 8.0.6
var altg = $event.getModifierState ? $event.getModifierState('AltGraph') : false; var altg = $event.getModifierState ? $event.getModifierState('AltGraph') : false;
// Mac OSX behaves differntly for altgr, so we check for that // Mac OSX behaves differntly for altgr, so we check for that
if (altg) { if (altg) {
// We don't handle any anything with altgr // We don't handle any anything with altgr
@ -239,17 +285,53 @@ weechat.directive('inputBar', function() {
var tmpIterCandidate = $scope.iterCandidate; var tmpIterCandidate = $scope.iterCandidate;
$scope.iterCandidate = null; $scope.iterCandidate = null;
var bufferNumber;
var sortedBuffers;
var filteredBufferNum;
var activeBufferId;
// if Alt+J was pressed last...
if ($rootScope.showJumpKeys) {
var cleanup = function() { // cleanup helper
$rootScope.showJumpKeys = false;
$rootScope.jumpDecimal = undefined;
$scope.$parent.search = '';
$scope.$parent.search_placeholder = 'Search';
$rootScope.refresh_filter_predicate();
};
// ... we expect two digits now
if (!$event.altKey && (code > 47 && code < 58)) {
// first digit
if ($rootScope.jumpDecimal === undefined) {
$rootScope.jumpDecimal = code - 48;
$event.preventDefault();
$scope.$parent.search = $rootScope.jumpDecimal;
$rootScope.refresh_filter_predicate();
// second digit, jump to correct buffer
} else {
bufferNumber = ($rootScope.jumpDecimal * 10) + (code - 48);
$scope.$parent.setActiveBuffer(bufferNumber, '$jumpKey');
$event.preventDefault();
cleanup();
}
} else {
// Not a decimal digit, abort
cleanup();
}
}
// Left Alt+[0-9] -> jump to buffer // Left Alt+[0-9] -> jump to buffer
if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58)) { if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58) && settings.enableQuickKeys) {
if (code === 48) { if (code === 48) {
code = 58; code = 58;
} }
var bufferNumber = code - 48 - 1 ; bufferNumber = code - 48 - 1 ;
var activeBufferId;
// quick select filtered entries // quick select filtered entries
if (($scope.$parent.search.length || $scope.$parent.onlyUnread) && $scope.$parent.filteredBuffers.length) { if (($scope.$parent.search.length || $scope.$parent.onlyUnread) && $scope.$parent.filteredBuffers.length) {
var filteredBufferNum = $scope.$parent.filteredBuffers[bufferNumber]; filteredBufferNum = $scope.$parent.filteredBuffers[bufferNumber];
if (filteredBufferNum !== undefined) { if (filteredBufferNum !== undefined) {
activeBufferId = [filteredBufferNum.number, filteredBufferNum.id]; activeBufferId = [filteredBufferNum.number, filteredBufferNum.id];
} }
@ -257,7 +339,7 @@ weechat.directive('inputBar', function() {
// Map the buffers to only their numbers and IDs so we don't have to // Map the buffers to only their numbers and IDs so we don't have to
// copy the entire (possibly very large) buffer object, and then sort // copy the entire (possibly very large) buffer object, and then sort
// the buffers according to their WeeChat number // the buffers according to their WeeChat number
var sortedBuffers = _.map(models.getBuffers(), function(buffer) { sortedBuffers = _.map(models.getBuffers(), function(buffer) {
return [buffer.number, buffer.id]; return [buffer.number, buffer.id];
}).sort(function(left, right) { }).sort(function(left, right) {
// By default, Array.prototype.sort() sorts alphabetically. // By default, Array.prototype.sort() sorts alphabetically.
@ -311,7 +393,8 @@ weechat.directive('inputBar', function() {
} }
// Alt+< -> switch to previous buffer // Alt+< -> switch to previous buffer
if ($event.altKey && (code === 60 || code === 226)) { // https://w3c.github.io/uievents-code/#code-IntlBackslash
if ($event.altKey && (code === 60 || code === 226 || key === "IntlBackslash")) {
var previousBuffer = models.getPreviousBuffer(); var previousBuffer = models.getPreviousBuffer();
if (previousBuffer) { if (previousBuffer) {
models.setActiveBuffer(previousBuffer.id); models.setActiveBuffer(previousBuffer.id);
@ -353,6 +436,30 @@ weechat.directive('inputBar', function() {
return true; return true;
} }
// Alt-h -> Toggle all as read
if ($event.altKey && !$event.ctrlKey && code === 72) {
var buffers = models.getBuffers();
_.each(buffers, function(buffer) {
buffer.unread = 0;
buffer.notification = 0;
});
var servers = models.getServers();
_.each(servers, function(server) {
server.unread = 0;
});
connection.sendHotlistClearAll();
}
// Alt+J -> Jump to buffer
if ($event.altKey && (code === 106 || code === 74)) {
$event.preventDefault();
// reset search state and show jump keys
$scope.$parent.search = '';
$scope.$parent.search_placeholder = 'Number';
$rootScope.showJumpKeys = true;
return true;
}
var caretPos; var caretPos;
// Arrow up -> go up in history // Arrow up -> go up in history
@ -449,7 +556,7 @@ weechat.directive('inputBar', function() {
// Ctrl-w // Ctrl-w
} else if (code == 87) { } else if (code == 87) {
var trimmedValue = $scope.command.slice(0, caretPos); var trimmedValue = $scope.command.slice(0, caretPos);
var lastSpace = trimmedValue.lastIndexOf(' ') + 1; var lastSpace = trimmedValue.replace(/\s+$/, '').lastIndexOf(' ') + 1;
$scope.command = $scope.command.slice(0, lastSpace) + $scope.command.slice(caretPos, $scope.command.length); $scope.command = $scope.command.slice(0, lastSpace) + $scope.command.slice(caretPos, $scope.command.length);
setTimeout(function() { setTimeout(function() {
inputNode.setSelectionRange(lastSpace, lastSpace); inputNode.setSelectionRange(lastSpace, lastSpace);
@ -462,7 +569,7 @@ weechat.directive('inputBar', function() {
} }
// Alt key down -> display quick key legend // Alt key down -> display quick key legend
if ($event.type === "keydown" && code === 18 && !$event.ctrlKey && !$event.shiftKey) { if ($event.type === "keydown" && code === 18 && !$event.ctrlKey && !$event.shiftKey && settings.enableQuickKeys) {
$rootScope.showQuickKeys = true; $rootScope.showQuickKeys = true;
} }
}; };
@ -483,6 +590,35 @@ weechat.directive('inputBar', function() {
return true; return true;
} }
}; };
$scope.handleCompleteNickButton = function($event) {
$event.preventDefault();
$scope.completeNick();
setTimeout(function() {
$scope.getInputNode().focus();
}, 0);
return true;
};
$scope.inputPasted = function(e) {
if (e.clipboardData && e.clipboardData.files && e.clipboardData.files.length) {
e.stopPropagation();
e.preventDefault();
var sendImageUrl = function(imageUrl) {
if(imageUrl !== undefined && imageUrl !== '') {
$rootScope.insertAtCaret(String(imageUrl));
}
};
for (var i = 0; i < e.clipboardData.files.length; i++) {
imgur.process(e.clipboardData.files[i], sendImageUrl);
}
return false;
}
return true;
};
}] }]
}; };
}); });

@ -44,7 +44,7 @@ IrcUtils.service('IrcUtils', [function() {
var foundNick = null; var foundNick = null;
nickList.some(function(nick) { nickList.some(function(nick) {
if (nick.toLowerCase().search(candidate.toLowerCase()) === 0) { if (nick.toLowerCase().indexOf(candidate.toLowerCase()) === 0) {
// found! // found!
foundNick = nick; foundNick = nick;
return true; return true;
@ -72,7 +72,7 @@ IrcUtils.service('IrcUtils', [function() {
// collect matching nicks // collect matching nicks
for (var i = 0; i < nickList.length; ++i) { for (var i = 0; i < nickList.length; ++i) {
var lcNick = nickList[i].toLowerCase(); var lcNick = nickList[i].toLowerCase();
if (lcNick.search(escapeRegExp(lcIterCandidate)) === 0) { if (lcNick.indexOf(lcIterCandidate) === 0) {
matchingNicks.push(nickList[i]); matchingNicks.push(nickList[i]);
if (lcCurrentNick === lcNick) { if (lcCurrentNick === lcNick) {
at = matchingNicks.length - 1; at = matchingNicks.length - 1;
@ -106,17 +106,20 @@ IrcUtils.service('IrcUtils', [function() {
* @param iterCandidate Current iteration candidate (null if not iterating) * @param iterCandidate Current iteration candidate (null if not iterating)
* @param nickList Array of current nicks * @param nickList Array of current nicks
* @param suf Custom suffix (at least one character, escaped for regex) * @param suf Custom suffix (at least one character, escaped for regex)
* @param addSpace Whether to add a space after nick completion in the middle
* @return Object with following properties: * @return Object with following properties:
* text: new complete replacement text * text: new complete replacement text
* caretPos: new caret position within new text * caretPos: new caret position within new text
* foundNick: completed nick (or null if not possible) * foundNick: completed nick (or null if not possible)
* iterCandidate: current iterating candidate * iterCandidate: current iterating candidate
*/ */
var completeNick = function(text, caretPos, iterCandidate, nickList, suf) { var completeNick = function(text, caretPos, iterCandidate, nickList, suf, addSpace) {
var doIterate = (iterCandidate !== null); var doIterate = (iterCandidate !== null);
if (suf === null) { if (suf === undefined) {
suf = ':'; suf = ':';
} }
// addSpace defaults to true
var addSpaceChar = (addSpace === undefined || addSpace === true) ? ' ' : '';
// new nick list to search in // new nick list to search in
var searchNickList = _ciNickList(nickList); var searchNickList = _ciNickList(nickList);
@ -158,7 +161,7 @@ IrcUtils.service('IrcUtils', [function() {
m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/); m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) { if (m) {
// try completing // try completing
newNick = _completeSingleNick(escapeRegExp(m[1]), searchNickList); newNick = _completeSingleNick(m[1], searchNickList);
if (newNick === null) { if (newNick === null) {
// no match // no match
return ret; return ret;
@ -182,7 +185,7 @@ IrcUtils.service('IrcUtils', [function() {
if (doIterate) { if (doIterate) {
// try iterating // try iterating
newNick = _nextNick(iterCandidate, m[2], searchNickList); newNick = _nextNick(iterCandidate, m[2], searchNickList);
beforeCaret = m[1] + newNick + ' '; beforeCaret = m[1] + newNick + addSpaceChar;
return { return {
text: beforeCaret + afterCaret, text: beforeCaret + afterCaret,
caretPos: beforeCaret.length, caretPos: beforeCaret.length,
@ -204,7 +207,7 @@ IrcUtils.service('IrcUtils', [function() {
// no match // no match
return ret; return ret;
} }
beforeCaret = m[1] + newNick + ' '; beforeCaret = m[1] + newNick + addSpaceChar;
if (afterCaret[0] === ' ') { if (afterCaret[0] === ' ') {
// swallow first space after caret if any // swallow first space after caret if any
afterCaret = afterCaret.substring(1); afterCaret = afterCaret.substring(1);

@ -3,6 +3,42 @@
var ls = angular.module('localStorage',[]); var ls = angular.module('localStorage',[]);
function StoragePolyfil() {
this.storage = Object.create(null);
this.keyIndex = [];
Object.defineProperty(this, "length", {
enumerable: true,
get: function() {
return this.keyIndex.length;
}
});
}
StoragePolyfil.prototype.key = function(idx) {
return this.keyIndex[idx];
};
StoragePolyfil.prototype.getItem = function(key) {
return (key in this.storage) ? this.storage[key] : null;
};
StoragePolyfil.prototype.setItem = function(key, value) {
if (!(key in this.storage)) {
this.keyIndex.push(key);
}
this.storage[key] = value;
};
StoragePolyfil.prototype.clear = function() {
this.storage = Object.create(null);
this.keyIndex = [];
};
StoragePolyfil.prototype.removeItem = function(key) {
if (!(key in storage)) {
return;
}
var at = this.keyIndex.indexOf(key);
this.keyIndex.splice(at, 1);
delete this.storage[key];
};
ls.factory("$store", ["$parse", function($parse){ ls.factory("$store", ["$parse", function($parse){
/** /**
* Global Vars * Global Vars
@ -10,6 +46,15 @@ ls.factory("$store", ["$parse", function($parse){
var storage = (typeof window.localStorage === 'undefined') ? undefined : window.localStorage, var storage = (typeof window.localStorage === 'undefined') ? undefined : window.localStorage,
supported = !(typeof storage == 'undefined' || typeof window.JSON == 'undefined'); supported = !(typeof storage == 'undefined' || typeof window.JSON == 'undefined');
try {
var storageTestKey = "eaf23ffe-6a8f-40a7-892b-4baf22d3ec75";
storage.setItem(storageTestKey, 1);
storage.removeItem(storageTestKey);
} catch (e) {
console.log('Warning: MobileSafari private mode detected. Switching to in-memory storage.');
storage = new StoragePolyfil();
}
if (!supported) { if (!supported) {
console.log('Warning: localStorage is not supported'); console.log('Warning: localStorage is not supported');
} }

@ -7,7 +7,7 @@
var models = angular.module('weechatModels', []); var models = angular.module('weechatModels', []);
models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) { models.service('models', ['$rootScope', '$filter', 'bufferResume', function($rootScope, $filter, bufferResume) {
// WeeChat version // WeeChat version
this.version = null; this.version = null;
@ -95,6 +95,9 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var plugin = message.local_variables.plugin; var plugin = message.local_variables.plugin;
var server = message.local_variables.server; var server = message.local_variables.server;
var pinned = message.local_variables.pinned === "true";
// Server buffers have this "irc.server.freenode" naming schema, which // Server buffers have this "irc.server.freenode" naming schema, which
// messes the sorting up. We need it to be "irc.freenode" instead. // messes the sorting up. We need it to be "irc.freenode" instead.
var serverSortKey = plugin + "." + server + var serverSortKey = plugin + "." + server +
@ -335,7 +338,8 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
getHistoryUp: getHistoryUp, getHistoryUp: getHistoryUp,
getHistoryDown: getHistoryDown, getHistoryDown: getHistoryDown,
isNicklistEmpty: isNicklistEmpty, isNicklistEmpty: isNicklistEmpty,
nicklistRequested: nicklistRequested nicklistRequested: nicklistRequested,
pinned: pinned,
}; };
}; };
@ -347,6 +351,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var buffer = message.buffer; var buffer = message.buffer;
var date = message.date; var date = message.date;
var shortTime = $filter('date')(date, 'HH:mm'); var shortTime = $filter('date')(date, 'HH:mm');
var formattedTime = $filter('date')(date, $rootScope.angularTimeFormat);
var prefix = parseRichText(message.prefix); var prefix = parseRichText(message.prefix);
var tags_array = message.tags_array; var tags_array = message.tags_array;
@ -354,60 +359,84 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var highlight = message.highlight; var highlight = message.highlight;
var content = parseRichText(message.message); var content = parseRichText(message.message);
// only put invisible angle brackets around nicks in normal messages
// (for copying/pasting)
var showHiddenBrackets = (tags_array.indexOf('irc_privmsg') >= 0 &&
tags_array.indexOf('irc_action') === -1);
if (highlight) { if (highlight) {
prefix.forEach(function(textEl) { prefix.forEach(function(textEl) {
textEl.classes.push('highlight'); textEl.classes.push('highlight');
}); });
} }
var prefixtext = "";
for (var pti = 0; pti < prefix.length; ++pti) {
prefixtext += prefix[pti].text;
}
var rtext = ""; var rtext = "";
for (var i = 0; i < content.length; ++i) { for (var i = 0; i < content.length; ++i) {
rtext += content[i].text; rtext += content[i].text;
} }
return { return {
prefix: prefix, prefix: prefix,
content: content, content: content,
date: date, date: date,
shortTime: shortTime, shortTime: shortTime,
formattedTime: formattedTime,
buffer: buffer, buffer: buffer,
tags: tags_array, tags: tags_array,
highlight: highlight, highlight: highlight,
displayed: displayed, displayed: displayed,
text: rtext prefixtext: prefixtext,
text: rtext,
showHiddenBrackets: showHiddenBrackets
}; };
}; };
function nickGetColorClasses(nickMsg, propName) { function nickGetColorClasses(nickMsg, propName) {
var colorClasses = [
'cwf-default'
];
if (propName in nickMsg && nickMsg[propName] && nickMsg[propName].length > 0) { if (propName in nickMsg && nickMsg[propName] && nickMsg[propName].length > 0) {
var color = nickMsg[propName]; var color = nickMsg[propName];
if (color.match(/^weechat/)) { if (color.match(/^weechat/)) {
// color option // color option
var colorName = color.match(/[a-zA-Z0-9_]+$/)[0]; var colorName = color.match(/[a-zA-Z0-9_]+$/)[0];
return [ colorClasses = [
'cof-' + colorName, 'cof-' + colorName,
'cob-' + colorName, 'cob-' + colorName,
'coa-' + colorName 'coa-' + colorName
]; ];
} else if (color.match(/^[a-zA-Z]+$/)) { } else {
// WeeChat color name if (color.match(/^[a-zA-Z]+(:|$)/)) {
return [ // WeeChat color name (foreground)
'cwf-' + color var cwfcolor = color.match(/^[a-zA-Z]+/)[0];
]; colorClasses = [
} else if (color.match(/^[0-9]+$/)) { 'cwf-' + cwfcolor
// extended color ];
return [ } else if (color.match(/^[0-9]+(:|$)/)) {
'cef-' + color // extended color (foreground)
]; var cefcolor = color.match(/^[0-9]+/)[0];
colorClasses = [
'cef-' + cefcolor
];
}
if (color.match(/:[a-zA-Z]+$/)) {
// WeeChat color name (background)
var cwbcolor = color.match(/:[a-zA-Z]+$/)[0].substring(1);
colorClasses.push('cwb-' + cwbcolor);
} else if (color.match(/:[0-9]+$/)) {
// extended color (background)
var cebcolor = color.match(/:[0-9]+$/)[0].substring(1);
colorClasses.push('ceb-' + cebcolor);
}
} }
} }
return colorClasses;
return [
'cwf-default'
];
} }
function nickGetClasses(nickMsg) { function nickGetClasses(nickMsg) {
@ -449,11 +478,26 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
}; };
}; };
this.Server = function() {
var id = 0; // will be set later on
var unread = 0;
return {
id: id,
unread: unread
};
};
var activeBuffer = null; var activeBuffer = null;
var previousBuffer = null; var previousBuffer = null;
this.model = { 'buffers': {} }; this.model = { 'buffers': {}, 'servers': {} };
this.registerServer = function(buffer) {
var key = buffer.plugin + '.' + buffer.server;
this.getServer(key).id = buffer.id;
};
/* /*
* Adds a buffer to the list * Adds a buffer to the list
@ -539,12 +583,15 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var unreadSum = activeBuffer.unread + activeBuffer.notification; var unreadSum = activeBuffer.unread + activeBuffer.notification;
this.getServerForBuffer(activeBuffer).unread -= unreadSum;
activeBuffer.active = true; activeBuffer.active = true;
activeBuffer.unread = 0; activeBuffer.unread = 0;
activeBuffer.notification = 0; activeBuffer.notification = 0;
$rootScope.$emit('activeBufferChanged', unreadSum); $rootScope.$emit('activeBufferChanged', unreadSum);
$rootScope.$emit('notificationChanged'); $rootScope.$emit('notificationChanged');
bufferResume.record(activeBuffer);
return true; return true;
}; };
@ -560,6 +607,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
*/ */
this.reinitialize = function() { this.reinitialize = function() {
this.model.buffers = {}; this.model.buffers = {};
this.model.servers = {};
}; };
/* /*
@ -572,6 +620,35 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
return this.model.buffers[bufferId]; return this.model.buffers[bufferId];
}; };
/*
* Returns the server list
*/
this.getServers = function() {
return this.model.servers;
};
/*
* Returns the server object for a specific key, creating it if it does not exist
* @param key the server key
* @return the server object
*/
this.getServer = function(key) {
if (this.model.servers[key] === undefined) {
this.model.servers[key] = this.Server();
}
return this.model.servers[key];
};
/*
* Returns info on the server buffer for a specific buffer
* @param buffer the buffer
* @return the server object
*/
this.getServerForBuffer = function(buffer) {
var key = buffer.plugin + '.' + buffer.server;
return this.getServer(key);
};
/* /*
* Closes a weechat buffer. Sets the first buffer * Closes a weechat buffer. Sets the first buffer
* as active, if the closing buffer was active before * as active, if the closing buffer was active before

@ -1,6 +1,6 @@
var weechat = angular.module('weechat'); var weechat = angular.module('weechat');
weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', function($rootScope, $log, models, settings) { weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', 'utils', function($rootScope, $log, models, settings, utils) {
var serviceworker = false; var serviceworker = false;
var notifications = []; var notifications = [];
// Ask for permission to display desktop notifications // Ask for permission to display desktop notifications
@ -24,7 +24,8 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
} }
} }
if ('serviceWorker' in navigator) { // Check for serviceWorker support, and also disable serviceWorker if we're running in electron process, since that's just problematic and not necessary, since gb then already is in a separate process
if ('serviceWorker' in navigator && window.is_electron !== 1) {
$log.info('Service Worker is supported'); $log.info('Service Worker is supported');
navigator.serviceWorker.register('serviceworker.js').then(function(reg) { navigator.serviceWorker.register('serviceworker.js').then(function(reg) {
$log.info('Service Worker install:', reg); $log.info('Service Worker install:', reg);
@ -33,6 +34,21 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
$log.info('Service Worker err:', err); $log.info('Service Worker err:', err);
}); });
} }
document.addEventListener('deviceready', function() {
// Add cordova local notification click handler
if (utils.isCordova() && window.cordova.plugins !== undefined && window.cordova.plugins.notification !== undefined &&
window.cordova.plugins.notification.local !== undefined) {
window.cordova.plugins.notification.local.on("click", function (notification) {
// go to buffer
var data = JSON.parse(notification.data);
models.setActiveBuffer(data.buffer);
window.focus();
// clear this notification
window.cordova.plugins.notification.local.clear(notification.id);
});
}
});
}; };
var showNotification = function(buffer, title, body) { var showNotification = function(buffer, title, body) {
@ -65,7 +81,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
toastNotifier.show(toast); toastNotifier.show(toast);
} else { } else if (typeof Notification !== 'undefined') {
var notification = new Notification(title, { var notification = new Notification(title, {
body: body, body: body,
@ -96,6 +112,22 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
delete notifications[this.id]; delete notifications[this.id];
}; };
} else if (utils.isCordova() && window.cordova.plugins !== undefined && window.cordova.plugins.notification !== undefined && window.cordova.plugins.notification.local !== undefined) {
// Cordova local notification
// Calculate notification id from buffer ID
// Needs to be unique number, but we'll only ever have one per buffer
var id = parseInt(buffer.id, 16);
// Cancel previous notification for buffer (if there was one)
window.cordova.plugins.notification.local.clear(id);
// Send new notification
window.cordova.plugins.notification.local.schedule({
id: id,
text: body,
title: title,
data: { buffer: buffer.id } // remember buffer id for when the notification is clicked
});
} }
}; };
@ -134,25 +166,46 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
}; };
var updateFavico = function() { var updateFavico = function() {
if (utils.isCordova()) {
return; // cordova doesn't have a favicon
}
var notifications = unreadCount('notification'); var notifications = unreadCount('notification');
if (notifications > 0) { if (notifications > 0) {
$rootScope.favico.badge(notifications, { $rootScope.favico.badge(notifications, {
bgColor: '#d00', bgColor: '#d00',
textColor: '#fff' textColor: '#fff'
}); });
// Set badge to notifications count
updateBadge(notifications);
} else { } else {
var unread = unreadCount('unread'); var unread = unreadCount('unread');
if (unread === 0) { if (unread === 0) {
$rootScope.favico.reset(); $rootScope.favico.reset();
// Remove badge form app icon
updateBadge('');
} else { } else {
$rootScope.favico.badge(unread, { $rootScope.favico.badge(unread, {
bgColor: '#5CB85C', bgColor: '#5CB85C',
textColor: '#ff0' textColor: '#ff0'
}); });
// Set app badge to "." when only unread and no notifications
updateBadge("•");
} }
} }
}; };
// Update app badge (electron only)
var updateBadge = function(value) {
// Send new value to preloaded global function
// if it exists
if (typeof setElectronBadge === 'function') {
setElectronBadge(value);
}
};
/* Function gets called from bufferLineAdded code if user should be notified */ /* Function gets called from bufferLineAdded code if user should be notified */
var createHighlight = function(buffer, message) { var createHighlight = function(buffer, message) {
var title = ''; var title = '';
@ -182,8 +235,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
showNotification(buffer, title, body); showNotification(buffer, title, body);
if (settings.soundnotification) { if (!utils.isCordova() && settings.soundnotification) {
// TODO fill in a sound file
var audioFile = "assets/audio/sonar"; var audioFile = "assets/audio/sonar";
var soundHTML = '<audio autoplay="autoplay"><source src="' + audioFile + '.ogg" type="audio/ogg" /><source src="' + audioFile + '.mp3" type="audio/mpeg" /></audio>'; var soundHTML = '<audio autoplay="autoplay"><source src="' + audioFile + '.ogg" type="audio/ogg" /><source src="' + audioFile + '.mp3" type="audio/mpeg" /></audio>';
document.getElementById("soundNotification").innerHTML = soundHTML; document.getElementById("soundNotification").innerHTML = soundHTML;
@ -203,6 +255,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
requestNotificationPermission: requestNotificationPermission, requestNotificationPermission: requestNotificationPermission,
updateTitle: updateTitle, updateTitle: updateTitle,
updateFavico: updateFavico, updateFavico: updateFavico,
updateBadge: updateBadge,
createHighlight: createHighlight, createHighlight: createHighlight,
cancelAll: cancelAll, cancelAll: cancelAll,
unreadCount: unreadCount unreadCount: unreadCount

@ -33,7 +33,7 @@ var urlRegexp = /(?:(?:https?|ftp):\/\/|www\.|ftp\.)\S*[^\s.;,(){}<>]/g;
var UrlPlugin = function(name, urlCallback) { var UrlPlugin = function(name, urlCallback) {
return { return {
contentForMessage: function(message) { contentForMessage: function(message) {
var urls = message.match(urlRegexp); var urls = _.uniq(message.match(urlRegexp));
var content = []; var content = [];
for (var i = 0; urls && i < urls.length; i++) { for (var i = 0; urls && i < urls.length; i++) {
@ -181,22 +181,25 @@ plugins.factory('userPlugins', function() {
* *
*/ */
var spotifyPlugin = new Plugin('Spotify track', function(message) { var spotifyPlugin = new Plugin('Spotify music', function(message) {
var content = []; var content = [];
var addMatch = function(match) { var addMatch = function(match) {
for (var i = 0; match && i < match.length; i++) { for (var i = 0; match && i < match.length; i++) {
var id = match[i].substr(match[i].length - 22, match[i].length);
var element = angular.element('<iframe></iframe>') var element = angular.element('<iframe></iframe>')
.attr('src', '//embed.spotify.com/?uri=spotify:track:' + id) .attr('src', '//embed.spotify.com/?uri=' + match[i])
.attr('width', '300') .attr('width', '350')
.attr('height', '80') .attr('height', '80')
.attr('frameborder', '0') .attr('frameborder', '0')
.attr('allowtransparency', 'true'); .attr('allowtransparency', 'true');
content.push(element.prop('outerHTML')); content.push(element.prop('outerHTML'));
} }
}; };
addMatch(message.match(/spotify:track:([a-zA-Z-0-9]{22})/g)); addMatch(message.match(/spotify:track:[a-zA-Z-0-9]{22}/g));
addMatch(message.match(/open.spotify.com\/track\/([a-zA-Z-0-9]{22})/g)); addMatch(message.match(/spotify:artist:[a-zA-Z-0-9]{22}/g));
addMatch(message.match(/spotify:user:\w+:playlist:[a-zA-Z-0-9]{22}/g));
addMatch(message.match(/(open|play)\.spotify\.com\/track\/[a-zA-Z-0-9]{22}/g));
addMatch(message.match(/(open|play)\.spotify\.com\/artist\/[a-zA-Z-0-9]{22}/g));
addMatch(message.match(/(open|play)\.spotify\.com\/user\/\w+\/playlist\/[a-zA-Z-0-9]{22}/g));
return content; return content;
}); });
@ -206,18 +209,18 @@ plugins.factory('userPlugins', function() {
* See: https://developers.google.com/youtube/player_parameters * See: https://developers.google.com/youtube/player_parameters
*/ */
var youtubePlugin = new UrlPlugin('YouTube video', function(url) { var youtubePlugin = new UrlPlugin('YouTube video', function(url) {
var regex = /(?:youtube.com|youtu.be)\/(?:v\/|embed\/|watch(?:\?v=|\/))?([a-zA-Z0-9-]+)/i, var regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i,
match = url.match(regex); match = url.match(regex);
if (match){ if (match){
var token = match[1], var token = match[1],
embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0", embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0",
element = angular.element('<iframe></iframe>') element = angular.element('<iframe></iframe>')
.attr('src', embedurl) .attr('src', embedurl)
.attr('width', '560') .attr('width', '560')
.attr('height', '315') .attr('height', '315')
.attr('frameborder', '0') .attr('frameborder', '0')
.attr('allowfullscreen', 'true'); .attr('allowfullscreen', 'true');
return element.prop('outerHTML'); return element.prop('outerHTML');
} }
}); });
@ -228,9 +231,9 @@ plugins.factory('userPlugins', function() {
* See: http://www.dailymotion.com/doc/api/player.html * See: http://www.dailymotion.com/doc/api/player.html
*/ */
var dailymotionPlugin = new Plugin('Dailymotion video', function(message) { var dailymotionPlugin = new Plugin('Dailymotion video', function(message) {
var rPath = /dailymotion.com\/.*video\/([^_?# ]+)/; var rPath = /dailymotion\.com\/.*video\/([^_?# ]+)/;
var rAnchor = /dailymotion.com\/.*#video=([^_& ]+)/; var rAnchor = /dailymotion\.com\/.*#video=([^_& ]+)/;
var rShorten = /dai.ly\/([^_?# ]+)/; var rShorten = /dai\.ly\/([^_?# ]+)/;
var match = message.match(rPath) || message.match(rAnchor) || message.match(rShorten); var match = message.match(rPath) || message.match(rAnchor) || message.match(rShorten);
if (match) { if (match) {
@ -251,8 +254,8 @@ plugins.factory('userPlugins', function() {
* AlloCine Embedded Player * AlloCine Embedded Player
*/ */
var allocinePlugin = new Plugin('AlloCine video', function(message) { var allocinePlugin = new Plugin('AlloCine video', function(message) {
var rVideokast = /allocine.fr\/videokast\/video-(\d+)/; var rVideokast = /allocine\.fr\/videokast\/video-(\d+)/;
var rCmedia = /allocine.fr\/.*cmedia=(\d+)/; var rCmedia = /allocine\.fr\/.*cmedia=(\d+)/;
var match = message.match(rVideokast) || message.match(rCmedia); var match = message.match(rVideokast) || message.match(rCmedia);
if (match) { if (match) {
@ -273,13 +276,13 @@ plugins.factory('userPlugins', function() {
* Image Preview * Image Preview
*/ */
var imagePlugin = new UrlPlugin('image', function(url) { var imagePlugin = new UrlPlugin('image', function(url) {
if (url.match(/\.(png|gif|jpg|jpeg)(:(small|medium|large))?\b/i)) { if (url.match(/\.(bmp|gif|ico|jpeg|jpg|png|svg|svgz|tif|tiff|webp)(:(small|medium|large))?\b/i)) {
/* A fukung.net URL may end by an image extension but is not a direct link. */ /* A fukung.net URL may end by an image extension but is not a direct link. */
if (url.indexOf("^https?://fukung.net/v/") != -1) { if (url.indexOf("^https?://fukung.net/v/") != -1) {
url = url.replace(/.*\//, "http://media.fukung.net/imgs/"); url = url.replace(/.*\//, "http://media.fukung.net/imgs/");
} else if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) { } else if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) {
// remove protocol specification to load over https if used by g-b // imgur: always use https. avoids mixed content warnings
url = url.replace(/http:/, ""); url = url.replace(/^http:/, "https:");
} else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\//i)) { } else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\//i)) {
// Dropbox requires a get parameter, dl=1 // Dropbox requires a get parameter, dl=1
var dbox_url = document.createElement("a"); var dbox_url = document.createElement("a");
@ -314,10 +317,10 @@ plugins.factory('userPlugins', function() {
}); });
/* /*
* audio Preview * Audio Preview
*/ */
var audioPlugin = new UrlPlugin('audio', function(url) { var audioPlugin = new UrlPlugin('audio', function(url) {
if (url.match(/\.(mp3|ogg|wav)\b/i)) { if (url.match(/\.(flac|m4a|mid|midi|mp3|oga|ogg|ogx|opus|pls|spx|wav|wave|wma)\b/i)) {
return function() { return function() {
var element = this.getElement(); var element = this.getElement();
var aelement = angular.element('<audio controls></audio>') var aelement = angular.element('<audio controls></audio>')
@ -332,21 +335,34 @@ plugins.factory('userPlugins', function() {
/* /*
* mp4 video Preview * Video Preview
*/ */
var videoPlugin = new UrlPlugin('video', function(url) { var videoPlugin = new UrlPlugin('video', function(url) {
if (url.match(/\.(mp4|webm|ogv|gifv)\b/i)) { if (url.match(/\.(3gp|avi|flv|gifv|mkv|mp4|ogv|webm|wmv)\b/i)) {
if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) { if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) {
// remove protocol specification to load over https if used by g-b // imgur: always use https. avoids mixed content warnings
url = url.replace(/\.(gifv)\b/i, ".webm"); url = url.replace(/^http:/, "https:");
} }
return function() { return function() {
var element = this.getElement(); var element = this.getElement(), src;
var velement = angular.element('<video autoplay loop muted></video>') var velement = angular.element('<video autoplay controls loop muted></video>')
.addClass('embed') .addClass('embed')
.attr('width', '560') .attr('width', '560');
.append(angular.element('<source></source>') // imgur doesn't always have webm for gifv so add sources for webm and mp4
.attr('src', url)); if (url.match(/^https:\/\/(i\.)?imgur\.com\/.*\.gifv/i)) {
src = angular.element('<source></source>')
.attr('src', url.replace(/\.gifv\b/i, ".webm"))
.attr('type', 'video/webm');
velement.append(src);
src = angular.element('<source></source>')
.attr('src', url.replace(/\.gifv\b/i, ".mp4"))
.attr('type', 'video/mp4');
velement.append(src);
} else {
src = angular.element('<source></source>')
.attr('src', url);
velement.append(src);
}
element.innerHTML = velement.prop('outerHTML'); element.innerHTML = velement.prop('outerHTML');
}; };
} }
@ -359,7 +375,7 @@ plugins.factory('userPlugins', function() {
var cloudmusicPlugin = new UrlPlugin('cloud music', function(url) { var cloudmusicPlugin = new UrlPlugin('cloud music', function(url) {
/* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */ /* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */
var element; var element;
if (url.match(/^https?:\/\/soundcloud.com\//)) { if (url.match(/^https?:\/\/soundcloud\.com\//)) {
element = angular.element('<iframe></iframe>') element = angular.element('<iframe></iframe>')
.attr('width', '100%') .attr('width', '100%')
.attr('height', '120') .attr('height', '120')
@ -370,7 +386,7 @@ plugins.factory('userPlugins', function() {
} }
/* MixCloud */ /* MixCloud */
if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud.com\//)) { if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud\.com\//)) {
element = angular.element('<iframe></iframe>') element = angular.element('<iframe></iframe>')
.attr('width', '480') .attr('width', '480')
.attr('height', '60') .attr('height', '60')
@ -400,7 +416,7 @@ plugins.factory('userPlugins', function() {
* Asciinema plugin * Asciinema plugin
*/ */
var asciinemaPlugin = new UrlPlugin('ascii cast', function(url) { var asciinemaPlugin = new UrlPlugin('ascii cast', function(url) {
var regexp = /^https?:\/\/(?:www\.)?asciinema.org\/a\/(\d+)/i, var regexp = /^https?:\/\/(?:www\.)?asciinema\.org\/a\/([0-9a-z]+)/i,
match = url.match(regexp); match = url.match(regexp);
if (match) { if (match) {
var id = match[1]; var id = match[1];
@ -435,11 +451,12 @@ plugins.factory('userPlugins', function() {
// Embed GitHub gists // Embed GitHub gists
var gistPlugin = new UrlPlugin('Gist', function(url) { var gistPlugin = new UrlPlugin('Gist', function(url) {
var regexp = /^https:\/\/gist\.github.com\/[^.?]+/i; // ignore trailing slashes and anchors
var regexp = /^(https:\/\/gist\.github\.com\/(?:.*?))[\/]?(?:\#.*)?$/i;
var match = url.match(regexp); var match = url.match(regexp);
if (match) { if (match) {
// get the URL from the match to trim away pseudo file endings and request parameters // get the URL from the match to trim away pseudo file endings and request parameters
url = match[0] + '.json'; url = match[1] + '.json';
// load gist asynchronously -- return a function here // load gist asynchronously -- return a function here
return function() { return function() {
var element = this.getElement(); var element = this.getElement();
@ -455,12 +472,26 @@ plugins.factory('userPlugins', function() {
} }
}); });
var pastebinPlugin = new UrlPlugin('Pastebin', function(url) {
var regexp = /^https?:\/\/pastebin\.com\/(raw\/)?([^.?]+)/i;
var match = url.match(regexp);
if (match) {
var id = match[2],
embedurl = "https://pastebin.com/embed_iframe/" + id,
element = angular.element('<iframe></iframe>')
.attr('src', embedurl)
.attr('width', '100%')
.attr('height', '480');
return element.prop('outerHTML');
}
});
/* match giphy links and display the assocaited gif images /* match giphy links and display the assocaited gif images
* sample input: http://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy * sample input: http://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy
* sample output: https://media.giphy.com/media/feqkVgjJpYtjy/giphy.gif * sample output: https://media.giphy.com/media/feqkVgjJpYtjy/giphy.gif
*/ */
var giphyPlugin = new UrlPlugin('Giphy', function(url) { var giphyPlugin = new UrlPlugin('Giphy', function(url) {
var regex = /^https?:\/\/giphy.com\/gifs\/.*-(.*)\/?/i; var regex = /^https?:\/\/giphy\.com\/gifs\/.*-(.*)\/?/i;
// on match, id will contain the entire url in [0] and the giphy id in [1] // on match, id will contain the entire url in [0] and the giphy id in [1]
var id = url.match(regex); var id = url.match(regex);
if (id) { if (id) {
@ -496,7 +527,7 @@ plugins.factory('userPlugins', function() {
// The script tag needs to be generated manually or the browser won't load it // The script tag needs to be generated manually or the browser won't load it
var scriptElem = document.createElement('script'); var scriptElem = document.createElement('script');
// Hardcoding the URL here, I don't suppose it's going to change anytime soon // Hardcoding the URL here, I don't suppose it's going to change anytime soon
scriptElem.src = "//platform.twitter.com/widgets.js"; scriptElem.src = "https://platform.twitter.com/widgets.js";
element.appendChild(scriptElem); element.appendChild(scriptElem);
}); });
}; };
@ -507,7 +538,7 @@ plugins.factory('userPlugins', function() {
* Vine plugin * Vine plugin
*/ */
var vinePlugin = new UrlPlugin('Vine', function (url) { var vinePlugin = new UrlPlugin('Vine', function (url) {
var regexp = /^https?:\/\/(www\.)?vine.co\/v\/([a-zA-Z0-9]+)(\/.*)?/i, var regexp = /^https?:\/\/(www\.)?vine\.co\/v\/([a-zA-Z0-9]+)(\/.*)?/i,
match = url.match(regexp); match = url.match(regexp);
if (match) { if (match) {
var id = match[2], embedurl = "https://vine.co/v/" + id + "/embed/simple?audio=1"; var id = match[2], embedurl = "https://vine.co/v/" + id + "/embed/simple?audio=1";
@ -517,12 +548,29 @@ plugins.factory('userPlugins', function() {
.attr('width', '600') .attr('width', '600')
.attr('height', '600') .attr('height', '600')
.attr('frameborder', '0'); .attr('frameborder', '0');
return element.prop('outerHTML') + '<script async src="//platform.vine.co/static/scripts/embed.js" charset="utf-8"></script>'; return element.prop('outerHTML') + '<script async src="https://platform.vine.co/static/scripts/embed.js" charset="utf-8"></script>';
}
});
/*
* Streamable Embedded Player
*/
var streamablePlugin = new UrlPlugin('Streamable video', function(url) {
var regexp = /^https?:\/\/streamable\.com\/s?\/?(.+)/,
match = url.match(regexp);
if (match) {
var id = match[1], embedurl = 'https://streamable.com/s/' + id;
var element = angular.element('<iframe></iframe>')
.attr('src', embedurl)
.attr('width', '480')
.attr('height', '270')
.attr('frameborder', '0');
return element.prop('outerHTML');
} }
}); });
return { return {
plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, giphyPlugin, tweetPlugin, vinePlugin] plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, pastebinPlugin, giphyPlugin, tweetPlugin, vinePlugin, streamablePlugin]
}; };

@ -21,9 +21,37 @@ weechat.factory('utils', function() {
return (document.body.clientWidth < mobile_cutoff); return (document.body.clientWidth < mobile_cutoff);
}; };
var isCordova = function() {
return window.cordova !== undefined;
};
// Inject a javascript (used by KaTeX)
var inject_script = function(script_url) {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = script_url;
var head = document.getElementsByTagName("head")[0];
head.appendChild(script);
};
// Inject a stylesheet (used by KaTeX and theme switching)
var inject_css = function(css_url, id) {
var elem = document.createElement("link");
elem.rel = "stylesheet";
elem.href = css_url;
if (id)
elem.id = id;
var head = document.getElementsByTagName("head")[0];
head.appendChild(elem);
};
return { return {
changeClassStyle: changeClassStyle, changeClassStyle: changeClassStyle,
getClassStyle: getClassStyle, getClassStyle: getClassStyle,
isMobileUi: isMobileUi isMobileUi: isMobileUi,
isCordova: isCordova,
inject_script: inject_script,
inject_css: inject_css,
}; };
}); });

@ -1,7 +1,7 @@
{ {
"name": "Glowing Bear", "name": "Glowing Bear",
"description": "WeeChat Web frontend", "description": "WeeChat Web frontend",
"version": "0.6.0", "version": "0.7.0",
"manifest_version": 2, "manifest_version": 2,
"icons": { "icons": {
"32": "assets/img/favicon.png", "32": "assets/img/favicon.png",
@ -9,11 +9,11 @@
}, },
"app": { "app": {
"urls": [ "urls": [
"http://glowing-bear.github.io/glowing-bear/" "https://www.glowing-bear.org/"
], ],
"launch": { "launch": {
"container": "panel", "container": "panel",
"web_url": "http://glowing-bear.github.io/glowing-bear/" "web_url": "https://www.glowing-bear.org/"
} }
}, },
"permissions": [ "permissions": [

@ -1,12 +1,12 @@
{ {
"name": "Glowing Bear", "name": "Glowing Bear",
"description": "WeeChat Web frontend", "description": "WeeChat Web frontend",
"launch_path": "/glowing-bear/index.html", "launch_path": "/index.html",
"icons": { "icons": {
"128": "/glowing-bear/assets/img/glowing_bear_128x128.png", "128": "/assets/img/glowing_bear_128x128.png",
"60": "/glowing-bear/assets/img/glowing_bear_60x60.png", "60": "/assets/img/glowing_bear_60x60.png",
"90": "/glowing-bear/assets/img/glowing_bear_90x90.png", "90": "/assets/img/glowing_bear_90x90.png",
"32": "/glowing-bear/assets/img/favicon.png" "32": "/assets/img/favicon.png"
}, },
"installs_allowed_from": [ "installs_allowed_from": [
"*" "*"
@ -25,5 +25,5 @@
"desktop-notification":{} "desktop-notification":{}
}, },
"default_locale": "en", "default_locale": "en",
"version": "0.6.0" "version": "0.7.0"
} }

@ -1,27 +1,28 @@
{ {
"name": "glowing-bear", "name": "glowing-bear",
"private": true, "private": true,
"version": "0.6.0", "version": "0.7.0",
"description": "A web client for Weechat", "description": "A web client for Weechat",
"repository": "https://github.com/glowing-bear/glowing-bear", "repository": "https://github.com/glowing-bear/glowing-bear",
"main": "electron-main.js",
"license": "GPLv3", "license": "GPLv3",
"devDependencies": { "devDependencies": {
"bower": "^1.3.1", "bower": "^1.8",
"http-server": "^0.6.1", "electron-packager": "^12.2.0",
"jasmine-core": "^2.4.1", "http-server": "^0.11",
"jshint": "^2.5.2", "jasmine-core": "^3.1",
"karma": "~0.13", "jshint": "^2.9.6",
"karma-jasmine": "^0.3.6", "karma": "^3.1.1",
"karma-junit-reporter": "^0.2.2", "karma-jasmine": "~1.1",
"karma-phantomjs-launcher": "^0.2.1", "karma-junit-reporter": "^1.2",
"phantomjs": "^1.9.19", "karma-phantomjs-launcher": "^1.0.0",
"protractor": "~0.20.1", "protractor": "^5.4.1",
"shelljs": "^0.2.6", "shelljs": "^0.8.0",
"uglify-js": "^2.4" "uglify-js": "^3.4.9"
}, },
"scripts": { "scripts": {
"postinstall": "bower install", "postinstall": "bower install -p",
"minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/settings.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/file-change.js js/imgur-drop-directive.js js/whenscrolled-directive.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js js/imgur.js -c -m --screw-ie8 -o min.js --source-map min.map", "minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/settings.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/file-change.js js/imgur-drop-directive.js js/whenscrolled-directive.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/bufferResume.js js/plugins.js js/imgur.js -c -m --screw-ie8 -o min.js --source-map url='min.js.map'",
"prestart": "npm install", "prestart": "npm install",
"start": "http-server -a localhost -p 8000", "start": "http-server -a localhost -p 8000",
"pretest": "npm install", "pretest": "npm install",
@ -31,6 +32,11 @@
"update-webdriver": "webdriver-manager update", "update-webdriver": "webdriver-manager update",
"preprotractor": "npm run update-webdriver", "preprotractor": "npm run update-webdriver",
"protractor": "protractor test/protractor-conf.js", "protractor": "protractor test/protractor-conf.js",
"premake-local": "bower install --dev",
"make-local": "make -f electron.makefile uselocal",
"build-electron-windows": "make -f electron.makefile build-electron-windows",
"build-electron-darwin": "make -f electron.makefile build-electron-darwin",
"build-electron-linux": "make -f electron.makefile build-electron-linux",
"update-index-async": "node -e \"require('shelljs/global'); sed('-i', /\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/, '//@@NG_LOADER_START@@\\n' + cat('app/bower_components/angular-loader/angular-loader.min.js') + '\\n//@@NG_LOADER_END@@', 'app/index-async.html');\"" "update-index-async": "node -e \"require('shelljs/global'); sed('-i', /\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/, '//@@NG_LOADER_START@@\\n' + cat('app/bower_components/angular-loader/angular-loader.min.js') + '\\n//@@NG_LOADER_END@@', 'app/index-async.html');\""
} }
} }

@ -9,6 +9,7 @@ module.exports = function(config){
'bower_components/angular-mocks/angular-mocks.js', 'bower_components/angular-mocks/angular-mocks.js',
'bower_components/angular-sanitize/angular-sanitize.js', 'bower_components/angular-sanitize/angular-sanitize.js',
'bower_components/angular-touch/angular-touch.js', 'bower_components/angular-touch/angular-touch.js',
'bower_components/underscore/underscore.js',
'js/localstorage.js', 'js/localstorage.js',
'js/weechat.js', 'js/weechat.js',
'js/irc-utils.js', 'js/irc-utils.js',
@ -22,6 +23,7 @@ module.exports = function(config){
'js/plugin-directive.js', 'js/plugin-directive.js',
'js/websockets.js', 'js/websockets.js',
'js/models.js', 'js/models.js',
'js/bufferResume.js',
'js/plugins.js', 'js/plugins.js',
'test/unit/**/*.js' 'test/unit/**/*.js'
], ],

@ -29,12 +29,16 @@ describe('filter', function() {
$provide.value('version', 'TEST_VER'); $provide.value('version', 'TEST_VER');
})); }));
it('should recognize spotify tracks', inject(function(plugins) { it('should recognize spotify links', inject(function(plugins) {
expectTheseMessagesToContain([ expectTheseMessagesToContain([
'spotify:track:6JEK0CvvjDjjMUBFoXShNZ', 'spotify:track:6JEK0CvvjDjjMUBFoXShNZ',
'https://open.spotify.com/track/6JEK0CvvjDjjMUBFoXShNZ' 'spotify:user:lorenzhs:playlist:18aXdzQ4Ar1p019OSICtu4',
'spotify:artist:0L5fC7Ogm2YwgqVCRcF1bT',
'https://open.spotify.com/track/6JEK0CvvjDjjMUBFoXShNZ',
'https://open.spotify.com/user/lorenzhs/playlist/18aXdzQ4Ar1p019OSICtu4',
'https://open.spotify.com/artist/0L5fC7Ogm2YwgqVCRcF1bT'
], ],
'Spotify track', 'Spotify music',
plugins); plugins);
})); }));
@ -139,6 +143,15 @@ describe('filter', function() {
plugins); plugins);
})); }));
it('should recognize pastebins', inject(function(plugins) {
expectTheseMessagesToContain([
'http://pastebin.com/Wn3TetSE',
'http://pastebin.com/raw/Wn3TetSE',
],
'Pastebin',
plugins);
}));
it('should recognize giphy gifs', inject(function(plugins) { it('should recognize giphy gifs', inject(function(plugins) {
expectTheseMessagesToContain([ expectTheseMessagesToContain([
'https://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy/', 'https://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy/',

@ -17,17 +17,20 @@
"src": "assets/img/glowing_bear_128x128.png", "src": "assets/img/glowing_bear_128x128.png",
"sizes": "128x128" "sizes": "128x128"
}], }],
"scope": "/glowing-bear/", "scope": "/",
"start_url": "/glowing-bear/index.html", "start_url": "/index.html",
"display": "standalone", "display": "standalone",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"theme_color": "#181818", "theme_color": "#181818",
"background_color": "#333", "background_color": "#333",
"prefer_related_applications": "false", "prefer_related_applications": false,
"chrome_related_applications": [{ "chrome_related_applications": [
"platform": "web" {
}, { "platform": "web"
"platform": "android", },
"location": "https://play.google.com/store/apps/details?id=com.glowing_bear" {
}] "platform": "android",
"location": "https://play.google.com/store/apps/details?id=com.glowing_bear"
}
]
} }

Loading…
Cancel
Save