Merge branch 'master' into gh-pages (v0.4.4)

This merge includes a minification run

Conflicts:
	index.html
gh-pages
Lorenz Hübschle-Schneider 11 years ago
commit a28a304e60
  1. 9
      .jshintrc
  2. 7
      3rdparty/favico-0.3.4-mod.min.js
  3. 7
      3rdparty/favico-0.3.5.min.js
  4. 180
      css/glowingbear.css
  5. 165
      css/themes/dark.css
  6. 2092
      css/themes/light.css
  7. 2
      directives/input.html
  8. 6
      directives/plugin.html
  9. 85
      index.html
  10. 276
      js/connection.js
  11. 128
      js/filters.js
  12. 1231
      js/glowingbear.js
  13. 213
      js/handlers.js
  14. 404
      js/inputbar.js
  15. 42
      js/irc-utils.js
  16. 12
      js/models.js
  17. 141
      js/notifications.js
  18. 82
      js/plugin-directive.js
  19. 18
      js/plugins.js
  20. 29
      js/utils.js
  21. 15
      js/websockets.js
  22. 24
      js/weechat.js
  23. 2
      manifest.json
  24. 2
      manifest.webapp
  25. 4
      min.js
  26. 2
      min.map
  27. 4
      package.json
  28. 15
      test/karma.conf.js

@ -1,8 +1,11 @@
{
"browser": true,
"devel": true,
"globals": {
"angular": false,
"$": false,
"window": false,
"console": false
"weeChat": false,
"_": false,
"Notification": false,
"Favico": false
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -2,52 +2,15 @@ html,
body {
height: 100%;
/* The html and body elements cannot have any padding or margin. */
color: #ddd;
background-color: #181818;
}
.no-overflow {
overflow: hidden;
}
.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 #121212;
}
.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 #121212;
}
.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 #121212;
}
.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 #121212;
}
a {
cursor: pointer;
}
.repeated-time {
}
.repeated-time .cof-chat_time,
.repeated-time .cof-chat_time_delimiters {
color: #333;
}
.repeated-time .cob-chat_time,
.repeated-time .cob-chat_time_delimiters {
background-color: transparent;
}
td.prefix {
text-align: right;
vertical-align: top;
@ -72,13 +35,15 @@ td.message {
hyphens: auto;
}
#readmarker {
margin-top: 5px;
margin-bottom: 5px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
border-bottom: 1px solid #121212;
border-top: 1px solid;
border-bottom: 1px solid;
height: 2px;
}
.text {
white-space: pre-wrap;
}
@ -97,11 +62,9 @@ td.message {
input[type=text], input[type=password], #sendMessage, .badge {
border: 0;
border-radius: 0;
color: #ccc;
box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset;
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3);
margin-bottom: 5px !important;
}
.input-group {
width: 100%;
}
@ -162,8 +125,8 @@ input[type=text], input[type=password], #sendMessage, .badge {
font-size: 30px;
position: fixed;
right: 0;
background: #282828;
}
#topbar .actions > * {
padding-left: 5px;
}
@ -172,9 +135,6 @@ input[type=text], input[type=password], #sendMessage, .badge {
padding-right: 6px;
}
#topbar, #sidebar, .panel, .dropdown-menu, .modal-content {
background: #282828;
}
#sidebar {
position: fixed;
width: 140px;
@ -204,7 +164,7 @@ input[type=text], input[type=password], #sendMessage, .badge {
#sidebar .badge {
border-radius: 0;
margin-right: -15px;
margin-right: -10px;
}
#sidebar ul.indented li.indent span.buffername {
@ -224,7 +184,7 @@ input[type=text], input[type=password], #sendMessage, .badge {
overflow-x: hidden;
right: 0;
top: 0;
padding-top: 35px;
padding-top: 39px;
padding-left: 5px;
padding-bottom: 35px;
z-index: 2;
@ -240,22 +200,19 @@ input[type=text], input[type=password], #sendMessage, .badge {
#nicklist a {
text-decoration: none;
}
#nicklist a:hover {
background: #3b3b3b;
}
#connection-infos {
float: left;
max-width: 10%;
padding-left: 5px;
font-size: 12px;
color: #aaa;
overflow: hidden;
}
.nav-pills > li > a {
border-radius: 0;
color: #ddd;
padding: 5px 10px;
}
.nav-pills > li > a:hover, .nav-pills > li > a:hover span {
color: #222;
@ -266,6 +223,7 @@ input[type=text], input[type=password], #sendMessage, .badge {
background-color: #eee;
color: #222;
}
.content {
height: 100%;
min-height: 100%;
@ -276,21 +234,20 @@ input[type=text], input[type=password], #sendMessage, .badge {
height: 100%;
overflow-y: auto;
width: auto;
bottom: 42px;
bottom: 35px; /* input bar */
padding-top: 42px; /* topbar */
padding-bottom: 7px;
-webkit-transition:0.35s ease all;
transition:0.35s ease all;
}
#bufferlines > table {
margin-top: 35px;
margin-top: 35px;
width: 100%;
}
tr.bufferline {
line-height: 100%;
}
tr.bufferline:hover {
background-color: #222222;
}
td.time {
padding: 1px 5px 1px 1px;
vertical-align: top;
@ -323,25 +280,9 @@ td.time {
padding-right: 100px;
}
.color-light-green {
color: chartreuse;
}
.color-27 {
color: deepskyblue;
}
.danger, .alert-danger, .badge .alert-danger {
background-color: rgb(217, 83, 79);
color: #ddd;
}
.alert-danger {
border-color: #121212;
color: black;
}
li.notification {
color: green;
/* fix for mobile firefox which ignores :hover */
.nav-pills > li > a:active, .nav-pills > li > a:active span {
text-decoration: none;
}
[ng-click],
@ -353,12 +294,9 @@ li.notification {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track-piece {
background-color: black;
}
::-webkit-scrollbar-thumb:vertical {
height: 15px;
background: rgba(255,255,255,0.5);
}
div.embed * {
@ -389,6 +327,30 @@ table.notimestampseconds td.time span.seconds {
display: none !important;
}
#sidebar .showquickkeys .buffer .buffer-quick-key {
transition: all ease 0.5s;
-webkit-transition: all ease 0.5s;
transition-delay: 0.2s;
-webkit-transition-delay: 0.2s;
opacity: 0.7;
}
#sidebar .buffer .buffer-quick-key {
margin-left: -0.7em;
margin-right: -0.2em;
font-size: smaller;
transition: all ease 0.5s;
-webkit-transition: all ease 0.5s;
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%;
@ -427,8 +389,8 @@ table.notimestampseconds td.time span.seconds {
height: 100%;
width: 100%;
overflow: none;
background-color:rgba(0, 0, 0, 0.5)
}
.gb-modal .modal-dialog {
z-index: 1001;
position: absolute;
@ -458,11 +420,12 @@ table.notimestampseconds td.time span.seconds {
border-bottom: 0;
}
#fontchoice label {
.standard-labels label {
font-weight: normal;
text-align: left;
}
h2 {
padding-bottom: 5px;
height: 72px;
@ -511,11 +474,43 @@ h2 span, h2 small {
display: none;
}
}
/* bold hash before channels */
li.buffer.channel a span:last-of-type:before {
color: #888;
content: "#";
font-weight: bold;
}
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;
}
li.buffer.indent.private a {
padding-left: 17px;
}
.make-thinner {
padding-right: -15px;
}
.settings-help {
display: block;
margin: -5px 0 -3px 19px;
font-size: small;
}
.unselectable {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
/* */
/* Mobile layout */
/* */
@ -572,14 +567,10 @@ h2 span, h2 small {
#nicklist {
height: auto;
width: auto;
padding: 35px 7px 35px 10px;
text-align: center;
-webkit-box-shadow: 0px 0px 120px #000;
box-shadow: 0px 0px 120px #000;
position: fixed;
margin-top: 10px;
background: #282828;
bottom: 0px;
}
@ -591,6 +582,10 @@ h2 span, h2 small {
min-height: 0%;
}
.nav-pills > li > a {
padding: 10px 15px;
}
#bufferlines {
height: 100%;
padding-bottom: 0;
@ -598,12 +593,12 @@ h2 span, h2 small {
#bufferlines td.time {
padding-right: 3px;
font-size: 12px;
font-size: 0.8em;
}
#bufferlines td.time span.date {
display: block;
margin-top: -2px;
margin-bottom: -1px;
}
#bufferlines td.prefix {
@ -611,7 +606,7 @@ h2 span, h2 small {
padding-right: 5px;
border: 0;
font-weight: bold;
font-size: 15px;
font-size: 1.06em;
}
#bufferlines td.message {
@ -624,17 +619,10 @@ h2 span, h2 small {
width: 96%;
}
/* a different colour is too irregular on mobile */
.repeated-time .cof-chat_time,
.repeated-time .cof-chat_time_delimiters {
color: #999;
}
.footer {
padding-left: 0px !important;
padding-right: 0px !important;
width: 100% !important;
background: rgb(24,24,24);
}
.footer.withnicklist {

@ -1,3 +1,131 @@
body {
background-color: #181818;
color: #ddd;
}
.form-control {
color: #ccc;
box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset;
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3);
border: 0px none;
}
html {
background-color: inherit;
}
.repeated-time .cob-chat_time,
.repeated-time .cob-chat_time_delimiters {
background-color: transparent;
color: #333;
}
/* fix for mobile firefox which ignores :hover */
.nav-pills > li > a:active, .nav-pills > li > a:active span {
background-color: #eee;
color: #222;
}
tr.bufferline:hover {
background-color: #222222;
}
.danger, .alert-danger, .badge .alert-danger, .badge.danger {
background-color: rgb(217, 83, 79);
color: #ddd;
}
.alert-danger {
border-color: #121212;
color: black;
}
li.notification {
color: green;
}
::-webkit-scrollbar-track-piece {
background-color: black;
}
::-webkit-scrollbar-thumb:vertical {
height: 15px;
background: rgba(255,255,255,0.5);
}
.gb-modal .backdrop {
background-color:rgba(0, 0, 0, 0.5)
}
input[type=text], input[type=password], #sendMessage, .badge {
color: #ccc;
box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset;
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3);
}
#connection-infos {
color: #aaa;
}
.nav-pills > li > a {
color: #ddd;
}
.nav-pills > li > a:hover, .nav-pills > li > a:hover span {
color: #222;
}
.color-light-green {
color: chartreuse;
}
.color-27 {
color: deepskyblue;
}
#topbar .actions {
background: #282828;
}
#topbar, #sidebar, .panel, .dropdown-menu, .modal-content {
background: #282828;
}
#nicklist a:hover {
background: #3b3b3b;
}
.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 #121212;
}
.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 #121212;
}
.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 #121212;
}
.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 #121212;
}
#readmarker {
border-top-color: rgba(255, 255, 255, 0.3);
border-bottom-color: #121212;
}
/****************************/
/* Weechat colors and style */
/****************************/
/* style options, foreground */
.cof-separator {
color: #68b5d4;
@ -263,6 +391,9 @@
.cwf-default {
color: #d9d9d9;
}
.light-theme .cwf-default {
color: #282828;
}
.cwf-black {
color: #000000;
}
@ -370,13 +501,13 @@
color: #000000; /* 000 Black */
}
.cef-1 {
color: #cd0000; /* 001 DarkRed */
color: #7f0000; /* 001 DarkRed */
}
.cef-2 {
color: #00cd00; /* 002 DarkGreen */
}
.cef-3 {
color: #cdcd00; /* 003 DarkYellow */
color: #fc7f00; /* 003 Orange */
}
.cef-4 {
color: #0000ee; /* 004 DarkBlue */
@ -1140,13 +1271,13 @@
background-color: #000000; /* 000 Black */
}
.ceb-1 {
background-color: #cd0000; /* 001 DarkRed */
background-color: #7f0000; /* 001 DarkRed */
}
.ceb-2 {
background-color: #00cd00; /* 002 DarkGreen */
}
.ceb-3 {
background-color: #cdcd00; /* 003 DarkYellow */
background-color: #fc7f00; /* 003 Orange */
}
.ceb-4 {
background-color: #0000ee; /* 004 DarkBlue */
@ -1935,3 +2066,29 @@
color: yellow;
font-weight: bold;
}
/* */
/* Mobile layout */
/* */
@media (max-width: 968px) {
/* a different colour is too irregular on mobile */
.repeated-time .cof-chat_time,
.repeated-time .cof-chat_time_delimiters {
color: #999;
}
#nicklist {
-webkit-box-shadow: 0px 0px 120px #000;
box-shadow: 0px 0px 120px #000;
background: #282828;
}
.footer {
background: rgb(24,24,24);
}
}

File diff suppressed because it is too large Load Diff

@ -3,7 +3,7 @@
<textarea id="{{inputId}}" class="form-control favorite-font" ng-trim="false" rows="1" autocomplete="off" ng-model="command" ng-focus="hideSidebar()">
</textarea>
<span class="input-group-btn">
<button class="btn btn-default btn-primary">Send</button>
<button class="btn btn-default btn-primary unselectable">Send</button>
</span>
</div>
</form>

@ -1,14 +1,14 @@
<div ng-show="plugin.visible">
<button class="btn btn-primary btn-sm pull-right"
<button class="btn btn-primary btn-sm pull-right unselectable"
ng-click="hideContent()">
Hide {{ ::plugin.name }}
</button>
<div ng-bind-html="displayedContent" class="embed" ng-class="::('embed_' + plugin.$$hashKey)"></div>
<div ng-bind-html="displayedContent" class="embed" ng-class="::plugin.className"></div>
</div>
<div ng-hide="plugin.visible">
<button class="btn btn-sm pull-right"
<button class="btn btn-sm pull-right unselectable"
ng-class="::{
'btn-warning': plugin.nsfw,
'btn-primary': !plugin.nsfw}"

@ -12,18 +12,18 @@
<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/style.css" rel="stylesheet" media="screen">
<link href="css/glowingbear.css" rel="stylesheet" media="screen">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.19/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.19/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.19/angular-sanitize.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.19/angular-touch.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-sanitize.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-touch.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script type="text/javascript" src="3rdparty/inflate.min.js"></script>
<script type="text/javascript" src="min.js"></script>
<script type="text/javascript" src="3rdparty/favico-0.3.4-mod.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-keypress="handleKeyPress($event)" ng-class="{'no-overflow': connected}" lang="en-US">
<body ng-controller="WeechatCtrl" ng-keydown="handleKeyPress($event)" ng-keyup="handleKeyRelease($event)" ng-keypress="handleKeyPress($event)" ng-class="{'no-overflow': connected}" lang="en-US">
<link ng-href="css/themes/{{theme}}.css" rel="stylesheet" media="screen" />
<div ng-hide="connected" class="container">
<h2>
<img alt="logo" src="assets/img/glowing-bear.svg">
@ -56,7 +56,7 @@
<div class="input-group">
<div class="row no-gutter">
<div class="col-sm-9">
<input type="text" class="form-control favorite-font" id="host" ng-model="host" placeholder="Address" >
<input type="text" class="form-control favorite-font" id="host" ng-model="host" placeholder="Address" autocapitalize="off">
</div>
<div class="col-sm-3">
<input type="text" class="form-control favorite-font" id="port" ng-model="port" placeholder="Port">
@ -107,7 +107,7 @@
<div>To start using glowing bear, please enable the relay plugin in your WeeChat client:
<pre>
/set relay.network.password yourpassword
/relay add weechat 9001
/relay add weechat {{ 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.
@ -116,16 +116,16 @@
<h3>Shortcuts</h3>
Glowing Bear has a few shortcuts:
<ul>
<li>ALT-n: Toggle nicklist</li>
<li>ALT-l: Focus on input bar</li>
<li>ALT-[0-9]: Switch to buffer number N</li>
<li>ALT-a: Focus on next buffer with activity</li>
<li>ALT-&lt;: Switch to previous active buffer</li>
<li>ALT-g: Focus on buffer list filter</li>
<li>Esc-Esc: Disconnect (double-tap)</li>
<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-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>Esc-Esc</kbd>: Disconnect (double-tap)</li>
<li>Arrow keys: Navigate history</li>
<li>Tab key: Complete nick</li>
<li>The following readline/emacs style keybindings can be enabled with a setting: <span title="Move cursor to beginning of line">Ctrl-a</span>, <span title="Move cursor to te end of the line">Ctrl-e</span>, <span title="Delete from cursor to beginning of the line">Ctrl-u</span>, <span title="Delete from cursor to the end of the line">Ctrl-k</span>, <span title="Delete from cursor to previous space">Ctrl-w</span></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>
</div>
@ -141,18 +141,19 @@
</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 SSL.</p>
<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://{{ host }}:{{ port }}/">https://{{ host || 'weechathost' }}:{{ port || 'relayport' }}/</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 SSL. 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 does not require any security exceptions. As the process for requesting a certificate is different for every certification authority, we detail the method for setting up WeeChat with a self-signed certificate here. To create one, execute the following commands in a shell on the same host and as the user running WeeChat:</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 -subj "/CN={{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 8000 with these WeeChat commands:</p>
<p>If WeeChat is already running, you can reload the certificate and private key and set up an encrypted relay on port {{ port || 9001 }} with these WeeChat commands:</p>
<pre>
/set relay.network.password yourpassword
/relay sslcertkey
/relay add ssl.weechat 8000
/relay add ssl.weechat {{ port || 9001 }}
</pre>
</div>
</div>
@ -201,7 +202,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</a>
<button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button>
</div>
<div class="title" ng-bind-html="activeBuffer().title | irclinky:'_blank'" title="{{activeBuffer().title}}"></div>
<div class="title" ng-bind-html="activeBuffer().title | linky:'_blank' | DOMfilter:'irclinky'" title="{{activeBuffer().title}}"></div>
<div class="actions pull-right vertical-line-left">
<div class="pull-left">
<a class="settings-toggle" ng-click="showModal('settingsModal')" title="Options menu">
@ -214,16 +215,17 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</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')}">
<ul class="nav nav-pills nav-stacked" ng-class="{'indented': (predicate === 'serverSortKey'), 'showquickkeys': showQuickKeys}">
<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">
</form>
</li>
<li class="buffer" ng-class="{'active': buffer.active, 'indent': buffer.indent }" ng-repeat="(key, buffer) in (filteredBuffers = (getBuffers() | toArray | filter:{fullName:search} | filter:hasUnread | orderBy:predicate))">
<li class="buffer" ng-class="{'active': buffer.active, 'indent': buffer.indent, 'channel': buffer.type === 'channel', 'private': buffer.type === 'private'}" ng-repeat="(key, buffer) in (filteredBuffers = (getBuffers() | toArray:'withidx' | filter:{fullName:search} | filter:hasUnread | orderBy:predicate | getBufferQuickKeys:this))">
<a href="#" ng-click="setActiveBuffer(buffer.id)" title="{{ buffer.fullName }}">
<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="buffername">{{ buffer.shortName || buffer.fullName }}</span>
<span class="buffer-quick-key">{{ buffer.$quickKey }}</span>
<span class="buffername">{{ buffer.trimmedName || buffer.fullName }}</span>
</a>
</li>
</ul>
@ -252,10 +254,10 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<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>
</td>
<td class="prefix"><a ng-click="addMention(bufferline.prefix)"><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text"></span></a></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" ng-bind-html="::part.text | irclinky:'_blank' | inlinecolour"></span>
<td class="prefix"><a ng-click="addMention(bufferline.prefix)"><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text"></span></a></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" ng-bind-html="::part.text | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'inlinecolour'"></span>
</td>
</tr>
<tr class="readmarker" ng-if="activeBuffer().lastSeen==$index">
@ -264,10 +266,10 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</td>
</tr>
</tbody>
</table>
</table><span id="end-of-buffer"></span>
</div>
<div class="footer" ng-class="{'withnicklist': showNicklist}">
<div input-bar input-id="sendMessage"></div>
<div input-bar input-id="sendMessage" command="command"></div>
</div>
</div>
<div id="soundNotification"></div>
@ -282,7 +284,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</div>
<div class="modal-body">
<ul class="">
<li id="fontchoice">
<li class="standard-labels">
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="font" class="col-sm-3 control-label make-thinner">Preferred font</label>
@ -292,11 +294,22 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<label for="size" class="col-sm-1 control-label">Size</label>
<div class="col-sm-2">
<input type="text" ng-model="fontsize" class="form-control" id="size" placeholder="14px">
<input type="text" ng-model="fontsize" class="form-control" id="size">
</div>
</div>
</form>
</li>
<li class="standard-labels">
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="theme" class="col-sm-3 control-label make-thinner">Theme</label>
<div class="col-sm-7">
<select id="theme" class="form-control" ng-model="theme" ng-options="theme for theme in themes"></select>
</div>
</div>
</form>
</li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
@ -334,7 +347,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div class="checkbox">
<label>
<input type="checkbox" ng-model="noembed">
Hide embedded content by default
Hide embedded content by default<span class="text-muted settings-help">NSFW content will be hidden</small>
</label>
</div>
</form>

@ -0,0 +1,276 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.factory('connection',
['$rootScope', '$log', 'handlers', 'models', 'ngWebsockets', function($rootScope,
$log,
handlers,
models,
ngWebsockets) {
var protocol = new weeChat.Protocol();
// Takes care of the connection and websocket hooks
var connect = function (host, port, passwd, ssl, noCompression) {
var proto = ssl ? 'wss' : 'ws';
// If host is an IPv6 literal wrap it in brackets
if (host.indexOf(":") !== -1) {
host = "[" + host + "]";
}
var url = proto + "://" + host + ":" + port + "/weechat";
$log.debug('Connecting to URL: ', url);
var onopen = function () {
// Helper methods for initialization commands
var _initializeConnection = function(passwd) {
// 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
// will be received before formatInfo
ngWebsockets.send(
weeChat.Protocol.formatInit({
password: passwd,
compression: noCompression ? 'off' : 'zlib'
})
);
return ngWebsockets.send(
weeChat.Protocol.formatInfo({
name: 'version'
})
);
};
var _requestHotlist = function() {
return ngWebsockets.send(
weeChat.Protocol.formatHdata({
path: "hotlist:gui_hotlist(*)",
keys: []
})
);
};
var _requestBufferInfos = function() {
return ngWebsockets.send(
weeChat.Protocol.formatHdata({
path: 'buffer:gui_buffers(*)',
keys: ['local_variables,notify,number,full_name,short_name,title']
})
);
};
var _requestSync = function() {
return ngWebsockets.send(
weeChat.Protocol.formatSync({})
);
};
// First command asks for the password and issues
// a version command. If it fails, it means the we
// did not provide the proper password.
_initializeConnection(passwd).then(
function() {
// Connection is successful
// Send all the other commands required for initialization
_requestBufferInfos().then(function(bufinfo) {
//XXX move to handlers?
var bufferInfos = bufinfo.objects[0].content;
// buffers objects
for (var i = 0; i < bufferInfos.length ; i++) {
var buffer = new models.Buffer(bufferInfos[i]);
models.addBuffer(buffer);
// Switch to first buffer on startup
if (i === 0) {
models.setActiveBuffer(buffer.id);
}
}
});
_requestHotlist().then(function(hotlist) {
handlers.handleHotlistInfo(hotlist);
});
_requestSync();
$log.info("Connected to relay");
$rootScope.connected = true;
},
function() {
// Connection got closed, lets check if we ever was connected successfully
if (!$rootScope.waseverconnected) {
$rootScope.passwordError = true;
}
}
);
};
var onmessage = function() {
// If we recieve a message from WeeChat it means that
// password was OK. Store that result and check for it
// in the failure handler.
$rootScope.waseverconnected = true;
};
var onclose = function (evt) {
/*
* Handles websocket disconnection
*/
$log.info("Disconnected from relay");
ngWebsockets.failCallbacks('disconnection');
$rootScope.connected = false;
$rootScope.$emit('relayDisconnect');
if (ssl && evt.code === 1006) {
// A password error doesn't trigger onerror, but certificate issues do. Check time of last error.
if (typeof $rootScope.lastError !== "undefined" && (Date.now() - $rootScope.lastError) < 1000) {
// abnormal disconnect by client, most likely ssl error
$rootScope.sslError = true;
}
}
$rootScope.$apply();
};
var onerror = function (evt) {
/*
* Handles cases when connection issues come from
* the relay.
*/
$log.error("Relay error", evt);
$rootScope.lastError = Date.now();
if (evt.type === "error" && this.readyState !== 1) {
ngWebsockets.failCallbacks('error');
$rootScope.errorMessage = true;
}
};
try {
ngWebsockets.connect(url,
protocol,
{
'binaryType': "arraybuffer",
'onopen': onopen,
'onclose': onclose,
'onmessage': onmessage,
'onerror': onerror
});
} catch(e) {
$log.debug("Websocket caught DOMException:", e);
$rootScope.lastError = Date.now();
$rootScope.errorMessage = true;
$rootScope.securityError = true;
$rootScope.$emit('relayDisconnect');
}
};
var disconnect = function() {
ngWebsockets.send(weeChat.Protocol.formatQuit());
};
/*
* Format and send a weechat message
*
* @returns the angular promise
*/
var sendMessage = function(message) {
ngWebsockets.send(weeChat.Protocol.formatInput({
buffer: models.getActiveBuffer().fullName,
data: message
}));
};
var sendCoreCommand = function(command) {
ngWebsockets.send(weeChat.Protocol.formatInput({
buffer: 'core.weechat',
data: command
}));
};
var requestNicklist = function(bufferId, callback) {
bufferId = bufferId || null;
ngWebsockets.send(
weeChat.Protocol.formatNicklist({
buffer: bufferId
})
).then(function(nicklist) {
handlers.handleNicklist(nicklist);
if (callback !== undefined) {
callback();
}
});
};
var fetchMoreLines = function(numLines) {
$log.debug('Fetching ', numLines, ' lines');
var buffer = models.getActiveBuffer();
if (numLines === undefined) {
// Math.max(undefined, *) = NaN -> need a number here
numLines = 0;
}
// Calculate number of lines to fetch, at least as many as the parameter
numLines = Math.max(numLines, buffer.requestedLines * 2);
// Indicator that we are loading lines, hides "load more lines" link
$rootScope.loadingLines = true;
// Send hdata request to fetch lines for this particular buffer
return ngWebsockets.send(
weeChat.Protocol.formatHdata({
// "0x" is important, otherwise it won't work
path: "buffer:0x" + buffer.id + "/own_lines/last_line(-" + numLines + ")/data",
keys: []
})
).then(function(lineinfo) {
//XXX move to handlers?
// delete old lines and add new ones
var oldLength = buffer.lines.length;
// whether we already had all unread lines
var hadAllUnreadLines = buffer.lastSeen >= 0;
// clear the old lines
buffer.lines.length = 0;
// We need to set the number of requested lines to 0 here, because parsing a line
// increments it. This is needed to also count newly arriving lines while we're
// already connected.
buffer.requestedLines = 0;
// Count number of lines recieved
var linesReceivedCount = lineinfo.objects[0].content.length;
// Parse the lines
handlers.handleLineInfo(lineinfo, true);
// Correct the read marker for the lines that were counted twice
buffer.lastSeen -= oldLength;
// We requested more lines than we got, no more lines.
if (linesReceivedCount < numLines) {
buffer.allLinesFetched = true;
}
$rootScope.loadingLines = false;
// Only scroll to read marker if we didn't have all unread lines previously, but have them now
var scrollToReadmarker = !hadAllUnreadLines && buffer.lastSeen >= 0;
// Scroll to correct position
$rootScope.scrollWithBuffer(scrollToReadmarker, true);
});
};
return {
connect: connect,
disconnect: disconnect,
sendMessage: sendMessage,
sendCoreCommand: sendCoreCommand,
fetchMoreLines: fetchMoreLines,
requestNicklist: requestNicklist
};
}]);
})();

@ -0,0 +1,128 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.filter('toArray', function () {
return function (obj, storeIdx) {
if (!(obj instanceof Object)) {
return obj;
}
if (storeIdx) {
return Object.keys(obj).map(function (key, idx) {
return Object.defineProperties(obj[key], {
'$key' : { value: key },
'$idx' : { value: idx, configurable: true }
});
});
}
return Object.keys(obj).map(function (key) {
return Object.defineProperty(obj[key], '$key', { value: key });
});
};
});
weechat.filter('irclinky', ['$filter', function($filter) {
return function(text) {
if (!text) {
return text;
}
// This regex in no way matches all IRC channel names (they could also begin with &, + or an
// exclamation mark followed by 5 alphanumeric characters, and are bounded in length by 50).
// However, it matches all *common* IRC channels while trying to minimise false positives.
// "#1" is much more likely to be "number 1" than "IRC channel #1".
// Thus, we only match channels beginning with a # and having at least one letter in them.
var channelRegex = /(^|[\s,.:;?!"'()+@-])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi;
// This is SUPER nasty, but ng-click does not work inside a filter, as the markup has to be $compiled first, which is not possible in filter afaik.
// Therefore, get the scope, fire the method, and $apply. Yuck. I sincerely hope someone finds a better way of doing this.
var substitute = '$1<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'$2\'); $scope.$apply();">$2</a>';
return text.replace(channelRegex, substitute);
};
}]);
weechat.filter('inlinecolour', function() {
return function(text) {
if (!text) {
return text;
}
// only match 6-digit colour codes, 3-digit ones have too many false positives (issue numbers, etc)
var hexColourRegex = /(^|[^&])\#([0-9a-f]{6})($|[^\w'"])/gmi;
var substitute = '$1#$2 <div class="colourbox" style="background-color:#$2"></div> $3';
return text.replace(hexColourRegex, substitute);
};
});
// 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) {
return function(text, filter) {
if (!text || !filter) {
return text;
}
var filterFunction = $filter(filter);
var el = document.createElement('div');
el.innerHTML = text;
// Recursive DOM-walking function applying the filter to the text nodes
var process = function(node) {
if (node.nodeType === 3) { // text node
var value = filterFunction(node.nodeValue);
if (value !== node.nodeValue) {
// we changed something. create a new node to replace the current one
// we could also only add its children but that would probably incur
// more overhead than it would gain us
var newNode = document.createElement('span');
newNode.innerHTML = value;
var parent = node.parentNode;
var sibling = node.nextSibling;
parent.removeChild(node);
if (sibling) {
parent.insertBefore(newNode, sibling);
} else {
parent.appendChild(newNode);
}
}
}
// recurse
node = node.firstChild;
while (node) {
process(node);
node = node.nextSibling;
}
};
process(el);
return $sce.trustAsHtml(el.innerHTML);
};
}]);
weechat.filter('getBufferQuickKeys', function () {
return function (obj, $scope) {
if (!$scope) { return obj; }
if (($scope.search !== undefined && $scope.search.length) || $scope.onlyUnread) {
obj.forEach(function(buf, idx) {
buf.$quickKey = idx < 10 ? (idx + 1) % 10 : '';
});
} else {
_.map(obj, function(buffer, idx) {
return [buffer.number, buffer.$idx, idx];
}).sort(function(left, right) {
// By default, Array.prototype.sort() sorts alphabetically.
// Pass an ordering function to sort by first element.
return left[0] - right[0] || left[1] - right[1];
}).forEach(function(info, keyIdx) {
obj[ info[2] ].$quickKey = keyIdx < 10 ? (keyIdx + 1) % 10 : '';
});
}
return obj;
};
});
})();

File diff suppressed because it is too large Load Diff

@ -0,0 +1,213 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) {
var handleBufferClosing = function(message) {
var bufferMessage = message.objects[0].content[0];
var bufferId = bufferMessage.pointers[0];
models.closeBuffer(bufferId);
};
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) {
message = plugins.PluginManager.contentForMessage(message);
buffer.addLine(message);
if (manually) {
buffer.lastSeen++;
}
if (buffer.active && !manually) {
$rootScope.scrollWithBuffer();
}
if (!manually && (!buffer.active || !$rootScope.isWindowFocused())) {
if (buffer.notify > 1 && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) {
buffer.unread++;
$rootScope.$emit('notificationChanged');
}
if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) {
buffer.notification++;
notifications.createHighlight(buffer, message);
$rootScope.$emit('notificationChanged');
}
}
}
};
var handleBufferLineAdded = function(message) {
message.objects[0].content.forEach(function(l) {
handleLine(l, false);
});
};
var handleBufferOpened = function(message) {
var bufferMessage = message.objects[0].content[0];
var buffer = new models.Buffer(bufferMessage);
models.addBuffer(buffer);
/* Until we can decide if user asked for this buffer to be opened
* or not we will let user click opened buffers.
models.setActiveBuffer(buffer.id);
*/
};
var handleBufferTitleChanged = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
old.fullName = obj.full_name;
old.title = obj.title;
old.number = obj.number;
};
var handleBufferRenamed = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
old.fullName = obj.full_name;
old.shortName = obj.short_name;
old.trimmedName = obj.short_name.replace(/^[#&+]/, '');
};
var handleBufferLocalvarChanged = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
var localvars = obj.local_variables;
if (old !== undefined && localvars !== undefined) {
// Update indentation status
old.type = localvars.type;
old.indent = (['channel', 'private'].indexOf(localvars.type) >= 0);
}
};
/*
* Handle answers to (lineinfo) messages
*
* (lineinfo) messages are specified by this client. It is request after bufinfo completes
*/
var handleLineInfo = function(message, manually) {
var lines = message.objects[0].content.reverse();
if (manually === undefined) {
manually = true;
}
lines.forEach(function(l) {
handleLine(l, manually);
});
};
/*
* Handle answers to hotlist request
*/
var handleHotlistInfo = function(message) {
if (message.objects.length === 0) {
return;
}
var hotlist = message.objects[0].content;
hotlist.forEach(function(l) {
var buffer = models.getBuffer(l.buffer);
// 1 is message
buffer.unread += l.count[1];
// 2 is private
buffer.notification += l.count[2];
// 3 is highlight
buffer.notification += l.count[3];
/* Since there is unread messages, we can guess
* what the last read line is and update it accordingly
*/
var unreadSum = _.reduce(l.count, function(memo, num) { return memo + num; }, 0);
buffer.lastSeen = buffer.lines.length - 1 - unreadSum;
});
};
/*
* Handle nicklist event
*/
var handleNicklist = function(message) {
var nicklist = message.objects[0].content;
var group = 'root';
nicklist.forEach(function(n) {
var buffer = models.getBuffer(n.pointers[0]);
if (n.group === 1) {
var g = new models.NickGroup(n);
group = g.name;
buffer.nicklist[group] = g;
} else {
var nick = new models.Nick(n);
buffer.addNick(group, nick);
}
});
};
/*
* Handle nicklist diff event
*/
var handleNicklistDiff = function(message) {
var nicklist = message.objects[0].content;
var group;
nicklist.forEach(function(n) {
var buffer = models.getBuffer(n.pointers[0]);
var d = n._diff;
if (n.group === 1) {
group = n.name;
if (group === undefined) {
var g = new models.NickGroup(n);
buffer.nicklist[group] = g;
group = g.name;
}
} else {
var nick = new models.Nick(n);
if (d === 43) { // +
buffer.addNick(group, nick);
} else if (d === 45) { // -
buffer.delNick(group, nick);
} else if (d === 42) { // *
buffer.updateNick(group, nick);
}
}
});
};
var eventHandlers = {
_buffer_closing: handleBufferClosing,
_buffer_line_added: handleBufferLineAdded,
_buffer_localvar_added: handleBufferLocalvarChanged,
_buffer_localvar_removed: handleBufferLocalvarChanged,
_buffer_opened: handleBufferOpened,
_buffer_title_changed: handleBufferTitleChanged,
_buffer_renamed: handleBufferRenamed,
_nicklist: handleNicklist,
_nicklist_diff: handleNicklistDiff
};
$rootScope.$on('onMessage', function(event, message) {
if (_.has(eventHandlers, message.id)) {
eventHandlers[message.id](message);
} else {
$log.debug('Unhandled event received: ' + message.id);
}
});
var handleEvent = function(event) {
if (_.has(eventHandlers, event.id)) {
eventHandlers[event.id](event);
}
};
return {
handleEvent: handleEvent,
handleLineInfo: handleLineInfo,
handleHotlistInfo: handleHotlistInfo,
handleNicklist: handleNicklist
};
}]);
})();

@ -0,0 +1,404 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('inputBar', function() {
return {
templateUrl: 'directives/input.html',
scope: {
inputId: '@inputId',
command: '=command'
},
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', 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
models,
IrcUtils) {
/*
* Returns the input element
*/
$scope.getInputNode = function() {
return document.querySelector('textarea#' + $scope.inputId);
};
$scope.hideSidebar = function() {
$rootScope.hideSidebar();
};
$scope.completeNick = function() {
// input DOM node
var inputNode = $scope.getInputNode();
// get current caret position
var caretPos = inputNode.selectionStart;
// get current active buffer
var activeBuffer = models.getActiveBuffer();
// Empty input makes $scope.command undefined -- use empty string instead
var input = $scope.command || '';
// complete nick
var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate,
activeBuffer.getNicklistByTime(), ':');
// remember iteration candidate
$scope.iterCandidate = nickComp.iterCandidate;
// update current input
$scope.command = nickComp.text;
// update current caret position
setTimeout(function() {
inputNode.focus();
inputNode.setSelectionRange(nickComp.caretPos, nickComp.caretPos);
}, 0);
};
// Send the message to the websocket
$scope.sendMessage = function() {
//XXX Use a signal here
var ab = models.getActiveBuffer();
// It's undefined early in the lifecycle of the program.
// Don't send empty commands
if($scope.command !== undefined && $scope.command !== '') {
// log to buffer history
ab.addToHistory($scope.command);
// Split the command into multiple commands based on line breaks
_.each($scope.command.split(/\r?\n/), function(line) {
// Ask before a /quit
if (line === '/quit' || line.indexOf('/quit ') === 0) {
if (!window.confirm("Are you sure you want to quit WeeChat? This will prevent you from connecting with Glowing Bear until you restart WeeChat on the command line!")) {
// skip this line
return;
}
}
connection.sendMessage(line);
});
// Check for /clear command
if ($scope.command === '/buffer clear' || $scope.command === '/c') {
$log.debug('Clearing lines');
ab.clear();
}
// Empty the input after it's sent
$scope.command = '';
}
$scope.getInputNode().focus();
};
//XXX THIS DOES NOT BELONG HERE!
$rootScope.addMention = function(prefix) {
// Extract nick from bufferline prefix
var nick = prefix[prefix.length - 1].text;
var newValue = $scope.command || ''; // can be undefined, in that case, use the empty string
var addColon = newValue.length === 0;
if (newValue.length > 0) {
// Try to determine if it's a sequence of nicks
var trimmedValue = newValue.trim();
if (trimmedValue.charAt(trimmedValue.length - 1) === ':') {
// get last word
var lastSpace = trimmedValue.lastIndexOf(' ') + 1;
var lastWord = trimmedValue.slice(lastSpace, trimmedValue.length - 1);
var nicklist = models.getActiveBuffer().getNicklistByTime();
// check against nicklist to see if it's a list of highlights
for (var index in nicklist) {
if (nicklist[index].name === lastWord) {
// It's another highlight!
newValue = newValue.slice(0, newValue.lastIndexOf(':')) + ' ';
addColon = true;
break;
}
}
}
// Add a space before the nick if there isn't one already
// Last char might have changed above, so re-check
if (newValue.charAt(newValue.length - 1) !== ' ') {
newValue += ' ';
}
}
// Add highlight to nicklist
newValue += nick;
if (addColon) {
newValue += ': ';
}
$scope.command = newValue;
$scope.getInputNode().focus();
};
// Handle key presses in the input bar
$rootScope.handleKeyPress = function($event) {
// don't do anything if not connected
if (!$rootScope.connected) {
return true;
}
var inputNode = $scope.getInputNode();
// Support different browser quirks
var code = $event.keyCode ? $event.keyCode : $event.charCode;
// reset quick keys display
$rootScope.showQuickKeys = false;
// any other key than Tab resets nick completion iteration
var tmpIterCandidate = $scope.iterCandidate;
$scope.iterCandidate = null;
// Left Alt+[0-9] -> jump to buffer
if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58)) {
if (code === 48) {
code = 58;
}
var 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];
if (filteredBufferNum !== undefined) {
activeBufferId = [filteredBufferNum.number, filteredBufferNum.id];
}
} else {
// 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) {
return [buffer.number, buffer.id];
}).sort(function(left, right) {
// By default, Array.prototype.sort() sorts alphabetically.
// Pass an ordering function to sort by first element.
return left[0] - right[0];
});
activeBufferId = sortedBuffers[bufferNumber];
}
if (activeBufferId) {
$scope.$parent.setActiveBuffer(activeBufferId[1]);
$event.preventDefault();
}
}
// Tab -> nick completion
if (code === 9 && !$event.altKey && !$event.ctrlKey) {
$event.preventDefault();
$scope.iterCandidate = tmpIterCandidate;
$scope.completeNick();
return true;
}
// Left Alt+n -> toggle nicklist
if ($event.altKey && !$event.ctrlKey && code === 78) {
$event.preventDefault();
$rootScope.toggleNicklist();
return true;
}
// Alt+A -> switch to buffer with activity
if ($event.altKey && (code === 97 || code === 65)) {
$event.preventDefault();
$rootScope.switchToActivityBuffer();
return true;
}
// Alt+L -> focus on input bar
if ($event.altKey && (code === 76 || code === 108)) {
$event.preventDefault();
inputNode.focus();
inputNode.setSelectionRange($scope.command.length, $scope.command.length);
return true;
}
// Alt+< -> switch to previous buffer
if ($event.altKey && (code === 60 || code === 226)) {
var previousBuffer = models.getPreviousBuffer();
if (previousBuffer) {
models.setActiveBuffer(previousBuffer.id);
$event.preventDefault();
return true;
}
}
// Double-tap Escape -> disconnect
if (code === 27) {
$event.preventDefault();
// Check if a modal is visible. If so, close it instead of disconnecting
var modals = document.querySelectorAll('.gb-modal');
for (var modalId = 0; modalId < modals.length; modalId++) {
if (modals[modalId].getAttribute('data-state') === 'visible') {
modals[modalId].setAttribute('data-state', 'hidden');
return true;
}
}
if (typeof $scope.lastEscape !== "undefined" && (Date.now() - $scope.lastEscape) <= 500) {
// Double-tap
connection.disconnect();
}
$scope.lastEscape = Date.now();
return true;
}
// Alt+G -> focus on buffer filter input
if ($event.altKey && (code === 103 || code === 71)) {
$event.preventDefault();
if (!$scope.$parent.isSidebarVisible()) {
$scope.$parent.showSidebar();
}
setTimeout(function() {
document.getElementById('bufferFilter').focus();
});
return true;
}
var caretPos;
// Arrow up -> go up in history
if ($event.type === "keydown" && code === 38 && document.activeElement === inputNode) {
caretPos = inputNode.selectionStart;
if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) {
return false;
}
$scope.command = models.getActiveBuffer().getHistoryUp($scope.command);
// Set cursor to last position. Need 0ms timeout because browser sets cursor
// position to the beginning after this key handler returns.
setTimeout(function() {
if ($scope.command) {
inputNode.setSelectionRange($scope.command.length, $scope.command.length);
}
}, 0);
return true;
}
// Arrow down -> go down in history
if ($event.type === "keydown" && code === 40 && document.activeElement === inputNode) {
caretPos = inputNode.selectionStart;
if ($scope.command.slice(caretPos).indexOf("\n") !== -1) {
return false;
}
$scope.command = models.getActiveBuffer().getHistoryDown($scope.command);
// We don't need to set the cursor to the rightmost position here, the browser does that for us
return true;
}
// Enter to submit, shift-enter for newline
if (code == 13 && !$event.shiftKey && document.activeElement === inputNode) {
$event.preventDefault();
$scope.sendMessage();
return true;
}
var bufferlines = document.getElementById("bufferlines");
var lines;
var i;
// Page up -> scroll up
if ($event.type === "keydown" && code === 33 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) {
if (bufferlines.scrollTop === 0) {
if (!$rootScope.loadingLines) {
$scope.$parent.fetchMoreLines();
}
return true;
}
lines = bufferlines.querySelectorAll("tr");
for (i = lines.length - 1; i >= 0; i--) {
if ((lines[i].offsetTop-bufferlines.scrollTop)<bufferlines.clientHeight/2) {
lines[i].scrollIntoView(false);
break;
}
}
return true;
}
// Page down -> scroll down
if ($event.type === "keydown" && code === 34 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) {
lines = bufferlines.querySelectorAll("tr");
for (i = 0; i < lines.length; i++) {
if ((lines[i].offsetTop-bufferlines.scrollTop)>bufferlines.clientHeight/2) {
lines[i].scrollIntoView(true);
break;
}
}
return true;
}
// Some readline keybindings
if ($rootScope.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) {
// get current caret position
caretPos = inputNode.selectionStart;
// Ctrl-a
if (code == 65) {
inputNode.setSelectionRange(0, 0);
// Ctrl-e
} else if (code == 69) {
inputNode.setSelectionRange($scope.command.length, $scope.command.length);
// Ctrl-u
} else if (code == 85) {
$scope.command = $scope.command.slice(caretPos);
setTimeout(function() {
inputNode.setSelectionRange(0, 0);
});
// Ctrl-k
} else if (code == 75) {
$scope.command = $scope.command.slice(0, caretPos);
setTimeout(function() {
inputNode.setSelectionRange($scope.command.length, $scope.command.length);
});
// Ctrl-w
} else if (code == 87) {
var trimmedValue = $scope.command.slice(0, caretPos);
var lastSpace = trimmedValue.lastIndexOf(' ') + 1;
$scope.command = $scope.command.slice(0, lastSpace) + $scope.command.slice(caretPos, $scope.command.length);
setTimeout(function() {
inputNode.setSelectionRange(lastSpace, lastSpace);
});
} else {
return false;
}
$event.preventDefault();
return true;
}
// Alt key down -> display quick key legend
if ($event.type === "keydown" && code === 18 && !$event.ctrlKey && !$event.shiftKey) {
$rootScope.showQuickKeys = true;
}
};
$rootScope.handleKeyRelease = function($event) {
// Alt key up -> remove quick key legend
if ($event.keyCode === 18) {
if ($rootScope.quickKeysTimer !== undefined) {
clearTimeout($rootScope.quickKeysTimer);
}
$rootScope.quickKeysTimer = setTimeout(function() {
if ($rootScope.showQuickKeys) {
$rootScope.showQuickKeys = false;
$rootScope.$apply();
}
delete $rootScope.quickKeysTimer;
}, 1000);
return true;
}
};
}]
};
});
})();

@ -2,14 +2,19 @@
* Portable utilities for IRC.
*/
var IrcUtils = {
(function() {
'use strict';
var IrcUtils = angular.module('IrcUtils', []);
IrcUtils.service('IrcUtils', [function() {
/**
* Get a new version of a nick list, sorted by last speaker
*
* @param nickList Original nick list
* @return Sorted nick list
*/
_ciNickList: function(nickList) {
var _ciNickList = function(nickList) {
var newList = _(nickList).sortBy(function(nickObj) {
return -nickObj.spokeAt;
@ -17,7 +22,7 @@ var IrcUtils = {
newList = _(newList).pluck('name');
return newList;
},
};
/**
* Completes a single nick.
@ -26,7 +31,7 @@ var IrcUtils = {
* @param nickList Array of current nicks sorted for case insensitive searching
* @return Completed nick (null if not found)
*/
_completeSingleNick: function(candidate, nickList) {
var _completeSingleNick = function(candidate, nickList) {
var foundNick = null;
nickList.some(function(nick) {
@ -39,7 +44,7 @@ var IrcUtils = {
});
return foundNick;
},
};
/**
* Get the next nick when iterating nicks.
@ -49,7 +54,7 @@ var IrcUtils = {
* @param nickList Array of current nicks sorted for case insensitive searching
* @return Next nick (may be the same)
*/
_nextNick: function(iterCandidate, currentNick, nickList) {
var _nextNick = function(iterCandidate, currentNick, nickList) {
var matchingNicks = [];
var at = null;
var lcIterCandidate = iterCandidate.toLowerCase();
@ -63,7 +68,7 @@ var IrcUtils = {
if (lcCurrentNick === lcNick) {
at = matchingNicks.length - 1;
}
}
}
/* Since we aren't sorted any more torhve disabled this:
else if (matchingNicks.length > 0) {
// end of group, no need to check after this
@ -82,7 +87,7 @@ var IrcUtils = {
}
return matchingNicks[at];
}
},
};
/**
* Nicks tab completion.
@ -98,14 +103,14 @@ var IrcUtils = {
* foundNick: completed nick (or null if not possible)
* iterCandidate: current iterating candidate
*/
completeNick: function(text, caretPos, iterCandidate, nickList, suf) {
var completeNick = function(text, caretPos, iterCandidate, nickList, suf) {
var doIterate = (iterCandidate !== null);
if (suf === null) {
suf = ':';
}
// new nick list to search in
var searchNickList = IrcUtils._ciNickList(nickList);
var searchNickList = _ciNickList(nickList);
// text before and after caret
var beforeCaret = text.substring(0, caretPos);
@ -126,7 +131,7 @@ var IrcUtils = {
if (m) {
if (doIterate) {
// try iterating
newNick = IrcUtils._nextNick(iterCandidate, m[1], searchNickList);
newNick = _nextNick(iterCandidate, m[1], searchNickList);
beforeCaret = newNick + suf + ' ';
return {
text: beforeCaret + afterCaret,
@ -144,7 +149,7 @@ var IrcUtils = {
m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) {
// try completing
newNick = IrcUtils._completeSingleNick(m[1], searchNickList);
newNick = _completeSingleNick(m[1], searchNickList);
if (newNick === null) {
// no match
return ret;
@ -167,7 +172,7 @@ var IrcUtils = {
if (m) {
if (doIterate) {
// try iterating
newNick = IrcUtils._nextNick(iterCandidate, m[2], searchNickList);
newNick = _nextNick(iterCandidate, m[2], searchNickList);
beforeCaret = m[1] + newNick + ' ';
return {
text: beforeCaret + afterCaret,
@ -185,7 +190,7 @@ var IrcUtils = {
m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) {
// try completing
newNick = IrcUtils._completeSingleNick(m[2], searchNickList);
newNick = _completeSingleNick(m[2], searchNickList);
if (newNick === null) {
// no match
return ret;
@ -205,5 +210,10 @@ var IrcUtils = {
// completion not possible
return ret;
}
};
};
return {
'completeNick': completeNick
};
}]);
})();

@ -2,6 +2,9 @@
* This file contains the weechat models and various
* helper methods to work with them.
*/
(function() {
'use strict';
var models = angular.module('weechatModels', []);
models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) {
@ -12,6 +15,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
// weechat properties
var fullName = message.full_name;
var shortName = message.short_name;
var trimmedName = shortName.replace(/^[#&+]/, '');
var title = message.title;
var number = message.number;
var pointer = message.pointers[0];
@ -26,7 +30,7 @@ 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");
var serverSortKey = fullName.replace(/^irc\.server\.(\w+)/, "irc.$1");
var type = message.local_variables.type;
var indent = (['channel', 'private'].indexOf(type) >= 0);
@ -221,6 +225,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
id: pointer,
fullName: fullName,
shortName: shortName,
trimmedName: trimmedName,
number: number,
title: title,
lines: lines,
@ -238,6 +243,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
getNicklistByTime: getNicklistByTime,
serverSortKey: serverSortKey,
indent: indent,
type: type,
history: history,
addToHistory: addToHistory,
getHistoryUp: getHistoryUp,
@ -282,6 +288,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
if (textEl.attrs.name !== null) {
textEl.classes.push('coa-' + textEl.attrs.name);
}
var val;
for (var attr in textEl.attrs.override) {
val = textEl.attrs.override[attr];
if (val) {
@ -449,7 +456,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
if (key === 'id') {
activeBuffer = this.model.buffers[bufferId];
}
else {
else {
activeBuffer = _.find(this.model.buffers, function(buffer) {
if (buffer[key] === bufferId) {
return buffer;
@ -526,3 +533,4 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
delete(this.model.buffers[bufferId]);
};
}]);
})();

@ -0,0 +1,141 @@
var weechat = angular.module('weechat');
weechat.factory('notifications', ['$rootScope', '$log', 'models', function($rootScope, $log, models) {
// Ask for permission to display desktop notifications
var requestNotificationPermission = function() {
// Firefox
if (window.Notification) {
Notification.requestPermission(function(status) {
$log.info('Notification permission status: ', status);
if (Notification.permission !== status) {
Notification.permission = status;
}
});
}
// Webkit
if (window.webkitNotifications !== undefined) {
var havePermission = window.webkitNotifications.checkPermission();
if (havePermission !== 0) { // 0 is PERMISSION_ALLOWED
$log.info('Notification permission status: ', havePermission === 0);
window.webkitNotifications.requestPermission();
}
}
};
// Reduce buffers with "+" operation over a key. Mostly useful for unread/notification counts.
var unreadCount = function(type) {
if (!type) {
type = "unread";
}
// Do this the old-fashioned way with iterating over the keys, as underscore proved to be error-prone
var keys = Object.keys(models.model.buffers);
var count = 0;
for (var key in keys) {
count += models.model.buffers[keys[key]][type];
}
return count;
};
var updateTitle = function() {
var notifications = unreadCount('notification');
if (notifications > 0) {
// New notifications deserve an exclamation mark
$rootScope.notificationStatus = '(' + notifications + ') ';
} else {
$rootScope.notificationStatus = '';
}
var activeBuffer = models.getActiveBuffer();
if (activeBuffer) {
$rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.title;
}
};
var updateFavico = function() {
var notifications = unreadCount('notification');
if (notifications > 0) {
$rootScope.favico.badge(notifications, {
bgColor: '#d00',
textColor: '#fff'
});
} else {
var unread = unreadCount('unread');
if (unread === 0) {
$rootScope.favico.reset();
} else {
$rootScope.favico.badge(unread, {
bgColor: '#5CB85C',
textColor: '#ff0'
});
}
}
};
/* Function gets called from bufferLineAdded code if user should be notified */
var createHighlight = function(buffer, message) {
var title = '';
var body = '';
var numNotifications = buffer.notification;
if (['#', '&', '+', '!'].indexOf(buffer.shortName.charAt(0)) < 0) {
if (numNotifications > 1) {
title = numNotifications.toString() + ' private messages from ';
} else {
title = 'Private message from ';
}
body = message.text;
} else {
if (numNotifications > 1) {
title = numNotifications.toString() + ' highlights in ';
} else {
title = 'Highlight in ';
}
var prefix = '';
for (var i = 0; i < message.prefix.length; i++) {
prefix += message.prefix[i].text;
}
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'
});
// 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();
};
if ($rootScope.soundnotification) {
// TODO fill in a sound file
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;
}
};
return {
requestNotificationPermission: requestNotificationPermission,
updateTitle: updateTitle,
updateFavico: updateFavico,
createHighlight: createHighlight,
};
}]);

@ -0,0 +1,82 @@
(function() {
'use strict';
var weechat = angular.module('weechat');
weechat.directive('plugin', ['$rootScope', function($rootScope) {
/*
* Plugin directive
* Shows additional plugin content
*/
return {
templateUrl: 'directives/plugin.html',
scope: {
plugin: '=data'
},
controller: ['$scope', function($scope) {
$scope.displayedContent = "";
// Auto-display embedded content only if it isn't NSFW
$scope.plugin.visible = $rootScope.auto_display_embedded_content && !$scope.plugin.nsfw;
// user-accessible hash key that is a valid CSS class name
$scope.plugin.className = "embed_" + $scope.plugin.$$hashKey.replace(':','_');
$scope.plugin.getElement = function() {
return document.querySelector("." + $scope.plugin.className);
};
$scope.hideContent = function() {
$scope.plugin.visible = false;
};
$scope.showContent = function(automated) {
/*
* Shows the plugin content.
* displayedContent is bound to the DOM.
* Actual plugin content is only fetched when
* content is shown.
*/
var embed = $scope.plugin.getElement();
// If the plugin is asynchronous / lazy, execute it now and let it insert itself
// TODO store the result between channel switches
if ($scope.plugin.content instanceof Function){
// Don't rerun if the result is already there
if (embed.innerHTML === "") {
$scope.plugin.content();
}
} else {
$scope.displayedContent = $scope.plugin.content;
}
$scope.plugin.visible = true;
// Scroll embed content into view
var scroll;
if (automated) {
var wasBottom = $rootScope.bufferBottom;
scroll = function() {
$rootScope.updateBufferBottom(wasBottom);
};
} else {
scroll = function() {
if (embed && embed.scrollIntoViewIfNeeded !== undefined) {
embed.scrollIntoViewIfNeeded();
$rootScope.updateBufferBottom();
}
};
}
setTimeout(scroll, 500);
};
if ($scope.plugin.visible) {
$scope.showContent(true);
}
}]
};
}]);
})();

@ -2,7 +2,10 @@
* This file contains the plugin definitions
*/
plugins = angular.module('plugins', []);
(function() {
'use strict';
var plugins = angular.module('plugins', []);
/*
* Definition of a user provided plugin with sensible default values
@ -119,10 +122,10 @@ plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) {
*
* To create your own plugin, you need to:
*
* 1. Define it's contentForMessage function. The contentForMessage
* 1. Define its contentForMessage function. The contentForMessage
* function takes a string as a parameter and returns a HTML string.
*
* 2. Instanciate a Plugin object with contentForMessage function as it's
* 2. Instantiate a Plugin object with contentForMessage function as its
* argument.
*
* 3. Add it to the plugins array.
@ -143,7 +146,7 @@ plugins.factory('userPlugins', function() {
document.body.appendChild(script);
};
var urlRegexp = RegExp(/(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g);
var urlRegexp = new RegExp(/(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g);
var urlPlugin = function(callback) {
return function(message) {
@ -168,7 +171,7 @@ plugins.factory('userPlugins', function() {
*/
var spotifyPlugin = new Plugin(function(message) {
content = [];
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);
@ -345,7 +348,7 @@ plugins.factory('userPlugins', function() {
url = match[0] + '.json';
// load gist asynchronously -- return a function here
return function() {
var element = document.querySelector('.embed_' + this.$$hashKey);
var element = this.getElement();
jsonp(url, function(data) {
// Add the gist stylesheet only once
if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) {
@ -367,7 +370,7 @@ plugins.factory('userPlugins', function() {
if (match) {
url = 'https://api.twitter.com/1/statuses/oembed.json?id=' + match[2];
return function() {
var element = document.querySelector('.embed_' + this.$$hashKey);
var element = this.getElement();
jsonp(url, function(data) {
// sepearate the HTML into content and script tag
var scriptIndex = data.html.indexOf("<script ");
@ -394,3 +397,4 @@ plugins.factory('userPlugins', function() {
});
})();

@ -0,0 +1,29 @@
var weechat = angular.module('weechat');
weechat.factory('utils', function() {
// Helper to change style of a class
var changeClassStyle = function(classSelector, attr, value) {
_.each(document.getElementsByClassName(classSelector), function(e) {
e.style[attr] = value;
});
};
// Helper to get style from a class
var getClassStyle = function(classSelector, attr) {
_.each(document.getElementsByClassName(classSelector), function(e) {
return e.style[attr];
});
};
var isMobileUi = function() {
// TODO don't base detection solely on screen width
// You are right. In the meantime I am renaming isMobileDevice to isMobileUi
var mobile_cutoff = 968;
return (document.body.clientWidth < mobile_cutoff);
};
return {
changeClassStyle: changeClassStyle,
getClassStyle: getClassStyle,
isMobileUi: isMobileUi
};
});

@ -1,3 +1,6 @@
(function() {
'use strict';
var websockets = angular.module('ngWebsockets', []);
websockets.factory('ngWebsockets',
@ -5,7 +8,7 @@ websockets.factory('ngWebsockets',
function($rootScope, $q) {
this.protocol = null;
var protocol = null;
var ws = null;
var callbacks = {};
@ -17,7 +20,7 @@ function($rootScope, $q) {
*
* @param reason reason for failure
*/
failCallbacks = function(reason) {
var failCallbacks = function(reason) {
for (var i in callbacks) {
callbacks[i].cb.reject(reason);
}
@ -111,11 +114,11 @@ function($rootScope, $q) {
};
var connect = function(url,
protocol,
protocol_,
properties) {
ws = new WebSocket(url);
protocol = protocol;
protocol = protocol_;
for (var property in properties) {
ws[property] = properties[property];
}
@ -138,7 +141,9 @@ function($rootScope, $q) {
send: send,
sendAll: sendAll,
connect: connect,
disconnect: disconnect
disconnect: disconnect,
failCallbacks: failCallbacks
};
}]);
})();

@ -1,4 +1,5 @@
(function(exports) {// http://weechat.org/files/doc/devel/weechat_dev.en.html#color_codes_in_strings
'use strict';
/**
* WeeChat protocol handling.
@ -604,16 +605,6 @@
return defaults;
};
/**
* Add the ID to the previously formatted command
*
* @param id Command ID
* @param command previously formatted command
*/
WeeChatProtocol.setId = function(id, command) {
return '(' + id + ') ' + command;
};
/**
* Formats a command.
*
@ -966,7 +957,7 @@
var objs = [];
var hpath = this._getString();
keys = this._getString().split(',');
var keys = this._getString().split(',');
paths = hpath.split('/');
count = this._getInt();
@ -1179,6 +1170,17 @@
this._data = data;
},
/**
* Add the ID to the previously formatted command
*
* @param id Command ID
* @param command previously formatted command
*/
setId: function(id, command) {
return '(' + id + ') ' + command;
},
/**
* Parses a WeeChat message.
*

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

@ -16,5 +16,5 @@
"url": "https://github.com/glowing-bear"
},
"default_locale": "en",
"version": "0.4.0"
"version": "0.4.4"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,7 +1,7 @@
{
"name": "glowing-bear",
"private": true,
"version": "0.4.1",
"version": "0.4.4",
"description": "A web client for Weechat",
"repository": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3",
@ -18,7 +18,7 @@
"scripts": {
"postinstall": "bower install",
"minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.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/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",
"prestart": "npm install",
"start": "http-server -a localhost -p 8000",

@ -7,7 +7,20 @@ module.exports = function(config){
'bower_components/angular/angular.js',
'bower_components/angular-route/angular-route.js',
'bower_components/angular-mocks/angular-mocks.js',
'js/**/*.js',
'js/localstorage.js',
'js/weechat.js',
'js/irc-utils.js',
'js/glowingbear.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',
'test/unit/**/*.js'
],

Loading…
Cancel
Save