Merge branch 'master' into develop

This commit is contained in:
Barış Soner Uşaklı
2026-01-20 20:15:07 -05:00
9 changed files with 38 additions and 99 deletions

View File

@@ -11,6 +11,7 @@ define('forum/chats', [
'forum/chats/user-list',
'forum/chats/message-search',
'forum/chats/pinned-messages',
'forum/chats/events',
'autocomplete',
'hooks',
'bootbox',
@@ -21,15 +22,14 @@ define('forum/chats', [
], function (
components, mousetrap, recentChats, create,
manage, messages, userList, messageSearch, pinnedMessages,
autocomplete, hooks, bootbox, alerts, chatModule, api,
uploadHelpers
events, autocomplete, hooks, bootbox, alerts, chatModule,
api, uploadHelpers
) {
const Chats = {
initialised: false,
activeAutocomplete: {},
newMessage: false,
};
let newMessage = false;
let chatNavWrapper = null;
$(window).on('action:ajaxify.start', function () {
@@ -54,10 +54,9 @@ define('forum/chats', [
socket.emit('modules.chats.enterPublic', ajaxify.data.publicRooms.map(r => r.roomId));
const env = utils.findBootstrapEnvironment();
chatNavWrapper = $('[component="chat/nav-wrapper"]');
if (!Chats.initialised) {
Chats.addSocketListeners();
Chats.addGlobalEventListeners();
}
Chats.addSocketListeners();
Chats.addGlobalEventListeners();
recentChats.init();
@@ -68,7 +67,6 @@ define('forum/chats', [
Chats.addHotkeys();
}
Chats.initialised = true;
const chatContentEl = $('[component="chat/message/content"]');
messages.wrapImagesInLinks(chatContentEl);
if (ajaxify.data.scrollToIndex) {
@@ -647,89 +645,22 @@ define('forum/chats', [
};
Chats.addGlobalEventListeners = function () {
$(window).on('mousemove keypress click', function () {
if (newMessage && ajaxify.data.roomId) {
api.del(`/chats/${ajaxify.data.roomId}/state`, {});
newMessage = false;
}
});
$(window).off('mousemove keypress click', onUserInteraction)
.on('mousemove keypress click', onUserInteraction);
};
function onUserInteraction() {
if (Chats.newMessage && ajaxify.data.roomId) {
// mark current room read on user interaction
api.del(`/chats/${ajaxify.data.roomId}/state`, {});
Chats.newMessage = false;
}
}
Chats.addSocketListeners = function () {
socket.on('event:chats.receive', function (data) {
if (chatModule.isFromBlockedUser(data.fromUid)) {
return;
}
if (parseInt(data.roomId, 10) === parseInt(ajaxify.data.roomId, 10)) {
data.self = parseInt(app.user.uid, 10) === parseInt(data.fromUid, 10) ? 1 : 0;
if (!newMessage) {
newMessage = data.self === 0;
}
data.message.self = data.self;
data.message.timestamp = Math.min(Date.now(), data.message.timestamp);
data.message.timestampISO = utils.toISOString(data.message.timestamp);
messages.appendChatMessage($('[component="chat/message/content"]'), data.message);
Chats.updateTeaser(data.roomId, {
content: utils.stripHTMLTags(utils.decodeHTMLEntities(data.message.content)),
user: data.message.fromUser,
timestampISO: data.message.timestampISO,
});
}
});
socket.on('event:chats.public.unread', function (data) {
if (
chatModule.isFromBlockedUser(data.fromUid) ||
chatModule.isLookingAtRoom(data.roomId) ||
app.user.uid === parseInt(data.fromUid, 10)
) {
return;
}
Chats.markChatPageElUnread(data);
Chats.increasePublicRoomUnreadCount(chatNavWrapper.find('[data-roomid=' + data.roomId + ']'));
});
socket.on('event:user_status_change', function (data) {
app.updateUserStatus($('.chats-list [data-uid="' + data.uid + '"] [component="user/status"]'), data.status);
});
events.init();
messages.addSocketListeners();
socket.on('event:chats.roomRename', function (data) {
const roomEl = components.get('chat/recent/room', data.roomId);
if (roomEl.length) {
const titleEl = roomEl.find('[component="chat/room/title"]');
ajaxify.data.roomName = data.newName;
titleEl.translateText(data.newName ? data.newName : ajaxify.data.usernames);
}
const titleEl = $(`[component="chat/main-wrapper"][data-roomid="${data.roomId}"] [component="chat/header/title"]`);
if (titleEl.length) {
titleEl.html(
data.newName ?
`<i class="fa ${ajaxify.data.icon} text-muted"></i> ${data.newName}` :
ajaxify.data.chatWithMessage
);
}
});
socket.on('event:chats.mark', ({ roomId, state }) => {
const roomEls = $(`[component="chat/recent"] [data-roomid="${roomId}"], [component="chat/list"] [data-roomid="${roomId}"], [component="chat/public"] [data-roomid="${roomId}"]`);
roomEls.each((idx, el) => {
const roomEl = $(el);
chatModule.markChatElUnread(roomEl, state === 1);
if (state === 0) {
Chats.updatePublicRoomUnreadCount(roomEl, 0);
}
});
});
socket.on('event:chats.typing', async (data) => {
if (data.uid === app.user.uid || chatModule.isFromBlockedUser(data.uid)) {
return;
}
chatModule.updateTypingUserList($(`[component="chat/main-wrapper"][data-roomid="${data.roomId}"]`), data);
});
};
Chats.updateTeaser = async function (roomId, teaser) {

View File

@@ -301,7 +301,7 @@ const utils = {
return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
},
stripBidiControls: function (input) {
return input.replace(/[\u202A-\u202E\u2066-\u2069]/g, '');
return input.replace(/[\u202A-\u202E\u2066-\u2069]/gi, '');
},
cleanUpTag: function (tag, maxLength) {
if (typeof tag !== 'string' || !tag.length) {

View File

@@ -17,12 +17,14 @@ module.exports = function (Categories) {
await async.eachLimit(tids, 10, async (tid) => {
await topics.purgePostsAndTopic(tid, uid);
});
await db.sortedSetRemove(`cid:${cid}:tids`, tids);
}, { alwaysStartAt: 0 });
const pinnedTids = await db.getSortedSetRevRange(`cid:${cid}:tids:pinned`, 0, -1);
await async.eachLimit(pinnedTids, 10, async (tid) => {
await topics.purgePostsAndTopic(tid, uid);
});
await db.sortedSetRemove(`cid:${cid}:tids:pinned`, pinnedTids);
const categoryData = await Categories.getCategoryData(cid);
await purgeCategory(cid, categoryData);
plugins.hooks.fire('action:category.delete', { cid: cid, uid: uid, category: categoryData });

View File

@@ -261,6 +261,7 @@ events.deleteEvents = async function (eids) {
events.deleteAll = async function () {
await batch.processSortedSet('events:time', async (eids) => {
await events.deleteEvents(eids);
await db.sortedSetRemove('events:time', eids);
}, { alwaysStartAt: 0, batch: 500 });
};

View File

@@ -376,9 +376,9 @@ function sortPosts(posts, data) {
} else {
posts.sort((p1, p2) => {
if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) {
return direction;
} else if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) {
return -direction;
} else if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) {
return direction;
}
return 0;
});

View File

@@ -73,14 +73,11 @@ module.exports = function (Topics) {
};
Topics.purge = async function (tid, uid) {
const [deletedTopic, tags] = await Promise.all([
Topics.getTopicData(tid),
Topics.getTopicTags(tid),
]);
const deletedTopic = await Topics.getTopicData(tid);
if (!deletedTopic) {
return;
}
deletedTopic.tags = tags;
deletedTopic.tags = deletedTopic.tags.map(tag => tag.value);
await deleteFromFollowersIgnorers(tid);
await Promise.all([

View File

@@ -333,7 +333,7 @@ module.exports = function (User) {
user.displayname = validator.escape(String(
meta.config.showFullnameAsDisplayName && showfullname && user.fullname ?
user.fullname :
utils.stripBidiControls(user.fullname) :
user.username
));
}

View File

@@ -43,15 +43,17 @@ module.exports = function (User) {
async function deletePosts(callerUid, uid) {
await batch.processSortedSet(`uid:${uid}:posts`, async (pids) => {
await posts.purge(pids, callerUid);
await db.sortedSetRemove(`uid:${uid}:posts`, pids);
}, { alwaysStartAt: 0, batch: 500 });
}
async function deleteTopics(callerUid, uid) {
await batch.processSortedSet(`uid:${uid}:topics`, async (ids) => {
await async.eachSeries(ids, async (tid) => {
await batch.processSortedSet(`uid:${uid}:topics`, async (tids) => {
await async.eachSeries(tids, async (tid) => {
await topics.purge(tid, callerUid);
});
}, { alwaysStartAt: 0 });
await db.sortedSetRemove(`uid:${uid}:topics`, tids);
}, { alwaysStartAt: 0, batch: 100 });
}
async function deleteUploads(callerUid, uid) {

View File

@@ -51,6 +51,12 @@ describe('Utility Methods', () => {
assert.strictEqual(out, 'Hello World Dwellers');
});
it('should remove common bidi embedding and override controls if they are lowercase', () => {
const input = '\u202aHello\u202c \u202bWorld\u202c \u202dDwellers\u202e';
const out = utils.stripBidiControls(input);
assert.strictEqual(out, 'Hello World Dwellers');
});
it('should remove bidirectional isolate formatting characters', () => {
const input = '\u2066abc\u2067def\u2068ghi\u2069';
const out = utils.stripBidiControls(input);