Files
NodeBB/public/src/client/topic/threadTools.js
Barış Soner Uşaklı 7ba70d1561 Bootstrap5 (#10894)
* chore: up deps

* chore: up composer

* fix(deps): bump 2factor to v7

* chore: up harmony

* chore: up harmony

* fix: missing await

* feat: allow middlewares to pass in template values via res.locals

* feat: buildAccountData middleware automatically added ot all account routes

* fix: properly allow values in res.locals.templateValues to be added to the template data

* refactor: user/blocks

* refactor(accounts): categories and consent

* feat: automatically 404 if exposeUid or exposeGroupName come up empty

* refactor: remove calls to getUserDataByUserSlug for most account routes, since it is populated via middleware now

* fix: allow exposeUid and exposeGroupName to work with slugs with mixed capitalization

* fix: move reputation removal check to accountHelpers method

* test: skip i18n tests if ref branch when present is not develop

* fix(deps): bump theme versions

* fix(deps): bump ntfy and 2factor

* chore: up harmony

* fix: add missing return

* fix: #11191, only focus on search input on md environments and up

* feat: allow file uploads on mobile chat

closes https://github.com/NodeBB/NodeBB/issues/11217

* chore: up themes

* chore: add lang string

* fix(deps): bump ntfy to 1.0.15

* refactor: use new if/each syntax

* chore: up composer

* fix: regression from user helper refactor

* chore: up harmony

* chore: up composer

* chore: up harmony

* chore: up harmony

* chore: up harmony

* chore: fix composer version

* feat: add increment helper

* chore: up harmony

* fix: #11228 no timestamps in future 

* chore: up harmony

* check config.theme as well

fire action:posts.loaded after processing dom

* chore: up harmony

* chore: up harmony

* chore: up harmony

* chore: up themes

* chore: up harmony

* remove extra class

* refactor: move these to core from harmony

* chore: up widgets

* chore: up widgets

* height auto

* fix: closes #11238

* dont focus inputs, annoying on mobile

* fix: dont focus twice, only focus on chat input on desktop

dont wrap widget footer in row

* chore: up harmony

* chore: up harmony

* update chat window

* chore: up themes

* fix cache buster for skins

* chat fixes

* chore: up harmony

* chore: up composer

* refactor: change hook logs to debug

* fix: scroll to post right after adding to dom

* fix: hash scrolling and highlighting correct post

* test: re-enable read API schema tests

* fix: add back schema changes for 179faa2270 and c3920ccb10

* fix: schema changes from 488f0978a4

* fix: schema changes for f4cf482a87

* fix: schema update for be6bbabd0e

* fix: schema changes for 69c96078ea

* fix: schema changes for d1364c3130

* fix: schema changes for 84ff1152f7

* fix: schema changes for b860c2605c

* fix: schema changes for 23cb67a112

* fix: schema changes for b916e42f40

* fix: schema change for a9bbb586fc

* fix: schema changes for 4b738c8cd3

* fix: schema changes for 58b5781cea

* fix: schema changes for 794bf01b21

* fix: schema changes for 80ea12c1c1, e368feef51, and 52ead114be

* fix: composer-default object in config?

* fix: schema changes for 9acdc6808c and 0930934200

* fix: schema changes for c0a52924f1

* fix: schema change for aba420a3f3, move loggedInUser to optional props

* fix: schema changes for 8c67031609

* fix: schema changes for 27e53b42f3

* fix: schema changes for 2835966518

* fix: breaking test for email confirmation API call

* fix: schema changes for refactored search page

* fix: schema changes for user object

* fix: schema changes for 9f531f957e

* fix: schema changes for c4042c70de and 23175110a2

* fix: schema changes for 9b3616b103

* fix: schema changes for 5afd5de07d

* fix: schema change for 1d7baf1217

* fix: schema changes for 57bfb37c55 and be6bbabd0e

* fix: schema changes for 6e86b4afa2 and 3efad2e13b and 68f66223e7

* fix: allowing optional qs prop in pagination keys (not sure why this didn't break before)

* fix: re-login on email change

* fix: schema changes for c926358d73

* fix: schema changes for 388a8270c9

* fix: schema change for 2658bcc821

* fix: no need to call account middlewares for chats routes

* fix: schema changes for 71743affc3

* fix: final schema changes

* test: support for anyOf and oneOf

* fix: check thumb

* dont scroll to top on back press

* remove group log

* fix: add top margin to merged and deleted alerts

* chore: up widgets

* fix: improve fix-lists mixin

* chore: up harmony/composer

* feat: allow hiding quicksearch results during search

* dont record searches made by composer

* chore: up 54

* chore: up spam be gone

* feat: add prev/next page and page count into mobile paginator

* chore: up harmony

* chore: up harmony

* use old style for IS

* fix: hide entire toolbar row if no posts or not singlePost

* fix: updated messaging for post-queue template, #11206

* fix: btn-sm on post queue back button

* fix: bump harmony, closes #11206

* fix: remove unused alert module import

* fix: bump harmony

* fix: bump harmony

* chore: up harmony

* refactor: IS scrolltop

* fix: update users:search-user-for-chat source string

* feat: support for mark-read toggle on chats dropdown and recent chats list

* feat: api v3 calls to mark chat read/unread

* feat: send event:chats.mark socket event on mark read or unread

* refactor: allow frontend to mark chats as unread, use new API v3 routes instead of socket calls, better frontend event handling

* docs: openapi schema updates for chat marking

* fix: allow unread state toggling in chats dropdown too

* fix: issue where repeated openings of the chats dropdown would continually add events for mark-read/unread

* fix: debug log

* refactor: move userSearch filter to a module

* feat(routes): allow remounting /categories (#11230)

* feat: send flags count to frontend on flags list page

* refactor: filter form client-side js to extract out some logic

* fix: applyFilters to not take any arguments, update selectedCids in updateButton instead of onHidden

* fix: use userFilter module for assignee, reporterId, targetUid

* fix(openapi): schema changes for updated flags page

* fix: dont allow adding duplicates to userFilter

* use same var

* remove log

* fix: closes #11282

* feat: lang key for x-topics

* chore: up harmony

* chore: up emoji

* chore: up harmony

* fix: update userFilter to allow new option `selectedBlock`

* fix: wrong block name passed to userFilter

* fix: https://github.com/NodeBB/NodeBB/issues/11283

* fix: chats, allow multiple dropdowns like in harmony

* chore: up harmony

* refactor: flag note adding/editing, closes #11285

* fix: remove old prepareEdit logic

* chore: add caveat about hacky code block in userFilter module

* fix: placeholders for userFilter module

* refactor: navigator so it works with multiple thumbs/navigators

* chore: up harmony

* fix: closes #11287, destroy quick reply autocomplete

on navigation

* fix: filter disabled categories on user categories page count

* chore: up harmony

* docs: update openapi spec to include info about passing in timestamps for topic creation, removing timestamp as valid request param for topic replying

* fix: send back null values on ACP search dashboard for startDate and endDate if not expicitly passed in, fix tests

* fix: tweak table order in ACP dash searches

* fix: only invoke navigator click drag on left mouse button

* feat: add back unread indicator to navigator

* clear bookmark on mark unread

* fix: navigator crash on ajaxify

* better thumb top calculation

* fix: reset user bookmark when topic is marked unread

* Revert "fix: reset user bookmark when topic is marked unread"

This reverts commit 9bcd85c2c6.

* fix: update unread indicator on scroll, add unread count

* chore: bump harmony

* fix: crash on navigator unread update when backing out of a topic

* fix: closes #11183

* fix: update topics:recent zset when rescheduling a topic

* fix: dupe quote button, increase delay, hide immediately on empty selection

* fix: navigator not showing up on first load

* refactor: remove glance

assorted fixes to navigator
dont reduce remaning count if user scrolls down and up quickly
only call topic.navigatorCallback when index changes

* more sanity checks for bookmark

dont allow setting bookmark higher than topic postcount

* closes #11218, 🚋

* Revert "fix: update topics:recent zset when rescheduling a topic"

This reverts commit 737973cca9.

* fix: #11306, show proper error if queued post doesn't exist

was showing no-privileges if someone else accepted the post

* https://github.com/NodeBB/NodeBB/issues/11307

dont use li

* chore: up harmony

* chore: bump version string

* fix: copy paste fail

* feat: closes #7382, tag filtering

add client side support for filtering by tags on /category, /recent and /unread

* chore: up harmony

* chore: up harmony

* Revert "fix: add back req.query fallback for backwards compatibility" [breaking]

This reverts commit cf6cc2c454.
This commit is no longer required as passing in a CSRF token via query parameter is no longer supported as of NodeBB v3.x

This is a breaking change.

* fix: pass csrf token in form data, re: NodeBB/NodeBB#11309

* chore: up deps

* fix: tests, use x-csrf-token query param removed

* test: fix csrf_token

* lint: remove unused

* feat: add itemprop="image" to avatar helper

* fix: get chat upload button in chat modal

* breaking: remove deprecated socket.io methods

* test: update messaging tests to not use sockets

* fix: parent post links

* fix: prevent post tooltip if mouse leaves before data/tpl is loaded

* chore: up harmony

* chore: up harmony

* chore: up harmony

* chore: up harmony

* fix: nested replies indices

* fix(deps): bump 2factor

* feat: add loggedIn user to all api routes

* chore: up themes

* refactor: audit admin v3 write api routes as per #11321

* refactor: audit category v3 write api routes as per #11321 [breaking]

docs: fix open api spec for #11321

* refactor: audit chat v3 write api routes as per #11321

* refactor: audit files v3 write api routes as per #11321

* refactor: audit flags v3 write api routes as per #11321

* refactor: audit posts v3 write api routes as per #11321

* refactor: audit topics v3 write api routes as per #11321

* refactor: audit users v3 write api routes as per #11321

* fix: lang string

* remove min height

* fix: empty topic/labels taking up space

* fix: tag filtering when changing filter to watched topics

or changing popular time limit to month

* chore: up harmony

* fix: closes #11354, show no post error if queued post already accepted/rejected

* test: #11354

* test: #11354

* fix(deps): bump 2factor

* fix: #11357 clear cache on thumb remove

* fix: thumb remove on windows, closes #11357

* test: openapi for thumbs

* test: fix openapi

---------

Co-authored-by: Julian Lam <julian@nodebb.org>
Co-authored-by: Opliko <opliko.reg@protonmail.com>
2023-03-17 11:58:31 -04:00

422 lines
13 KiB
JavaScript

'use strict';
define('forum/topic/threadTools', [
'components',
'translator',
'handleBack',
'forum/topic/posts',
'api',
'hooks',
'bootbox',
'alerts',
'bootstrap',
], function (components, translator, handleBack, posts, api, hooks, bootbox, alerts, bootstrap) {
const ThreadTools = {};
ThreadTools.init = function (tid, topicContainer) {
renderMenu(topicContainer);
$('.topic-main-buttons [title]').tooltip({
container: '#content',
animation: false,
});
observeTopicLabels();
// function topicCommand(method, path, command, onComplete) {
topicContainer.on('click', '[component="topic/delete"]', function () {
topicCommand('del', '/state', 'delete');
return false;
});
topicContainer.on('click', '[component="topic/restore"]', function () {
topicCommand('put', '/state', 'restore');
return false;
});
topicContainer.on('click', '[component="topic/purge"]', function () {
topicCommand('del', '', 'purge');
return false;
});
topicContainer.on('click', '[component="topic/lock"]', function () {
topicCommand('put', '/lock', 'lock');
return false;
});
topicContainer.on('click', '[component="topic/unlock"]', function () {
topicCommand('del', '/lock', 'unlock');
return false;
});
topicContainer.on('click', '[component="topic/pin"]', function () {
topicCommand('put', '/pin', 'pin');
return false;
});
topicContainer.on('click', '[component="topic/unpin"]', function () {
topicCommand('del', '/pin', 'unpin');
return false;
});
topicContainer.on('click', '[component="topic/event/delete"]', function () {
const eventId = $(this).attr('data-topic-event-id');
const eventEl = $(this).parents('[component="topic/event"]');
bootbox.confirm('[[topic:delete-event-confirm]]', (ok) => {
if (ok) {
api.del(`/topics/${tid}/events/${eventId}`, {})
.then(function () {
eventEl.remove();
})
.catch(alerts.error);
}
});
});
// todo: should also use topicCommand, but no write api call exists for this yet
topicContainer.on('click', '[component="topic/mark-unread"]', function () {
socket.emit('topics.markUnread', tid, function (err) {
if (err) {
return alerts.error(err);
}
if (app.previousUrl && !app.previousUrl.match('^/topic')) {
ajaxify.go(app.previousUrl, function () {
handleBack.onBackClicked(true);
});
} else if (ajaxify.data.category) {
ajaxify.go('category/' + ajaxify.data.category.slug, handleBack.onBackClicked);
}
alerts.success('[[topic:mark_unread.success]]');
});
return false;
});
topicContainer.on('click', '[component="topic/mark-unread-for-all"]', function () {
const btn = $(this);
socket.emit('topics.markAsUnreadForAll', [tid], function (err) {
if (err) {
return alerts.error(err);
}
alerts.success('[[topic:markAsUnreadForAll.success]]');
btn.parents('.thread-tools.open').find('.dropdown-toggle').trigger('click');
});
return false;
});
topicContainer.on('click', '[component="topic/move"]', function () {
require(['forum/topic/move'], function (move) {
move.init([tid], ajaxify.data.cid);
});
return false;
});
topicContainer.on('click', '[component="topic/delete/posts"]', function () {
require(['forum/topic/delete-posts'], function (deletePosts) {
deletePosts.init();
});
});
topicContainer.on('click', '[component="topic/fork"]', function () {
require(['forum/topic/fork'], function (fork) {
fork.init();
});
});
topicContainer.on('click', '[component="topic/merge"]', function () {
require(['forum/topic/merge'], function (merge) {
merge.init(function () {
merge.addTopic(ajaxify.data.tid);
});
});
});
topicContainer.on('click', '[component="topic/move-posts"]', function () {
require(['forum/topic/move-post'], function (movePosts) {
movePosts.init();
});
});
topicContainer.on('click', '[component="topic/following"]', function () {
changeWatching('follow');
});
topicContainer.on('click', '[component="topic/not-following"]', function () {
changeWatching('follow', 0);
});
topicContainer.on('click', '[component="topic/ignoring"]', function () {
changeWatching('ignore');
});
function changeWatching(type, state = 1) {
const method = state ? 'put' : 'del';
api[method](`/topics/${tid}/${type}`, {}, () => {
let message = '';
if (type === 'follow') {
message = state ? '[[topic:following_topic.message]]' : '[[topic:not_following_topic.message]]';
} else if (type === 'ignore') {
message = state ? '[[topic:ignoring_topic.message]]' : '[[topic:not_following_topic.message]]';
}
// From here on out, type changes to 'unfollow' if state is falsy
if (!state) {
type = 'unfollow';
}
setFollowState(type);
alerts.alert({
alert_id: 'follow_thread',
message: message,
type: 'success',
timeout: 5000,
});
hooks.fire('action:topics.changeWatching', { tid: tid, type: type });
}, () => {
alerts.alert({
type: 'danger',
alert_id: 'topic_follow',
title: '[[global:please_log_in]]',
message: '[[topic:login_to_subscribe]]',
timeout: 5000,
});
});
return false;
}
};
function observeTopicLabels() {
// show or hide topic/labels container depending on children visibility
const labels = $('[component="topic/labels"]');
const mut = new MutationObserver(function (mutations) {
const first = mutations[0];
if (first && first.attributeName === 'class') {
const visibleChildren = labels.children().filter((index, el) => !$(el).hasClass('hidden'));
labels.toggleClass('hidden', !visibleChildren.length);
}
});
labels.children().each((index, el) => {
mut.observe(el, { attributes: true });
});
}
function renderMenu(container) {
container = container.get(0);
if (!container) {
return;
}
container.querySelectorAll('.thread-tools').forEach((toolsEl) => {
toolsEl.addEventListener('show.bs.dropdown', (e) => {
const dropdownMenu = e.target.nextElementSibling;
if (!dropdownMenu) {
return;
}
socket.emit('topics.loadTopicTools', { tid: ajaxify.data.tid, cid: ajaxify.data.cid }, function (err, data) {
if (err) {
return alerts.error(err);
}
app.parseAndTranslate('partials/topic/topic-menu-list', data, function (html) {
$(dropdownMenu).html(html);
hooks.fire('action:topic.tools.load', {
element: $(dropdownMenu),
});
});
});
}, {
once: true,
});
});
}
function topicCommand(method, path, command, onComplete) {
if (!onComplete) {
onComplete = function () {};
}
const tid = ajaxify.data.tid;
const body = {};
const execute = function (ok) {
if (ok) {
api[method](`/topics/${tid}${path}`, body)
.then(onComplete)
.catch(alerts.error);
}
};
switch (command) {
case 'delete':
case 'restore':
case 'purge':
bootbox.confirm(`[[topic:thread_tools.${command}_confirm]]`, execute);
break;
case 'pin':
ThreadTools.requestPinExpiry(body, execute.bind(null, true));
break;
default:
execute(true);
break;
}
}
ThreadTools.requestPinExpiry = function (body, onSuccess) {
app.parseAndTranslate('modals/set-pin-expiry', {}, function (html) {
const modal = bootbox.dialog({
title: '[[topic:thread_tools.pin]]',
message: html,
onEscape: true,
size: 'small',
buttons: {
cancel: {
label: '[[modules:bootbox.cancel]]',
className: 'btn-link',
},
save: {
label: '[[global:save]]',
className: 'btn-primary',
callback: function () {
const expiryEl = modal.get(0).querySelector('#expiry');
let expiry = expiryEl.value;
// No expiry set
if (expiry === '') {
return onSuccess();
}
// Expiration date set
expiry = new Date(expiry);
if (expiry && expiry.getTime() > Date.now()) {
body.expiry = expiry.getTime();
onSuccess();
} else {
alerts.error('[[error:invalid-date]]');
}
},
},
},
});
});
};
ThreadTools.setLockedState = function (data) {
const threadEl = components.get('topic');
if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) {
return;
}
const isLocked = data.isLocked && !ajaxify.data.privileges.isAdminOrMod;
components.get('topic/lock').toggleClass('hidden', data.isLocked).parent().attr('hidden', data.isLocked ? '' : null);
components.get('topic/unlock').toggleClass('hidden', !data.isLocked).parent().attr('hidden', !data.isLocked ? '' : null);
const hideReply = !!((data.isLocked || ajaxify.data.deleted) && !ajaxify.data.privileges.isAdminOrMod);
components.get('topic/reply/container').toggleClass('hidden', hideReply);
components.get('topic/reply/locked').toggleClass('hidden', ajaxify.data.privileges.isAdminOrMod || !data.isLocked || ajaxify.data.deleted);
threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply);
threadEl.find('[component="post/edit"], [component="post/delete"]').toggleClass('hidden', isLocked);
threadEl.find('[component="post"][data-uid="' + app.user.uid + '"].deleted [component="post/tools"]').toggleClass('hidden', isLocked);
$('[component="topic/labels"] [component="topic/locked"]').toggleClass('hidden', !data.isLocked);
$('[component="post/tools"] .dropdown-menu').html('');
ajaxify.data.locked = data.isLocked;
posts.addTopicEvents(data.events);
};
ThreadTools.setDeleteState = function (data) {
const threadEl = components.get('topic');
if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) {
return;
}
components.get('topic/delete').toggleClass('hidden', data.isDelete).parent().attr('hidden', data.isDelete ? '' : null);
components.get('topic/restore').toggleClass('hidden', !data.isDelete).parent().attr('hidden', !data.isDelete ? '' : null);
components.get('topic/purge').toggleClass('hidden', !data.isDelete).parent().attr('hidden', !data.isDelete ? '' : null);
components.get('topic/deleted/message').toggleClass('hidden', !data.isDelete);
if (data.isDelete) {
app.parseAndTranslate('partials/topic/deleted-message', {
deleter: data.user,
deleted: true,
deletedTimestampISO: utils.toISOString(Date.now()),
}, function (html) {
components.get('topic/deleted/message').replaceWith(html);
html.find('.timeago').timeago();
});
}
const hideReply = data.isDelete && !ajaxify.data.privileges.isAdminOrMod;
components.get('topic/reply/container').toggleClass('hidden', hideReply);
components.get('topic/reply/locked').toggleClass('hidden', ajaxify.data.privileges.isAdminOrMod || !ajaxify.data.locked || data.isDelete);
threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply);
threadEl.toggleClass('deleted', data.isDelete);
ajaxify.data.deleted = data.isDelete ? 1 : 0;
posts.addTopicEvents(data.events);
};
ThreadTools.setPinnedState = function (data) {
const threadEl = components.get('topic');
if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) {
return;
}
components.get('topic/pin').toggleClass('hidden', data.pinned).parent().attr('hidden', data.pinned ? '' : null);
components.get('topic/unpin').toggleClass('hidden', !data.pinned).parent().attr('hidden', !data.pinned ? '' : null);
const icon = $('[component="topic/labels"] [component="topic/pinned"]');
icon.toggleClass('hidden', !data.pinned);
if (data.pinned) {
icon.translateAttr('title', (
data.pinExpiry && data.pinExpiryISO ?
'[[topic:pinned-with-expiry, ' + data.pinExpiryISO + ']]' :
'[[topic:pinned]]'
));
}
ajaxify.data.pinned = data.pinned;
posts.addTopicEvents(data.events);
};
function setFollowState(state) {
const titles = {
follow: '[[topic:watching]]',
unfollow: '[[topic:not-watching]]',
ignore: '[[topic:ignoring]]',
};
translator.translate(titles[state], function (translatedTitle) {
const tooltip = bootstrap.Tooltip.getInstance('[component="topic/watch"]');
if (tooltip) {
tooltip.setContent({ '.tooltip-inner': translatedTitle });
}
});
let menu = components.get('topic/following/menu');
menu.toggleClass('hidden', state !== 'follow');
components.get('topic/following/check').toggleClass('fa-check', state === 'follow');
menu = components.get('topic/not-following/menu');
menu.toggleClass('hidden', state !== 'unfollow');
components.get('topic/not-following/check').toggleClass('fa-check', state === 'unfollow');
menu = components.get('topic/ignoring/menu');
menu.toggleClass('hidden', state !== 'ignore');
components.get('topic/ignoring/check').toggleClass('fa-check', state === 'ignore');
}
return ThreadTools;
});