Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clever-zebras-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes an issue where thread content would disappear after clicking "Jump to recent messages".
6 changes: 6 additions & 0 deletions apps/meteor/app/lib/client/methods/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { onClientMessageReceived } from '../../../../client/lib/onClientMessageR
import { settings } from '../../../../client/lib/settings';
import { dispatchToastMessage } from '../../../../client/lib/toast';
import { getUser, getUserId } from '../../../../client/lib/user';
import { upsertThreadMessageInCache } from '../../../../client/lib/utils/threadMessageUtils';
import { Messages, Rooms } from '../../../../client/stores';
import { trim } from '../../../../lib/utils/stringUtils';
import { t } from '../../../utils/lib/i18n';
Expand Down Expand Up @@ -45,5 +46,10 @@ export const runOptimisticSendMessage = async (

const processed = await onClientMessageReceived(optimistic);
Messages.state.store(processed);

if (processed.tmid) {
upsertThreadMessageInCache(processed, processed.rid, processed.tmid);
}

await clientCallbacks.run('afterSaveMessage', processed, { room, user });
};
8 changes: 7 additions & 1 deletion apps/meteor/client/lib/chats/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Messages, Rooms, Subscriptions } from '../../stores';
import { settings } from '../settings';
import { getUserId } from '../user';
import { prependReplies } from '../utils/prependReplies';
import { upsertThreadMessageInCache } from '../utils/threadMessageUtils';

export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage['_id'] | undefined }): DataAPI => {
const composeMessage = async (
Expand Down Expand Up @@ -162,7 +163,12 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
};

const pushEphemeralMessage = async (message: Omit<IMessage, 'rid' | 'tmid'>): Promise<void> => {
Messages.state.store({ ...message, rid, ...(tmid && { tmid }) });
const fullMessage = { ...message, rid, ...(tmid && { tmid }) } as IMessage;
Messages.state.store(fullMessage);

if (tmid) {
upsertThreadMessageInCache(fullMessage, rid, tmid);
}
};

const updateMessage = async (message: IEditedMessage, previewUrls?: string[]): Promise<void> => {
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/client/lib/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const roomsQueryKeys = {
images: (rid: IRoom['_id'], options?: { startingFromId?: string }) => [...roomsQueryKeys.room(rid), 'images', options] as const,
autocomplete: (text: string) => [...roomsQueryKeys.all, 'autocomplete', text] as const,
discussions: (rid: IRoom['_id'], ...args: [filter: { text?: string }]) => [...roomsQueryKeys.room(rid), 'discussions', ...args] as const,
threadMessages: (rid: IRoom['_id'], tmid: IMessage['_id']) => [...roomsQueryKeys.room(rid), 'threads', tmid, 'messages'] as const,
threadMainMessage: (rid: IRoom['_id'], tmid: IMessage['_id']) => [...roomsQueryKeys.room(rid), 'threads', tmid, 'main-message'] as const,
};

export const subscriptionsQueryKeys = {
Expand Down
105 changes: 105 additions & 0 deletions apps/meteor/client/lib/utils/threadMessageUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { IMessage, IThreadMessage, MessageAttachment } from '@rocket.chat/core-typings';
import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter';
import type { QueryClient } from '@tanstack/react-query';
import type { Condition, Filter } from 'mongodb';

import { queryClient as defaultQueryClient } from '../queryClient';
import { roomsQueryKeys } from '../queryKeys';

export type NotifyRoomRidDeleteBulkEvent = {
rid: IMessage['rid'];
excludePinned: boolean;
ignoreDiscussion: boolean;
ts: Condition<Date>;
users: string[];
ids?: string[];
showDeletedStatus?: boolean;
} & (
| {
filesOnly: true;
replaceFileAttachmentsWith?: MessageAttachment;
}
| {
filesOnly?: false;
}
);

export const createDeleteCriteria = (params: NotifyRoomRidDeleteBulkEvent): ((message: IMessage) => boolean) => {
const query: Filter<IMessage> = {};

if (params.ids) {
query._id = { $in: params.ids };
} else {
query.ts = params.ts;
}

if (params.excludePinned) {
query.pinned = { $ne: true };
}

if (params.ignoreDiscussion) {
query.drid = { $exists: false };
}
if (params.users?.length) {
query['u.username'] = { $in: params.users };
}

return createPredicateFromFilter(query);
};

export const upsertThreadMessageInCache = (
message: IMessage,
rid: IMessage['rid'],
tmid: IMessage['_id'],
client: QueryClient = defaultQueryClient,
): void => {
const queryKey = roomsQueryKeys.threadMessages(rid, tmid);
client.setQueryData<IMessage[]>(queryKey, (old) => {
if (!old) {
return [message];
}
const idx = old.findIndex((m) => m._id === message._id);
if (idx >= 0) {
const updated = [...old];
updated[idx] = message;
return updated;
}
return [...old, message].sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
});
};

export const markThreadMessagesAsRead = (messages: IThreadMessage[], until?: Date): IThreadMessage[] => {
return messages.map((msg) => {
if (!msg.unread) {
return msg;
}
if (until && new Date(msg.ts) > until) {
return msg;
}
const { unread: _, ...rest } = msg;
return rest as IThreadMessage;
});
};

export const mergeThreadMessages = (cachedMessages: IThreadMessage[], fetchedMessages: IThreadMessage[]): IThreadMessage[] => {
const messageMap = new Map<string, IThreadMessage>();

for (const msg of cachedMessages) {
messageMap.set(msg._id, msg);
}

for (const msg of fetchedMessages) {
const existing = messageMap.get(msg._id);
if (!existing) {
messageMap.set(msg._id, msg);
} else {
const msgTime = new Date(msg._updatedAt ?? msg.ts).getTime();
const existingTime = new Date(existing._updatedAt ?? existing.ts).getTime();
if (msgTime > existingTime) {
messageMap.set(msg._id, msg);
}
}
}

return Array.from(messageMap.values()).sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useDateScroll } from '../../../hooks/useDateScroll';
import { useFirstUnreadMessageId } from '../../../hooks/useFirstUnreadMessageId';
import { useMessageListNavigation } from '../../../hooks/useMessageListNavigation';
import { useLegacyThreadMessageListScrolling } from '../hooks/useLegacyThreadMessageListScrolling';
import { useLegacyThreadMessages } from '../hooks/useLegacyThreadMessages';
import { useThreadMessagesQuery } from '../hooks/useThreadMessagesQuery';
import './threads.css';

const isMessageSequential = (current: IMessage, previous: IMessage | undefined, groupingRange: number): boolean => {
Expand Down Expand Up @@ -54,7 +54,7 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen
const { t } = useTranslation();
const { innerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll();

const { messages, loading } = useLegacyThreadMessages(mainMessage._id);
Comment thread
abhinavkrin marked this conversation as resolved.
const { data: messages = [], isLoading: loading } = useThreadMessagesQuery(mainMessage._id);

const { innerRef: listScrollRef, jumpToRef } = useLegacyThreadMessageListScrolling(mainMessage);

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,60 +1,19 @@
import type { IMessage, IThreadMainMessage, MessageAttachment } from '@rocket.chat/core-typings';
import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter';
import { useStream } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient, useQuery } from '@tanstack/react-query';
import type { Condition, Filter } from 'mongodb';
import { useCallback, useEffect, useRef } from 'react';

import { useGetMessageByID } from './useGetMessageByID';
import { withDebouncing } from '../../../../../../lib/utils/highOrderFunctions';
import { onClientMessageReceived } from '../../../../../lib/onClientMessageReceived';
import { roomsQueryKeys } from '../../../../../lib/queryKeys';
import { modifyMessageOnFilesDelete } from '../../../../../lib/utils/modifyMessageOnFilesDelete';
import { createDeleteCriteria } from '../../../../../lib/utils/threadMessageUtils';
import { useRoom } from '../../../contexts/RoomContext';

type RoomMessagesRidEvent = IMessage;

type NotifyRoomRidDeleteBulkEvent = {
rid: IMessage['rid'];
excludePinned: boolean;
ignoreDiscussion: boolean;
ts: Condition<Date>;
users: string[];
ids?: string[]; // message ids have priority over ts
showDeletedStatus?: boolean;
} & (
| {
filesOnly: true;
replaceFileAttachmentsWith?: MessageAttachment;
}
| {
filesOnly?: false;
}
);

const createDeleteCriteria = (params: NotifyRoomRidDeleteBulkEvent): ((message: IMessage) => boolean) => {
const query: Filter<IMessage> = {};

if (params.ids) {
query._id = { $in: params.ids };
} else {
query.ts = params.ts;
}

if (params.excludePinned) {
query.pinned = { $ne: true };
}

if (params.ignoreDiscussion) {
query.drid = { $exists: false };
}
if (params.users?.length) {
query['u.username'] = { $in: params.users };
}

return createPredicateFromFilter(query);
};

const useSubscribeToMessage = () => {
const subscribeToRoomMessages = useStream('room-messages');
const subscribeToNotifyRoom = useStream('notify-room');
Expand All @@ -66,10 +25,12 @@ const useSubscribeToMessage = () => {
onMutate,
onDelete,
onFilesDelete,
onMessagesRead,
}: {
onMutate?: (message: IMessage) => void;
onDelete?: () => void;
onFilesDelete?: (replaceFileAttachmentsWith?: MessageAttachment) => void;
onMutate?: (message: IMessage) => void | Promise<void>;
onDelete?: () => void | Promise<void>;
onFilesDelete?: (replaceFileAttachmentsWith?: MessageAttachment) => void | Promise<void>;
onMessagesRead?: (event: { tmid?: string; until: Date }) => void | Promise<void>;
},
) => {
const unsubscribeFromRoomMessages = subscribeToRoomMessages(message.rid, (event: RoomMessagesRidEvent) => {
Expand All @@ -90,10 +51,15 @@ const useSubscribeToMessage = () => {
}
});

const unsubscribeFromMessagesRead = subscribeToNotifyRoom(`${message.rid}/messagesRead`, (event) => {
onMessagesRead?.(event);
});

return () => {
unsubscribeFromRoomMessages();
unsubscribeFromDeleteMessage();
unsubscribeFromDeleteMessageBulk();
unsubscribeFromMessagesRead();
};
},
[subscribeToNotifyRoom, subscribeToRoomMessages],
Expand All @@ -120,7 +86,7 @@ export const useThreadMainMessageQuery = (
}, [tmid]);

return useQuery({
queryKey: ['rooms', room._id, 'threads', tmid, 'main-message'] as const,
queryKey: roomsQueryKeys.threadMainMessage(room._id, tmid),

queryFn: async ({ queryKey }) => {
const mainMessage = await getMessage(tmid);
Expand Down Expand Up @@ -156,6 +122,24 @@ export const useThreadMainMessageQuery = (
queryClient.setQueryData(queryKey, () => msg);
debouncedInvalidate();
},
onMessagesRead: ({ tmid: eventTmid, until }) => {
if (eventTmid) {
return;
}

queryClient.setQueryData<IThreadMainMessage>(queryKey, (old) => {
if (!old?.unread) {
return old;
}

if (new Date(old.ts).getTime() <= new Date(until).getTime()) {
const { unread: _, ...rest } = old;
return rest as IThreadMainMessage;
}

return old;
});
},
});

return mainMessage;
Expand Down
Loading
Loading