Skip to content

Commit e26ed03

Browse files
committed
chore: extract subscriptions.read tmid migration into separate PR
The thread read-marker migration (subscriptions.read tmid endpoint + ThreadChat/useThreadMessagesQuery callers) moves to its own PR #40957.
1 parent d12ff77 commit e26ed03

9 files changed

Lines changed: 44 additions & 132 deletions

File tree

.changeset/ddp-migrate-batch2-callers.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
'@rocket.chat/meteor': patch
33
---
44

5-
Migrate seven client DDP callers to their REST equivalents (the DDP methods stay registered on the server for external SDK/mobile clients, with a deprecation log pointing at the REST route until 9.0.0 removes them):
5+
Migrate six client DDP callers to their REST equivalents (the DDP methods stay registered on the server for external SDK/mobile clients, with a deprecation log pointing at the REST route until 9.0.0 removes them):
66

77
- `loadMissedMessages``GET /v1/chat.syncMessages`
88
- `joinRoom``POST /v1/channels.join` (channel-only; non-`c` rooms now error via REST the same way they used to via DDP)
99
- `userSetUtcOffset``POST /v1/users.setPreferences` (new `utcOffset` field)
1010
- `deleteFileMessage``POST /v1/chat.delete` (new `fileId` body shape)
11-
- `readThreads``POST /v1/subscriptions.read` (new `tmid` field)
1211
- `spotlight``GET /v1/spotlight` (new `usernames` / `type` / `rid` query params)
1312
- `listCustomSounds``GET /v1/custom-sounds.list`

.changeset/rest-subscriptions-read-tmid.md

Lines changed: 0 additions & 6 deletions
This file was deleted.

apps/meteor/app/api/server/v1/subscriptions.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ISubscription } from '@rocket.chat/core-typings';
2-
import { Messages, Rooms, Subscriptions } from '@rocket.chat/models';
2+
import { Rooms, Subscriptions } from '@rocket.chat/models';
33
import {
44
ajv,
55
isSubscriptionsGetProps,
@@ -14,7 +14,6 @@ import { Meteor } from 'meteor/meteor';
1414
import { readMessages } from '../../../../server/lib/readMessages';
1515
import { getSubscriptions } from '../../../../server/publications/subscription';
1616
import { unreadMessages } from '../../../message-mark-as-unread/server/unreadMessages';
17-
import { readThreadMethod } from '../../../threads/server/functions';
1817
import { API } from '../api';
1918

2019
const subscriptionsGetResponseSchema = ajv.compile<{
@@ -140,23 +139,14 @@ API.v1.post(
140139
},
141140
},
142141
async function action() {
143-
const { readThreads = false, tmid } = this.bodyParams;
142+
const { readThreads = false } = this.bodyParams;
144143
const roomId = 'rid' in this.bodyParams ? this.bodyParams.rid : this.bodyParams.roomId;
145144

146145
const room = await Rooms.findOneById(roomId);
147146
if (!room) {
148147
throw new Error('error-invalid-subscription');
149148
}
150149

151-
if (tmid) {
152-
const thread = await Messages.findOneById(tmid);
153-
if (thread?.rid !== room._id) {
154-
throw new Error('error-invalid-thread');
155-
}
156-
await readThreadMethod({ user: this.user, tmid });
157-
return API.v1.success();
158-
}
159-
160150
await readMessages(room, this.userId, readThreads);
161151

162152
return API.v1.success();

apps/meteor/app/threads/server/functions.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
22
import { isEditedMessage } from '@rocket.chat/core-typings';
3-
import { Messages, Rooms, Subscriptions, NotificationQueue } from '@rocket.chat/models';
4-
import { Meteor } from 'meteor/meteor';
3+
import { Messages, Subscriptions, NotificationQueue } from '@rocket.chat/models';
54

65
import { callbacks } from '../../../server/lib/callbacks';
7-
import { canAccessRoomAsync } from '../../authorization/server';
86
import {
97
notifyOnSubscriptionChangedByRoomIdAndUserIds,
108
notifyOnSubscriptionChangedByRoomIdAndUserId,
119
} from '../../lib/server/lib/notifyListener';
1210
import { getMentions, getUserIdsFromHighlights } from '../../lib/server/lib/notifyUsersOnMessage';
13-
import { settings } from '../../settings/server';
1411

1512
export async function reply({ tmid }: { tmid?: string }, message: IMessage, parentMessage: IMessage, followers: string[]) {
1613
if (!tmid || isEditedMessage(message)) {
@@ -101,32 +98,3 @@ export const readThread = async ({ user, room, tmid }: { user: IUser; room: IRoo
10198

10299
callbacks.runAsync('afterReadMessages', room, { uid: user._id, tmid });
103100
};
104-
105-
/**
106-
* Marks a thread as read for a user, replicating the full behavior of the
107-
* legacy `readThreads` DDP method: it validates that threads are enabled, that
108-
* the thread and its room exist, that the user can access the room, and runs
109-
* the `beforeReadMessages` callback before clearing the thread unread state.
110-
*/
111-
export const readThreadMethod = async ({ user, tmid }: { user: IUser; tmid: IMessage['_id'] }) => {
112-
if (!settings.get('Threads_enabled')) {
113-
throw new Meteor.Error('error-not-allowed', 'Threads Disabled', { method: 'readThreads' });
114-
}
115-
116-
const thread = await Messages.findOneById(tmid);
117-
if (!thread) {
118-
return;
119-
}
120-
121-
const room = await Rooms.findOneById(thread.rid);
122-
if (!room) {
123-
throw new Meteor.Error('error-room-does-not-exist', 'This room does not exist', { method: 'readThreads' });
124-
}
125-
126-
if (!(await canAccessRoomAsync(room, user))) {
127-
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'readThreads' });
128-
}
129-
130-
await callbacks.run('beforeReadMessages', thread.rid, user._id);
131-
await readThread({ user, room, tmid });
132-
};

apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings';
22
import { isEditedMessage } from '@rocket.chat/core-typings';
33
import { Box, CheckBox, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage';
44
import { clientCallbacks, ContextualbarContent } from '@rocket.chat/ui-client';
5-
import { useEndpoint, useTranslation, useUserPreference, useRoomToolbox } from '@rocket.chat/ui-contexts';
5+
import { useMethod, useTranslation, useUserPreference, useRoomToolbox } from '@rocket.chat/ui-contexts';
66
import { useState, useEffect, useCallback, useId } from 'react';
77

88
import ThreadMessageList from './ThreadMessageList';
@@ -62,7 +62,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => {
6262
}, [chat?.messageEditing]);
6363

6464
const room = useRoom();
65-
const markThreadRead = useEndpoint('POST', '/v1/subscriptions.read');
65+
const readThreads = useMethod('readThreads');
6666
useEffect(() => {
6767
clientCallbacks.add(
6868
'streamNewMessage',
@@ -71,7 +71,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => {
7171
return;
7272
}
7373

74-
void markThreadRead({ rid: room._id, tmid: mainMessage._id });
74+
readThreads(mainMessage._id);
7575
},
7676
clientCallbacks.priority.MEDIUM,
7777
`thread-${room._id}`,
@@ -80,7 +80,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => {
8080
return () => {
8181
clientCallbacks.remove('streamNewMessage', `thread-${room._id}`);
8282
};
83-
}, [mainMessage._id, markThreadRead, room._id]);
83+
}, [mainMessage._id, readThreads, room._id]);
8484

8585
const subscription = useRoomSubscription();
8686
const sendToChannelID = useId();

apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMessagesQuery.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isThreadMessage, type IMessage, type IRoom, type IThreadMainMessage, type IThreadMessage } from '@rocket.chat/core-typings';
2-
import { useEndpoint, useStream } from '@rocket.chat/ui-contexts';
2+
import { useEndpoint, useMethod, useStream } from '@rocket.chat/ui-contexts';
33
import { useQuery, useQueryClient } from '@tanstack/react-query';
44
import { useEffect, useRef } from 'react';
55

@@ -26,7 +26,10 @@ export const useThreadMessagesQuery = (tmid: IThreadMainMessage['_id'], rid?: IR
2626
const queryClient = useQueryClient();
2727
const queryKey = roomsQueryKeys.threadMessages(roomId, tmid);
2828
const getThreadMessages = useEndpoint('GET', '/v1/chat.getThreadMessages');
29-
const markThreadRead = useEndpoint('POST', '/v1/subscriptions.read');
29+
// REST has no per-thread read-marker endpoint yet; fall back to the
30+
// `readThreads` DDP method so the side effect that DDP getThreadMessages
31+
// used to do server-side keeps happening for callers.
32+
const readThreads = useMethod('readThreads');
3033

3134
const subscribeToRoomMessages = useStream('room-messages');
3235
const subscribeToNotifyRoom = useStream('notify-room');
@@ -108,7 +111,7 @@ export const useThreadMessagesQuery = (tmid: IThreadMainMessage['_id'], rid?: IR
108111
const cachedMessages = queryClient.getQueryData<IThreadMessage[]>(queryKey) || [];
109112

110113
const { messages } = await getThreadMessages({ tmid });
111-
void markThreadRead({ rid: roomId, tmid }).catch(() => undefined);
114+
void Promise.resolve(readThreads(tmid)).catch(() => undefined);
112115
const filtered = messages
113116
.map((m) => mapMessageFromApi(m))
114117
.filter((msg): msg is IThreadMessage => isThreadMessage(msg) && msg.tmid === tmid && msg._id !== tmid && msg._hidden !== true);
Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { IMessage, IUser } from '@rocket.chat/core-typings';
22
import type { ServerMethods } from '@rocket.chat/ddp-client';
3+
import { Messages, Rooms } from '@rocket.chat/models';
34
import { check } from 'meteor/check';
45
import { Meteor } from 'meteor/meteor';
56

6-
import { methodDeprecationLogger } from '../../app/lib/server/lib/deprecationWarningLogger';
7-
import { readThreadMethod } from '../../app/threads/server/functions';
7+
import { canAccessRoomAsync } from '../../app/authorization/server';
8+
import { settings } from '../../app/settings/server';
9+
import { readThread } from '../../app/threads/server/functions';
10+
import { callbacks } from '../lib/callbacks';
811

912
declare module '@rocket.chat/ddp-client' {
1013
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -15,14 +18,33 @@ declare module '@rocket.chat/ddp-client' {
1518

1619
Meteor.methods<ServerMethods>({
1720
async readThreads(tmid) {
18-
methodDeprecationLogger.method('readThreads', '9.0.0', '/v1/subscriptions.read');
1921
check(tmid, String);
2022

21-
const user = (await Meteor.userAsync()) as IUser | null;
22-
if (!user) {
23-
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'readThreads' });
23+
if (!Meteor.userId() || !settings.get('Threads_enabled')) {
24+
throw new Meteor.Error('error-not-allowed', 'Threads Disabled', {
25+
method: 'getThreadMessages',
26+
});
2427
}
2528

26-
await readThreadMethod({ user, tmid });
29+
const thread = await Messages.findOneById(tmid);
30+
if (!thread) {
31+
return;
32+
}
33+
34+
const user = (await Meteor.userAsync()) ?? undefined;
35+
36+
const room = await Rooms.findOneById(thread.rid);
37+
if (!room) {
38+
throw new Meteor.Error('error-room-does-not-exist', 'This room does not exist', { method: 'getThreadMessages' });
39+
}
40+
41+
if (!(await canAccessRoomAsync(room, user))) {
42+
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' });
43+
}
44+
45+
await callbacks.run('beforeReadMessages', thread.rid, user?._id);
46+
if (user?._id) {
47+
await readThread({ user: user as IUser, room, tmid });
48+
}
2749
},
2850
});

apps/meteor/tests/end-to-end/api/subscriptions.ts

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -394,60 +394,6 @@ describe('[Subscriptions]', () => {
394394
expect(res.body.subscription.tunread).to.deep.equal([threadId]);
395395
});
396396
});
397-
398-
it('should mark a single thread as read when a tmid is provided', async () => {
399-
await request
400-
.post(api('chat.sendMessage'))
401-
.set(threadUserCredentials)
402-
.send({
403-
message: {
404-
rid: testChannel._id,
405-
msg: `@${adminUsername} making admin follow this thread`,
406-
tmid: threadId,
407-
},
408-
});
409-
410-
await request
411-
.post(api('subscriptions.read'))
412-
.set(credentials)
413-
.send({
414-
rid: testChannel._id,
415-
tmid: threadId,
416-
})
417-
.expect(200)
418-
.expect((res) => {
419-
expect(res.body).to.have.property('success', true);
420-
});
421-
422-
await request
423-
.get(api('subscriptions.getOne'))
424-
.set(credentials)
425-
.query({
426-
roomId: testChannel._id,
427-
})
428-
.expect(200)
429-
.expect((res) => {
430-
expect(res.body).to.have.property('success', true);
431-
// removeUnreadThreadByRoomIdAndUserId only $pulls the tmid, leaving an empty array
432-
expect(res.body.subscription).to.have.property('tunread').that.is.an('array').and.does.not.include(threadId);
433-
});
434-
});
435-
436-
it('should fail when the tmid does not belong to the provided room', (done) => {
437-
void request
438-
.post(api('subscriptions.read'))
439-
.set(credentials)
440-
.send({
441-
rid: testGroup._id,
442-
tmid: threadId,
443-
})
444-
.expect(400)
445-
.expect((res) => {
446-
expect(res.body).to.have.property('success', false);
447-
expect(res.body).to.have.property('error');
448-
})
449-
.end(done);
450-
});
451397
});
452398
});
453399

packages/rest-typings/src/v1/subscriptionsEndpoints.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ type SubscriptionsGet = { updatedSince?: string };
66

77
type SubscriptionsGetOne = { roomId: IRoom['_id'] };
88

9-
type SubscriptionsRead =
10-
| { rid: IRoom['_id']; readThreads?: boolean; tmid?: IMessage['_id'] }
11-
| { roomId: IRoom['_id']; readThreads?: boolean; tmid?: IMessage['_id'] };
9+
type SubscriptionsRead = { rid: IRoom['_id']; readThreads?: boolean } | { roomId: IRoom['_id']; readThreads?: boolean };
1210

1311
type SubscriptionsUnread = { roomId: IRoom['_id'] } | { firstUnreadMessage: Pick<IMessage, '_id'> };
1412

@@ -51,10 +49,6 @@ const SubscriptionsReadSchema = {
5149
type: 'boolean',
5250
nullable: true,
5351
},
54-
tmid: {
55-
type: 'string',
56-
nullable: true,
57-
},
5852
},
5953
required: ['rid'],
6054
additionalProperties: false,
@@ -69,10 +63,6 @@ const SubscriptionsReadSchema = {
6963
type: 'boolean',
7064
nullable: true,
7165
},
72-
tmid: {
73-
type: 'string',
74-
nullable: true,
75-
},
7666
},
7767
required: ['roomId'],
7868
additionalProperties: false,

0 commit comments

Comments
 (0)