diff --git a/node_modules/nodebb-plugin-composer-default/static/lib/composer.js b/node_modules/nodebb-plugin-composer-default/static/lib/composer.js index 336f6a5543..0e665bb01f 100644 --- a/node_modules/nodebb-plugin-composer-default/static/lib/composer.js +++ b/node_modules/nodebb-plugin-composer-default/static/lib/composer.js @@ -23,6 +23,892 @@ define('composer', [ 'search', 'screenfull', ], function (taskbar, translator, uploads, formatting, drafts, tags, + + categoryList, preview, resize, autocomplete, scheduler, postQueue, scrollStop, + topicThumbs, api, bootbox, alerts, hooks, messagesModule, search, screenfull) { + var composer = { + active: undefined, + posts: {}, + bsEnvironment: undefined, + formatting: undefined, + }; + + var isAnonymous = false; + var best = false; + + $(window).off('resize', onWindowResize).on('resize', onWindowResize); + onWindowResize(); + + $(window).on('action:composer.topics.post', function (ev, data) { + localStorage.removeItem('category:' + data.data.cid + ':bookmark'); + localStorage.removeItem('category:' + data.data.cid + ':bookmark:clicked'); + }); + + $(window).on('popstate', function () { + var env = utils.findBootstrapEnvironment(); + if (composer.active && (env === 'xs' || env === 'sm')) { + if (!composer.posts[composer.active].modified) { + composer.discard(composer.active); + if (composer.discardConfirm && composer.discardConfirm.length) { + composer.discardConfirm.modal('hide'); + delete composer.discardConfirm; + } + return; + } + + translator.translate('[[modules:composer.discard]]', function (translated) { + composer.discardConfirm = bootbox.confirm(translated, function (confirm) { + if (confirm) { + composer.discard(composer.active); + } else { + composer.posts[composer.active].modified = true; + } + }); + composer.posts[composer.active].modified = false; + }); + } + }); + + function removeComposerHistory() { + var env = composer.bsEnvironment; + if (ajaxify.data.template.compose === true || env === 'xs' || env === 'sm') { + history.back(); + } + } + + function onWindowResize() { + var env = utils.findBootstrapEnvironment(); + var isMobile = env === 'xs' || env === 'sm'; + + if (preview.toggle) { + if (preview.env !== env && isMobile) { + preview.env = env; + preview.toggle(false); + } + preview.env = env; + } + + if (composer.active !== undefined) { + resize.reposition($('.composer[data-uuid="' + composer.active + '"]')); + + if (!isMobile && window.location.pathname.startsWith(config.relative_path + '/compose')) { + /* + * If this conditional is met, we're no longer in mobile/tablet + * resolution but we've somehow managed to have a mobile + * composer load, so let's go back to the topic + */ + history.back(); + } else if (isMobile && !window.location.pathname.startsWith(config.relative_path + '/compose')) { + /* + * In this case, we're in mobile/tablet resolution but the composer + * that loaded was a regular composer, so let's fix the address bar + */ + mobileHistoryAppend(); + } + } + composer.bsEnvironment = env; + } + + function alreadyOpen(post) { + // If a composer for the same cid/tid/pid is already open, return the uuid, else return bool false + var type; + var id; + + if (post.hasOwnProperty('cid')) { + type = 'cid'; + } else if (post.hasOwnProperty('tid')) { + type = 'tid'; + } else if (post.hasOwnProperty('pid')) { + type = 'pid'; + } + + id = post[type]; + + // Find a match + for (var uuid in composer.posts) { + if (composer.posts[uuid].hasOwnProperty(type) && id === composer.posts[uuid][type]) { + return uuid; + } + } + + // No matches... + return false; + } + + function push(post) { + if (!post) { + return; + } + + var uuid = utils.generateUUID(); + var existingUUID = alreadyOpen(post); + + if (existingUUID) { + taskbar.updateActive(existingUUID); + return composer.load(existingUUID); + } + + var actionText = '[[topic:composer.new-topic]]'; + if (post.action === 'posts.reply') { + actionText = '[[topic:composer.replying-to]]'; + } else if (post.action === 'posts.edit') { + actionText = '[[topic:composer.editing-in]]'; + } + + translator.translate(actionText, function (translatedAction) { + taskbar.push('composer', uuid, { + title: translatedAction.replace('%1', '"' + post.title + '"'), + }); + }); + + composer.posts[uuid] = post; + composer.load(uuid); + } + + async function composerAlert(post_uuid, message) { + $('.composer[data-uuid="' + post_uuid + '"]').find('.composer-submit').removeAttr('disabled'); + + const { showAlert } = await hooks.fire('filter:composer.error', { post_uuid, message, showAlert: true }); + + if (showAlert) { + alerts.alert({ + type: 'danger', + timeout: 10000, + title: '', + message: message, + alert_id: 'post_error', + }); + } + } + + composer.findByTid = function (tid) { + // Iterates through the initialised composers and returns the uuid of the matching composer + for (var uuid in composer.posts) { + if (composer.posts.hasOwnProperty(uuid) && composer.posts[uuid].hasOwnProperty('tid') && parseInt(composer.posts[uuid].tid, 10) === parseInt(tid, 10)) { + return uuid; + } + } + + return null; + }; + + composer.addButton = function (iconClass, onClick, title) { + formatting.addButton(iconClass, onClick, title); + }; + composer.newTopic = async (data) => { + let pushData = { + save_id: data.save_id, + action: 'topics.post', + cid: data.cid, + handle: data.handle, + title: data.title || '', + body: data.body || '', + tags: data.tags || [], + modified: !!((data.title && data.title.length) || (data.body && data.body.length)), + isMain: true, + isAnonymous: isAnonymous, // Add isAnonymous here + best: best, + }; + + ({ pushData } = await hooks.fire('filter:composer.topic.push', { + data: data, + pushData: pushData, + })); + + push(pushData); + }; + + composer.newReply = function (data) { + translator.translate(data.body, config.defaultLang, function (translated) { + push({ + save_id: data.save_id, + action: 'posts.reply', + tid: data.tid, + toPid: data.toPid, + title: data.title, + body: translated, + modified: !!(translated && translated.length), + isMain: false, + isAnonymous: isAnonymous, // Add isAnonymous here + best: best, + }); + }); + }; + + composer.editPost = function (data) { + socket.emit('plugins.composer.push', data.pid, function (err, postData) { + if (err) { + return alerts.error(err); + } + postData.save_id = data.save_id; + postData.action = 'posts.edit'; + postData.pid = data.pid; + postData.modified = false; + if (data.body) { + postData.body = data.body; + postData.modified = true; + } + if (data.title) { + postData.title = data.title; + postData.modified = true; + } + postData.isAnonymous = isAnonymous; + postData.best = best; // Add isAnonymous here + push(postData); + }); + }; + + + composer.addQuote = function (data) { + // tid, toPid, selectedPid, title, username, text, uuid + data.uuid = data.uuid || composer.active; + + var escapedTitle = (data.title || '') + .replace(/([\\`*_{}[\]()#+\-.!])/g, '\\$1') + .replace(/\[/g, '[') + .replace(/\]/g, ']') + .replace(/%/g, '%') + .replace(/,/g, ','); + + if (data.body) { + data.body = '> ' + data.body.replace(/\n/g, '\n> ') + '\n\n'; + } + var link = '[' + escapedTitle + '](' + config.relative_path + '/post/' + encodeURIComponent(data.selectedPid || data.toPid) + ')'; + if (data.uuid === undefined) { + if (data.title && (data.selectedPid || data.toPid)) { + composer.newReply({ + tid: data.tid, + toPid: data.toPid, + title: data.title, + body: '[[modules:composer.user-said-in, ' + data.username + ', ' + link + ']]\n' + data.body, + }); + } else { + composer.newReply({ + tid: data.tid, + toPid: data.toPid, + title: data.title, + body: '[[modules:composer.user-said, ' + data.username + ']]\n' + data.body, + }); + } + return; + } else if (data.uuid !== composer.active) { + // If the composer is not currently active, activate it + composer.load(data.uuid); + } + + var postContainer = $('.composer[data-uuid="' + data.uuid + '"]'); + var bodyEl = postContainer.find('textarea'); + var prevText = bodyEl.val(); + if (data.title && (data.selectedPid || data.toPid)) { + translator.translate('[[modules:composer.user-said-in, ' + data.username + ', ' + link + ']]\n', config.defaultLang, onTranslated); + } else { + translator.translate('[[modules:composer.user-said, ' + data.username + ']]\n', config.defaultLang, onTranslated); + } + + function onTranslated(translated) { + composer.posts[data.uuid].body = (prevText.length ? prevText + '\n\n' : '') + translated + data.body; + bodyEl.val(composer.posts[data.uuid].body); + focusElements(postContainer); + preview.render(postContainer); + } + }; + + composer.load = function (post_uuid) { + var postContainer = $('.composer[data-uuid="' + post_uuid + '"]'); + if (postContainer.length) { + activate(post_uuid); + resize.reposition(postContainer); + focusElements(postContainer); + onShow(); + } else if (composer.formatting) { + createNewComposer(post_uuid); + } else { + socket.emit('plugins.composer.getFormattingOptions', function (err, options) { + if (err) { + return alerts.error(err); + } + composer.formatting = options; + createNewComposer(post_uuid); + }); + } + }; + + composer.enhance = function (postContainer, post_uuid, postData) { + /* + This method enhances a composer container with client-side sugar (preview, etc) + Everything in here also applies to the /compose route + */ + + if (!post_uuid && !postData) { + post_uuid = utils.generateUUID(); + composer.posts[post_uuid] = ajaxify.data; + postData = ajaxify.data; + postContainer.attr('data-uuid', post_uuid); + } + + categoryList.init(postContainer, composer.posts[post_uuid]); + scheduler.init(postContainer, composer.posts); + + formatting.addHandler(postContainer); + formatting.addComposerButtons(); + preview.handleToggler(postContainer); + postQueue.showAlert(postContainer, postData); + uploads.initialize(post_uuid); + tags.init(postContainer, composer.posts[post_uuid]); + autocomplete.init(postContainer, post_uuid); + + postContainer.on('change', 'input, textarea', function () { + composer.posts[post_uuid].modified = true; + }); + + postContainer.on('change', '#anonymousToggle', function () { + + isAnonymous = $(this).is(':checked'); + }); + + postContainer.on('click', '.composer-submit', function (e) { + e.preventDefault(); + e.stopPropagation(); // Other click events bring composer back to active state which is undesired on submit + + $(this).attr('disabled', true); + post(post_uuid); + }); + + require(['mousetrap'], function (mousetrap) { + mousetrap(postContainer.get(0)).bind('mod+enter', function () { + postContainer.find('.composer-submit').attr('disabled', true); + post(post_uuid); + }); + }); + + postContainer.find('.composer-discard').on('click', function (e) { + e.preventDefault(); + + if (!composer.posts[post_uuid].modified) { + composer.discard(post_uuid); + return removeComposerHistory(); + } + + formatting.exitFullscreen(); + + var btn = $(this).prop('disabled', true); + translator.translate('[[modules:composer.discard]]', function (translated) { + bootbox.confirm(translated, function (confirm) { + if (confirm) { + composer.discard(post_uuid); + removeComposerHistory(); + } + btn.prop('disabled', false); + }); + }); + }); + + postContainer.find('.composer-minimize, .minimize .trigger').on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + composer.minimize(post_uuid); + }); + + const textareaEl = postContainer.find('textarea'); + textareaEl.on('input propertychange', utils.debounce(function () { + preview.render(postContainer); + }, 250)); + + textareaEl.on('scroll', function () { + preview.matchScroll(postContainer); + }); + + drafts.init(postContainer, postData); + const draft = drafts.get(postData.save_id); + + preview.render(postContainer, function () { + preview.matchScroll(postContainer); + }); + + handleHelp(postContainer); + handleSearch(postContainer); + focusElements(postContainer); + if (postData.action === 'posts.edit') { + composer.updateThumbCount(post_uuid, postContainer); + } + + // Hide "zen mode" if fullscreen API is not enabled/available (ahem, iOS...) + if (!screenfull.isEnabled) { + $('[data-format="zen"]').parent().addClass('hidden'); + } + + hooks.fire('action:composer.enhanced', { postContainer, postData, draft }); + }; + + async function getSelectedCategory(postData) { + if (ajaxify.data.template.category && parseInt(postData.cid, 10) === parseInt(ajaxify.data.cid, 10)) { + // no need to load data if we are already on the category page + return ajaxify.data; + } else if (parseInt(postData.cid, 10)) { + return await api.get(`/api/category/${postData.cid}`, {}); + } + return null; + } + + async function createNewComposer(post_uuid) { + var postData = composer.posts[post_uuid]; + + var isTopic = postData ? postData.hasOwnProperty('cid') : false; + var isMain = postData ? !!postData.isMain : false; + var isEditing = postData ? !!postData.pid : false; + var isGuestPost = postData ? parseInt(postData.uid, 10) === 0 : false; + const isScheduled = postData.timestamp > Date.now(); + + // see + // https://github.com/NodeBB/NodeBB/issues/2994 and + // https://github.com/NodeBB/NodeBB/issues/1951 + // remove when 1951 is resolved + + var title = postData.title.replace(/%/g, '%').replace(/,/g, ','); + postData.category = await getSelectedCategory(postData); + const privileges = postData.category ? postData.category.privileges : ajaxify.data.privileges; + var data = { + topicTitle: title, + titleLength: title.length, + body: translator.escape(utils.escapeHTML(postData.body)), + mobile: composer.bsEnvironment === 'xs' || composer.bsEnvironment === 'sm', + resizable: true, + thumb: postData.thumb, + isTopicOrMain: isTopic || isMain, + maximumTitleLength: config.maximumTitleLength, + maximumPostLength: config.maximumPostLength, + minimumTagLength: config.minimumTagLength, + maximumTagLength: config.maximumTagLength, + 'composer:showHelpTab': config['composer:showHelpTab'], + isTopic: isTopic, + isEditing: isEditing, + canSchedule: !!(isMain && privileges && + ((privileges['topics:schedule'] && !isEditing) || (isScheduled && privileges.view_scheduled))), + showHandleInput: config.allowGuestHandles && + (app.user.uid === 0 || (isEditing && isGuestPost && app.user.isAdmin)), + handle: postData ? postData.handle || '' : undefined, + formatting: composer.formatting, + tagWhitelist: postData.category ? postData.category.tagWhitelist : ajaxify.data.tagWhitelist, + privileges: app.user.privileges, + selectedCategory: postData.category, + submitOptions: [ + // Add items using `filter:composer.create`, or just add them to the