diff --git a/.changeset/embed-link-previews.md b/.changeset/embed-link-previews.md new file mode 100644 index 0000000000000..1edce178e0efe --- /dev/null +++ b/.changeset/embed-link-previews.md @@ -0,0 +1,10 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/models': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/i18n': minor +--- + +Adds per-channel setting to disable link previews. diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 0a9b282160619..0551d41d01e70 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -48,6 +48,7 @@ type RoomSettings = { favorite: boolean; defaultValue: boolean; }; + disableLinkPreviews: boolean; }; type RoomSettingsValidators = { @@ -351,6 +352,9 @@ const settingSavers: RoomSettingsSavers = { async roomAvatar({ value, rid, user }) { await setRoomAvatar(rid, value, user); }, + async disableLinkPreviews({ value, rid }) { + await Rooms.saveDisableLinkPreviewsById(rid, value); + }, }; declare module '@rocket.chat/ddp-client' { @@ -387,6 +391,7 @@ const fields: (keyof RoomSettings)[] = [ 'retentionOverrideGlobal', 'encrypted', 'favorite', + 'disableLinkPreviews', ]; const validate = ( 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 e3f2d079b0fd2..d5ae3e22077ce 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -120,6 +120,7 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => archived, joinCodeRequired, hideSysMes, + disableLinkPreviews, retentionEnabled, retentionOverrideGlobal, roomType: roomTypeP, @@ -223,6 +224,7 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => const archivedField = useId(); const joinCodeRequiredField = useId(); const hideSysMesField = useId(); + const disableLinkPreviewsField = useId(); const retentionEnabledField = useId(); const retentionOverrideGlobalField = useId(); const retentionMaxAgeField = useId(); @@ -490,6 +492,20 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => )} + {canViewHideSysMes && ( + + + {t('Disable_Link_Previews')} + ( + + )} + /> + + + )} )} 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..6429bd2931a09 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -18,6 +18,7 @@ export type EditRoomInfoFormData = { archived: boolean; joinCodeRequired: boolean; hideSysMes: boolean; + disableLinkPreviews: boolean; encrypted: boolean; retentionEnabled: boolean; retentionOverrideGlobal: boolean; @@ -35,7 +36,7 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy): Partia const retentionPolicy = useRetentionPolicy(room); const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); - const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly } = room; + const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly, disableLinkPreviews } = room; return useMemo( (): Partial => ({ @@ -52,6 +53,7 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy): Partia joinCodeRequired: !!joinCodeRequired, systemMessages: Array.isArray(sysMes) ? sysMes : [], hideSysMes: Array.isArray(sysMes) ? !!sysMes?.length : !!sysMes, + disableLinkPreviews: !!disableLinkPreviews, encrypted, ...(canEditRoomRetentionPolicy && retentionPolicy?.enabled && { @@ -77,6 +79,7 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy): Partia topic, encrypted, reactWhenReadOnly, + disableLinkPreviews, canEditRoomRetentionPolicy, ], ); diff --git a/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts b/apps/meteor/server/services/messages/hooks/AfterSaveOEmbed.ts index 2c1144d1f6904..320c0a4284ccc 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 { isAbsoluteURL } from '@rocket.chat/tools'; import he from 'he'; @@ -340,6 +340,13 @@ const rocketUrlParser = async function (message: IMessage): Promise { return message; } + if (message.rid) { + const room = await Rooms.findOneById(message.rid, { projection: { disableLinkPreviews: 1 } }); + if (room?.disableLinkPreviews) { + return message; + } + } + log.debug({ msg: 'URLs found in message', count: message.urls.length }); if ( diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 278bd66c23ab7..5db8fbc1d25a2 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -32,6 +32,8 @@ export interface IRoom extends IRocketChatRecord { // TODO: this boolean might be an accident sysMes?: MessageTypesValues[] | boolean; + disableLinkPreviews?: boolean; + u: Pick; uids?: Array; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 4a385b345cf7f..b968a396143c3 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1819,6 +1819,7 @@ "Disable": "Disable", "Disable_E2E_encryption": "Disable E2E encryption", "Disable_Facebook_integration": "Disable Facebook integration", + "Disable_Link_Previews": "Disable link previews", "Disable_Notifications": "Disable Notifications", "Disable_another_app": "Disable another app or upgrade to a Premium plan to enable this app.", "Disable_at_least_more_apps": "You will need to disable at least {{numberOfExceededApps}} other apps or upgrade to a Premium plan to enable this app.", diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 39ce7d7ae3379..0a552873017a0 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -302,6 +302,7 @@ export interface IRoomsModel extends IBaseModel { saveFeaturedById(rid: string, featured: boolean): Promise; saveDefaultById(rid: string, defaultValue: boolean): Promise; saveFavoriteById(rid: string, favorite: boolean, defaultValue: boolean): Promise; + saveDisableLinkPreviewsById(rid: string, disableLinkPreviews: boolean): Promise; saveRetentionEnabledById(rid: string, retentionEnabled: boolean): Promise; saveRetentionMaxAgeById(rid: string, retentionMaxAge: number): Promise; saveRetentionExcludePinnedById(rid: string, retentionExcludePinned: boolean): Promise; diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index c930207f79b13..029532836c133 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -1897,6 +1897,20 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateOne(query, update); } + saveDisableLinkPreviewsById(_id: IRoom['_id'], value: boolean): Promise { + const query: Filter = { _id }; + + const update: UpdateFilter = {}; + + if (value == null) { + update.$unset = { disableLinkPreviews: 1 }; + } else { + update.$set = { disableLinkPreviews: value }; + } + + return this.updateOne(query, update); + } + saveRetentionEnabledById(_id: IRoom['_id'], value: boolean): Promise { const query: Filter = { _id }; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index eff60fe5c1ee5..61d4e98c1e852 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -382,6 +382,7 @@ type RoomsSaveRoomSettingsProps = { retentionFilesOnly?: boolean; retentionIgnoreThreads?: boolean; retentionOverrideGlobal?: boolean; + disableLinkPreviews?: boolean; }; const RoomsSaveRoomSettingsSchema = { @@ -466,6 +467,7 @@ const RoomsSaveRoomSettingsSchema = { retentionFilesOnly: { type: 'boolean', nullable: true }, retentionIgnoreThreads: { type: 'boolean', nullable: true }, retentionOverrideGlobal: { type: 'boolean', nullable: true }, + disableLinkPreviews: { type: 'boolean', nullable: true }, }, required: ['rid'], additionalProperties: false,