diff --git a/customize.dist/src/less2/include/buttons.less b/customize.dist/src/less2/include/buttons.less
index 7936a5ee6..d769f8e07 100644
--- a/customize.dist/src/less2/include/buttons.less
+++ b/customize.dist/src/less2/include/buttons.less
@@ -9,7 +9,7 @@
@alertify-input-bg: @colortheme_modal-input;
@alertify-input-fg: @colortheme_modal-input-fg;
- input:not(.form-control), textarea {
+ input:not(.form-control), textarea, div.cp-textarea {
// background-color: @alertify-input-fg;
color: @cryptpad_text_col;
border: 1px solid @alertify-input-bg;
@@ -44,13 +44,24 @@
}
}
- textarea {
+ textarea, div.cp-textarea {
padding: 8px;
&[readonly] {
overflow: hidden;
resize: none;
}
}
+ div.cp-textarea {
+ height: 60px;
+ width: 100%;
+ background-color: white;
+ cursor: text;
+ outline: none;
+ white-space: pre-wrap;
+ overflow-y: auto;
+ word-break: break-word;
+ resize: vertical;
+ }
div.cp-button-confirm {
display: inline-block;
diff --git a/customize.dist/src/less2/include/comments.less b/customize.dist/src/less2/include/comments.less
index 721f308cb..6859c47e3 100644
--- a/customize.dist/src/less2/include/comments.less
+++ b/customize.dist/src/less2/include/comments.less
@@ -24,8 +24,9 @@
.avatar_main(40px);
display: flex;
align-items: flex-start;
- textarea {
+ div.cp-textarea {
flex: 1;
+ min-height: 50px;
height: 50px;
padding: 2px 8px;
}
@@ -38,6 +39,9 @@
}
}
+ #cp-comments-label {
+ display: none;
+ }
.cp-comment-container {
outline: none;
@@ -81,7 +85,7 @@
background-color: white;
padding: 10px 5px 5px;
white-space: pre-wrap;
- word-break: break-all;
+ word-break: break-word;
}
.cp-comment-actions {
display: none;
diff --git a/customize.dist/src/less2/include/dropdown.less b/customize.dist/src/less2/include/dropdown.less
index 271603216..0cabeabbf 100644
--- a/customize.dist/src/less2/include/dropdown.less
+++ b/customize.dist/src/less2/include/dropdown.less
@@ -1,11 +1,30 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./tools.less";
+@import (reference) "./avatar.less";
/* The container
- needed to position the dropdown content */
.dropdown_main () {
--LessLoader_require: LessLoader_currentFile();
}
& {
+ .cp-autocomplete-value {
+ display: flex;
+ align-items: center;
+ .cp-avatar {
+ .avatar_main(30px);
+ padding: 1px;
+ }
+ & > span:last-child {
+ flex: 1;
+ height: 32px;
+ line-height: 32px;
+ padding: 0 10px;
+ }
+ span {
+ margin: 0px !important;
+ border: none !important;
+ }
+ }
.cp-dropdown-container {
@dropdown_font: @colortheme_app-font-size @colortheme_font;
position: relative;
diff --git a/customize.dist/src/less2/include/framework.less b/customize.dist/src/less2/include/framework.less
index 0eefababb..0d34361ab 100644
--- a/customize.dist/src/less2/include/framework.less
+++ b/customize.dist/src/less2/include/framework.less
@@ -15,6 +15,7 @@
@import (reference) "./messenger.less";
@import (reference) "./cursor.less";
@import (reference) "./usergrid.less";
+@import (reference) "./mentions.less";
@import (reference) "./modals-ui-elements.less";
.framework_main(@bg-color, @warn-color, @color) {
@@ -44,6 +45,7 @@
.messenger_main();
.cursor_main();
.usergrid_main();
+ .mentions_main();
.creation_main(
@bg-color: @bg-color,
@color: @color
diff --git a/customize.dist/src/less2/include/mentions.less b/customize.dist/src/less2/include/mentions.less
new file mode 100644
index 000000000..424b8b710
--- /dev/null
+++ b/customize.dist/src/less2/include/mentions.less
@@ -0,0 +1,31 @@
+@import (reference) "./tools.less";
+@import (reference) "./avatar.less";
+
+.mentions_main() {
+ --LessLoader_require: LessLoader_currentFile();
+}
+
+& {
+ .cp-mentions {
+ .avatar_main(20px);
+ .tools_unselectable();
+ display: inline-flex;
+ align-items: center;
+ vertical-align: bottom;
+ background-color: #eee;
+
+ span.cp-mentions-name {
+ max-width: 150px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ &.cp-mentions-clickable {
+ outline: none;
+ cursor: pointer;
+ &:hover {
+ background-color: #ddd;
+ }
+ }
+ }
+}
diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js
index a5c1396c2..63e110c0b 100644
--- a/www/common/common-ui-elements.js
+++ b/www/common/common-ui-elements.js
@@ -3666,5 +3666,259 @@ define([
UI.proposal(div, todo);
};
+ var insertTextAtCursor = function (text) {
+ var selection = window.getSelection();
+ var range = selection.getRangeAt(0);
+ range.deleteContents();
+ var node = document.createTextNode(text);
+ range.insertNode(node);
+
+ for (var position = 0; position != text.length; position++) {
+ selection.modify("move", "right", "character");
+ };
+ }
+
+ var getSource = {};
+ getSource['contacts'] = function (common, sources) {
+ var priv = common.getMetadataMgr().getPrivateData();
+ Object.keys(priv.friends || {}).forEach(function (key) {
+ if (key === 'me') { return; }
+ var f = priv.friends[key];
+ if (!f.curvePublic || sources[f.curvePublic]) { return; }
+ sources[f.curvePublic] = {
+ avatar: f.avatar,
+ name: f.displayName,
+ curvePublic: f.curvePublic,
+ profile: f.profile,
+ notifications: f.notifications
+ };
+ });
+ };
+ UIElements.addMentions = function (common, options) {
+ if (!options.$input) { return; }
+ var $t = options.$input;
+
+ var getValue = function () { return $t.val(); };
+ var setValue = function (val) { $t.val(val); };
+
+ var div = false;
+ if (options.contenteditable) {
+ div = true;
+ getValue = function () { return $t.html(); };
+ setValue = function () {}; // Not used, we insert data at the node level
+ $t.on('paste', function (e) {
+ try {
+ insertTextAtCursor(e.originalEvent.clipboardData.getData('text'));
+ e.preventDefault();
+ } catch (e) { console.error(e); }
+ });
+
+ // Fix backspace with "contenteditable false" children
+ $t.on('keydown', function (e) {
+ if (e.which !== 8 && e.which !== 46) { return; } // Backspace or del
+ var sel = document.getSelection();
+ if (sel.anchorNode.nodeType !== Node.TEXT_NODE) { return; } // text nodes only
+
+ // Only fix node located after mentions
+ var n = sel.anchorNode;
+ var prev = n && n.previousSibling;
+ // Check if our caret is just after a mention
+ if (!prev || !prev.classList || !prev.classList.contains('cp-mentions')) { return; }
+
+ // Del: if we're at offset 0, make sure we won't delete the text node
+ if (e.which === 46) {
+ if (!sel.anchorOffset && sel.anchorNode.length === 1) {
+ sel.anchorNode.nodeValue = " ";
+ e.preventDefault();
+ }
+ return;
+ }
+
+ // Backspace
+ // If we're not at offset 0, make sure we won't delete the text node
+ if (e.which === 8 && sel.anchorOffset) {
+ if (sel.anchorNode.length === 1) {
+ sel.anchorNode.nodeValue = " ";
+ e.preventDefault();
+ }
+ return;
+ }
+ // If we're at offset 0, We're just after a mention: delete it
+ prev.parentElement.removeChild(prev);
+ e.preventDefault();
+ });
+ }
+
+ // Add the sources
+ // NOTE: Sources must have a "name". They can have an "avatar".
+ var sources = options.sources || {};
+ if (!getSource[options.type]) { return; }
+ getSource[options.type](common, sources);
+
+
+ // Sort autocomplete result by label
+ var sort = function (a, b) {
+ var _a = a.label.toLowerCase();
+ var _b = b.label.toLowerCase();
+ if (_a.label < _b.label) { return -1; }
+ if (_b.label < _a.label) { return 1; }
+ return 0;
+ };
+
+ // Get the text between the last @ before the cursor and the cursor
+ var extractLast = function (term, offset) {
+ var offset = typeof(offset) !== "undefined" ? offset : $t[0].selectionStart;
+ var startOffset = term.slice(0,offset).lastIndexOf('@');
+ return term.slice(startOffset+1, offset);
+ };
+ // Insert the autocomplete value in the input field
+ var insertValue = function (value, offset, content) {
+ var offset = typeof(offset) !== "undefined" ? offset : $t[0].selectionStart;
+ var content = content || getValue();
+ var startOffset = content.slice(0,offset).lastIndexOf('@');
+ var length = offset - startOffset;
+ if (length <= 0) { return; }
+ var result = content.slice(0,startOffset) + value + content.slice(offset);
+ if (content) {
+ return {
+ result: result,
+ startOffset: startOffset
+ };
+ }
+ setValue(result);
+ };
+ // Set the value to receive from the autocomplete
+ var toInsert = function (data, key) {
+ var name = data.name.replace(/[^a-zA-Z0-9]+/g, "-");
+ return "[@"+name+"|"+key+"]";
+ };
+
+ // Fix the functions when suing a contenteditable div
+ if (div) {
+ var _extractLast = extractLast;
+ // Use getSelection to get the cursor position in contenteditable
+ extractLast = function () {
+ var val = getValue();
+ var sel = document.getSelection();
+ if (sel.anchorNode.nodeType !== Node.TEXT_NODE) { return; }
+ return _extractLast(sel.anchorNode.nodeValue, sel.anchorOffset);
+ };
+ var _insertValue = insertValue;
+ insertValue = function (value) {
+ // Get the selected node
+ var sel = document.getSelection();
+ if (sel.anchorNode.nodeType !== Node.TEXT_NODE) { return; }
+ var node = sel.anchorNode;
+
+ // Remove the "term"
+ var insert =_insertValue("", sel.anchorOffset, node.nodeValue);
+ if (insert) {
+ node.nodeValue = insert.result;
+ }
+ var breakAt = insert ? insert.startOffset : sel.anchorOffset;
+
+ var el;
+ if (typeof(value) === "string") { el = document.createTextNode(value); }
+ else { el = value; }
+
+ node.parentNode.insertBefore(el, node.splitText(breakAt));
+ var next = el.nextSibling;
+ if (!next) {
+ next = document.createTextNode(" ");
+ el.parentNode.appendChild(next);
+ } else if (next.nodeType === Node.TEXT_NODE && !next.nodeValue) {
+ next.nodeValue = " ";
+ }
+ var range = document.createRange();
+ range.setStart(next, 0);
+ range.setEnd(next, 0);
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ };
+
+ // Inserting contacts into contenteditable: use mention UI
+ if (options.type === "contacts") {
+ toInsert = function (data, key) {
+ var avatar = h('span.cp-avatar', {
+ contenteditable: false
+ });
+ common.displayAvatar($(avatar), data.avatar, data.name);
+ return h('span.cp-mentions', {
+ 'data-curve': data.curvePublic,
+ 'data-notifications': data.notifications,
+ 'data-profile': data.profile,
+ 'data-name': Util.fixHTML(data.name),
+ 'data-avatar': data.avatar || "",
+ }, [
+ avatar,
+ h('span.cp-mentions-name', {
+ contenteditable: false
+ }, data.name)
+ ]);
+ };
+ }
+ }
+
+
+ // don't navigate away from the field on tab when selecting an item
+ $t.on("keydown", function(e) {
+ // Tab or enter
+ if ((e.which === 13 || e.which === 9)) {
+ try {
+ var visible = $t.autocomplete("instance").menu.activeMenu.is(':visible');
+ if (visible) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ } catch (e) {}
+ }
+ }).autocomplete({
+ minLength: 0,
+ source: function(data, cb) {
+ var term = data.term;
+ var results = [];
+ if (term.indexOf("@") >= 0) {
+ term = extractLast(data.term) || '';
+ results = Object.keys(sources).filter(function (key) {
+ var data = sources[key];
+ return data.name.toLowerCase().indexOf(term.toLowerCase()) !== -1;
+ }).map(function (key) {
+ var data = sources[key];
+ return {
+ label: data.name,
+ value: key
+ };
+ });
+ results.sort(sort);
+ }
+ cb(results);
+ },
+ focus: function() {
+ // prevent value inserted on focus
+ return false;
+ },
+ select: function(event, ui) {
+ // add the selected item
+ var key = ui.item.value;
+ var data = sources[key];
+ var value = toInsert(data, key);
+ insertValue(value);
+ return false;
+ }
+ }).autocomplete( "instance" )._renderItem = function( ul, item ) {
+ var key = item.value;
+ var obj = sources[key];
+ if (!obj) { return; }
+ var avatar = h('span.cp-avatar');
+ common.displayAvatar($(avatar), obj.avatar, obj.name);
+ var li = h('li.cp-autocomplete-value', [
+ avatar,
+ h('span', obj.name)
+ ]);
+ return $(li).appendTo(ul);
+ };
+ };
+
return UIElements;
});
diff --git a/www/common/notifications.js b/www/common/notifications.js
index 8546f2787..771a84c3c 100644
--- a/www/common/notifications.js
+++ b/www/common/notifications.js
@@ -315,6 +315,59 @@ define([
}
};
+ Messages.comments_notification = 'Replies to your comment "{0}" in {1}'; // XXX
+ Messages.unknownPad = "Unknown pad"; // XXX
+ handlers['COMMENT_REPLY'] = function (common, data) {
+ var content = data.content;
+ var msg = content.msg;
+
+ // Display the notification
+ //var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
+ var comment = Util.fixHTML(msg.content.comment).slice(0,20).trim();
+ if (msg.content.comment.length > 20) {
+ comment += '...';
+ }
+ var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
+ var href = msg.content.href;
+
+ content.getFormatText = function () {
+ return Messages._getKey('comments_notification', [comment, title]);
+ };
+ if (href) {
+ content.handler = function () {
+ common.openURL(href);
+ defaultDismiss(common, data)();
+ };
+ }
+ if (!content.archived) {
+ content.dismissHandler = defaultDismiss(common, data);
+ }
+ };
+
+ Messages.mentions_notification = '{0} has mentionned you in {1}'; // XXX
+ handlers['MENTION'] = function (common, data) {
+ var content = data.content;
+ var msg = content.msg;
+
+ // Display the notification
+ var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
+ var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
+ var href = msg.content.href;
+
+ content.getFormatText = function () {
+ return Messages._getKey('mentions_notification', [name, title]);
+ };
+ if (href) {
+ content.handler = function () {
+ common.openURL(href);
+ defaultDismiss(common, data)();
+ };
+ }
+ if (!content.archived) {
+ content.dismissHandler = defaultDismiss(common, data);
+ }
+ };
+
// NOTE: don't forget to fixHTML everything returned by "getFormatText"
diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js
index fec467fc5..194d375d2 100644
--- a/www/common/outer/mailbox-handlers.js
+++ b/www/common/outer/mailbox-handlers.js
@@ -527,6 +527,111 @@ define([
};
+ // Hide duplicates when receiving a SHARE_PAD notification:
+ // Keep only one notification per channel: the stronger and more recent one
+ var comments = {};
+ handlers['COMMENT_REPLY'] = function (ctx, box, data, cb) {
+ var msg = data.msg;
+ var hash = data.hash;
+ var content = msg.content;
+
+ if (Util.find(ctx.store.proxy, ['settings', 'pad', 'disableNotif'])) {
+ return void cb(true);
+ }
+
+ var channel = content.channel;
+ if (!channel) { return void cb(true); }
+ var res = ctx.store.manager.findChannel(channel);
+
+ var title, href;
+ // Check if the pad is in our drive
+ res.some(function (obj) {
+ if (!obj.data) { return; }
+ href = obj.data.href || obj.data.roHref;
+ title = obj.data.filename || obj.data.title;
+ return true;
+ });
+
+ // If we don't have the edit url, ignore this notification
+ if (!href) { return void cb(true); }
+
+ // Add the title
+ content.href = href;
+ content.title = title;
+
+ // Remove duplicates
+ var old = comments[channel];
+ var toRemove = old ? old.data : undefined;
+
+ // Update the data
+ comments[channel] = {
+ data: {
+ type: box.type,
+ hash: hash
+ }
+ };
+
+ cb(false, toRemove);
+ };
+ removeHandlers['COMMENT_REPLY'] = function (ctx, box, data, hash) {
+ var content = data.content;
+ var channel = content.channel;
+ var old = comments[channel];
+ if (old && old.data && old.data.hash === hash) {
+ delete comments[channel];
+ }
+ };
+
+ // Hide duplicates when receiving a SHARE_PAD notification:
+ // Keep only one notification per channel: the stronger and more recent one
+ var mentions = {};
+ handlers['MENTION'] = function (ctx, box, data, cb) {
+ var msg = data.msg;
+ var hash = data.hash;
+ var content = msg.content;
+
+ if (isMuted(ctx, data)) { return void cb(true); }
+
+ var channel = content.channel;
+ if (!channel) { return void cb(true); }
+ var res = ctx.store.manager.findChannel(channel);
+
+ var title, href;
+ // Check if the pad is in our drive
+ res.some(function (obj) {
+ if (!obj.data) { return; }
+ href = obj.data.href || obj.data.roHref;
+ title = obj.data.filename || obj.data.title;
+ return true;
+ });
+
+ // Add the title
+ content.href = href;
+ content.title = title;
+
+ // Remove duplicates
+ var old = mentions[channel];
+ var toRemove = old ? old.data : undefined;
+
+ // Update the data
+ mentions[channel] = {
+ data: {
+ type: box.type,
+ hash: hash
+ }
+ };
+
+ cb(false, toRemove);
+ };
+ removeHandlers['MENTION'] = function (ctx, box, data, hash) {
+ var content = data.content;
+ var channel = content.channel;
+ var old = mentions[channel];
+ if (old && old.data && old.data.hash === hash) {
+ delete mentions[channel];
+ }
+ };
+
return {
add: function (ctx, box, data, cb) {
diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js
index 24e7d0abd..5cccc6cba 100644
--- a/www/common/sframe-app-framework.js
+++ b/www/common/sframe-app-framework.js
@@ -289,9 +289,11 @@ define([
throw new Error("Content must be an object or array, type is " + typeof(content));
}
+ /*
if (padChange && hasChanged(content)) {
//cpNfInner.metadataMgr.addAuthor();
}
+ */
oldContent = content;
if (Array.isArray(content)) {
diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js
index 0eb35b824..5d4a630c1 100644
--- a/www/common/sframe-common.js
+++ b/www/common/sframe-common.js
@@ -103,6 +103,7 @@ define([
funcs.getBurnAfterReadingWarning = callWithCommon(UIElements.getBurnAfterReadingWarning);
funcs.createNewPadModal = callWithCommon(UIElements.createNewPadModal);
funcs.onServerError = callWithCommon(UIElements.onServerError);
+ funcs.addMentions = callWithCommon(UIElements.addMentions);
funcs.importMediaTagMenu = callWithCommon(MT.importMediaTagMenu);
funcs.getMediaTagPreview = callWithCommon(MT.getMediaTagPreview);
diff --git a/www/pad/comments.js b/www/pad/comments.js
index bdb65b95f..0133a47c4 100644
--- a/www/pad/comments.js
+++ b/www/pad/comments.js
@@ -80,6 +80,7 @@ define([
data.avatar = userData.avatar;
data.profile = userData.profile;
data.curvePublic = userData.curvePublic;
+ data.notifications = userData.notifications;
if (typeof(onChange) === "function" && Sortify(data) !== old) {
onChange();
}
@@ -92,17 +93,72 @@ define([
Env.metadataMgr.updateMetadata(md);
};
+ var sendReplyNotification = function (Env, uid) {
+ if (!Env.comments || !Env.comments.data || !Env.comments.authors) { return; }
+ if (!Env.common.isLoggedIn()) { return; }
+ var thread = Env.comments.data[uid];
+ if (!thread || !Array.isArray(thread.m)) { return; }
+ var userData = Env.metadataMgr.getUserData();
+ var privateData = Env.metadataMgr.getPrivateData();
+ var others = {};
+ // Get all the other registered users with a mailbox
+ thread.m.forEach(function (obj) {
+ var u = obj.u;
+ if (typeof(u) !== "number") { return; }
+ var author = Env.comments.authors[u];
+ if (!author || others[u] || !author.notifications || !author.curvePublic) { return; }
+ if (author.curvePublic === userData.curvePublic) { return; } // don't send to yourself
+ others[u] = {
+ curvePublic: author.curvePublic,
+ comment: obj.m,
+ content: obj.v,
+ notifications: author.notifications
+ };
+ });
+ // Send the notification
+ Object.keys(others).forEach(function (id) {
+ var data = others[id];
+ Env.common.mailbox.sendTo("COMMENT_REPLY", {
+ channel: privateData.channel,
+ comment: data.comment,
+ content: data.content
+ }, {
+ channel: data.notifications,
+ curvePublic: data.curvePublic
+ });
+ });
+
+ };
+
+ var cleanMentions = function ($el, full) {
+ $el.html('');
+ var el = $el[0];
+ var allowed = full ? ['data-profile', 'data-name', 'data-avatar', 'class']
+ : ['class'];
+ // Remove unnecessary/unsafe attributes
+ for (var i=el.attributes.length-1; i>0; i--) {
+ var name = el.attributes[i] && el.attributes[i].name;
+ if (allowed.indexOf(name) === -1) {
+ $el.removeAttr(name);
+ }
+ }
+ };
+
Messages.comments_submit = "Submit"; // XXX
Messages.comments_reply = "Reply"; // XXX
Messages.comments_resolve = "Resolve"; // XXX
-
var getCommentForm = function (Env, reply, _cb) {
var cb = Util.once(_cb);
var userData = Env.metadataMgr.getUserData();
var name = Util.fixHTML(userData.name || Messages.anonymous);
var avatar = h('span.cp-avatar');
- var textarea = h('textarea', {
- tabindex: 1
+ var textarea = h('div.cp-textarea', {
+ tabindex: 1,
+ role: 'textbox',
+ 'aria-multiline': true,
+ 'aria-labelledby': 'cp-comments-label',
+ 'aria-required': true,
+ contenteditable: true,
});
Env.common.displayAvatar($(avatar), userData.avatar, name);
@@ -119,25 +175,80 @@ define([
Messages.comments_submit
]);
+ // List of allowed attributes in mentions
$(submit).click(function (e) {
e.stopPropagation();
- cb(textarea.value);
+ var clone = textarea.cloneNode(true);
+ var notify = {};
+ var $clone = $(clone);
+ $clone.find('span.cp-mentions').each(function (i, el) {
+ var $el = $(el);
+ var curve = $el.attr('data-curve');
+ var notif = $el.attr('data-notifications');
+ cleanMentions($el, true);
+ if (!curve || !notif) { return; }
+ notify[curve] = notif;
+ });
+ $clone.find('> *:not(.cp-mentions)').remove();
+ var content = clone.innerHTML.trim();
+ if (!content) { return; }
+
+ // Send notification
+ var privateData = Env.metadataMgr.getPrivateData();
+ var userData = Env.metadataMgr.getUserData();
+ Object.keys(notify).forEach(function (curve) {
+ if (curve === userData.curvePublic) { return; }
+ Env.common.mailbox.sendTo("MENTION", {
+ channel: privateData.channel,
+ }, {
+ channel: notify[curve],
+ curvePublic: curve
+ });
+ });
+
+ // Push the content
+ cb(content);
});
$(cancel).click(function (e) {
e.stopPropagation();
cb();
});
- $(textarea).keydown(function (e) {
+ var $text = $(textarea).keydown(function (e) {
+ e.stopPropagation();
if (e.which === 27) {
$(cancel).click();
}
if (e.which === 13 && !e.shiftKey) {
+ // Submit form on Enter is the autocompelte menu is not visible
+ try {
+ var visible = $text.autocomplete("instance").menu.activeMenu.is(':visible');
+ if (visible) { return; }
+ } catch (e) {}
$(submit).click();
e.preventDefault();
}
+ }).click(function (e) {
+ e.stopPropagation();
});
+
+ if (Env.common.isLoggedIn()) {
+ var authors = {};
+ Object.keys((Env.comments && Env.comments.authors) || {}).forEach(function (id) {
+ var obj = Util.clone(Env.comments.authors[id]);
+ authors[obj.curvePublic] = obj;
+ });
+ Env.common.addMentions({
+ $input: $text,
+ contenteditable: true,
+ type: 'contacts',
+ sources: authors
+ });
+ }
+
+
+
setTimeout(function () {
$(textarea).focus();
});
@@ -168,6 +279,9 @@ define([
Env.$container.html('');
+ var label = h('label#cp-comments-label', Messages.comments_comment);
+ Env.$container.append(label);
+
var show = false;
if ($oldInput && !$oldInput.attr('data-uid')) {
@@ -207,6 +321,40 @@ define([
});
}
+ // Build sanitized html with mentions
+ var m = h('div.cp-comment-content');
+ m.innerHTML = msg.m;
+ var $m = $(m);
+ $m.find('> *:not(span.cp-mentions)').remove();
+ $m.find('span.cp-mentions').each(function (i, el) {
+ var $el = $(el);
+ var name = $el.attr('data-name');
+ var avatarUrl = $el.attr('data-avatar');
+ var profile = $el.attr('data-profile');
+ if (!name && !avatar && !profile) {
+ $el.remove();
+ return;
+ }
+ cleanMentions($el);
+ var avatar = h('span.cp-avatar');
+ Env.common.displayAvatar($(avatar), avatarUrl, name);
+ $el.append([
+ avatar,
+ h('span.cp-mentions-name', name)
+ ]);
+ if (profile) {
+ $el.attr('tabindex', 1);
+ $el.addClass('cp-mentions-clickable').click(function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ Env.common.openURL(Hash.hashToHref(profile, 'profile'));
+ }).focus(function (e) {
+ e.stopPropagation();
+ });
+ }
+ });
+
+ // Add the comment
content.push(h('div.cp-comment'+(i === 0 ? '' : '.cp-comment-reply'), [
h('div.cp-comment-header', [
avatar,
@@ -215,9 +363,7 @@ define([
h('span.cp-comment-time', date.toLocaleString())
])
]),
- h('div.cp-comment-content', [
- msg.m
- ])
+ m
]));
});
@@ -253,9 +399,9 @@ define([
e.stopPropagation();
$actions.hide();
var form = getCommentForm(Env, key, function (val) {
- $(form).remove();
$(form).closest('.cp-comment-container')
.find('.cp-comment-actions').css('display', '');
+ $(form).remove();
if (!val) { return; }
var obj = Env.comments.data[key];
@@ -276,6 +422,9 @@ define([
v: value
});
+ // Notify other users
+ sendReplyNotification(Env, key);
+
// Send to chainpad
updateMetadata(Env);
Env.framework.localChange();
@@ -356,7 +505,7 @@ define([
}
} else if (Env.ready) {
// Everytime there is a metadata change, check if our user data have changed
- // and puhs the update sif necessary
+ // and push the updates if necessary
updateAuthorData(Env, function () {
updateMetadata(Env);
Env.framework.localChange();
diff --git a/www/pad/icons/arrows-h.png b/www/pad/icons/arrows-h.png
new file mode 100644
index 000000000..a3002cf4d
Binary files /dev/null and b/www/pad/icons/arrows-h.png differ
diff --git a/www/pad/inner.js b/www/pad/inner.js
index c037f8d0d..c77b99713 100644
--- a/www/pad/inner.js
+++ b/www/pad/inner.js
@@ -397,7 +397,7 @@ define([
var addToolbarHideBtn = function (framework, $bar) {
// Expand / collapse the toolbar
var cfg = {
- element: $bar.find('.cke_toolbox_main')
+ element: $bar
};
var onClick = function (visible) {
framework._.sfCommon.setAttribute(['pad', 'showToolbar'], visible);
@@ -672,7 +672,7 @@ define([
});
if (!framework.isReadOnly()) {
- addToolbarHideBtn(framework, $contentContainer);
+ addToolbarHideBtn(framework, $('.cke_toolbox_main'));
} else {
$('.cke_toolbox_main').hide();
}
diff --git a/www/settings/inner.js b/www/settings/inner.js
index 5541277ed..9353b2c64 100644
--- a/www/settings/inner.js
+++ b/www/settings/inner.js
@@ -78,6 +78,7 @@ define([
'pad': [
'cp-settings-pad-width',
'cp-settings-pad-spellcheck',
+ 'cp-settings-pad-notif',
],
'code': [
'cp-settings-code-indent-unit',
@@ -1275,6 +1276,38 @@ define([
return $div;
};
+ // XXX Messages.settings_padNotifTitle
+ // XXX Messages.settings_padNotifHint
+ // XXX Messages.settings_padNotifCheckbox ("disable comments notifications")
+ Messages.settings_padNotifTitle = "Comments notifications"; // XXX
+ Messages.settings_padNotifHint = "Block notifications when someone replies to one of your comments"; // XXX
+ Messages.settings_padNotifCheckbox = "Disable comment notifications";
+ makeBlock('pad-notif', function (cb) {
+ var $cbox = $(UI.createCheckbox('cp-settings-pad-notif',
+ Messages.settings_padNotifCheckbox,
+ false, { label: {class: 'noTitle'} }));
+
+ var spinner = UI.makeSpinner($cbox);
+
+ // Checkbox: "Enable safe links"
+ var $checkbox = $cbox.find('input').on('change', function () {
+ spinner.spin();
+ var val = $checkbox.is(':checked');
+ common.setAttribute(['pad', 'disableNotif'], val, function () {
+ spinner.done();
+ });
+ });
+
+ common.getAttribute(['pad', 'disableNotif'], function (e, val) {
+ if (e) { return void console.error(e); }
+ if (val === true) {
+ $checkbox.attr('checked', 'checked');
+ }
+ });
+
+ cb($cbox);
+ }, true);
+
// Code settings
create['code-indent-unit'] = function () {