feat: show online users at the top of userlist and update

when they enter/leave
isekai-main
Barış Soner Uşaklı 2 years ago
parent 887333478a
commit 911ef0581c

@ -135,6 +135,8 @@ get:
nullable: true
status:
type: string
online:
type: boolean
icon:text:
type: string
description: A single-letter representation of a username. This is used in the

@ -29,6 +29,7 @@ define('forum/chats', [
let newMessage = false;
let chatNavWrapper = null;
let userListEl = null;
$(window).on('action:ajaxify.start', function () {
Chats.destroyAutoComplete(ajaxify.data.roomId);
@ -47,7 +48,7 @@ define('forum/chats', [
socket.emit('modules.chats.enterPublic', ajaxify.data.publicRooms.map(r => r.roomId));
const env = utils.findBootstrapEnvironment();
chatNavWrapper = $('[component="chat/nav-wrapper"]');
userListEl = $('[component="chat/user/list"]');
if (!Chats.initialised) {
Chats.addSocketListeners();
Chats.addGlobalEventListeners();
@ -468,6 +469,7 @@ define('forum/chats', [
const mainWrapper = components.get('chat/main-wrapper');
mainWrapper.html(html);
chatNavWrapper = $('[component="chat/nav-wrapper"]');
userListEl = $('[component="chat/user/list"]');
html.find('.timeago').timeago();
ajaxify.data = { ...ajaxify.data, ...payload, roomId: roomId };
ajaxify.updateTitle(ajaxify.data.title);
@ -526,6 +528,10 @@ define('forum/chats', [
Chats.increasePublicRoomUnreadCount(chatNavWrapper.find('[data-roomid=' + data.roomId + ']'));
});
socket.on('event:chats.user-online', function (data) {
userListEl.find(`[data-uid="${data.uid}"]`).toggleClass('online', !!data.state);
});
socket.on('event:user_status_change', function (data) {
app.updateUserStatus($('.chats-list [data-uid="' + data.uid + '"] [component="user/status"]'), data.status);
});

@ -171,7 +171,9 @@ chatsAPI.users = async (caller, data) => {
const [isOwner, isUserInRoom, users] = await Promise.all([
messaging.isRoomOwner(caller.uid, data.roomId),
messaging.isUserInRoom(caller.uid, data.roomId),
messaging.getUsersInRoom(data.roomId, start, stop),
messaging.getUsersInRoomFromSet(
`chat:room:${data.roomId}:uids:online`, data.roomId, start, stop, true
),
]);
if (!isUserInRoom) {
throw new Error('[[error:no-privileges]]');

@ -39,7 +39,7 @@ module.exports = function (Messaging) {
}
// push unread count only for private rooms
const uids = await Messaging.getAllUidsInRoom(roomId);
const uids = await Messaging.getAllUidsInRoomFromSet(`chat:room:${roomId}:uids:online`);
Messaging.pushUnreadCount(uids, unreadData);
// Delayed notifications
@ -77,7 +77,7 @@ module.exports = function (Messaging) {
path: `/chats/${messageObj.roomId}`,
});
await batch.processSortedSet(`chat:room:${roomId}:uids`, async (uids) => {
await batch.processSortedSet(`chat:room:${roomId}:uids:online`, async (uids) => {
const hasRead = await Messaging.hasRead(uids, roomId);
uids = uids.filter((uid, index) => !hasRead[index] && parseInt(fromUid, 10) !== parseInt(uid, 10));

@ -10,6 +10,7 @@ const groups = require('../groups');
const plugins = require('../plugins');
const privileges = require('../privileges');
const meta = require('../meta');
const io = require('../socket.io');
const cache = require('../cache');
const cacheCreate = require('../cacheCreate');
@ -92,7 +93,10 @@ module.exports = function (Messaging) {
await Promise.all([
db.setObject(`chat:room:${roomId}`, room),
db.sortedSetAdd('chat:rooms', now, roomId),
db.sortedSetAdd(`chat:room:${roomId}:uids`, now, uid),
db.sortedSetsAdd([
`chat:room:${roomId}:uids`,
`chat:room:${roomId}:uids:online`,
], now, uid),
]);
await Promise.all([
@ -133,13 +137,14 @@ module.exports = function (Messaging) {
.map(uid => `uid:${uid}:chat:rooms`)
.concat(uids.map(uid => `uid:${uid}:chat:rooms:unread`));
await Promise.all([
db.sortedSetRemove(`chat:room:${roomId}:uids`, uids),
db.sortedSetsRemove(keys, roomId),
]);
await db.sortedSetsRemove(keys, roomId);
}));
await Promise.all([
db.deleteAll(roomIds.map(id => `chat:room:${id}`)),
db.deleteAll([
...roomIds.map(id => `chat:room:${id}`),
...roomIds.map(id => `chat:room:${id}:uids`),
...roomIds.map(id => `chat:room:${id}:uids:online`),
]),
db.sortedSetRemove('chat:rooms', roomIds),
db.sortedSetRemove('chat:rooms:public', roomIds),
db.sortedSetRemove('chat:rooms:public:order', roomIds),
@ -193,7 +198,7 @@ module.exports = function (Messaging) {
return single ? data.inRooms.pop() : data.inRooms;
};
Messaging.roomExists = async roomId => db.exists(`chat:room:${roomId}:uids`);
Messaging.roomExists = async roomId => db.exists(`chat:room:${roomId}`);
Messaging.getUserCountInRoom = async roomId => db.sortedSetCard(`chat:room:${roomId}:uids`);
@ -231,7 +236,10 @@ module.exports = function (Messaging) {
async function addUidsToRoom(uids, roomId) {
const now = Date.now();
const timestamps = uids.map(() => now);
await db.sortedSetAdd(`chat:room:${roomId}:uids`, timestamps, uids);
await Promise.all([
db.sortedSetAdd(`chat:room:${roomId}:uids`, timestamps, uids),
db.sortedSetAdd(`chat:room:${roomId}:uids:online`, timestamps, uids),
]);
await updateUserCount([roomId]);
await Promise.all(uids.map(uid => Messaging.addSystemMessage('user-join', uid, roomId)));
}
@ -275,7 +283,10 @@ module.exports = function (Messaging) {
.concat(uids.map(uid => `uid:${uid}:chat:rooms:unread`));
await Promise.all([
db.sortedSetRemove(`chat:room:${roomId}:uids`, uids),
db.sortedSetRemove([
`chat:room:${roomId}:uids`,
`chat:room:${roomId}:uids:online`,
], uids),
db.sortedSetsRemove(keys, roomId),
]);
@ -288,7 +299,10 @@ module.exports = function (Messaging) {
const isInRoom = await Promise.all(roomIds.map(roomId => Messaging.isUserInRoom(uid, roomId)));
roomIds = roomIds.filter((roomId, index) => isInRoom[index]);
const roomKeys = roomIds.map(roomId => `chat:room:${roomId}:uids`);
const roomKeys = [
...roomIds.map(roomId => `chat:room:${roomId}:uids`),
...roomIds.map(roomId => `chat:room:${roomId}:uids:online`),
];
await Promise.all([
db.sortedSetsRemove(roomKeys, uid),
db.sortedSetRemove([
@ -310,21 +324,34 @@ module.exports = function (Messaging) {
await db.setObjectField(`chat:room:${roomId}`, 'owner', newOwner);
}
Messaging.getAllUidsInRoom = async function (roomId) {
const cacheKey = `chat:room:${roomId}:users`;
Messaging.getAllUidsInRoomFromSet = async function (set) {
const cacheKey = `${set}:all`;
let uids = roomUidCache.get(cacheKey);
if (uids !== undefined) {
return uids;
}
uids = await Messaging.getUidsInRoom(roomId, 0, -1);
uids = await Messaging.getUidsInRoomFromSet(set, 0, -1);
roomUidCache.set(cacheKey, uids);
return uids;
};
Messaging.getUidsInRoom = async (roomId, start, stop) => db.getSortedSetRange(`chat:room:${roomId}:uids`, start, stop);
Messaging.getUidsInRoomFromSet = async (set, start, stop, reverse = false) => db[
reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'
](set, start, stop);
Messaging.getUidsInRoom = async (roomId, start, stop, reverse = false) => db[
reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'
](`chat:room:${roomId}:uids`, start, stop);
Messaging.getUsersInRoom = async (roomId, start, stop, reverse = false) => {
const users = await Messaging.getUsersInRoomFromSet(
`chat:room:${roomId}:uids`, roomId, start, stop, reverse
);
return users;
};
Messaging.getUsersInRoom = async (roomId, start, stop) => {
const uids = await Messaging.getUidsInRoom(roomId, start, stop);
Messaging.getUsersInRoomFromSet = async (set, roomId, start, stop, reverse = false) => {
const uids = await Messaging.getUidsInRoomFromSet(set, start, stop, reverse);
const [users, isOwners] = await Promise.all([
user.getUsersFields(uids, ['uid', 'username', 'picture', 'status']),
Messaging.isRoomOwner(uids, roomId),
@ -373,10 +400,12 @@ module.exports = function (Messaging) {
Messaging.loadRoom = async (uid, data) => {
const { roomId } = data;
const [room, inRoom, canChat] = await Promise.all([
const [room, inRoom, canChat, isAdmin, isGlobalMod] = await Promise.all([
Messaging.getRoomData(roomId),
Messaging.isUserInRoom(uid, roomId),
privileges.global.can('chat', uid),
user.isAdministrator(uid),
user.isGlobalModerator(uid),
]);
if (!canChat) {
@ -395,23 +424,30 @@ module.exports = function (Messaging) {
if (room.public && !inRoom) {
await addUidsToRoom([uid], roomId);
room.userCount += 1;
} else if (inRoom) {
await db.sortedSetAdd(`chat:room:${roomId}:uids:online`, Date.now(), uid);
}
const [canReply, users, messages, isAdmin, isGlobalMod, settings, isOwner] = await Promise.all([
const [canReply, users, messages, settings, isOwner, onlineUids] = await Promise.all([
Messaging.canReply(roomId, uid),
Messaging.getUsersInRoom(roomId, 0, 39),
Messaging.getUsersInRoomFromSet(`chat:room:${roomId}:uids:online`, roomId, 0, 39, true),
Messaging.getMessages({
callerUid: uid,
uid: data.uid || uid,
roomId: roomId,
isNew: false,
}),
user.isAdministrator(uid),
user.isGlobalModerator(uid),
user.getSettings(uid),
Messaging.isRoomOwner(uid, roomId),
io.getUidsInRoom(`chat_room_${roomId}`),
]);
users.forEach((user) => {
if (user) {
user.online = parseInt(user.uid, 10) === parseInt(uid, 10) || onlineUids.includes(String(user.uid));
}
});
room.messages = messages;
room.isOwner = isOwner;
room.users = users;

@ -292,6 +292,26 @@ Sockets.getCountInRoom = function (room) {
return roomMap ? roomMap.size : 0;
};
// works across multiple nodes
Sockets.getUidsInRoom = async function (room) {
if (!Sockets.server) {
return [];
}
const ioRoom = Sockets.server.in(room);
const uids = {};
if (ioRoom) {
const sockets = await ioRoom.fetchSockets();
for (const s of sockets) {
for (const r of s.rooms) {
if (r.startsWith('uid_')) {
uids[r.split('_').pop()] = 1;
}
}
}
}
return Object.keys(uids);
};
Sockets.warnDeprecated = (socket, replacement) => {
if (socket.previousEvents && socket.emit) {
socket.emit('event:deprecated_call', {

@ -106,12 +106,18 @@ async function joinLeave(socket, roomIds, method, prefix = 'chat_room') {
Messaging.isUserInRoom(socket.uid, roomIds),
Messaging.getRoomsData(roomIds, ['public', 'groups']),
]);
const io = require('./index');
await Promise.all(roomIds.map(async (roomId, idx) => {
const isPublic = roomData[idx] && roomData[idx].public;
const roomGroups = roomData[idx] && roomData[idx].groups;
if (isAdmin || (inRooms[idx] && (!isPublic || await groups.isMemberOfAny(socket.uid, roomGroups)))) {
socket[method](`${prefix}_${roomId}`);
if (prefix === 'chat_room') {
io.in(`chat_room_${roomId}`).emit('event:chats.user-online', {
uid: socket.uid,
state: method === 'join' ? 1 : 0,
});
}
}
}));
}

@ -0,0 +1,34 @@
'use strict';
const _ = require('lodash');
const db = require('../../database');
const batch = require('../../batch');
module.exports = {
name: 'Create chat:room:<room_id>uids:online zset',
timestamp: Date.UTC(2023, 6, 14),
method: async function () {
const { progress } = this;
progress.total = await db.sortedSetCard('chat:rooms');
await batch.processSortedSet('chat:rooms', async (roomIds) => {
progress.incr(roomIds.length);
const arrayOfUids = await db.getSortedSetsMembersWithScores(roomIds.map(roomId => `chat:room:${roomId}:uids`));
const bulkAdd = [];
arrayOfUids.forEach((uids, idx) => {
const roomId = roomIds[idx];
uids.forEach((uid) => {
bulkAdd.push([`chat:room:${roomId}:uids:online`, uid.score, uid.value]);
});
});
await db.sortedSetAddBulk(bulkAdd);
}, {
batch: 500,
});
},
};
Loading…
Cancel
Save