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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Box } from '@rocket.chat/fuselage';
import DOMPurify from 'dompurify';
import { useMemo } from 'react';

import OEmbedCollapsible from './OEmbedCollapsible';
import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata';
Expand All @@ -10,10 +11,14 @@ const purifyOptions = {
ALLOW_UNKNOWN_PROTOCOLS: true,
};

const OEmbedHtmlPreview = ({ html, ...props }: OEmbedPreviewMetadata) => (
<OEmbedCollapsible {...props}>
{html && <Box withRichContent dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html, purifyOptions) }} />}
</OEmbedCollapsible>
);
const OEmbedHtmlPreview = ({ html, ...props }: OEmbedPreviewMetadata) => {
// Memoize the dangerouslySetInnerHTML object so its identity is stable across
// re-renders. React 19 re-applies innerHTML when this object is a new reference
// (even with an identical string), which reloads embedded iframes (e.g. YouTube)
// on every re-render — visible as a flicker on new messages / reactions.
const dangerous = useMemo(() => (html ? { __html: DOMPurify.sanitize(html, purifyOptions) } : undefined), [html]);

return <OEmbedCollapsible {...props}>{dangerous && <Box withRichContent dangerouslySetInnerHTML={dangerous} />}</OEmbedCollapsible>;
};

export default OEmbedHtmlPreview;
14 changes: 8 additions & 6 deletions apps/meteor/client/contexts/EmojiPickerContext.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, useContext } from 'react';
import { createContext, useContext, useMemo } from 'react';

import type { EmojiPickerItem, CategoriesIndexes } from '../../app/emoji/client';

Expand Down Expand Up @@ -33,11 +33,13 @@ const useEmojiPickerContext = (): EmojiPickerContextValue => {
return context;
};

export const useEmojiPicker = () => ({
open: useEmojiPickerContext().open,
isOpen: useEmojiPickerContext().isOpen,
close: useEmojiPickerContext().close,
});
export const useEmojiPicker = () => {
const { open, isOpen, close } = useEmojiPickerContext();
// Stable identity: consumers (e.g. ChatAPI.emojiPicker) use this as a memo
// dependency, so returning a fresh object each render invalidates them and
// cascades re-renders across the whole message list.
return useMemo(() => ({ open, isOpen, close }), [open, isOpen, close]);
};

export const usePreviewEmoji = () => ({
emojiToPreview: useEmojiPickerContext().emojiToPreview,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ const EmojiPickerProvider = ({ children }: EmojiPickerProviderProps) => {
return setEmojiPicker(<EmojiPicker reference={ref} onClose={() => setEmojiPicker(null)} onPickEmoji={(emoji) => callback(emoji)} />);
}, []);

const close = useCallback(() => setEmojiPicker(null), []);

const handlePreview = useCallback(
(emoji: string, name: string) =>
setEmojiToPreview((preview) => {
Expand All @@ -96,7 +98,7 @@ const EmojiPickerProvider = ({ children }: EmojiPickerProviderProps) => {
const contextValue = useMemo(
(): ContextType<typeof EmojiPickerContext> => ({
isOpen: emojiPicker !== null,
close: () => setEmojiPicker(null),
close,
open,
emojiToPreview,
handlePreview,
Expand All @@ -117,6 +119,7 @@ const EmojiPickerProvider = ({ children }: EmojiPickerProviderProps) => {
[
emojiPicker,
open,
close,
emojiToPreview,
addRecentEmoji,
emojiListByCategory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import type { IMessage } from '@rocket.chat/core-typings';
export const useKeepMountedMessages = (messages: IMessage[], canPreview: boolean = false): number[] => {
const offset = canPreview ? 1 : 0;
return messages.reduce<number[]>((acc, message, index) => {
if (message.files?.length && message.files.length > 0) {
// 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.
const hasUrlPreview = message.urls?.some((url) => Object.keys(url.meta ?? {}).length > 0 || !!url.headers) ?? false;
if ((message.files?.length ?? 0) > 0 || hasUrlPreview) {
acc.push(index + offset);
}
return acc;
Expand Down
Loading