diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 630e541c59..79657b519e 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -202,6 +202,8 @@ paths: $ref: 'write/chats/roomId/state.yaml' /chats/{roomId}/watch: $ref: 'write/chats/roomId/watch.yaml' + /chats/{roomId}/typing: + $ref: 'write/chats/roomId/typing.yaml' /chats/{roomId}/users: $ref: 'write/chats/roomId/users.yaml' /chats/{roomId}/users/{uid}: diff --git a/public/openapi/write/chats/roomId/messages/pinned.yaml b/public/openapi/write/chats/roomId/messages/pinned.yaml index 683b29b290..3d8ea371dd 100644 --- a/public/openapi/write/chats/roomId/messages/pinned.yaml +++ b/public/openapi/write/chats/roomId/messages/pinned.yaml @@ -5,6 +5,8 @@ get: description: > This operation retrieves a list of pinned messages for a given chat room. This call will always return a maximum of 50 items, of which the result set can be offset based on the passed-in `start` parameter. + + N.B. The calling user must be in the chat room for this call to succeed. parameters: - in: path name: roomId diff --git a/public/openapi/write/chats/roomId/typing.yaml b/public/openapi/write/chats/roomId/typing.yaml new file mode 100644 index 0000000000..f30ec694b7 --- /dev/null +++ b/public/openapi/write/chats/roomId/typing.yaml @@ -0,0 +1,39 @@ +put: + tags: + - chats + summary: update typing state in chat room + description: > + This operation updates the typing state in a given chat room, so that other users in the room see that that user is typing. + + N.B. The calling user must be in the chat room for this call to succeed. + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: a valid room id + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + typing: + type: boolean + example: true + required: + - typing + responses: + '200': + description: Chat room typing state updated. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: {} \ No newline at end of file diff --git a/public/src/client/chats.js b/public/src/client/chats.js index d831170e78..a5cb58caf0 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -391,11 +391,7 @@ define('forum/chats', [ Chats.addTypingHandler = function (parent, roomId) { const textarea = parent.find('[component="chat/input"]'); function emitTyping(typing) { - socket.emit('modules.chats.typing', { - roomId: roomId, - typing: typing, - username: app.user.username, - }); + api.put(`/chats/${roomId}/typing`, { typing }).catch(alerts.error); } textarea.on('focus', () => textarea.val() && emitTyping(true)); @@ -744,7 +740,7 @@ define('forum/chats', [ }); socket.on('event:chats.typing', async (data) => { - if (chatModule.isFromBlockedUser(data.uid)) { + if (data.uid === app.user.uid || chatModule.isFromBlockedUser(data.uid)) { return; } chatModule.updateTypingUserList($(`[component="chat/main-wrapper"][data-roomid="${data.roomId}"]`), data); diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 97b580b0b2..0794af22a8 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -245,7 +245,7 @@ define('chat', [ }; module.onUserTyping = function (data) { - if (module.isFromBlockedUser(data.uid)) { + if (data.uid === app.user.uid || module.isFromBlockedUser(data.uid)) { return; } const modal = module.getModal(data.roomId); diff --git a/src/api/chats.js b/src/api/chats.js index ec6e1d5c32..964bfdc071 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -10,7 +10,9 @@ const messaging = require('../messaging'); const notifications = require('../notifications'); const privileges = require('../privileges'); const plugins = require('../plugins'); +const utils = require('../utils'); +const websockets = require('../socket.io'); const socketHelpers = require('../socket.io/helpers'); const chatsAPI = module.exports; @@ -207,6 +209,27 @@ chatsAPI.watch = async (caller, { roomId, state }) => { await messaging.setUserNotificationSetting(caller.uid, roomId, state); }; +chatsAPI.toggleTyping = async (caller, { roomId, typing }) => { + if (!utils.isNumber(roomId) || typeof typing !== 'boolean') { + throw new Error('[[error:invalid-data]]'); + } + + const [isInRoom, username] = await Promise.all([ + messaging.isUserInRoom(caller.uid, roomId), + user.getUserField(caller.uid, 'username'), + ]); + if (!isInRoom) { + throw new Error('[[error:no-privileges]]'); + } + + websockets.in(`chat_room_${roomId}`).emit('event:chats.typing', { + uid: caller.uid, + roomId, + typing, + username, + }); +}; + chatsAPI.users = async (caller, data) => { const start = data.hasOwnProperty('start') ? data.start : 0; const stop = start + 39; diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index baf397f9de..ae21235f88 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -95,6 +95,13 @@ Chats.watch = async (req, res) => { helpers.formatApiResponse(200, res); }; +Chats.toggleTyping = async (req, res) => { + const { typing } = req.body; + + await api.chats.toggleTyping(req, { typing, ...req.params }); + helpers.formatApiResponse(200, res); +}; + Chats.users = async (req, res) => { const { roomId } = req.params; const start = parseInt(req.query.start, 10) || 0; diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index bbd464d369..7fd2c8e392 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -27,6 +27,8 @@ module.exports = function () { setupApiRoute(router, 'put', '/:roomId/watch', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['value'])], controllers.write.chats.watch); setupApiRoute(router, 'delete', '/:roomId/watch', [...middlewares, middleware.assert.room], controllers.write.chats.watch); + setupApiRoute(router, 'put', '/:roomId/typing', [...middlewares, middleware.assert.room], controllers.write.chats.toggleTyping); + setupApiRoute(router, 'get', '/:roomId/users', [...middlewares, middleware.assert.room], controllers.write.chats.users); setupApiRoute(router, 'post', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.invite); setupApiRoute(router, 'delete', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.kick); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index a3f90fd81e..5f805eb899 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -213,19 +213,16 @@ SocketModules.chats.loadPinnedMessages = async (socket, data) => { }; SocketModules.chats.typing = async (socket, data) => { - if (!data || !utils.isNumber(data.roomId) || typeof data.typing !== 'boolean') { + sockets.warnDeprecated(socket, 'PUT /api/v3/chats/:roomId/typing'); + + if (!data) { throw new Error('[[error:invalid-data]]'); } - const isInRoom = await Messaging.isUserInRoom(socket.uid, data.roomId); - if (!isInRoom) { - throw new Error('[[error:no-privileges]]'); - } - socket.to(`chat_room_${data.roomId}`).emit('event:chats.typing', { - uid: socket.uid, - roomId: data.roomId, - typing: data.typing, - username: validator.escape(String(data.username)), - }); + + // `username` is now inferred from caller uid + delete data.username; + + await api.chats.toggleTyping(socket, data); };