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

gh-pages
Lorenz Hübschle-Schneider 10 years ago
commit 958fcb04c4
  1. 1
      .travis.yml
  2. 186
      css/glowingbear.css
  3. 30
      css/themes/dark.css
  4. 9
      css/themes/light.css
  5. 4
      directives/input.html
  6. 4
      directives/plugin.html
  7. 57
      index.html
  8. 133
      js/connection.js
  9. 49
      js/filters.js
  10. 124
      js/glowingbear.js
  11. 90
      js/handlers.js
  12. 32
      js/inputbar.js
  13. 13
      js/irc-utils.js
  14. 30
      js/models.js
  15. 1
      js/notifications.js
  16. 13
      js/settings.js
  17. 3
      js/websockets.js
  18. 2
      manifest.json
  19. 2
      manifest.webapp
  20. 2
      package.json
  21. 4
      test/unit/filters.js

@ -11,3 +11,4 @@ notifications:
skip_join: true skip_join: true
on_success: never on_success: never
on_failure: always on_failure: always
sudo: false

@ -7,10 +7,20 @@ body {
overflow: hidden; overflow: hidden;
} }
.mobile {
display: none;
}
a { a {
cursor: pointer; cursor: pointer;
} }
.hidden-bracket {
position: absolute;
left: -1000px;
overflow: hidden;
}
td.prefix { td.prefix {
text-align: right; text-align: right;
vertical-align: top; vertical-align: top;
@ -50,16 +60,25 @@ td.message {
#sendMessage { #sendMessage {
width: 100%; width: 100%;
height: 36px; height: 35px;
resize: none; resize: none;
} }
#sendMessage:focus, #sendMessage:active {
border-bottom: 2px solid #555;
}
.input-group-addon, .input-group-btn {
vertical-align: top;
}
.footer button { .footer button {
border-radius: 0; border-radius: 0;
} }
.panel input, .panel .input-group { .panel input, .panel .input-group {
max-width: 300px; max-width: 300px;
} }
input[type=text], input[type=password], #sendMessage, .badge { input[type=text], input[type=password], #sendMessage {
border: 0; border: 0;
border-radius: 0; border-radius: 0;
margin-bottom: 5px !important; margin-bottom: 5px !important;
@ -81,10 +100,6 @@ input[type=text], input[type=password], #sendMessage, .badge {
.glyphicon { .glyphicon {
top: 0; /* Fixes alignment issue in top bar */ top: 0; /* Fixes alignment issue in top bar */
} }
.glyphicon-off {
top: 1px; /* Fixes for relative glyphicon size */
font-size: 28px;
}
#topbar { #topbar {
position: fixed; position: fixed;
width: 100%; width: 100%;
@ -97,7 +112,10 @@ input[type=text], input[type=password], #sendMessage, .badge {
#topbar .brand { #topbar .brand {
float: left; float: left;
height: 35px; height: 35px;
padding-left: 5px; }
#topbar .brand a {
display: inline-block;
padding: 0 10px;
} }
#topbar .brand img { #topbar .brand img {
height: 32px; height: 32px;
@ -114,21 +132,25 @@ input[type=text], input[type=password], #sendMessage, .badge {
left: 145px; /* sidebar */ left: 145px; /* sidebar */
overflow: hidden; overflow: hidden;
} }
#topbar .actions { #topbar .actions {
margin-left: 5px; margin-left: 5px;
padding-left: 5px; padding-left: 5px;
margin-right: 0; margin-right: 0;
padding-right: 5px; padding-right: 5px;
padding-top: 2px;
height: 35px; height: 35px;
line-height: 35px; line-height: 35px;
font-size: 30px; font-size: 22px;
position: fixed; position: fixed;
right: 0; right: 0;
} }
#topbar .actions > * { #topbar .actions > * {
padding-left: 5px; padding: 0 5px;
display: inline-block;
}
#topbar .actions .glyphicon {
line-height: 35px;
top: 0;
} }
#topbar .dropdown-menu form { #topbar .dropdown-menu form {
padding-left: 6px; padding-left: 6px;
@ -165,6 +187,7 @@ input[type=text], input[type=password], #sendMessage, .badge {
#sidebar .badge { #sidebar .badge {
border-radius: 0; border-radius: 0;
margin-right: -10px; margin-right: -10px;
padding: 4px 7px;
} }
#sidebar ul.indented li.indent span.buffername { #sidebar ul.indented li.indent span.buffername {
@ -209,6 +232,12 @@ input[type=text], input[type=password], #sendMessage, .badge {
overflow: hidden; overflow: hidden;
} }
.nav-pills li {
min-height: 20px;
}
.nav-pills li+li {
margin-top: 0;
}
.nav-pills > li > a { .nav-pills > li > a {
border-radius: 0; border-radius: 0;
color: #ddd; color: #ddd;
@ -224,6 +253,13 @@ input[type=text], input[type=password], #sendMessage, .badge {
color: #222; color: #222;
} }
.nav-pills > li > a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content { .content {
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
@ -237,8 +273,9 @@ input[type=text], input[type=password], #sendMessage, .badge {
bottom: 35px; /* input bar */ bottom: 35px; /* input bar */
padding-top: 42px; /* topbar */ padding-top: 42px; /* topbar */
padding-bottom: 7px; padding-bottom: 7px;
-webkit-transition:0.35s ease all; -webkit-transition:0.2 ease-in-out all;
transition:0.35s ease all; transition:0.2s ease-in-out all;
-webkit-overflow-scrolling: touch; /* Native scroll on ios */
} }
#bufferlines > table { #bufferlines > table {
margin-top: 35px; margin-top: 35px;
@ -263,13 +300,26 @@ td.time {
font-family: sans-serif; font-family: sans-serif;
} }
#reconnect {
top: 35px;
position: fixed;
z-index: 9999;
width: 80%;
margin: 0;
padding: 5px;
left: 10%;
}
#reconnect a {
color: white;
}
.footer { .footer {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
height: 35px; height: 35px;
width: 100%; width: 100%;
-webkit-transition:0.35s ease all; -webkit-transition:0.2s ease-in-out all;
transition:0.35s ease all; transition:0.2s ease-in-out all;
z-index: 1; z-index: 1;
} }
.content[sidebar-state=visible] .footer { .content[sidebar-state=visible] .footer {
@ -332,8 +382,8 @@ table.notimestampseconds td.time span.seconds {
} }
#sidebar .showquickkeys .buffer .buffer-quick-key { #sidebar .showquickkeys .buffer .buffer-quick-key {
transition: all ease 0.5s; transition: all ease-in-out 0.5s;
-webkit-transition: all ease 0.5s; -webkit-transition: all ease-in-out 0.5s;
transition-delay: 0.2s; transition-delay: 0.2s;
-webkit-transition-delay: 0.2s; -webkit-transition-delay: 0.2s;
opacity: 0.7; opacity: 0.7;
@ -342,8 +392,8 @@ table.notimestampseconds td.time span.seconds {
margin-left: -0.7em; margin-left: -0.7em;
margin-right: -0.2em; margin-right: -0.2em;
font-size: smaller; font-size: smaller;
transition: all ease 0.5s; transition: all ease-in-out 0.5s;
-webkit-transition: all ease 0.5s; -webkit-transition: all ease-in-out 0.5s;
opacity: 0; opacity: 0;
text-shadow: -1px 0px 4px rgba(255, 255, 255, 0.4), text-shadow: -1px 0px 4px rgba(255, 255, 255, 0.4),
0px -1px 4px rgba(255, 255, 255, 0.4), 0px -1px 4px rgba(255, 255, 255, 0.4),
@ -479,20 +529,20 @@ h2 span, h2 small {
} }
} }
/* bold hash before channels */ /* bold hash before channels */
li.buffer.channel a span:last-of-type:before { li.buffer.channel a span:last-of-type:before, #topbar .title .channel:before {
color: #888; color: #888;
font-weight: bold; font-weight: bold;
} }
li.buffer.channel_hash a span:last-of-type:before { li.buffer.channel_hash a span:last-of-type:before, #topbar .title .channel_hash:before {
content: '#'; content: '#';
} }
li.buffer.channel_plus a span:last-of-type:before { li.buffer.channel_plus a span:last-of-type:before, #topbar .title .channel_plus:before {
content: '+'; content: '+';
} }
li.buffer.channel_ampersand a span:last-of-type:before { li.buffer.channel_ampersand a span:last-of-type:before, #topbar .title .channel_ampersand:before {
content: '&'; content: '&';
} }
@ -526,12 +576,52 @@ li.buffer.indent.private a {
user-select: none; user-select: none;
} }
/* Scales emoji to font size */ .emojione {
img.emoji { font-size: inherit;
height: 1em; height: 1em;
width: 1em; width: 1.1em;
margin: 0 .05em 0 .1em; min-height: 16px;
vertical-align: -0.1em; min-width: 16px;
display: inline-block;
margin: -.2ex .15em .2ex;
line-height: normal;
vertical-align: middle;
}
img.emojione {
width: auto;
}
@media (min-width: 1400px) {
#sidebar[data-state=visible], #sidebar {
width: 200px;
}
.content[sidebar-state="visible"] #bufferlines {
margin-left: 205px;
}
#topbar .title {
left: 205px;
}
.content[sidebar-state=visible] .footer {
padding-left: 200px;
}
.nav-pills {
font-size: 14px;
}
.nav-pills li a {
padding: 10px 15px;
}
#nicklist {
width: 140px;
}
.withnicklist {
margin-right: 140px !important; /* nicklist */
}
.footer.withnicklist {
padding-right: 148px !important;
}
} }
/* */ /* */
@ -539,6 +629,14 @@ img.emoji {
/* */ /* */
@media (max-width: 968px) { @media (max-width: 968px) {
.mobile {
display: inherit;
}
.desktop {
display: none;
}
#bufferlines table { #bufferlines table {
border-collapse: separate; border-collapse: separate;
border-spacing: 2px 3px; border-spacing: 2px 3px;
@ -549,6 +647,7 @@ img.emoji {
bottom: 0px; bottom: 0px;
top: 0px; top: 0px;
padding-bottom: 35px; padding-bottom: 35px;
width: 200px;
} }
#sidebar.in, #sidebar.collapsing { #sidebar.in, #sidebar.collapsing {
@ -558,28 +657,37 @@ img.emoji {
} }
#sidebar[data-state=visible] { #sidebar[data-state=visible] {
width: 200px; transform: translate(0,0);
-webkit-transform: translate(0,0); /* Safari */
} }
#sidebar[data-state=hidden] { #sidebar[data-state=hidden] {
left: -200px; transform: translate(-200px,0);
-webkit-transform: translate(-200px,0);
} }
.content[sidebar-state=visible] #bufferlines, .content[sidebar-state=visible] .footer { .content[sidebar-state=visible] #bufferlines, .content[sidebar-state=visible] .footer {
margin-left: 0px; margin-left: 0px;
transform: translate(200px,0);
-webkit-transform: translate(200px,0);
} }
#topbar .title { #topbar .title {
left: 40px; left: 40px;
right: 60px;
text-align: center;
font-size: 18px;
} }
#topbar .actions { #topbar .brand img {
line-height: 35px; height: 28px;
height: 35px;
font-size: 31px;
margin-right: 0;
} }
#topbar .badge {
display: none;
}
#bufferlines, #nicklist { #bufferlines, #nicklist {
position: relative; position: relative;
min-height: 0; min-height: 0;
@ -605,7 +713,11 @@ img.emoji {
min-height: 0%; min-height: 0%;
} }
.nav-pills > li > a { .nav-pills {
font-size: 14px;
}
.nav-pills li a {
padding: 10px 15px; padding: 10px 15px;
} }

@ -20,8 +20,12 @@ html {
color: #333; color: #333;
} }
.nav-pills > li.active > a {
background-color: #555;
}
/* fix for mobile firefox which ignores :hover */ /* fix for mobile firefox which ignores :hover */
.nav-pills > li > a:active, .nav-pills > li > a:active span { .nav-pills > li > a:active, .nav-pills > li > a:active span, .nav-pills > li.active > a:hover {
background-color: #eee; background-color: #eee;
color: #222; color: #222;
} }
@ -39,6 +43,15 @@ tr.bufferline:hover {
color: black; color: black;
} }
.btn-default {
background-color: #555;
border-color: #444;
}
.btn-default:hover {
background-color: #666;
border-color: #555;
}
li.notification { li.notification {
color: green; color: green;
} }
@ -56,15 +69,27 @@ li.notification {
} }
input[type=text], input[type=password], #sendMessage, .badge { 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; box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset;
}
input[type=text], input[type=password], #sendMessage, .badge, .btn-send {
color: #ccc;
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3);
} }
.btn-send:hover, .btn-send:focus {
background-color: #555;
color: white;
}
#connection-infos { #connection-infos {
color: #aaa; color: #aaa;
} }
.nav-pills li:nth-child(2n) {
background: #232323;
}
.nav-pills > li > a { .nav-pills > li > a {
color: #ddd; color: #ddd;
} }
@ -83,6 +108,7 @@ input[type=text], input[type=password], #sendMessage, .badge {
#topbar .actions { #topbar .actions {
background: #282828; background: #282828;
color: #666;
} }
#topbar, #sidebar, .panel, .dropdown-menu, .modal-content { #topbar, #sidebar, .panel, .dropdown-menu, .modal-content {

@ -22,6 +22,11 @@ html {
background-color: #222; background-color: #222;
} }
.btn-send {
background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.3);
color: #428BCA;
}
tr.bufferline:hover { tr.bufferline:hover {
background-color: #efefef; background-color: #efefef;
} }
@ -61,6 +66,10 @@ select.form-control, select option, input[type=text], input[type=password], #sen
color: #aaa; color: #aaa;
} }
.nav-pills li:nth-child(2n) {
background: #e1e1e1;
}
.nav-pills > li > a { .nav-pills > li > a {
color: #222; color: #222;
} }

@ -1,9 +1,9 @@
<form class="form form-horizontal" id="inputform" ng-submit="sendMessage()"> <form class="form form-horizontal" id="inputform" ng-submit="sendMessage()">
<div class="input-group"> <div class="input-group">
<textarea id="{{inputId}}" class="form-control favorite-font" ng-trim="false" rows="1" autocomplete="on" ng-model="command" ng-focus="hideSidebar()"> <textarea id="{{inputId}}" class="form-control favorite-font" ng-trim="false" rows="1" ng-change="inputChanged()" autocomplete="on" ng-model="command" ng-focus="hideSidebar()">
</textarea> </textarea>
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-default btn-primary unselectable">Send</button> <button class="btn btn-send unselectable"><i class="glyphicon glyphicon-send"></i></button>
</span> </span>
</div> </div>
</form> </form>

@ -1,5 +1,5 @@
<div ng-show="plugin.visible"> <div ng-show="plugin.visible">
<button class="btn btn-primary btn-sm pull-right unselectable" <button class="btn btn-default btn-sm pull-right unselectable"
ng-click="hideContent()"> ng-click="hideContent()">
Hide {{ ::plugin.name }} Hide {{ ::plugin.name }}
</button> </button>
@ -8,7 +8,7 @@
</div> </div>
<div ng-hide="plugin.visible"> <div ng-hide="plugin.visible">
<button class="btn btn-sm pull-right unselectable" <button class="btn btn-default 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}"

@ -7,6 +7,7 @@
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Glowing Bear">
<meta name="theme-color" content="#2779d3"> <meta name="theme-color" content="#2779d3">
<title ng-bind-template="{{ notificationStatus }}Glowing Bear {{ pageTitle}}"></title> <title ng-bind-template="{{ notificationStatus }}Glowing Bear {{ pageTitle}}"></title>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen"> <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen">
@ -14,18 +15,18 @@
<link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png"> <link rel="apple-touch-icon" sizes="128x128" href="assets/img/glowing_bear_128x128.png">
<link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" > <link rel="shortcut icon" type="image/png" href="assets/img/favicon.png" >
<link href="css/glowingbear.css" rel="stylesheet" media="screen"> <link href="css/glowingbear.css" rel="stylesheet" media="screen">
<link href="css/themes/dark.css" rel="stylesheet" media="screen" id="themeCSS" />
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-route.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-sanitize.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-sanitize.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-touch.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-touch.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="//twemoji.maxcdn.com/twemoji.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/emojione/1.4.0/lib/js/emojione.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.5.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-keyup="handleKeyRelease($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}" ng-init="init()" lang="en-US">
<link ng-href="css/themes/{{settings.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">
@ -206,17 +207,21 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
<a href="#" ng-click="toggleSidebar()"> <a href="#" ng-click="toggleSidebar()">
<img alt="brand" src="assets/img/favicon.png" title="Connected to {{ settings.host }}:{{ settings.port}}"> <img alt="brand" src="assets/img/favicon.png" title="Connected to {{ settings.host }}:{{ settings.port}}">
</a> </a>
<span class="badge" ng-show="unread > 0">{{unread}}</span>
<span class="badge danger" ng-show="notifications > 0">{{notifications}}</span>
<button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button> <button ng-if="debugMode" ng-click="countWatchers()">Count<br />Watchers</button>
</div> </div>
<div class="title" title="{{activeBuffer().rtitle}}"> <div class="title" title="{{activeBuffer().rtitle}}">
<span ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></span> <span class="desktop" ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></span>
<span class="mobile" ng-click="showModal('topicModal')" ng-class="{'active': activeBuffer().active, 'channel': activeBuffer().type === 'channel', 'channel_hash': activeBuffer().prefix === '#', 'channel_plus': activeBuffer().prefix === '+', 'channel_ampersand': activeBuffer().prefix === '&'}">{{ activeBuffer().trimmedName || activeBuffer().fullName }}</span>
</div> </div>
<div class="actions pull-right vertical-line-left"> <div class="actions pull-right vertical-line-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"> <i class="glyphicon glyphicon-cog"></i>
<i class="glyphicon glyphicon-cog"></i> </a>
</a>
</div>
<a ng-click="disconnect()" title="Disconnect from WeeChat"> <a ng-click="disconnect()" title="Disconnect from WeeChat">
<i class="glyphicon glyphicon-off"></i> <i class="glyphicon glyphicon-off"></i>
</a> </a>
@ -262,10 +267,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 class="hidden-bracket">&lt;</span><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text|prefixlimit:25"></span><span class="hidden-bracket">&gt;</span></a></td><!--
--><td class="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 | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'emojify':settings.enableJSEmoji | DOMfilter:'inlinecolour' "></span> --><span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes.concat(['line-' + part.$$hashKey.replace(':','_')])" ng-bind-html="::part.text | linky:'_blank' | DOMfilter:'irclinky' | DOMfilter:'emojify':settings.enableJSEmoji | DOMfilter:'inlinecolour' | DOMfilter:'mathjax':('.line-' + part.$$hashKey.replace(':','_')):settings.enableMathjax"></span>
</td> </td>
</tr> </tr>
<tr class="readmarker" ng-if="activeBuffer().lastSeen==$index"> <tr class="readmarker" ng-if="activeBuffer().lastSeen==$index">
@ -281,6 +286,11 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</div> </div>
</div> </div>
<div id="soundNotification"></div> <div id="soundNotification"></div>
<div id="reconnect" class="alert alert-danger" ng-click="reconnect()" ng-show="reconnecting">
<p><strong>Connection to WeeChat lost</strong></p>
<i class="glyphicon glyphicon-refresh"></i>
Reconnecting... <a class="btn btn-tiny" ng-click="reconnect()">Click to try to reconnect now</a>
</div>
<div id="settingsModal" class="gb-modal" data-state="hidden"> <div id="settingsModal" class="gb-modal" data-state="hidden">
<div class="backdrop" ng-click="closeModal($event)"></div> <div class="backdrop" ng-click="closeModal($event)"></div>
<div class="modal-dialog"> <div class="modal-dialog">
@ -430,6 +440,16 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</div> </div>
</form> </form>
</li> </li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.enableMathjax">
Enable LaTeX math rendering
</label>
</div>
</form>
</li>
</ul> </ul>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -438,5 +458,20 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
</div><!-- /.modal --> </div><!-- /.modal -->
<div id="topicModal" class="gb-modal" data-state="hidden">
<div class="backdrop" ng-click="closeModal($event)"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button>
<h4 class="modal-title">Channel topic</h4>
<p ng-repeat="part in activeBuffer().title" ng-class="::part.classes" ng-bind-html="::(part.text | linky:'_blank' | DOMfilter:'irclinky')"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="closeModal($event)">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</body> </body>
</html> </html>

@ -12,9 +12,12 @@ weechat.factory('connection',
var protocol = new weeChat.Protocol(); var protocol = new weeChat.Protocol();
// Takes care of the connection and websocket hooks var connectionData = [];
var reconnectTimer;
var connect = function (host, port, passwd, ssl, noCompression) { // Takes care of the connection and websocket hooks
var connect = function (host, port, passwd, ssl, noCompression, successCallback, failCallback) {
connectionData = [host, port, passwd, ssl, noCompression];
var proto = ssl ? 'wss' : 'ws'; var proto = ssl ? 'wss' : 'ws';
// If host is an IPv6 literal wrap it in brackets // If host is an IPv6 literal wrap it in brackets
if (host.indexOf(":") !== -1) { if (host.indexOf(":") !== -1) {
@ -59,7 +62,7 @@ weechat.factory('connection',
return ngWebsockets.send( return ngWebsockets.send(
weeChat.Protocol.formatHdata({ weeChat.Protocol.formatHdata({
path: 'buffer:gui_buffers(*)', path: 'buffer:gui_buffers(*)',
keys: ['local_variables,notify,number,full_name,short_name,title'] keys: ['local_variables,notify,number,full_name,short_name,title,hidden']
}) })
); );
}; };
@ -75,25 +78,20 @@ weechat.factory('connection',
// a version command. If it fails, it means the we // a version command. If it fails, it means the we
// did not provide the proper password. // did not provide the proper password.
_initializeConnection(passwd).then( _initializeConnection(passwd).then(
function() { function(version) {
handlers.handleVersionInfo(version);
// Connection is successful // Connection is successful
// Send all the other commands required for initialization // Send all the other commands required for initialization
_requestBufferInfos().then(function(bufinfo) { _requestBufferInfos().then(function(bufinfo) {
//XXX move to handlers? handlers.handleBufferInfo(bufinfo);
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) { _requestHotlist().then(function(hotlist) {
handlers.handleHotlistInfo(hotlist); handlers.handleHotlistInfo(hotlist);
if (successCallback) {
successCallback();
}
}); });
_requestSync(); _requestSync();
@ -123,17 +121,23 @@ weechat.factory('connection',
* Handles websocket disconnection * Handles websocket disconnection
*/ */
$log.info("Disconnected from relay"); $log.info("Disconnected from relay");
ngWebsockets.failCallbacks('disconnection'); if ($rootScope.userdisconnect || !$rootScope.waseverconnected) {
$rootScope.connected = false; handleClose(evt);
$rootScope.$emit('relayDisconnect'); $rootScope.userdisconnect = false;
if (ssl && evt.code === 1006) { } else {
reconnect(evt);
}
};
var handleClose = function (evt) {
if (ssl && evt && evt.code === 1006) {
// A password error doesn't trigger onerror, but certificate issues do. Check time of last error. // 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) { if (typeof $rootScope.lastError !== "undefined" && (Date.now() - $rootScope.lastError) < 1000) {
// abnormal disconnect by client, most likely ssl error // abnormal disconnect by client, most likely ssl error
$rootScope.sslError = true; $rootScope.sslError = true;
$rootScope.$apply();
} }
} }
$rootScope.$apply();
}; };
var onerror = function (evt) { var onerror = function (evt) {
@ -166,12 +170,81 @@ weechat.factory('connection',
$rootScope.errorMessage = true; $rootScope.errorMessage = true;
$rootScope.securityError = true; $rootScope.securityError = true;
$rootScope.$emit('relayDisconnect'); $rootScope.$emit('relayDisconnect');
if (failCallback) {
failCallback();
}
}
};
var attemptReconnect = function (bufferId, timeout) {
$log.info('Attempting to reconnect...');
var d = connectionData;
connect(d[0], d[1], d[2], d[3], d[4], function() {
$rootScope.reconnecting = false;
// on success, update active buffer
models.setActiveBuffer(bufferId);
$log.info('Sucessfully reconnected to relay');
}, function() {
// on failure, schedule another attempt
if (timeout >= 600000) {
// If timeout is ten minutes or more, give up
$log.info('Failed to reconnect, giving up');
handleClose();
} else {
$log.info('Failed to reconnect, scheduling next attempt in', timeout/1000, 'seconds');
// Clear previous timer, if exists
if (reconnectTimer !== undefined) {
clearTimeout(reconnectTimer);
}
reconnectTimer = setTimeout(function() {
// exponential timeout increase
attemptReconnect(bufferId, timeout * 1.5);
}, timeout);
}
});
};
var reconnect = function (evt) {
if (connectionData.length < 5) {
// something is wrong
$log.error('Cannot reconnect, connection information is missing');
return;
} }
// reinitialise everything, clear all buffers
// TODO: this can be further extended in the future by looking
// at the last line in ever buffer and request more buffers from
// WeeChat based on that
models.reinitialize();
$rootScope.reconnecting = true;
// Have to do this to get the reconnect banner to show
$rootScope.$apply();
var bufferId = models.getActiveBuffer().id,
timeout = 3000; // start with a three-second timeout
reconnectTimer = setTimeout(function() {
attemptReconnect(bufferId, timeout);
}, timeout);
}; };
var disconnect = function() { var disconnect = function() {
$log.info('Disconnecting from relay');
$rootScope.userdisconnect = true;
ngWebsockets.send(weeChat.Protocol.formatQuit()); ngWebsockets.send(weeChat.Protocol.formatQuit());
// In case the backend doesn't repond we will close from our end
var closeTimer = setTimeout(function() {
ngWebsockets.disconnect();
// We pretend we are not connected anymore
// The connection can time out on its own
ngWebsockets.failCallbacks('disconnection');
$rootScope.connected = false;
$rootScope.$emit('relayDisconnect');
$rootScope.$apply();
});
}; };
/* /*
@ -181,7 +254,7 @@ weechat.factory('connection',
*/ */
var sendMessage = function(message) { var sendMessage = function(message) {
ngWebsockets.send(weeChat.Protocol.formatInput({ ngWebsockets.send(weeChat.Protocol.formatInput({
buffer: models.getActiveBuffer().fullName, buffer: models.getActiveBufferReference(),
data: message data: message
})); }));
}; };
@ -193,6 +266,20 @@ weechat.factory('connection',
})); }));
}; };
var sendHotlistClear = function() {
if (models.version[0] >= 1) {
// WeeChat >= 1 supports clearing hotlist with this command
sendMessage('/buffer set hotlist -1');
// Also move read marker
sendMessage('/input set_unread_current_buffer');
} else {
// If user wants to sync hotlist with weechat
// we will send a /buffer bufferName command every time
// the user switches a buffer. This will ensure that notifications
// are cleared in the buffer the user switches to
sendCoreCommand('/buffer ' + models.getActiveBuffer().fullName);
}
};
var requestNicklist = function(bufferId, callback) { var requestNicklist = function(bufferId, callback) {
bufferId = bufferId || null; bufferId = bufferId || null;
@ -269,8 +356,10 @@ weechat.factory('connection',
disconnect: disconnect, disconnect: disconnect,
sendMessage: sendMessage, sendMessage: sendMessage,
sendCoreCommand: sendCoreCommand, sendCoreCommand: sendCoreCommand,
sendHotlistClear: sendHotlistClear,
fetchMoreLines: fetchMoreLines, fetchMoreLines: fetchMoreLines,
requestNicklist: requestNicklist requestNicklist: requestNicklist,
attemptReconnect: attemptReconnect
}; };
}]); }]);
})(); })();

@ -24,7 +24,7 @@ weechat.filter('toArray', function () {
}; };
}); });
weechat.filter('irclinky', ['$filter', function($filter) { weechat.filter('irclinky', function() {
return function(text) { return function(text) {
if (!text) { if (!text) {
return text; return text;
@ -36,12 +36,12 @@ weechat.filter('irclinky', ['$filter', function($filter) {
// "#1" is much more likely to be "number 1" than "IRC channel #1". // "#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. // 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; 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. // Call the method we bound to window.openBuffer when we instantiated
// Therefore, get the scope, fire the method, and $apply. Yuck. I sincerely hope someone finds a better way of doing this. // the Weechat controller.
var substitute = '$1<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'$2\'); $scope.$apply();">$2</a>'; var substitute = '$1<a href="#" onclick="openBuffer(\'$2\');">$2</a>';
return text.replace(channelRegex, substitute); return text.replace(channelRegex, substitute);
}; };
}]); });
weechat.filter('inlinecolour', function() { weechat.filter('inlinecolour', function() {
return function(text) { return function(text) {
@ -75,9 +75,11 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
}); });
}; };
// hacky way to pass an extra argument without using .apply, which // hacky way to pass extra arguments without using .apply, which
// would require assembling an argument array. PERFORMANCE!!! // would require assembling an argument array. PERFORMANCE!!!
var extraArgument = (arguments.length > 2) ? arguments[2] : null; var extraArgument = (arguments.length > 2) ? arguments[2] : null;
var thirdArgument = (arguments.length > 3) ? arguments[3] : null;
var filterFunction = $filter(filter); var filterFunction = $filter(filter);
var el = document.createElement('div'); var el = document.createElement('div');
el.innerHTML = text; el.innerHTML = text;
@ -89,7 +91,7 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) {
// it changed the escaped value. This is because setting the result // it changed the escaped value. This is because setting the result
// as innerHTML causes it to be unescaped. // as innerHTML causes it to be unescaped.
var input = escape_html(node.nodeValue); var input = escape_html(node.nodeValue);
var value = filterFunction(input, extraArgument); var value = filterFunction(input, extraArgument, thirdArgument);
if (value !== input) { if (value !== input) {
// we changed something. create a new node to replace the current one // we changed something. create a new node to replace the current one
// we could also only add its children but that would probably incur // we could also only add its children but that would probably incur
@ -145,15 +147,42 @@ weechat.filter('getBufferQuickKeys', function () {
}; };
}); });
// Emojifis the string using https://github.com/twitter/twemoji // Emojifis the string using https://github.com/Ranks/emojione
weechat.filter('emojify', function() { weechat.filter('emojify', function() {
return function(text, enable_JS_Emoji) { return function(text, enable_JS_Emoji) {
if (enable_JS_Emoji === true) { if (enable_JS_Emoji === true && window.emojione !== undefined) {
return twemoji.parse(text); return emojione.unicodeToImage(text);
} else { } else {
return(text); return(text);
} }
}; };
}); });
weechat.filter('mathjax', function() {
return function(text, selector, enabled) {
if (!enabled || typeof(MathJax) === "undefined") {
return text;
}
if (text.indexOf("$$") != -1 || text.indexOf("\\[") != -1 || text.indexOf("\\(") != -1) {
// contains math
var math = document.querySelector(selector);
MathJax.Hub.Queue(["Typeset",MathJax.Hub,math]);
}
return text;
};
});
weechat.filter('prefixlimit', function() {
return function(input, chars) {
if (isNaN(chars)) return input;
if (chars <= 0) return '';
if (input && input.length > chars) {
input = input.substring(0, chars);
return input + '+';
}
return input;
};
});
})(); })();

@ -15,6 +15,11 @@ weechat.config(['$compileProvider', function ($compileProvider) {
weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', 'settings', weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', 'settings',
function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils, settings) { function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils, settings) {
window.openBuffer = function(channel) {
$scope.openBuffer(channel);
$scope.$apply();
};
$scope.command = ''; $scope.command = '';
$scope.themes = ['dark', 'light']; $scope.themes = ['dark', 'light'];
@ -26,7 +31,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
'savepassword': false, 'savepassword': false,
'autoconnect': false, 'autoconnect': false,
'nonicklist': utils.isMobileUi(), 'nonicklist': utils.isMobileUi(),
'noembed': utils.isMobileUi(), 'noembed': true,
'onlyUnread': false, 'onlyUnread': false,
'hotlistsync': true, 'hotlistsync': true,
'orderbyserver': true, 'orderbyserver': true,
@ -36,7 +41,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
'fontsize': '14px', 'fontsize': '14px',
'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'), 'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'),
'readlineBindings': false, 'readlineBindings': false,
'enableJSEmoji': false 'enableJSEmoji': (utils.isMobileUi() ? false : true),
'enableMathjax': false,
}); });
$scope.settings = settings; $scope.settings = settings;
@ -208,17 +214,12 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
); );
} }
notifications.updateTitle(ab); notifications.updateTitle(ab);
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
$timeout(function() { $timeout(function() {
$rootScope.scrollWithBuffer(true); $rootScope.scrollWithBuffer(true);
}); });
// If user wants to sync hotlist with weechat
// we will send a /buffer bufferName command every time
// the user switches a buffer. This will ensure that notifications
// are cleared in the buffer the user switches to
if (settings.hotlistsync && ab.fullName) {
connection.sendCoreCommand('/buffer ' + ab.fullName);
}
// Clear search term on buffer change // Clear search term on buffer change
$scope.search = ''; $scope.search = '';
@ -231,12 +232,21 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
document.getElementById('sendMessage').focus(); document.getElementById('sendMessage').focus();
}, 0); }, 0);
} }
// Do this part last since it's not important for the UI
if (settings.hotlistsync && ab.fullName) {
connection.sendHotlistClear();
}
}); });
$rootScope.favico = new Favico({animation: 'none'}); $rootScope.favico = new Favico({animation: 'none'});
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
$rootScope.$on('notificationChanged', function() { $rootScope.$on('notificationChanged', function() {
notifications.updateTitle(); notifications.updateTitle();
$scope.notifications = notifications.unreadCount('notification');
$scope.unread = notifications.unreadCount('unread');
if (settings.useFavico && $rootScope.favico) { if (settings.useFavico && $rootScope.favico) {
notifications.updateFavico(); notifications.updateFavico();
@ -264,6 +274,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.connected = false; $rootScope.connected = false;
$rootScope.waseverconnected = false; $rootScope.waseverconnected = false;
$rootScope.userdisconnect = false;
$rootScope.reconnecting = false;
$rootScope.models = models; $rootScope.models = models;
@ -375,6 +387,51 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
} }
}); });
// To prevent unnecessary loading times for users who don't
// want MathJax, load it only if the setting is enabled.
// This also fires when the page is loaded if enabled.
settings.addCallback('enableMathjax', function(enabled) {
if (enabled && !$rootScope.mathjax_init) {
// Load MathJax only once
$rootScope.mathjax_init = true;
(function () {
var head = document.getElementsByTagName("head")[0], script;
script = document.createElement("script");
script.type = "text/x-mathjax-config";
script[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({\n" +
" tex2jax: { inlineMath: [['$$','$$'], ['\\\\(','\\\\)']], displayMath: [['\\\\[','\\\\]']] },\n" +
"});";
head.appendChild(script);
script = document.createElement("script");
script.type = "text/javascript";
script.src = "//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML";
head.appendChild(script);
})();
}
});
// Inject theme CSS
settings.addCallback('theme', function(theme) {
// Unload old theme
var oldThemeCSS = document.getElementById("themeCSS");
if (oldThemeCSS) {
oldThemeCSS.parentNode.removeChild(oldThemeCSS);
}
// Load new theme
(function() {
var elem = document.createElement("link");
elem.rel = "stylesheet";
elem.href = "css/themes/" + theme + ".css";
elem.media = "screen";
elem.id = "themeCSS";
document.getElementsByTagName("head")[0].appendChild(elem);
})();
});
// Update font family when changed // Update font family when changed
settings.addCallback('fontfamily', function(fontfamily) { settings.addCallback('fontfamily', function(fontfamily) {
utils.changeClassStyle('favorite-font', 'fontFamily', fontfamily); utils.changeClassStyle('favorite-font', 'fontFamily', fontfamily);
@ -390,6 +447,15 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
if (utils.isMobileUi()) { if (utils.isMobileUi()) {
$scope.hideSidebar(); $scope.hideSidebar();
} }
// Clear the hotlist for this buffer, because presumable you have read
// the messages in this buffer before you switched to the new one
// this is only needed with new type of clearing since in the old
// way WeeChat itself takes care of that part
if (models.version[0] >= 1) {
connection.sendHotlistClear();
}
return models.setActiveBuffer(bufferId, key); return models.setActiveBuffer(bufferId, key);
}; };
@ -398,9 +464,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
fullName = fullName.substring(0, fullName.lastIndexOf('.') + 1) + bufferName; // substitute the last part fullName = fullName.substring(0, fullName.lastIndexOf('.') + 1) + bufferName; // substitute the last part
if (!$scope.setActiveBuffer(fullName, 'fullName')) { if (!$scope.setActiveBuffer(fullName, 'fullName')) {
var command = 'join'; // WeeChat 0.4.0+ supports /join -noswitch
// As Glowing Bear requires 0.4.2+, we don't need to check the version
var command = 'join -noswitch';
// Check if it's a query and we need to use /query instead
if (['#', '&', '+', '!'].indexOf(bufferName.charAt(0)) < 0) { // these are the characters a channel name can start with (RFC 2813-2813) if (['#', '&', '+', '!'].indexOf(bufferName.charAt(0)) < 0) { // these are the characters a channel name can start with (RFC 2813-2813)
command = 'query'; command = 'query';
// WeeChat 1.2+ supports /query -noswitch. See also #577 (different context)
if ((models.version[0] == 1 && models.version[1] >= 2) || models.version[1] > 1) {
command += " -noswitch";
}
} }
connection.sendMessage('/' + command + ' ' + bufferName); connection.sendMessage('/' + command + ' ' + bufferName);
} }
@ -513,6 +587,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$scope.connectbutton = 'Connect'; $scope.connectbutton = 'Connect';
connection.disconnect(); connection.disconnect();
}; };
$scope.reconnect = function() {
var bufferId = models.getActiveBuffer().id;
connection.attemptReconnect(bufferId, 3000);
};
//XXX this is a bit out of place here, either move up to the rest of the firefox install code or remove //XXX this is a bit out of place here, either move up to the rest of the firefox install code or remove
$scope.install = function() { $scope.install = function() {
@ -581,12 +659,13 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
return true; return true;
} }
// Always show core buffer in the list (issue #438) // Always show core buffer in the list (issue #438)
if (buffer.fullName === "core.weechat") { // Also show server buffers in hierarchical view
if (buffer.fullName === "core.weechat" || (settings.orderbyserver && buffer.type === 'server')) {
return true; return true;
} }
return buffer.unread > 0 || buffer.notification > 0; return (buffer.unread > 0 || buffer.notification > 0) && !buffer.hidden;
} }
return true; return !buffer.hidden;
}; };
// Watch model and update show setting when it changes // Watch model and update show setting when it changes
@ -681,6 +760,25 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
} }
}; };
$scope.init = function() {
if (window.location.hash) {
var rawStr = atob(window.location.hash.substring(1));
window.location.hash = "";
var spl = rawStr.split(":");
var host = spl[0];
var port = parseInt(spl[1]);
var password = spl[2];
var ssl = spl.length > 3;
notifications.requestNotificationPermission();
$rootScope.sslError = false;
$rootScope.securityError = false;
$rootScope.errorMessage = false;
$rootScope.bufferBottom = true;
$scope.connectbutton = 'Connecting ...';
connection.connect(host, port, password, ssl);
}
};
}]); }]);
weechat.config(['$routeProvider', weechat.config(['$routeProvider',

@ -5,6 +5,14 @@ var weechat = angular.module('weechat');
weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) { weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) {
var handleVersionInfo = function(message) {
var content = message.objects[0].content;
var version = content.value;
// Store the WeeChat version in models
// this eats things like 1.3-dev -> [1,3]
models.version = version.split(".").map(function(c) { return parseInt(c); });
};
var handleBufferClosing = function(message) { var handleBufferClosing = function(message) {
var bufferMessage = message.objects[0].content[0]; var bufferMessage = message.objects[0].content[0];
var bufferId = bufferMessage.pointers[0]; var bufferId = bufferMessage.pointers[0];
@ -43,6 +51,54 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
} }
}; };
var handleBufferInfo = function(message) {
var bufferInfos = message.objects[0].content;
// buffers objects
for (var i = 0; i < bufferInfos.length ; i++) {
var bufferId = bufferInfos[i].pointers[0];
var buffer = models.getBuffer(bufferId);
if (buffer !== undefined) {
// We already know this buffer
handleBufferUpdate(buffer, bufferInfos[i]);
} else {
buffer = new models.Buffer(bufferInfos[i]);
models.addBuffer(buffer);
// Switch to first buffer on startup
if (i === 0) {
models.setActiveBuffer(buffer.id);
}
}
}
};
var handleBufferUpdate = function(buffer, message) {
if (message.pointers[0] !== buffer.id) {
// this is information about some other buffer!
return;
}
// weechat properties -- short name can be changed
buffer.shortName = message.short_name;
buffer.trimmedName = buffer.shortName.replace(/^[#&+]/, '');
buffer.title = message.title;
buffer.number = message.number;
buffer.hidden = message.hidden;
// reset these, hotlist info will arrive shortly
buffer.notification = 0;
buffer.unread = 0;
buffer.lastSeen = -1;
if (message.local_variables.type !== undefined) {
buffer.type = message.local_variables.type;
buffer.indent = (['channel', 'private'].indexOf(buffer.type) >= 0);
}
if (message.notify !== undefined) {
buffer.notify = message.notify;
}
};
var handleBufferLineAdded = function(message) { var handleBufferLineAdded = function(message) {
message.objects[0].content.forEach(function(l) { message.objects[0].content.forEach(function(l) {
handleLine(l, false); handleLine(l, false);
@ -53,10 +109,6 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
var bufferMessage = message.objects[0].content[0]; var bufferMessage = message.objects[0].content[0];
var buffer = new models.Buffer(bufferMessage); var buffer = new models.Buffer(bufferMessage);
models.addBuffer(buffer); 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 handleBufferTitleChanged = function(message) {
@ -84,6 +136,29 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
// prefix + fullname, which would happen otherwise). Else, use null so that full_name is used // prefix + fullname, which would happen otherwise). Else, use null so that full_name is used
old.trimmedName = obj.short_name.replace(/^[#&+]/, '') || (obj.short_name ? ' ' : null); old.trimmedName = obj.short_name.replace(/^[#&+]/, '') || (obj.short_name ? ' ' : null);
old.prefix = ['#', '&', '+'].indexOf(obj.short_name.charAt(0)) >= 0 ? obj.short_name.charAt(0) : ''; old.prefix = ['#', '&', '+'].indexOf(obj.short_name.charAt(0)) >= 0 ? obj.short_name.charAt(0) : '';
// After a buffer openes we get the name change event from relay protocol
// Here we check our outgoing commands that openes a buffer and switch
// to it if we find the buffer name it the list
var position = models.outgoingQueries.indexOf(old.shortName);
if (position >= 0) {
models.outgoingQueries.splice(position, 1);
models.setActiveBuffer(old.id);
}
};
var handleBufferHidden = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
old.hidden = true;
};
var handleBufferUnhidden = function(message) {
var obj = message.objects[0].content[0];
var buffer = obj.pointers[0];
var old = models.getBuffer(buffer);
old.hidden = false;
}; };
var handleBufferLocalvarChanged = function(message) { var handleBufferLocalvarChanged = function(message) {
@ -190,9 +265,12 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
_buffer_line_added: handleBufferLineAdded, _buffer_line_added: handleBufferLineAdded,
_buffer_localvar_added: handleBufferLocalvarChanged, _buffer_localvar_added: handleBufferLocalvarChanged,
_buffer_localvar_removed: handleBufferLocalvarChanged, _buffer_localvar_removed: handleBufferLocalvarChanged,
_buffer_localvar_changed: handleBufferLocalvarChanged,
_buffer_opened: handleBufferOpened, _buffer_opened: handleBufferOpened,
_buffer_title_changed: handleBufferTitleChanged, _buffer_title_changed: handleBufferTitleChanged,
_buffer_renamed: handleBufferRenamed, _buffer_renamed: handleBufferRenamed,
_buffer_hidden: handleBufferHidden,
_buffer_unhidden: handleBufferUnhidden,
_nicklist: handleNicklist, _nicklist: handleNicklist,
_nicklist_diff: handleNicklistDiff _nicklist_diff: handleNicklistDiff
}; };
@ -212,10 +290,12 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
}; };
return { return {
handleVersionInfo: handleVersionInfo,
handleEvent: handleEvent, handleEvent: handleEvent,
handleLineInfo: handleLineInfo, handleLineInfo: handleLineInfo,
handleHotlistInfo: handleHotlistInfo, handleHotlistInfo: handleHotlistInfo,
handleNicklist: handleNicklist handleNicklist: handleNicklist,
handleBufferInfo: handleBufferInfo
}; };
}]); }]);

@ -23,6 +23,11 @@ weechat.directive('inputBar', function() {
IrcUtils, IrcUtils,
settings) { settings) {
// E.g. Turn :smile: into the unicode equivalent
$scope.inputChanged = function() {
$scope.command = emojione.shortnameToUnicode($scope.command);
};
/* /*
* Returns the input element * Returns the input element
*/ */
@ -95,10 +100,28 @@ weechat.directive('inputBar', function() {
ab.clear(); ab.clear();
} }
// Check against a list of commands that opens a new
// buffer and save the name of the buffer so we can
// also automatically switch to the new buffer in gb
var opencommands = ['/query', '/join', '/j', '/q'];
var spacepos = $scope.command.indexOf(' ');
var firstword = $scope.command.substr(0, spacepos);
var index = opencommands.indexOf(firstword);
if (index >= 0) {
var queryName = $scope.command.substring(spacepos + 1);
// Cache our queries so when a buffer gets opened we can open in UI
models.outgoingQueries.push(queryName);
}
// Empty the input after it's sent // Empty the input after it's sent
$scope.command = ''; $scope.command = '';
} }
// New style clearing requires this, old does not
if (models.version[0] >= 1) {
connection.sendHotlistClear();
}
$scope.getInputNode().focus(); $scope.getInputNode().focus();
}; };
@ -156,6 +179,15 @@ weechat.directive('inputBar', function() {
// Support different browser quirks // Support different browser quirks
var code = $event.keyCode ? $event.keyCode : $event.charCode; var code = $event.keyCode ? $event.keyCode : $event.charCode;
// Safari doesn't implement DOM 3 input events yet as of 8.0.6
var altg = $event.getModifierState ? $event.getModifierState('AltGraph') : false;
// Mac OSX behaves differntly for altgr, so we check for that
if (altg) {
// We don't handle any anything with altgr
return false;
}
// reset quick keys display // reset quick keys display
$rootScope.showQuickKeys = false; $rootScope.showQuickKeys = false;

@ -8,6 +8,15 @@
var IrcUtils = angular.module('IrcUtils', []); var IrcUtils = angular.module('IrcUtils', []);
IrcUtils.service('IrcUtils', [function() { IrcUtils.service('IrcUtils', [function() {
/**
* Escape a string for usage in a larger regexp
* @param str String to escape
* @return Escaped string
*/
var escapeRegExp = function(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};
/** /**
* Get a new version of a nick list, sorted by last speaker * Get a new version of a nick list, sorted by last speaker
* *
@ -63,7 +72,7 @@ IrcUtils.service('IrcUtils', [function() {
// collect matching nicks // collect matching nicks
for (var i = 0; i < nickList.length; ++i) { for (var i = 0; i < nickList.length; ++i) {
var lcNick = nickList[i].toLowerCase(); var lcNick = nickList[i].toLowerCase();
if (lcNick.search(lcIterCandidate) === 0) { if (lcNick.search(escapeRegExp(lcIterCandidate)) === 0) {
matchingNicks.push(nickList[i]); matchingNicks.push(nickList[i]);
if (lcCurrentNick === lcNick) { if (lcCurrentNick === lcNick) {
at = matchingNicks.length - 1; at = matchingNicks.length - 1;
@ -149,7 +158,7 @@ IrcUtils.service('IrcUtils', [function() {
m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/); m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) { if (m) {
// try completing // try completing
newNick = _completeSingleNick(m[1], searchNickList); newNick = _completeSingleNick(escapeRegExp(m[1]), searchNickList);
if (newNick === null) { if (newNick === null) {
// no match // no match
return ret; return ret;

@ -8,6 +8,12 @@
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) {
// WeeChat version
this.version = null;
// Save outgoing queries
this.outgoingQueries = [];
var parseRichText = function(text) { var parseRichText = function(text) {
var textElements = weeChat.Protocol.rawText2Rich(text), var textElements = weeChat.Protocol.rawText2Rich(text),
typeToClassPrefixFg = { typeToClassPrefixFg = {
@ -57,6 +63,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 hidden = message.hidden;
// If it's a channel, trim away the prefix (#, &, or +). If that is empty and the buffer // If it's a channel, trim away the prefix (#, &, or +). If that is empty and the buffer
// has a short name, use a space (because the prefix will be displayed separately, and we don't want // has a short name, use a space (because the prefix will be displayed separately, and we don't want
// prefix + fullname, which would happen otherwise). Else, use null so that full_name is used // prefix + fullname, which would happen otherwise). Else, use null so that full_name is used
@ -134,6 +141,12 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
*/ */
var updateNick = function(group, nick) { var updateNick = function(group, nick) {
group = nicklist[group]; group = nicklist[group];
if (group === undefined) {
// We are getting nicklist events for a buffer where not yet
// have populated the nicklist, so there will be nothing to
// update. Just ignore the event.
return;
}
for(var i in group.nicks) { for(var i in group.nicks) {
if (group.nicks[i].name === nick.name) { if (group.nicks[i].name === nick.name) {
group.nicks[i] = nick; group.nicks[i] = nick;
@ -277,6 +290,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
id: pointer, id: pointer,
fullName: fullName, fullName: fullName,
shortName: shortName, shortName: shortName,
hidden: hidden,
trimmedName: trimmedName, trimmedName: trimmedName,
prefix: prefix, prefix: prefix,
number: number, number: number,
@ -442,6 +456,22 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter)
return activeBuffer; return activeBuffer;
}; };
/*
* Returns a reference to the currently active buffer that
* WeeChat understands without crashing, even if it's invalid
*
* @return active buffer pointer (WeeChat 1.0+) or fullname (older versions)
*/
this.getActiveBufferReference = function() {
if (this.version !== null && this.version[0] >= 1) {
// pointers are being validated, they're more reliable than
// fullName (e.g. if fullName contains spaces)
return "0x"+activeBuffer.id;
} else {
return activeBuffer.fullName;
}
};
/* /*
* Returns the previous current active buffer * Returns the previous current active buffer
* *

@ -157,5 +157,6 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu
updateFavico: updateFavico, updateFavico: updateFavico,
createHighlight: createHighlight, createHighlight: createHighlight,
cancelAll: cancelAll, cancelAll: cancelAll,
unreadCount: unreadCount
}; };
}]); }]);

@ -6,6 +6,13 @@ var weechat = angular.module('weechat');
weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope) { weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope) {
var that = this; var that = this;
this.callbacks = {}; this.callbacks = {};
// This cache is important for two reasons. One, angular hits it up really often
// (because it needs to check for changes and it's not very clever about it).
// Two, it prevents weird type conversion issues that otherwise arise in
// $store.parseValue (e.g. converting "123." to the number 123 even though it
// actually was the beginning of an IP address that the user was in the
// process of entering)
this.cache = {};
// Define a property for a setting, retrieving it on read // Define a property for a setting, retrieving it on read
// and writing it to localStorage on write // and writing it to localStorage on write
@ -14,9 +21,13 @@ weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope
enumerable: true, enumerable: true,
key: key, key: key,
get: function() { get: function() {
return $store.get(key); if (!(key in this.cache)) {
this.cache[key] = $store.get(key);
}
return this.cache[key];
}, },
set: function(newVal) { set: function(newVal) {
this.cache[key] = newVal;
$store.set(key, newVal); $store.set(key, newVal);
// Call any callbacks // Call any callbacks
var callbacks = that.callbacks[key]; var callbacks = that.callbacks[key];

@ -109,8 +109,9 @@ function($rootScope, $q) {
// otherwise emit it // otherwise emit it
$rootScope.$emit('onMessage', message); $rootScope.$emit('onMessage', message);
} }
// Make sure all UI is updated with new data
$rootScope.$apply(); $rootScope.$apply();
}; };
var connect = function(url, var connect = function(url,

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

@ -25,5 +25,5 @@
"desktop-notification":{} "desktop-notification":{}
}, },
"default_locale": "en", "default_locale": "en",
"version": "0.4.8" "version": "0.5.0"
} }

@ -1,7 +1,7 @@
{ {
"name": "glowing-bear", "name": "glowing-bear",
"private": true, "private": true,
"version": "0.4.8", "version": "0.5.0",
"description": "A web client for Weechat", "description": "A web client for Weechat",
"repository": "https://github.com/glowing-bear/glowing-bear", "repository": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3", "license": "GPLv3",

@ -16,11 +16,11 @@ describe('Filters', function() {
})); }));
it('should linkify IRC channels', inject(function(irclinkyFilter) { it('should linkify IRC channels', inject(function(irclinkyFilter) {
expect(irclinkyFilter('#foo')).toEqual('<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'#foo\'); $scope.$apply();">#foo</a>'); expect(irclinkyFilter('#foo')).toEqual('<a href="#" onclick="openBuffer(\'#foo\');">#foo</a>');
})); }));
it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) { it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) {
expect(irclinkyFilter('<"#foo">')).toEqual('<"<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'#foo">\'); $scope.$apply();">#foo"></a>'); expect(irclinkyFilter('<"#foo">')).toEqual('<"<a href="#" onclick="openBuffer(\'#foo">\');">#foo"></a>');
})); }));
}); });

Loading…
Cancel
Save