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 () {