Compare commits

...
Author SHA1 Message Date
jared 508a123150 Merge pull request 'fix(rooms): "Rename for me" names in Ctrl+K, forward/share pickers and notifications (#336)' (#359) from fix/local-room-names-everywhere into lotus
CI / Build & Quality Checks (push) Successful in 3m37s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 2s
CI / Playwright smoke (e2e) (push) Successful in 15m26s
2026-10-12 16:44:08 -04:00
jared fe61352d88 Merge pull request 'fix(messages): collapsed long messages work with the keyboard and on any background (#343)' (#358) from fix/collapsible-messages into lotus
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
2026-10-12 16:43:59 -04:00
jared 0bec28b651 Merge pull request 'fix(logout): clear the call-volume list and settings-sync stamp (#347)' (#357) from fix/logout-volume-residue into lotus
CI / Build & Quality Checks (push) Successful in 3m9s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 8s
CI / Trigger Desktop Build (push) Successful in 5s
CI / Playwright smoke (e2e) (push) Canceled after 6s
2026-10-12 16:07:43 -04:00
jared 75089ecd3f Merge pull request 'fix(settings): "ENTER for Newline" stays per device (#348)' (#356) from fix/enter-newline-device-local into lotus
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
2026-10-12 16:07:33 -04:00
Lotus CIandClaude Opus 5.5 a4a163df3d fix(rooms): "Rename for me" names in Ctrl+K, forward/share pickers and notifications (#336)
CI / Build & Quality Checks (pull_request) Successful in 3m4s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 13m29s
The personal room name (io.lotus.room_names) was only used by the
sidebar, header, intro and call bar; the quick switcher couldn't find a
room by it ("No match found") and showed the server name, as did the
forward/share pickers, toasts, OS notifications and reminders.

useRoomMeta gains useLocalRoomNames() / getLocalRoomName(); the quick
switcher matches both names and shows the personal one, the forward and
share pickers show and filter by it, and message/reminder notifications
use it for the room title.

Verified headless with general renamed to "Zebra Lounge": Ctrl+K "Zebra"
and "general" both list "Zebra Lounge"; the forward picker filtered by
"zebra" lists "Zebra Lounge".

Not covered here (follow-up): message-search result headers, the inbox
notifications page and the threads list still use the server name.

Refs #336

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-12 15:40:56 -04:00
Lotus CIandClaude Opus 5.5 70f1d640ed fix(messages): collapsed long messages work with the keyboard and on any background (#343)
CI / Build & Quality Checks (pull_request) Successful in 3m4s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m15s
- Tab into a link in the hidden part used to scroll the overflow:hidden box
  itself (top cut off, and it stayed that way); focus inside now expands
  the message, and collapsing resets the scroll.
- "Read more" / "Show less" is announced as a toggle (aria-expanded,
  aria-controls); the arrows are aria-hidden.
- The fade is a mask on the content instead of a gradient in a fixed
  surface colour, so there's no band in bubble layout, on highlights or
  over chat backgrounds.
- The cap is 20rem instead of 320px, so a larger font shows as many lines.

Verified headless on a 41-line message with a link on the last line:
aria-expanded false → focusing the link expands it (aria-expanded true,
no clipping); bubble layout fades into the bubble with no band.

Fixes #343

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-12 15:31:50 -04:00
Lotus CIandClaude Opus 5.5 85a2abc46a fix(logout): clear the call-volume list and settings-sync stamp (#347)
CI / Build & Quality Checks (pull_request) Successful in 3m24s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 13m33s
Logout left lotus-per-user-volume (Element Call's per-person volume map:
whom the previous user called and turned up/down; same origin on the web)
and settings-sync-meta:<userId> (a key naming the account) in
localStorage. clearPlaintextCaches now removes both; device preferences
(`settings`) are kept as before.

Verified: after joining/leaving a call and logging out via Settings, both
keys are gone (unit test added).

Fixes #347

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-12 15:28:41 -04:00
8 changed files with 115 additions and 33 deletions
+32 -19
View File
@@ -1,4 +1,4 @@
import React, { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'; import React, { CSSProperties, ReactNode, useEffect, useId, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, Button, config, Icon, Icons, Text, color, toRem } from 'folds'; import { Box, Button, config, Icon, Icons, Text, color, toRem } from 'folds';
import { IContent, MatrixEvent } from 'matrix-js-sdk'; import { IContent, MatrixEvent } from 'matrix-js-sdk';
@@ -41,7 +41,10 @@ import { formatAccuracy } from '../../utils/geoShare';
import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment'; import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment';
import { FileHeader, FileDownloadButton } from './FileHeader'; import { FileHeader, FileDownloadButton } from './FileHeader';
const COLLAPSE_MAX_HEIGHT = 320; // px ≈ 20 lines // ≈ 20 lines; in rem so a larger font setting doesn't show fewer lines (Gitea #343).
const COLLAPSE_MAX_HEIGHT_REM = 20;
const collapseMaxHeightPx = (): number =>
COLLAPSE_MAX_HEIGHT_REM * (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16);
type CollapsibleBodyProps = { type CollapsibleBodyProps = {
eventId?: string; eventId?: string;
@@ -79,7 +82,7 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) {
const el = bodyRef.current; const el = bodyRef.current;
if (!el) return undefined; if (!el) return undefined;
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
setNeedsCollapse(el.scrollHeight > COLLAPSE_MAX_HEIGHT); setNeedsCollapse(el.scrollHeight > collapseMaxHeightPx());
}); });
observer.observe(el); observer.observe(el);
return () => observer.disconnect(); return () => observer.disconnect();
@@ -89,16 +92,36 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) {
// (Gitea #85); useReducedMotion subscribes to the change event instead. // (Gitea #85); useReducedMotion subscribes to the change event instead.
const prefersReducedMotion = useReducedMotion(); const prefersReducedMotion = useReducedMotion();
const bodyId = useId();
const isClipped = needsCollapse && collapsed;
// [Gitea #343] The clipped part stays in the DOM, so Tab can reach a link in
// it; the browser would then scroll the overflow:hidden box itself (top cut
// off, and it stayed that way). Expand instead when focus lands inside.
const handleFocus = () => {
if (isClipped) setCollapsed(false);
};
useEffect(() => {
if (isClipped && bodyRef.current) bodyRef.current.scrollTop = 0;
}, [isClipped]);
// Fade the content itself rather than painting a fixed-colour gradient over
// it, so it blends on any background (bubbles, highlights, chat backgrounds).
const fadeMask = 'linear-gradient(#000 calc(100% - 3rem), transparent)';
return ( return (
<div> <div>
<div <div
ref={bodyRef} ref={bodyRef}
id={bodyId}
onFocus={handleFocus}
style={{ style={{
position: 'relative', position: 'relative',
...(needsCollapse && collapsed ...(isClipped
? { ? {
maxHeight: `${COLLAPSE_MAX_HEIGHT}px`, maxHeight: `${COLLAPSE_MAX_HEIGHT_REM}rem`,
overflow: 'hidden', overflow: 'hidden',
maskImage: fadeMask,
WebkitMaskImage: fadeMask,
transition: prefersReducedMotion ? undefined : 'max-height 0.2s ease', transition: prefersReducedMotion ? undefined : 'max-height 0.2s ease',
} }
: { : {
@@ -107,23 +130,12 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) {
}} }}
> >
{children} {children}
{needsCollapse && collapsed && (
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '3rem',
background: `linear-gradient(transparent, ${color.Surface.Container})`,
pointerEvents: 'none',
}}
/>
)}
</div> </div>
{needsCollapse && ( {needsCollapse && (
<button <button
type="button" type="button"
aria-expanded={!collapsed}
aria-controls={bodyId}
onClick={() => setCollapsed((c) => !c)} onClick={() => setCollapsed((c) => !c)}
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
@@ -134,7 +146,8 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) {
}} }}
> >
<Text as="span" size="T200" style={{ color: color.Primary.Main }}> <Text as="span" size="T200" style={{ color: color.Primary.Main }}>
{collapsed ? 'Read more ↓' : 'Show less ↑'} {collapsed ? 'Read more' : 'Show less'}
<span aria-hidden="true">{collapsed ? ' ↓' : ' ↑'}</span>
</Text> </Text>
</button> </button>
)} )}
@@ -48,6 +48,7 @@ import {
addRecentForwardTarget, addRecentForwardTarget,
} from '../../../state/recentForwardTargets'; } from '../../../state/recentForwardTargets';
import { buildForwardContent, buildPlaintextAttachmentContent } from './forwardContent'; import { buildForwardContent, buildPlaintextAttachmentContent } from './forwardContent';
import { useLocalRoomNames } from '../../../hooks/useRoomMeta';
type RoomRowProps = { type RoomRowProps = {
room: Room; room: Room;
@@ -58,6 +59,7 @@ type RoomRowProps = {
sending: boolean; sending: boolean;
}; };
function RoomRow({ room, dm, useAuthentication, selected, onToggle, sending }: RoomRowProps) { function RoomRow({ room, dm, useAuthentication, selected, onToggle, sending }: RoomRowProps) {
const localNames = useLocalRoomNames();
const mx = useMatrixClient(); const mx = useMatrixClient();
const avatarMxc = room.getMxcAvatarUrl(); const avatarMxc = room.getMxcAvatarUrl();
const avatarUrl = avatarMxc const avatarUrl = avatarMxc
@@ -97,7 +99,7 @@ function RoomRow({ room, dm, useAuthentication, selected, onToggle, sending }: R
> >
<Box direction="Column"> <Box direction="Column">
<Text size="T300" truncate> <Text size="T300" truncate>
{room.name} {localNames[room.roomId] ?? room.name}
</Text> </Text>
{dm && ( {dm && (
<Text size="T200" priority="300" truncate> <Text size="T200" priority="300" truncate>
@@ -212,6 +214,7 @@ function RecentChip({
onToggle: () => void; onToggle: () => void;
sending: boolean; sending: boolean;
}) { }) {
const localNames = useLocalRoomNames();
const mx = useMatrixClient(); const mx = useMatrixClient();
const avatarMxc = room.getMxcAvatarUrl(); const avatarMxc = room.getMxcAvatarUrl();
const avatarUrl = avatarMxc const avatarUrl = avatarMxc
@@ -240,7 +243,7 @@ function RecentChip({
} }
> >
<Text size="B300" truncate style={{ maxWidth: toRem(120) }}> <Text size="B300" truncate style={{ maxWidth: toRem(120) }}>
{room.name} {localNames[room.roomId] ?? room.name}
</Text> </Text>
</Chip> </Chip>
); );
@@ -252,6 +255,7 @@ type Props = {
}; };
export function ForwardMessageDialog({ mEvent, onClose }: Props) { export function ForwardMessageDialog({ mEvent, onClose }: Props) {
const localNames = useLocalRoomNames();
const mx = useMatrixClient(); const mx = useMatrixClient();
const modalStyle = useModalStyle(400); const modalStyle = useModalStyle(400);
const directs = useAtomValue(mDirectAtom); const directs = useAtomValue(mDirectAtom);
@@ -296,8 +300,10 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (!query) return allRooms; if (!query) return allRooms;
const q = query.toLowerCase(); const q = query.toLowerCase();
return allRooms.filter((r) => r.name.toLowerCase().includes(q)); return allRooms.filter((r) =>
}, [allRooms, query]); [r.name, localNames[r.roomId]].some((nm) => nm?.toLowerCase().includes(q)),
);
}, [allRooms, query, localNames]);
// Resolve recent target ids to still-joined rooms (drop ones left/gone). // Resolve recent target ids to still-joined rooms (drop ones left/gone).
const recentRooms = useMemo( const recentRooms = useMemo(
+11 -5
View File
@@ -37,6 +37,7 @@ import {
useAsyncSearch, useAsyncSearch,
UseAsyncSearchOptions, UseAsyncSearchOptions,
} from '../../hooks/useAsyncSearch'; } from '../../hooks/useAsyncSearch';
import { useLocalRoomNames } from '../../hooks/useRoomMeta';
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom'; import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { import {
@@ -157,17 +158,20 @@ export function Search({ requestClose }: SearchProps) {
const topActiveRooms = useTopActiveRooms(searchRoomType, rooms, directs, spaces); const topActiveRooms = useTopActiveRooms(searchRoomType, rooms, directs, spaces);
const targetRooms = useSearchTargetRooms(searchRoomType, rooms, directs, spaces); const targetRooms = useSearchTargetRooms(searchRoomType, rooms, directs, spaces);
const localNames = useLocalRoomNames();
const getTargetStr: SearchItemStrGetter<string> = useCallback( const getTargetStr: SearchItemStrGetter<string> = useCallback(
(roomId: string) => { (roomId: string) => {
const roomName = getRoom(roomId)?.name ?? roomId; const roomName = getRoom(roomId)?.name ?? roomId;
// [Gitea #336] Match the user's own "Rename for me" name as well.
const names = localNames[roomId] ? [localNames[roomId], roomName] : [roomName];
if (mDirects.has(roomId)) { if (mDirects.has(roomId)) {
const targetUserId = getDmUserId(roomId, getRoom, mx.getSafeUserId()); const targetUserId = getDmUserId(roomId, getRoom, mx.getSafeUserId());
const targetUsername = targetUserId && getMxIdLocalPart(targetUserId); const targetUsername = targetUserId && getMxIdLocalPart(targetUserId);
if (targetUsername) return [roomName, targetUsername]; if (targetUsername) return [...names, targetUsername];
} }
return roomName; return names.length === 1 ? names[0] : names;
}, },
[getRoom, mDirects, mx], [getRoom, mDirects, mx, localNames],
); );
const [result, search, resetSearch] = useAsyncSearch(targetRooms, getTargetStr, SEARCH_OPTIONS); const [result, search, resetSearch] = useAsyncSearch(targetRooms, getTargetStr, SEARCH_OPTIONS);
@@ -388,8 +392,10 @@ export function Search({ requestClose }: SearchProps) {
<Box grow="Yes" alignItems="Center" gap="100"> <Box grow="Yes" alignItems="Center" gap="100">
<Text size="T400" truncate> <Text size="T400" truncate>
{queryHighlighRegex {queryHighlighRegex
? highlightText(queryHighlighRegex, [room.name]) ? highlightText(queryHighlighRegex, [
: room.name} localNames[room.roomId] ?? room.name,
])
: (localNames[room.roomId] ?? room.name)}
</Text> </Text>
{dmUsername && ( {dmUsername && (
<Text as="span" size="T200" priority="300" truncate> <Text as="span" size="T200" priority="300" truncate>
+16
View File
@@ -102,6 +102,22 @@ export const useLocalRoomName = (room: Room): string => {
return localNames[room.roomId] ?? name; return localNames[room.roomId] ?? name;
}; };
/**
* [Gitea #336] The whole "Rename for me" map, for lists that show many rooms
* (quick switcher, forward/share pickers) so they use the name the user sees in
* the sidebar, not the server name.
*/
export const useLocalRoomNames = (): Readonly<Record<string, string>> => {
const mx = useMatrixClient();
return roomNamesStore.useValue(mx);
};
/** Non-hook read for notification paths (toast / OS notification titles). */
export const getLocalRoomName = (
mx: ReturnType<typeof useMatrixClient>,
roomId: string,
): string | undefined => roomNamesStore.getLatest(mx)[roomId];
export const useHasLocalRoomName = (roomId: string): boolean => { export const useHasLocalRoomName = (roomId: string): boolean => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const localNames = roomNamesStore.useValue(mx); const localNames = roomNamesStore.useValue(mx);
+4 -2
View File
@@ -14,6 +14,7 @@ import {
ThreadEvent, ThreadEvent,
} from 'matrix-js-sdk'; } from 'matrix-js-sdk';
import { useNotificationsQuiet } from '../../hooks/useNotificationsQuiet'; import { useNotificationsQuiet } from '../../hooks/useNotificationsQuiet';
import { getLocalRoomName } from '../../hooks/useRoomMeta';
import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import NotificationSound from '../../../../public/sound/notification.ogg'; import NotificationSound from '../../../../public/sound/notification.ogg';
import InviteSound from '../../../../public/sound/invite.ogg'; import InviteSound from '../../../../public/sound/invite.ogg';
@@ -639,7 +640,8 @@ function MessageNotifications() {
const avatarMxc = const avatarMxc =
room.getAvatarFallbackMember()?.getMxcAvatarUrl() ?? room.getMxcAvatarUrl(); room.getAvatarFallbackMember()?.getMxcAvatarUrl() ?? room.getMxcAvatarUrl();
notify({ notify({
roomName: room.name ?? 'Unknown', // [Gitea #336] the user's own "Rename for me" name when they set one
roomName: getLocalRoomName(mx, room.roomId) ?? room.name ?? 'Unknown',
roomAvatar: avatarMxc roomAvatar: avatarMxc
? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined) ? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined)
: undefined, : undefined,
@@ -808,7 +810,7 @@ function ReminderMonitor() {
if (!firedRef.current.has(key)) { if (!firedRef.current.has(key)) {
firedRef.current.add(key); firedRef.current.add(key);
const room = mx.getRoom(r.roomId); const room = mx.getRoom(r.roomId);
const roomName = room?.name ?? 'Unknown Room'; const roomName = getLocalRoomName(mx, r.roomId) ?? room?.name ?? 'Unknown Room';
// Reminders for E2EE rooms carry no text (account data is server- // Reminders for E2EE rooms carry no text (account data is server-
// readable), so resolve the body from the local timeline at fire time. // readable), so resolve the body from the local timeline at fire time.
const localBody = room?.findEventById(r.eventId)?.getContent()?.body; const localBody = room?.findEventById(r.eventId)?.getContent()?.body;
+10 -3
View File
@@ -38,6 +38,7 @@ import {
import { BlockType } from '../../../components/editor'; import { BlockType } from '../../../components/editor';
import { bytesToSize } from '../../../utils/common'; import { bytesToSize } from '../../../utils/common';
import { getHomePath } from '../../pathUtils'; import { getHomePath } from '../../pathUtils';
import { useLocalRoomNames } from '../../../hooks/useRoomMeta';
type Shared = { payload: SharePayload; files: File[] }; type Shared = { payload: SharePayload; files: File[] };
@@ -47,6 +48,7 @@ type Shared = { payload: SharePayload; files: File[] };
* as the draft) — the user still presses Send. * as the draft) — the user still presses Send.
*/ */
export function Share() { export function Share() {
const localNames = useLocalRoomNames();
const mx = useMatrixClient(); const mx = useMatrixClient();
const navigate = useNavigate(); const navigate = useNavigate();
const { navigateRoom } = useRoomNavigate(); const { navigateRoom } = useRoomNavigate();
@@ -69,8 +71,12 @@ export function Share() {
.filter((r) => r.getMyMembership() === 'join' && !r.isSpaceRoom()) .filter((r) => r.getMyMembership() === 'join' && !r.isSpaceRoom())
.sort((a, b) => (b.getLastActiveTimestamp() ?? 0) - (a.getLastActiveTimestamp() ?? 0)); .sort((a, b) => (b.getLastActiveTimestamp() ?? 0) - (a.getLastActiveTimestamp() ?? 0));
const q = query.trim().toLowerCase(); const q = query.trim().toLowerCase();
return q ? all.filter((r) => r.name.toLowerCase().includes(q)) : all; return q
}, [mx, query]); ? all.filter((r) =>
[r.name, localNames[r.roomId]].some((nm) => nm?.toLowerCase().includes(q)),
)
: all;
}, [mx, query, localNames]);
// Written straight into the room's draft atoms so its composer shows the // Written straight into the room's draft atoms so its composer shows the
// upload board + text as soon as it mounts. // upload board + text as soon as it mounts.
@@ -210,6 +216,7 @@ function ShareRoomRow({
disabled: boolean; disabled: boolean;
onPick: (room: Room) => void; onPick: (room: Room) => void;
}) { }) {
const localNames = useLocalRoomNames();
const mx = useMatrixClient(); const mx = useMatrixClient();
const avatarMxc = room.getMxcAvatarUrl(); const avatarMxc = room.getMxcAvatarUrl();
const avatarUrl = avatarMxc const avatarUrl = avatarMxc
@@ -237,7 +244,7 @@ function ShareRoomRow({
> >
<Box direction="Column"> <Box direction="Column">
<Text size="T300" truncate> <Text size="T300" truncate>
{room.name} {localNames[room.roomId] ?? room.name}
</Text> </Text>
{dm && ( {dm && (
<Text size="T200" priority="300" truncate> <Text size="T200" priority="300" truncate>
+11
View File
@@ -83,3 +83,14 @@ test('clearPlaintextCaches wipes drafts (#41) and the status message (#204) but
assert.ok(!removed.includes('cinny_session'), 'session key not this module’s concern'); assert.ok(!removed.includes('cinny_session'), 'session key not this module’s concern');
assert.ok(store.has('cinny_session')); assert.ok(store.has('cinny_session'));
}); });
test('clearPlaintextCaches removes the call-volume list and the settings-sync stamp (#347)', () => {
store.clear();
store.set('lotus-per-user-volume', '{"@bob:server":0.3}');
store.set('settings-sync-meta:@me:server', '{"lastSyncedAt":1}');
store.set('settings', '{}');
clearPlaintextCaches('@me:server');
assert.equal(store.has('lotus-per-user-volume'), false);
assert.equal(store.has('settings-sync-meta:@me:server'), false);
assert.equal(store.has('settings'), true, 'device preferences are kept');
});
+21
View File
@@ -47,6 +47,8 @@ const clearMsgDrafts = (): void => {
* - `cinny_recent_gifs_v1` / `cinny_recent_stickers_v1` — media the user sent * - `cinny_recent_gifs_v1` / `cinny_recent_stickers_v1` — media the user sent
* - `navToActivePath<userId>` — per-space last-visited room paths (needs userId) * - `navToActivePath<userId>` — per-space last-visited room paths (needs userId)
* - `lotus_outbox_v1` — decrypted content of messages not yet sent ([Gitea #112]) * - `lotus_outbox_v1` — decrypted content of messages not yet sent ([Gitea #112])
* - `lotus-per-user-volume`, `settings-sync-meta:<userId>` — whom you adjusted in
* calls, and a key naming the account ([Gitea #347])
* - `draft-msg-*` — unsent composer drafts (decrypted message text, unscoped by * - `draft-msg-*` — unsent composer drafts (decrypted message text, unscoped by
* user — see [Gitea #41]; previously deliberately preserved across logout * user — see [Gitea #41]; previously deliberately preserved across logout
* (N98), which let the next account on this device see/send a prior user's * (N98), which let the next account on this device see/send a prior user's
@@ -92,6 +94,24 @@ const clearStatusMessage = (): void => {
}); });
}; };
/**
* [Gitea #347] Per-person residue that isn't message text but still says whom
* the previous user dealt with: Element Call's per-user volume memory (same
* origin on the web — element-call `lotusVolumeMemory.ts`) and the settings-sync
* stamp, whose key names the account.
*/
export const CALL_VOLUME_KEY = 'lotus-per-user-volume';
const clearCallVolumesAndSyncMeta = (userId?: string): void => {
const keys = [CALL_VOLUME_KEY, ...(userId ? [`settings-sync-meta:${userId}`] : [])];
keys.forEach((key) => {
try {
localStorage.removeItem(key);
} catch {
// best-effort
}
});
};
export const clearPlaintextCaches = (userId?: string): void => { export const clearPlaintextCaches = (userId?: string): void => {
clearTranslationCache(); clearTranslationCache();
clearScheduledMessages(); clearScheduledMessages();
@@ -103,5 +123,6 @@ export const clearPlaintextCaches = (userId?: string): void => {
clearOutbox(); clearOutbox();
clearCallSession(); clearCallSession();
clearStatusMessage(); clearStatusMessage();
clearCallVolumesAndSyncMeta(userId);
if (userId) clearNavToActivePathStore(userId); if (userId) clearNavToActivePathStore(userId);
}; };