From 5b5b4a5d163463c81aeb73bfb2f885d2614a7bb0 Mon Sep 17 00:00:00 2001 From: Kush Kumar Date: Tue, 27 Jan 2026 08:43:43 +0000 Subject: [PATCH 1/5] feat(channel-settings): add per-channel link preview setting (#7734) --- .../server/methods/saveRoomSettings.ts | 7 ++++++ .../Info/EditRoomInfo/EditRoomInfo.tsx | 22 +++++++++++++++++++ .../EditRoomInfo/useEditRoomInitialValues.ts | 2 ++ .../messages/hooks/AfterSaveOEmbed.ts | 12 +++++++++- packages/core-typings/src/IRoom.ts | 8 +++++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 56dbef2d1cdb1..1f8c5bc312078 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -44,6 +44,7 @@ type RoomSettings = { retentionIgnoreThreads: boolean; retentionOverrideGlobal: boolean; encrypted: boolean; + linksEmbed: boolean; favorite: { favorite: boolean; defaultValue: boolean; @@ -265,6 +266,10 @@ const settingSavers: RoomSettingsSavers = { await saveRoomTopic(rid, value, user); } }, + async linksEmbed({ value, rid }) { + // This saves the value directly to the room document in MongoDB + await Rooms.updateOne({ _id: rid }, { $set: { linksEmbed: value } }); + }, async roomAnnouncement({ value, room, rid, user }) { if (!value && !room.announcement) { return; @@ -386,6 +391,8 @@ const fields: (keyof RoomSettings)[] = [ 'retentionIgnoreThreads', 'retentionOverrideGlobal', 'encrypted', + + 'linksEmbed', 'favorite', ]; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx index 59c198f669c17..46d6a91b4b391 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -228,6 +228,9 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => const retentionExcludePinnedField = useId(); const retentionFilesOnlyField = useId(); const retentionIgnoreThreads = useId(); + + + const linksEmbedField = useId(); const showAdvancedSettings = canViewReadOnly || readOnly || canViewArchived || canViewJoinCode || canViewHideSysMes; const showRetentionPolicy = canEditRoomRetentionPolicy && retentionPolicy?.enabled; @@ -489,6 +492,25 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => )} + {/* START OF NEW LINK PREVIEW TOGGLE */} + + + Enable Link Previews + ( + + )} + /> + + + {/* END OF NEW LINK PREVIEW TOGGLE */} )} diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts index 5aeb06a3a2114..29589ec9c28dd 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -28,6 +28,7 @@ export type EditRoomInfoFormData = { showChannels: boolean; showDiscussions: boolean; joinCode: string; + linksEmbed: boolean; systemMessages: MessageTypesValues[]; }; @@ -52,6 +53,7 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy): Partia joinCodeRequired: !!joinCodeRequired, systemMessages: Array.isArray(sysMes) ? sysMes : [], hideSysMes: Array.isArray(sysMes) ? !!sysMes?.length : !!sysMes, + linksEmbed: room.linksEmbed !== false, encrypted, ...(canEditRoomRetentionPolicy && retentionPolicy?.enabled && { diff --git a/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts b/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts index 3623b3ddd90ae..caf13b194371c 100644 --- a/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts +++ b/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts @@ -8,7 +8,7 @@ import type { } from '@rocket.chat/core-typings'; import { isOEmbedUrlWithMetadata } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; -import { OEmbedCache, Messages } from '@rocket.chat/models'; +import { OEmbedCache, Messages, Rooms } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import he from 'he'; import iconv from 'iconv-lite'; @@ -327,13 +327,23 @@ const getRelevantMetaTags = function (metaObj: OEmbedMeta): Record oembedHtml?.replace('iframe', 'iframe style="max-width: 100%;width:400px;height:225px"'); + const rocketUrlParser = async function (message: IMessage): Promise { log.debug({ msg: 'Parsing message URLs' }); + // 1. Keep the Global Check if (!settings.get('API_Embed')) { return message; } + // 2. NEW: Check the Per-Channel Setting + // We fetch the room to see if the user disabled previews for this specific channel. + const room = await Rooms.findOneById(message.rid, { projection: { linksEmbed: 1 } }); + if (room?.linksEmbed === false) { + return message; // STOP here if the room says "No Previews" + } + + // 3. Keep the rest of the existing checks... if (!Array.isArray(message.urls)) { return message; } diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index fb243a8f92e52..481eca77932d1 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -10,6 +10,10 @@ import type { RoomType } from './RoomType'; import type { Branded } from './utils'; export interface IRoom extends IRocketChatRecord { + /** + * Enable or disable link previews for this room. If undefined, falls back to global setting. + */ + linksEmbed?: boolean; t: RoomType; name?: string; fname?: string; @@ -361,6 +365,10 @@ export type RoomAdminFieldsType = | 'abacAttributes'; export interface IRoomWithRetentionPolicy extends IRoom { + /** + * Enable or disable link previews for this room. If undefined, falls back to global setting. + */ + linksEmbed?: boolean; retention: { enabled?: boolean; maxAge: number; From d131eb66f36c83e506bdc3b6e9b454664e1b2f83 Mon Sep 17 00:00:00 2001 From: Kush Kumar Date: Tue, 27 Jan 2026 09:31:46 +0000 Subject: [PATCH 2/5] chore: add changeset --- .changeset/cold-vans-suffer.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/cold-vans-suffer.md diff --git a/.changeset/cold-vans-suffer.md b/.changeset/cold-vans-suffer.md new file mode 100644 index 0000000000000..3d195b9a22b21 --- /dev/null +++ b/.changeset/cold-vans-suffer.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/core-typings': patch +'@rocket.chat/meteor': patch +--- + +Added per-channel link preview setting From a40718f05583f7e103b01c77278b8ed2bddb185e Mon Sep 17 00:00:00 2001 From: Kush Kumar Date: Tue, 27 Jan 2026 09:54:32 +0000 Subject: [PATCH 3/5] refactor: use translation key and optimize db lookup --- .../Info/EditRoomInfo/EditRoomInfo.tsx | 6 +++--- .../services/messages/hooks/AfterSaveOEmbed.ts | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx index 46d6a91b4b391..bf7a428e71074 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -492,10 +492,10 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => )} - {/* START OF NEW LINK PREVIEW TOGGLE */} + - Enable Link Previews + {t('Enable_Link_Previews')} /> - {/* END OF NEW LINK PREVIEW TOGGLE */} + )} diff --git a/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts b/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts index caf13b194371c..15d6a7768955b 100644 --- a/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts +++ b/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts @@ -328,23 +328,25 @@ const insertMaxWidthInOembedHtml = (oembedHtml?: string): string | undefined => oembedHtml?.replace('iframe', 'iframe style="max-width: 100%;width:400px;height:225px"'); + const rocketUrlParser = async function (message: IMessage): Promise { log.debug({ msg: 'Parsing message URLs' }); - // 1. Keep the Global Check + // 1. Global Check stays at the top if (!settings.get('API_Embed')) { return message; } - // 2. NEW: Check the Per-Channel Setting - // We fetch the room to see if the user disabled previews for this specific channel. - const room = await Rooms.findOneById(message.rid, { projection: { linksEmbed: 1 } }); - if (room?.linksEmbed === false) { - return message; // STOP here if the room says "No Previews" + // 2. CHECK URLs FIRST (Optimization) + // If the message has no URLs, we stop here. We don't bother the database. + if (!Array.isArray(message.urls)) { + return message; } - // 3. Keep the rest of the existing checks... - if (!Array.isArray(message.urls)) { + // 3. NOW check the Room Setting + // We only fetch the room data if we know we actually need to process URLs. + const room = await Rooms.findOneById(message.rid, { projection: { linksEmbed: 1 } }); + if (room?.linksEmbed === false) { return message; } From 69d2686851455c3b114573c075842e64fb7f2c23 Mon Sep 17 00:00:00 2001 From: Kush Kumar Date: Tue, 27 Jan 2026 10:00:58 +0000 Subject: [PATCH 4/5] refactor: use translation key and optimize db lookup --- packages/i18n/src/locales/en.i18n.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index e3ac97c6b6dc8..bb33d84ee836a 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2062,6 +2062,7 @@ "Enterprise_capabilities": "Enterprise capabilities", "Enterprise_capability": "Enterprise capability", "Entertainment": "Entertainment", + "Enable_Link_Previews": "Enable Link Previews", "Error": "Error", "Error_404": "Error:404", "Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances": "Error: Rocket.Chat requires oplog tailing when running in multiple instances", From 57f82a21554799ca6ae1e7bacbc290a2403da758 Mon Sep 17 00:00:00 2001 From: Kush Kumar Date: Tue, 27 Jan 2026 10:37:25 +0000 Subject: [PATCH 5/5] style: add field hint and remove inline comments per review --- .../Info/EditRoomInfo/EditRoomInfo.tsx | 9 ++++----- .../services/messages/hooks/AfterSaveOEmbed.ts | 15 --------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx index bf7a428e71074..ddd953d0e2257 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -228,8 +228,6 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => const retentionExcludePinnedField = useId(); const retentionFilesOnlyField = useId(); const retentionIgnoreThreads = useId(); - - const linksEmbedField = useId(); const showAdvancedSettings = canViewReadOnly || readOnly || canViewArchived || canViewJoinCode || canViewHideSysMes; @@ -492,10 +490,10 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => )} - + - {t('Enable_Link_Previews')} + {t('Enable_Link_Previews' as any)} {...field} checked={value} disabled={isFederated} + aria-describedby={`${linksEmbedField}-hint`} /> )} /> + {t('Enable_Link_Previews_Description' as any)} - )} diff --git a/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts b/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts index 15d6a7768955b..9cc6c9a99cc66 100644 --- a/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts +++ b/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts @@ -331,27 +331,17 @@ const insertMaxWidthInOembedHtml = (oembedHtml?: string): string | undefined => const rocketUrlParser = async function (message: IMessage): Promise { log.debug({ msg: 'Parsing message URLs' }); - - // 1. Global Check stays at the top if (!settings.get('API_Embed')) { return message; } - - // 2. CHECK URLs FIRST (Optimization) - // If the message has no URLs, we stop here. We don't bother the database. if (!Array.isArray(message.urls)) { return message; } - - // 3. NOW check the Room Setting - // We only fetch the room data if we know we actually need to process URLs. const room = await Rooms.findOneById(message.rid, { projection: { linksEmbed: 1 } }); if (room?.linksEmbed === false) { return message; } - log.debug({ msg: 'URLs found in message', count: message.urls.length }); - if ( (message.attachments && message.attachments.length > 0) || message.urls.filter((item) => !item.url.includes(settings.get('Site_Url'))).length > MAX_EXTERNAL_URL_PREVIEWS @@ -359,24 +349,19 @@ const rocketUrlParser = async function (message: IMessage): Promise { log.debug({ msg: 'All URLs ignored for OEmbed' }); return message; } - let changed = false; for await (const item of message.urls) { if (item.ignoreParse === true) { log.debug({ msg: 'URL ignored for OEmbed', url: item.url }); continue; } - const { urlPreview, foundMeta } = await parseUrl(item.url); - Object.assign(item, foundMeta ? urlPreview : {}); changed = changed || foundMeta; } - if (changed === true) { await Messages.setUrlsById(message._id, message.urls); } - return message; };