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('
')($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("