Merge branch 'master' into gh-pages

gh-pages
Lorenz Hübschle-Schneider 5 years ago
commit 0125e0fae7
  1. 4
      README.md
  2. 2
      bower.json
  3. 34
      css/glowingbear.css
  4. 2
      css/themes/base16-default.css
  5. 2
      css/themes/blue.css
  6. 2
      css/themes/dark.css
  7. 2
      css/themes/light.css
  8. 1
      electron.makefile
  9. 25
      index.html
  10. 22
      js/connection.js
  11. 18
      js/filters.js
  12. 13
      js/glowingbear.js
  13. 9
      js/handlers.js
  14. 98
      js/imgur.js
  15. 204
      js/inputbar.js
  16. 8
      js/models.js
  17. 36
      js/plugins.js
  18. 27
      js/weechat.js
  19. 2
      manifest.json
  20. 2
      manifest.webapp
  21. 2
      package.json
  22. 10
      test/unit/plugins.js

@ -10,7 +10,7 @@ Glowing Bear connects to the WeeChat instance you're already running (version 0.
/relay add weechat 9001 /relay add weechat 9001
/set relay.network.password YOURPASSWORD /set relay.network.password YOURPASSWORD
Now point your browser to the [Glowing Bear](http://www.glowing-bear.org)! If you're having trouble connecting, check that the host and port of your WeeChat host are entered correctly, and that your server's firewall permits incoming connections on the relay port (9001 in this example). Now point your browser to the [Glowing Bear](https://www.glowing-bear.org)! If you're having trouble connecting, check that the host and port of your WeeChat host are entered correctly, and that your server's firewall permits incoming connections on the relay port (9001 in this example).
**Please note that the above instructions set up an unencrypted relay, and all your data will be transmitted in clear.** You should not use this over the internet. We strongly recommend that you set up encryption if you want to keep using Glowing Bear. There's a guide on setting it up with Let's Encrypt on the landing page of the [next version of Glowing Bear](https://latest.glowing-bear.org), under "Getting Started". Ask us in `#glowing-bear` on freenode if something is unclear. **Please note that the above instructions set up an unencrypted relay, and all your data will be transmitted in clear.** You should not use this over the internet. We strongly recommend that you set up encryption if you want to keep using Glowing Bear. There's a guide on setting it up with Let's Encrypt on the landing page of the [next version of Glowing Bear](https://latest.glowing-bear.org), under "Getting Started". Ask us in `#glowing-bear` on freenode if something is unclear.
@ -61,7 +61,7 @@ Now you can point your browser to [http://localhost:8000](http://localhost:8000)
Remember that **you don't need to host Glowing Bear yourself to use it**, you can just use [our hosted version](https://www.glowing-bear.org) powered by GitHub pages, and we'll take care of updates for you. Your browser connects to WeeChat directly, so it does not matter where Glowing Bear is hosted. Remember that **you don't need to host Glowing Bear yourself to use it**, you can just use [our hosted version](https://www.glowing-bear.org) powered by GitHub pages, and we'll take care of updates for you. Your browser connects to WeeChat directly, so it does not matter where Glowing Bear is hosted.
You can also use the latest and greatest development version of Glowing Bear at [https://latest.glowing-bear.org/](https://latest.glowing-bear.org/). Branches of this repository are available as [https://latest.glowing-bear.org/**branchname**/](https://latest.glowing-bear.org/branchname/), and pull requests as [https://latest.glowing-bear.org/pull/**123**/](https://latest.glowing-bear.org/pull/123/)—note the trailing slashes. You can also use the latest and greatest development version of Glowing Bear at [https://latest.glowing-bear.org/](https://latest.glowing-bear.org/). For developers, branches of this repository are available at [https://pull.glowing-bear.org/**branchname**/](https://pull.glowing-bear.org/branchname/), and pull requests at [https://pull.glowing-bear.org/**123**/](https://pull.glowing-bear.org/123/)—note the trailing slashes.
### Running the tests ### Running the tests
Glowing Bear uses Karma and Jasmine to run its unit tests. To run the tests locally, you will first need to install `npm` on your machine. Check out the wonderful [nvm](https://github.com/creationix/nvm) if you don't know it already, it's highly recommended. Glowing Bear uses Karma and Jasmine to run its unit tests. To run the tests locally, you will first need to install `npm` on your machine. Check out the wonderful [nvm](https://github.com/creationix/nvm) if you don't know it already, it's highly recommended.

@ -1,7 +1,7 @@
{ {
"name": "glowing-bear", "name": "glowing-bear",
"description": "A webclient for WeeChat", "description": "A webclient for WeeChat",
"version": "0.8.0", "version": "0.9.0",
"homepage": "https://github.com/glowing-bear/glowing-bear", "homepage": "https://github.com/glowing-bear/glowing-bear",
"license": "GPLv3", "license": "GPLv3",
"private": true, "private": true,

@ -693,22 +693,7 @@ li.buffer.indent.private a {
user-select: none; user-select: none;
} }
.emojione { .toast {
font-size: inherit;
height: 1em;
width: 1.1em;
min-height: 16px;
min-width: 16px;
display: inline-block;
margin: -.2ex .15em .2ex;
line-height: normal;
vertical-align: middle;
}
img.emojione {
width: auto;
}
#toast {
position: fixed; position: fixed;
left: 50%; left: 50%;
bottom: 50px; bottom: 50px;
@ -719,7 +704,14 @@ img.emojione {
border-radius: 3px; border-radius: 3px;
padding: 10px 15px; padding: 10px 15px;
z-index: 100; z-index: 100;
animation: fadein 0.5s, fadeout 0.5s 4.5s; }
.toast-short {
animation: fadein 0.5s, fadeout 0.5s 4.5s;
}
.toast-long {
animation: fadein 0.5s, fadeout 0.5s 14.5s;
} }
@keyframes fadein { @keyframes fadein {
@ -960,3 +952,11 @@ code {
color: #444; color: #444;
border: 1pt solid #444; border: 1pt solid #444;
} }
#bufferlines.hideTime td.time {
display:none;
}
#bufferlines.hideTime td.prefix {
display:none;
}

@ -423,7 +423,7 @@ button.close:hover {
color: var(--base01); color: var(--base01);
} }
#toast { .toast {
background-color: var(--base01); background-color: var(--base01);
} }

@ -134,7 +134,7 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-se
border: 1px solid #363943; border: 1px solid #363943;
} }
#toast { .toast {
background-color: #283244; background-color: #283244;
border: 1px solid; border: 1px solid;
border-color: rgb(29, 94, 152); border-color: rgb(29, 94, 152);

@ -2126,7 +2126,7 @@ code {
color: #fff; color: #fff;
} }
#toast { .toast {
background-color: #333; background-color: #333;
} }

@ -2092,7 +2092,7 @@ input[type=text].is-invalid{
font-weight: bold; font-weight: bold;
} }
#toast { .toast {
background-color: #ddd; background-color: #ddd;
} }

@ -9,7 +9,6 @@ bower:
copylocal: copylocal:
find bower_components \( -name "*min.js" -o -name "*min.css" \) -exec cp {} 3rdparty \; find bower_components \( -name "*min.js" -o -name "*min.css" \) -exec cp {} 3rdparty \;
cp -r bower_components/bootstrap/fonts . cp -r bower_components/bootstrap/fonts .
cp bower_components/emojione/assets/sprites/emojione.sprites.svg 3rdparty
# modify index.html to use local files # modify index.html to use local files
uselocal: copylocal uselocal: copylocal

@ -19,10 +19,10 @@
<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" /> <link href="css/themes/dark.css" rel="stylesheet" media="screen" id="themeCSS" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js" integrity="sha256-QRJz3b0/ZZC4ilKmBRRjY0MgnVhQ+RR1tpWLYaRRjSo=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.9/angular.min.js" integrity="sha256-b5NvmvUcyr0wpBOLnNbaWH5zKQAivhj8yMYhfXEumQA=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular-route.min.js" integrity="sha256-PQfkC+TI/HZv0O9JbmrLmPyhgOT2hry24vA5yAV59zY=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-route/1.7.9/angular-route.min.js" integrity="sha256-WTkeb5AZHX/sDacGSGiF3NX38HvQhfv0U1uilADksXc=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular-sanitize.min.js" integrity="sha256-LLlLr1XzKUXSFI9SiuEJOAn88Dwge+/zld523N2c8+8=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-sanitize/1.7.9/angular-sanitize.min.js" integrity="sha256-nne9nFlD03jNmaV9DT9Ns51XCopbolhNWl8C2s379tU=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular-touch.min.js" integrity="sha256-4IS2pHNTST2Jl6dSzNsERpYleiQi1r4L2MLPElG8LZ4=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-touch/1.7.9/angular-touch.min.js" integrity="sha256-tqkwQDD7/6S5kru/7UjpaiFx9xbdotmPhVJSNhwsdUM=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js" integrity="sha256-G7A4JrJjJlFqP0yamznwPjAApIKPkadeHfyIwiaa9e0=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js" integrity="sha256-G7A4JrJjJlFqP0yamznwPjAApIKPkadeHfyIwiaa9e0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.7/lib/js/emojione.min.js" integrity="sha256-9cBkVeU53NiJ9/BdcJta3HbERAmf5X9DE2WvL8V+gDs=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.7/lib/js/emojione.min.js" integrity="sha256-9cBkVeU53NiJ9/BdcJta3HbERAmf5X9DE2WvL8V+gDs=" crossorigin="anonymous"></script>
<script type="text/javascript" src="3rdparty/inflate.min.js"></script> <script type="text/javascript" src="3rdparty/inflate.min.js"></script>
@ -30,6 +30,7 @@
<script type="text/javascript" src="3rdparty/favico-0.3.10.min.js"></script> <script type="text/javascript" src="3rdparty/favico-0.3.10.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}" ng-init="init()" 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">
<audio id="audioNotificationInitializer"><source src="data:audio/mp3;base64,/+MYxAAJs2H8AABLSZv4Af/5yAsCIElB/v/+Y///U+QiEaSchGO+IMQjZCgOLw4KVoIEkf/r69Kbfc7/WbLRPsyvp7/p/p///+MYxBQK+1oUAACNMUWSyLcrqpERhDqKCBWcgbTmuIq8ISkQSv+yf/tbZf9krRTptRUv/XT////66NJQFZTpOd3KUEtM+a+l/+MYxCMKM2YcAACNMB+Jqe+HLv+75fSreYSWGtqHJghl6y///89fcz/p8qIRKeFWYKEGo5mLFmCGBjV0FEJn/9f85V87iy98/+MYxDULA1YYAABHLbI52f2/4v////n7U/swVr0SzhrRIueMLLog0qIKcOwu/5v/lLR/r9DPN/R+vmf//////V8tAxlMY1sy/+MYxEQKM2IYAABHLZjRjUlgUSUAhTUBFHVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxFYJ22oAAACHMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"></audio>
<div class="alert alert-danger upload-error" ng-show="uploadError" ng-cloak> <div class="alert alert-danger upload-error" ng-show="uploadError" ng-cloak>
<p><strong>Upload error:</strong> Image upload failed.</p> <p><strong>Upload error:</strong> Image upload failed.</p>
</div> </div>
@ -376,7 +377,7 @@ npm run build-electron-{windows, darwin, linux}</pre>
</li> </li>
</ul> </ul>
</div> </div>
<div id="bufferlines" class="favorite-font" ng-swipe-right="swipeRight()" ng-swipe-left="swipeLeft()" ng-swipe-disable-mouse ng-class="{'withnicklist': showNicklist}" when-scrolled="infiniteScroll()" imgur-drop> <div id="bufferlines" class="favorite-font" ng-swipe-right="swipeRight()" ng-swipe-left="swipeLeft()" ng-swipe-disable-mouse ng-class="{'withnicklist': showNicklist, 'hideTime': activeBuffer().hideBufferLineTimes}" when-scrolled="infiniteScroll()" imgur-drop>
<table> <table>
<tbody> <tbody>
<tr class="bufferline"> <tr class="bufferline">
@ -396,7 +397,7 @@ npm run build-electron-{windows, darwin, linux}</pre>
<td class="prefix"><span ng-class="::{'repeated-prefix': bufferline.prefixtext==bufferlines[$index-1].prefixtext}"><a ng-click="addMention(bufferline)"><span class="hidden-bracket" ng-if="::(bufferline.showHiddenBrackets)">&lt;</span><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text|prefixlimit:25"></span><span class="hidden-bracket" ng-if="::(bufferline.showHiddenBrackets)">&gt;</span></a></span></td><!-- <td class="prefix"><span ng-class="::{'repeated-prefix': bufferline.prefixtext==bufferlines[$index-1].prefixtext}"><a ng-click="addMention(bufferline)"><span class="hidden-bracket" ng-if="::(bufferline.showHiddenBrackets)">&lt;</span><span ng-repeat="part in ::bufferline.prefix" ng-class="::part.classes" ng-bind="::part.text|prefixlimit:25"></span><span class="hidden-bracket" ng-if="::(bufferline.showHiddenBrackets)">&gt;</span></a></span></td><!--
--><td class="message"><!-- --><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.concat(['line-' + part.$$hashKey.replace(':','_')])" ng-bind-html="::part.text | conditionalLinkify:part.classes.includes('cof-chat_host') | DOMfilter:'codify' | DOMfilter:'irclinky' | DOMfilter:'emojify':settings.enableJSEmoji | DOMfilter:'inlinecolour' | DOMfilter:'latexmath':('.line-' + part.$$hashKey.replace(':','_')):settings.enableMathjax"></span> --><span ng-repeat="part in ::bufferline.content" class="text" ng-class="::part.classes.concat(['line-' + part.$$hashKey.replace(':','_')])" ng-bind-html="::part.text | conditionalLinkify:part.classes.includes('cof-chat_host') | DOMfilter:'codify' | DOMfilter:'irclinky' | DOMfilter:'inlinecolour' | DOMfilter:'latexmath':('.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">
@ -423,7 +424,7 @@ npm run build-electron-{windows, darwin, linux}</pre>
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button> <button type="button" class="close" ng-click="closeModal($event)" aria-hidden="true">&times;</button>
<span class="pull-right version">Glowing Bear version 0.8.0</span> <span class="pull-right version">Glowing Bear version 0.9.0</span>
<h4 class="modal-title">Settings</h4> <h4 class="modal-title">Settings</h4>
<p>Settings will be stored in your browser.</p> <p>Settings will be stored in your browser.</p>
</div> </div>
@ -571,16 +572,6 @@ npm run build-electron-{windows, darwin, linux}</pre>
</div> </div>
</form> </form>
</li> </li>
<li>
<form class="form-inline" role="form">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="settings.enableJSEmoji">
Enable non-native Emoji support <span class="text-muted settings-help">Displays Emoji characters as images. Emoji provided free by <a href="http://emojione.com">http://emojione.com</a></span>
</label>
</div>
</form>
</li>
<li> <li>
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">

@ -528,6 +528,25 @@ weechat.factory('connection',
}); });
}; };
var requestCompletion = function(bufferId, position, data) {
// Prevent requesting completion if bufferId is invalid
if (!bufferId) {
return;
}
return ngWebsockets.send(
weeChat.Protocol.formatCompletion({
buffer: "0x" + bufferId,
position: position,
data: data
})
).then(function(message) {
return new Promise(function (resolve) {
resolve( handlers.handleCompletion(message) );
});
});
};
return { return {
connect: connect, connect: connect,
@ -538,7 +557,8 @@ weechat.factory('connection',
sendHotlistClearAll: sendHotlistClearAll, sendHotlistClearAll: sendHotlistClearAll,
fetchMoreLines: fetchMoreLines, fetchMoreLines: fetchMoreLines,
requestNicklist: requestNicklist, requestNicklist: requestNicklist,
attemptReconnect: attemptReconnect attemptReconnect: attemptReconnect,
requestCompletion: requestCompletion
}; };
}]); }]);
})(); })();

@ -184,24 +184,6 @@ weechat.filter('getBufferQuickKeys', function () {
}; };
}); });
// Emojifis the string using https://github.com/Ranks/emojione
weechat.filter('emojify', function() {
return function(text, enable_JS_Emoji) {
if (enable_JS_Emoji === true && window.emojione !== undefined) {
// Emoji live in the D800-DFFF surrogate plane; only bother passing
// this range to CPU-expensive unicodeToImage();
var emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
if (emojiRegex.test(text)) {
return emojione.unicodeToImage(text);
} else {
return(text);
}
} else {
return(text);
}
};
});
weechat.filter('latexmath', function() { weechat.filter('latexmath', function() {
return function(text, selector, enabled) { return function(text, selector, enabled) {
if (!enabled || typeof(katex) === "undefined") { if (!enabled || typeof(katex) === "undefined") {

@ -59,7 +59,6 @@ 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': !utils.isMobileUi(),
'enableMathjax': false, 'enableMathjax': false,
'enableQuickKeys': true, 'enableQuickKeys': true,
'customCSS': '', 'customCSS': '',
@ -79,7 +78,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$log.debug($rootScope.$$watchersCount); $log.debug($rootScope.$$watchersCount);
}; };
// Detect page visibility attributes // Detect page visibility attributes
(function() { (function() {
// Sadly, the page visibility API still has a lot of vendor prefixes // Sadly, the page visibility API still has a lot of vendor prefixes
@ -119,13 +117,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
(["localhost", "127.0.0.1", "::1"].indexOf(window.location.hostname) === -1) && (["localhost", "127.0.0.1", "::1"].indexOf(window.location.hostname) === -1) &&
!window.is_electron && !utils.isCordova(); !window.is_electron && !utils.isCordova();
if (window.is_electron) {
// Use packaged emojione sprite in the electron app
emojione.imageType = 'svg';
emojione.sprites = true;
emojione.imagePathSVGSprites = './3rdparty/emojione.sprites.svg';
}
$rootScope.isWindowFocused = function() { $rootScope.isWindowFocused = function() {
if (typeof $scope.documentHidden === "undefined") { if (typeof $scope.documentHidden === "undefined") {
// Page Visibility API not supported, assume yes // Page Visibility API not supported, assume yes
@ -729,7 +720,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}; };
$scope.connect = function() { $scope.connect = function() {
document.getElementById('audioNotificationInitializer').play(); // Plays some silence, this will enable autoplay for notifications
notifications.requestNotificationPermission(); notifications.requestNotificationPermission();
$rootScope.sslError = false; $rootScope.sslError = false;
$rootScope.securityError = false; $rootScope.securityError = false;
@ -738,7 +729,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$scope.connectbutton = 'Connecting'; $scope.connectbutton = 'Connecting';
$scope.connectbuttonicon = 'glyphicon-refresh glyphicon-spin'; $scope.connectbuttonicon = 'glyphicon-refresh glyphicon-spin';
connection.connect(settings.host, settings.port, settings.path, $scope.password, settings.ssl, settings.useTotp, $scope.totp); connection.connect(settings.host, settings.port, settings.path, $scope.password, settings.ssl, settings.useTotp, $scope.totp);
$scope.totp = "";//clear for next time $scope.totp = ""; // Clear for next time
}; };
$scope.disconnect = function() { $scope.disconnect = function() {

@ -491,6 +491,12 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
}); });
}; };
var handleCompletion = function(message) {
var completionInfo = message.objects[0].content[0];
return completionInfo;
};
var eventHandlers = { var eventHandlers = {
_buffer_closing: handleBufferClosing, _buffer_closing: handleBufferClosing,
_buffer_line_added: handleBufferLineAdded, _buffer_line_added: handleBufferLineAdded,
@ -529,7 +535,8 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
handleLineInfo: handleLineInfo, handleLineInfo: handleLineInfo,
handleHotlistInfo: handleHotlistInfo, handleHotlistInfo: handleHotlistInfo,
handleNicklist: handleNicklist, handleNicklist: handleNicklist,
handleBufferInfo: handleBufferInfo handleBufferInfo: handleBufferInfo,
handleCompletion: handleCompletion
}; };
}]); }]);

@ -6,11 +6,9 @@ var weechat = angular.module('weechat');
weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, settings) { weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, settings) {
var process = function(image, callback) { var process = function(image, callback) {
// Is it an image? // Is it an image?
if (!image || !image.type.match(/image.*/)) return; if (!image || !image.type.match(/image.*/)) return;
// New file reader
var reader = new FileReader(); var reader = new FileReader();
// When image is read // When image is read
@ -18,111 +16,106 @@ weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, setting
var image = event.target.result.split(',')[1]; var image = event.target.result.split(',')[1];
upload(image, callback); upload(image, callback);
}; };
// Read image as data url
reader.readAsDataURL(image); reader.readAsDataURL(image);
}; };
// Upload image to imgur from base64 var authenticate = function(xhr) {
var upload = function( base64img, callback ) {
// API authorization, either via Client ID (anonymous) or access token // API authorization, either via Client ID (anonymous) or access token
// (add to user's imgur account), see also: // (add to user's imgur account), see also:
// https://github.com/glowing-bear/glowing-bear/wiki/Getting-an-imgur-token-&-album-hash // https://github.com/glowing-bear/glowing-bear/wiki/Getting-an-imgur-token-&-album-hash
var accessToken = "164efef8979cd4b"; var accessToken = "164efef8979cd4b";
var isClientID = true;
// Check whether the user has provided an access token // Check whether the user has configured a bearer token, if so, use it
if (settings.iToken.length > 37){ // to add the image to the user's account
accessToken = settings.iToken; if (settings.iToken.length >= 38){
isClientID = false; xhr.setRequestHeader("Authorization", "Bearer " + settings.iToken);
} else {
xhr.setRequestHeader("Authorization", "Client-ID " + accessToken);
} }
};
// Upload image to imgur from base64
var upload = function( base64img, callback ) {
// Progress bars container // Progress bars container
var progressBars = document.getElementById("imgur-upload-progress"), var progressBars = document.getElementById("imgur-upload-progress"),
currentProgressBar = document.createElement("div"); currentProgressBar = document.createElement("div");
// Set progress bar attributes
currentProgressBar.className='imgur-progress-bar'; currentProgressBar.className='imgur-progress-bar';
currentProgressBar.style.width = '0'; currentProgressBar.style.width = '0';
// Append progress bar
progressBars.appendChild(currentProgressBar); progressBars.appendChild(currentProgressBar);
// Create new form data // Assemble the form data for the upload
var fd = new FormData(); var fd = new FormData();
fd.append("image", base64img); // Append the file fd.append("image", base64img);
fd.append("type", "base64"); // Set image type to base64 fd.append("type", "base64");
// Add the image to the provided album if configured to do so // Add the image to the provided album if configured to do so
if (!isClientID && settings.iAlb.length >= 6) { if (settings.iToken.length >= 38 && settings.iAlb.length >= 6) {
fd.append("album", settings.iAlb); fd.append("album", settings.iAlb);
} }
// Create new XMLHttpRequest
var xhttp = new XMLHttpRequest();
// Post request to imgur api // Post request to imgur api
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "https://api.imgur.com/3/image", true); xhttp.open("POST", "https://api.imgur.com/3/image", true);
authenticate(xhttp);
// Set headers
if (isClientID) {
xhttp.setRequestHeader("Authorization", "Client-ID " + accessToken);
} else {
xhttp.setRequestHeader("Authorization", "Bearer " + accessToken);
}
xhttp.setRequestHeader("Accept", "application/json"); xhttp.setRequestHeader("Accept", "application/json");
// Handler for response // Handler for response
xhttp.onload = function() { xhttp.onload = function() {
// Remove progress bar // Remove progress bar
currentProgressBar.parentNode.removeChild(currentProgressBar); progressBars.removeChild(currentProgressBar);
// Check state and response status // Check state and response status
if(xhttp.status === 200) { if (xhttp.status === 200) {
// Get response text
var response = JSON.parse(xhttp.responseText); var response = JSON.parse(xhttp.responseText);
// Send link as message // Send link as message
if( response.data && response.data.link ) { if (response.data && response.data.link) {
if (callback && typeof(callback) === "function") { if (callback && typeof(callback) === "function") {
callback(response.data.link.replace(/^http:/, "https:")); callback(response.data.link.replace(/^http:/, "https:"), response.data.deletehash);
} }
} else { } else {
showErrorMsg(); showErrorMsg();
} }
} else { } else {
showErrorMsg(); showErrorMsg();
} }
}; };
if( "upload" in xhttp ) { if ("upload" in xhttp) {
// Update the progress bar if we can compute progress
// Set progress
xhttp.upload.onprogress = function (event) { xhttp.upload.onprogress = function (event) {
// Check if we can compute progress
if (event.lengthComputable) { if (event.lengthComputable) {
// Complete in percent
var complete = (event.loaded / event.total * 100 | 0); var complete = (event.loaded / event.total * 100 | 0);
// Set progress bar width
currentProgressBar.style.width = complete + '%'; currentProgressBar.style.width = complete + '%';
} }
}; };
} }
// Send request with form data // Send request with form data
xhttp.send(fd); xhttp.send(fd);
};
// Delete an image from imgur with the deletion link
var deleteImage = function( deletehash, callback ) {
var xhttp = new XMLHttpRequest();
// Post request to imgur api
xhttp.open("DELETE", "https://api.imgur.com/3/image/" + deletehash, true);
authenticate(xhttp);
xhttp.setRequestHeader("Accept", "application/json");
// Handler for response
xhttp.onload = function() {
// Check state and response status
if (xhttp.status === 200) {
callback(deletehash);
} else {
showErrorMsg();
}
};
// Send request with form data
xhttp.send(null);
}; };
var showErrorMsg = function() { var showErrorMsg = function() {
@ -139,7 +132,8 @@ weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, setting
}; };
return { return {
process: process process: process,
deleteImage: deleteImage
}; };
}]); }]);

@ -14,10 +14,11 @@ weechat.directive('inputBar', function() {
command: '=command' command: '=command'
}, },
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'imgur', 'models', 'IrcUtils', 'settings', 'utils', function($rootScope, controller: ['$rootScope', '$scope', '$element', '$log', '$compile', 'connection', 'imgur', 'models', 'IrcUtils', 'settings', 'utils', function($rootScope,
$scope, $scope,
$element, //XXX do we need this? don't seem to be using it $element, //XXX do we need this? don't seem to be using it
$log, $log,
$compile,
connection, //XXX we should eliminate this dependency and use signals instead connection, //XXX we should eliminate this dependency and use signals instead
imgur, imgur,
models, models,
@ -31,6 +32,9 @@ weechat.directive('inputBar', function() {
// Emojify input. E.g. Turn :smile: into the unicode equivalent, but // Emojify input. E.g. Turn :smile: into the unicode equivalent, but
// don't do replacements in the middle of a word (e.g. std::io::foo) // don't do replacements in the middle of a word (e.g. std::io::foo)
$scope.inputChanged = function() { $scope.inputChanged = function() {
// Cancel any command completion that was still ongoing
commandCompletionInputChanged = true;
var emojiRegex = /^(?:[\uD800-\uDBFF][\uDC00-\uDFFF])+$/, // *only* emoji var emojiRegex = /^(?:[\uD800-\uDBFF][\uDC00-\uDFFF])+$/, // *only* emoji
changed = false, // whether a segment was modified changed = false, // whether a segment was modified
inputNode = $scope.getInputNode(), inputNode = $scope.getInputNode(),
@ -76,6 +80,13 @@ weechat.directive('inputBar', function() {
}; };
$scope.completeNick = function() { $scope.completeNick = function() {
if ((models.version[0] == 2 && models.version[1] >= 9 || models.version[0] > 2) &&
$scope.command.startsWith('/') ) {
// We are completing a command, another function will do
// this on WeeChat 2.9 and later
return;
}
// input DOM node // input DOM node
var inputNode = $scope.getInputNode(); var inputNode = $scope.getInputNode();
@ -108,6 +119,124 @@ weechat.directive('inputBar', function() {
}, 0); }, 0);
}; };
var previousInput;
var commandCompletionList;
var commandCompletionAddSpace;
var commandCompletionBaseWord;
var commandCompletionPosition;
var commandCompletionPositionInList;
var commandCompletionInputChanged;
$scope.completeCommand = function(direction) {
if (models.version[0] < 2 || (models.version[0] == 2 && models.version[1] < 9)) {
// Command completion is only supported on WeeChat 2.9+
return;
}
if ( !$scope.command.startsWith('/') ) {
// We are not completing a command, maybe a nick?
return;
}
// Cancel if input changes
commandCompletionInputChanged = false;
// 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 || '';
// This function is for later cycling the list after we got it
var cycleCompletionList = function (direction) {
// Don't do anything, the input has changed before we were able to complete the command
if ( commandCompletionInputChanged ) {
return;
}
// Check if the list has elements and we have not cycled to the end yet
if ( !commandCompletionList || !commandCompletionList[0] ) {
return;
}
// If we are cycling in the other direction, go back two placed in the list
if ( direction === 'backward' ) {
commandCompletionPositionInList -= 2;
if ( commandCompletionPositionInList < 0 ) {
// We have reached the beginning of list and are going backward, so go to the end;
commandCompletionPositionInList = commandCompletionList.length - 1;
}
}
// Check we have not reached the end of the cycle list
if ( commandCompletionList.length <= commandCompletionPositionInList ) {
// We have reached the end of the list, start at the beginning
commandCompletionPositionInList = 0;
}
// Cycle the list
// First remove the word that's to be completed
var commandBeforeReplace = $scope.command.substring(0, commandCompletionPosition - commandCompletionBaseWord.length);
var commandAfterReplace = $scope.command.substring(commandCompletionPosition, $scope.command.length);
var replacedWord = commandCompletionList[commandCompletionPositionInList];
var suffix = commandCompletionAddSpace ? ' ' : '';
// Fill in the new command
$scope.command = commandBeforeReplace + replacedWord + suffix + commandAfterReplace;
// Set the cursor position
var newCursorPos = commandBeforeReplace.length + replacedWord.length + suffix.length;
setTimeout(function() {
inputNode.focus();
inputNode.setSelectionRange(newCursorPos, newCursorPos);
}, 0);
// If there is only one item in the list, we are done, no next cycle
if ( commandCompletionList.length === 1) {
previousInput = '';
return;
}
// Setup for the next cycle
commandCompletionPositionInList++;
commandCompletionBaseWord = replacedWord + suffix;
previousInput = $scope.command + activeBuffer.id;
commandCompletionPosition = newCursorPos;
};
// Check if we have requested this completion info before
if (input + activeBuffer.id !== previousInput) {
// Remeber we requested this input for next time
previousInput = input + activeBuffer.id;
// Ask weechat for the completion list
connection.requestCompletion(activeBuffer.id, caretPos, input).then( function(completionObject) {
// Save the list of completion object, we will only request is once
// and cycle through it as long as the input doesn't change
commandCompletionList = completionObject.list;
commandCompletionAddSpace = completionObject.add_space;
commandCompletionBaseWord = completionObject.base_word;
commandCompletionPosition = caretPos;
commandCompletionPositionInList = 0;
}).then( function () {
//after we get the list we can continue with our first cycle
cycleCompletionList(direction);
});
} else {
// Input hasn't changed so we should already have our completion list
cycleCompletionList(direction);
}
};
$rootScope.insertAtCaret = function(toInsert) { $rootScope.insertAtCaret = function(toInsert) {
// caret position in the input bar // caret position in the input bar
var inputNode = $scope.getInputNode(), var inputNode = $scope.getInputNode(),
@ -135,8 +264,8 @@ weechat.directive('inputBar', function() {
$scope.uploadImage = function($event, files) { $scope.uploadImage = function($event, files) {
// Send image url after upload // Send image url after upload
var sendImageUrl = function(imageUrl) { var sendImageUrl = function(imageUrl, deleteHash) {
// Send image // Put link in input box
if(imageUrl !== undefined && imageUrl !== '') { if(imageUrl !== undefined && imageUrl !== '') {
$rootScope.insertAtCaret(String(imageUrl)); $rootScope.insertAtCaret(String(imageUrl));
} }
@ -148,10 +277,29 @@ weechat.directive('inputBar', function() {
// Process image // Process image
imgur.process(files[i], sendImageUrl); imgur.process(files[i], sendImageUrl);
} }
}
};
var deleteCallback = function (deleteHash) {
// Image got sucessfully deleted.
// Show toast with delete link
var toastDeleted = $compile('<div class="toast toast-short">Successfully deleted.</div>')($scope)[0];
document.body.appendChild(toastDeleted);
setTimeout(function() { document.body.removeChild(toastDeleted); }, 5000);
// Try to remove the toast with the deletion link (it stays 15s
// instead of the 5 of the deletion notification, so it could
// come back beneath it, which would be confusing)
var pasteToast = document.querySelector("[data-imgur-deletehash='" + deleteHash + "']");
if (!!pasteToast) {
document.body.removeChild(pasteToast);
} }
}; };
$scope.imgurDelete = function (deleteHash) {
imgur.deleteImage( deleteHash, deleteCallback );
};
// Send the message to the websocket // Send the message to the websocket
$scope.sendMessage = function() { $scope.sendMessage = function() {
//XXX Use a signal here //XXX Use a signal here
@ -220,10 +368,10 @@ weechat.directive('inputBar', function() {
// Check whether the user is still online // Check whether the user is still online
var buffer = models.getBuffer(bufferline.buffer); var buffer = models.getBuffer(bufferline.buffer);
var is_online = buffer.queryNicklist(nick); var is_online = buffer.queryNicklist(nick);
if (!is_online) { if (buffer.type === 'channel' && !is_online) {
// show a toast that the user left // show a toast that the user left
var toast = document.createElement('div'); var toast = document.createElement('div');
toast.id = "toast"; toast.className = "toast toast-short";
toast.innerHTML = nick + " has left the room"; toast.innerHTML = nick + " has left the room";
document.body.appendChild(toast); document.body.appendChild(toast);
setTimeout(function() { document.body.removeChild(toast); }, 5000); setTimeout(function() { document.body.removeChild(toast); }, 5000);
@ -367,10 +515,18 @@ weechat.directive('inputBar', function() {
} }
// Tab -> nick completion // Tab -> nick completion
if (code === 9 && !$event.altKey && !$event.ctrlKey) { if (code === 9 && !$event.altKey && !$event.ctrlKey && !$event.shiftKey) {
$event.preventDefault(); $event.preventDefault();
$scope.iterCandidate = tmpIterCandidate; $scope.iterCandidate = tmpIterCandidate;
$scope.completeNick(); $scope.completeNick();
$scope.completeCommand('forward');
return true;
}
// Shitft-Tab -> nick completion backward (only commands)
if (code === 9 && !$event.altKey && !$event.ctrlKey && $event.shiftKey) {
$event.preventDefault();
$scope.completeCommand('backward');
return true; return true;
} }
@ -476,26 +632,32 @@ weechat.directive('inputBar', function() {
// Arrow up -> go up in history // Arrow up -> go up in history
if ($event.type === "keydown" && code === 38 && document.activeElement === inputNode) { if ($event.type === "keydown" && code === 38 && document.activeElement === inputNode) {
caretPos = inputNode.selectionStart; // In case of multiline we don't want to do this unless at the first line
if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) { if ($scope.command) {
return false; caretPos = inputNode.selectionStart;
if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) {
return false;
}
} }
$scope.command = models.getActiveBuffer().getHistoryUp($scope.command); $scope.command = models.getActiveBuffer().getHistoryUp($scope.command);
// Set cursor to last position. Need 0ms timeout because browser sets cursor // Set cursor to last position. Need 1ms (0ms works for chrome) timeout because
// position to the beginning after this key handler returns. // browser sets cursor position to the beginning after this key handler returns.
setTimeout(function() { setTimeout(function() {
if ($scope.command) { if ($scope.command) {
inputNode.setSelectionRange($scope.command.length, $scope.command.length); inputNode.setSelectionRange($scope.command.length, $scope.command.length);
} }
}, 0); }, 1);
return true; return true;
} }
// Arrow down -> go down in history // Arrow down -> go down in history
if ($event.type === "keydown" && code === 40 && document.activeElement === inputNode) { if ($event.type === "keydown" && code === 40 && document.activeElement === inputNode) {
caretPos = inputNode.selectionStart; // In case of multiline we don't want to do this unless it's the last line
if ($scope.command.slice(caretPos).indexOf("\n") !== -1) { if ($scope.command) {
return false; caretPos = inputNode.selectionStart;
if ( $scope.command.slice(caretPos).indexOf("\n") !== -1) {
return false;
}
} }
$scope.command = models.getActiveBuffer().getHistoryDown($scope.command); $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 // We don't need to set the cursor to the rightmost position here, the browser does that for us
@ -606,6 +768,7 @@ weechat.directive('inputBar', function() {
$scope.handleCompleteNickButton = function($event) { $scope.handleCompleteNickButton = function($event) {
$event.preventDefault(); $event.preventDefault();
$scope.completeNick(); $scope.completeNick();
$scope.completeCommand('forward');
setTimeout(function() { setTimeout(function() {
$scope.getInputNode().focus(); $scope.getInputNode().focus();
@ -613,15 +776,24 @@ weechat.directive('inputBar', function() {
return true; return true;
}; };
$scope.inputPasted = function(e) { $scope.inputPasted = function(e) {
if (e.clipboardData && e.clipboardData.files && e.clipboardData.files.length) { if (e.clipboardData && e.clipboardData.files && e.clipboardData.files.length) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
var sendImageUrl = function(imageUrl) { var sendImageUrl = function(imageUrl, deleteHash) {
if(imageUrl !== undefined && imageUrl !== '') { if(imageUrl !== undefined && imageUrl !== '') {
$rootScope.insertAtCaret(String(imageUrl)); $rootScope.insertAtCaret(String(imageUrl));
} }
// Show toast with delete link
var toastImgur = $compile('<div class="toast toast-long" data-imgur-deletehash=\'' + deleteHash + '\'>Image uploaded to Imgur. <a id="deleteImgur" ng-click="imgurDelete(\'' + deleteHash + '\')" href="">Delete?</a></div>')($scope)[0];
document.body.appendChild(toastImgur);
setTimeout(function() { document.body.removeChild(toastImgur); }, 15000);
// Log the delete hash to the console in case the toast was missed.
console.log('An image was uploaded to imgur, delete it with $scope.imgurDelete(\'' + deleteHash + '\')');
}; };
for (var i = 0; i < e.clipboardData.files.length; i++) { for (var i = 0; i < e.clipboardData.files.length; i++) {

@ -90,7 +90,9 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo
// There are two kinds of types: bufferType (free vs formatted) and // There are two kinds of types: bufferType (free vs formatted) and
// the kind of type that distinguishes queries from channels etc // the kind of type that distinguishes queries from channels etc
var bufferType = message.type; var bufferType = message.type;
var type = message.local_variables.type;
// If type is undefined set it as other to avoid later errors
var type = message.local_variables.type || 'other';
var indent = (['channel', 'private'].indexOf(type) >= 0); var indent = (['channel', 'private'].indexOf(type) >= 0);
var plugin = message.local_variables.plugin; var plugin = message.local_variables.plugin;
@ -98,6 +100,9 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo
var pinned = message.local_variables.pinned === "true"; var pinned = message.local_variables.pinned === "true";
// hide timestamps for certain buffer types
var hideBufferLineTimes = type && type === 'relay';
// Server buffers have this "irc.server.freenode" naming schema, which // Server buffers have this "irc.server.freenode" naming schema, which
// messes the sorting up. We need it to be "irc.freenode" instead. // messes the sorting up. We need it to be "irc.freenode" instead.
var serverSortKey = plugin + "." + server + var serverSortKey = plugin + "." + server +
@ -365,6 +370,7 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo
getHistoryDown: getHistoryDown, getHistoryDown: getHistoryDown,
isNicklistEmpty: isNicklistEmpty, isNicklistEmpty: isNicklistEmpty,
nicklistRequested: nicklistRequested, nicklistRequested: nicklistRequested,
hideBufferLineTimes: hideBufferLineTimes,
pinned: pinned, pinned: pinned,
queryNicklist: queryNicklist, queryNicklist: queryNicklist,
}; };

@ -553,8 +553,42 @@ plugins.factory('userPlugins', function() {
} }
}); });
/*
* TikTok embedded player
* Very similar to twitter
*/
var tikTokPlugin = new UrlPlugin('TikTok', function(url) {
var regex = /^https?:\/\/(?:www\.)?tiktok\.com\/@(?:.+)\/video\/(?:.+)\/?$|^https?:\/\/vm\.tiktok\.com\/[a-zA-Z1-9]{7}\/?$/i;
var match = url.match(regex);
if (match) {
return function() {
var element = this.getElement();
fetch("https://www.tiktok.com/oembed?url=" + url)
.then(function(response) {
return response.json();
})
.then(function(data) {
// Separate the HTML into content and script tag
var scriptIndex = data.html.indexOf("<script ");
var content = data.html.substr(0, scriptIndex);
element.innerHTML = content;
// Change the width so we get the deskop version of the embed
element.children[0].style.maxWidth = "650px";
// The script tag needs to be generated manually or the browser won't load it
var scriptElem = document.createElement('script');
// Hardcoding the URL here, I don't suppose it's going to change anytime soon
scriptElem.src = "https://www.tiktok.com/embed.js";
element.appendChild(scriptElem);
});
};
}
});
return { return {
plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, pastebinPlugin, giphyPlugin, tweetPlugin, streamablePlugin] plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, pastebinPlugin, giphyPlugin, tweetPlugin, streamablePlugin, tikTokPlugin]
}; };

@ -777,6 +777,33 @@
return WeeChatProtocol._formatCmd(params.id, 'input', parts); return WeeChatProtocol._formatCmd(params.id, 'input', parts);
}; };
/**
* Formats a completion command.
* https://weechat.org/files/doc/stable/weechat_relay_protocol.en.html#command_completion
* @param params Parameters:
* id: command ID (optional)
* buffer: target buffer (mandatory)
* position: position for completion in string (optional)
* data: input data (optional)
* @return Formatted input command string
*/
WeeChatProtocol.formatCompletion = function(params) {
var defaultParams = {
id: null,
position: -1
};
var parts = [];
params = WeeChatProtocol._mergeParams(defaultParams, params);
parts.push(params.buffer);
parts.push(params.position);
if (params.data) {
parts.push(params.data);
}
return WeeChatProtocol._formatCmd(params.id, 'completion', parts);
};
/** /**
* Formats a sync or a desync command. * Formats a sync or a desync command.
* *

@ -1,7 +1,7 @@
{ {
"name": "Glowing Bear", "name": "Glowing Bear",
"description": "WeeChat Web frontend", "description": "WeeChat Web frontend",
"version": "0.8.0", "version": "0.9.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.8.0" "version": "0.9.0"
} }

@ -1,7 +1,7 @@
{ {
"name": "glowing-bear", "name": "glowing-bear",
"private": true, "private": true,
"version": "0.8.0", "version": "0.9.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",
"main": "electron-main.js", "main": "electron-main.js",

@ -169,5 +169,15 @@ describe('filter', function() {
plugins); plugins);
})); }));
it('should recognize tiktoks', inject(function(plugins) {
expectTheseMessagesToContain([
'https://www.tiktok.com/@scout2015/video/6718335390845095173',
'https://www.tiktok.com/@lewiscatpaldi/video/6800461190058298629',
'https://www.tiktok.com/@bhattrai_fam5_kavita/video/6811222914768047365',
],
'TikTok',
plugins);
}));
}); });
}); });

Loading…
Cancel
Save