diff --git a/Dockerfile b/Dockerfile index 7c6abd406f..9991cb9ece 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ## Base -FROM --platform=$BUILDPLATFORM node:24.13.1-alpine AS base +FROM --platform=$BUILDPLATFORM node:24.15.0-alpine AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable diff --git a/src/app/components/setting-menu-selector/options.ts b/src/app/components/setting-menu-selector/options.ts index c9a2e50c1e..5ff7f03928 100644 --- a/src/app/components/setting-menu-selector/options.ts +++ b/src/app/components/setting-menu-selector/options.ts @@ -40,6 +40,16 @@ export const PER_ROOM_SHOW_ROOM_ICON_OPTIONS: SettingMenuOption[] = [ + { value: PRIVACY_BLUR_DEFAULT, label: 'Default' }, + { value: 'on', label: 'Blur' }, + { value: 'off', label: "Don't Blur" }, +]; + /** Labels are the current date rendered in each pattern, so they are built at render. */ export const DATE_FORMATS: DateFormat[] = [ 'D MMM YYYY', diff --git a/src/app/features/common-settings/cosmetics/Cosmetics.tsx b/src/app/features/common-settings/cosmetics/Cosmetics.tsx index 90de9a01c2..dafa76c725 100644 --- a/src/app/features/common-settings/cosmetics/Cosmetics.tsx +++ b/src/app/features/common-settings/cosmetics/Cosmetics.tsx @@ -42,6 +42,14 @@ import { CustomStateEvent } from '$types/matrix/room'; import { AvatarUploadTile } from '$components/avatar-upload-tile/AvatarUploadTile'; import type { CustomRoomMemberEventContent } from '$unstable/CustomRoomMemberEventContent'; import * as prefix from '$unstable/prefixes'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; +import { + PER_ROOM_PRIVACY_BLUR_OPTIONS, + PRIVACY_BLUR_DEFAULT, + SettingMenuSelector, + type PrivacyBlurValue, +} from '$components/setting-menu-selector'; const log = createLogger('Cosmetics'); @@ -237,6 +245,33 @@ function CosmeticsFont({ ); } +function SelectPerRoomPrivacyBlur({ roomId }: { roomId: string }) { + const [perRoomBlurArray, setPerRoomBlurArray] = useSetting(settingsAtom, 'perRoomPrivacyBlur'); + const override = perRoomBlurArray?.find((item) => item.roomId === roomId); + const value: PrivacyBlurValue = + override === undefined ? PRIVACY_BLUR_DEFAULT : override.blur ? 'on' : 'off'; + + const handleSelect = (next: PrivacyBlurValue) => { + const filtered = perRoomBlurArray.filter((item) => item.roomId !== roomId); + setPerRoomBlurArray( + next === PRIVACY_BLUR_DEFAULT ? filtered : [...filtered, { roomId, blur: next === 'on' }] + ); + }; + + return ( + ( + + {selected ? {option.label} : option.label} + + )} + /> + ); +} + type CosmeticsProps = { requestBack?: () => void; requestClose: () => void; @@ -407,12 +442,20 @@ export function Cosmetics({ requestBack, requestClose }: CosmeticsProps) { Settings - + {!isSpace && ( + + } + /> + + )} entry.roomId === activeRoomId) + : undefined; + const effectiveBlurMedia = roomOverride ? roomOverride.blur : blurMedia; useEffect(() => { - document.body.classList.toggle('sable-blur-media', blurMedia); + document.body.classList.toggle('sable-blur-media', effectiveBlurMedia); document.body.classList.toggle('sable-blur-avatars', blurAvatars); document.body.classList.toggle('sable-blur-emotes', blurEmotes); - }, [blurMedia, blurAvatars, blurEmotes]); + }, [effectiveBlurMedia, blurAvatars, blurEmotes]); return null; } diff --git a/src/app/pages/pathUtils.ts b/src/app/pages/pathUtils.ts index c8493c93cf..7b48e16e41 100644 --- a/src/app/pages/pathUtils.ts +++ b/src/app/pages/pathUtils.ts @@ -266,6 +266,29 @@ export const resolveSection = (pathname: string): SectionNav | null => { return null; }; +const ROOM_ID_OR_ALIAS_PATH_PATTERNS = [ + HOME_ROOM_PATH, + HOME_ROOM_FORUM_PATH, + DIRECT_ROOM_PATH, + DIRECT_ROOM_FORUM_PATH, + SPACE_ROOM_PATH, + SPACE_ROOM_FORUM_PATH, +]; + +/** + * Extracts the room id/alias segment straight from a pathname, independent of any + * navigation history. Route components normally get this via `useParams`, but that + * requires being mounted inside the matched route element — callers mounted above + * the router (e.g. app-wide feature components) need to match the pathname directly. + */ +export const matchRoomIdOrAlias = (pathname: string): string | undefined => { + for (const pattern of ROOM_ID_OR_ALIAS_PATH_PATTERNS) { + const encoded = matchPath({ path: pattern, end: false }, pathname)?.params.roomIdOrAlias; + if (encoded) return decodeURIComponent(encoded); + } + return undefined; +}; + export const getSettingsPath = (section?: string, focus?: string): string => { const path = trimTrailingSlash(generatePath(SETTINGS_PATH, { section: section ?? null })); if (!focus) return path; diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 22e5287c7f..920c7f818e 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -43,6 +43,11 @@ export type PerRoomShowRoomIcon = { display: ShowRoomIcon; }; +export type PerRoomPrivacyBlur = { + roomId: string; + blur: boolean; +}; + export type JumboEmojiSize = 'none' | 'extraSmall' | 'small' | 'normal' | 'large' | 'extraLarge'; /** Reorderable inline trigger buttons in the message composer. */ @@ -248,6 +253,7 @@ export interface Settings { showPersonaSetting: boolean; closeFoldersByDefault: boolean; perRoomShowRoomIcon: PerRoomShowRoomIcon[]; + perRoomPrivacyBlur: PerRoomPrivacyBlur[]; showRoomIcon: ShowRoomIcon; roomIconOverlay: boolean; showRoomBanners: boolean; @@ -443,6 +449,7 @@ export const defaultSettings: Settings = { showPersonaSetting: false, closeFoldersByDefault: false, perRoomShowRoomIcon: [], + perRoomPrivacyBlur: [], showRoomIcon: ShowRoomIcon.Strict, roomIconOverlay: true, showRoomBanners: true,