diff --git a/install/package.json b/install/package.json index 029693c979..852dab82b0 100644 --- a/install/package.json +++ b/install/package.json @@ -104,10 +104,10 @@ "nodebb-plugin-ntfy": "1.7.4", "nodebb-plugin-spam-be-gone": "2.2.2", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.2.57", + "nodebb-theme-harmony": "1.2.58", "nodebb-theme-lavender": "7.1.8", "nodebb-theme-peace": "2.2.5", - "nodebb-theme-persona": "13.3.20", + "nodebb-theme-persona": "13.3.21", "nodebb-widget-essentials": "7.0.16", "nodemailer": "6.9.13", "nprogress": "0.2.0", diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 571361734f..f8d2ca8933 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -36,7 +36,7 @@ define('forum/topic/postTools', [ if (!container) { return; } - $('[component="topic"]').on('show.bs.dropdown', '.moderator-tools', function () { + $('[component="topic"]').on('show.bs.dropdown', '[component="post/tools"]', function () { const $this = $(this); const dropdownMenu = $this.find('.dropdown-menu'); const { top } = this.getBoundingClientRect(); @@ -45,6 +45,10 @@ define('forum/topic/postTools', [ if (dropdownMenu.attr('data-loaded')) { return; } + dropdownMenu.html(helpers.generatePlaceholderWave([ + 3, 5, 9, 7, 10, 'divider', 10, + ])); + const postEl = $this.parents('[data-pid]'); const pid = postEl.attr('data-pid'); const index = parseInt(postEl.attr('data-index'), 10); diff --git a/public/src/client/topic/replies.js b/public/src/client/topic/replies.js index a70862c119..0fb2da8a38 100644 --- a/public/src/client/topic/replies.js +++ b/public/src/client/topic/replies.js @@ -8,21 +8,22 @@ define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts', 'api'], f const post = button.closest('[data-pid]'); const pid = post.data('pid'); const open = button.find('[component="post/replies/open"]'); - const loading = button.find('[component="post/replies/loading"]'); - const close = button.find('[component="post/replies/close"]'); - if (open.is(':not(.hidden)') && loading.is('.hidden')) { - open.addClass('hidden'); - loading.removeClass('hidden'); + if (open.attr('loading') !== '1' && open.attr('loaded') !== '1') { + open.attr('loading', '1') + .removeClass('fa-chevron-down') + .addClass('fa-spin fa-spinner'); + api.get(`/posts/${pid}/replies`, {}, function (err, { replies }) { const postData = replies; - loading.addClass('hidden'); + open.removeAttr('loading') + .attr('loaded', '1') + .removeClass('fa-spin fa-spinner') + .addClass('fa-chevron-up'); if (err) { - open.removeClass('hidden'); return alerts.error(err); } - close.removeClass('hidden'); postData.forEach((post, index) => { if (post) { post.index = index; @@ -50,10 +51,11 @@ define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts', 'api'], f hooks.fire('action:posts.loaded', { posts: postData }); }); }); - } else if (close.is(':not(.hidden)')) { - close.addClass('hidden'); - open.removeClass('hidden'); - loading.addClass('hidden'); + } else if (open.attr('loaded') === '1') { + open.removeAttr('loaded') + .removeAttr('loading') + .removeClass('fa-spin fa-spinner fa-chevron-up') + .addClass('fa-chevron-down'); post.find('[component="post/replies"]').slideUp('fast', function () { $(this).remove(); }); diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index 60dfd8b766..a66b293a73 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -11,7 +11,8 @@ define('forum/topic/threadTools', [ 'bootbox', 'alerts', 'bootstrap', -], function (components, translator, handleBack, posts, api, hooks, bootbox, alerts, bootstrap) { + 'helpers', +], function (components, translator, handleBack, posts, api, hooks, bootbox, alerts, bootstrap, helpers) { const ThreadTools = {}; ThreadTools.init = function (tid, topicContainer) { @@ -211,6 +212,7 @@ define('forum/topic/threadTools', [ if (dropdownMenu.attr('data-loaded')) { return; } + dropdownMenu.html(helpers.generatePlaceholderWave([8, 8, 8])); const data = await socket.emit('topics.loadTopicTools', { tid: ajaxify.data.tid, cid: ajaxify.data.cid }); const html = await app.parseAndTranslate('partials/topic/topic-menu-list', data); $(dropdownMenu).attr('data-loaded', 'true').html(html); diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index 6c17927918..c5533cf56b 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -25,12 +25,14 @@ module.exports = function (utils, Benchpress, relative_path) { userAgentIcons, buildAvatar, increment, + generateWroteReplied, generateRepliedTo, generateWrote, isoTimeToLocaleString, shouldHideReplyContainer, humanReadableNumber, formattedNumber, + generatePlaceholderWave, register, __escape: identity, }; @@ -295,34 +297,24 @@ module.exports = function (utils, Benchpress, relative_path) { if (!userObj) { userObj = this; } - + classNames = classNames || ''; const attributes = new Map([ - ['alt', userObj.username], ['title', userObj.username], ['data-uid', userObj.uid], - ['loading', 'lazy'], - ['aria-label', `[[aria:user-avatar-for, ${userObj.username}]]`], + ['class', `avatar ${classNames}${rounded ? ' avatar-rounded' : ''}`], ]); const styles = [`--avatar-size: ${size};`]; const attr2String = attributes => Array.from(attributes).reduce((output, [prop, value]) => { output += ` ${prop}="${value}"`; return output; }, ''); - classNames = classNames || ''; - - attributes.set('class', `avatar ${classNames}${rounded ? ' avatar-rounded' : ''}`); let output = ''; if (userObj.picture) { - attributes.set('component', component || 'avatar/picture'); - output += ''; + output += ``; } - - attributes.set('component', component || 'avatar/icon'); - styles.push('background-color: ' + userObj['icon:bgColor'] + ';'); - output += '' + userObj['icon:text'] + ''; - + output += `${userObj['icon:text']}`; return output; } @@ -330,6 +322,13 @@ module.exports = function (utils, Benchpress, relative_path) { return String(value + parseInt(inc, 10)); } + function generateWroteReplied(post, timeagoCutoff) { + if (post.toPid) { + return generateRepliedTo(post, timeagoCutoff); + } + return generateWrote(post, timeagoCutoff); + } + function generateRepliedTo(post, timeagoCutoff) { const displayname = post.parent && post.parent.displayname ? post.parent.displayname : '[[global:guest]]'; @@ -367,6 +366,21 @@ module.exports = function (utils, Benchpress, relative_path) { return utils.addCommas(number); } + function generatePlaceholderWave(items) { + const html = items.map((i) => { + if (i === 'divider') { + return ''; + } + return ` + `; + }); + + return html; + } + function register() { Object.keys(helpers).forEach(function (helperName) { Benchpress.registerHelper(helperName, helpers[helperName]); diff --git a/src/controllers/api.js b/src/controllers/api.js index 56b629f6db..a4d1f34291 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -89,7 +89,9 @@ apiController.loadConfig = async function (req) { size: meta.config.topicThumbSize, }, emailPrompt: meta.config.emailPrompt, - useragent: req.useragent, + useragent: { + isSafari: req.useragent.isSafari, + }, fontawesome: { pro: fontawesome_pro, styles: fontawesome_styles, diff --git a/src/meta/templates.js b/src/meta/templates.js index ec3c54882d..7661db48e8 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -114,7 +114,7 @@ async function compile() { let files = await plugins.getActive(); files = await getTemplateDirs(files); files = await getTemplateFiles(files); - + const minify = process.env.NODE_ENV !== 'development'; await Promise.all(Object.keys(files).map(async (name) => { const filePath = files[name]; let imported = await fs.promises.readFile(filePath, 'utf8'); @@ -122,6 +122,11 @@ async function compile() { await mkdirp(path.join(viewsPath, path.dirname(name))); + // remove empty lines and whitespace + if (minify) { + imported = imported.split('\n').map(line => line.trim()).filter(Boolean).join('\n'); + } + await fs.promises.writeFile(path.join(viewsPath, name), imported); const compiled = await Benchpress.precompile(imported, { filename: name }); await fs.promises.writeFile(path.join(viewsPath, name.replace(/\.tpl$/, '.js')), compiled);