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/components/create-room/AdditionalCreatorInput.tsx b/src/app/components/create-room/AdditionalCreatorInput.tsx index d88c1e555..1ac523a4d 100644 --- a/src/app/components/create-room/AdditionalCreatorInput.tsx +++ b/src/app/components/create-room/AdditionalCreatorInput.tsx @@ -17,9 +17,10 @@ import { } from 'folds'; import { isKeyHotkey } from 'is-hotkey'; import FocusTrap from 'focus-trap-react'; +import { getMxIdServer } from '$utils/mxIdHelper'; import type { ChangeEventHandler, KeyboardEventHandler, MouseEventHandler } from 'react'; 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/CreateRoomAliasInput.tsx b/src/app/components/create-room/CreateRoomAliasInput.tsx index a5f12f95a..f1d798307 100644 --- a/src/app/components/create-room/CreateRoomAliasInput.tsx +++ b/src/app/components/create-room/CreateRoomAliasInput.tsx @@ -3,12 +3,12 @@ import { useCallback, useEffect, useRef, useState } from 'react'; 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 type { AsyncState } from '$hooks/useAsyncCallback'; import { 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/create-room/utils.ts b/src/app/components/create-room/utils.ts index 65d125da1..063d91947 100644 --- a/src/app/components/create-room/utils.ts +++ b/src/app/components/create-room/utils.ts @@ -9,7 +9,7 @@ import { JoinRule, RestrictedAllowType, EventType, RoomType } from '$types/matri import type { StateEvents } from '$types/matrix-sdk'; 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 36279710a..b7b935f6f 100644 --- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx @@ -9,9 +9,9 @@ import { useAtomValue } from 'jotai'; import { getDirectRoomAvatarUrl } from '$utils/room'; import { useMatrixClient } from '$hooks/useMatrixClient'; -import { getMxIdServer, isRoomAlias } from '$utils/matrix'; -import type { UseAsyncSearchOptions } from '$hooks/useAsyncSearch'; +import { isRoomAlias } from '$utils/matrix'; import { useAsyncSearch } from '$hooks/useAsyncSearch'; +import type { UseAsyncSearchOptions } from '$hooks/useAsyncSearch'; import { onTabPress } from '$utils/keyboard'; import { useKeyDown } from '$hooks/useKeyDown'; import { mDirectAtom } from '$state/mDirectList'; @@ -20,6 +20,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 type { AutocompleteQuery } from './autocompleteQuery'; diff --git a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx index f8aed1284..1806fb2d7 100644 --- a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx @@ -11,7 +11,7 @@ import type { SearchItemStrGetter, UseAsyncSearchOptions } from '$hooks/useAsync import { 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'; @@ -19,6 +19,7 @@ import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; 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 type { AutocompleteQuery } from './autocompleteQuery'; import { KnownMembership } from '$types/matrix-sdk'; diff --git a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx index 9ab427ab3..0d81bc5d7 100644 --- a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx +++ b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx @@ -27,8 +27,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 type { UseAsyncSearchOptions } from '$hooks/useAsyncSearch'; import { useAsyncSearch } from '$hooks/useAsyncSearch'; import { highlightText, makeHighlightRegex } from '$plugins/react-custom-html-parser'; @@ -36,6 +35,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'; import { KnownMembership } from '$types/matrix-sdk'; const SEARCH_OPTIONS: UseAsyncSearchOptions = { diff --git a/src/app/components/message/modals/MessageForward.tsx b/src/app/components/message/modals/MessageForward.tsx index 94df68049..ebd8566c0 100644 --- a/src/app/components/message/modals/MessageForward.tsx +++ b/src/app/components/message/modals/MessageForward.tsx @@ -18,17 +18,15 @@ 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'; 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 { createDebugLogger } from '$utils/debugLogger'; import * as Sentry from '@sentry/react'; +import { isRoomPrivate } from '$utils/roomVisibility'; const debugLog = createDebugLogger('MessageForward'); @@ -112,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, EventType.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); @@ -191,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 = ''; @@ -230,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 = { @@ -271,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/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx index 07b5bbe62..586ff9d50 100644 --- a/src/app/components/user-profile/UserChips.tsx +++ b/src/app/components/user-profile/UserChips.tsx @@ -22,7 +22,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'; @@ -43,6 +42,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/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 4f5c4a69a..15eba1aae 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { useAtomValue } from 'jotai'; import type { Opts as LinkifyOpts } from 'linkifyjs'; import type { 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'; @@ -37,6 +37,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 { TextViewerContent } from '$components/text-viewer'; import { CreatorChip } from './CreatorChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; diff --git a/src/app/features/common-settings/general/RoomAddress.tsx b/src/app/features/common-settings/general/RoomAddress.tsx index 269f2c1b4..d7b6a400c 100644 --- a/src/app/features/common-settings/general/RoomAddress.tsx +++ b/src/app/features/common-settings/general/RoomAddress.tsx @@ -31,9 +31,9 @@ import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { CutoutCard } from '$components/cutout-card'; import { replaceSpaceWithDash } from '$utils/common'; import { useAlive } from '$hooks/useAlive'; +import { getMxIdServer } from '$utils/mxIdHelper'; import type { RoomPermissionsAPI } from '$hooks/useRoomPermissions'; -import { getMxIdServer } from '$utils/matrix'; import { EventType } from '$types/matrix-sdk'; type RoomPublishedAddressesProps = { diff --git a/src/app/features/common-settings/general/RoomJoinRules.tsx b/src/app/features/common-settings/general/RoomJoinRules.tsx index 4cdea0dfd..042fbd5af 100644 --- a/src/app/features/common-settings/general/RoomJoinRules.tsx +++ b/src/app/features/common-settings/general/RoomJoinRules.tsx @@ -22,7 +22,7 @@ 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 { knockRestrictedSupported, restrictedSupported, knockSupported } from '$utils/roomSupport'; import type { RoomPermissionsAPI } from '$hooks/useRoomPermissions'; type RestrictedRoomAllowContent = { diff --git a/src/app/features/common-settings/general/RoomUpgrade.tsx b/src/app/features/common-settings/general/RoomUpgrade.tsx index 0c1534f4a..70131e7f0 100644 --- a/src/app/features/common-settings/general/RoomUpgrade.tsx +++ b/src/app/features/common-settings/general/RoomUpgrade.tsx @@ -37,9 +37,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 71ef0a74d..cfdc7615e 100644 --- a/src/app/features/common-settings/members/Members.tsx +++ b/src/app/features/common-settings/members/Members.tsx @@ -25,7 +25,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 type { SearchItemStrGetter, UseAsyncSearchOptions } from '$hooks/useAsyncSearch'; @@ -44,6 +44,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 be1cb7da6..ba43bafa0 100644 --- a/src/app/features/common-settings/permissions/PowersEditor.tsx +++ b/src/app/features/common-settings/permissions/PowersEditor.tsx @@ -41,14 +41,13 @@ import { CompactUploadCardRenderer } from '$components/upload-card'; import type { UploadSuccess } from '$state/upload'; import { createUploadAtom } from '$state/upload'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; -import type { MemberPowerTag, MemberPowerTagIcon } from '$types/matrix/room'; +import { CustomStateEvent, type MemberPowerTag, type MemberPowerTagIcon } from '$types/matrix/room'; 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 { CustomStateEvent } from '$types/matrix/room'; +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 911fb455b..59b2ec571 100644 --- a/src/app/features/create-room/CreateRoom.tsx +++ b/src/app/features/create-room/CreateRoom.tsx @@ -18,12 +18,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'; @@ -44,6 +38,12 @@ import { 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 5a7c0f7b1..8518110ad 100644 --- a/src/app/features/create-space/CreateSpace.tsx +++ b/src/app/features/create-space/CreateSpace.tsx @@ -18,12 +18,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'; @@ -39,6 +33,12 @@ import { RoomVersionSelector, useAdditionalCreators, } from '$components/create-room'; +import { + restrictedSupported, + creatorsSupported, + knockSupported, + knockRestrictedSupported, +} from '$utils/roomSupport'; import { ErrorCode } from '../../cs-errorcode'; diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 3eb901dc6..d0223f6bc 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -58,6 +58,7 @@ import { ANYWHERE_AUTOCOMPLETE_PREFIXES, BEGINNING_AUTOCOMPLETE_PREFIXES, getLinks, + replaceWithElement, BlockType, } from '$components/editor'; import { EmojiBoard, EmojiBoardTab } from '$components/emoji-board'; @@ -127,6 +128,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 { MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME } from '$types/matrix/common'; +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'; import { SchedulePickerDialog } from './schedule-send'; import * as css from './schedule-send/SchedulePickerDialog.css'; @@ -165,7 +174,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; }; @@ -238,6 +247,7 @@ export const RoomInput = forwardRef( const [mentionInReplies] = useSetting(settingsAtom, 'mentionInReplies'); const settingsLinkBaseUrl = useSettingsLinkBaseUrl(); const commands = useCommands(mx, room); + const imagePacksUsedRef = useRef(new SerializableMap()); /** * handle pluralkit-style messages */ @@ -756,6 +766,7 @@ export const RoomInput = forwardRef( nickNameReplacement: nicknameReplacement, }) ); + let msgType = MsgType.Text; // quick text react @@ -816,6 +827,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'] = []; @@ -881,6 +893,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({ @@ -1128,8 +1141,18 @@ 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)) { + const imgPkRef = getImagePackReferencesForMxc(key, mx, ImageUsage.Emoticon, room); + if (imgPkRef?.room_id && imgPkRef?.shortcode) imagePacksUsedRef.current.set(key, imgPkRef); + } moveCursor(editor); + handleCloseAutocomplete(); }; const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => { @@ -1141,12 +1164,20 @@ 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] = getImagePackReferencesForMxcWrappedInMap( + mxc, + mx, + ImageUsage.Sticker, + room + ); + /** * 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, @@ -1160,6 +1191,12 @@ export const RoomInput = forwardRef( false ); } + content[MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME] = getImagePackReferencesForMxcWrappedInMap( + mxc, + mx, + ImageUsage.Sticker, + room + ); if (replyDraft) { content['m.relates_to'] = getReplyContent(replyDraft, room); @@ -1258,6 +1295,7 @@ export const RoomInput = forwardRef( editor={editor} query={autocompleteQuery} requestClose={handleCloseAutocomplete} + onEmoticonSelected={handleEmoticonSelect} /> )} {autocompleteQuery?.prefix === AutocompletePrefix.Reaction && diff --git a/src/app/features/search/Search.tsx b/src/app/features/search/Search.tsx index 1a80070d5..3ca25fbb6 100644 --- a/src/app/features/search/Search.tsx +++ b/src/app/features/search/Search.tsx @@ -39,7 +39,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'; @@ -49,6 +49,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 d6a4a286d..430c36bb7 100644 --- a/src/app/hooks/useRoomCreators.ts +++ b/src/app/hooks/useRoomCreators.ts @@ -1,9 +1,8 @@ import type { MatrixClient, MatrixEvent, Room } from '$types/matrix-sdk'; import { useMemo } from 'react'; import type { IRoomCreateContent } from '$types/matrix/room'; - -import { creatorsSupported } from '$utils/matrix'; import { getStateEvent } from '$utils/room'; +import { creatorsSupported } from '$utils/roomSupport'; import { useStateEvent } from './useStateEvent'; import { EventType } from '$types/matrix-sdk'; diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx index c428cc8a3..ab5f2364e 100644 --- a/src/app/pages/auth/login/PasswordLoginForm.tsx +++ b/src/app/pages/auth/login/PasswordLoginForm.tsx @@ -20,8 +20,8 @@ import { } from 'folds'; import FocusTrap from 'focus-trap-react'; import { Link } from 'react-router-dom'; +import { getMxIdLocalPart, isUserId } from '$utils/matrix'; import type { MatrixError } from '$types/matrix-sdk'; -import { getMxIdLocalPart, getMxIdServer, isUserId } from '$utils/matrix'; import { EMAIL_REGEX } from '$utils/regex'; import { useAutoDiscoveryInfo } from '$hooks/useAutoDiscoveryInfo'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; @@ -32,6 +32,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 type { CustomLoginResponse } from './loginUtil'; import { LoginError, factoryGetBaseUrl, login, useLoginComplete } from './loginUtil'; diff --git a/src/app/pages/auth/register/registerUtil.ts b/src/app/pages/auth/register/registerUtil.ts index 49cde3ca3..cba4e6279 100644 --- a/src/app/pages/auth/register/registerUtil.ts +++ b/src/app/pages/auth/register/registerUtil.ts @@ -9,8 +9,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 { diff --git a/src/app/pages/client/AutoDiscovery.tsx b/src/app/pages/client/AutoDiscovery.tsx index 9059e40ad..4b936e60a 100644 --- a/src/app/pages/client/AutoDiscovery.tsx +++ b/src/app/pages/client/AutoDiscovery.tsx @@ -1,10 +1,10 @@ +import { getMxIdServer } from '$utils/mxIdHelper'; import type { ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo'; import { AsyncStatus, useAsyncCallbackValue } from '../../hooks/useAsyncCallback'; import type { AutoDiscoveryInfo } from '../../cs-api'; import { autoDiscovery } 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 cf4f69297..4fb5906d4 100644 --- a/src/app/pages/client/explore/Explore.tsx +++ b/src/app/pages/client/explore/Explore.tsx @@ -24,11 +24,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 60fed2fd3..3233ea66c 100644 --- a/src/app/pages/client/explore/Server.tsx +++ b/src/app/pages/client/explore/Server.tsx @@ -33,10 +33,10 @@ import type { 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 5832a1cfb..0a7d560a7 100644 --- a/src/app/plugins/via-servers.ts +++ b/src/app/plugins/via-servers.ts @@ -1,8 +1,8 @@ +import { getMxIdServer } from '$utils/mxIdHelper'; +import { creatorsSupported } from '$utils/roomSupport'; import type { Room } from '$types/matrix-sdk'; import type { IRoomCreateContent } from '$types/matrix/room'; - import type { IPowerLevels } from '$hooks/usePowerLevels'; -import { creatorsSupported, getMxIdServer } from '$utils/matrix'; import { getStateEvent } from '$utils/room'; import { EventType } from '$types/matrix-sdk'; diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index c647038c1..5ecf00966 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -16,18 +16,14 @@ import to from 'await-to-js'; import type { IImageInfo, IThumbnailContent, IVideoInfo } from '$types/matrix/common'; 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('@'); @@ -396,23 +392,6 @@ 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, @@ -437,9 +416,16 @@ export const toggleReaction = ( } const rShortcode = shortcode || (reactions.find(eventWithShortcode)?.getContent().shortcode as string | undefined); + // send the reaction mx.sendEvent( room.roomId, EventType.Reaction as string as unknown as keyof TimelineEvents, - getReactionContent(targetEventId, key, 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 new file mode 100644 index 000000000..cf42ea607 --- /dev/null +++ b/src/app/utils/messageReaction.ts @@ -0,0 +1,27 @@ +import { MATRIX_IMAGE_SOURCE_PACK_PROPERTY_NAME } from '$types/matrix/common'; +import type { MatrixReactionEvent } from '$types/matrix/common'; +import type { MatrixClient, Room } from 'matrix-js-sdk'; +import { ImageUsage } from '$plugins/custom-emoji'; +import { getImagePackReferencesForMxcWrappedInMap } from './msc4459helper'; + +export const getReactionContent = ( + eventId: string, + key: string, + matrixClient: MatrixClient, + room: Room, + 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]: getImagePackReferencesForMxcWrappedInMap( + key, + matrixClient, + ImageUsage.Emoticon, + room + ), +}); diff --git a/src/app/utils/msc4459helper.ts b/src/app/utils/msc4459helper.ts new file mode 100644 index 000000000..d999130d5 --- /dev/null +++ b/src/app/utils/msc4459helper.ts @@ -0,0 +1,110 @@ +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, Room } 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'; +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, + imageUsage: ImageUsage, + room: Room +): SerializableMap { + const retMap = new SerializableMap(); + 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 || !result?.state_key || !result?.shortcode) return retMap; + retMap.set(mxcUrl, result); + return retMap; +} + +function getImagePackReferencesForMxcInternal( + mxcUrl: string, + matrixClient: MatrixClient, + 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) && !bypassPrivateFilter)) return; + 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); + return { + room_id: pack.address?.roomId, + state_key: pack.address?.stateKey, + via: viaServers, + shortcode: img?.shortcode, + } satisfies MSC4459ImagePackReference; + }) + .find((val) => val != undefined); +} + +export function getImagePackReferencesForMxc( + mxcUrl: string, + matrixClient: MatrixClient, + imageUsage: ImageUsage, + room: Room +): MSC4459ImagePackReference { + if (!mxcUrl.startsWith('mxc')) return {}; + if (roomLookupTable.get(room.roomId)?.has(mxcUrl)) + return roomLookupTable.get(room.roomId)!.get(mxcUrl)!; + const roomLocalImgPacks: ImagePack[] = getRoomImagePacks(room); + const roomLocalMatch = getImagePackReferencesForMxcInternal( + mxcUrl, + matrixClient, + roomLocalImgPacks, + 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?.room_id && roomLocalMatch?.shortcode) return roomLocalMatch; + // simple caching + if (globalLookupTable.has(mxcUrl)) return globalLookupTable.get(mxcUrl)!; + const globalImgPacks: ImagePack[] = getGlobalImagePacks(matrixClient); + const globalMatch = getImagePackReferencesForMxcInternal( + mxcUrl, + matrixClient, + globalImgPacks, + imageUsage, + false + ); + if (globalMatch?.room_id && globalMatch?.shortcode) { + globalLookupTable.set(mxcUrl, globalMatch); + return globalMatch; + } + + return {}; +} 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 ad93a3220..2d68b5d86 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -587,15 +587,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/app/utils/roomVisibility.ts b/src/app/utils/roomVisibility.ts new file mode 100644 index 000000000..2f88f092a --- /dev/null +++ b/src/app/utils/roomVisibility.ts @@ -0,0 +1,32 @@ +import { EventType, JoinRule } from 'matrix-js-sdk'; +import type { 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, EventType.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 e3e2de7cd..2f7c8abe2 100644 --- a/src/types/matrix/common.ts +++ b/src/types/matrix/common.ts @@ -1,3 +1,5 @@ +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'; @@ -5,6 +7,7 @@ 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,55 @@ 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 interface IGenericMSC4459 { + [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); + } +}