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. 31
      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. 258
      index.html
  26. 56
      js/bufferResume.js
  27. 151
      js/connection.js
  28. 32
      js/filters.js
  29. 341
      js/glowingbear.js
  30. 86
      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. 115
      js/models.js
  36. 63
      js/notifications.js
  37. 124
      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. 15
      webapp.manifest.json

4
.gitignore vendored

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,22 +1,28 @@
# 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
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
/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)
<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
@ -35,8 +41,9 @@ Glowing Bear uses WeeChat directly as its backend through the relay plugin. This
## 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).
- *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).
- *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 "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
@ -55,11 +62,9 @@ python -m http.server
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.
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/).
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.
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
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.
@ -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.
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",
"description": "A webclient for WeeChat",
"version": "0.6.0",
"version": "0.7.0",
"homepage": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3",
"private": true,
"dependencies": {
"angular": "1.4.x",
"angular-route": "1.4.x",
"angular-sanitize": "1.4.x",
"angular-touch": "1.4.x",
"angular-loader": "1.4.x",
"angular-mocks": "1.4.x",
"html5-boilerplate": "~4.3.0"
}
"angular": "1.7.x",
"angular-route": "1.7.x",
"angular-sanitize": "1.7.x",
"angular-touch": "1.7.x",
"angular-loader": "1.7.x",
"angular-mocks": "1.7.x",
"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;
}
.input-group-addon, .input-group-btn {
.input-group-btn {
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 {
border-radius: 0;
}
@ -89,6 +108,12 @@ input[type=text], input[type=password], #sendMessage {
margin-bottom: 5px !important;
}
.btn-complete-nick {
position: relative;
overflow: hidden;
cursor: pointer;
}
.btn-send-image {
position: relative;
overflow: hidden;
@ -155,6 +180,7 @@ input[type=text], input[type=password], #sendMessage {
position: fixed;
left: 145px; /* sidebar */
overflow: hidden;
width: 100%; /* for title modal click area */
}
#topbar .actions {
@ -197,7 +223,9 @@ input[type=text], input[type=password], #sendMessage {
overflow-y: auto;
overflow-x: hidden;
padding-top: 35px; /* topbar */
padding-right: env(safe-area-inset-right);
padding-bottom: 1px; /* need to force a padding here */
padding-left: env(safe-area-inset-left);
font-size: smaller;
transition:0.2s ease-in-out;
z-index: 2;
@ -216,7 +244,13 @@ input[type=text], input[type=password], #sendMessage {
display:block!important;
}
#sidebar .badge {
#sidebar .buffer .badge {
display: none;
}
#sidebar .buffer.unread .badge, #sidebar .buffer.notification .badge {
display: inline-block;
border-radius: 0;
margin-right: -10px;
padding: 4px 7px;
@ -239,9 +273,9 @@ input[type=text], input[type=password], #sendMessage {
overflow-x: hidden;
right: 0;
top: 0;
padding-top: 39px;
margin-top: 39px;
padding-left: 5px;
padding-bottom: 35px;
padding-bottom: 44px;
z-index: 2;
}
#nicklist ul {
@ -285,6 +319,12 @@ input[type=text], input[type=password], #sendMessage {
color: #222;
}
.nav-pills > li.highlight > a, .nav-pills > li.highlight > a span {
text-decoration: none;
color: #fff;
background: #428BCA;
}
.nav-pills > li > a {
display: block;
overflow: hidden;
@ -295,6 +335,7 @@ input[type=text], input[type=password], #sendMessage {
.content {
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 {
@ -350,6 +391,7 @@ td.time {
bottom: 0;
height: 35px;
width: 100%;
margin-bottom: env(safe-area-inset-bottom); /* margin for home-indicator on iPhone X */
-webkit-transition:0.2s ease-in-out all;
transition:0.2s ease-in-out all;
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 {
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;
}
#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 {
z-index: 1000;
height: 100%;
@ -749,6 +810,7 @@ img.emojione {
right: 60px;
text-align: center;
font-size: 18px;
width: initial;
}
#topbar .brand img {
@ -777,6 +839,10 @@ img.emojione {
bottom: 0px;
}
.content[sidebar-state=visible] #nicklist {
display: none;
}
.navbar-fixed-bottom {
margin: 0;
}
@ -789,6 +855,11 @@ img.emojione {
font-size: 14px;
}
.nav-pills li.buffer {
min-height: 30px;
max-height: 30px;
}
.nav-pills li a {
padding: 10px 15px;
}
@ -850,3 +921,8 @@ img.emojione {
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;
}
.nav-pills > li.highlight > a, .nav-pills > li.highlight > a span {
color: #fff;
background: #428BCA;
}
tr.bufferline:hover {
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;
}
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;
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;
@ -155,6 +161,15 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-se
border-bottom-color: #121212;
}
button.close {
color: #ddd;
opacity: 1;
}
button.close:hover {
color: #ddd;
}
/****************************/
/* 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);
}
.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;
}
.nav-pills > li.highlight > a, .nav-pills > li.highlight > a span {
color: #fff;
background: #428BCA;
}
.btn-complete-nick,
.btn-send,
.btn-send-image, {
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);
}
.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>
<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>
<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">
<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)">

@ -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>
<html ng-app="weechat" ng-cloak>
<html ng-app="weechat">
<head>
<meta charset="utf-8">
<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="mobile-web-app-capable" content="yes">
<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 -->
<link rel="manifest" href="webapp.manifest.json">
<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="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 href="css/glowingbear.css" rel="stylesheet" media="screen">
<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.4.8/angular-route.min.js" integrity="sha384-fQQcs0/yvL0uyyzpXoTKfcQl5e9GYh7GKIft35qSjfKXSILYNI6YZOM0Ju94DY+/" 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.4.8/angular-touch.min.js" integrity="sha384-bnrVwYH8/uQCvK9n+xYQKdf1xtgSNHBYcy0djCofRUPvAt93iEhBfHlngRP/aXsg" 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/emojione/2.1.0/lib/js/emojione.min.js" integrity="sha384-pJb7FFLYTcgO7KbgirAXNIHFIKzywqq4LIcWx9cavPapYWdCH5mcYptrkpHHEkH1" 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.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.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.7.5/angular-touch.min.js" integrity="sha256-4IS2pHNTST2Jl6dSzNsERpYleiQi1r4L2MLPElG8LZ4=" 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.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="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>
<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>
</div>
<div ng-hide="connected" class="container">
@ -39,20 +39,36 @@
<span>Glowing Bear</span>
<small>WeeChat web frontend</small>
</h2>
<div class="alert alert-info alert-dismissable" ng-show="!settings.hideTLSinfo">
<a href="#" class="close" ng-click="settings.hideTLSinfo=true;" aria-label="close">&times;</a>
<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">
<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>
<div class="alert alert-danger" ng-show="errorMessage" ng-cloak>
<strong>Connection error</strong> The client was unable to connect to the WeeChat relay
</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!
</div>
<div class="alert alert-danger" ng-show="securityError">
<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.
<div class="alert alert-danger" ng-show="securityError" ng-cloak>
<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 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-heading">
<h4 class="panel-title">
@ -61,7 +77,7 @@
</a>
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse in">
<div id="collapseOne" class="panel-collapse collapse">
<div class="panel-body">
<form class="form-signin" role="form">
<div class="form-group">
@ -78,7 +94,7 @@
</div>
<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">
<div class="alert alert-danger" ng-show="passwordError">
<div class="alert alert-danger" ng-show="passwordError" ng-cloak>
Error: wrong password
</div>
@ -97,11 +113,11 @@
<div class="checkbox">
<label class="control-label" for="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>
</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>
</div>
</div>
@ -110,68 +126,72 @@
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
Usage instructions
Getting Started
</a>
</h4>
</div>
<div id="collapseTwo" class="panel-collapse collapse in">
<div id="collapseTwo" class="panel-collapse collapse">
<div class="panel-body">
<h3>Configuring the relay</h3>
<div>To start using glowing bear, please enable the relay plugin in your WeeChat client:
<p><span class="label label-danger">WeeChat version 0.4.2 or higher is required—we recommend at least 1.0.</p>
<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>
/set relay.network.password yourpassword
/relay add weechat {{ settings.port || 9001 }}
/set relay.network.password y0ur_StRonG-pa$sw0rd:of*choice
/relay sslcertkey
/relay add ssl.weechat {{ settings.port || 9001 }}
</pre>
<span class="label label-danger">WeeChat version 0.4.2 or higher is required.</span><br>
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.
Connection settings, including your password, are saved locally in your own browser between sessions.
<br>
<h3>Shortcuts</h3>
<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>
</div>
</div>
</div>
<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:
<ul>
<li><kbd>ALT-n</kbd>: Toggle nicklist</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-↑/↓</kbd>: Switch to buffer above/below</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-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>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>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>
</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 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">
<h4 class="panel-title">
<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>
</h4>
</div>
<div id="collapseFour" class="panel-collapse collapse in">
<div id="collapseFour" class="panel-collapse collapse">
<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>
<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><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>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></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>
<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><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>
<p>Firefox used to support apps, but this was removed from Firefox. There's nothing we can do about it. Sorry!</p>
<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>
</div>
@ -202,16 +226,16 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</a>
</h4>
</div>
<div id="collapseFive" class="panel-collapse collapse in">
<div id="collapseFive" class="panel-collapse collapse">
<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>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 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 class="brand">
<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>
</div>
<div class="title" title="{{activeBuffer().rtitle}}">
<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="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>
<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':{rel:'noopener noreferrer'} | DOMfilter:'irclinky')"></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 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>
</div>
</div>
<div id="sidebar" data-state="visible" ng-swipe-left="hideSidebar()" class="vertical-line">
<ul class="nav nav-pills nav-stacked" ng-class="{'indented': (predicate === 'serverSortKey'), 'showquickkeys': showQuickKeys}">
<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, 'showjumpkeys': showJumpKeys}">
<li class="bufferfilter">
<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>
</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="#">
<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-jump-key">{{ ("0" + buffer.$jumpKey).slice(-2) }}</span>
<span class="buffername">{{ buffer.trimmedName || buffer.fullName }}</span>
</a>
</li>
</ul>
</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="closeNick()" class="vertical-line-left">
<div id="nicklist" ng-if="showNicklist" ng-swipe-right="swipeRight()" ng-swipe-disable-mouse class="vertical-line-left">
<ul class="nicklistgroup list-unstyled" ng-repeat="group in nicklist">
<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>
</li>
</ul>
</div>
<table ng-class="{'notimestamp':!settings.showtimestamp,'notimestampseconds':!settings.showtimestampSeconds}">
<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>
<tbody>
<tr class="bufferline">
<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">
<td class="time">
<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>
</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"><!--
--><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>
</tr>
<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 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>
<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>
@ -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-header">
<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>
<p>Settings will be stored in your browser.</p>
</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">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.showtimestamp">
Show timestamps
</label>
</div>
</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
<input type="checkbox" ng-model="settings.noembed">
Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden regardless of this choice</span>
</label>
</div>
</form>
</li>
</ul>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.noembed">
Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden regardless of this choice</span>
<input type="checkbox" ng-model="settings.hotlistsync">
Mark messages as read in WeeChat
</label>
</div>
</form>
</li>
<li>
<li class="mobile">
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.hotlistsync">
Mark messages as read in WeeChat
<input type="checkbox" ng-model="settings.alwaysnicklist">
Always show nicklist
</label>
</div>
</form>
</li>
<li>
<li class="desktop">
<form class="form-inline" role="form">
<div class="checkbox">
<label>
@ -472,6 +498,16 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</div>
</form>
</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>
</div>
<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-header">
<button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button>
<h4 class="modal-title">Channel topic</h4>
<p ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></p>
<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':{rel:'noopener noreferrer'} | DOMfilter:'irclinky')"></p>
</div>
<div class="modal-footer">
<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 reconnectTimer;
// Global connection lock to prevent multiple connections from being opened
var locked = false;
// Takes care of the connection and websocket hooks
var connect = function (host, port, passwd, ssl, noCompression, successCallback, failCallback) {
$rootScope.passwordError = false;
@ -32,6 +35,8 @@ weechat.factory('connection',
// Helper methods for initialization commands
var _initializeConnection = function(passwd) {
// Escape comma in password (#937)
passwd = passwd.replace(',', '\\,');
// This is not the proper way to do this.
// WeeChat does not send a confirmation for the init.
// 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
// a version command. If it fails, it means the we
@ -90,14 +193,37 @@ weechat.factory('connection',
_requestHotlist().then(function(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();
$log.info("Connected to relay");
$rootScope.connected = true;
if (successCallback) {
successCallback();
}
},
function() {
handleWrongPassword();
@ -120,6 +246,7 @@ weechat.factory('connection',
*/
$log.info("Disconnected from relay");
$rootScope.$emit('relayDisconnect');
locked = false;
if ($rootScope.userdisconnect || !$rootScope.waseverconnected) {
handleClose(evt);
$rootScope.userdisconnect = false;
@ -154,6 +281,7 @@ weechat.factory('connection',
* the relay.
*/
$log.error("Relay error", evt);
locked = false; // release connection lock
$rootScope.lastError = Date.now();
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 {
ngWebsockets.connect(url,
protocol,
@ -173,6 +308,7 @@ weechat.factory('connection',
'onerror': onerror
});
} catch(e) {
locked = false;
$log.debug("Websocket caught DOMException:", e);
$rootScope.lastError = Date.now();
$rootScope.errorMessage = true;
@ -250,6 +386,7 @@ weechat.factory('connection',
// The connection can time out on its own
ngWebsockets.failCallbacks('disconnection');
$rootScope.connected = false;
locked = false; // release the connection lock
$rootScope.$emit('relayDisconnect');
$rootScope.$apply();
});
@ -289,6 +426,10 @@ weechat.factory('connection',
}
};
var sendHotlistClearAll = function() {
sendMessage("/input hotlist_clear");
};
var requestNicklist = function(bufferId, callback) {
// Prevent requesting nicklist for all buffers if bufferId is invalid
if (!bufferId) {
@ -306,7 +447,7 @@ weechat.factory('connection',
});
};
var fetchConfValue = function(name) {
var fetchConfValue = function(name, callback) {
ngWebsockets.send(
weeChat.Protocol.formatInfolist({
name: "option",
@ -315,6 +456,9 @@ weechat.factory('connection',
})
).then(function(i) {
handlers.handleConfValue(i);
if (callback !== undefined) {
callback();
}
});
};
@ -379,6 +523,7 @@ weechat.factory('connection',
sendMessage: sendMessage,
sendCoreCommand: sendCoreCommand,
sendHotlistClear: sendHotlistClear,
sendHotlistClearAll: sendHotlistClearAll,
fetchMoreLines: fetchMoreLines,
requestNicklist: requestNicklist,
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 () {
return function (obj, $scope) {
if (!$scope) { return obj; }
@ -151,6 +162,11 @@ weechat.filter('getBufferQuickKeys', function () {
return left[0] - right[0] || left[1] - right[1];
}).forEach(function(info, keyIdx) {
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;
@ -175,15 +191,23 @@ weechat.filter('emojify', function() {
};
});
weechat.filter('mathjax', function() {
weechat.filter('latexmath', function() {
return function(text, selector, enabled) {
if (!enabled || typeof(MathJax) === "undefined") {
if (!enabled || typeof(katex) === "undefined") {
return text;
}
if (text.indexOf("$$") != -1 || text.indexOf("\\[") != -1 || text.indexOf("\\(") != -1) {
// contains math
// contains math -> delayed rendering
setTimeout(function() {
var math = document.querySelector(selector);
MathJax.Hub.Queue(["Typeset",MathJax.Hub,math]);
renderMathInElement(math, {
delimiters: [
{left: "$$", right: "$$", display: false},
{left: "\\[", right: "\\]", display: true},
{left: "\\(", right: "\\)", display: false}
]
});
});
}
return text;

@ -1,7 +1,14 @@
(function() {
'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
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',
function ($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, bufferResume, connection, notifications, utils, settings)
{
window.openBuffer = function(channel) {
$scope.openBuffer(channel);
@ -21,7 +29,13 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
};
$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
// or else they won't be saved to the localStorage.
@ -33,21 +47,21 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
'savepassword': false,
'autoconnect': false,
'nonicklist': utils.isMobileUi(),
'alwaysnicklist': false, // only significant on mobile
'noembed': true,
'onlyUnread': false,
'hotlistsync': true,
'orderbyserver': true,
'useFavico': true,
'showtimestamp': true,
'showtimestampSeconds': false,
'useFavico': !utils.isCordova(),
'soundnotification': true,
'fontsize': '14px',
'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'),
'readlineBindings': false,
'enableJSEmoji': (utils.isMobileUi() ? false : true),
'enableJSEmoji': !utils.isMobileUi(),
'enableMathjax': false,
'enableQuickKeys': true,
'customCSS': '',
'hideTLSinfo': false,
"currentlyViewedBuffers":{},
});
$scope.settings = settings;
@ -55,22 +69,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$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
(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() {
if (typeof $scope.documentHidden === "undefined") {
@ -124,6 +134,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
var buffer = models.getActiveBuffer();
// This can also be triggered before connecting to the relay, check for null (not undefined!)
if (buffer !== null) {
var server = models.getServerForBuffer(buffer);
server.unread -= (buffer.unread + buffer.notification);
buffer.unread = 0;
buffer.notification = 0;
@ -141,11 +153,9 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.$on('activeBufferChanged', function(event, unreadSum) {
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
// case where a buffer is opened for the first time ;)
var minRetainUnread = ab.lines.length - unreadSum + 5; // do not discard unread lines and keep 5 additional lines for context
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);
// Discard unread lines above 2 screenfuls. We can click through to get more if needs be
// This is to keep GB responsive when loading buffers which have seen a lot of traffic. See issue #859
var linesToRemove = ab.lines.length - (2 * $scope.lines_per_screen + 10);
if (linesToRemove > 0) {
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
if (!ab.nicklistRequested()) {
connection.requestNicklist(ab.id, function() {
$scope.showNicklist = $scope.updateShowNicklist();
$scope.updateShowNicklist();
// Scroll after nicklist has been loaded, as it may break long lines
$rootScope.scrollWithBuffer(true);
});
} else {
// Check if we should show nicklist or not
$scope.showNicklist = $scope.updateShowNicklist();
$scope.updateShowNicklist();
}
if (ab.requestedLines < $scope.lines_per_screen) {
@ -216,6 +226,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Clear search term on buffer change
$scope.search = '';
$scope.search_placeholder = 'Search';
if (!utils.isMobileUi()) {
// This needs to happen asynchronously to prevent the enter key handler
@ -232,7 +243,9 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}
});
if (!utils.isCordova()) {
$rootScope.favico = new Favico({animation: 'none'});
}
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
@ -241,7 +254,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
if (settings.useFavico && $rootScope.favico) {
if (!utils.isCordova() && settings.useFavico && $rootScope.favico) {
notifications.updateFavico();
}
});
@ -250,12 +263,18 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Reset title
$rootScope.pageTitle = '';
$rootScope.notificationStatus = '';
// cancel outstanding notifications (incl cordova)
notifications.cancelAll();
if (window.plugin !== undefined && window.plugin.notification !== undefined && window.plugin.notification.local !== undefined) {
window.plugin.notification.local.cancelAll();
}
models.reinitialize();
$rootScope.$emit('notificationChanged');
$scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
bufferResume.reset();
});
$scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
@ -314,6 +333,44 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
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() {
document.getElementById('sidebar').setAttribute('data-state', 'visible');
document.getElementById('content').setAttribute('sidebar-state', 'visible');
@ -323,14 +380,18 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$timeout(function(){elem.blur();});
});
}
$scope.swipeStatus = 1;
};
$rootScope.hideSidebar = function() {
if (utils.isMobileUi()) {
// make sure nicklist is hidden
document.getElementById('sidebar').setAttribute('data-state', 'hidden');
document.getElementById('content').setAttribute('sidebar-state', 'hidden');
}
$scope.swipeStatus = 0;
};
settings.addCallback('autoconnect', function(autoconnect) {
if (autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) {
$scope.connect();
@ -348,61 +409,53 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}
};
// Open and close panels while on mobile devices through swiping
$scope.openNick = function() {
if (utils.isMobileUi()) {
if (settings.nonicklist) {
settings.nonicklist = false;
}
// Watch model and update channel sorting when it changes
var set_filter_predicate = function(orderbyserver) {
if ($rootScope.showJumpKeys) {
$rootScope.predicate = '$jumpKey';
} else if (orderbyserver) {
$rootScope.predicate = 'serverSortKey';
} else {
$rootScope.predicate = 'number';
}
};
$scope.closeNick = function() {
if (utils.isMobileUi()) {
if (!settings.nonicklist) {
settings.nonicklist = true;
}
}
settings.addCallback('orderbyserver', set_filter_predicate);
// convenience wrapper for jump keys
$rootScope.refresh_filter_predicate = function() {
set_filter_predicate(settings.orderbyserver);
};
// Watch model and update channel sorting when it changes
settings.addCallback('orderbyserver', function(orderbyserver) {
$rootScope.predicate = orderbyserver ? 'serverSortKey' : 'number';
});
settings.addCallback('useFavico', function(useFavico) {
// this check is necessary as this is called on page load, too
if (!$rootScope.connected) {
return;
}
if (utils.isCordova()) {
return; // cordova doesn't have a favicon
}
if (useFavico) {
notifications.updateFavico();
} else {
$rootScope.favico.reset();
notifications.updateBadge('');
}
});
// 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.
// Note that this says MathJax but we switched to KaTeX
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
$rootScope.mathjax_init = true;
(function () {
var head = document.getElementsByTagName("head")[0], script;
script = document.createElement("script");
script.type = "text/x-mathjax-config";
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);
})();
utils.inject_css("https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css");
utils.inject_script("https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js");
utils.inject_script("https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/contrib/auto-render.min.js");
}
});
@ -416,14 +469,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}
// Load new theme
(function() {
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);
})();
utils.inject_css("css/themes/" + theme + ".css", "themeCSS");
});
settings.addCallback('customCSS', function(css) {
@ -451,6 +497,15 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
});
// Update font size when changed
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);
});
@ -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)
if ($scope.wasMobileUi && !utils.isMobileUi()) {
$scope.showSidebar();
$scope.updateShowNicklist();
}
$scope.wasMobileUi = utils.isMobileUi();
$scope.calculateNumLines();
@ -611,6 +667,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$scope.disconnect = function() {
$scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
bufferResume.reset();
connection.disconnect();
};
$scope.reconnect = function() {
@ -618,30 +675,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
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) {
document.getElementById(elementId).setAttribute('data-state', 'visible');
};
@ -685,18 +718,46 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
return true;
}
// Always show core buffer in the list (issue #438)
// Also show server buffers in hierarchical view
if (buffer.fullName === "core.weechat" || (settings.orderbyserver && buffer.type === 'server')) {
if (buffer.fullName === "core.weechat") {
return true;
}
// 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.notification > 0) && !buffer.hidden;
return (buffer.unread > 0 && !buffer.hidden) || buffer.notification > 0;
}
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
settings.addCallback('nonicklist', function() {
$scope.showNicklist = $scope.updateShowNicklist();
$scope.updateShowNicklist();
// restore bottom view
if ($rootScope.connected && $rootScope.bufferBottom) {
$timeout(function(){
@ -704,23 +765,32 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}, 500);
}
});
settings.addCallback('alwaysnicklist', function() {
$scope.updateShowNicklist();
});
$scope.showNicklist = false;
// Utility function that template can use to check if nicklist should
// be displayed for current buffer or not
// is called on buffer switch
// Utility function that template can use to check if nicklist should be
// displayed for current buffer or not is called on buffer switch and
// certain swipe actions. Sets $scope.showNicklist accordingly and returns
// whether the buffer even has a nicklist to show.
$scope.updateShowNicklist = function() {
var ab = models.getActiveBuffer();
if (!ab) {
// Check whether buffer exists and nicklist is non-empty
if (!ab || ab.isNicklistEmpty()) {
$scope.showNicklist = false;
return false;
}
// Check if option no nicklist is set
if (settings.nonicklist) {
return false;
// Check if nicklist is disabled in settings (ignored on mobile)
if (!utils.isMobileUi() && settings.nonicklist) {
$scope.showNicklist = false;
return true;
}
// Check if nicklist is empty
if (ab.isNicklistEmpty()) {
return false;
// mobile: hide nicklist unless overriden by setting or swipe action
if (utils.isMobileUi() && !settings.alwaysnicklist && $scope.swipeStatus !== -1) {
$scope.showNicklist = false;
return true;
}
$scope.showNicklist = true;
return true;
};
@ -740,7 +810,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// No notifications, find first buffer with unread lines instead
for (i in sortedBuffers) {
buffer = sortedBuffers[i];
if (buffer.unread > 0) {
if (buffer.unread > 0 && !buffer.hidden) {
$scope.setActiveBuffer(buffer.id);
return;
}
@ -755,29 +825,57 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// direction is +1 for next buffer, -1 for previous buffer
var sortedBuffers = _.sortBy($scope.getBuffers(), $rootScope.predicate);
var activeBuffer = models.getActiveBuffer();
var index = sortedBuffers.indexOf(activeBuffer);
if (index >= 0) {
var newBuffer = sortedBuffers[index + direction];
if (newBuffer) {
$scope.setActiveBuffer(newBuffer.id);
var index = sortedBuffers.indexOf(activeBuffer) + direction;
var newBuffer;
// look for next non-hidden buffer
while (index >= 0 && index < sortedBuffers.length &&
(!newBuffer || newBuffer.hidden)) {
newBuffer = sortedBuffers[index];
index += direction;
}
if (!!newBuffer) {
$scope.setActiveBuffer(newBuffer.id);
}
};
$scope.handleSearchBoxKey = function($event) {
// Support different browser quirks
var code = $event.keyCode ? $event.keyCode : $event.charCode;
// Handle escape
if (code === 27) {
$event.preventDefault();
$scope.search = '';
} // Handle enter
else if (code === 13) {
var index;
$event.preventDefault();
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 = '';
} // 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,8 +909,11 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
if ($rootScope.connected) {
$scope.disconnect();
}
if (!utils.isCordova()) {
$scope.favico.reset();
}
}
};
$scope.init = function() {

@ -3,7 +3,7 @@
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 content = message.objects[0].content;
@ -161,13 +161,16 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
}
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')) {
buffer.unread++;
server.unread++;
$rootScope.$emit('notificationChanged');
}
if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) {
buffer.notification++;
server.unread++;
notifications.createHighlight(buffer, message);
$rootScope.$emit('notificationChanged');
}
@ -186,13 +189,25 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
handleBufferUpdate(buffer, bufferInfos[i]);
} else {
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);
// Switch to first buffer on startup
if (i === 0) {
var shouldResume = bufferResume.shouldResume(buffer);
if(shouldResume){
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) {
@ -208,7 +223,9 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
buffer.number = message.number;
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.unread = 0;
buffer.lastSeen = -1;
@ -232,6 +249,12 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
var handleBufferOpened = function(message) {
var bufferMessage = message.objects[0].content[0];
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);
};
@ -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 obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
@ -300,13 +344,14 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
old.server = localvars.server;
old.serverSortKey = old.plugin + "." + old.server +
(old.type === "server" ? "" : ("." + old.shortName));
old.pinned = localvars.pinned === "true";
}
};
var handleBufferTypeChanged = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
//var old = models.getBuffer(buffer);
// 0 = formatted (normal); 1 = free
buffer.bufferType = obj.type;
};
@ -340,23 +385,47 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
* Handle answers to hotlist request
*/
var handleHotlistInfo = function(message) {
if (message.objects.length === 0) {
return;
}
// Hotlist includes only buffers with unread counts so first we
// 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];
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];
});
}
// the unread badges in the bufferlist doesn't update if we don't do this
setTimeout(function() {
$rootScope.$apply();
$rootScope.$emit('notificationChanged');
});
};
@ -413,6 +482,7 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
_buffer_localvar_added: handleBufferLocalvarChanged,
_buffer_localvar_removed: handleBufferLocalvarChanged,
_buffer_localvar_changed: handleBufferLocalvarChanged,
_buffer_moved: handleBufferMoved,
_buffer_opened: handleBufferOpened,
_buffer_title_changed: handleBufferTitleChanged,
_buffer_type_changed: handleBufferTypeChanged,

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

@ -14,7 +14,7 @@ weechat.directive('inputBar', function() {
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,
$element, //XXX do we need this? don't seem to be using it
$log,
@ -22,11 +22,46 @@ weechat.directive('inputBar', function() {
imgur,
models,
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.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 || '';
// 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,
activeBuffer.getNicklistByTime(), ':');
activeBuffer.getNicklistByTime(),
completion_suffix, add_space);
// remember iteration candidate
$scope.iterCandidate = nickComp.iterCandidate;
@ -170,7 +208,12 @@ weechat.directive('inputBar', function() {
};
//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
var nick = prefix[prefix.length - 1].text;
@ -223,9 +266,12 @@ weechat.directive('inputBar', function() {
// Support different browser quirks
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
var altg = $event.getModifierState ? $event.getModifierState('AltGraph') : false;
// Mac OSX behaves differntly for altgr, so we check for that
if (altg) {
// We don't handle any anything with altgr
@ -239,17 +285,53 @@ weechat.directive('inputBar', function() {
var tmpIterCandidate = $scope.iterCandidate;
$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
if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58)) {
if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58) && settings.enableQuickKeys) {
if (code === 48) {
code = 58;
}
var bufferNumber = code - 48 - 1 ;
bufferNumber = code - 48 - 1 ;
var activeBufferId;
// quick select filtered entries
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) {
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
// copy the entire (possibly very large) buffer object, and then sort
// 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];
}).sort(function(left, right) {
// By default, Array.prototype.sort() sorts alphabetically.
@ -311,7 +393,8 @@ weechat.directive('inputBar', function() {
}
// 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();
if (previousBuffer) {
models.setActiveBuffer(previousBuffer.id);
@ -353,6 +436,30 @@ weechat.directive('inputBar', function() {
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;
// Arrow up -> go up in history
@ -449,7 +556,7 @@ weechat.directive('inputBar', function() {
// Ctrl-w
} else if (code == 87) {
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);
setTimeout(function() {
inputNode.setSelectionRange(lastSpace, lastSpace);
@ -462,7 +569,7 @@ weechat.directive('inputBar', function() {
}
// 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;
}
};
@ -483,6 +590,35 @@ weechat.directive('inputBar', function() {
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;
nickList.some(function(nick) {
if (nick.toLowerCase().search(candidate.toLowerCase()) === 0) {
if (nick.toLowerCase().indexOf(candidate.toLowerCase()) === 0) {
// found!
foundNick = nick;
return true;
@ -72,7 +72,7 @@ IrcUtils.service('IrcUtils', [function() {
// collect matching nicks
for (var i = 0; i < nickList.length; ++i) {
var lcNick = nickList[i].toLowerCase();
if (lcNick.search(escapeRegExp(lcIterCandidate)) === 0) {
if (lcNick.indexOf(lcIterCandidate) === 0) {
matchingNicks.push(nickList[i]);
if (lcCurrentNick === lcNick) {
at = matchingNicks.length - 1;
@ -106,17 +106,20 @@ IrcUtils.service('IrcUtils', [function() {
* @param iterCandidate Current iteration candidate (null if not iterating)
* @param nickList Array of current nicks
* @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:
* text: new complete replacement text
* caretPos: new caret position within new text
* foundNick: completed nick (or null if not possible)
* 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);
if (suf === null) {
if (suf === undefined) {
suf = ':';
}
// addSpace defaults to true
var addSpaceChar = (addSpace === undefined || addSpace === true) ? ' ' : '';
// new nick list to search in
var searchNickList = _ciNickList(nickList);
@ -158,7 +161,7 @@ IrcUtils.service('IrcUtils', [function() {
m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) {
// try completing
newNick = _completeSingleNick(escapeRegExp(m[1]), searchNickList);
newNick = _completeSingleNick(m[1], searchNickList);
if (newNick === null) {
// no match
return ret;
@ -182,7 +185,7 @@ IrcUtils.service('IrcUtils', [function() {
if (doIterate) {
// try iterating
newNick = _nextNick(iterCandidate, m[2], searchNickList);
beforeCaret = m[1] + newNick + ' ';
beforeCaret = m[1] + newNick + addSpaceChar;
return {
text: beforeCaret + afterCaret,
caretPos: beforeCaret.length,
@ -204,7 +207,7 @@ IrcUtils.service('IrcUtils', [function() {
// no match
return ret;
}
beforeCaret = m[1] + newNick + ' ';
beforeCaret = m[1] + newNick + addSpaceChar;
if (afterCaret[0] === ' ') {
// swallow first space after caret if any
afterCaret = afterCaret.substring(1);

@ -3,6 +3,42 @@
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){
/**
* Global Vars
@ -10,6 +46,15 @@ ls.factory("$store", ["$parse", function($parse){
var storage = (typeof window.localStorage === 'undefined') ? undefined : window.localStorage,
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) {
console.log('Warning: localStorage is not supported');
}

@ -7,7 +7,7 @@
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
this.version = null;
@ -95,6 +95,9 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var plugin = message.local_variables.plugin;
var server = message.local_variables.server;
var pinned = message.local_variables.pinned === "true";
// Server buffers have this "irc.server.freenode" naming schema, which
// messes the sorting up. We need it to be "irc.freenode" instead.
var serverSortKey = plugin + "." + server +
@ -335,7 +338,8 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
getHistoryUp: getHistoryUp,
getHistoryDown: getHistoryDown,
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 date = message.date;
var shortTime = $filter('date')(date, 'HH:mm');
var formattedTime = $filter('date')(date, $rootScope.angularTimeFormat);
var prefix = parseRichText(message.prefix);
var tags_array = message.tags_array;
@ -354,12 +359,22 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var highlight = message.highlight;
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) {
prefix.forEach(function(textEl) {
textEl.classes.push('highlight');
});
}
var prefixtext = "";
for (var pti = 0; pti < prefix.length; ++pti) {
prefixtext += prefix[pti].text;
}
var rtext = "";
for (var i = 0; i < content.length; ++i) {
rtext += content[i].text;
@ -370,44 +385,58 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
content: content,
date: date,
shortTime: shortTime,
formattedTime: formattedTime,
buffer: buffer,
tags: tags_array,
highlight: highlight,
displayed: displayed,
text: rtext
prefixtext: prefixtext,
text: rtext,
showHiddenBrackets: showHiddenBrackets
};
};
function nickGetColorClasses(nickMsg, propName) {
var colorClasses = [
'cwf-default'
];
if (propName in nickMsg && nickMsg[propName] && nickMsg[propName].length > 0) {
var color = nickMsg[propName];
if (color.match(/^weechat/)) {
// color option
var colorName = color.match(/[a-zA-Z0-9_]+$/)[0];
return [
colorClasses = [
'cof-' + colorName,
'cob-' + colorName,
'coa-' + colorName
];
} else if (color.match(/^[a-zA-Z]+$/)) {
// WeeChat color name
return [
'cwf-' + color
} else {
if (color.match(/^[a-zA-Z]+(:|$)/)) {
// WeeChat color name (foreground)
var cwfcolor = color.match(/^[a-zA-Z]+/)[0];
colorClasses = [
'cwf-' + cwfcolor
];
} else if (color.match(/^[0-9]+$/)) {
// extended color
return [
'cef-' + color
} else if (color.match(/^[0-9]+(:|$)/)) {
// 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 [
'cwf-default'
];
}
}
return colorClasses;
}
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 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
@ -539,12 +583,15 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var unreadSum = activeBuffer.unread + activeBuffer.notification;
this.getServerForBuffer(activeBuffer).unread -= unreadSum;
activeBuffer.active = true;
activeBuffer.unread = 0;
activeBuffer.notification = 0;
$rootScope.$emit('activeBufferChanged', unreadSum);
$rootScope.$emit('notificationChanged');
bufferResume.record(activeBuffer);
return true;
};
@ -560,6 +607,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
*/
this.reinitialize = function() {
this.model.buffers = {};
this.model.servers = {};
};
/*
@ -572,6 +620,35 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
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
* as active, if the closing buffer was active before

@ -1,6 +1,6 @@
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 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');
navigator.serviceWorker.register('serviceworker.js').then(function(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);
});
}
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) {
@ -65,7 +81,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
toastNotifier.show(toast);
} else {
} else if (typeof Notification !== 'undefined') {
var notification = new Notification(title, {
body: body,
@ -96,6 +112,22 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
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,23 +166,44 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
};
var updateFavico = function() {
if (utils.isCordova()) {
return; // cordova doesn't have a favicon
}
var notifications = unreadCount('notification');
if (notifications > 0) {
$rootScope.favico.badge(notifications, {
bgColor: '#d00',
textColor: '#fff'
});
// Set badge to notifications count
updateBadge(notifications);
} else {
var unread = unreadCount('unread');
if (unread === 0) {
$rootScope.favico.reset();
// Remove badge form app icon
updateBadge('');
} else {
$rootScope.favico.badge(unread, {
bgColor: '#5CB85C',
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 */
@ -182,8 +235,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
showNotification(buffer, title, body);
if (settings.soundnotification) {
// TODO fill in a sound file
if (!utils.isCordova() && settings.soundnotification) {
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>';
document.getElementById("soundNotification").innerHTML = soundHTML;
@ -203,6 +255,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
requestNotificationPermission: requestNotificationPermission,
updateTitle: updateTitle,
updateFavico: updateFavico,
updateBadge: updateBadge,
createHighlight: createHighlight,
cancelAll: cancelAll,
unreadCount: unreadCount

@ -33,7 +33,7 @@ var urlRegexp = /(?:(?:https?|ftp):\/\/|www\.|ftp\.)\S*[^\s.;,(){}<>]/g;
var UrlPlugin = function(name, urlCallback) {
return {
contentForMessage: function(message) {
var urls = message.match(urlRegexp);
var urls = _.uniq(message.match(urlRegexp));
var content = [];
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 addMatch = function(match) {
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>')
.attr('src', '//embed.spotify.com/?uri=spotify:track:' + id)
.attr('width', '300')
.attr('src', '//embed.spotify.com/?uri=' + match[i])
.attr('width', '350')
.attr('height', '80')
.attr('frameborder', '0')
.attr('allowtransparency', 'true');
content.push(element.prop('outerHTML'));
}
};
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: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;
});
@ -206,12 +209,12 @@ plugins.factory('userPlugins', function() {
* See: https://developers.google.com/youtube/player_parameters
*/
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);
if (match){
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>')
.attr('src', embedurl)
.attr('width', '560')
@ -228,9 +231,9 @@ plugins.factory('userPlugins', function() {
* See: http://www.dailymotion.com/doc/api/player.html
*/
var dailymotionPlugin = new Plugin('Dailymotion video', function(message) {
var rPath = /dailymotion.com\/.*video\/([^_?# ]+)/;
var rAnchor = /dailymotion.com\/.*#video=([^_& ]+)/;
var rShorten = /dai.ly\/([^_?# ]+)/;
var rPath = /dailymotion\.com\/.*video\/([^_?# ]+)/;
var rAnchor = /dailymotion\.com\/.*#video=([^_& ]+)/;
var rShorten = /dai\.ly\/([^_?# ]+)/;
var match = message.match(rPath) || message.match(rAnchor) || message.match(rShorten);
if (match) {
@ -251,8 +254,8 @@ plugins.factory('userPlugins', function() {
* AlloCine Embedded Player
*/
var allocinePlugin = new Plugin('AlloCine video', function(message) {
var rVideokast = /allocine.fr\/videokast\/video-(\d+)/;
var rCmedia = /allocine.fr\/.*cmedia=(\d+)/;
var rVideokast = /allocine\.fr\/videokast\/video-(\d+)/;
var rCmedia = /allocine\.fr\/.*cmedia=(\d+)/;
var match = message.match(rVideokast) || message.match(rCmedia);
if (match) {
@ -273,13 +276,13 @@ plugins.factory('userPlugins', function() {
* Image Preview
*/
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. */
if (url.indexOf("^https?://fukung.net/v/") != -1) {
url = url.replace(/.*\//, "http://media.fukung.net/imgs/");
} else if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) {
// remove protocol specification to load over https if used by g-b
url = url.replace(/http:/, "");
// imgur: always use https. avoids mixed content warnings
url = url.replace(/^http:/, "https:");
} else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\//i)) {
// Dropbox requires a get parameter, dl=1
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) {
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() {
var element = this.getElement();
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) {
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)) {
// remove protocol specification to load over https if used by g-b
url = url.replace(/\.(gifv)\b/i, ".webm");
// imgur: always use https. avoids mixed content warnings
url = url.replace(/^http:/, "https:");
}
return function() {
var element = this.getElement();
var velement = angular.element('<video autoplay loop muted></video>')
var element = this.getElement(), src;
var velement = angular.element('<video autoplay controls loop muted></video>')
.addClass('embed')
.attr('width', '560')
.append(angular.element('<source></source>')
.attr('src', url));
.attr('width', '560');
// imgur doesn't always have webm for gifv so add sources for webm and mp4
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');
};
}
@ -359,7 +375,7 @@ plugins.factory('userPlugins', function() {
var cloudmusicPlugin = new UrlPlugin('cloud music', function(url) {
/* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */
var element;
if (url.match(/^https?:\/\/soundcloud.com\//)) {
if (url.match(/^https?:\/\/soundcloud\.com\//)) {
element = angular.element('<iframe></iframe>')
.attr('width', '100%')
.attr('height', '120')
@ -370,7 +386,7 @@ plugins.factory('userPlugins', function() {
}
/* MixCloud */
if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud.com\//)) {
if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud\.com\//)) {
element = angular.element('<iframe></iframe>')
.attr('width', '480')
.attr('height', '60')
@ -400,7 +416,7 @@ plugins.factory('userPlugins', function() {
* Asciinema plugin
*/
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);
if (match) {
var id = match[1];
@ -435,11 +451,12 @@ plugins.factory('userPlugins', function() {
// Embed GitHub gists
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);
if (match) {
// 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
return function() {
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
* sample input: http://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy
* sample output: https://media.giphy.com/media/feqkVgjJpYtjy/giphy.gif
*/
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]
var id = url.match(regex);
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
var scriptElem = document.createElement('script');
// 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);
});
};
@ -507,7 +538,7 @@ plugins.factory('userPlugins', function() {
* Vine plugin
*/
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);
if (match) {
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('height', '600')
.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 {
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);
};
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 {
changeClassStyle: changeClassStyle,
getClassStyle: getClassStyle,
isMobileUi: isMobileUi
isMobileUi: isMobileUi,
isCordova: isCordova,
inject_script: inject_script,
inject_css: inject_css,
};
});

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

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

@ -1,27 +1,28 @@
{
"name": "glowing-bear",
"private": true,
"version": "0.6.0",
"version": "0.7.0",
"description": "A web client for Weechat",
"repository": "https://github.com/glowing-bear/glowing-bear",
"main": "electron-main.js",
"license": "GPLv3",
"devDependencies": {
"bower": "^1.3.1",
"http-server": "^0.6.1",
"jasmine-core": "^2.4.1",
"jshint": "^2.5.2",
"karma": "~0.13",
"karma-jasmine": "^0.3.6",
"karma-junit-reporter": "^0.2.2",
"karma-phantomjs-launcher": "^0.2.1",
"phantomjs": "^1.9.19",
"protractor": "~0.20.1",
"shelljs": "^0.2.6",
"uglify-js": "^2.4"
"bower": "^1.8",
"electron-packager": "^12.2.0",
"http-server": "^0.11",
"jasmine-core": "^3.1",
"jshint": "^2.9.6",
"karma": "^3.1.1",
"karma-jasmine": "~1.1",
"karma-junit-reporter": "^1.2",
"karma-phantomjs-launcher": "^1.0.0",
"protractor": "^5.4.1",
"shelljs": "^0.8.0",
"uglify-js": "^3.4.9"
},
"scripts": {
"postinstall": "bower install",
"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",
"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/bufferResume.js js/plugins.js js/imgur.js -c -m --screw-ie8 -o min.js --source-map url='min.js.map'",
"prestart": "npm install",
"start": "http-server -a localhost -p 8000",
"pretest": "npm install",
@ -31,6 +32,11 @@
"update-webdriver": "webdriver-manager update",
"preprotractor": "npm run update-webdriver",
"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');\""
}
}

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

@ -29,12 +29,16 @@ describe('filter', function() {
$provide.value('version', 'TEST_VER');
}));
it('should recognize spotify tracks', inject(function(plugins) {
it('should recognize spotify links', inject(function(plugins) {
expectTheseMessagesToContain([
'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);
}));
@ -139,6 +143,15 @@ describe('filter', function() {
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) {
expectTheseMessagesToContain([
'https://giphy.com/gifs/eyes-shocked-bird-feqkVgjJpYtjy/',

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

Loading…
Cancel
Save