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. 178
      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. 1221
      js/glowingbear.js
  13. 213
      js/handlers.js
  14. 404
      js/inputbar.js
  15. 40
      js/irc-utils.js
  16. 10
      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": { "globals": {
"angular": false, "angular": false,
"$": false, "weeChat": false,
"window": false, "_": false,
"console": 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 { body {
height: 100%; height: 100%;
/* The html and body elements cannot have any padding or margin. */ /* The html and body elements cannot have any padding or margin. */
color: #ddd;
background-color: #181818;
} }
.no-overflow { .no-overflow {
overflow: hidden; 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 { a {
cursor: pointer; 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 { td.prefix {
text-align: right; text-align: right;
vertical-align: top; vertical-align: top;
@ -72,13 +35,15 @@ td.message {
hyphens: auto; hyphens: auto;
} }
#readmarker { #readmarker {
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
border-top: 1px solid rgba(255, 255, 255, 0.3); border-top: 1px solid;
border-bottom: 1px solid #121212; border-bottom: 1px solid;
height: 2px; height: 2px;
} }
.text { .text {
white-space: pre-wrap; white-space: pre-wrap;
} }
@ -97,11 +62,9 @@ td.message {
input[type=text], input[type=password], #sendMessage, .badge { input[type=text], input[type=password], #sendMessage, .badge {
border: 0; border: 0;
border-radius: 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; margin-bottom: 5px !important;
} }
.input-group { .input-group {
width: 100%; width: 100%;
} }
@ -162,8 +125,8 @@ input[type=text], input[type=password], #sendMessage, .badge {
font-size: 30px; font-size: 30px;
position: fixed; position: fixed;
right: 0; right: 0;
background: #282828;
} }
#topbar .actions > * { #topbar .actions > * {
padding-left: 5px; padding-left: 5px;
} }
@ -172,9 +135,6 @@ input[type=text], input[type=password], #sendMessage, .badge {
padding-right: 6px; padding-right: 6px;
} }
#topbar, #sidebar, .panel, .dropdown-menu, .modal-content {
background: #282828;
}
#sidebar { #sidebar {
position: fixed; position: fixed;
width: 140px; width: 140px;
@ -204,7 +164,7 @@ input[type=text], input[type=password], #sendMessage, .badge {
#sidebar .badge { #sidebar .badge {
border-radius: 0; border-radius: 0;
margin-right: -15px; margin-right: -10px;
} }
#sidebar ul.indented li.indent span.buffername { #sidebar ul.indented li.indent span.buffername {
@ -224,7 +184,7 @@ input[type=text], input[type=password], #sendMessage, .badge {
overflow-x: hidden; overflow-x: hidden;
right: 0; right: 0;
top: 0; top: 0;
padding-top: 35px; padding-top: 39px;
padding-left: 5px; padding-left: 5px;
padding-bottom: 35px; padding-bottom: 35px;
z-index: 2; z-index: 2;
@ -240,22 +200,19 @@ input[type=text], input[type=password], #sendMessage, .badge {
#nicklist a { #nicklist a {
text-decoration: none; text-decoration: none;
} }
#nicklist a:hover {
background: #3b3b3b;
}
#connection-infos { #connection-infos {
float: left; float: left;
max-width: 10%; max-width: 10%;
padding-left: 5px; padding-left: 5px;
font-size: 12px; font-size: 12px;
color: #aaa;
overflow: hidden; overflow: hidden;
} }
.nav-pills > li > a { .nav-pills > li > a {
border-radius: 0; border-radius: 0;
color: #ddd; color: #ddd;
padding: 5px 10px;
} }
.nav-pills > li > a:hover, .nav-pills > li > a:hover span { .nav-pills > li > a:hover, .nav-pills > li > a:hover span {
color: #222; color: #222;
@ -266,6 +223,7 @@ input[type=text], input[type=password], #sendMessage, .badge {
background-color: #eee; background-color: #eee;
color: #222; color: #222;
} }
.content { .content {
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
@ -276,8 +234,9 @@ input[type=text], input[type=password], #sendMessage, .badge {
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
width: auto; width: auto;
bottom: 42px; bottom: 35px; /* input bar */
padding-top: 42px; /* topbar */ padding-top: 42px; /* topbar */
padding-bottom: 7px;
-webkit-transition:0.35s ease all; -webkit-transition:0.35s ease all;
transition:0.35s ease all; transition:0.35s ease all;
} }
@ -288,9 +247,7 @@ input[type=text], input[type=password], #sendMessage, .badge {
tr.bufferline { tr.bufferline {
line-height: 100%; line-height: 100%;
} }
tr.bufferline:hover {
background-color: #222222;
}
td.time { td.time {
padding: 1px 5px 1px 1px; padding: 1px 5px 1px 1px;
vertical-align: top; vertical-align: top;
@ -323,25 +280,9 @@ td.time {
padding-right: 100px; padding-right: 100px;
} }
.color-light-green { /* fix for mobile firefox which ignores :hover */
color: chartreuse; .nav-pills > li > a:active, .nav-pills > li > a:active span {
} text-decoration: none;
.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;
} }
[ng-click], [ng-click],
@ -353,12 +294,9 @@ li.notification {
width: 10px; width: 10px;
height: 10px; height: 10px;
} }
::-webkit-scrollbar-track-piece {
background-color: black;
}
::-webkit-scrollbar-thumb:vertical { ::-webkit-scrollbar-thumb:vertical {
height: 15px; height: 15px;
background: rgba(255,255,255,0.5);
} }
div.embed * { div.embed * {
@ -389,6 +327,30 @@ table.notimestampseconds td.time span.seconds {
display: none !important; 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 { .gb-modal {
z-index: 1000; z-index: 1000;
height: 100%; height: 100%;
@ -427,8 +389,8 @@ table.notimestampseconds td.time span.seconds {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: none; overflow: none;
background-color:rgba(0, 0, 0, 0.5)
} }
.gb-modal .modal-dialog { .gb-modal .modal-dialog {
z-index: 1001; z-index: 1001;
position: absolute; position: absolute;
@ -458,11 +420,12 @@ table.notimestampseconds td.time span.seconds {
border-bottom: 0; border-bottom: 0;
} }
#fontchoice label { .standard-labels label {
font-weight: normal; font-weight: normal;
text-align: left; text-align: left;
} }
h2 { h2 {
padding-bottom: 5px; padding-bottom: 5px;
height: 72px; height: 72px;
@ -511,11 +474,43 @@ h2 span, h2 small {
display: none; 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 { .make-thinner {
padding-right: -15px; 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 */ /* Mobile layout */
/* */ /* */
@ -572,14 +567,10 @@ h2 span, h2 small {
#nicklist { #nicklist {
height: auto; height: auto;
width: auto;
padding: 35px 7px 35px 10px; padding: 35px 7px 35px 10px;
text-align: center; text-align: center;
-webkit-box-shadow: 0px 0px 120px #000;
box-shadow: 0px 0px 120px #000;
position: fixed; position: fixed;
margin-top: 10px; margin-top: 10px;
background: #282828;
bottom: 0px; bottom: 0px;
} }
@ -591,6 +582,10 @@ h2 span, h2 small {
min-height: 0%; min-height: 0%;
} }
.nav-pills > li > a {
padding: 10px 15px;
}
#bufferlines { #bufferlines {
height: 100%; height: 100%;
padding-bottom: 0; padding-bottom: 0;
@ -598,12 +593,12 @@ h2 span, h2 small {
#bufferlines td.time { #bufferlines td.time {
padding-right: 3px; padding-right: 3px;
font-size: 12px; font-size: 0.8em;
} }
#bufferlines td.time span.date { #bufferlines td.time span.date {
display: block; display: block;
margin-top: -2px; margin-bottom: -1px;
} }
#bufferlines td.prefix { #bufferlines td.prefix {
@ -611,7 +606,7 @@ h2 span, h2 small {
padding-right: 5px; padding-right: 5px;
border: 0; border: 0;
font-weight: bold; font-weight: bold;
font-size: 15px; font-size: 1.06em;
} }
#bufferlines td.message { #bufferlines td.message {
@ -624,17 +619,10 @@ h2 span, h2 small {
width: 96%; width: 96%;
} }
/* a different colour is too irregular on mobile */
.repeated-time .cof-chat_time,
.repeated-time .cof-chat_time_delimiters {
color: #999;
}
.footer { .footer {
padding-left: 0px !important; padding-left: 0px !important;
padding-right: 0px !important; padding-right: 0px !important;
width: 100% !important; width: 100% !important;
background: rgb(24,24,24);
} }
.footer.withnicklist { .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 */ /* style options, foreground */
.cof-separator { .cof-separator {
color: #68b5d4; color: #68b5d4;
@ -263,6 +391,9 @@
.cwf-default { .cwf-default {
color: #d9d9d9; color: #d9d9d9;
} }
.light-theme .cwf-default {
color: #282828;
}
.cwf-black { .cwf-black {
color: #000000; color: #000000;
} }
@ -370,13 +501,13 @@
color: #000000; /* 000 Black */ color: #000000; /* 000 Black */
} }
.cef-1 { .cef-1 {
color: #cd0000; /* 001 DarkRed */ color: #7f0000; /* 001 DarkRed */
} }
.cef-2 { .cef-2 {
color: #00cd00; /* 002 DarkGreen */ color: #00cd00; /* 002 DarkGreen */
} }
.cef-3 { .cef-3 {
color: #cdcd00; /* 003 DarkYellow */ color: #fc7f00; /* 003 Orange */
} }
.cef-4 { .cef-4 {
color: #0000ee; /* 004 DarkBlue */ color: #0000ee; /* 004 DarkBlue */
@ -1140,13 +1271,13 @@
background-color: #000000; /* 000 Black */ background-color: #000000; /* 000 Black */
} }
.ceb-1 { .ceb-1 {
background-color: #cd0000; /* 001 DarkRed */ background-color: #7f0000; /* 001 DarkRed */
} }
.ceb-2 { .ceb-2 {
background-color: #00cd00; /* 002 DarkGreen */ background-color: #00cd00; /* 002 DarkGreen */
} }
.ceb-3 { .ceb-3 {
background-color: #cdcd00; /* 003 DarkYellow */ background-color: #fc7f00; /* 003 Orange */
} }
.ceb-4 { .ceb-4 {
background-color: #0000ee; /* 004 DarkBlue */ background-color: #0000ee; /* 004 DarkBlue */
@ -1935,3 +2066,29 @@
color: yellow; color: yellow;
font-weight: bold; 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 id="{{inputId}}" class="form-control favorite-font" ng-trim="false" rows="1" autocomplete="off" ng-model="command" ng-focus="hideSidebar()">
</textarea> </textarea>
<span class="input-group-btn"> <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> </span>
</div> </div>
</form> </form>

@ -1,14 +1,14 @@
<div ng-show="plugin.visible"> <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()"> ng-click="hideContent()">
Hide {{ ::plugin.name }} Hide {{ ::plugin.name }}
</button> </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>
<div ng-hide="plugin.visible"> <div ng-hide="plugin.visible">
<button class="btn btn-sm pull-right" <button class="btn btn-sm pull-right unselectable"
ng-class="::{ ng-class="::{
'btn-warning': plugin.nsfw, 'btn-warning': plugin.nsfw,
'btn-primary': !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="shortcut icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
<link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png"> <link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
<link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" > <link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" >
<link href="css/style.css" rel="stylesheet" media="screen">
<link href="css/glowingbear.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.2/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.2/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.2/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="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular-touch.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-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="3rdparty/inflate.min.js"></script>
<script type="text/javascript" src="min.js"></script> <script type="text/javascript" src="min.js"></script>
<script type="text/javascript" src="3rdparty/favico-0.3.4-mod.min.js"></script> <script type="text/javascript" src="3rdparty/favico-0.3.5.min.js"></script>
</head> </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"> <div ng-hide="connected" class="container">
<h2> <h2>
<img alt="logo" src="assets/img/glowing-bear.svg"> <img alt="logo" src="assets/img/glowing-bear.svg">
@ -56,7 +56,7 @@
<div class="input-group"> <div class="input-group">
<div class="row no-gutter"> <div class="row no-gutter">
<div class="col-sm-9"> <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>
<div class="col-sm-3"> <div class="col-sm-3">
<input type="text" class="form-control favorite-font" id="port" ng-model="port" placeholder="Port"> <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: <div>To start using glowing bear, please enable the relay plugin in your WeeChat client:
<pre> <pre>
/set relay.network.password yourpassword /set relay.network.password yourpassword
/relay add weechat 9001 /relay add weechat {{ port || 9001 }}
</pre> </pre>
<span class="label label-danger">WeeChat version 0.4.2 or higher is required.</span><br> <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. 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> <h3>Shortcuts</h3>
Glowing Bear has a few shortcuts: Glowing Bear has a few shortcuts:
<ul> <ul>
<li>ALT-n: Toggle nicklist</li> <li><kbd>ALT-n</kbd>: Toggle nicklist</li>
<li>ALT-l: Focus on input bar</li> <li><kbd>ALT-l</kbd>: Focus on input bar</li>
<li>ALT-[0-9]: Switch to buffer number N</li> <li><kbd>ALT-[0-9]</kbd>: Switch to buffer number N</li>
<li>ALT-a: Focus on next buffer with activity</li> <li><kbd>ALT-a</kbd>: Focus on next buffer with activity</li>
<li>ALT-&lt;: Switch to previous active buffer</li> <li><kbd>ALT-&lt;</kbd>: Switch to previous active buffer</li>
<li>ALT-g: Focus on buffer list filter</li> <li><kbd>ALT-g</kbd>: Focus on buffer list filter</li>
<li>Esc-Esc: Disconnect (double-tap)</li> <li><kbd>Esc-Esc</kbd>: Disconnect (double-tap)</li>
<li>Arrow keys: Navigate history</li> <li>Arrow keys: Navigate history</li>
<li>Tab key: Complete nick</li> <li><kbd>Tab</kbd>: Complete nick</li>
<li>The following readline/emacs style keybindings can be enabled with a setting: <span title="Move cursor to beginning of line">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>The following readline/emacs style keybindings can be enabled with a setting: <span title="Move cursor to beginning of line"><kbd>Ctrl-a</kbd></span>, <span title="Move cursor to te end of the line"><kbd>Ctrl-e</kbd></span>, <span title="Delete from cursor to beginning of the line"><kbd>Ctrl-u</kbd></span>, <span title="Delete from cursor to the end of the line"><kbd>Ctrl-k</kbd></span>, <span title="Delete from cursor to previous space"><kbd>Ctrl-w</kbd></span></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -141,18 +141,19 @@
</div> </div>
<div id="collapseThree" class="panel-collapse collapse in"> <div id="collapseThree" class="panel-collapse collapse in">
<div class="panel-body"> <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>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> <pre>
$ mkdir -p ~/.weechat/ssl $ mkdir -p ~/.weechat/ssl
$ cd ~/.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'}}/" $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out relay.pem -subj "/CN={{host || 'your weechat host'}}/"
</pre> </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> <pre>
/set relay.network.password yourpassword
/relay sslcertkey /relay sslcertkey
/relay add ssl.weechat 8000 /relay add ssl.weechat {{ port || 9001 }}
</pre> </pre>
</div> </div>
</div> </div>
@ -201,7 +202,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</a> </a>
<button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button> <button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button>
</div> </div>
<div class="title" 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="actions pull-right vertical-line-left">
<div class="pull-left"> <div class="pull-left">
<a class="settings-toggle" ng-click="showModal('settingsModal')" title="Options menu"> <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> </div>
<div id="sidebar" data-state="visible" ng-swipe-left="hideSidebar()" class="vertical-line"> <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"> <li class="bufferfilter">
<form role="form"> <form role="form">
<input class="form-control favorite-font" type="text" id="bufferFilter" ng-model="search" ng-keydown="handleSearchBoxKey($event)" placeholder="Search"> <input class="form-control favorite-font" type="text" id="bufferFilter" ng-model="search" ng-keydown="handleSearchBoxKey($event)" placeholder="Search">
</form> </form>
</li> </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 }}"> <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="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> </a>
</li> </li>
</ul> </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 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> </span>
</td> </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="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"> --><td class="message"><!--
<div ng-repeat="metadata in ::bufferline.metadata" plugin data="::metadata"></div> --><div ng-repeat="metadata in ::bufferline.metadata" plugin data="::metadata"></div><!--
<span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes" ng-bind-html="::part.text | irclinky:'_blank' | inlinecolour"></span> --><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> </td>
</tr> </tr>
<tr class="readmarker" ng-if="activeBuffer().lastSeen==$index"> <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> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table><span id="end-of-buffer"></span>
</div> </div>
<div class="footer" ng-class="{'withnicklist': showNicklist}"> <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> </div>
<div id="soundNotification"></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>
<div class="modal-body"> <div class="modal-body">
<ul class=""> <ul class="">
<li id="fontchoice"> <li class="standard-labels">
<form class="form-horizontal" role="form"> <form class="form-horizontal" role="form">
<div class="form-group"> <div class="form-group">
<label for="font" class="col-sm-3 control-label make-thinner">Preferred font</label> <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> <label for="size" class="col-sm-1 control-label">Size</label>
<div class="col-sm-2"> <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>
</div> </div>
</form> </form>
</li> </li>
<li> <li>
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">
@ -334,7 +347,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="noembed"> <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> </label>
</div> </div>
</form> </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. * 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 * Get a new version of a nick list, sorted by last speaker
* *
* @param nickList Original nick list * @param nickList Original nick list
* @return Sorted nick list * @return Sorted nick list
*/ */
_ciNickList: function(nickList) { var _ciNickList = function(nickList) {
var newList = _(nickList).sortBy(function(nickObj) { var newList = _(nickList).sortBy(function(nickObj) {
return -nickObj.spokeAt; return -nickObj.spokeAt;
@ -17,7 +22,7 @@ var IrcUtils = {
newList = _(newList).pluck('name'); newList = _(newList).pluck('name');
return newList; return newList;
}, };
/** /**
* Completes a single nick. * Completes a single nick.
@ -26,7 +31,7 @@ var IrcUtils = {
* @param nickList Array of current nicks sorted for case insensitive searching * @param nickList Array of current nicks sorted for case insensitive searching
* @return Completed nick (null if not found) * @return Completed nick (null if not found)
*/ */
_completeSingleNick: function(candidate, nickList) { var _completeSingleNick = function(candidate, nickList) {
var foundNick = null; var foundNick = null;
nickList.some(function(nick) { nickList.some(function(nick) {
@ -39,7 +44,7 @@ var IrcUtils = {
}); });
return foundNick; return foundNick;
}, };
/** /**
* Get the next nick when iterating nicks. * Get the next nick when iterating nicks.
@ -49,7 +54,7 @@ var IrcUtils = {
* @param nickList Array of current nicks sorted for case insensitive searching * @param nickList Array of current nicks sorted for case insensitive searching
* @return Next nick (may be the same) * @return Next nick (may be the same)
*/ */
_nextNick: function(iterCandidate, currentNick, nickList) { var _nextNick = function(iterCandidate, currentNick, nickList) {
var matchingNicks = []; var matchingNicks = [];
var at = null; var at = null;
var lcIterCandidate = iterCandidate.toLowerCase(); var lcIterCandidate = iterCandidate.toLowerCase();
@ -82,7 +87,7 @@ var IrcUtils = {
} }
return matchingNicks[at]; return matchingNicks[at];
} }
}, };
/** /**
* Nicks tab completion. * Nicks tab completion.
@ -98,14 +103,14 @@ var IrcUtils = {
* foundNick: completed nick (or null if not possible) * foundNick: completed nick (or null if not possible)
* iterCandidate: current iterating candidate * iterCandidate: current iterating candidate
*/ */
completeNick: function(text, caretPos, iterCandidate, nickList, suf) { var completeNick = function(text, caretPos, iterCandidate, nickList, suf) {
var doIterate = (iterCandidate !== null); var doIterate = (iterCandidate !== null);
if (suf === null) { if (suf === null) {
suf = ':'; suf = ':';
} }
// new nick list to search in // new nick list to search in
var searchNickList = IrcUtils._ciNickList(nickList); var searchNickList = _ciNickList(nickList);
// text before and after caret // text before and after caret
var beforeCaret = text.substring(0, caretPos); var beforeCaret = text.substring(0, caretPos);
@ -126,7 +131,7 @@ var IrcUtils = {
if (m) { if (m) {
if (doIterate) { if (doIterate) {
// try iterating // try iterating
newNick = IrcUtils._nextNick(iterCandidate, m[1], searchNickList); newNick = _nextNick(iterCandidate, m[1], searchNickList);
beforeCaret = newNick + suf + ' '; beforeCaret = newNick + suf + ' ';
return { return {
text: beforeCaret + afterCaret, text: beforeCaret + afterCaret,
@ -144,7 +149,7 @@ var IrcUtils = {
m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/); m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) { if (m) {
// try completing // try completing
newNick = IrcUtils._completeSingleNick(m[1], searchNickList); newNick = _completeSingleNick(m[1], searchNickList);
if (newNick === null) { if (newNick === null) {
// no match // no match
return ret; return ret;
@ -167,7 +172,7 @@ var IrcUtils = {
if (m) { if (m) {
if (doIterate) { if (doIterate) {
// try iterating // try iterating
newNick = IrcUtils._nextNick(iterCandidate, m[2], searchNickList); newNick = _nextNick(iterCandidate, m[2], searchNickList);
beforeCaret = m[1] + newNick + ' '; beforeCaret = m[1] + newNick + ' ';
return { return {
text: beforeCaret + afterCaret, text: beforeCaret + afterCaret,
@ -185,7 +190,7 @@ var IrcUtils = {
m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+)$/); m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) { if (m) {
// try completing // try completing
newNick = IrcUtils._completeSingleNick(m[2], searchNickList); newNick = _completeSingleNick(m[2], searchNickList);
if (newNick === null) { if (newNick === null) {
// no match // no match
return ret; return ret;
@ -205,5 +210,10 @@ var IrcUtils = {
// completion not possible // completion not possible
return ret; return ret;
} };
};
return {
'completeNick': completeNick
};
}]);
})();

@ -2,6 +2,9 @@
* This file contains the weechat models and various * This file contains the weechat models and various
* helper methods to work with them. * helper methods to work with them.
*/ */
(function() {
'use strict';
var models = angular.module('weechatModels', []); var models = angular.module('weechatModels', []);
models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) { models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) {
@ -12,6 +15,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
// weechat properties // weechat properties
var fullName = message.full_name; var fullName = message.full_name;
var shortName = message.short_name; var shortName = message.short_name;
var trimmedName = shortName.replace(/^[#&+]/, '');
var title = message.title; var title = message.title;
var number = message.number; var number = message.number;
var pointer = message.pointers[0]; var pointer = message.pointers[0];
@ -26,7 +30,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
var notification = 0; var notification = 0;
var unread = 0; var unread = 0;
var lastSeen = -1; 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 type = message.local_variables.type;
var indent = (['channel', 'private'].indexOf(type) >= 0); var indent = (['channel', 'private'].indexOf(type) >= 0);
@ -221,6 +225,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
id: pointer, id: pointer,
fullName: fullName, fullName: fullName,
shortName: shortName, shortName: shortName,
trimmedName: trimmedName,
number: number, number: number,
title: title, title: title,
lines: lines, lines: lines,
@ -238,6 +243,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
getNicklistByTime: getNicklistByTime, getNicklistByTime: getNicklistByTime,
serverSortKey: serverSortKey, serverSortKey: serverSortKey,
indent: indent, indent: indent,
type: type,
history: history, history: history,
addToHistory: addToHistory, addToHistory: addToHistory,
getHistoryUp: getHistoryUp, getHistoryUp: getHistoryUp,
@ -282,6 +288,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
if (textEl.attrs.name !== null) { if (textEl.attrs.name !== null) {
textEl.classes.push('coa-' + textEl.attrs.name); textEl.classes.push('coa-' + textEl.attrs.name);
} }
var val;
for (var attr in textEl.attrs.override) { for (var attr in textEl.attrs.override) {
val = textEl.attrs.override[attr]; val = textEl.attrs.override[attr];
if (val) { if (val) {
@ -526,3 +533,4 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
delete(this.model.buffers[bufferId]); 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 * 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 * 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: * 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. * 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. * argument.
* *
* 3. Add it to the plugins array. * 3. Add it to the plugins array.
@ -143,7 +146,7 @@ plugins.factory('userPlugins', function() {
document.body.appendChild(script); 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) { var urlPlugin = function(callback) {
return function(message) { return function(message) {
@ -168,7 +171,7 @@ plugins.factory('userPlugins', function() {
*/ */
var spotifyPlugin = new Plugin(function(message) { var spotifyPlugin = new Plugin(function(message) {
content = []; var content = [];
var addMatch = function(match) { var addMatch = function(match) {
for (var i = 0; match && i < match.length; i++) { for (var i = 0; match && i < match.length; i++) {
var id = match[i].substr(match[i].length - 22, match[i].length); var id = match[i].substr(match[i].length - 22, match[i].length);
@ -345,7 +348,7 @@ plugins.factory('userPlugins', function() {
url = match[0] + '.json'; url = match[0] + '.json';
// load gist asynchronously -- return a function here // load gist asynchronously -- return a function here
return function() { return function() {
var element = document.querySelector('.embed_' + this.$$hashKey); var element = this.getElement();
jsonp(url, function(data) { jsonp(url, function(data) {
// Add the gist stylesheet only once // Add the gist stylesheet only once
if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) { if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) {
@ -367,7 +370,7 @@ plugins.factory('userPlugins', function() {
if (match) { if (match) {
url = 'https://api.twitter.com/1/statuses/oembed.json?id=' + match[2]; url = 'https://api.twitter.com/1/statuses/oembed.json?id=' + match[2];
return function() { return function() {
var element = document.querySelector('.embed_' + this.$$hashKey); var element = this.getElement();
jsonp(url, function(data) { jsonp(url, function(data) {
// sepearate the HTML into content and script tag // sepearate the HTML into content and script tag
var scriptIndex = data.html.indexOf("<script "); 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', []); var websockets = angular.module('ngWebsockets', []);
websockets.factory('ngWebsockets', websockets.factory('ngWebsockets',
@ -5,7 +8,7 @@ websockets.factory('ngWebsockets',
function($rootScope, $q) { function($rootScope, $q) {
this.protocol = null; var protocol = null;
var ws = null; var ws = null;
var callbacks = {}; var callbacks = {};
@ -17,7 +20,7 @@ function($rootScope, $q) {
* *
* @param reason reason for failure * @param reason reason for failure
*/ */
failCallbacks = function(reason) { var failCallbacks = function(reason) {
for (var i in callbacks) { for (var i in callbacks) {
callbacks[i].cb.reject(reason); callbacks[i].cb.reject(reason);
} }
@ -111,11 +114,11 @@ function($rootScope, $q) {
}; };
var connect = function(url, var connect = function(url,
protocol, protocol_,
properties) { properties) {
ws = new WebSocket(url); ws = new WebSocket(url);
protocol = protocol; protocol = protocol_;
for (var property in properties) { for (var property in properties) {
ws[property] = properties[property]; ws[property] = properties[property];
} }
@ -138,7 +141,9 @@ function($rootScope, $q) {
send: send, send: send,
sendAll: sendAll, sendAll: sendAll,
connect: connect, 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 (function(exports) {// http://weechat.org/files/doc/devel/weechat_dev.en.html#color_codes_in_strings
'use strict';
/** /**
* WeeChat protocol handling. * WeeChat protocol handling.
@ -604,16 +605,6 @@
return defaults; 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. * Formats a command.
* *
@ -966,7 +957,7 @@
var objs = []; var objs = [];
var hpath = this._getString(); var hpath = this._getString();
keys = this._getString().split(','); var keys = this._getString().split(',');
paths = hpath.split('/'); paths = hpath.split('/');
count = this._getInt(); count = this._getInt();
@ -1179,6 +1170,17 @@
this._data = data; 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. * Parses a WeeChat message.
* *

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

@ -16,5 +16,5 @@
"url": "https://github.com/glowing-bear" "url": "https://github.com/glowing-bear"
}, },
"default_locale": "en", "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", "name": "glowing-bear",
"private": true, "private": true,
"version": "0.4.1", "version": "0.4.4",
"description": "A web client for Weechat", "description": "A web client for Weechat",
"repository": "https://github.com/glowing-bear/glowing-bear", "repository": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3", "license": "GPLv3",
@ -18,7 +18,7 @@
"scripts": { "scripts": {
"postinstall": "bower install", "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", "prestart": "npm install",
"start": "http-server -a localhost -p 8000", "start": "http-server -a localhost -p 8000",

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

Loading…
Cancel
Save