Skip to content
Open
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/app/components/setting-menu-selector/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ export const PER_ROOM_SHOW_ROOM_ICON_OPTIONS: SettingMenuOption<ShowRoomIconValu
...SHOW_ROOM_ICON_OPTIONS,
];

/** Sentinel for rooms that follow the account wide media-blur setting. */
export const PRIVACY_BLUR_DEFAULT = 'default';
export type PrivacyBlurValue = typeof PRIVACY_BLUR_DEFAULT | 'on' | 'off';

export const PER_ROOM_PRIVACY_BLUR_OPTIONS: SettingMenuOption<PrivacyBlurValue>[] = [
{ 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',
Expand Down
55 changes: 49 additions & 6 deletions src/app/features/common-settings/cosmetics/Cosmetics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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 (
<SettingMenuSelector
value={value}
options={PER_ROOM_PRIVACY_BLUR_OPTIONS}
onSelect={handleSelect}
renderOption={({ option, selected }) => (
<Box grow="Yes">
<Text size="T300">{selected ? <b>{option.label}</b> : option.label}</Text>
</Box>
)}
/>
);
}

type CosmeticsProps = {
requestBack?: () => void;
requestClose: () => void;
Expand Down Expand Up @@ -407,12 +442,20 @@ export function Cosmetics({ requestBack, requestClose }: CosmeticsProps) {
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Settings</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
></SequenceCard>
{!isSpace && (
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Blur Media"
description="Override your account-wide media-blurring preference for this room only."
after={<SelectPerRoomPrivacyBlur roomId={room.roomId} />}
/>
</SequenceCard>
)}
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
Expand Down
18 changes: 16 additions & 2 deletions src/app/pages/client/client-non-ui/appearance.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router';
import { useSetting } from '$state/hooks/settings';
import { settingsAtom } from '$state/settings';
import { matchRoomIdOrAlias } from '$pages/pathUtils';
import { useResolvedRoomIdOrAlias } from '$hooks/router/useResolvedRoomId';

export function SystemEmojiFeature() {
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
Expand Down Expand Up @@ -30,12 +33,23 @@ export function PrivacyBlurFeature() {
const [blurMedia] = useSetting(settingsAtom, 'privacyBlur');
const [blurAvatars] = useSetting(settingsAtom, 'privacyBlurAvatars');
const [blurEmotes] = useSetting(settingsAtom, 'privacyBlurEmotes');
const [perRoomBlur] = useSetting(settingsAtom, 'perRoomPrivacyBlur');

const location = useLocation();
// Read straight from the URL rather than any "last visited room" bookkeeping,
// so this works on a fresh load/refresh and not just after in-app navigation.
const roomIdOrAlias = matchRoomIdOrAlias(location.pathname);
const { roomId: activeRoomId } = useResolvedRoomIdOrAlias(roomIdOrAlias);
const roomOverride = activeRoomId
? perRoomBlur.find((entry) => 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;
}
23 changes: 23 additions & 0 deletions src/app/pages/pathUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/app/state/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -248,6 +253,7 @@ export interface Settings {
showPersonaSetting: boolean;
closeFoldersByDefault: boolean;
perRoomShowRoomIcon: PerRoomShowRoomIcon[];
perRoomPrivacyBlur: PerRoomPrivacyBlur[];
showRoomIcon: ShowRoomIcon;
roomIconOverlay: boolean;
showRoomBanners: boolean;
Expand Down Expand Up @@ -443,6 +449,7 @@ export const defaultSettings: Settings = {
showPersonaSetting: false,
closeFoldersByDefault: false,
perRoomShowRoomIcon: [],
perRoomPrivacyBlur: [],
showRoomIcon: ShowRoomIcon.Strict,
roomIconOverlay: true,
showRoomBanners: true,
Expand Down
Loading