mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-03 12:36:02 +01:00
feat: show online users at the top of userlist and update
when they enter/leave
This commit is contained in:
@@ -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.getUsersInRoom = async (roomId, start, stop) => {
|
||||
const uids = await Messaging.getUidsInRoom(roomId, 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.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
34
src/upgrades/3.3.0/chat_room_online_zset.js
Normal file
34
src/upgrades/3.3.0/chat_room_online_zset.js
Normal file
@@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user