Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 5130dfc9c8 fix(settings): "ENTER for Newline" stays per device (#348)
CI / Build & Quality Checks (pull_request) Successful in 3m6s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 13s
CI / Playwright smoke (e2e) (pull_request) Successful in 14m37s
Settings sync mirrored enterForNewline to io.lotus.settings, so turning it
on for a phone keyboard switched the desktop to Ctrl+Enter-to-send (and
vice versa). It is now in DEVICE_LOCAL_KEYS.

Verified with one account on a phone and a desktop profile: toggling it
on the phone leaves the desktop at Enter-to-send, and it is no longer in
the synced account data.

Fixes #348

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-12 15:27:01 -04:00
7 changed files with 23 additions and 51 deletions
@@ -48,7 +48,6 @@ import {
addRecentForwardTarget,
} from '../../../state/recentForwardTargets';
import { buildForwardContent, buildPlaintextAttachmentContent } from './forwardContent';
import { useLocalRoomNames } from '../../../hooks/useRoomMeta';
type RoomRowProps = {
room: Room;
@@ -59,7 +58,6 @@ type RoomRowProps = {
sending: boolean;
};
function RoomRow({ room, dm, useAuthentication, selected, onToggle, sending }: RoomRowProps) {
const localNames = useLocalRoomNames();
const mx = useMatrixClient();
const avatarMxc = room.getMxcAvatarUrl();
const avatarUrl = avatarMxc
@@ -99,7 +97,7 @@ function RoomRow({ room, dm, useAuthentication, selected, onToggle, sending }: R
>
<Box direction="Column">
<Text size="T300" truncate>
{localNames[room.roomId] ?? room.name}
{room.name}
</Text>
{dm && (
<Text size="T200" priority="300" truncate>
@@ -214,7 +212,6 @@ function RecentChip({
onToggle: () => void;
sending: boolean;
}) {
const localNames = useLocalRoomNames();
const mx = useMatrixClient();
const avatarMxc = room.getMxcAvatarUrl();
const avatarUrl = avatarMxc
@@ -243,7 +240,7 @@ function RecentChip({
}
>
<Text size="B300" truncate style={{ maxWidth: toRem(120) }}>
{localNames[room.roomId] ?? room.name}
{room.name}
</Text>
</Chip>
);
@@ -255,7 +252,6 @@ type Props = {
};
export function ForwardMessageDialog({ mEvent, onClose }: Props) {
const localNames = useLocalRoomNames();
const mx = useMatrixClient();
const modalStyle = useModalStyle(400);
const directs = useAtomValue(mDirectAtom);
@@ -300,10 +296,8 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
const filtered = useMemo(() => {
if (!query) return allRooms;
const q = query.toLowerCase();
return allRooms.filter((r) =>
[r.name, localNames[r.roomId]].some((nm) => nm?.toLowerCase().includes(q)),
);
}, [allRooms, query, localNames]);
return allRooms.filter((r) => r.name.toLowerCase().includes(q));
}, [allRooms, query]);
// Resolve recent target ids to still-joined rooms (drop ones left/gone).
const recentRooms = useMemo(
+5 -11
View File
@@ -37,7 +37,6 @@ import {
useAsyncSearch,
UseAsyncSearchOptions,
} from '../../hooks/useAsyncSearch';
import { useLocalRoomNames } from '../../hooks/useRoomMeta';
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import {
@@ -158,20 +157,17 @@ export function Search({ requestClose }: SearchProps) {
const topActiveRooms = useTopActiveRooms(searchRoomType, rooms, directs, spaces);
const targetRooms = useSearchTargetRooms(searchRoomType, rooms, directs, spaces);
const localNames = useLocalRoomNames();
const getTargetStr: SearchItemStrGetter<string> = useCallback(
(roomId: string) => {
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)) {
const targetUserId = getDmUserId(roomId, getRoom, mx.getSafeUserId());
const targetUsername = targetUserId && getMxIdLocalPart(targetUserId);
if (targetUsername) return [...names, targetUsername];
if (targetUsername) return [roomName, targetUsername];
}
return names.length === 1 ? names[0] : names;
return roomName;
},
[getRoom, mDirects, mx, localNames],
[getRoom, mDirects, mx],
);
const [result, search, resetSearch] = useAsyncSearch(targetRooms, getTargetStr, SEARCH_OPTIONS);
@@ -392,10 +388,8 @@ export function Search({ requestClose }: SearchProps) {
<Box grow="Yes" alignItems="Center" gap="100">
<Text size="T400" truncate>
{queryHighlighRegex
? highlightText(queryHighlighRegex, [
localNames[room.roomId] ?? room.name,
])
: (localNames[room.roomId] ?? room.name)}
? highlightText(queryHighlighRegex, [room.name])
: room.name}
</Text>
{dmUsername && (
<Text as="span" size="T200" priority="300" truncate>
-16
View File
@@ -102,22 +102,6 @@ export const useLocalRoomName = (room: Room): string => {
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 => {
const mx = useMatrixClient();
const localNames = roomNamesStore.useValue(mx);
+2 -4
View File
@@ -14,7 +14,6 @@ import {
ThreadEvent,
} from 'matrix-js-sdk';
import { useNotificationsQuiet } from '../../hooks/useNotificationsQuiet';
import { getLocalRoomName } from '../../hooks/useRoomMeta';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import NotificationSound from '../../../../public/sound/notification.ogg';
import InviteSound from '../../../../public/sound/invite.ogg';
@@ -640,8 +639,7 @@ function MessageNotifications() {
const avatarMxc =
room.getAvatarFallbackMember()?.getMxcAvatarUrl() ?? room.getMxcAvatarUrl();
notify({
// [Gitea #336] the user's own "Rename for me" name when they set one
roomName: getLocalRoomName(mx, room.roomId) ?? room.name ?? 'Unknown',
roomName: room.name ?? 'Unknown',
roomAvatar: avatarMxc
? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined)
: undefined,
@@ -810,7 +808,7 @@ function ReminderMonitor() {
if (!firedRef.current.has(key)) {
firedRef.current.add(key);
const room = mx.getRoom(r.roomId);
const roomName = getLocalRoomName(mx, r.roomId) ?? room?.name ?? 'Unknown Room';
const roomName = room?.name ?? 'Unknown Room';
// Reminders for E2EE rooms carry no text (account data is server-
// readable), so resolve the body from the local timeline at fire time.
const localBody = room?.findEventById(r.eventId)?.getContent()?.body;
+3 -10
View File
@@ -38,7 +38,6 @@ import {
import { BlockType } from '../../../components/editor';
import { bytesToSize } from '../../../utils/common';
import { getHomePath } from '../../pathUtils';
import { useLocalRoomNames } from '../../../hooks/useRoomMeta';
type Shared = { payload: SharePayload; files: File[] };
@@ -48,7 +47,6 @@ type Shared = { payload: SharePayload; files: File[] };
* as the draft) — the user still presses Send.
*/
export function Share() {
const localNames = useLocalRoomNames();
const mx = useMatrixClient();
const navigate = useNavigate();
const { navigateRoom } = useRoomNavigate();
@@ -71,12 +69,8 @@ export function Share() {
.filter((r) => r.getMyMembership() === 'join' && !r.isSpaceRoom())
.sort((a, b) => (b.getLastActiveTimestamp() ?? 0) - (a.getLastActiveTimestamp() ?? 0));
const q = query.trim().toLowerCase();
return q
? all.filter((r) =>
[r.name, localNames[r.roomId]].some((nm) => nm?.toLowerCase().includes(q)),
)
: all;
}, [mx, query, localNames]);
return q ? all.filter((r) => r.name.toLowerCase().includes(q)) : all;
}, [mx, query]);
// Written straight into the room's draft atoms so its composer shows the
// upload board + text as soon as it mounts.
@@ -216,7 +210,6 @@ function ShareRoomRow({
disabled: boolean;
onPick: (room: Room) => void;
}) {
const localNames = useLocalRoomNames();
const mx = useMatrixClient();
const avatarMxc = room.getMxcAvatarUrl();
const avatarUrl = avatarMxc
@@ -244,7 +237,7 @@ function ShareRoomRow({
>
<Box direction="Column">
<Text size="T300" truncate>
{localNames[room.roomId] ?? room.name}
{room.name}
</Text>
{dm && (
<Text size="T200" priority="300" truncate>
+6
View File
@@ -104,3 +104,9 @@ describe('buildSyncedContent', () => {
assert.deepEqual(c.settings, pickSyncable(base));
});
});
describe('device-local keys', () => {
it('keeps enterForNewline on each device (#348)', () => {
assert.equal(DEVICE_LOCAL_KEYS.has('enterForNewline'), true);
});
});
+3
View File
@@ -49,6 +49,9 @@ export const DEVICE_LOCAL_KEYS: ReadonlySet<keyof Settings> = new Set<keyof Sett
'ringtoneVolume',
'soundboardVolume',
'cameraOnJoin',
// [Gitea #348] Enter = new line suits a phone keyboard, Enter = send a desktop one;
// syncing it switched the desktop to Ctrl+Enter when set on the phone.
'enterForNewline',
]);
/** The subset of `settings` that is synced (deep-copied so callers can't alias). */