From b1ee3ada9850b18fe8355aed3acd03a6dd985263 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 7 Jul 2026 23:14:35 -0400 Subject: [PATCH] DP16: add typed get/setAccountData helpers, remove ~19 any-casts Add centralized typed helpers in src/app/utils/accountData.ts: - getAccountData(mx, eventType): T | undefined (returns content) - setAccountData(mx, eventType, content): Promise These wrap the single `as any` cast needed because matrix-js-sdk's typed overloads reject the fork's custom account-data event names. Every call site now stays fully typed on its content shape. Route all account-data reads/writes that previously used `(mx as any).getAccountData/setAccountData` or `mx.getAccountData(... as any)` through the helpers (or, where a MatrixEvent is needed, through the existing utils/room.ts getAccountData whose param is widened to accept string keys). No behavior change: same event types, same content shapes. Co-Authored-By: Claude Opus 4.8 --- src/app/features/lobby/Lobby.tsx | 3 +- src/app/features/room-nav/RoomNavItem.tsx | 10 +++---- src/app/features/settings/account/Profile.tsx | 7 ++--- .../settings/developer-tools/DevelopTools.tsx | 5 ++-- .../settings/emojis-stickers/GlobalPacks.tsx | 9 +++--- src/app/hooks/useAccountData.ts | 5 ++-- src/app/hooks/useBookmarks.ts | 8 ++---- src/app/hooks/useReminders.ts | 8 ++---- src/app/hooks/useRoomMeta.ts | 5 ++-- src/app/hooks/useThreadNotifications.ts | 7 ++--- src/app/hooks/useUserNotes.ts | 5 ++-- src/app/pages/client/sidebar/SpaceTabs.tsx | 5 ++-- src/app/plugins/recent-emoji.ts | 3 +- src/app/state/threadNotifications.ts | 5 ++-- src/app/utils/accountData.ts | 28 +++++++++++++++++++ src/app/utils/room.ts | 2 +- 16 files changed, 69 insertions(+), 46 deletions(-) create mode 100644 src/app/utils/accountData.ts diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 292ea8890..ab3ab31a6 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -41,6 +41,7 @@ import { StateEvent } from '../../../types/matrix/room'; import { CanDropCallback, useDnDMonitor } from './DnD'; import { ASCIILexicalTable, orderKeys } from '../../utils/ASCIILexicalTable'; import { getStateEvent } from '../../utils/room'; +import { setAccountData } from '../../utils/accountData'; import { useClosedLobbyCategoriesAtom } from '../../state/hooks/closedLobbyCategories'; import { makeCinnySpacesContent, @@ -427,7 +428,7 @@ export function Lobby() { newItems.push(rId); } const newSpacesContent = makeCinnySpacesContent(mx, newItems); - mx.setAccountData(AccountDataEvent.CinnySpaces as any, newSpacesContent as any); + setAccountData(mx, AccountDataEvent.CinnySpaces, newSpacesContent); }, [mx, sidebarItems, sidebarSpaces], ); diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index cb5e8c305..371661662 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -34,6 +34,7 @@ import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../componen import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl, getStateEvent } from '../../utils/room'; +import { setAccountData } from '../../utils/accountData'; import { nameInitials } from '../../utils/common'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRoomUnread } from '../../state/hooks/unread'; @@ -127,11 +128,9 @@ function RenameRoomDialog({ room, onClose }: RenameRoomDialogProps) { const existing = getLocalRoomNamesContent(mx); if (newName === '') { const { [room.roomId]: _removed, ...rest } = existing.rooms; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (mx as any).setAccountData(LOCAL_ROOM_NAMES_KEY, { rooms: rest }); + setAccountData(mx, LOCAL_ROOM_NAMES_KEY, { rooms: rest }); } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (mx as any).setAccountData(LOCAL_ROOM_NAMES_KEY, { + setAccountData(mx, LOCAL_ROOM_NAMES_KEY, { rooms: { ...existing.rooms, [room.roomId]: newName }, }); } @@ -141,8 +140,7 @@ function RenameRoomDialog({ room, onClose }: RenameRoomDialogProps) { const handleClear = useCallback(() => { const existing = getLocalRoomNamesContent(mx); const { [room.roomId]: _removed, ...rest } = existing.rooms; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (mx as any).setAccountData(LOCAL_ROOM_NAMES_KEY, { rooms: rest }); + setAccountData(mx, LOCAL_ROOM_NAMES_KEY, { rooms: rest }); onClose(); }, [mx, room.roomId, onClose]); diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index 198686f31..6a480243a 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -35,6 +35,7 @@ import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { getAccountData, setAccountData } from '../../../utils/accountData'; import { presenceStateFromSetting } from '../../../hooks/usePresenceUpdater'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; @@ -725,9 +726,7 @@ function ProfileTimezone() { const [savedTimezone, setSavedTimezone] = useState(''); useEffect(() => { - const cached = mx - .getAccountData('im.lotus.timezone' as any) - ?.getContent<{ timezone: string }>(); + const cached = getAccountData<{ timezone: string }>(mx, 'im.lotus.timezone'); if (cached?.timezone) { setTimezone(cached.timezone); setSavedTimezone(cached.timezone); @@ -754,7 +753,7 @@ function ProfileTimezone() { Promise.all([ // Self-fallback: account data is readable by useExtendedProfile for the // own user even on servers without extended-profile (m.tz) support. - (mx as any).setAccountData('im.lotus.timezone', { timezone: value }), + setAccountData(mx, 'im.lotus.timezone', { timezone: value }), // Mirror the pronouns write path so OTHER users can read the timezone // via the m.tz profile field. Best-effort: standard Synapse rejects // unknown profile fields, so a failure here must not fail the save. diff --git a/src/app/features/settings/developer-tools/DevelopTools.tsx b/src/app/features/settings/developer-tools/DevelopTools.tsx index 9ed034fde..a62767836 100644 --- a/src/app/features/settings/developer-tools/DevelopTools.tsx +++ b/src/app/features/settings/developer-tools/DevelopTools.tsx @@ -12,6 +12,7 @@ import { AccountDataSubmitCallback, } from '../../../components/AccountDataEditor'; import { copyToClipboard } from '../../../utils/dom'; +import { getAccountData, setAccountData } from '../../../utils/accountData'; import { AccountData } from './AccountData'; import { CryptoDiagnostics } from '../developer/CryptoDiagnostics'; @@ -26,7 +27,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) { const submitAccountData: AccountDataSubmitCallback = useCallback( async (type, content) => { - await (mx as any).setAccountData(type, content); + await setAccountData(mx, type, content); }, [mx], ); @@ -36,7 +37,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) { (mx, accountDataType) : undefined } submitChange={submitAccountData} requestClose={() => setAccountDataType(undefined)} diff --git a/src/app/features/settings/emojis-stickers/GlobalPacks.tsx b/src/app/features/settings/emojis-stickers/GlobalPacks.tsx index bc39b003e..d83f00c5b 100644 --- a/src/app/features/settings/emojis-stickers/GlobalPacks.tsx +++ b/src/app/features/settings/emojis-stickers/GlobalPacks.tsx @@ -1,4 +1,4 @@ -import { MatrixEvent, Room } from 'matrix-js-sdk'; +import { Room } from 'matrix-js-sdk'; import React, { MouseEventHandler, useCallback, useEffect, useMemo, useState } from 'react'; import { Box, @@ -41,6 +41,7 @@ import { import { LineClamp2 } from '../../../styles/Text.css'; import { allRoomsAtom } from '../../../state/room-list/roomList'; import { AccountDataEvent } from '../../../../types/matrix/accountData'; +import { getAccountData, setAccountData } from '../../../utils/accountData'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { stopPropagation } from '../../../utils/keyboard'; @@ -303,9 +304,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) { const [applyState, applyChanges] = useAsyncCallback( useCallback(async () => { const content = - ( - (mx as any).getAccountData(AccountDataEvent.PoniesEmoteRooms) as MatrixEvent | undefined - )?.getContent() ?? {}; + getAccountData(mx, AccountDataEvent.PoniesEmoteRooms) ?? {}; const updatedContent: EmoteRoomsContent = JSON.parse(JSON.stringify(content)); selectedPacks.forEach((addr) => { @@ -322,7 +321,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) { } }); - await (mx as any).setAccountData(AccountDataEvent.PoniesEmoteRooms, updatedContent); + await setAccountData(mx, AccountDataEvent.PoniesEmoteRooms, updatedContent); }, [mx, selectedPacks, removedPacks]), ); diff --git a/src/app/hooks/useAccountData.ts b/src/app/hooks/useAccountData.ts index de2cac21c..5f3411cfe 100644 --- a/src/app/hooks/useAccountData.ts +++ b/src/app/hooks/useAccountData.ts @@ -2,11 +2,12 @@ import { MatrixEvent } from 'matrix-js-sdk'; import { useState, useCallback } from 'react'; import { useMatrixClient } from './useMatrixClient'; import { useAccountDataCallback } from './useAccountDataCallback'; +import { getAccountData } from '../utils/room'; export function useAccountData(eventType: string): MatrixEvent | undefined { const mx = useMatrixClient(); - const [event, setEvent] = useState( - () => (mx as any).getAccountData(eventType) as MatrixEvent | undefined, + const [event, setEvent] = useState(() => + getAccountData(mx, eventType), ); useAccountDataCallback( diff --git a/src/app/hooks/useBookmarks.ts b/src/app/hooks/useBookmarks.ts index a1911f228..63bcdce54 100644 --- a/src/app/hooks/useBookmarks.ts +++ b/src/app/hooks/useBookmarks.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { ClientEvent, ClientEventHandlerMap, MatrixClient } from 'matrix-js-sdk'; import { useMatrixClient } from './useMatrixClient'; +import { getAccountData, setAccountData } from '../utils/accountData'; export type Bookmark = { roomId: string; @@ -18,10 +19,7 @@ type BookmarksContent = { }; function readBookmarks(mx: MatrixClient): Bookmark[] { - return ( - (mx.getAccountData(BOOKMARKS_KEY as any)?.getContent() as BookmarksContent | undefined) - ?.bookmarks ?? [] - ); + return getAccountData(mx, BOOKMARKS_KEY)?.bookmarks ?? []; } // Module-scoped serialization state. @@ -85,7 +83,7 @@ function enqueueBookmarkWrite( const next = compute(state.latest); state.latest = next; state.listeners.forEach((listener) => listener(next)); - await (mx as any).setAccountData(BOOKMARKS_KEY, { bookmarks: next }); + await setAccountData(mx, BOOKMARKS_KEY, { bookmarks: next }); }); // Keep the chain alive even if one write rejects, but propagate the // rejection to this caller so it can react (e.g. retry). diff --git a/src/app/hooks/useReminders.ts b/src/app/hooks/useReminders.ts index 35b79cce8..18d090515 100644 --- a/src/app/hooks/useReminders.ts +++ b/src/app/hooks/useReminders.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { ClientEvent, ClientEventHandlerMap, MatrixClient } from 'matrix-js-sdk'; import { useMatrixClient } from './useMatrixClient'; +import { getAccountData, setAccountData } from '../utils/accountData'; export type Reminder = { roomId: string; @@ -16,10 +17,7 @@ type RemindersContent = { }; function readReminders(mx: MatrixClient): Reminder[] { - return ( - (mx.getAccountData(REMINDERS_KEY as any)?.getContent() as RemindersContent | undefined) - ?.reminders ?? [] - ); + return getAccountData(mx, REMINDERS_KEY)?.reminders ?? []; } // Module-scoped serialization state. @@ -83,7 +81,7 @@ function enqueueReminderWrite( const next = compute(state.latest); state.latest = next; state.listeners.forEach((listener) => listener(next)); - await (mx as any).setAccountData(REMINDERS_KEY, { reminders: next }); + await setAccountData(mx, REMINDERS_KEY, { reminders: next }); }); // Keep the chain alive even if one write rejects, but propagate the // rejection to this caller so it can react (e.g. retry). diff --git a/src/app/hooks/useRoomMeta.ts b/src/app/hooks/useRoomMeta.ts index 9b7585ae5..d3fecd61b 100644 --- a/src/app/hooks/useRoomMeta.ts +++ b/src/app/hooks/useRoomMeta.ts @@ -4,6 +4,7 @@ import { ClientEvent, MatrixEvent, Room, RoomEvent, RoomEventHandlerMap } from ' import { StateEvent } from '../../types/matrix/room'; import { useStateEvent } from './useStateEvent'; import { useMatrixClient } from './useMatrixClient'; +import { getAccountData } from '../utils/accountData'; export const useRoomAvatar = (room: Room, dm?: boolean): string | undefined => { const avatarEvent = useStateEvent(room, StateEvent.RoomAvatar); @@ -42,9 +43,7 @@ export type LocalRoomNamesContent = { rooms: Record }; export function getLocalRoomNamesContent( mx: ReturnType, ): LocalRoomNamesContent { - // Use any-cast because LOCAL_ROOM_NAMES_KEY is not in matrix-js-sdk AccountDataEvents - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const raw: unknown = (mx as any).getAccountData(LOCAL_ROOM_NAMES_KEY)?.getContent(); + const raw: unknown = getAccountData(mx, LOCAL_ROOM_NAMES_KEY); if ( raw && typeof raw === 'object' && diff --git a/src/app/hooks/useThreadNotifications.ts b/src/app/hooks/useThreadNotifications.ts index 77fbe050c..8e81849c6 100644 --- a/src/app/hooks/useThreadNotifications.ts +++ b/src/app/hooks/useThreadNotifications.ts @@ -12,6 +12,7 @@ import { } from '../utils/threadNotifications'; import { useMatrixClient } from './useMatrixClient'; import { AsyncState, useAsyncCallback } from './useAsyncCallback'; +import { getAccountData, setAccountData } from '../utils/accountData'; /** Read the current notification mode for a thread from the bound atom. */ export function useThreadNotificationMode( @@ -23,9 +24,7 @@ export function useThreadNotificationMode( } const readContent = (mx: MatrixClient): ThreadNotificationsContent => - ((mx as any).getAccountData(AccountDataEvent.LotusThreadNotifications)?.getContent() as - | ThreadNotificationsContent - | undefined) ?? {}; + getAccountData(mx, AccountDataEvent.LotusThreadNotifications) ?? {}; const getJoinedRoomIds = (mx: MatrixClient): Set => { const joined = new Set(); @@ -74,7 +73,7 @@ const writeThreadNotificationMode = async ( // ALWAYS prune before persisting to keep account data bounded. const finalContent = pruneThreadNotifications(next, getJoinedRoomIds(mx), now); - await (mx as any).setAccountData(AccountDataEvent.LotusThreadNotifications, finalContent); + await setAccountData(mx, AccountDataEvent.LotusThreadNotifications, finalContent); }; export function useSetThreadNotificationMode( diff --git a/src/app/hooks/useUserNotes.ts b/src/app/hooks/useUserNotes.ts index 808337298..1423ea668 100644 --- a/src/app/hooks/useUserNotes.ts +++ b/src/app/hooks/useUserNotes.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { ClientEvent, ClientEventHandlerMap, MatrixClient } from 'matrix-js-sdk'; import { useMatrixClient } from './useMatrixClient'; +import { getAccountData, setAccountData } from '../utils/accountData'; const NOTES_KEY = 'io.lotus.user_notes'; export const USER_NOTE_MAX_LENGTH = 500; @@ -8,7 +9,7 @@ export const USER_NOTE_MAX_LENGTH = 500; type UserNotesContent = Record; function readNotes(mx: MatrixClient): UserNotesContent { - return (mx.getAccountData(NOTES_KEY as any)?.getContent() as UserNotesContent | undefined) ?? {}; + return getAccountData(mx, NOTES_KEY) ?? {}; } // Module-scoped serialization state. @@ -72,7 +73,7 @@ function enqueueNotesWrite( const next = compute(state.latest); state.latest = next; state.listeners.forEach((listener) => listener(next)); - await (mx as any).setAccountData(NOTES_KEY, next); + await setAccountData(mx, NOTES_KEY, next); }); // Keep the chain alive even if one write rejects, but propagate the // rejection to this caller so it can react (e.g. retry). diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 6ce1635b8..262454e25 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -74,6 +74,7 @@ import { useSidebarItems, } from '../../../hooks/useSidebarItems'; import { AccountDataEvent } from '../../../../types/matrix/accountData'; +import { setAccountData } from '../../../utils/accountData'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath'; import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFolder'; @@ -752,7 +753,7 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) { const newSpacesContent = makeCinnySpacesContent(mx, newItems); localEchoSidebarItem(parseSidebar(mx, orphanSpaces, newSpacesContent)); - (mx as any).setAccountData(AccountDataEvent.CinnySpaces, newSpacesContent); + setAccountData(mx, AccountDataEvent.CinnySpaces, newSpacesContent); }, [mx, sidebarItems, setOpenedFolder, localEchoSidebarItem, orphanSpaces], ), @@ -798,7 +799,7 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) { const newSpacesContent = makeCinnySpacesContent(mx, newItems); localEchoSidebarItem(parseSidebar(mx, orphanSpaces, newSpacesContent)); - (mx as any).setAccountData(AccountDataEvent.CinnySpaces, newSpacesContent); + setAccountData(mx, AccountDataEvent.CinnySpaces, newSpacesContent); }, [mx, sidebarItems, orphanSpaces, localEchoSidebarItem], ); diff --git a/src/app/plugins/recent-emoji.ts b/src/app/plugins/recent-emoji.ts index 476f6536c..f7e60932a 100644 --- a/src/app/plugins/recent-emoji.ts +++ b/src/app/plugins/recent-emoji.ts @@ -1,5 +1,6 @@ import { MatrixClient } from 'matrix-js-sdk'; import { getAccountData } from '../utils/room'; +import { setAccountData } from '../utils/accountData'; import { IEmoji, emojis } from './emoji'; import { AccountDataEvent } from '../../types/matrix/accountData'; @@ -39,7 +40,7 @@ export function addRecentEmoji(mx: MatrixClient, unicode: string) { entry[1] += 1; } recentEmoji.unshift(entry); - (mx as any).setAccountData(AccountDataEvent.ElementRecentEmoji, { + setAccountData(mx, AccountDataEvent.ElementRecentEmoji, { recent_emoji: recentEmoji.slice(0, 100), }); } diff --git a/src/app/state/threadNotifications.ts b/src/app/state/threadNotifications.ts index 3c3a58aa1..48eddaf47 100644 --- a/src/app/state/threadNotifications.ts +++ b/src/app/state/threadNotifications.ts @@ -3,15 +3,14 @@ import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk'; import { useEffect } from 'react'; import { AccountDataEvent } from '../../types/matrix/accountData'; import { ThreadNotificationsContent } from '../utils/threadNotifications'; +import { getAccountData } from '../utils/accountData'; // Holds the parsed `io.lotus.thread_notifications` account data. Seeded and // kept in sync by `useBindThreadNotificationsAtom`. export const threadNotificationsAtom = atom({}); const readContent = (mx: MatrixClient): ThreadNotificationsContent => - ((mx as any).getAccountData(AccountDataEvent.LotusThreadNotifications)?.getContent() as - | ThreadNotificationsContent - | undefined) ?? {}; + getAccountData(mx, AccountDataEvent.LotusThreadNotifications) ?? {}; export const useBindThreadNotificationsAtom = ( mx: MatrixClient, diff --git a/src/app/utils/accountData.ts b/src/app/utils/accountData.ts new file mode 100644 index 000000000..ce0feb310 --- /dev/null +++ b/src/app/utils/accountData.ts @@ -0,0 +1,28 @@ +import { MatrixClient, MatrixEvent } from 'matrix-js-sdk'; +import { AccountDataEvent } from '../../types/matrix/accountData'; + +// Typed global account-data read/write helpers. +// +// matrix-js-sdk's `getAccountData`/`setAccountData` overloads only accept the +// SDK's built-in AccountDataEvents union and reject the fork's custom event +// names (the `io.lotus.*` / `in.cinny.*` values in the AccountDataEvent enum, as +// well as a few dynamic keys used by developer tools). We centralize the single +// `as any` cast here so every call site stays fully typed on its content shape. + +export function getAccountData( + mx: MatrixClient, + eventType: AccountDataEvent | string, +): T | undefined { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const event = (mx as any).getAccountData(eventType) as MatrixEvent | undefined; + return event?.getContent() as T | undefined; +} + +export function setAccountData( + mx: MatrixClient, + eventType: AccountDataEvent | string, + content: T, +): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (mx as any).setAccountData(eventType, content); +} diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index ba86922de..b79b79bd6 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -45,7 +45,7 @@ export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[] export const getAccountData = ( mx: MatrixClient, - eventType: AccountDataEvent, + eventType: AccountDataEvent | string, ): MatrixEvent | undefined => mx.getAccountData(eventType as any); export const getMDirects = (mDirectEvent: MatrixEvent): Set => {