diff --git a/README.md b/README.md index e8fe7de..ddb7525 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Glowing Bear connects to the WeeChat instance you're already running (version 0. /relay add weechat 9001 /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. @@ -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. -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 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. diff --git a/bower.json b/bower.json index 4bd05c4..84d39a2 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "glowing-bear", "description": "A webclient for WeeChat", - "version": "0.8.0", + "version": "0.9.0", "homepage": "https://github.com/glowing-bear/glowing-bear", "license": "GPLv3", "private": true, diff --git a/css/glowingbear.css b/css/glowingbear.css index 62a10fb..1aac1f8 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -693,22 +693,7 @@ li.buffer.indent.private a { user-select: none; } -.emojione { - 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 { +.toast { position: fixed; left: 50%; bottom: 50px; @@ -719,7 +704,14 @@ img.emojione { border-radius: 3px; padding: 10px 15px; 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 { @@ -960,3 +952,11 @@ code { color: #444; border: 1pt solid #444; } + +#bufferlines.hideTime td.time { + display:none; +} + +#bufferlines.hideTime td.prefix { + display:none; +} diff --git a/css/themes/base16-default.css b/css/themes/base16-default.css index 2324240..b5afe88 100644 --- a/css/themes/base16-default.css +++ b/css/themes/base16-default.css @@ -423,7 +423,7 @@ button.close:hover { color: var(--base01); } -#toast { +.toast { background-color: var(--base01); } diff --git a/css/themes/blue.css b/css/themes/blue.css index cb666bc..3036162 100644 --- a/css/themes/blue.css +++ b/css/themes/blue.css @@ -134,7 +134,7 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-se border: 1px solid #363943; } -#toast { +.toast { background-color: #283244; border: 1px solid; border-color: rgb(29, 94, 152); diff --git a/css/themes/dark.css b/css/themes/dark.css index 4d845fe..982ab9b 100644 --- a/css/themes/dark.css +++ b/css/themes/dark.css @@ -2126,7 +2126,7 @@ code { color: #fff; } -#toast { +.toast { background-color: #333; } diff --git a/css/themes/light.css b/css/themes/light.css index 900404d..dcb475e 100644 --- a/css/themes/light.css +++ b/css/themes/light.css @@ -2092,7 +2092,7 @@ input[type=text].is-invalid{ font-weight: bold; } -#toast { +.toast { background-color: #ddd; } diff --git a/electron.makefile b/electron.makefile index 966c2ca..adf67a0 100644 --- a/electron.makefile +++ b/electron.makefile @@ -9,7 +9,6 @@ bower: copylocal: find bower_components \( -name "*min.js" -o -name "*min.css" \) -exec cp {} 3rdparty \; cp -r bower_components/bootstrap/fonts . - cp bower_components/emojione/assets/sprites/emojione.sprites.svg 3rdparty # modify index.html to use local files uselocal: copylocal diff --git a/index.html b/index.html index a34d217..fe1c50b 100644 --- a/index.html +++ b/index.html @@ -19,10 +19,10 @@ - - - - + + + + @@ -30,6 +30,7 @@ +

Upload error: Image upload failed.

@@ -376,7 +377,7 @@ npm run build-electron-{windows, darwin, linux} -
+
@@ -396,7 +397,7 @@ npm run build-electron-{windows, darwin, linux} @@ -423,7 +424,7 @@ npm run build-electron-{windows, darwin, linux} -
  • -
    -
    - -
    - -
  • diff --git a/js/connection.js b/js/connection.js index a8f813d..1d150ca 100644 --- a/js/connection.js +++ b/js/connection.js @@ -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 { connect: connect, @@ -538,7 +557,8 @@ weechat.factory('connection', sendHotlistClearAll: sendHotlistClearAll, fetchMoreLines: fetchMoreLines, requestNicklist: requestNicklist, - attemptReconnect: attemptReconnect + attemptReconnect: attemptReconnect, + requestCompletion: requestCompletion }; }]); })(); diff --git a/js/filters.js b/js/filters.js index 61f13c0..193abcf 100644 --- a/js/filters.js +++ b/js/filters.js @@ -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() { return function(text, selector, enabled) { if (!enabled || typeof(katex) === "undefined") { diff --git a/js/glowingbear.js b/js/glowingbear.js index 04519bb..2888d4b 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -59,7 +59,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', 'fontsize': '14px', 'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'), 'readlineBindings': false, - 'enableJSEmoji': !utils.isMobileUi(), 'enableMathjax': false, 'enableQuickKeys': true, 'customCSS': '', @@ -79,7 +78,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $log.debug($rootScope.$$watchersCount); }; - // Detect page visibility attributes (function() { // 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) && !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() { if (typeof $scope.documentHidden === "undefined") { // Page Visibility API not supported, assume yes @@ -729,7 +720,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', }; $scope.connect = function() { - + document.getElementById('audioNotificationInitializer').play(); // Plays some silence, this will enable autoplay for notifications notifications.requestNotificationPermission(); $rootScope.sslError = false; $rootScope.securityError = false; @@ -738,7 +729,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $scope.connectbutton = 'Connecting'; $scope.connectbuttonicon = 'glyphicon-refresh glyphicon-spin'; 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() { diff --git a/js/handlers.js b/js/handlers.js index 9891e8f..16d6288 100644 --- a/js/handlers.js +++ b/js/handlers.js @@ -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 = { _buffer_closing: handleBufferClosing, _buffer_line_added: handleBufferLineAdded, @@ -529,7 +535,8 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific handleLineInfo: handleLineInfo, handleHotlistInfo: handleHotlistInfo, handleNicklist: handleNicklist, - handleBufferInfo: handleBufferInfo + handleBufferInfo: handleBufferInfo, + handleCompletion: handleCompletion }; }]); diff --git a/js/imgur.js b/js/imgur.js index 0ec0f97..6164568 100644 --- a/js/imgur.js +++ b/js/imgur.js @@ -6,11 +6,9 @@ var weechat = angular.module('weechat'); weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, settings) { var process = function(image, callback) { - // Is it an image? if (!image || !image.type.match(/image.*/)) return; - // New file reader var reader = new FileReader(); // When image is read @@ -18,111 +16,106 @@ weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, setting var image = event.target.result.split(',')[1]; upload(image, callback); }; - - // Read image as data url reader.readAsDataURL(image); - }; - // Upload image to imgur from base64 - var upload = function( base64img, callback ) { - + var authenticate = function(xhr) { // API authorization, either via Client ID (anonymous) or access token // (add to user's imgur account), see also: // https://github.com/glowing-bear/glowing-bear/wiki/Getting-an-imgur-token-&-album-hash var accessToken = "164efef8979cd4b"; - var isClientID = true; - // Check whether the user has provided an access token - if (settings.iToken.length > 37){ - accessToken = settings.iToken; - isClientID = false; + // Check whether the user has configured a bearer token, if so, use it + // to add the image to the user's account + if (settings.iToken.length >= 38){ + 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 var progressBars = document.getElementById("imgur-upload-progress"), currentProgressBar = document.createElement("div"); - - // Set progress bar attributes currentProgressBar.className='imgur-progress-bar'; currentProgressBar.style.width = '0'; - // Append progress bar progressBars.appendChild(currentProgressBar); - // Create new form data + // Assemble the form data for the upload var fd = new FormData(); - fd.append("image", base64img); // Append the file - fd.append("type", "base64"); // Set image type to base64 + fd.append("image", base64img); + fd.append("type", "base64"); // 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); } - // Create new XMLHttpRequest - var xhttp = new XMLHttpRequest(); - // Post request to imgur api + var xhttp = new XMLHttpRequest(); xhttp.open("POST", "https://api.imgur.com/3/image", true); - - // Set headers - if (isClientID) { - xhttp.setRequestHeader("Authorization", "Client-ID " + accessToken); - } else { - xhttp.setRequestHeader("Authorization", "Bearer " + accessToken); - } + authenticate(xhttp); xhttp.setRequestHeader("Accept", "application/json"); // Handler for response xhttp.onload = function() { - // Remove progress bar - currentProgressBar.parentNode.removeChild(currentProgressBar); + progressBars.removeChild(currentProgressBar); // Check state and response status - if(xhttp.status === 200) { - - // Get response text + if (xhttp.status === 200) { var response = JSON.parse(xhttp.responseText); // Send link as message - if( response.data && response.data.link ) { - + if (response.data && response.data.link) { if (callback && typeof(callback) === "function") { - callback(response.data.link.replace(/^http:/, "https:")); + callback(response.data.link.replace(/^http:/, "https:"), response.data.deletehash); } - } else { showErrorMsg(); } - } else { showErrorMsg(); } - }; - if( "upload" in xhttp ) { - - // Set progress + if ("upload" in xhttp) { + // Update the progress bar if we can compute progress xhttp.upload.onprogress = function (event) { - - // Check if we can compute progress if (event.lengthComputable) { - // Complete in percent var complete = (event.loaded / event.total * 100 | 0); - - // Set progress bar width currentProgressBar.style.width = complete + '%'; } }; - } - // Send request with form data xhttp.send(fd); + }; + // 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() { @@ -139,7 +132,8 @@ weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, setting }; return { - process: process + process: process, + deleteImage: deleteImage }; }]); diff --git a/js/inputbar.js b/js/inputbar.js index 656a4a7..d6ea251 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -14,10 +14,11 @@ weechat.directive('inputBar', function() { 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, $element, //XXX do we need this? don't seem to be using it $log, + $compile, connection, //XXX we should eliminate this dependency and use signals instead imgur, models, @@ -31,6 +32,9 @@ weechat.directive('inputBar', function() { // 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) $scope.inputChanged = function() { + // Cancel any command completion that was still ongoing + commandCompletionInputChanged = true; + var emojiRegex = /^(?:[\uD800-\uDBFF][\uDC00-\uDFFF])+$/, // *only* emoji changed = false, // whether a segment was modified inputNode = $scope.getInputNode(), @@ -76,6 +80,13 @@ weechat.directive('inputBar', 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 var inputNode = $scope.getInputNode(); @@ -108,6 +119,124 @@ weechat.directive('inputBar', function() { }, 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) { // caret position in the input bar var inputNode = $scope.getInputNode(), @@ -135,8 +264,8 @@ weechat.directive('inputBar', function() { $scope.uploadImage = function($event, files) { // Send image url after upload - var sendImageUrl = function(imageUrl) { - // Send image + var sendImageUrl = function(imageUrl, deleteHash) { + // Put link in input box if(imageUrl !== undefined && imageUrl !== '') { $rootScope.insertAtCaret(String(imageUrl)); } @@ -148,10 +277,29 @@ weechat.directive('inputBar', function() { // Process image imgur.process(files[i], sendImageUrl); } + } + }; + var deleteCallback = function (deleteHash) { + // Image got sucessfully deleted. + // Show toast with delete link + var toastDeleted = $compile('
    Successfully deleted.
    ')($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 $scope.sendMessage = function() { //XXX Use a signal here @@ -220,10 +368,10 @@ weechat.directive('inputBar', function() { // Check whether the user is still online var buffer = models.getBuffer(bufferline.buffer); var is_online = buffer.queryNicklist(nick); - if (!is_online) { + if (buffer.type === 'channel' && !is_online) { // show a toast that the user left var toast = document.createElement('div'); - toast.id = "toast"; + toast.className = "toast toast-short"; toast.innerHTML = nick + " has left the room"; document.body.appendChild(toast); setTimeout(function() { document.body.removeChild(toast); }, 5000); @@ -367,10 +515,18 @@ weechat.directive('inputBar', function() { } // Tab -> nick completion - if (code === 9 && !$event.altKey && !$event.ctrlKey) { + if (code === 9 && !$event.altKey && !$event.ctrlKey && !$event.shiftKey) { $event.preventDefault(); $scope.iterCandidate = tmpIterCandidate; $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; } @@ -476,26 +632,32 @@ weechat.directive('inputBar', function() { // Arrow up -> go up in history if ($event.type === "keydown" && code === 38 && document.activeElement === inputNode) { - caretPos = inputNode.selectionStart; - if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) { - return false; + // In case of multiline we don't want to do this unless at the first line + if ($scope.command) { + caretPos = inputNode.selectionStart; + if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) { + return false; + } } $scope.command = models.getActiveBuffer().getHistoryUp($scope.command); - // Set cursor to last position. Need 0ms timeout because browser sets cursor - // position to the beginning after this key handler returns. + // Set cursor to last position. Need 1ms (0ms works for chrome) timeout because + // browser sets cursor position to the beginning after this key handler returns. setTimeout(function() { if ($scope.command) { inputNode.setSelectionRange($scope.command.length, $scope.command.length); } - }, 0); + }, 1); return true; } // Arrow down -> go down in history if ($event.type === "keydown" && code === 40 && document.activeElement === inputNode) { - caretPos = inputNode.selectionStart; - if ($scope.command.slice(caretPos).indexOf("\n") !== -1) { - return false; + // In case of multiline we don't want to do this unless it's the last line + if ($scope.command) { + caretPos = inputNode.selectionStart; + if ( $scope.command.slice(caretPos).indexOf("\n") !== -1) { + return false; + } } $scope.command = models.getActiveBuffer().getHistoryDown($scope.command); // We don't need to set the cursor to the rightmost position here, the browser does that for us @@ -606,6 +768,7 @@ weechat.directive('inputBar', function() { $scope.handleCompleteNickButton = function($event) { $event.preventDefault(); $scope.completeNick(); + $scope.completeCommand('forward'); setTimeout(function() { $scope.getInputNode().focus(); @@ -613,15 +776,24 @@ weechat.directive('inputBar', function() { return true; }; + $scope.inputPasted = function(e) { if (e.clipboardData && e.clipboardData.files && e.clipboardData.files.length) { e.stopPropagation(); e.preventDefault(); - var sendImageUrl = function(imageUrl) { + var sendImageUrl = function(imageUrl, deleteHash) { if(imageUrl !== undefined && imageUrl !== '') { $rootScope.insertAtCaret(String(imageUrl)); } + + // Show toast with delete link + var toastImgur = $compile('
    Image uploaded to Imgur. Delete?
    ')($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++) { diff --git a/js/models.js b/js/models.js index 1ebc411..62ed94d 100644 --- a/js/models.js +++ b/js/models.js @@ -90,7 +90,9 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo // There are two kinds of types: bufferType (free vs formatted) and // the kind of type that distinguishes queries from channels etc var bufferType = message.type; - var type = message.local_variables.type; + + // 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 plugin = message.local_variables.plugin; @@ -98,6 +100,9 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo 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 // messes the sorting up. We need it to be "irc.freenode" instead. var serverSortKey = plugin + "." + server + @@ -365,6 +370,7 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo getHistoryDown: getHistoryDown, isNicklistEmpty: isNicklistEmpty, nicklistRequested: nicklistRequested, + hideBufferLineTimes: hideBufferLineTimes, pinned: pinned, queryNicklist: queryNicklist, }; diff --git a/js/plugins.js b/js/plugins.js index 2dcabc4..9d76376 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -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("
  • <>
    + -->