Merge branch 'master' into gh-pages

Also run minification
gh-pages
Lorenz Hübschle-Schneider 10 years ago
commit 8a898d8278
  1. 19
      .editorconfig
  2. 10
      README.md
  3. BIN
      assets/img/badge_firefoxos.png
  4. BIN
      assets/img/badge_playstore.png
  5. 14
      bower.json
  6. 88
      css/glowingbear.css
  7. 22
      css/themes/black.css
  8. 12
      css/themes/dark.css
  9. 5
      css/themes/light.css
  10. 9
      directives/input.html
  11. 56
      index.html
  12. 38
      js/connection.js
  13. 23
      js/file-change.js
  14. 21
      js/filters.js
  15. 97
      js/glowingbear.js
  16. 149
      js/handlers.js
  17. 49
      js/imgur-drop-directive.js
  18. 128
      js/imgur.js
  19. 54
      js/inputbar.js
  20. 20
      js/models.js
  21. 114
      js/notifications.js
  22. 52
      js/plugins.js
  23. 66
      js/weechat.js
  24. 21
      js/whenscrolled-directive.js
  25. 2
      manifest.json
  26. 2
      manifest.webapp
  27. 5
      min.js
  28. 2
      min.map
  29. 22
      package.json
  30. 46
      serviceworker.js
  31. 30
      test/unit/filters.js
  32. 33
      webapp.manifest.json

@ -0,0 +1,19 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Indent JS with four spaces
[*.js]
indent_style = space
indent_size = 4
# Indent HTML with two spaces
[*.html]
indent_style = space
indent_size = 2

@ -16,7 +16,7 @@ Please note that the above instructions set up an *unencrypted* relay, and all y
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.
<a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" /></a><a href="https://marketplace.firefox.com/app/glowing-bear/"><img alt="Firefox OS app in the Firefox Marketplace" src="https://marketplace.cdn.mozilla.net/media/img/mkt/badges/firefox-marketplace_badge-orange_129_45.png" /></a>
<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>
##Screenshots
@ -24,6 +24,9 @@ Running as Chrome application in a separate window on Windows and as Android app
![Glowing bear screenshot](https://4z2.de/glowingbear.png)
Are you good with design? We'd love your help!
![Glowing Bear screenshot with lots of Comic Sans MS](https://4z2.de/glowing-bear3.png)
##How it Works
What follows is a more technical explanation of how Glowing Bear works, and you don't need to understand it to use it.
@ -44,7 +47,10 @@ Here's a simple example using the python simple web server:
```bash
git clone https://github.com/glowing-bear/glowing-bear
cd glowing-bear
# python 2.*
python -m SimpleHTTPServer
# or python 3.*
python -m http.server
```
Now you can point your browser to [http://localhost:8000](http://localhost:8000)!
@ -83,4 +89,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/), [twemoji](https://github.com/twitter/twemoji), 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](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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

@ -1,17 +1,17 @@
{
"name": "glowing-bear",
"description": "A webclient for WeeChat",
"version": "0.5.2",
"version": "0.6.0",
"homepage": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3",
"private": true,
"dependencies": {
"angular": "1.3.x",
"angular-route": "1.3.x",
"angular-sanitize": "1.3.x",
"angular-touch": "1.3.x",
"angular-loader": "1.3.x",
"angular-mocks": "1.3.x",
"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"
}
}

@ -15,6 +15,10 @@ a {
cursor: pointer;
}
.version {
margin-right: 1em;
}
.hidden-bracket {
position: absolute;
left: -1000px;
@ -29,6 +33,7 @@ td.prefix {
border-right: 1px solid #444;
}
td.message {
overflow: hidden;
vertical-align: top;
width: 100%;
padding: 1px 1px 1px 5px;
@ -84,6 +89,25 @@ input[type=text], input[type=password], #sendMessage {
margin-bottom: 5px !important;
}
.btn-send-image {
position: relative;
overflow: hidden;
cursor: pointer;
}
.imgur-upload {
position: absolute;
bottom: 0;
right: 0;
cursor: inherit;
font-size: 1000px !important;
height: 300px;
margin: 0;
padding: 0;
opacity: 0;
filter: ~"alpha(opacity=0)";
}
.input-group {
width: 100%;
}
@ -157,6 +181,14 @@ input[type=text], input[type=password], #sendMessage {
padding-right: 6px;
}
.upload-error {
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 4;
}
#sidebar {
position: fixed;
width: 140px;
@ -330,6 +362,25 @@ td.time {
padding-right: 100px;
}
#inputform {
position: relative;
}
#imgur-upload-progress {
width: 100%;
height: auto;
position: absolute;
bottom: 100%;
left: 0;
}
.imgur-progress-bar {
width: 0%;
height: 5px;
margin-top: 1px;
background: #428BCA;
}
/* fix for mobile firefox which ignores :hover */
.nav-pills > li > a:active, .nav-pills > li > a:active span {
text-decoration: none;
@ -547,11 +598,7 @@ li.buffer.channel_ampersand a span:last-of-type:before, #topbar .title .channel_
}
li.buffer.channel.active a span:last-of-type:before {
color: #444;
}
li.buffer.channel.active a:hover span:last-of-type:before {
color: #222;
color: #aaa;
}
li.buffer.indent.private a {
@ -591,6 +638,31 @@ img.emojione {
width: auto;
}
.glyphicon-spin {
-webkit-animation: spin 1000ms infinite linear;
animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@media (min-width: 1400px) {
#sidebar[data-state=visible], #sidebar {
width: 200px;
@ -726,7 +798,13 @@ img.emojione {
padding-bottom: 0;
}
#bufferlines tr.bufferline {
display: block;
overflow: hidden;
}
#bufferlines td.time {
display: inline-block;
padding-right: 3px;
font-size: 0.8em;
}

@ -0,0 +1,22 @@
@import "dark.css";
body, .modal-content {
background-color: #000;
}
#topbar, #sidebar, .panel, .dropdown-menu, #topbar .actions {
background: #080808;
}
.nav-pills li:nth-child(2n) {
background: #000;
}
.form-control option, input.form-control, select.form-control {
color: #ccc;
background: #080808;
}
.close, .close:hover, .close:focus {
color: #ddd;
}

@ -10,6 +10,11 @@ body {
border: 0px none;
}
.form-control option {
color: #eee;
background: #282828;
}
html {
background-color: inherit;
}
@ -72,12 +77,13 @@ 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 {
input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-send-image {
color: #ccc;
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3);
}
.btn-send:hover, .btn-send:focus {
.btn-send:hover, .btn-send:focus,
.btn-send-image:hover, .btn-send-image:focus {
background-color: #555;
color: white;
}
@ -271,7 +277,7 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send {
.cob-chat {
}
.cob-chat_time {
color: #999;
color: #999;
}
.cob-chat_time_delimiters {
}

@ -22,7 +22,8 @@ html {
background-color: #222;
}
.btn-send {
.btn-send,
.btn-send-image, {
background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.3);
color: #428BCA;
}
@ -257,7 +258,7 @@ select.form-control, select option, input[type=text], input[type=password], #sen
.cob-chat {
}
.cob-chat_time {
color: #999;
color: #999;
}
.cob-chat_time_delimiters {
}

@ -1,9 +1,14 @@
<form class="form form-horizontal" id="inputform" ng-submit="sendMessage()">
<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>
<span class="input-group-btn">
<button class="btn btn-send unselectable"><i class="glyphicon glyphicon-send"></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)">
</label>
<button class="btn btn-send unselectable" title="Send"><i class="glyphicon glyphicon-send"></i></button>
</span>
</div>
<div id="imgur-upload-progress"></div>
</form>

@ -8,25 +8,31 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Glowing Bear">
<meta name="theme-color" content="#2779d3">
<meta name="theme-color" content="#181818">
<meta http-equiv="x-dns-prefetch-control" content="off">
<!-- 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="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen">
<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="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="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-sanitize.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-touch.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/emojione/1.4.0/lib/js/emojione.min.js"></script>
<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 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>
</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">
<p><strong>Upload error:</strong> Image upload failed.</p>
</div>
<div ng-hide="connected" class="container">
<h2>
<img alt="logo" src="assets/img/glowing-bear.svg">
@ -91,7 +97,7 @@
</label>
</div>
</div>
<button class="btn btn-lg btn-primary" ng-click="connect()">{{ connectbutton }} <i class="glyphicon glyphicon-chevron-right"></i></button>
<button class="btn btn-lg btn-primary" ng-click="connect()">{{ connectbutton }} <i ng-class="connectbuttonicon" class="glyphicon"></i></button>
</form>
</div>
</div>
@ -150,7 +156,7 @@
<pre>
$ mkdir -p ~/.weechat/ssl
$ cd ~/.weechat/ssl
$ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out relay.pem -subj "/CN={{settings.host || 'your weechat host'}}/"
$ 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>
@ -174,7 +180,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<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>
<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="https://developer.android.com/images/brand/en_app_rgb_wo_45.png" /></a> <a href="https://marketplace.firefox.com/app/glowing-bear/"><img alt="Firefox OS app in the Firefox Marketplace" src="https://marketplace.cdn.mozilla.net/media/img/mkt/badges/firefox-marketplace_badge-orange_129_45.png" /></a></p>
<p><a href="https://play.google.com/store/apps/details?id=com.glowing_bear"><img alt="Android app on Google Play" src="assets/img/badge_playstore.png" /></a> <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>
<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>
@ -219,10 +225,10 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</div>
<div class="actions pull-right vertical-line-left">
<a class="settings-toggle" ng-click="showModal('settingsModal')" title="Options menu">
<a class="settings-toggle" ng-click="showModal('settingsModal')" title="Options menu" href="#">
<i class="glyphicon glyphicon-cog"></i>
</a>
<a ng-click="disconnect()" title="Disconnect from WeeChat">
<a ng-click="disconnect()" title="Disconnect from WeeChat" href="#">
<i class="glyphicon glyphicon-off"></i>
</a>
</div>
@ -235,7 +241,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</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))">
<a ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}">
<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="buffer-quick-key">{{ buffer.$quickKey }}</span>
<span class="buffername">{{ buffer.trimmedName || buffer.fullName }}</span>
@ -243,7 +249,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</li>
</ul>
</div>
<div id="bufferlines" class="favorite-font" ng-swipe-right="showSidebar()" ng-swipe-left="hideSidebar()" ng-class="{'withnicklist': showNicklist}">
<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">
<ul class="nicklistgroup list-unstyled" ng-repeat="group in nicklist">
<li ng-repeat="nick in group.nicks|orderBy:'name'">
@ -255,8 +261,8 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<tbody>
<tr class="bufferline">
<td ng-hide="activeBuffer().allLinesFetched" colspan="3">
<a class="fetchmorelines" ng-click="fetchMoreLines()" ng-hide="loadingLines">Fetch more lines</a>
<span ng-show="loadingLines">Fetching more lines...</span>
<a class="fetchmorelines btn btn-xs btn-primary" ng-click="fetchMoreLines()" ng-hide="loadingLines" href="#">Fetch more lines</a>
<span ng-show="loadingLines">Fetching more lines <i class="glyphicon glyphicon-refresh glyphicon-spin"></i></span>
</td>
</tr>
</tbody>
@ -289,7 +295,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div id="reconnect" class="alert alert-danger" ng-click="reconnect()" ng-show="reconnecting">
<p><strong>Connection to WeeChat lost</strong></p>
<i class="glyphicon glyphicon-refresh"></i>
Reconnecting... <a class="btn btn-tiny" ng-click="reconnect()">Click to try to reconnect now</a>
Reconnecting... <i class="glyphicon glyphicon-spin glyphicon-refresh"></i> <a class="btn btn-xs" ng-click="reconnect()" href="#">Click to try to reconnect now</a>
</div>
<div id="settingsModal" class="gb-modal" data-state="hidden">
<div class="backdrop" ng-click="closeModal($event)"></div>
@ -297,6 +303,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>
<h4 class="modal-title">Settings</h4>
<p>Settings will be stored in your browser.</p>
</div>
@ -328,6 +335,17 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</form>
</li>
<li class="standard-labels">
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="custom-css" class="col-sm-3 control-label make-thinner">Custom CSS</label>
<div class="col-sm-7">
<textarea id="custom-css" class="form-control" ng-model="settings.customCSS"></textarea>
</div>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
@ -435,7 +453,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.enableJSEmoji">
Enable non-native Emoji support <span class="text-muted settings-help">Displays Emoji characters as images</span>
Enable non-native Emoji support <span class="text-muted settings-help">Displays Emoji characters as images. Emoji provided free by <a href="http://emojione.com">http://emojione.com</a></span>
</label>
</div>
</form>

@ -17,10 +17,11 @@ weechat.factory('connection',
// Takes care of the connection and websocket hooks
var connect = function (host, port, passwd, ssl, noCompression, successCallback, failCallback) {
$rootScope.passwordError = false;
connectionData = [host, port, passwd, ssl, noCompression];
var proto = ssl ? 'wss' : 'ws';
// If host is an IPv6 literal wrap it in brackets
if (host.indexOf(":") !== -1) {
if (host.indexOf(":") !== -1 && host[0] !== "[" && host[host.length-1] !== "]") {
host = "[" + host + "]";
}
var url = proto + "://" + host + ":" + port + "/weechat";
@ -62,7 +63,7 @@ weechat.factory('connection',
return ngWebsockets.send(
weeChat.Protocol.formatHdata({
path: 'buffer:gui_buffers(*)',
keys: ['local_variables,notify,number,full_name,short_name,title,hidden']
keys: ['local_variables,notify,number,full_name,short_name,title,hidden,type']
})
);
};
@ -99,10 +100,7 @@ weechat.factory('connection',
$rootScope.connected = true;
},
function() {
// Connection got closed, lets check if we ever was connected successfully
if (!$rootScope.waseverconnected) {
$rootScope.passwordError = true;
}
handleWrongPassword();
}
);
@ -121,12 +119,14 @@ weechat.factory('connection',
* Handles websocket disconnection
*/
$log.info("Disconnected from relay");
$rootScope.$emit('relayDisconnect');
if ($rootScope.userdisconnect || !$rootScope.waseverconnected) {
handleClose(evt);
$rootScope.userdisconnect = false;
} else {
reconnect(evt);
}
handleWrongPassword();
};
var handleClose = function (evt) {
@ -140,6 +140,14 @@ weechat.factory('connection',
}
};
var handleWrongPassword = function() {
// Connection got closed, lets check if we ever was connected successfully
if (!$rootScope.waseverconnected && !$rootScope.errorMessage) {
$rootScope.passwordError = true;
$rootScope.$apply();
}
};
var onerror = function (evt) {
/*
* Handles cases when connection issues come from
@ -282,10 +290,13 @@ weechat.factory('connection',
};
var requestNicklist = function(bufferId, callback) {
bufferId = bufferId || null;
// Prevent requesting nicklist for all buffers if bufferId is invalid
if (!bufferId) {
return;
}
ngWebsockets.send(
weeChat.Protocol.formatNicklist({
buffer: bufferId
buffer: "0x"+bufferId
})
).then(function(nicklist) {
handlers.handleNicklist(nicklist);
@ -295,6 +306,17 @@ weechat.factory('connection',
});
};
var fetchConfValue = function(name) {
ngWebsockets.send(
weeChat.Protocol.formatInfolist({
name: "option",
pointer: 0,
args: name
})
).then(function(i) {
handlers.handleConfValue(i);
});
};
var fetchMoreLines = function(numLines) {
$log.debug('Fetching ', numLines, ' lines');

@ -0,0 +1,23 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('fileChange', ['$parse', function($parse) {
return {
restrict: 'A',
link: function ($scope, element, attrs) {
var attrHandler = $parse(attrs.fileChange);
var handler = function (e) {
$scope.$apply(function () {
attrHandler($scope, { $event: e, files: e.target.files });
});
};
element[0].addEventListener('change', handler, false);
}
};
}]);
})();

@ -61,10 +61,15 @@ weechat.filter('inlinecolour', function() {
// apply a filter to an HTML string's text nodes, and do so with not exceedingly terrible performance
weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
// To prevent nested anchors, we need to know if a filter is going to create them.
// Here's a list of names. See #681 for more information.
var filtersThatCreateAnchors = ['irclinky'];
return function(text, filter) {
if (!text || !filter) {
return text;
}
var createsAnchor = filtersThatCreateAnchors.indexOf(filter) > -1;
var escape_html = function(text) {
// First, escape entities to prevent escaping issues because it's a bad idea
@ -92,6 +97,7 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
// as innerHTML causes it to be unescaped.
var input = escape_html(node.nodeValue);
var value = filterFunction(input, extraArgument, thirdArgument);
if (value !== input) {
// we changed something. create a new node to replace the current one
// we could also only add its children but that would probably incur
@ -114,7 +120,11 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
if (node === undefined || node === null) return;
node = node.firstChild;
while (node) {
var nextNode = process(node);
var nextNode = null;
// do not recurse inside links if the filter would create a nested link
if (!(createsAnchor && node.tagName === 'A')) {
nextNode = process(node);
}
node = (nextNode ? nextNode : node).nextSibling;
}
};
@ -151,7 +161,14 @@ weechat.filter('getBufferQuickKeys', function () {
weechat.filter('emojify', function() {
return function(text, enable_JS_Emoji) {
if (enable_JS_Emoji === true && window.emojione !== undefined) {
return emojione.unicodeToImage(text);
// Emoji live in the D800-DFFF surrogate plane; only bother passing
// this range to CPU-expensive unicodeToImage();
var emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
if (emojiRegex.test(text)) {
return emojione.unicodeToImage(text);
} else {
return(text);
}
} else {
return(text);
}

@ -21,8 +21,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
};
$scope.command = '';
$scope.themes = ['dark', 'light'];
$scope.themes = ['dark', 'light', 'black'];
// Initialise all our settings, this needs to include all settings
// or else they won't be saved to the localStorage.
settings.setDefaults({
'theme': 'dark',
'host': 'localhost',
@ -38,30 +40,18 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
'useFavico': true,
'showtimestamp': true,
'showtimestampSeconds': false,
'soundnotification': true,
'fontsize': '14px',
'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'),
'readlineBindings': false,
'enableJSEmoji': (utils.isMobileUi() ? false : true),
'enableMathjax': false,
'customCSS': '',
});
$scope.settings = settings;
// From: http://stackoverflow.com/a/18539624 by StackOverflow user "plantian"
$rootScope.countWatchers = function () {
var q = [$rootScope], watchers = 0, scope;
while (q.length > 0) {
scope = q.pop();
if (scope.$$watchers) {
watchers += scope.$$watchers.length;
}
if (scope.$$childHead) {
q.push(scope.$$childHead);
}
if (scope.$$nextSibling) {
q.push(scope.$$nextSibling);
}
}
$log.debug(watchers);
$log.debug($rootScope.$$watchersCount);
};
$scope.isinstalled = (function() {
@ -168,7 +158,7 @@ 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.fullName, function() {
connection.requestNicklist(ab.id, function() {
$scope.showNicklist = $scope.updateShowNicklist();
// Scroll after nicklist has been loaded, as it may break long lines
$rootScope.scrollWithBuffer(true);
@ -214,8 +204,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
);
}
notifications.updateTitle(ab);
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
setTimeout(function(){
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
});
$timeout(function() {
$rootScope.scrollWithBuffer(true);
@ -262,8 +254,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
models.reinitialize();
$rootScope.$emit('notificationChanged');
$scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
});
$scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
$scope.getBuffers = models.getBuffers.bind(models);
@ -431,6 +425,24 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
})();
});
settings.addCallback('customCSS', function(css) {
// We need to delete the old tag and add a new one so that the browser
// notices the change. Thus, first remove old custom CSS.
var old_css = document.getElementById('custom-css-tag');
if (old_css) {
old_css.parentNode.removeChild(old_css);
}
// Create new CSS tag
var new_css = document.createElement("style");
new_css.type = "text/css";
new_css.id = "custom-css-tag";
new_css.appendChild(document.createTextNode(css));
// Append it to the <head> tag
var heads = document.getElementsByTagName("head");
heads[0].appendChild(new_css);
});
// Update font family when changed
settings.addCallback('fontfamily', function(fontfamily) {
@ -532,6 +544,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
return connection.fetchMoreLines(numLines);
};
$scope.infiniteScroll = function() {
// Check if we are already fetching
if ($rootScope.loadingLines) {
return;
}
var buffer = models.getActiveBuffer();
if (!buffer.allLinesFetched) {
$scope.fetchMoreLines();
}
};
$rootScope.updateBufferBottom = function(bottom) {
var eob = document.getElementById("end-of-buffer");
var bl = document.getElementById('bufferlines');
@ -580,11 +603,13 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.securityError = false;
$rootScope.errorMessage = false;
$rootScope.bufferBottom = true;
$scope.connectbutton = 'Connecting ...';
$scope.connectbutton = 'Connecting';
$scope.connectbuttonicon = 'glyphicon-refresh glyphicon-spin';
connection.connect(settings.host, settings.port, $scope.password, settings.ssl);
};
$scope.disconnect = function() {
$scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
connection.disconnect();
};
$scope.reconnect = function() {
@ -725,6 +750,18 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
settings.nonicklist = !settings.nonicklist;
};
$rootScope.switchToAdjacentBuffer = function(direction) {
// 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);
}
}
};
$scope.handleSearchBoxKey = function($event) {
// Support different browser quirks
@ -743,6 +780,23 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}
};
$rootScope.supports_formatting_date = (function() {
// function toLocaleDateStringSupportsLocales taken from MDN:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString#Checking_for_support_for_locales_and_options_arguments
try {
new Date().toLocaleDateString('i');
} catch (e) {
if (e.name !== 'RangeError') {
$log.info("Browser does not support toLocaleDateString()," +
" falling back to en-US");
}
return e.name === 'RangeError';
}
$log.info("Browser does not support toLocaleDateString()," +
" falling back to en-US");
return false;
})();
// Prevent user from accidentally leaving the page
window.onbeforeunload = function(event) {
@ -774,7 +828,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.securityError = false;
$rootScope.errorMessage = false;
$rootScope.bufferBottom = true;
$scope.connectbutton = 'Connecting ...';
$scope.connectbutton = 'Connecting';
$scope.connectbuttonicon = 'glyphicon-chevron-right';
connection.connect(host, port, password, ssl);
}
};

@ -13,18 +13,142 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
models.version = version.split(".").map(function(c) { return parseInt(c); });
};
var handleConfValue = function(message) {
var infolist = message.objects[0].content;
for (var i = 0; i < infolist.length ; i++) {
var key, val;
var item = infolist[i];
for (var j = 0; j < item.length ; j++) {
var confitem = item[j];
if (confitem.full_name) {
key = confitem.full_name;
}
if (confitem.value) {
val = confitem.value;
}
}
if (key && val) {
$log.debug('Setting wconfig "' + key + '" to value "' + val + '"');
models.wconfig[key] = val;
}
}
};
var handleBufferClosing = function(message) {
var bufferMessage = message.objects[0].content[0];
var bufferId = bufferMessage.pointers[0];
models.closeBuffer(bufferId);
};
// inject a fake buffer line for date change if needed
var injectDateChangeMessageIfNeeded = function(buffer, manually, old_date, new_date) {
if (buffer.bufferType === 1) {
// Don't add date change messages to free buffers
return;
}
old_date.setHours(0, 0, 0, 0);
new_date.setHours(0, 0, 0, 0);
// Check if the date changed
if (old_date.valueOf() !== new_date.valueOf()) {
if (manually) {
// if the message that caused this date change to be sent
// would increment buffer.lastSeen, we should increment as
// well.
++buffer.lastSeen;
}
var old_date_plus_one = old_date;
old_date_plus_one.setDate(old_date.getDate() + 1);
// it's not always true that a date with time 00:00:00
// plus one day will be time 00:00:00
old_date_plus_one.setHours(0, 0, 0, 0);
var content = "\u001943"; // this colour corresponds to chat_day_change
// Add day of the week
if ($rootScope.supports_formatting_date) {
content += new_date.toLocaleDateString(window.navigator.language,
{weekday: "long"});
} else {
// Gross code that only does English dates ew gross
var dow_to_word = [
"Sunday", "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday", "Saturday"];
content += dow_to_word[new_date.getDay()];
}
// if you're testing different date formats,
// make sure to test different locales such as "en-US",
// "en-US-u-ca-persian" (which has different weekdays, year 0, and an ERA)
// "ja-JP-u-ca-persian-n-thai" (above, diff numbering, diff text)
var extra_date_format = {
day: "numeric",
month: "long"
};
if (new_date.getYear() !== old_date.getYear()) {
extra_date_format.year = "numeric";
}
content += " (";
if ($rootScope.supports_formatting_date) {
content += new_date.toLocaleDateString(window.navigator.language,
extra_date_format);
} else {
// ew ew not more gross code
var month_to_word = [
"January", "February", "March", "April",
"May", "June", "July", "August",
"September", "October", "November", "December"];
content += month_to_word[new_date.getMonth()] + " " + new_date.getDate().toString();
if (extra_date_format.year === "numeric") {
content += ", " + new_date.getFullYear().toString();
}
}
// Result should be something like
// Friday (November 27)
// or if the year is different,
// Friday (November 27, 2015)
// Comparing dates in javascript is beyond tedious
if (old_date_plus_one.valueOf() !== new_date.valueOf()) {
var date_diff = Math.round((new_date - old_date)/(24*60*60*1000)) + 1;
if (date_diff < 0) {
date_diff = -1*(date_diff);
if (date_diff === 1) {
content += ", 1 day before";
} else {
content += ", " + date_diff + " days before";
}
} else {
content += ", " + date_diff + " days later";
}
// Result: Friday (November 27, 5 days later)
}
content += ")";
var line = {
buffer: buffer.id,
date: new_date,
prefix: '\u001943\u2500',
tags_array: [],
displayed: true,
highlight: 0,
message: content
};
var new_message = new models.BufferLine(line);
buffer.addLine(new_message);
}
};
var handleLine = function(line, manually) {
var message = new models.BufferLine(line);
var buffer = models.getBuffer(message.buffer);
buffer.requestedLines++;
// Only react to line if its displayed
if (message.displayed) {
// Check for date change
if (buffer.lines.length > 0) {
var old_date = new Date(buffer.lines[buffer.lines.length - 1].date),
new_date = new Date(message.date);
injectDateChangeMessageIfNeeded(buffer, manually, old_date, new_date);
}
message = plugins.PluginManager.contentForMessage(message);
buffer.addLine(message);
@ -171,9 +295,22 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
// Update indentation status
old.type = localvars.type;
old.indent = (['channel', 'private'].indexOf(localvars.type) >= 0);
// Update serverSortKey and related variables
old.plugin = localvars.plugin;
old.server = localvars.server;
old.serverSortKey = old.plugin + "." + old.server +
(old.type === "server" ? "" : ("." + old.shortName));
}
};
var handleBufferTypeChanged = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
// 0 = formatted (normal); 1 = free
buffer.bufferType = obj.type;
};
/*
* Handle answers to (lineinfo) messages
*
@ -187,6 +324,16 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
lines.forEach(function(l) {
handleLine(l, manually);
});
if (message.objects[0].content.length > 0) {
// fiddle out the buffer ID and take the last line's date
var last_line =
message.objects[0].content[message.objects[0].content.length-1];
var buffer = models.getBuffer(last_line.buffer);
if (buffer.lines.length > 0) {
var last_date = new Date(buffer.lines[buffer.lines.length - 1].date);
injectDateChangeMessageIfNeeded(buffer, true, last_date, new Date());
}
}
};
/*
@ -268,6 +415,7 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
_buffer_localvar_changed: handleBufferLocalvarChanged,
_buffer_opened: handleBufferOpened,
_buffer_title_changed: handleBufferTitleChanged,
_buffer_type_changed: handleBufferTypeChanged,
_buffer_renamed: handleBufferRenamed,
_buffer_hidden: handleBufferHidden,
_buffer_unhidden: handleBufferUnhidden,
@ -291,6 +439,7 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
return {
handleVersionInfo: handleVersionInfo,
handleConfValue: handleConfValue,
handleEvent: handleEvent,
handleLineInfo: handleLineInfo,
handleHotlistInfo: handleHotlistInfo,

@ -0,0 +1,49 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('imgurDrop', ['connection','imgur','$rootScope', function(connection, imgur, $rootScope) {
return {
restrict: 'A',
link: function($scope, element, attr) {
var elem = element[0];
elem.ondragover = function () { this.classList.add('imgur-drop-hover'); return false; };
elem.ondragend = function () { this.classList.remove('imgur-drop-hover'); return false; };
elem.ondrop = function(e) {
// Remove hover class
this.classList.remove('imgur-drop-hover');
// Get files
var files = e.dataTransfer.files;
// Stop default behaviour
e.stopPropagation();
e.preventDefault();
// Send image url after upload
var sendImageUrl = function(imageUrl) {
// Send image
if(imageUrl !== undefined && imageUrl !== '') {
$rootScope.insertAtCaret(String(imageUrl));
}
};
// Check files
if(typeof files !== "undefined" && files.length > 0) {
// Loop through files
for (var i = 0; i < files.length; i++) {
// Upload to imgur
imgur.process(files[i], sendImageUrl);
}
}
};
}
};
}]);
})();

@ -0,0 +1,128 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.factory('imgur', ['$rootScope', function($rootScope) {
var process = function(image, callback) {
// Is it an image?
if (!image || !image.type.match(/image.*/)) return;
// New file reader
var reader = new FileReader();
// When image is read
reader.onload = function (event) {
var image = event.target.result.split(',')[1];
upload(image, callback);
};
// Read image as data url
reader.readAsDataURL(image);
};
// Upload image to imgur from base64
var upload = function( base64img, callback ) {
// Set client ID (Glowing Bear)
var clientId = "164efef8979cd4b";
// Progress bars container
var progressBars = document.getElementById("imgur-upload-progress"),
currentProgressBar = document.createElement("div");
// Set progress bar attributes
currentProgressBar.className='imgur-progress-bar';
currentProgressBar.style.width = '0';
// Append progress bar
progressBars.appendChild(currentProgressBar);
// Create new form data
var fd = new FormData();
fd.append("image", base64img); // Append the file
fd.append("type", "base64"); // Set image type to base64
// Create new XMLHttpRequest
var xhttp = new XMLHttpRequest();
// Post request to imgur api
xhttp.open("POST", "https://api.imgur.com/3/image", true);
// Set headers
xhttp.setRequestHeader("Authorization", "Client-ID " + clientId);
xhttp.setRequestHeader("Accept", "application/json");
// Handler for response
xhttp.onload = function() {
// Remove progress bar
currentProgressBar.parentNode.removeChild(currentProgressBar);
// Check state and response status
if(xhttp.status === 200) {
// Get response text
var response = JSON.parse(xhttp.responseText);
// Send link as message
if( response.data && response.data.link ) {
if (callback && typeof(callback) === "function") {
callback(response.data.link);
}
} else {
showErrorMsg();
}
} else {
showErrorMsg();
}
};
if( "upload" in xhttp ) {
// Set progress
xhttp.upload.onprogress = function (event) {
// Check if we can compute progress
if (event.lengthComputable) {
// Complete in percent
var complete = (event.loaded / event.total * 100 | 0);
// Set progress bar width
currentProgressBar.style.width = complete + '%';
}
};
}
// Send request with form data
xhttp.send(fd);
};
var showErrorMsg = function() {
// Show error msg
$rootScope.uploadError = true;
$rootScope.$apply();
// Hide after 5 seconds
setTimeout(function(){
// Hide error msg
$rootScope.uploadError = false;
$rootScope.$apply();
}, 5000);
};
return {
process: process
};
}]);
})();

@ -14,11 +14,12 @@ weechat.directive('inputBar', function() {
command: '=command'
},
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', 'settings', function($rootScope,
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'imgur', 'models', 'IrcUtils', 'settings', function($rootScope,
$scope,
$element, //XXX do we need this? don't seem to be using it
$log,
connection, //XXX we should eliminate this dependency and use signals instead
imgur,
models,
IrcUtils,
settings) {
@ -69,6 +70,49 @@ weechat.directive('inputBar', function() {
}, 0);
};
$rootScope.insertAtCaret = function(toInsert) {
// caret position in the input bar
var inputNode = $scope.getInputNode(),
caretPos = inputNode.selectionStart;
var prefix = $scope.command.substring(0, caretPos),
suffix = $scope.command.substring(caretPos, $scope.command.length);
// Add spaces if missing
if (prefix.length > 0 && prefix[prefix.length - 1] !== ' ') {
prefix += ' ';
}
if (suffix.length > 0 && suffix[0] !== ' ') {
suffix = ' '.concat(suffix);
}
$scope.command = prefix + toInsert + suffix;
setTimeout(function() {
inputNode.focus();
var pos = $scope.command.length - suffix.length;
inputNode.setSelectionRange(pos, pos);
// force refresh?
$scope.$apply();
}, 0);
};
$scope.uploadImage = function($event, files) {
// Send image url after upload
var sendImageUrl = function(imageUrl) {
// Send image
if(imageUrl !== undefined && imageUrl !== '') {
$rootScope.insertAtCaret(String(imageUrl));
}
};
if(typeof files !== "undefined" && files.length > 0) {
// Loop through files
for (var i = 0; i < files.length; i++) {
// Process image
imgur.process(files[i], sendImageUrl);
}
}
};
// Send the message to the websocket
$scope.sendMessage = function() {
@ -250,6 +294,14 @@ weechat.directive('inputBar', function() {
return true;
}
// Alt+Arrow up/down -> switch to prev/next adjacent buffer
if ($event.altKey && !$event.ctrlKey && (code === 38 || code === 40)) {
$event.preventDefault();
var direction = code - 39;
$rootScope.switchToAdjacentBuffer(direction);
return true;
}
// Alt+L -> focus on input bar
if ($event.altKey && (code === 76 || code === 108)) {
$event.preventDefault();

@ -11,6 +11,9 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
// WeeChat version
this.version = null;
// WeeChat configuration values
this.wconfig = {};
// Save outgoing queries
this.outgoingQueries = [];
@ -84,10 +87,22 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var notification = 0;
var unread = 0;
var lastSeen = -1;
var serverSortKey = fullName.replace(/^irc\.server\.(\w+)/, "irc.$1");
// There are two kinds of types: bufferType (free vs formatted) and
// the kind of type that distinguishes queries from channels etc
var bufferType = message.type;
var type = message.local_variables.type;
var indent = (['channel', 'private'].indexOf(type) >= 0);
var plugin = message.local_variables.plugin;
var server = message.local_variables.server;
// 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 +
(type === "server" ? "" : ("." + shortName));
// Lowercase it so alt+up/down traverses buffers in the same order
// angular's sortBy directive puts them in
serverSortKey = serverSortKey.toLowerCase();
// Buffer opened message does not include notify level
if (message.notify !== undefined) {
notify = message.notify;
@ -311,7 +326,10 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
getNicklistByTime: getNicklistByTime,
serverSortKey: serverSortKey,
indent: indent,
bufferType: bufferType,
type: type,
plugin: plugin,
server: server,
history: history,
addToHistory: addToHistory,
getHistoryUp: getHistoryUp,

@ -1,8 +1,9 @@
var weechat = angular.module('weechat');
weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', function($rootScope, $log, models, settings) {
// Ask for permission to display desktop notifications
var serviceworker = false;
var notifications = [];
// Ask for permission to display desktop notifications
var requestNotificationPermission = function() {
// Firefox
if (window.Notification) {
@ -22,6 +23,81 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
window.webkitNotifications.requestPermission();
}
}
if ('serviceWorker' in navigator) {
$log.info('Service Worker is supported');
navigator.serviceWorker.register('serviceworker.js').then(function(reg) {
$log.info('Service Worker install:', reg);
serviceworker = true;
}).catch(function(err) {
$log.info('Service Worker err:', err);
});
}
};
var showNotification = function(buffer, title, body) {
if (serviceworker) {
navigator.serviceWorker.ready.then(function(registration) {
registration.showNotification(title, {
body: body,
icon: 'assets/img/glowing_bear_128x128.png',
vibrate: [200, 100],
tag: 'gb-highlight-vib'
});
});
} else if (typeof Windows !== 'undefined' && typeof Windows.UI !== 'undefined' && typeof Windows.UI.Notifications !== 'undefined') {
var winNotifications = Windows.UI.Notifications;
var toastNotifier = winNotifications.ToastNotificationManager.createToastNotifier();
var template = winNotifications.ToastTemplateType.toastText02;
var toastXml = winNotifications.ToastNotificationManager.getTemplateContent(template);
var toastTextElements = toastXml.getElementsByTagName("text");
toastTextElements[0].appendChild(toastXml.createTextNode(title));
toastTextElements[1].appendChild(toastXml.createTextNode(body));
var toast = new winNotifications.ToastNotification(toastXml);
toast.onactivated = function() {
models.setActiveBuffer(buffer.id);
window.focus();
};
toastNotifier.show(toast);
} else {
var notification = new Notification(title, {
body: body,
icon: 'assets/img/favicon.png'
});
// Save notification, so we can close all outstanding ones when disconnecting
notification.id = notifications.length;
notifications.push(notification);
// Cancel notification automatically
var timeout = 15*1000;
notification.onshow = function() {
setTimeout(function() {
notification.close();
}, timeout);
};
// Click takes the user to the buffer
notification.onclick = function() {
models.setActiveBuffer(buffer.id);
window.focus();
notification.close();
};
// Remove from list of active notifications
notification.onclose = function() {
delete notifications[this.id];
};
}
};
@ -83,7 +159,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
var body = '';
var numNotifications = buffer.notification;
if (['#', '&', '+', '!'].indexOf(buffer.shortName.charAt(0)) < 0) {
if (buffer.type === "private") {
if (numNotifications > 1) {
title = numNotifications.toString() + ' private messages from ';
} else {
@ -102,37 +178,9 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
}
body = '<' + prefix + '> ' + message.text;
}
title += buffer.shortName;
title += buffer.fullName.replace(/irc.([^\.]+)\..+/, " ($1)");
var notification = new Notification(title, {
body: body,
icon: 'assets/img/favicon.png'
});
// Save notification, so we can close all outstanding ones when disconnecting
notification.id = notifications.length;
notifications.push(notification);
// Cancel notification automatically
var timeout = 15*1000;
notification.onshow = function() {
setTimeout(function() {
notification.close();
}, timeout);
};
// Click takes the user to the buffer
notification.onclick = function() {
models.setActiveBuffer(buffer.id);
window.focus();
notification.close();
};
// Remove from list of active notifications
notification.onclose = function() {
delete notifications[this.id];
};
title += buffer.shortName + " (" + buffer.server + ")";
showNotification(buffer, title, body);
if (settings.soundnotification) {
// TODO fill in a sound file

@ -23,7 +23,7 @@ var Plugin = function(name, contentForMessage) {
// Regular expression that detects URLs for UrlPlugin
var urlRegexp = /(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g;
var urlRegexp = /(?:(?:https?|ftp):\/\/|www\.|ftp\.)\S*[^\s.;,(){}<>]/g;
/*
* Definition of a user provided plugin that consumes URLs
*
@ -280,10 +280,25 @@ plugins.factory('userPlugins', function() {
} 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:/, "");
} else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\/[^?]+$/i)) {
} else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\//i)) {
// Dropbox requires a get parameter, dl=1
// TODO strip an existing dl=0 parameter
url = url + "?dl=1";
var dbox_url = document.createElement("a");
dbox_url.href = url;
var base_url = dbox_url.protocol + '//' + dbox_url.host + dbox_url.pathname + '?';
var dbox_params = dbox_url.search.substring(1).split('&');
var dl_added = false;
for (var i = 0; i < dbox_params.length; i++) {
if (dbox_params[i].split('=')[0] === "dl") {
dbox_params[i] = "dl=1";
dl_added = true;
// we continue looking at the other parameters in case
// it's specified twice or something
}
}
if (!dl_added) {
dbox_params.push("dl=1");
}
url = base_url + dbox_params.join('&');
}
return function() {
var element = this.getElement();
@ -298,14 +313,36 @@ plugins.factory('userPlugins', function() {
}
});
/*
* audio Preview
*/
var audioPlugin = new UrlPlugin('audio', function(url) {
if (url.match(/\.(mp3|ogg|wav)\b/i)) {
return function() {
var element = this.getElement();
var aelement = angular.element('<audio controls></audio>')
.addClass('embed')
.attr('width', '560')
.append(angular.element('<source></source>')
.attr('src', url));
element.innerHTML = aelement.prop('outerHTML');
};
}
});
/*
* mp4 video Preview
*/
var videoPlugin = new UrlPlugin('video', function(url) {
if (url.match(/\.(mp4|webm|ogv)\b/i)) {
if (url.match(/\.(mp4|webm|ogv|gifv)\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");
}
return function() {
var element = this.getElement();
var velement = angular.element('<video></video>')
var velement = angular.element('<video autoplay loop muted></video>')
.addClass('embed')
.attr('width', '560')
.append(angular.element('<source></source>')
@ -314,6 +351,7 @@ plugins.factory('userPlugins', function() {
};
}
});
/*
* Cloud Music Embedded Players
@ -484,7 +522,7 @@ plugins.factory('userPlugins', function() {
});
return {
plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, giphyPlugin, tweetPlugin, vinePlugin]
plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, giphyPlugin, tweetPlugin, vinePlugin]
};

@ -23,9 +23,7 @@
'buf': this._getString,
'arr': this._getArray,
'htb': this._getHashTable,
'inl': function() {
this._warnUnimplemented('infolist');
}
'inl': this._getInfolist,
};
// string value for some object types
@ -290,7 +288,7 @@
var ret = {};
var optionCode = parseInt(m[1]);
if (optionCode > 43) {
if (optionCode >= WeeChatProtocol._colorsOptionsNames.length) {
// should never happen
return {
fgColor: null,
@ -699,6 +697,37 @@
return WeeChatProtocol._formatCmd(params.id, 'info', parts);
};
/**
* Formats an infolist command.
*
* @param params Parameters:
* id: command ID (optional)
* name: infolist name (mandatory)
* pointer: optional
* arguments: optional
* @return Formatted infolist command string
*/
WeeChatProtocol.formatInfolist = function(params) {
var defaultParams = {
id: null,
pointer: null,
args: null
};
var parts = [];
params = WeeChatProtocol._mergeParams(defaultParams, params);
parts.push(params.name);
if (params.pointer !== null) {
parts.push(params.pointer);
}
if (params.pointer !== null) {
parts.push(params.args);
}
return WeeChatProtocol._formatCmd(params.id, 'infolist', parts);
};
/**
* Formats a nicklist command.
*
@ -1143,6 +1172,35 @@
return values;
},
/**
* Reads an infolist object from the current set of data
*
* @return Array
*/
_getInfolist: function() {
var self = this;
var name;
var count;
var values;
name = this._getString();
count = this._getInt();
values = [];
for (var i = 0; i < count; i++) {
var itemcount = self._getInt();
var litem = [];
for (var j = 0; j < itemcount; j++) {
var item = {};
item[self._getString()] = self._runType(self._getType());
litem.push(item);
}
values.push(litem);
}
return values;
},
/**
* Reads a specified number of bytes from current set data.
*

@ -0,0 +1,21 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('whenScrolled', function() {
return function(scope, elm, attr) {
var raw = elm[0];
var fun = function() {
if (raw.scrollTop === 0) {
scope.$apply(attr.whenScrolled);
}
};
elm.bind('scroll', function() {
_.debounce(fun, 200)();
});
};
});
})();

@ -1,7 +1,7 @@
{
"name": "Glowing Bear",
"description": "WeeChat Web frontend",
"version": "0.5.2",
"version": "0.6.0",
"manifest_version": 2,
"icons": {
"32": "assets/img/favicon.png",

@ -25,5 +25,5 @@
"desktop-notification":{}
},
"default_locale": "en",
"version": "0.5.2"
"version": "0.6.0"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,38 +1,36 @@
{
"name": "glowing-bear",
"private": true,
"version": "0.5.2",
"version": "0.6.0",
"description": "A web client for Weechat",
"repository": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3",
"devDependencies": {
"karma": "~0.10",
"protractor": "~0.20.1",
"http-server": "^0.6.1",
"bower": "^1.3.1",
"shelljs": "^0.2.6",
"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"
},
"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/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js -c -m --screw-ie8 -o min.js --source-map min.map",
"minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/settings.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/file-change.js js/imgur-drop-directive.js js/whenscrolled-directive.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js js/imgur.js -c -m --screw-ie8 -o min.js --source-map min.map",
"prestart": "npm install",
"start": "http-server -a localhost -p 8000",
"pretest": "npm install",
"test": "karma start test/karma.conf.js",
"test-single-run": "karma start test/karma.conf.js --single-run",
"preupdate-webdriver": "npm install",
"update-webdriver": "webdriver-manager update",
"preprotractor": "npm run update-webdriver",
"protractor": "protractor test/protractor-conf.js",
"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');\""
}
}

@ -0,0 +1,46 @@
// File needs to be stored in the root of the app.
this.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'assets/img/glowing_bear_128x128.png',
]);
})
);
});
this.addEventListener('push', function(event) {
// TODO, support GCM here
var title = 'Push message';
event.waitUntil(
self.registration.showNotification(title, {
body: 'The Message',
icon: 'assets/img/favicon.png',
tag: 'my-tag'
}));
});
this.onnotificationclick = function(event) {
// Android doesn't close the notification when you click on it
// See: http://crbug.com/463146
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.waitUntil(clients.matchAll({
type: "window"
}).then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if ('focus' in client) {
return client.focus();
}
}
/*
if (clients.openWindow) {
return clients.openWindow('/glowing-bear/');
}
*/
}));
};

@ -22,6 +22,13 @@ describe('Filters', function() {
it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) {
expect(irclinkyFilter('<"#foo">')).toEqual('<"<a href="#" onclick="openBuffer(\'#foo">\');">#foo"></a>');
}));
it('should not touch links created by `linky`', inject(function(linkyFilter, DOMfilterFilter) {
var url = 'http://foo.bar/#baz',
link = linkyFilter(url, '_blank'),
result = DOMfilterFilter(link, 'irclinky').$$unwrapTrustedValue();
expect(result).toEqual(link);
}));
});
describe('inlinecolour', function() {
@ -62,4 +69,27 @@ describe('Filters', function() {
}));
});
describe('DOMfilter', function() {
it('should run a filter on all text nodes', inject(function(DOMfilterFilter) {
var dom = 'a<p>b<i>c<b>d</b>e<b>f</b>g</i>h</p>i',
expected = '<span>A</span><p><span>B</span><i><span>C</span><b><span>D</span></b><span>E</span><b><span>F</span></b><span>G</span></i><span>H</span></p><span>I</span>',
result = DOMfilterFilter(dom, 'uppercase').$$unwrapTrustedValue();
expect(result).toEqual(expected);
}));
it('should pass additional arguments to the filter', inject(function(DOMfilterFilter) {
var dom = '1<p>2</p>3.14159265',
expected = '<span>1.00</span><p><span>2.00</span></p><span>3.14</span>',
result = DOMfilterFilter(dom, 'number', 2).$$unwrapTrustedValue();
expect(result).toEqual(expected);
}));
it('should never lock up like in bug #688', inject(function(linkyFilter, DOMfilterFilter) {
var msg = '#crash http://google.com',
linked = linkyFilter(msg),
irclinked = DOMfilterFilter(linked, 'irclinky');
// With the bug, the DOMfilterFilter call ends up in an infinite loop.
// I.e. if we ever got this far, the bug is fixed.
}));
});
});

@ -0,0 +1,33 @@
{
"lang": "en-US",
"name": "Glowing Bear",
"short_name": "Glowing Bear",
"icons": [{
"src": "assets/img/glowing_bear_60x60.png",
"sizes": "60x60",
"type": "image/webapp"
}, {
"src": "assets/img/glowing_bear_90x90.png",
"sizes": "90x90"
}, {
"src": "assets/img/glowing_bear_128x128.png",
"sizes": "128x128"
}],
"splash_screens": [{
"src": "assets/img/glowing_bear_128x128.png",
"sizes": "128x128"
}],
"scope": "/glowing-bear/",
"start_url": "/glowing-bear/index.html",
"display": "standalone",
"orientation": "portrait-primary",
"theme_color": "#181818",
"background_color": "#333",
"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