From 0e61fc9cdbc240e0e1ed863ef26fd77395b098b8 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 3 May 2026 17:17:43 +0200 Subject: [PATCH 1/9] initial commit for MSC4459 support + a fair bit of refactoring --- .../create-room/AdditionalCreatorInput.tsx | 3 +- src/app/components/create-room/utils.ts | 2 +- .../autocomplete/RoomMentionAutocomplete.tsx | 3 +- .../autocomplete/UserMentionAutocomplete.tsx | 3 +- .../invite-user-prompt/InviteUserPrompt.tsx | 3 +- src/app/components/user-profile/UserChips.tsx | 2 +- .../common-settings/general/RoomAddress.tsx | 2 +- .../common-settings/general/RoomJoinRules.tsx | 2 +- .../common-settings/general/RoomUpgrade.tsx | 2 +- .../common-settings/members/Members.tsx | 3 +- .../permissions/PowersEditor.tsx | 2 +- src/app/features/create-room/CreateRoom.tsx | 12 +++--- src/app/features/create-space/CreateSpace.tsx | 12 +++--- src/app/features/room/RoomInput.tsx | 2 +- src/app/features/search/Search.tsx | 3 +- src/app/hooks/useRoomCreators.ts | 2 +- .../pages/auth/login/PasswordLoginForm.tsx | 3 +- src/app/pages/client/AutoDiscovery.tsx | 2 +- src/app/pages/client/explore/Explore.tsx | 2 +- src/app/pages/client/explore/Server.tsx | 2 +- src/app/pages/client/sidebar/ExploreTab.tsx | 2 +- src/app/plugins/via-servers.ts | 3 +- src/app/utils/matrix.ts | 31 +++---------- src/app/utils/messageReaction.ts | 24 +++++++++++ src/app/utils/msc4459helper.ts | 41 ++++++++++++++++++ src/app/utils/mxIdHelper.ts | 4 ++ src/app/utils/room.ts | 9 ---- src/app/utils/roomSupport.ts | 16 +++++++ src/types/matrix/common.ts | 43 +++++++++++++++++++ src/types/wrapper/SerializableMap.ts | 14 ++++++ src/types/wrapper/SerializableSet.ts | 13 ++++++ 31 files changed, 201 insertions(+), 66 deletions(-) create mode 100644 src/app/utils/messageReaction.ts create mode 100644 src/app/utils/msc4459helper.ts create mode 100644 src/app/utils/mxIdHelper.ts create mode 100644 src/app/utils/roomSupport.ts create mode 100644 src/types/wrapper/SerializableMap.ts create mode 100644 src/types/wrapper/SerializableSet.ts diff --git a/src/app/components/create-room/AdditionalCreatorInput.tsx b/src/app/components/create-room/AdditionalCreatorInput.tsx index f362122a0..37a70fce9 100644 --- a/src/app/components/create-room/AdditionalCreatorInput.tsx +++ b/src/app/components/create-room/AdditionalCreatorInput.tsx @@ -17,6 +17,7 @@ import { } from 'folds'; import { isKeyHotkey } from 'is-hotkey'; import FocusTrap from 'focus-trap-react'; +import { getMxIdServer } from '$utils/mxIdHelper'; import { ChangeEventHandler, KeyboardEventHandler, @@ -24,7 +25,7 @@ import { useMemo, useState, } from 'react'; -import { getMxIdLocalPart, getMxIdServer, isUserId } from '$utils/matrix'; +import { getMxIdLocalPart, isUserId } from '$utils/matrix'; import { useDirectUsers } from '$hooks/useDirectUsers'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { stopPropagation } from '$utils/keyboard'; diff --git a/src/app/components/create-room/utils.ts b/src/app/components/create-room/utils.ts index d03226823..04da743e6 100644 --- a/src/app/components/create-room/utils.ts +++ b/src/app/components/create-room/utils.ts @@ -9,7 +9,7 @@ import { } from '$types/matrix-sdk'; import { RoomType, StateEvent } from '$types/matrix/room'; import { getViaServers } from '$plugins/via-servers'; -import { getMxIdServer } from '$utils/matrix'; +import { getMxIdServer } from '$utils/mxIdHelper'; import { CreateRoomAccess } from './types'; export const createRoomCreationContent = ( diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx index 9ad67aad4..285f7ec2c 100644 --- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx @@ -7,7 +7,7 @@ import { useAtomValue } from 'jotai'; import { getDirectRoomAvatarUrl } from '$utils/room'; import { useMatrixClient } from '$hooks/useMatrixClient'; -import { getMxIdServer, isRoomAlias } from '$utils/matrix'; +import { isRoomAlias } from '$utils/matrix'; import { UseAsyncSearchOptions, useAsyncSearch } from '$hooks/useAsyncSearch'; import { onTabPress } from '$utils/keyboard'; import { useKeyDown } from '$hooks/useKeyDown'; @@ -17,6 +17,7 @@ import { factoryRoomIdByActivity } from '$utils/sort'; import { RoomAvatar, RoomIcon } from '$components/room-avatar'; import { getViaServers } from '$plugins/via-servers'; import { createMentionElement, moveCursor, replaceWithElement } from '$components/editor/utils'; +import { getMxIdServer } from '$utils/mxIdHelper'; import { AutocompleteMenu } from './AutocompleteMenu'; import { AutocompleteQuery } from './autocompleteQuery'; diff --git a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx index e38838070..f0303fdca 100644 --- a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx @@ -9,7 +9,7 @@ import { useMatrixClient } from '$hooks/useMatrixClient'; import { SearchItemStrGetter, UseAsyncSearchOptions, useAsyncSearch } from '$hooks/useAsyncSearch'; import { onTabPress } from '$utils/keyboard'; import { useKeyDown } from '$hooks/useKeyDown'; -import { getMxIdLocalPart, getMxIdServer, isUserId } from '$utils/matrix'; +import { getMxIdLocalPart, isUserId } from '$utils/matrix'; import { getMemberDisplayName, getMemberSearchStr } from '$utils/room'; import { UserAvatar } from '$components/user-avatar'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; @@ -17,6 +17,7 @@ import { Membership } from '$types/matrix/room'; import { useAtomValue } from 'jotai'; import { nicknamesAtom } from '$state/nicknames'; import { createMentionElement, moveCursor, replaceWithElement } from '$components/editor/utils'; +import { getMxIdServer } from '$utils/mxIdHelper'; import { AutocompleteMenu } from './AutocompleteMenu'; import { AutocompleteQuery } from './autocompleteQuery'; diff --git a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx index c8fbabef5..e03e621ae 100644 --- a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx +++ b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx @@ -34,7 +34,7 @@ import { isKeyHotkey } from 'is-hotkey'; import FocusTrap from 'focus-trap-react'; import { stopPropagation } from '$utils/keyboard'; import { useDirectUsers } from '$hooks/useDirectUsers'; -import { getMxIdLocalPart, getMxIdServer, isUserId } from '$utils/matrix'; +import { getMxIdLocalPart, isUserId } from '$utils/matrix'; import { Membership } from '$types/matrix/room'; import { useAsyncSearch, UseAsyncSearchOptions } from '$hooks/useAsyncSearch'; import { highlightText, makeHighlightRegex } from '$plugins/react-custom-html-parser'; @@ -42,6 +42,7 @@ import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { BreakWord } from '$styles/Text.css'; import { useAlive } from '$hooks/useAlive'; +import { getMxIdServer } from '$utils/mxIdHelper'; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, diff --git a/src/app/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx index 1ce3b26c1..89df0273e 100644 --- a/src/app/components/user-profile/UserChips.tsx +++ b/src/app/components/user-profile/UserChips.tsx @@ -29,7 +29,6 @@ import { Avatar, } from 'folds'; import { useMatrixClient } from '$hooks/useMatrixClient'; -import { getMxIdServer } from '$utils/matrix'; import { useCloseUserRoomProfile } from '$state/hooks/userRoomProfile'; import { stopPropagation } from '$utils/keyboard'; import { copyToClipboard } from '$utils/dom'; @@ -50,6 +49,7 @@ import { useNickname, useSetNickname } from '$hooks/useNickname'; import { CutoutCard } from '$components/cutout-card'; import { SettingTile } from '$components/setting-tile'; import { RoomAvatar, RoomIcon } from '$components/room-avatar'; +import { getMxIdServer } from '$utils/mxIdHelper'; export function ServerChip({ server }: { server: string }) { const mx = useMatrixClient(); diff --git a/src/app/features/common-settings/general/RoomAddress.tsx b/src/app/features/common-settings/general/RoomAddress.tsx index f1ae66968..fff539b62 100644 --- a/src/app/features/common-settings/general/RoomAddress.tsx +++ b/src/app/features/common-settings/general/RoomAddress.tsx @@ -32,7 +32,7 @@ import { replaceSpaceWithDash } from '$utils/common'; import { useAlive } from '$hooks/useAlive'; import { StateEvent } from '$types/matrix/room'; import { RoomPermissionsAPI } from '$hooks/useRoomPermissions'; -import { getMxIdServer } from '$utils/matrix'; +import { getMxIdServer } from '$utils/mxIdHelper'; type RoomPublishedAddressesProps = { permissions: RoomPermissionsAPI; diff --git a/src/app/features/common-settings/general/RoomJoinRules.tsx b/src/app/features/common-settings/general/RoomJoinRules.tsx index 5a6c3697f..bef5eb868 100644 --- a/src/app/features/common-settings/general/RoomJoinRules.tsx +++ b/src/app/features/common-settings/general/RoomJoinRules.tsx @@ -26,8 +26,8 @@ import { getStateEvents } from '$utils/room'; import { useRecursiveChildSpaceScopeFactory, useSpaceChildren } from '$state/hooks/roomList'; import { allRoomsAtom } from '$state/room-list/roomList'; import { roomToParentsAtom } from '$state/room/roomToParents'; -import { knockRestrictedSupported, knockSupported, restrictedSupported } from '$utils/matrix'; import { RoomPermissionsAPI } from '$hooks/useRoomPermissions'; +import { knockRestrictedSupported, restrictedSupported, knockSupported } from '$utils/roomSupport'; type RestrictedRoomAllowContent = { room_id: string; diff --git a/src/app/features/common-settings/general/RoomUpgrade.tsx b/src/app/features/common-settings/general/RoomUpgrade.tsx index 073465688..2dfc90e88 100644 --- a/src/app/features/common-settings/general/RoomUpgrade.tsx +++ b/src/app/features/common-settings/general/RoomUpgrade.tsx @@ -35,9 +35,9 @@ import { useAdditionalCreators, } from '$components/create-room'; import { useAlive } from '$hooks/useAlive'; -import { creatorsSupported } from '$utils/matrix'; import { useRoomCreators } from '$hooks/useRoomCreators'; import { BreakWord } from '$styles/Text.css'; +import { creatorsSupported } from '$utils/roomSupport'; function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) { const mx = useMatrixClient(); diff --git a/src/app/features/common-settings/members/Members.tsx b/src/app/features/common-settings/members/Members.tsx index 678d29248..46b4e46a2 100644 --- a/src/app/features/common-settings/members/Members.tsx +++ b/src/app/features/common-settings/members/Members.tsx @@ -31,7 +31,7 @@ import { useGetMemberPowerLevel, usePowerLevels } from '$hooks/usePowerLevels'; import { VirtualTile } from '$components/virtualizer'; import { MemberTile } from '$components/member-tile'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; -import { getMxIdLocalPart, getMxIdServer } from '$utils/matrix'; +import { getMxIdLocalPart } from '$utils/matrix'; import { ServerBadge } from '$components/server-badge'; import { useDebounce } from '$hooks/useDebounce'; import { SearchItemStrGetter, useAsyncSearch, UseAsyncSearchOptions } from '$hooks/useAsyncSearch'; @@ -49,6 +49,7 @@ import { useSpaceOptionally } from '$hooks/useSpace'; import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '$hooks/useMemberPowerTag'; import { useRoomCreators } from '$hooks/useRoomCreators'; import { getMouseEventCords } from '$utils/dom'; +import { getMxIdServer } from '$utils/mxIdHelper'; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, diff --git a/src/app/features/common-settings/permissions/PowersEditor.tsx b/src/app/features/common-settings/permissions/PowersEditor.tsx index f53f3a237..5254e4aa6 100644 --- a/src/app/features/common-settings/permissions/PowersEditor.tsx +++ b/src/app/features/common-settings/permissions/PowersEditor.tsx @@ -47,8 +47,8 @@ import { MemberPowerTag, MemberPowerTagIcon, StateEvent } from '$types/matrix/ro import { useAlive } from '$hooks/useAlive'; import { BetaNoticeBadge } from '$components/BetaNoticeBadge'; import { getPowerTagIconSrc } from '$hooks/useMemberPowerTag'; -import { creatorsSupported } from '$utils/matrix'; import { SequenceCardStyle } from '$features/common-settings/styles.css'; +import { creatorsSupported } from '$utils/roomSupport'; type EditPowerProps = { maxPower: number; diff --git a/src/app/features/create-room/CreateRoom.tsx b/src/app/features/create-room/CreateRoom.tsx index 7347ba586..d4bd52c57 100644 --- a/src/app/features/create-room/CreateRoom.tsx +++ b/src/app/features/create-room/CreateRoom.tsx @@ -16,12 +16,6 @@ import { } from 'folds'; import { SettingTile } from '$components/setting-tile'; import { SequenceCard } from '$components/sequence-card'; -import { - creatorsSupported, - knockRestrictedSupported, - knockSupported, - restrictedSupported, -} from '$utils/matrix'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { millisecondsToMinutes, replaceSpaceWithDash } from '$utils/common'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; @@ -42,6 +36,12 @@ import { RoomType } from '$types/matrix/room'; import { CreateRoomTypeSelector } from '$components/create-room/CreateRoomTypeSelector'; import { getRoomIconSrc } from '$utils/room'; import { createDebugLogger } from '$utils/debugLogger'; +import { + restrictedSupported, + creatorsSupported, + knockSupported, + knockRestrictedSupported, +} from '$utils/roomSupport'; import { ErrorCode } from '../../cs-errorcode'; const debugLog = createDebugLogger('CreateRoom'); diff --git a/src/app/features/create-space/CreateSpace.tsx b/src/app/features/create-space/CreateSpace.tsx index 2fbeb198b..fc049bc8c 100644 --- a/src/app/features/create-space/CreateSpace.tsx +++ b/src/app/features/create-space/CreateSpace.tsx @@ -16,12 +16,6 @@ import { } from 'folds'; import { SettingTile } from '$components/setting-tile'; import { SequenceCard } from '$components/sequence-card'; -import { - creatorsSupported, - knockRestrictedSupported, - knockSupported, - restrictedSupported, -} from '$utils/matrix'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { millisecondsToMinutes, replaceSpaceWithDash } from '$utils/common'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; @@ -38,6 +32,12 @@ import { useAdditionalCreators, } from '$components/create-room'; import { RoomType } from '$types/matrix/room'; +import { + restrictedSupported, + creatorsSupported, + knockSupported, + knockRestrictedSupported, +} from '$utils/roomSupport'; import { ErrorCode } from '../../cs-errorcode'; const getCreateSpaceAccessToIcon = (access: CreateRoomAccess) => { diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index bc30145c8..fe00ac076 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -657,7 +657,7 @@ export const RoomInput = forwardRef( const lastMessageId = lastMessage?.getId(); if (lastMessageId) { - toggleReaction(mx, room, lastMessageId, key, shortcode); + toggleReaction(mx, room, lastMessageId, key, mx, shortcode); } } diff --git a/src/app/features/search/Search.tsx b/src/app/features/search/Search.tsx index 65c79ba83..85b9d63f6 100644 --- a/src/app/features/search/Search.tsx +++ b/src/app/features/search/Search.tsx @@ -46,7 +46,7 @@ import { factoryRoomIdByActivity } from '$utils/sort'; import { nameInitials } from '$utils/common'; import { useRoomNavigate } from '$hooks/useRoomNavigate'; import { useListFocusIndex } from '$hooks/useListFocusIndex'; -import { getMxIdLocalPart, getMxIdServer, guessDmRoomUserId } from '$utils/matrix'; +import { getMxIdLocalPart, guessDmRoomUserId } from '$utils/matrix'; import { roomToParentsAtom } from '$state/room/roomToParents'; import { roomToUnreadAtom } from '$state/room/roomToUnread'; import { UnreadBadge, UnreadBadgeCenter } from '$components/unread-badge'; @@ -56,6 +56,7 @@ import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; import { KeySymbol } from '$utils/key-symbol'; import { isMacOS } from '$utils/user-agent'; import { useSelectedSpace } from '$hooks/router/useSelectedSpace'; +import { getMxIdServer } from '$utils/mxIdHelper'; enum SearchRoomType { Rooms = '#', diff --git a/src/app/hooks/useRoomCreators.ts b/src/app/hooks/useRoomCreators.ts index 6c4f740c1..a60889d5f 100644 --- a/src/app/hooks/useRoomCreators.ts +++ b/src/app/hooks/useRoomCreators.ts @@ -1,8 +1,8 @@ import { MatrixClient, MatrixEvent, Room } from '$types/matrix-sdk'; import { useMemo } from 'react'; import { IRoomCreateContent, StateEvent } from '$types/matrix/room'; -import { creatorsSupported } from '$utils/matrix'; import { getStateEvent } from '$utils/room'; +import { creatorsSupported } from '$utils/roomSupport'; import { useStateEvent } from './useStateEvent'; export const getRoomCreators = (createEvent: MatrixEvent): Set => { diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx index ba888bda5..e7f32fe3f 100644 --- a/src/app/pages/auth/login/PasswordLoginForm.tsx +++ b/src/app/pages/auth/login/PasswordLoginForm.tsx @@ -20,7 +20,7 @@ import { import FocusTrap from 'focus-trap-react'; import { Link } from 'react-router-dom'; import { MatrixError } from '$types/matrix-sdk'; -import { getMxIdLocalPart, getMxIdServer, isUserId } from '$utils/matrix'; +import { getMxIdLocalPart, isUserId } from '$utils/matrix'; import { EMAIL_REGEX } from '$utils/regex'; import { useAutoDiscoveryInfo } from '$hooks/useAutoDiscoveryInfo'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; @@ -31,6 +31,7 @@ import { getResetPasswordPath } from '$pages/pathUtils'; import { stopPropagation } from '$utils/keyboard'; import { FieldError } from '$pages/auth/FiledError'; import { deviceDisplayName } from '$utils/user-agent'; +import { getMxIdServer } from '$utils/mxIdHelper'; import { CustomLoginResponse, LoginError, diff --git a/src/app/pages/client/AutoDiscovery.tsx b/src/app/pages/client/AutoDiscovery.tsx index c39f16cd3..8294b019c 100644 --- a/src/app/pages/client/AutoDiscovery.tsx +++ b/src/app/pages/client/AutoDiscovery.tsx @@ -1,8 +1,8 @@ import { ReactNode, useCallback, useMemo } from 'react'; +import { getMxIdServer } from '$utils/mxIdHelper'; import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo'; import { AsyncStatus, useAsyncCallbackValue } from '../../hooks/useAsyncCallback'; import { autoDiscovery, AutoDiscoveryInfo } from '../../cs-api'; -import { getMxIdServer } from '../../utils/matrix'; type AutoDiscoveryProps = { userId: string; diff --git a/src/app/pages/client/explore/Explore.tsx b/src/app/pages/client/explore/Explore.tsx index 9ac55e2d2..96ee176e3 100644 --- a/src/app/pages/client/explore/Explore.tsx +++ b/src/app/pages/client/explore/Explore.tsx @@ -23,11 +23,11 @@ import { getExploreFeaturedPath, getExploreServerPath } from '$pages/pathUtils'; import { useClientConfig } from '$hooks/useClientConfig'; import { useExploreFeaturedSelected, useExploreServer } from '$hooks/router/useExploreSelected'; import { useMatrixClient } from '$hooks/useMatrixClient'; -import { getMxIdServer } from '$utils/matrix'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { useNavToActivePathMapper } from '$hooks/useNavToActivePathMapper'; import { PageNav, PageNavContent, PageNavHeader } from '$components/page'; import { stopPropagation } from '$utils/keyboard'; +import { getMxIdServer } from '$utils/mxIdHelper'; export function AddServer() { const mx = useMatrixClient(); diff --git a/src/app/pages/client/explore/Server.tsx b/src/app/pages/client/explore/Server.tsx index d58aa2a10..daf33e72d 100644 --- a/src/app/pages/client/explore/Server.tsx +++ b/src/app/pages/client/explore/Server.tsx @@ -40,10 +40,10 @@ import { ExploreServerPathSearchParams } from '$pages/paths'; import { getExploreServerPath, withSearchParam } from '$pages/pathUtils'; import { allRoomsAtom } from '$state/room-list/roomList'; import { useRoomNavigate } from '$hooks/useRoomNavigate'; -import { getMxIdServer } from '$utils/matrix'; import { stopPropagation } from '$utils/keyboard'; import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; import { BackRouteHandler } from '$components/BackRouteHandler'; +import { getMxIdServer } from '$utils/mxIdHelper'; import * as css from './style.css'; const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams => diff --git a/src/app/pages/client/sidebar/ExploreTab.tsx b/src/app/pages/client/sidebar/ExploreTab.tsx index ee65975ed..e255aeac9 100644 --- a/src/app/pages/client/sidebar/ExploreTab.tsx +++ b/src/app/pages/client/sidebar/ExploreTab.tsx @@ -11,9 +11,9 @@ import { } from '$pages/pathUtils'; import { useClientConfig } from '$hooks/useClientConfig'; import { useMatrixClient } from '$hooks/useMatrixClient'; -import { getMxIdServer } from '$utils/matrix'; import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; import { useNavToActivePathAtom } from '$state/hooks/navToActivePath'; +import { getMxIdServer } from '$utils/mxIdHelper'; export function ExploreTab() { const mx = useMatrixClient(); diff --git a/src/app/plugins/via-servers.ts b/src/app/plugins/via-servers.ts index e789e5f8f..525241023 100644 --- a/src/app/plugins/via-servers.ts +++ b/src/app/plugins/via-servers.ts @@ -1,8 +1,9 @@ import { Room } from '$types/matrix-sdk'; import { IRoomCreateContent, StateEvent } from '$types/matrix/room'; import { IPowerLevels } from '$hooks/usePowerLevels'; -import { creatorsSupported, getMxIdServer } from '$utils/matrix'; import { getStateEvent } from '$utils/room'; +import { getMxIdServer } from '$utils/mxIdHelper'; +import { creatorsSupported } from '$utils/roomSupport'; export const getViaServers = (room: Room): string[] => { const getHighestPowerUserId = (): string | undefined => { diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index f04f71d90..1a1628c3b 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -19,18 +19,14 @@ import { IImageInfo, IThumbnailContent, IVideoInfo } from '$types/matrix/common' import { AccountDataEvent } from '$types/matrix/accountData'; import { Membership, MessageEvent, StateEvent } from '$types/matrix/room'; import * as Sentry from '@sentry/react'; -import { getEventReactions, getReactionContent, getStateEvent } from './room'; +import { getEventReactions, getStateEvent } from './room'; +import { getReactionContent } from './messageReaction'; +import { matchMxId, validMxId } from './mxIdHelper'; const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/; export const isServerName = (serverName: string): boolean => DOMAIN_REGEX.test(serverName); -const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@$+#])([^\s:]+):(\S+)$/); - -const validMxId = (id: string): boolean => !!matchMxId(id); - -export const getMxIdServer = (userId: string): string | undefined => matchMxId(userId)?.[3]; - export const getMxIdLocalPart = (userId: string): string | undefined => matchMxId(userId)?.[2]; export const isUserId = (id: string): boolean => validMxId(id) && id.startsWith('@'); @@ -384,28 +380,12 @@ export const rateLimitedActions = async ( } }; -export const knockSupported = (version: string): boolean => { - const unsupportedVersion = ['1', '2', '3', '4', '5', '6']; - return !unsupportedVersion.includes(version); -}; -export const restrictedSupported = (version: string): boolean => { - const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7']; - return !unsupportedVersion.includes(version); -}; -export const knockRestrictedSupported = (version: string): boolean => { - const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; - return !unsupportedVersion.includes(version); -}; -export const creatorsSupported = (version: string): boolean => { - const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; - return !unsupportedVersion.includes(version); -}; - export const toggleReaction = ( mx: MatrixClient, room: Room, targetEventId: string, key: string, + matrixClient: MatrixClient, shortcode?: string, timelineSet?: EventTimelineSet ) => { @@ -424,9 +404,10 @@ export const toggleReaction = ( } const rShortcode = shortcode || (reactions.find(eventWithShortcode)?.getContent().shortcode as string | undefined); + // send the reaction mx.sendEvent( room.roomId, MessageEvent.Reaction as any, - getReactionContent(targetEventId, key, rShortcode) + getReactionContent(targetEventId, key, matrixClient, rShortcode) ); }; diff --git a/src/app/utils/messageReaction.ts b/src/app/utils/messageReaction.ts new file mode 100644 index 000000000..7fd10205c --- /dev/null +++ b/src/app/utils/messageReaction.ts @@ -0,0 +1,24 @@ +import { MatrixReactionEvent, MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME } from '$types/matrix/common'; +import { MatrixClient } from 'matrix-js-sdk'; +import { ImageUsage } from '$plugins/custom-emoji'; +import { getImagePackReferencesForMxc } from './msc4459helper'; + +export const getReactionContent = ( + eventId: string, + key: string, + matrixClient: MatrixClient, + shortcode?: string +): MatrixReactionEvent => ({ + 'm.relates_to': { + event_id: eventId, + key, + rel_type: 'm.annotation', + }, + shortcode, + 'com.beeper.reaction.shortcode': shortcode, + [MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME]: getImagePackReferencesForMxc( + key, + matrixClient, + ImageUsage.Emoticon + ), +}); diff --git a/src/app/utils/msc4459helper.ts b/src/app/utils/msc4459helper.ts new file mode 100644 index 000000000..070965408 --- /dev/null +++ b/src/app/utils/msc4459helper.ts @@ -0,0 +1,41 @@ +import { getGlobalImagePacks } from '$plugins/custom-emoji/utils'; +import { ImagePack } from '$plugins/custom-emoji/ImagePack'; +import { MSC4459ImagePackReference } from '$types/matrix/common'; +import { SerializableMap } from '$types/wrapper/SerializableMap'; +import { MatrixClient } from 'matrix-js-sdk'; +import { ImageUsage } from '$plugins/custom-emoji'; +import { SerializableSet } from '$types/wrapper/SerializableSet'; +import { getViaServers } from '$plugins/via-servers'; +import { getMxIdServer } from './mxIdHelper'; + +export function getImagePackReferencesForMxc( + mxcUrl: string, + matrixClient: MatrixClient, + imageUsage: ImageUsage +): SerializableMap { + const globalImgPacks: ImagePack[] = getGlobalImagePacks(matrixClient); + if (!mxcUrl.startsWith('mxc')) return new SerializableMap(); + const imagePackReferences = new SerializableMap(); + globalImgPacks + .filter((val) => val.getImages(imageUsage).find((img) => img.url === mxcUrl)) + .forEach((pack) => { + const img = pack.getImages(imageUsage).find((val) => val.url === mxcUrl); + const room = matrixClient.getRoom(pack.address?.roomId); + const viaServers = new SerializableSet(); + if (room) + getViaServers(room).forEach((via) => { + viaServers.add(via); + }); + // add ones own hs as via server, as that server evidently is alive + const ownViaHS = getMxIdServer(matrixClient.getSafeUserId()); + if (ownViaHS) viaServers.add(ownViaHS); + const imgPkRef = { + room_id: pack.address?.roomId, + state_key: pack.address?.stateKey, + via: viaServers, + shortcode: img?.shortcode, + } satisfies MSC4459ImagePackReference; + imagePackReferences.set(mxcUrl, imgPkRef); + }); + return imagePackReferences; +} diff --git a/src/app/utils/mxIdHelper.ts b/src/app/utils/mxIdHelper.ts new file mode 100644 index 000000000..91414b8aa --- /dev/null +++ b/src/app/utils/mxIdHelper.ts @@ -0,0 +1,4 @@ +export const matchMxId = (id: string): RegExpMatchArray | null => + id.match(/^([@$+#])([^\s:]+):(\S+)$/); +export const validMxId = (id: string): boolean => !!matchMxId(id); +export const getMxIdServer = (userId: string): string | undefined => matchMxId(userId)?.[3]; diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index 9391dbc90..5ca6f5890 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -580,15 +580,6 @@ export const decryptAllTimelineEvent = async (mx: MatrixClient, timeline: EventT } }; -export const getReactionContent = (eventId: string, key: string, shortcode?: string) => ({ - 'm.relates_to': { - event_id: eventId, - key, - rel_type: 'm.annotation', - }, - shortcode, -}); - export const getEventReactions = (timelineSet: EventTimelineSet, eventId: string) => timelineSet.relations.getChildEventsForEvent( eventId, diff --git a/src/app/utils/roomSupport.ts b/src/app/utils/roomSupport.ts new file mode 100644 index 000000000..1a59fd3c1 --- /dev/null +++ b/src/app/utils/roomSupport.ts @@ -0,0 +1,16 @@ +export const knockSupported = (version: string): boolean => { + const unsupportedVersion = ['1', '2', '3', '4', '5', '6']; + return !unsupportedVersion.includes(version); +}; +export const restrictedSupported = (version: string): boolean => { + const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7']; + return !unsupportedVersion.includes(version); +}; +export const knockRestrictedSupported = (version: string): boolean => { + const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; + return !unsupportedVersion.includes(version); +}; +export const creatorsSupported = (version: string): boolean => { + const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; + return !unsupportedVersion.includes(version); +}; diff --git a/src/types/matrix/common.ts b/src/types/matrix/common.ts index e91993578..074777483 100644 --- a/src/types/matrix/common.ts +++ b/src/types/matrix/common.ts @@ -1,10 +1,13 @@ import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; import { MsgType } from '$types/matrix-sdk'; +import { SerializableMap } from '$types/wrapper/SerializableMap'; +import { SerializableSet } from '$types/wrapper/SerializableSet'; export const MATRIX_BLUR_HASH_PROPERTY_NAME = 'xyz.amorgan.blurhash'; export const MATRIX_SPOILER_PROPERTY_NAME = 'page.codeberg.everypizza.msc4193.spoiler'; export const MATRIX_SPOILER_REASON_PROPERTY_NAME = 'page.codeberg.everypizza.msc4193.spoiler.reason'; +export const MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME = 'com.beeper.msc4459.image_source_packs'; export type IImageInfo = { w?: number; @@ -14,11 +17,51 @@ export type IImageInfo = { [MATRIX_BLUR_HASH_PROPERTY_NAME]?: string; }; +export type MatrixRelatesTo = { + rel_type: 'm.annotation'; + event_id: string; + key?: string; +}; + +/** + * Image Pack Reference + * as per https://github.com/matrix-org/matrix-spec-proposals/pull/4459 + */ +export type MSC4459ImagePackReference = { + /** + * Id of the room where the image pack lives + */ + room_id?: string; + /** + * via servers to help join the room, + * optional + */ + via?: SerializableSet; + /** + * TODO doc + */ + state_key?: string; + /** + * the shortcode this emoji is refered by + */ + shortcode?: string; +}; + export type MSC1767Text = { body: string; mimetype?: string; }; +export type MatrixReactionEvent = { + 'm.relates_to': MatrixRelatesTo; + shortcode?: string; + 'com.beeper.reaction.shortcode'?: string; + /** + * a map of image pack references + */ + [MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME]?: SerializableMap; +}; + export type IVideoInfo = { w?: number; h?: number; diff --git a/src/types/wrapper/SerializableMap.ts b/src/types/wrapper/SerializableMap.ts new file mode 100644 index 000000000..3d2a402d2 --- /dev/null +++ b/src/types/wrapper/SerializableMap.ts @@ -0,0 +1,14 @@ +/** + * a simple wrapper to make a map seriazable to json + * + * @export + * @class SerializableMap + * @extends {Map} + * @template KeyType the type of the key + * @template ItemType the type of the item + */ +export class SerializableMap extends Map { + toJSON() { + return Object.fromEntries(this); + } +} diff --git a/src/types/wrapper/SerializableSet.ts b/src/types/wrapper/SerializableSet.ts new file mode 100644 index 000000000..3d6daa9de --- /dev/null +++ b/src/types/wrapper/SerializableSet.ts @@ -0,0 +1,13 @@ +/** + * a simple wrapper to make a set seriazable to json + * + * @export + * @class SerializableSet + * @extends {Set} + * @template ItemType the type of the items in the set + */ +export class SerializableSet extends Set { + toJSON() { + return Array.from(this); + } +} From f518ff50fe5e714a4cb0a9c056df05528f017826 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 3 May 2026 17:20:19 +0200 Subject: [PATCH 2/9] fixup! initial commit for MSC4459 support + a fair bit of refactoring --- src/app/components/create-room/CreateRoomAliasInput.tsx | 2 +- src/app/components/user-profile/UserRoomProfile.tsx | 3 ++- src/app/pages/auth/register/registerUtil.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/components/create-room/CreateRoomAliasInput.tsx b/src/app/components/create-room/CreateRoomAliasInput.tsx index 0460717a5..52e7b0186 100644 --- a/src/app/components/create-room/CreateRoomAliasInput.tsx +++ b/src/app/components/create-room/CreateRoomAliasInput.tsx @@ -9,11 +9,11 @@ import { import { MatrixError } from '$types/matrix-sdk'; import { Box, color, Icon, Icons, Input, Spinner, Text, toRem } from 'folds'; import { isKeyHotkey } from 'is-hotkey'; -import { getMxIdServer } from '$utils/matrix'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { replaceSpaceWithDash } from '$utils/common'; import { AsyncState, AsyncStatus, useAsync } from '$hooks/useAsyncCallback'; import { useDebounce } from '$hooks/useDebounce'; +import { getMxIdServer } from '$utils/mxIdHelper'; export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) { const mx = useMatrixClient(); diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 124642f84..ad63cb38c 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { useAtomValue } from 'jotai'; import { Opts as LinkifyOpts } from 'linkifyjs'; import { HTMLReactParserOptions } from 'html-react-parser'; -import { getMxIdServer, mxcUrlToHttp } from '$utils/matrix'; +import { mxcUrlToHttp } from '$utils/matrix'; import { getMemberAvatarMxc, getMemberDisplayName } from '$utils/room'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; @@ -35,6 +35,7 @@ import { getSettings, settingsAtom } from '$state/settings'; import { filterPronounsByLanguage } from '$utils/pronouns'; import { useSetting } from '$state/hooks/settings'; import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl'; +import { getMxIdServer } from '$utils/mxIdHelper'; import { CreatorChip } from './CreatorChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { PowerChip } from './PowerChip'; diff --git a/src/app/pages/auth/register/registerUtil.ts b/src/app/pages/auth/register/registerUtil.ts index 513aabfc0..97ddcd919 100644 --- a/src/app/pages/auth/register/registerUtil.ts +++ b/src/app/pages/auth/register/registerUtil.ts @@ -15,8 +15,9 @@ import { getAfterLoginRedirectPath, } from '$pages/afterLoginRedirectPath'; import { getHomePath, getLoginPath, withSearchParam } from '$pages/pathUtils'; -import { getMxIdLocalPart, getMxIdServer } from '$utils/matrix'; +import { getMxIdLocalPart } from '$utils/matrix'; import { activeSessionIdAtom, sessionsAtom, setFallbackSession } from '$state/sessions'; +import { getMxIdServer } from '$utils/mxIdHelper'; import { ErrorCode } from '../../../cs-errorcode'; export enum RegisterError { From 0b8b7bc534e3ddea08ddb0cecf5e43c1e3437c40 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 3 May 2026 17:39:25 +0200 Subject: [PATCH 3/9] fix(msc4459): remove duplicate matrix client argument --- src/app/features/room/RoomInput.tsx | 2 +- src/app/utils/matrix.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index fe00ac076..bc30145c8 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -657,7 +657,7 @@ export const RoomInput = forwardRef( const lastMessageId = lastMessage?.getId(); if (lastMessageId) { - toggleReaction(mx, room, lastMessageId, key, mx, shortcode); + toggleReaction(mx, room, lastMessageId, key, shortcode); } } diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index 1a1628c3b..fcb8f77c0 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -385,7 +385,6 @@ export const toggleReaction = ( room: Room, targetEventId: string, key: string, - matrixClient: MatrixClient, shortcode?: string, timelineSet?: EventTimelineSet ) => { @@ -408,6 +407,6 @@ export const toggleReaction = ( mx.sendEvent( room.roomId, MessageEvent.Reaction as any, - getReactionContent(targetEventId, key, matrixClient, rShortcode) + getReactionContent(targetEventId, key, mx, rShortcode) ); }; From 6a4d5059b8543f6e5034f8857a48fe7db0004f97 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 3 May 2026 18:19:45 +0200 Subject: [PATCH 4/9] fix(msc4459): refactoring and restricting getImagePackReferencesForMxc to public rooms --- .../message/modals/MessageForward.tsx | 33 +++---------------- src/app/features/room/RoomInput.tsx | 19 ++++++++++- src/app/utils/msc4459helper.ts | 2 ++ src/app/utils/roomVisibility.ts | 32 ++++++++++++++++++ src/types/matrix/common.ts | 4 +++ 5 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 src/app/utils/roomVisibility.ts diff --git a/src/app/components/message/modals/MessageForward.tsx b/src/app/components/message/modals/MessageForward.tsx index 133dc940e..c5d7134c0 100644 --- a/src/app/components/message/modals/MessageForward.tsx +++ b/src/app/components/message/modals/MessageForward.tsx @@ -17,17 +17,16 @@ import { as, } from 'folds'; import { useAtomValue, useSetAtom } from 'jotai'; -import { JoinRule, MatrixEvent, Room } from '$types/matrix-sdk'; +import { MatrixEvent, Room } from '$types/matrix-sdk'; import { useEffect, useMemo, useState } from 'react'; import { allRoomsAtom } from '$state/room-list/roomList'; import { useAllJoinedRoomsSet, useGetRoom } from '$hooks/useGetRoom'; import { factoryRoomIdByActivity } from '$utils/sort'; import * as css from '$features/room/message/styles.css'; import { sanitizeCustomHtml, sanitizeText } from '$utils/sanitize'; -import { getStateEvents } from '$utils/room'; -import { StateEvent } from '$types/matrix/room'; import { createDebugLogger } from '$utils/debugLogger'; import * as Sentry from '@sentry/react'; +import { isRoomPrivate } from '$utils/roomVisibility'; const debugLog = createDebugLogger('MessageForward'); @@ -111,28 +110,6 @@ export function MessageForwardInternal({ const [isForwarding, setIsForwarding] = useState(false); const [forwardError, setForwardError] = useState(null); const [targetRoomId, setTargetRoomId] = useState(null); - - // detect if it's a public room or not - const joinRule = room.getJoinRule() ?? JoinRule.Invite; - - const parentSpaceIds = getStateEvents(room, StateEvent.SpaceParent) - .map((e) => e.getStateKey()) - .filter((id): id is string => Boolean(id)); - - const isInPublicSpace = parentSpaceIds.some((spaceId) => { - const space = mx.getRoom(spaceId); - return Boolean(space?.isSpaceRoom()) && space?.getJoinRule() === JoinRule.Public; - }); - - // A room is private if its join rule is Invite (or other non-public/non-knock/non-restricted), - // or it's Restricted but NOT inside a public space. - const isPrivate = - joinRule === JoinRule.Invite || - (joinRule === JoinRule.Restricted && !isInPublicSpace) || - (joinRule !== JoinRule.Public && - joinRule !== JoinRule.Knock && - joinRule !== JoinRule.Restricted); - const allRooms = useAtomValue(allRoomsAtom); const allJoinedRooms = useAllJoinedRoomsSet(); const getRoom = useGetRoom(allJoinedRooms); @@ -190,7 +167,7 @@ export function MessageForwardInternal({ : undefined; const bodyModifText = `(Forwarded message from ${ - isPrivate ? 'a private room' : (getRoom(room.roomId)?.name ?? 'a room') + isRoomPrivate(mx, room) ? 'a private room' : (getRoom(room.roomId)?.name ?? 'a room') })`; let newBodyPlain = ''; @@ -229,7 +206,7 @@ export function MessageForwardInternal({ delete baseContent['com.beeper.per_message_profile']; // remove per-message profile as that could confuse clients in the target room let content; // handle privacy stuff - if (isPrivate) { + if (isRoomPrivate(mx, room)) { // if the message is from a private room, we should strip any media or mentions to avoid leaking information to the target room // we can still include the original message content in the body of the message, so we'll just use a fallback text/plain content with the original message body content = { @@ -270,7 +247,7 @@ export function MessageForwardInternal({ sourceRoomId: room.roomId, targetRoomId: targetRoom.roomId, msgtype, - isPrivate, + isPrivate: isRoomPrivate(mx, room), }); Sentry.metrics.count('sable.message.forward.attempt', 1, { attributes: { msgtype } }); mx.sendEvent(targetRoom.roomId, null, eventType, content as unknown as SendEventContent) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index bc30145c8..73b9e50e3 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -147,6 +147,14 @@ import { getSupportedAudioExtension } from '$plugins/voice-recorder-kit/supporte import { sanitizeText } from '$utils/sanitize'; import { PKitCommandMessageHandler } from '$plugins/pluralkit-handler/PKitCommandMessageHandler'; import { PKitProxyMessageHandler } from '$plugins/pluralkit-handler/PKitProxyMessageHandler'; +import { + IGenericMSC4459, + MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME, + MSC4459ImagePackReference, +} from '$types/matrix/common'; +import { getImagePackReferencesForMxc } from '$utils/msc4459helper'; +import { ImageUsage } from '$plugins/custom-emoji'; +import { SerializableMap } from '$types/wrapper/SerializableMap'; import { SchedulePickerDialog } from './schedule-send'; import * as css from './schedule-send/SchedulePickerDialog.css'; import { @@ -252,6 +260,7 @@ export const RoomInput = forwardRef( const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [mentionInReplies] = useSetting(settingsAtom, 'mentionInReplies'); const commands = useCommands(mx, room); + const imagePacksUsedRef = useRef(new SerializableMap()); /** * handle pluralkit-style messages */ @@ -854,6 +863,7 @@ export const RoomInput = forwardRef( resetEditor(editor); resetEditorHistory(editor); setInputKey((prev) => prev + 1); + imagePacksUsedRef.current.clear(); if (threadRootId) { // Re-seed the thread reply draft so the next message also goes to the thread. setReplyDraft({ @@ -1104,12 +1114,19 @@ export const RoomInput = forwardRef( await getImageUrlBlob(stickerUrl) ); - const content: StickerEventContent & ReplyEventContent & IContent = { + const content: StickerEventContent & ReplyEventContent & IContent & IGenericMSC4459 = { body: label, url: mxc, info, }; + // add the image pack reference + content[MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME] = getImagePackReferencesForMxc( + mxc, + mx, + ImageUsage.Sticker + ); + /** * the currently with the room associated per-message profile, if any, so that it can be included in the message content when sending. * This allows the server to apply the correct profile-based transformations (e.g. font size adjustments) when processing the message, diff --git a/src/app/utils/msc4459helper.ts b/src/app/utils/msc4459helper.ts index 070965408..6a700765d 100644 --- a/src/app/utils/msc4459helper.ts +++ b/src/app/utils/msc4459helper.ts @@ -7,6 +7,7 @@ import { ImageUsage } from '$plugins/custom-emoji'; import { SerializableSet } from '$types/wrapper/SerializableSet'; import { getViaServers } from '$plugins/via-servers'; import { getMxIdServer } from './mxIdHelper'; +import { isRoomPrivate } from './roomVisibility'; export function getImagePackReferencesForMxc( mxcUrl: string, @@ -21,6 +22,7 @@ export function getImagePackReferencesForMxc( .forEach((pack) => { const img = pack.getImages(imageUsage).find((val) => val.url === mxcUrl); const room = matrixClient.getRoom(pack.address?.roomId); + if (!room || isRoomPrivate(matrixClient, room)) return; const viaServers = new SerializableSet(); if (room) getViaServers(room).forEach((via) => { diff --git a/src/app/utils/roomVisibility.ts b/src/app/utils/roomVisibility.ts new file mode 100644 index 000000000..a76f3aeee --- /dev/null +++ b/src/app/utils/roomVisibility.ts @@ -0,0 +1,32 @@ +import { StateEvent } from '$types/matrix/room'; +import { JoinRule, MatrixClient, Room } from 'matrix-js-sdk'; +import { getStateEvents } from './room'; + +/** + * simple check to see if a room can be considered private + * + * @export + * @param {MatrixClient} mx the matrix client + * @param {Room} room the room to check + * @return {*} {boolean} true if the room is considered private + */ +export function isRoomPrivate(mx: MatrixClient, room: Room): boolean { + // detect if it's a public room or not + const joinRule = room.getJoinRule() ?? JoinRule.Invite; + + const parentSpaceIds = getStateEvents(room, StateEvent.SpaceParent) + .map((e) => e.getStateKey()) + .filter((id): id is string => Boolean(id)); + const isInPublicSpace = parentSpaceIds.some((spaceId) => { + const space = mx.getRoom(spaceId); + return Boolean(space?.isSpaceRoom()) && space?.getJoinRule() === JoinRule.Public; + }); + + return ( + joinRule === JoinRule.Invite || + (joinRule === JoinRule.Restricted && !isInPublicSpace) || + (joinRule !== JoinRule.Public && + joinRule !== JoinRule.Knock && + joinRule !== JoinRule.Restricted) + ); +} diff --git a/src/types/matrix/common.ts b/src/types/matrix/common.ts index 074777483..669ee9e2d 100644 --- a/src/types/matrix/common.ts +++ b/src/types/matrix/common.ts @@ -62,6 +62,10 @@ export type MatrixReactionEvent = { [MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME]?: SerializableMap; }; +export interface IGenericMSC4459 { + [MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME]?: SerializableMap; +} + export type IVideoInfo = { w?: number; h?: number; From 3d1b1026bbb34fd1a058daccfb33c594783d273d Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 3 May 2026 19:08:56 +0200 Subject: [PATCH 5/9] fix(formatting): fix formatting issues --- src/app/components/message/modals/MessageForward.tsx | 1 - src/app/features/room/RoomInput.tsx | 7 ++----- src/app/utils/messageReaction.ts | 5 +++-- src/app/utils/msc4459helper.ts | 8 ++++---- src/app/utils/roomVisibility.ts | 6 +++--- src/types/matrix/common.ts | 4 ++-- 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/app/components/message/modals/MessageForward.tsx b/src/app/components/message/modals/MessageForward.tsx index f0cbed796..ebd8566c0 100644 --- a/src/app/components/message/modals/MessageForward.tsx +++ b/src/app/components/message/modals/MessageForward.tsx @@ -18,7 +18,6 @@ import { } from 'folds'; import { useAtomValue, useSetAtom } from 'jotai'; import type { MatrixEvent, Room } from '$types/matrix-sdk'; -import { JoinRule, EventType } from '$types/matrix-sdk'; import { useEffect, useMemo, useState } from 'react'; import { allRoomsAtom } from '$state/room-list/roomList'; import { useAllJoinedRoomsSet, useGetRoom } from '$hooks/useGetRoom'; diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 9662916ec..38a91eac2 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -128,11 +128,8 @@ import { getSupportedAudioExtension } from '$plugins/voice-recorder-kit/supporte import { sanitizeText } from '$utils/sanitize'; import { PKitCommandMessageHandler } from '$plugins/pluralkit-handler/PKitCommandMessageHandler'; import { PKitProxyMessageHandler } from '$plugins/pluralkit-handler/PKitProxyMessageHandler'; -import { - IGenericMSC4459, - MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME, - MSC4459ImagePackReference, -} from '$types/matrix/common'; +import { MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME } from '$types/matrix/common'; +import type {IGenericMSC4459, MSC4459ImagePackReference} from '$types/matrix/common' import { getImagePackReferencesForMxc } from '$utils/msc4459helper'; import { ImageUsage } from '$plugins/custom-emoji'; import { SerializableMap } from '$types/wrapper/SerializableMap'; diff --git a/src/app/utils/messageReaction.ts b/src/app/utils/messageReaction.ts index 7fd10205c..4fbb1e856 100644 --- a/src/app/utils/messageReaction.ts +++ b/src/app/utils/messageReaction.ts @@ -1,5 +1,6 @@ -import { MatrixReactionEvent, MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME } from '$types/matrix/common'; -import { MatrixClient } from 'matrix-js-sdk'; +import { MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME } from '$types/matrix/common'; +import type { MatrixReactionEvent } from '$types/matrix/common'; +import type { MatrixClient } from 'matrix-js-sdk'; import { ImageUsage } from '$plugins/custom-emoji'; import { getImagePackReferencesForMxc } from './msc4459helper'; diff --git a/src/app/utils/msc4459helper.ts b/src/app/utils/msc4459helper.ts index 6a700765d..d8d05a923 100644 --- a/src/app/utils/msc4459helper.ts +++ b/src/app/utils/msc4459helper.ts @@ -1,9 +1,9 @@ import { getGlobalImagePacks } from '$plugins/custom-emoji/utils'; -import { ImagePack } from '$plugins/custom-emoji/ImagePack'; -import { MSC4459ImagePackReference } from '$types/matrix/common'; +import type { ImagePack } from '$plugins/custom-emoji/ImagePack'; +import type { MSC4459ImagePackReference } from '$types/matrix/common'; import { SerializableMap } from '$types/wrapper/SerializableMap'; -import { MatrixClient } from 'matrix-js-sdk'; -import { ImageUsage } from '$plugins/custom-emoji'; +import type { MatrixClient } from 'matrix-js-sdk'; +import type { ImageUsage } from '$plugins/custom-emoji'; import { SerializableSet } from '$types/wrapper/SerializableSet'; import { getViaServers } from '$plugins/via-servers'; import { getMxIdServer } from './mxIdHelper'; diff --git a/src/app/utils/roomVisibility.ts b/src/app/utils/roomVisibility.ts index a76f3aeee..2f88f092a 100644 --- a/src/app/utils/roomVisibility.ts +++ b/src/app/utils/roomVisibility.ts @@ -1,5 +1,5 @@ -import { StateEvent } from '$types/matrix/room'; -import { JoinRule, MatrixClient, Room } from 'matrix-js-sdk'; +import { EventType, JoinRule } from 'matrix-js-sdk'; +import type { MatrixClient, Room } from 'matrix-js-sdk'; import { getStateEvents } from './room'; /** @@ -14,7 +14,7 @@ export function isRoomPrivate(mx: MatrixClient, room: Room): boolean { // detect if it's a public room or not const joinRule = room.getJoinRule() ?? JoinRule.Invite; - const parentSpaceIds = getStateEvents(room, StateEvent.SpaceParent) + const parentSpaceIds = getStateEvents(room, EventType.SpaceParent) .map((e) => e.getStateKey()) .filter((id): id is string => Boolean(id)); const isInPublicSpace = parentSpaceIds.some((spaceId) => { diff --git a/src/types/matrix/common.ts b/src/types/matrix/common.ts index 52c269c16..2f7c8abe2 100644 --- a/src/types/matrix/common.ts +++ b/src/types/matrix/common.ts @@ -1,5 +1,5 @@ -import { SerializableMap } from '$types/wrapper/SerializableMap'; -import { SerializableSet } from '$types/wrapper/SerializableSet'; +import type { SerializableMap } from '$types/wrapper/SerializableMap'; +import type { SerializableSet } from '$types/wrapper/SerializableSet'; import type { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; import type { MsgType } from '$types/matrix-sdk'; From 58a98b1d9697c6cb3413553853ec11500bc0d875 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 3 May 2026 20:08:16 +0200 Subject: [PATCH 6/9] fix(msc4459): emoji discoverable as per MSC4459 --- .changeset/initial_msc4459_support.md | 5 ++++ src/app/features/room/RoomInput.tsx | 33 +++++++++++++++++++++++---- src/app/utils/messageReaction.ts | 4 ++-- src/app/utils/msc4459helper.ts | 27 ++++++++++++++-------- 4 files changed, 53 insertions(+), 16 deletions(-) create mode 100644 .changeset/initial_msc4459_support.md diff --git a/.changeset/initial_msc4459_support.md b/.changeset/initial_msc4459_support.md new file mode 100644 index 000000000..8d0571a37 --- /dev/null +++ b/.changeset/initial_msc4459_support.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +# add initial support for sending discoverable emojis and sticker diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 38a91eac2..f4738e0b5 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -60,6 +60,7 @@ import { ANYWHERE_AUTOCOMPLETE_PREFIXES, BEGINNING_AUTOCOMPLETE_PREFIXES, getLinks, + replaceWithElement, } from '$components/editor'; import { EmojiBoard, EmojiBoardTab } from '$components/emoji-board'; import { UseStateProvider } from '$components/UseStateProvider'; @@ -129,8 +130,11 @@ import { sanitizeText } from '$utils/sanitize'; import { PKitCommandMessageHandler } from '$plugins/pluralkit-handler/PKitCommandMessageHandler'; import { PKitProxyMessageHandler } from '$plugins/pluralkit-handler/PKitProxyMessageHandler'; import { MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME } from '$types/matrix/common'; -import type {IGenericMSC4459, MSC4459ImagePackReference} from '$types/matrix/common' -import { getImagePackReferencesForMxc } from '$utils/msc4459helper'; +import type { IGenericMSC4459, MSC4459ImagePackReference } from '$types/matrix/common'; +import { + getImagePackReferencesForMxc, + getImagePackReferencesForMxcWrappedInMap, +} from '$utils/msc4459helper'; import { ImageUsage } from '$plugins/custom-emoji'; import { SerializableMap } from '$types/wrapper/SerializableMap'; import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl'; @@ -171,7 +175,7 @@ const getLatestThreadEventId = (room: Room, threadRootId: string): string => { ev.threadRootId === threadRootId && ev.getId() !== threadRootId && !reactionOrEditEvent(ev) ); if (liveEvents.length > 0) { - return liveEvents[liveEvents.length - 1]!.getId() ?? threadRootId; + return liveEvents.at(-1)!.getId() ?? threadRootId; } return threadRootId; }; @@ -749,6 +753,7 @@ export const RoomInput = forwardRef( nickNameReplacement: nicknameReplacement, }) ); + let msgType = MsgType.Text; // quick text react @@ -809,6 +814,7 @@ export const RoomInput = forwardRef( } content['m.mentions'] = getMentionContent(Array.from(mentionData.users), mentionData.room); + content[MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME] = imagePacksUsedRef.current.toJSON(); const links = getLinks(serializedChildren); content['com.beeper.linkpreviews'] = []; @@ -1122,8 +1128,19 @@ export const RoomInput = forwardRef( ); const handleEmoticonSelect = (key: string, shortcode: string) => { - editor.insertNode(createEmoticonElement(key, shortcode)); + const emoticonEl = createEmoticonElement(key, shortcode); + if (autocompleteQuery) { + replaceWithElement(editor, autocompleteQuery.range, emoticonEl); + } else { + editor.insertNode(emoticonEl); + } + if (!imagePacksUsedRef.current.has(key)) + imagePacksUsedRef.current.set( + key, + getImagePackReferencesForMxc(key, mx, ImageUsage.Emoticon) + ); moveCursor(editor); + handleCloseAutocomplete(); }; const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => { @@ -1142,7 +1159,7 @@ export const RoomInput = forwardRef( }; // add the image pack reference - content[MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME] = getImagePackReferencesForMxc( + content[MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME] = getImagePackReferencesForMxcWrappedInMap( mxc, mx, ImageUsage.Sticker @@ -1161,6 +1178,11 @@ export const RoomInput = forwardRef( false ); } + content[MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME] = getImagePackReferencesForMxcWrappedInMap( + mxc, + mx, + ImageUsage.Sticker + ); if (replyDraft) { content['m.relates_to'] = getReplyContent(replyDraft, room); @@ -1259,6 +1281,7 @@ export const RoomInput = forwardRef( editor={editor} query={autocompleteQuery} requestClose={handleCloseAutocomplete} + onEmoticonSelected={handleEmoticonSelect} /> )} {autocompleteQuery?.prefix === AutocompletePrefix.Reaction && diff --git a/src/app/utils/messageReaction.ts b/src/app/utils/messageReaction.ts index 4fbb1e856..58a4f0a8b 100644 --- a/src/app/utils/messageReaction.ts +++ b/src/app/utils/messageReaction.ts @@ -2,7 +2,7 @@ import { MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME } from '$types/matrix/common'; import type { MatrixReactionEvent } from '$types/matrix/common'; import type { MatrixClient } from 'matrix-js-sdk'; import { ImageUsage } from '$plugins/custom-emoji'; -import { getImagePackReferencesForMxc } from './msc4459helper'; +import { getImagePackReferencesForMxcWrappedInMap } from './msc4459helper'; export const getReactionContent = ( eventId: string, @@ -17,7 +17,7 @@ export const getReactionContent = ( }, shortcode, 'com.beeper.reaction.shortcode': shortcode, - [MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME]: getImagePackReferencesForMxc( + [MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME]: getImagePackReferencesForMxcWrappedInMap( key, matrixClient, ImageUsage.Emoticon diff --git a/src/app/utils/msc4459helper.ts b/src/app/utils/msc4459helper.ts index d8d05a923..c8277ccac 100644 --- a/src/app/utils/msc4459helper.ts +++ b/src/app/utils/msc4459helper.ts @@ -9,17 +9,26 @@ import { getViaServers } from '$plugins/via-servers'; import { getMxIdServer } from './mxIdHelper'; import { isRoomPrivate } from './roomVisibility'; -export function getImagePackReferencesForMxc( +export function getImagePackReferencesForMxcWrappedInMap( mxcUrl: string, matrixClient: MatrixClient, imageUsage: ImageUsage ): SerializableMap { + const retMap = new SerializableMap(); + retMap.set(mxcUrl, getImagePackReferencesForMxc(mxcUrl, matrixClient, imageUsage)); + return retMap; +} + +export function getImagePackReferencesForMxc( + mxcUrl: string, + matrixClient: MatrixClient, + imageUsage: ImageUsage +): MSC4459ImagePackReference { const globalImgPacks: ImagePack[] = getGlobalImagePacks(matrixClient); - if (!mxcUrl.startsWith('mxc')) return new SerializableMap(); - const imagePackReferences = new SerializableMap(); - globalImgPacks + if (!mxcUrl.startsWith('mxc')) return {}; + const imgPkRef = globalImgPacks .filter((val) => val.getImages(imageUsage).find((img) => img.url === mxcUrl)) - .forEach((pack) => { + .map((pack) => { const img = pack.getImages(imageUsage).find((val) => val.url === mxcUrl); const room = matrixClient.getRoom(pack.address?.roomId); if (!room || isRoomPrivate(matrixClient, room)) return; @@ -31,13 +40,13 @@ export function getImagePackReferencesForMxc( // add ones own hs as via server, as that server evidently is alive const ownViaHS = getMxIdServer(matrixClient.getSafeUserId()); if (ownViaHS) viaServers.add(ownViaHS); - const imgPkRef = { + return { room_id: pack.address?.roomId, state_key: pack.address?.stateKey, via: viaServers, shortcode: img?.shortcode, } satisfies MSC4459ImagePackReference; - imagePackReferences.set(mxcUrl, imgPkRef); - }); - return imagePackReferences; + }) + .at(0); + return imgPkRef ?? {}; } From 952212e8d2720268eed990884c0ededa973ee165 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 3 May 2026 21:11:34 +0200 Subject: [PATCH 7/9] fix(msc4459): fix room local image packs --- src/app/features/room/RoomInput.tsx | 8 ++-- src/app/utils/matrix.ts | 8 +++- src/app/utils/messageReaction.ts | 6 ++- src/app/utils/msc4459helper.ts | 59 ++++++++++++++++++++++------- 4 files changed, 62 insertions(+), 19 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index f4738e0b5..9498439b9 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -1137,7 +1137,7 @@ export const RoomInput = forwardRef( if (!imagePacksUsedRef.current.has(key)) imagePacksUsedRef.current.set( key, - getImagePackReferencesForMxc(key, mx, ImageUsage.Emoticon) + getImagePackReferencesForMxc(key, mx, ImageUsage.Emoticon, room) ); moveCursor(editor); handleCloseAutocomplete(); @@ -1162,7 +1162,8 @@ export const RoomInput = forwardRef( content[MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME] = getImagePackReferencesForMxcWrappedInMap( mxc, mx, - ImageUsage.Sticker + ImageUsage.Sticker, + room ); /** @@ -1181,7 +1182,8 @@ export const RoomInput = forwardRef( content[MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME] = getImagePackReferencesForMxcWrappedInMap( mxc, mx, - ImageUsage.Sticker + ImageUsage.Sticker, + room ); if (replyDraft) { diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index 93849cb71..5ecf00966 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -420,6 +420,12 @@ export const toggleReaction = ( mx.sendEvent( room.roomId, EventType.Reaction as string as unknown as keyof TimelineEvents, - getReactionContent(targetEventId, key, mx, rShortcode) as TimelineEvents[keyof TimelineEvents] + getReactionContent( + targetEventId, + key, + mx, + room, + rShortcode + ) as TimelineEvents[keyof TimelineEvents] ); }; diff --git a/src/app/utils/messageReaction.ts b/src/app/utils/messageReaction.ts index 58a4f0a8b..cf42ea607 100644 --- a/src/app/utils/messageReaction.ts +++ b/src/app/utils/messageReaction.ts @@ -1,6 +1,6 @@ import { MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME } from '$types/matrix/common'; import type { MatrixReactionEvent } from '$types/matrix/common'; -import type { MatrixClient } from 'matrix-js-sdk'; +import type { MatrixClient, Room } from 'matrix-js-sdk'; import { ImageUsage } from '$plugins/custom-emoji'; import { getImagePackReferencesForMxcWrappedInMap } from './msc4459helper'; @@ -8,6 +8,7 @@ export const getReactionContent = ( eventId: string, key: string, matrixClient: MatrixClient, + room: Room, shortcode?: string ): MatrixReactionEvent => ({ 'm.relates_to': { @@ -20,6 +21,7 @@ export const getReactionContent = ( [MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME]: getImagePackReferencesForMxcWrappedInMap( key, matrixClient, - ImageUsage.Emoticon + ImageUsage.Emoticon, + room ), }); diff --git a/src/app/utils/msc4459helper.ts b/src/app/utils/msc4459helper.ts index c8277ccac..04a1c06d9 100644 --- a/src/app/utils/msc4459helper.ts +++ b/src/app/utils/msc4459helper.ts @@ -1,8 +1,8 @@ -import { getGlobalImagePacks } from '$plugins/custom-emoji/utils'; +import { getGlobalImagePacks, getRoomImagePacks } from '$plugins/custom-emoji/utils'; import type { ImagePack } from '$plugins/custom-emoji/ImagePack'; import type { MSC4459ImagePackReference } from '$types/matrix/common'; import { SerializableMap } from '$types/wrapper/SerializableMap'; -import type { MatrixClient } from 'matrix-js-sdk'; +import type { MatrixClient, Room } from 'matrix-js-sdk'; import type { ImageUsage } from '$plugins/custom-emoji'; import { SerializableSet } from '$types/wrapper/SerializableSet'; import { getViaServers } from '$plugins/via-servers'; @@ -12,26 +12,31 @@ import { isRoomPrivate } from './roomVisibility'; export function getImagePackReferencesForMxcWrappedInMap( mxcUrl: string, matrixClient: MatrixClient, - imageUsage: ImageUsage + imageUsage: ImageUsage, + room: Room ): SerializableMap { const retMap = new SerializableMap(); - retMap.set(mxcUrl, getImagePackReferencesForMxc(mxcUrl, matrixClient, imageUsage)); + if (!mxcUrl.startsWith('mxc')) return retMap; + const result = getImagePackReferencesForMxc(mxcUrl, matrixClient, imageUsage, room); + // if the result is undefined return the empty map, to not produce invalid entries + if (!result?.room_id) return retMap; + retMap.set(mxcUrl, result); return retMap; } -export function getImagePackReferencesForMxc( +function getImagePackReferencesForMxcInternal( mxcUrl: string, matrixClient: MatrixClient, - imageUsage: ImageUsage -): MSC4459ImagePackReference { - const globalImgPacks: ImagePack[] = getGlobalImagePacks(matrixClient); - if (!mxcUrl.startsWith('mxc')) return {}; - const imgPkRef = globalImgPacks + packs: ImagePack[], + imageUsage: ImageUsage, + bypassPrivateFilter = false +) { + return packs .filter((val) => val.getImages(imageUsage).find((img) => img.url === mxcUrl)) .map((pack) => { const img = pack.getImages(imageUsage).find((val) => val.url === mxcUrl); const room = matrixClient.getRoom(pack.address?.roomId); - if (!room || isRoomPrivate(matrixClient, room)) return; + if (!room || (isRoomPrivate(matrixClient, room) && !bypassPrivateFilter)) return; const viaServers = new SerializableSet(); if (room) getViaServers(room).forEach((via) => { @@ -47,6 +52,34 @@ export function getImagePackReferencesForMxc( shortcode: img?.shortcode, } satisfies MSC4459ImagePackReference; }) - .at(0); - return imgPkRef ?? {}; + .find((val) => val != undefined); +} + +export function getImagePackReferencesForMxc( + mxcUrl: string, + matrixClient: MatrixClient, + imageUsage: ImageUsage, + room: Room +): MSC4459ImagePackReference { + if (!mxcUrl.startsWith('mxc')) return {}; + const globalImgPacks: ImagePack[] = getGlobalImagePacks(matrixClient); + const roomLocalImgPacks: ImagePack[] = getRoomImagePacks(room); + const roomLocalMatch = getImagePackReferencesForMxcInternal( + mxcUrl, + matrixClient, + roomLocalImgPacks, + imageUsage, + true + ); + // prefer room local match as they're probably often more relevant + if (roomLocalMatch) return roomLocalMatch; + const globalMatch = getImagePackReferencesForMxcInternal( + mxcUrl, + matrixClient, + globalImgPacks, + imageUsage, + false + ); + + return globalMatch ?? {}; } From ff41ff10611504259c3d82a08869bfcd4a59dad9 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 4 May 2026 06:40:25 +0200 Subject: [PATCH 8/9] perf: simple caching (might need some later adjustments) --- src/app/utils/msc4459helper.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/app/utils/msc4459helper.ts b/src/app/utils/msc4459helper.ts index 04a1c06d9..79b809522 100644 --- a/src/app/utils/msc4459helper.ts +++ b/src/app/utils/msc4459helper.ts @@ -9,6 +9,17 @@ import { getViaServers } from '$plugins/via-servers'; import { getMxIdServer } from './mxIdHelper'; import { isRoomPrivate } from './roomVisibility'; +/** + * lookup table for global mxc => MSC4459ImagePackReference + * TODO this is far from a perfect solution + */ +const globalLookupTable = new Map(); +/** + * lookup table for room local mxc => MSC4459ImagePackReference + * TODO this is far from a perfect solution + */ +const roomLookupTable = new Map>(); + export function getImagePackReferencesForMxcWrappedInMap( mxcUrl: string, matrixClient: MatrixClient, @@ -62,7 +73,8 @@ export function getImagePackReferencesForMxc( room: Room ): MSC4459ImagePackReference { if (!mxcUrl.startsWith('mxc')) return {}; - const globalImgPacks: ImagePack[] = getGlobalImagePacks(matrixClient); + if (roomLookupTable.get(room.roomId)?.has(mxcUrl)) + return roomLookupTable.get(room.roomId)!.get(mxcUrl)!; const roomLocalImgPacks: ImagePack[] = getRoomImagePacks(room); const roomLocalMatch = getImagePackReferencesForMxcInternal( mxcUrl, @@ -71,8 +83,17 @@ export function getImagePackReferencesForMxc( imageUsage, true ); + if (roomLocalMatch) { + const roomLookupTabRes = + roomLookupTable.get(room.roomId) ?? new Map(); + roomLookupTabRes.set(mxcUrl, roomLocalMatch); + roomLookupTable.set(room.roomId, roomLookupTabRes); + } // prefer room local match as they're probably often more relevant if (roomLocalMatch) return roomLocalMatch; + // simple caching + if (globalLookupTable.has(mxcUrl)) return globalLookupTable.get(mxcUrl)!; + const globalImgPacks: ImagePack[] = getGlobalImagePacks(matrixClient); const globalMatch = getImagePackReferencesForMxcInternal( mxcUrl, matrixClient, @@ -80,6 +101,7 @@ export function getImagePackReferencesForMxc( imageUsage, false ); + if (globalMatch) globalLookupTable.set(mxcUrl, globalMatch); return globalMatch ?? {}; } From f49e84d1af12e15050ac08057690cce2bc70e94f Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 4 May 2026 06:56:55 +0200 Subject: [PATCH 9/9] fix(msc4459): don't set mxc when there is no data to associate with it --- src/app/features/room/RoomInput.tsx | 9 ++++----- src/app/utils/msc4459helper.ts | 11 +++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 9498439b9..9c64bf116 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -1134,11 +1134,10 @@ export const RoomInput = forwardRef( } else { editor.insertNode(emoticonEl); } - if (!imagePacksUsedRef.current.has(key)) - imagePacksUsedRef.current.set( - key, - getImagePackReferencesForMxc(key, mx, ImageUsage.Emoticon, room) - ); + if (!imagePacksUsedRef.current.has(key)) { + const imgPkRef = getImagePackReferencesForMxc(key, mx, ImageUsage.Emoticon, room); + if (imgPkRef?.room_id && imgPkRef?.shortcode) imagePacksUsedRef.current.set(key, imgPkRef); + } moveCursor(editor); handleCloseAutocomplete(); }; diff --git a/src/app/utils/msc4459helper.ts b/src/app/utils/msc4459helper.ts index 79b809522..d999130d5 100644 --- a/src/app/utils/msc4459helper.ts +++ b/src/app/utils/msc4459helper.ts @@ -30,7 +30,7 @@ export function getImagePackReferencesForMxcWrappedInMap( if (!mxcUrl.startsWith('mxc')) return retMap; const result = getImagePackReferencesForMxc(mxcUrl, matrixClient, imageUsage, room); // if the result is undefined return the empty map, to not produce invalid entries - if (!result?.room_id) return retMap; + if (!result?.room_id || !result?.state_key || !result?.shortcode) return retMap; retMap.set(mxcUrl, result); return retMap; } @@ -90,7 +90,7 @@ export function getImagePackReferencesForMxc( roomLookupTable.set(room.roomId, roomLookupTabRes); } // prefer room local match as they're probably often more relevant - if (roomLocalMatch) return roomLocalMatch; + if (roomLocalMatch?.room_id && roomLocalMatch?.shortcode) return roomLocalMatch; // simple caching if (globalLookupTable.has(mxcUrl)) return globalLookupTable.get(mxcUrl)!; const globalImgPacks: ImagePack[] = getGlobalImagePacks(matrixClient); @@ -101,7 +101,10 @@ export function getImagePackReferencesForMxc( imageUsage, false ); - if (globalMatch) globalLookupTable.set(mxcUrl, globalMatch); + if (globalMatch?.room_id && globalMatch?.shortcode) { + globalLookupTable.set(mxcUrl, globalMatch); + return globalMatch; + } - return globalMatch ?? {}; + return {}; }