Skip to content
5 changes: 5 additions & 0 deletions .changeset/social-pugs-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes collapsed/expanded GIFs (e.g. sent via `/giphy`) and other rich attachments from apps and integrations toggling back to the default state when scrolling the message list.
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { IMessage, MessageAttachment } from '@rocket.chat/core-typings';

import { useKeepMountedMessages } from './useKeepMountedMessages';

const date = new Date('2021-10-27T00:00:00.000Z');
const baseMessage: IMessage = {
ts: date,
u: {
_id: 'userId',
name: 'userName',
username: 'userName',
},
msg: 'message',
rid: 'roomId',
_id: 'messageId',
_updatedAt: date,
urls: [],
};

it('should not keep mounted a plain message', () => {
const messages: IMessage[] = [{ ...baseMessage }];
expect(useKeepMountedMessages(messages)).toEqual([]);
});

it('should keep mounted a message with files', () => {
const messages: IMessage[] = [{ ...baseMessage, files: [{ _id: 'fileId', name: 'file.png', type: 'image/png', format: 'png', size: 123 }] }];

Check failure on line 26 in apps/meteor/client/views/room/MessageList/hooks/useKeepMountedMessages.spec.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Replace `{·...baseMessage,·files:·[{·_id:·'fileId',·name:·'file.png',·type:·'image/png',·format:·'png',·size:·123·}]·}` with `⏎↹↹{·...baseMessage,·files:·[{·_id:·'fileId',·name:·'file.png',·type:·'image/png',·format:·'png',·size:·123·}]·},⏎↹`
Comment thread
nazabucciarelli marked this conversation as resolved.
Outdated
expect(useKeepMountedMessages(messages)).toEqual([0]);
});

it('should keep mounted a message with a URL preview', () => {
const messages: IMessage[] = [{ ...baseMessage, urls: [{ url: 'https://example.com', meta: { title: 'Example' } }] }];
expect(useKeepMountedMessages(messages)).toEqual([0]);
});

it('should not keep mounted a message with a URL that has no preview metadata', () => {
const messages: IMessage[] = [{ ...baseMessage, urls: [{ url: 'https://example.com', meta: {} }] }];
expect(useKeepMountedMessages(messages)).toEqual([]);
});

it('should keep mounted a message with a rich attachment from an app/integration (e.g. /giphy)', () => {
const attachments: MessageAttachment[] = [{ title: 'GIPHY', image_url: 'https://media.giphy.com/foo.gif' }];
const messages: IMessage[] = [{ ...baseMessage, attachments }];
expect(useKeepMountedMessages(messages)).toEqual([0]);
});

it('should not keep mounted a message whose only attachment is a file attachment', () => {
const attachments: MessageAttachment[] = [{ type: 'file', title: 'file.png' } as MessageAttachment];
const messages: IMessage[] = [{ ...baseMessage, attachments }];
expect(useKeepMountedMessages(messages)).toEqual([]);
});

it('should not keep mounted a message whose only attachment is a quote attachment', () => {
const attachments: MessageAttachment[] = [
{ author_name: 'someone', author_icon: 'icon', message_link: 'https://example.com/msg', text: 'quoted text' },
];
const messages: IMessage[] = [{ ...baseMessage, attachments }];
expect(useKeepMountedMessages(messages)).toEqual([]);
});

it('should keep mounted a message quoting another message that had a rich attachment (e.g. quoting a /giphy gif)', () => {
const attachments: MessageAttachment[] = [
{
author_name: 'someone',
author_icon: 'icon',
message_link: 'https://example.com/msg',
text: 'quoted text',
attachments: [{ title: 'GIPHY', image_url: 'https://media.giphy.com/foo.gif' }],
},
];
const messages: IMessage[] = [{ ...baseMessage, attachments }];
expect(useKeepMountedMessages(messages)).toEqual([0]);
});

it('should not keep mounted a message quoting another message whose only nested attachment is a file attachment', () => {
const attachments: MessageAttachment[] = [
{
author_name: 'someone',
author_icon: 'icon',
message_link: 'https://example.com/msg',
text: 'quoted text',
attachments: [{ type: 'file', title: 'file.png' } as MessageAttachment],
},
];
const messages: IMessage[] = [{ ...baseMessage, attachments }];
expect(useKeepMountedMessages(messages)).toEqual([]);
});

it('should keep mounted a message with a chain of nested quotes ending in a rich attachment', () => {
const attachments: MessageAttachment[] = [
{
author_name: 'someone',
author_icon: 'icon',
message_link: 'https://example.com/msg1',
text: 'quoted text 1',
attachments: [
{
author_name: 'someone-else',
author_icon: 'icon',
message_link: 'https://example.com/msg2',
text: 'quoted text 2',
attachments: [{ title: 'GIPHY', image_url: 'https://media.giphy.com/foo.gif' }],
},
],
},
];
const messages: IMessage[] = [{ ...baseMessage, attachments }];
expect(useKeepMountedMessages(messages)).toEqual([0]);
});

it('should offset indexes by 1 when canPreview is true', () => {
const attachments: MessageAttachment[] = [{ title: 'GIPHY', image_url: 'https://media.giphy.com/foo.gif' }];
const messages: IMessage[] = [{ ...baseMessage }, { ...baseMessage, _id: 'messageId2', attachments }];
expect(useKeepMountedMessages(messages, true)).toEqual([2]);
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import type { IMessage } from '@rocket.chat/core-typings';
import type { IMessage, MessageAttachment } from '@rocket.chat/core-typings';
import { isFileAttachment, isQuoteAttachment } from '@rocket.chat/core-typings';

// Quote attachments can nest their own `attachments` (e.g. quoting a message that had a
// /giphy gif), which QuoteAttachment renders recursively, so this needs to look past them too.
const hasCollapsibleAttachment = (attachments: MessageAttachment[] | undefined): boolean =>
attachments?.some((attachment) => {
if (isFileAttachment(attachment)) {
return false;
}
if (isQuoteAttachment(attachment)) {
return hasCollapsibleAttachment(attachment.attachments);
}
return true;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}) ?? false;
Comment thread
nazabucciarelli marked this conversation as resolved.
Outdated

export const useKeepMountedMessages = (messages: IMessage[], canPreview: boolean = false): number[] => {
const offset = canPreview ? 1 : 0;
return messages.reduce<number[]>((acc, message, index) => {
// Keep mounted anything with an embed that reloads when re-mounted: file
// attachments (audio/video players) and URL previews (e.g. YouTube iframes).
// Otherwise virtua recycles them on scroll-to-bottom (new message / reaction
// growing a message) and the iframe flickers.
// Keep mounted anything with an embed that reloads or resets local UI state when
// re-mounted: file attachments (audio/video players), URL previews (e.g. YouTube
// iframes), and rich attachments from apps/integrations (e.g. /giphy) that hold their
// own collapse state via useCollapse. Otherwise virtua recycles them on scroll-to-bottom
// (new message / reaction growing a message) and the iframe flickers, or the
// collapsed/expanded state resets to the site default.
const hasUrlPreview = message.urls?.some((url) => Object.keys(url.meta ?? {}).length > 0 || !!url.headers) ?? false;
if ((message.files?.length ?? 0) > 0 || hasUrlPreview) {
if ((message.files?.length ?? 0) > 0 || hasUrlPreview || hasCollapsibleAttachment(message.attachments)) {
acc.push(index + offset);
}
return acc;
Expand Down
Loading