From b50b10f6967f4afac511398694edede81a90aedf Mon Sep 17 00:00:00 2001 From: Daisy Date: Wed, 29 Jul 2026 12:40:41 +0100 Subject: [PATCH 1/2] Impl MSC4461: Storing per-message profiles for users --- ..._storing_per_message_profiles_for_users.md | 5 + package.json | 1 + pnpm-lock.yaml | 10 + .../message-preview/MessagePreview.tsx | 12 +- src/app/components/message/Reply.tsx | 2 +- src/app/components/message/modals/Options.tsx | 4 +- src/app/features/room/RoomInput.tsx | 12 +- .../features/room/buildReplacementContent.ts | 8 +- src/app/features/room/message/Message.tsx | 8 +- .../room/persona-picker/PersonaPicker.tsx | 53 ++- .../Persona/PerMessageProfileEditor.tsx | 373 +++++++--------- .../Persona/PerMessageProfileEditorView.tsx | 36 +- .../Persona/PerMessageProfileOverview.tsx | 27 +- .../settings/Persona/ProfilesPage.tsx | 15 +- src/app/hooks/commands/pmp.ts | 13 +- src/app/hooks/usePerMessageProfile.ts | 399 +++++++++++------- .../PKitCommandMessageHandler.ts | 129 +++++- .../PKitProxyMessageHandler.test.ts | 26 +- .../PKitProxyMessageHandler.ts | 34 +- src/unstable/prefixes/msc/profile.ts | 4 +- src/unstable/prefixes/sable/accountdata.ts | 2 + 21 files changed, 651 insertions(+), 522 deletions(-) create mode 100644 .changeset/impl_msc4461_storing_per_message_profiles_for_users.md diff --git a/.changeset/impl_msc4461_storing_per_message_profiles_for_users.md b/.changeset/impl_msc4461_storing_per_message_profiles_for_users.md new file mode 100644 index 0000000000..814ed7a7f3 --- /dev/null +++ b/.changeset/impl_msc4461_storing_per_message_profiles_for_users.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +# Impl MSC4461: Storing per-message profiles for users diff --git a/package.json b/package.json index 2660b32fbb..1cc987ce9f 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "marked": "^18.0.5", "matrix-js-sdk": "42.0.0", "matrix-widget-api": "^1.17.0", + "nanoid": "^6.0.0", "pdfjs-dist": "^6.1.200", "react": "^18.3.1", "react-aria": "^3.50.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 834f3a991d..3ca5c94739 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,6 +202,9 @@ importers: matrix-widget-api: specifier: ^1.17.0 version: 1.17.0 + nanoid: + specifier: ^6.0.0 + version: 6.0.0 pdfjs-dist: specifier: ^6.1.200 version: 6.1.200 @@ -4634,6 +4637,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@6.0.0: + resolution: {integrity: sha512-mkUH+rPkwU2qPadJ0oJZOjeZ5Mxn8Q1UhevwkTRWNuUZzyia3h4rhzK39hxaHTk0o2OxB8W2SQ6A8k23ZDi1pQ==} + engines: {node: ^22 || ^24 || >=26} + hasBin: true + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -9824,6 +9832,8 @@ snapshots: nanoid@3.3.12: {} + nanoid@6.0.0: {} + no-case@3.0.4: dependencies: lower-case: 2.0.2 diff --git a/src/app/components/message-preview/MessagePreview.tsx b/src/app/components/message-preview/MessagePreview.tsx index 34017482a1..f78a64a121 100644 --- a/src/app/components/message-preview/MessagePreview.tsx +++ b/src/app/components/message-preview/MessagePreview.tsx @@ -415,11 +415,13 @@ export function MessagePreview({ const [showPronouns] = useSetting(settingsAtom, 'showPronouns'); const [parsePronouns] = useSetting(settingsAtom, 'parsePronouns'); const { cleanedDisplayName: displayName, inlinePronoun } = useMemo( - () => getParsedPronouns(perMessageProfile?.name || fallbackDisplayName, parsePronouns), - [perMessageProfile?.name, fallbackDisplayName, parsePronouns] + () => getParsedPronouns(perMessageProfile?.displayname || fallbackDisplayName, parsePronouns), + [perMessageProfile?.displayname, fallbackDisplayName, parsePronouns] ); const pronouns = useMemo(() => { - const resolved = [...(perMessageProfile?.pronouns ?? userProfile.pronouns ?? [])]; + const resolved = [ + ...(perMessageProfile?.['io.fsky.nyx.pronouns'] ?? userProfile.pronouns ?? []), + ]; if ( inlinePronoun && !resolved.some((item) => item.summary?.toLowerCase() === inlinePronoun.toLowerCase()) @@ -427,9 +429,9 @@ export function MessagePreview({ resolved.push({ summary: inlinePronoun, language: 'en' }); } return resolved; - }, [perMessageProfile?.pronouns, userProfile.pronouns, inlinePronoun]); + }, [perMessageProfile, userProfile.pronouns, inlinePronoun]); const avatarMxc = - perMessageProfile?.avatarUrl ?? + perMessageProfile?.avatar_url ?? getMemberAvatarMxc(room, sender) ?? userProfile.avatarUrl ?? profile?.avatarUrl; diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 66cf450c29..7939ef8548 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -512,7 +512,7 @@ export const Reply = as<'div', ReplyProps>( eventType !== EventType.RoomMember && ( - {pmp?.name ?? + {pmp?.displayname ?? getMemberDisplayName(room, sender, nicknames) ?? cachedProfiles[sender]?.displayName ?? getMxIdLocalPart(sender)} diff --git a/src/app/components/message/modals/Options.tsx b/src/app/components/message/modals/Options.tsx index 823e10139c..ce12ac7704 100644 --- a/src/app/components/message/modals/Options.tsx +++ b/src/app/components/message/modals/Options.tsx @@ -67,7 +67,7 @@ import { useFavoriteGifs } from '$hooks/useFavoriteGifs'; import type { IImageInfo } from '$types/matrix/common'; import { getIncomingMediaMxcUrl } from '../MsgTypeRenderers'; import { TemporaryPersonaPicker } from '$features/room/persona-picker/PersonaPicker'; -import { type PerMessageProfile } from '$hooks/usePerMessageProfile'; +import { type PerMessageProfileMsc4461 } from '$hooks/usePerMessageProfile'; import { buildReplacementPmpContent } from '$features/room/buildReplacementContent'; import { settingsAtom } from '$state/settings'; import { useSetting } from '$state/hooks/settings'; @@ -414,7 +414,7 @@ function OptionsReproxyPersonaPicker({ closeMenu, anchor, }: OptionsReproxyPersonaPickerProps) { - const reproxyMessage = async (profile: PerMessageProfile | undefined) => { + const reproxyMessage = async (profile: PerMessageProfileMsc4461 | undefined) => { const content = buildReplacementPmpContent(mEvent.getContent(), mEvent.getId()!, profile); await mx.sendMessage(roomId, content as RoomMessageEventContent); diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 34e5114699..2758e9ffd6 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -150,7 +150,7 @@ import { convertPerMessageProfileToBeeperFormat, getCurrentlyUsedPerMessageProfileForAccount, getCurrentlyUsedPerMessageProfileForRoom, - type PerMessageProfile, + type PerMessageProfileMsc4461, setCurrentlyUsedPerMessageProfileIdForRoom, } from '$hooks/usePerMessageProfile'; import { @@ -342,7 +342,7 @@ export const RoomInput = forwardRef( const [pmpLatchingEnable] = useSetting(settingsAtom, 'pmpLatching'); const [pmpPickerEnable] = useSetting(settingsAtom, 'pmpPicker'); - const [latchedPersona, setLatchedPersona] = useState(); + const [latchedPersona, setLatchedPersona] = useState(); const emojiBtnRef = useRef(null); const gifBtnRef = useRef(null); @@ -1361,12 +1361,12 @@ export const RoomInput = forwardRef( content[prefix.MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME] = convertPerMessageProfileToBeeperFormat( perMessageProfile, - perMessageProfile.name.trim() !== '' + perMessageProfile.displayname.trim() !== '' ); - if (perMessageProfile.name.trim() !== '') { + if (perMessageProfile.displayname.trim() !== '') { // if a per-message profile is used, it must per spec include a fallback - const pmpPrefix = `${perMessageProfile.name}: `; + const pmpPrefix = `${perMessageProfile.displayname}: `; if (!content.body.startsWith(pmpPrefix)) { // to prevent double-prefixing when the fallback is already present @@ -1376,7 +1376,7 @@ export const RoomInput = forwardRef( /** * html escaped version of the display name */ - const escapedName = sanitizeText(perMessageProfile.name); + const escapedName = sanitizeText(perMessageProfile.displayname); const htmlPrefix = `${escapedName}: `; diff --git a/src/app/features/room/buildReplacementContent.ts b/src/app/features/room/buildReplacementContent.ts index 2b3e24fa8f..011ba3b3de 100644 --- a/src/app/features/room/buildReplacementContent.ts +++ b/src/app/features/room/buildReplacementContent.ts @@ -2,7 +2,7 @@ import type { IContent, IMentions } from '$types/matrix-sdk'; import { MsgType, RelationType } from '$types/matrix-sdk'; import { customHtmlEqualsPlainText } from '$components/editor'; import { sanitizeText } from '$utils/sanitize'; -import type { PerMessageProfile } from '$hooks/usePerMessageProfile'; +import type { PerMessageProfileMsc4461 } from '$hooks/usePerMessageProfile'; import { convertPerMessageProfileToBeeperFormat } from '$hooks/usePerMessageProfile'; import { MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME } from '$unstable/prefixes'; @@ -100,7 +100,7 @@ export function buildReplacementContent( export function buildReplacementPmpContent( oldContent: IContent, eventId: string, - newProfile: PerMessageProfile | undefined + newProfile: PerMessageProfileMsc4461 | undefined ) { const profileBeeperFormat = newProfile && convertPerMessageProfileToBeeperFormat(newProfile, true); @@ -121,7 +121,7 @@ export function buildReplacementPmpContent( } if (newProfile) { - const escapedName = sanitizeText(newProfile.name); + const escapedName = sanitizeText(newProfile.displayname); const htmlPrefix = `${escapedName}: `; if (oldContent.formatted_body) { @@ -133,7 +133,7 @@ export function buildReplacementPmpContent( oldContent.formatted_body = `${htmlPrefix}${escapedBody}`; } - const pmpPrefix = `${newProfile.name}: `; + const pmpPrefix = `${newProfile.displayname}: `; oldContent.body = pmpPrefix + oldContent.body; } diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index c3e13c9608..d4698fd588 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -458,8 +458,8 @@ function MessageInternal( const pmpNameColor = useMemo(() => { if (!renderPersonaColors) return undefined; - const pmpNameColorLight = parsedPMPContent?.colors?.on_light; - const pmpNameColorDark = parsedPMPContent?.colors?.on_dark; + const pmpNameColorLight = parsedPMPContent?.['eu.she-a.color']?.on_light; + const pmpNameColorDark = parsedPMPContent?.['eu.she-a.color']?.on_dark; return activeTheme.kind === ThemeKind.Dark ? pmpNameColorDark : pmpNameColorLight; }, [parsedPMPContent, activeTheme, renderPersonaColors]); @@ -468,7 +468,7 @@ function MessageInternal( * boolean to indicate wheather we should indicate to the user that it is a pmp * We want to not show it, when the name is unset, or whitespace only */ - const showPmPInfo = parsedPMPContent?.name && parsedPMPContent.name?.trim() !== ''; + const showPmPInfo = parsedPMPContent?.displayname && parsedPMPContent.displayname?.trim() !== ''; // Profiles and Colors const profile = useUserProfile(senderId, room, undefined, true, isVisible); const { color: usernameColor, font: usernameFont } = useSableCosmetics( @@ -488,7 +488,7 @@ function MessageInternal( * otherwise we fall back to the profile pronouns. * This allows users to set pronouns on a per-message basis, while still falling back to their profile pronouns if they don't set any for a specific message. */ - const pronouns = parsedPMPContent?.pronouns ?? profile.pronouns; + const pronouns = parsedPMPContent?.['io.fsky.nyx.pronouns'] ?? profile.pronouns; const [highlightMentions] = useSetting(settingsAtom, 'highlightMentions'); diff --git a/src/app/features/room/persona-picker/PersonaPicker.tsx b/src/app/features/room/persona-picker/PersonaPicker.tsx index a5173d2946..6a747f2cb5 100644 --- a/src/app/features/room/persona-picker/PersonaPicker.tsx +++ b/src/app/features/room/persona-picker/PersonaPicker.tsx @@ -10,7 +10,7 @@ import { useMediaAuthentication } from '$hooks/useMediaAuthentication.ts'; import { getCurrentlyUsedPerMessageProfileForRoom, getAllPerMessageProfiles, - type PerMessageProfile, + type PerMessageProfileMsc4461, setCurrentlyUsedPerMessageProfileIdForRoom, getCurrentlyUsedPerMessageProfileForAccount, setCurrentlyUsedPerMessageProfileIdForAccount, @@ -58,9 +58,9 @@ type PersonaPickerProps = { roomId?: string; suppressEditorRefocus?: () => void; onTabChange?: (tab: PersonaPickerTab) => void; - latchedPersona?: PerMessageProfile; + latchedPersona?: PerMessageProfileMsc4461; hideTabs?: boolean; - onPersonaSelect?: (persona: PerMessageProfile | undefined) => void; + onPersonaSelect?: (persona: PerMessageProfileMsc4461 | undefined) => void; requestClose?: () => void; showNoneOption?: boolean; hideButton?: boolean; @@ -126,22 +126,26 @@ function PersonaPicker({ onTabChange, }: PersonaPickerProps & { presentation: PersonaPickerPresentation }) { const useAuthentication = useMediaAuthentication(); - const persistent = presentation === PersonaPickerPresentation.PersistentPicker; - const [tab, setTab] = useState(tabProp); const [AddPersonaMenuAnchor, setAddPersonaMenuAnchor] = useState(anchor); - const [profiles, setProfiles] = useState(undefined); - const [selectedGlobalPersona, setSelectedGlobalPersona] = useState( - null - ); - const [selectedRoomPersona, setSelectedRoomPersona] = useState( + const activeTheme = useActiveTheme(); + const [profiles, setProfiles] = useState(undefined); + const [selectedGlobalPersona, setSelectedGlobalPersona] = + useState(null); + const [selectedRoomPersona, setSelectedRoomPersona] = useState( latchedPersona ?? null ); - const defactoPersona = () => selectedRoomPersona ?? selectedGlobalPersona; + const nameColor = useCallback( + (persona: PerMessageProfileMsc4461) => + activeTheme.kind === ThemeKind.Dark + ? persona['eu.she-a.color']?.on_dark + : persona['eu.she-a.color']?.on_light, + [activeTheme] + ); - const activeTheme = useActiveTheme(); + const defactoPersona = () => selectedRoomPersona ?? selectedGlobalPersona; useEffect(() => { const syncProfile = async () => { @@ -172,7 +176,7 @@ function PersonaPicker({ const filtered = term ? profiles?.filter((profile) => searchInputRef.current - ? profile.name.toLocaleLowerCase().includes(searchInputRef.current?.value) || + ? profile.displayname.toLocaleLowerCase().includes(searchInputRef.current?.value) || profile.id.toLocaleLowerCase().includes(searchInputRef.current?.value) : true ) @@ -182,13 +186,8 @@ function PersonaPicker({ }, [profiles] ); - const nameColor = useCallback( - (persona: PerMessageProfile) => - activeTheme.kind === ThemeKind.Dark ? persona.colors?.on_dark : persona.colors?.on_light, - [activeTheme] - ); - const isSelected = (persona: PerMessageProfile | undefined) => { + const isSelected = (persona: PerMessageProfileMsc4461 | undefined) => { if (!persona) return undefined; const selected = tab === PersonaPickerTab.Global ? selectedGlobalPersona : selectedRoomPersona; return persona.id === selected?.id ? true : undefined; @@ -201,9 +200,9 @@ function PersonaPicker({ }, [mx]); const avatarUrl = useCallback( - (profile: PerMessageProfile) => { - if (profile.avatarUrl !== undefined) { - return mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined; + (profile: PerMessageProfileMsc4461) => { + if (profile.avatar_url !== undefined) { + return mxcUrlToHttp(mx, profile.avatar_url, useAuthentication, 96, 96, 'crop') ?? undefined; } else { return undefined; } @@ -212,7 +211,7 @@ function PersonaPicker({ ); const handleSelect = useCallback( - async (profile: PerMessageProfile | undefined) => { + async (profile: PerMessageProfileMsc4461 | undefined) => { if (onPersonaSelect) { onPersonaSelect(profile); return; @@ -320,10 +319,10 @@ function PersonaPicker({ ( - {nameInitials(profile.name)} + {nameInitials(profile.displayname)} )} alt={`Avatar for profile ${profile.id}`} @@ -335,7 +334,7 @@ function PersonaPicker({ truncate style={{ color: nameColor(profile) ?? undefined, maxWidth: toRem(150) }} > - {profile.name} + {profile.displayname} ))} @@ -422,7 +421,7 @@ function PersonaPicker({ src={avatarUrl(defactoPersona()!)} renderFallback={() => ( - {nameInitials(defactoPersona()!.name)} + {nameInitials(defactoPersona()!.displayname)} )} alt={`Avatar for profile ${defactoPersona()!.id}`} diff --git a/src/app/features/settings/Persona/PerMessageProfileEditor.tsx b/src/app/features/settings/Persona/PerMessageProfileEditor.tsx index 5463cdcb3f..9677ec821f 100644 --- a/src/app/features/settings/Persona/PerMessageProfileEditor.tsx +++ b/src/app/features/settings/Persona/PerMessageProfileEditor.tsx @@ -1,4 +1,5 @@ import { SequenceCard, SequenceCardStyle } from '$components/sequence-card'; +import { nanoid } from 'nanoid'; import { Box, Button, Text, Avatar, config, IconButton, Input, toRem, Spinner, color } from 'folds'; import { menuIcon, Trash, X } from '$components/icons/phosphor'; import type { MatrixClient } from '$types/matrix-sdk'; @@ -11,67 +12,60 @@ import { useObjectURL } from '$hooks/useObjectURL'; import { createUploadAtom } from '$state/upload'; import { UserAvatar } from '$components/user-avatar'; import { CompactUploadCardRenderer } from '$components/upload-card'; +import type { ProfileTrigger } from '$hooks/usePerMessageProfile'; import { addOrUpdatePerMessageProfile, - associateProxyWithProfile, - createProxyKey, deletePerMessageProfile, - dropProxyAssociationForPMP, - getAllPerMessageProfileProxies, - getAllProxiesForPMP, - getPerMessageProfileById, - type PerMessageProfileProxyAssociationV2, renamePerMessageProfile, } from '$hooks/usePerMessageProfile'; import type { PronounSet } from '$utils/pronouns'; import { parsePronounsStringToPronounsSetArray } from '$utils/pronouns'; -import { generateShortId } from '$utils/shortIdGen'; import { SettingTile } from '$components/setting-tile'; -import { type AsyncState, AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; +import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { NameColorEditor } from '../account/NameColorEditor'; +import { + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME, + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME, + MATRIX_UNSTABLE_COLORS, + MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME, +} from '$unstable/prefixes'; -const constructProxyString = (s: Shorthand) => { - return `${s.prefix ?? ''}text${s.suffix ?? ''}`; -}; - -type Shorthand = { id: string; prefix?: string; suffix?: string }; -type ShorthandRow = Shorthand & { rowId: string }; +type Shorthand = { prefix?: string; suffix?: string }; +type ShorthandRow = Shorthand & { id: string }; -type ShorthandListItemProps = Shorthand & { +type ShorthandListItemProps = ShorthandRow & { onDelete: (shorthandId: string) => void; - onSave: (shorthandId: string, shorthand: Shorthand) => void; - deleteState: AsyncState; - saveState: AsyncState; + onChange: (shorthandId: string, shorthand: Shorthand) => void; }; -function ShorthandListItem({ - id, - prefix, - suffix, - onDelete, - onSave, - deleteState, - saveState, -}: ShorthandListItemProps) { - const [currentPrefix] = useState(prefix); - const [currentSuffix] = useState(suffix); +function ShorthandListItem({ id, prefix, suffix, onDelete, onChange }: ShorthandListItemProps) { const [newPrefix, setNewPrefix] = useState(prefix); const [newSuffix, setNewSuffix] = useState(suffix); const [prefixWarn, setPrefixWarn] = useState(false); const [suffixWarn, setSuffixWarn] = useState(false); - const handlePrefixChange = useCallback((e: React.ChangeEvent) => { - setNewPrefix(e.target.value); - }, []); - const handleSuffixChange = useCallback((e: React.ChangeEvent) => { - setNewSuffix(e.target.value); - }, []); - - const hasChanges = useMemo( - () => - (newPrefix ?? '') !== (currentPrefix ?? '') || (newSuffix ?? '') !== (currentSuffix ?? ''), - [newPrefix, newSuffix, currentPrefix, currentSuffix] + const handlePrefixChange = useCallback( + (e: React.ChangeEvent) => { + onChange(id, { + prefix: e.target.value.trimStart(), + suffix: newSuffix?.trimEnd(), + }); + setNewPrefix(e.target.value); + }, + [newSuffix, id, onChange] + ); + const handleSuffixChange = useCallback( + (e: React.ChangeEvent) => { + // if you call setNewSuffix then use newSuffix you get race conditioned lol + onChange(id, { + prefix: newPrefix?.trimStart(), + suffix: e.target.value.trimEnd(), + }); + setNewSuffix(e.target.value); + }, + [newPrefix, id, onChange] ); + const isBlank = useMemo(() => !newPrefix && !newSuffix, [newPrefix, newSuffix]); useEffect(() => { @@ -79,13 +73,6 @@ function ShorthandListItem({ setSuffixWarn((newSuffix ?? '').startsWith(' ')); }, [newPrefix, newSuffix]); - const handleSave = () => - onSave(id, { - id: createProxyKey(newPrefix, newSuffix), - prefix: newPrefix?.trimStart(), - suffix: newSuffix?.trimEnd(), - }); - return ( - {deleteState.status === AsyncStatus.Loading ? ( - - ) : ( - menuIcon(Trash) - )} - - @@ -167,154 +134,51 @@ function ShorthandListItem({ ); } -type ShorthandEditorProps = { - mx: MatrixClient; - profileId: string; -}; -function ShorthandEditor({ mx, profileId }: ShorthandEditorProps) { - const [shorthands, setShorthands] = useState(); - - const containsBlankShorthand = useMemo( - () => shorthands && shorthands.some((shorthand) => !shorthand.prefix && !shorthand.suffix), - [shorthands] - ); - - const handleAddShorthand = () => { - if (shorthands !== undefined) { - setShorthands([...shorthands, { id: 'blank', rowId: generateShortId(16) }]); +function triggersToShorthandRows(trigger: ProfileTrigger): ShorthandRow[] { + const prefixes: ShorthandRow[] = trigger.prefix.map((str) => { + return { prefix: str, id: nanoid() }; + }); + const suffixes: ShorthandRow[] | undefined = trigger['net.f0rest.suffix']?.map((str) => { + return { suffix: str, id: nanoid() }; + }); + const circumfixes: ShorthandRow[] | undefined = trigger['net.f0rest.circumfix']?.map( + ({ prefix, suffix }) => { + return { prefix: prefix, suffix: suffix, id: nanoid() }; } - }; - - const [deleteShorthandState, handleDeleteShorthand] = useAsyncCallback( - useCallback( - async (id: string) => { - if (shorthands === undefined) return; - - const shorthandToDelete = shorthands.find((shorthand) => shorthand.id === id); - if (!shorthandToDelete) return; - - const proxy = constructProxyString(shorthandToDelete); - - await dropProxyAssociationForPMP(mx, proxy); - - setShorthands((s) => s?.filter((shorthand) => shorthand.id !== id)); - }, - [mx, shorthands] - ) ); - const [saveShorthandState, handleSaveShorthand] = useAsyncCallback< - void, - Error, - [string, Shorthand] - >( - useCallback( - async (oldId: string, shorthand: Shorthand) => { - if (shorthands === undefined) return; - - const shorthandAssociation = (await getAllPerMessageProfileProxies(mx)).find( - (association) => createProxyKey(association.prefix, association.suffix) === shorthand.id - ); - const shorthandAssociatedProfile = shorthandAssociation - ? await getPerMessageProfileById(mx, shorthandAssociation.profileId) - : undefined; - if (shorthandAssociatedProfile) { - throw new Error( - `Shorthand is already associated with profile ${shorthandAssociatedProfile.name} (${shorthandAssociatedProfile.id})` - ); - } - - if (oldId !== 'blank') { - await dropProxyAssociationForPMP(mx, oldId); - } - await associateProxyWithProfile(mx, profileId, shorthand.prefix, shorthand.suffix, false); - - setShorthands((currentShorthands) => { - if (currentShorthands === undefined) return currentShorthands; - - const oldShorthandIdx = currentShorthands.findIndex((s) => s.id === oldId); - if (oldShorthandIdx < 0) return currentShorthands; - const oldShorthand = currentShorthands[oldShorthandIdx]; - if (oldShorthand === undefined) return currentShorthands; - - return currentShorthands.with(oldShorthandIdx, { - ...shorthand, - rowId: oldShorthand.rowId, - }); - }); - }, - [mx, shorthands, profileId] - ) - ); + return prefixes.concat(suffixes ?? [], circumfixes ?? []); +} - useEffect(() => { - const fetchShorthands = async () => { - const fetchedShorthands: PerMessageProfileProxyAssociationV2[] = await getAllProxiesForPMP( - mx, - profileId - ); - const enumeratedShorthands: ShorthandRow[] = fetchedShorthands.map((v) => { - return { id: createProxyKey(v.prefix, v.suffix), rowId: generateShortId(16), ...v }; - }); - setShorthands(enumeratedShorthands); - }; - fetchShorthands(); - }, [mx, profileId]); +function shorthandRowsToTriggers(rows: ShorthandRow[]): ProfileTrigger { + const prefix: string[] = []; + const suffix: string[] = []; + const circumfix: { prefix: string; suffix: string }[] = []; + + rows.forEach((row) => { + if (row.prefix && row.suffix) { + circumfix.push({ prefix: row.prefix, suffix: row.suffix }); + } else if (row.prefix) { + prefix.push(row.prefix); + } else if (row.suffix) { + suffix.push(row.suffix); + } + }); - return ( - <> - - - {saveShorthandState.status === AsyncStatus.Error && ( - - {saveShorthandState.error.toString()} - - )} - {shorthands === undefined ? ( - - ) : ( - shorthands.map((shorthand: ShorthandRow) => ( - - )) - )} - - - - - ); + return { + prefix, + ...(suffix.length > 0 + ? { [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]: suffix } + : {}), + ...(circumfix.length > 0 + ? { [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]: circumfix } + : {}), + }; } - /** * the props we use for the per-message profile editor, which is used to edit a per-message profile. This is used in the settings page when the user wants to edit a profile. */ -type PerMessageProfileEditorProps = { +export type PerMessageProfileEditorProps = { mx: MatrixClient; profileId: string; avatarMxcUrl?: string; @@ -322,7 +186,7 @@ type PerMessageProfileEditorProps = { pronouns?: PronounSet[]; nameColorLightTheme?: string; nameColorDarkTheme?: string; - shorthands?: Shorthand[]; + shorthands?: ProfileTrigger; onDelete?: (profileId: string) => void; }; @@ -334,6 +198,7 @@ export function PerMessageProfileEditor({ pronouns = Array(), nameColorLightTheme, nameColorDarkTheme, + shorthands, onDelete, }: Readonly) { const useAuthentication = useMediaAuthentication(); @@ -364,6 +229,44 @@ export function PerMessageProfileEditor({ const [currentNameColorDark, setCurrentNameColorDark] = useState(nameColorDarkTheme ?? null); const [newNameColorDark, setNewNameColorDark] = useState(nameColorDarkTheme ?? null); + // shorthands + const shorthandProp = shorthands ? triggersToShorthandRows(shorthands) : undefined; + const [currentShorthands, setCurrentShorthands] = useState( + shorthandProp + ); + const [newShorthands, setNewShorthands] = useState(shorthandProp); + + const containsBlankShorthand = useMemo( + () => + newShorthands && newShorthands.some((shorthand) => !shorthand.prefix && !shorthand.suffix), + [newShorthands] + ); + + const handleAddShorthand = () => { + if (newShorthands !== undefined) { + setNewShorthands([...newShorthands, { id: nanoid() }]); + } + }; + + const handleDeleteShorthand = (id: string) => { + if (newShorthands === undefined) return; + setNewShorthands((s) => s?.filter((shorthand) => shorthand.id !== id)); + }; + + const handleSaveShorthand = (oldId: string, shorthand: Shorthand) => { + setNewShorthands((rows = []) => { + const index = rows.findIndex((row) => row.id === oldId); + if (index === -1) return rows; + const oldShorthand = rows[index]; + if (oldShorthand === undefined) return rows; + + return rows.with(index, { + ...shorthand, + id: oldShorthand.id, + }); + }); + }; + const [newDisplayName, setNewDisplayName] = useState(currentDisplayName); const [imageFile, setImageFile] = useState(); const [imageHasChanges, setImageHasChanges] = useState(false); @@ -409,6 +312,7 @@ export function PerMessageProfileEditor({ newPronounsString !== currentPronounsString || newNameColorLight !== currentNameColorLight || newNameColorDark !== currentNameColorDark || + newShorthands !== currentShorthands || hasIdChange || imageHasChanges, [ @@ -420,6 +324,8 @@ export function PerMessageProfileEditor({ currentNameColorLight, newNameColorDark, currentNameColorDark, + newShorthands, + currentShorthands, hasIdChange, imageHasChanges, ] @@ -447,10 +353,11 @@ export function PerMessageProfileEditor({ useCallback(async () => { await addOrUpdatePerMessageProfile(mx, { id: profileId, - name: newDisplayName, - avatarUrl: avatarMxc, - pronouns: newPronouns, - colors: { + displayname: newDisplayName, + avatar_url: avatarMxc, + [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]: newPronouns, + trigger: shorthandRowsToTriggers(newShorthands ?? []), + [MATRIX_UNSTABLE_COLORS]: { on_light: newNameColorLight ?? undefined, on_dark: newNameColorDark ?? undefined, }, @@ -460,6 +367,7 @@ export function PerMessageProfileEditor({ setCurrentPronouns(newPronouns); setCurrentNameColorLight(newNameColorLight); setCurrentNameColorDark(newNameColorDark); + setCurrentShorthands(newShorthands); setImageHasChanges(false); setChangingDisplayName(false); setDisableSetDisplayname(false); @@ -476,6 +384,7 @@ export function PerMessageProfileEditor({ newPronouns, newNameColorLight, newNameColorDark, + newShorthands, hasIdChange, newId, ]) @@ -753,7 +662,43 @@ export function PerMessageProfileEditor({ Shorthands - + + + {newShorthands === undefined ? ( + + ) : ( + newShorthands.map((shorthand: ShorthandRow) => ( + + )) + )} + + + ); } diff --git a/src/app/features/settings/Persona/PerMessageProfileEditorView.tsx b/src/app/features/settings/Persona/PerMessageProfileEditorView.tsx index 9ef6c505b5..623b5dabe8 100644 --- a/src/app/features/settings/Persona/PerMessageProfileEditorView.tsx +++ b/src/app/features/settings/Persona/PerMessageProfileEditorView.tsx @@ -1,32 +1,13 @@ import { Box, IconButton, Text, Scroll, Chip } from 'folds'; import { ArrowLeft, composerIcon, sizedIcon, X } from '$components/icons/phosphor'; import { Page, PageHeader, PageContent } from '$components/page'; -import type { MatrixClient } from 'matrix-js-sdk'; -import type { PronounSet } from '$utils/pronouns'; +import type { PerMessageProfileEditorProps } from './PerMessageProfileEditor'; import { PerMessageProfileEditor } from './PerMessageProfileEditor'; -type PerMessageProfileEditorViewProps = { - mx: MatrixClient; - profileId: string; - avatarMxcUrl?: string; - displayName?: string; - pronouns?: PronounSet[]; - nameColorLightTheme?: string; - nameColorDarkTheme?: string; - onChange?: (profile: { id: string; name: string; avatarUrl?: string }) => void; - onDelete?: (profileId: string) => void; - requestClose: () => void; -}; export function PerMessageProfileEditorView({ - mx, - profileId, - avatarMxcUrl, - displayName, - pronouns = Array(), - nameColorLightTheme, - nameColorDarkTheme, requestClose, -}: Readonly) { + ...editorProps +}: Readonly void }>) { return ( @@ -51,16 +32,7 @@ export function PerMessageProfileEditorView({ - + diff --git a/src/app/features/settings/Persona/PerMessageProfileOverview.tsx b/src/app/features/settings/Persona/PerMessageProfileOverview.tsx index 2ab2b53d3d..5f329b7a5e 100644 --- a/src/app/features/settings/Persona/PerMessageProfileOverview.tsx +++ b/src/app/features/settings/Persona/PerMessageProfileOverview.tsx @@ -1,5 +1,5 @@ import { useMatrixClient } from '$hooks/useMatrixClient'; -import type { PerMessageProfile } from '$hooks/usePerMessageProfile'; +import type { PerMessageProfileMsc4461 } from '$hooks/usePerMessageProfile'; import { addOrUpdatePerMessageProfile, getAllPerMessageProfiles, @@ -12,10 +12,14 @@ import { SequenceCard, SequenceCardStyle } from '$components/sequence-card'; import { PerMessageProfileListItem } from './PerMessageProfileListItem'; import { SettingTile } from '$components/setting-tile'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; +import { + MATRIX_UNSTABLE_COLORS, + MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME, +} from '$unstable/prefixes'; type PerMessageProfileOverviewProps = { - onCreateProfile: (profile: PerMessageProfile) => void; - onEditProfile: (profile: PerMessageProfile) => void; + onCreateProfile: (profile: PerMessageProfileMsc4461) => void; + onEditProfile: (profile: PerMessageProfileMsc4461) => void; }; /** * Renders a list of per-message profiles along with an editor. @@ -26,7 +30,7 @@ export function PerMessageProfileOverview({ onEditProfile, }: PerMessageProfileOverviewProps) { const mx = useMatrixClient(); - const [profiles, setProfiles] = useState([]); + const [profiles, setProfiles] = useState([]); useEffect(() => { const fetchProfiles = async () => { @@ -43,9 +47,10 @@ export function PerMessageProfileOverview({ const [addState, handleAdd] = useAsyncCallback( useCallback(async () => { - const newProfile: PerMessageProfile = { + const newProfile: PerMessageProfileMsc4461 = { id: generateShortId(5), - name: 'New Profile', + displayname: 'New Profile', + trigger: { prefix: [] }, }; await addOrUpdatePerMessageProfile(mx, newProfile); onCreateProfile(newProfile); @@ -92,12 +97,12 @@ export function PerMessageProfileOverview({ > diff --git a/src/app/features/settings/Persona/ProfilesPage.tsx b/src/app/features/settings/Persona/ProfilesPage.tsx index 0b47aebf38..60512ac983 100644 --- a/src/app/features/settings/Persona/ProfilesPage.tsx +++ b/src/app/features/settings/Persona/ProfilesPage.tsx @@ -3,7 +3,7 @@ import { Box, Scroll } from 'folds'; import { PerMessageProfileOverview } from './PerMessageProfileOverview'; import { PKCompatSettings } from './PKCompat'; import { PickerPageSettings } from './PickerPage'; -import type { PerMessageProfile } from '$hooks/usePerMessageProfile'; +import type { PerMessageProfileMsc4461 } from '$hooks/usePerMessageProfile'; import { useState } from 'react'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { PerMessageProfileEditorView } from './PerMessageProfileEditorView'; @@ -15,7 +15,7 @@ type PerMessageProfilePageProps = { export function PerMessageProfilePage({ requestBack, requestClose }: PerMessageProfilePageProps) { const mx = useMatrixClient(); - const [editingProfile, setEditingProfile] = useState(); + const [editingProfile, setEditingProfile] = useState(); const handleEditorClose = () => { setEditingProfile(undefined); @@ -26,11 +26,12 @@ export function PerMessageProfilePage({ requestBack, requestClose }: PerMessageP ); diff --git a/src/app/hooks/commands/pmp.ts b/src/app/hooks/commands/pmp.ts index 21a1f6d542..7af7046dfc 100644 --- a/src/app/hooks/commands/pmp.ts +++ b/src/app/hooks/commands/pmp.ts @@ -1,6 +1,6 @@ import { splitWithSpace } from '$utils/common'; import { sendFeedback } from '$utils/sendFeedbackToUser'; -import type { PerMessageProfile } from '../usePerMessageProfile'; +import type { PerMessageProfileMsc4461 } from '../usePerMessageProfile'; import { addOrUpdatePerMessageProfile, deletePerMessageProfile, @@ -26,19 +26,20 @@ export const createPmpCommands = (ctx: CommandContext): Partial = sendFeedback(`invalid payload`, room, mx.getSafeUserId()); return; } - const avatarUrl: string | undefined = args[5]; + const avatar_url: string | undefined = args[5]; const name: string | undefined = args[3]; const profileId = args[1]; - if (!avatarUrl || !name || !profileId) { + if (!avatar_url || !name || !profileId) { sendFeedback(`invalid payload`, room, mx.getSafeUserId()); return; } - const pmp: PerMessageProfile = { + const pmp: PerMessageProfileMsc4461 = { id: profileId, - name: name || '', - avatarUrl, + displayname: name || '', + avatar_url, + trigger: { prefix: [] }, }; await addOrUpdatePerMessageProfile(mx, pmp) .then(() => { diff --git a/src/app/hooks/usePerMessageProfile.ts b/src/app/hooks/usePerMessageProfile.ts index 3b18de2375..07a4ce53b1 100644 --- a/src/app/hooks/usePerMessageProfile.ts +++ b/src/app/hooks/usePerMessageProfile.ts @@ -3,16 +3,22 @@ import type { AccountDataCompatVersion } from '$types/matrix/accountData'; import type { PronounSet } from '$utils/pronouns'; import type { MatrixClient } from '$types/matrix-sdk'; import { CustomAccountDataEvent } from '$types/matrix/accountData'; -import { MATRIX_UNSTABLE_COLORS } from '$unstable/prefixes'; -import { MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME } from '$unstable/prefixes'; import type { ColorSet } from './useUserProfile'; +import { MATRIX_UNSTABLE_COLORS } from '$unstable/prefixes'; +import { + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME, + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME, + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, + MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME, +} from '$unstable/prefixes'; const ACCOUNT_DATA_PREFIX = CustomAccountDataEvent.SablePerProfileMessageProfiles; /** + * @deprecated in favour if {@link PerMessageProfileMsc4461} * a per message profile */ -export type PerMessageProfile = { +type PerMessageProfile = { /** * a unique id for this profile, can be generated using something like nanoid. * This is used to identify the profile when applying it to a message, and also used as the key when storing the profile in account data. @@ -36,6 +42,116 @@ export type PerMessageProfile = { colors?: ColorSet; }; +export type ProfileTrigger = { + prefix: string[]; + [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]?: string[]; + [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]?: { + prefix: string; + suffix: string; + }[]; +}; + +/** + * a per message profile + */ +export type PerMessageProfileIndexMsc4461 = { + type: 'm.per_message_profiles'; + content: { + profiles: PerMessageProfileMsc4461[]; + }; +}; + +/** + * a per message profile + */ +export type PerMessageProfileMsc4461 = { + /** + * a unique id for this profile, can be generated using something like nanoid. + * This is used to identify the profile when applying it to a message, and also used as the key when storing the profile in account data. + */ + id: string; + /** + * the display name to use for messages using this profile. + * This is required because otherwise the profile would have no effect on the message. + */ + displayname: string; + /** + * the avatar url to use for messages using this profile. + */ + avatar_url?: string; + /** + * a per message profile can also include pronouns + * @see PronounSet for the format of the pronouns, and how to parse them from a string input + */ + [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]?: PronounSet[]; + + /** + * following spec MSC4522 + */ + [MATRIX_UNSTABLE_COLORS]?: ColorSet; + + trigger: ProfileTrigger; + + compat?: AccountDataCompatVersion; +}; + +export function convertPmpToMsc4461( + mx: MatrixClient, + profile: PerMessageProfile +): PerMessageProfileMsc4461 { + const triggers: ProfileTrigger = { + prefix: [], + [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]: [], + [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]: [], + }; + + // lookup old proxyAssociations + getProxyAssociationMap( + mx + .getAccountData( + `${ACCOUNT_DATA_PREFIX}.proxyassociation` as Parameters[0] + ) + ?.getContent() + /* oxlint-disable no-unused-vars */ + ) + .entries() + .filter(([_k, assoc]) => assoc.profileId === profile.id) + .forEach(([k, assoc]) => { + const migratedAssoc = migratePmpProxyAssociation(k, assoc); + if (!migratedAssoc) return; + + if (migratedAssoc.prefix && !migratedAssoc.suffix) { + triggers.prefix.push(migratedAssoc.prefix); + } else if (!migratedAssoc.prefix && migratedAssoc.suffix) { + triggers[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]!.push( + migratedAssoc.suffix + ); + } else if (migratedAssoc.prefix && migratedAssoc.suffix) { + triggers[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]!.push({ + prefix: migratedAssoc.prefix, + suffix: migratedAssoc.suffix, + }); + } + }); + + const newPmp: PerMessageProfileMsc4461 = { + id: profile.id, + displayname: profile.name, + avatar_url: profile.avatarUrl, + [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]: profile.pronouns, + [MATRIX_UNSTABLE_COLORS]: profile.colors, + trigger: triggers, + }; + + // delete empty fields + // to-do maybe find a better way of doing it + if (!profile.avatarUrl) delete newPmp.avatar_url; + if (!profile.pronouns || profile.pronouns?.length === 0) + delete newPmp[MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]; + if (!profile.colors) delete newPmp[MATRIX_UNSTABLE_COLORS]; + return newPmp; +} + /** * the format used by Beeper for per message profiles * This is the format that Beeper expects when applying a profile to a message before sending it @@ -71,23 +187,28 @@ export type PerMessageProfileBeeperFormat = { * @return {*} {PerMessageProfileBeeperFormat} the per message profile in Beeper's format, which can be applied to a message before sending it */ export function convertPerMessageProfileToBeeperFormat( - profile: PerMessageProfile, + profile: PerMessageProfileMsc4461, has_fallback: boolean ): PerMessageProfileBeeperFormat { const beeperPMP: PerMessageProfileBeeperFormat = { id: profile.id, - displayname: profile.name, - avatar_url: profile.avatarUrl, - [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]: profile.pronouns, - [MATRIX_UNSTABLE_COLORS]: profile.colors, + displayname: profile.displayname, + avatar_url: profile.avatar_url, + [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]: + profile[MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME], + [MATRIX_UNSTABLE_COLORS]: profile[MATRIX_UNSTABLE_COLORS], has_fallback, }; // delete empty fields // to-do maybe find a better way of doing it - if (!profile.name || profile?.name.trim().length === 0) delete beeperPMP.displayname; - if (!profile.avatarUrl) delete beeperPMP.avatar_url; - if (!profile.pronouns || profile.pronouns?.length === 0) - delete beeperPMP[MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]; + if (!profile.displayname || profile?.displayname.trim().length === 0) + delete beeperPMP.displayname; + if (!profile.avatar_url) delete beeperPMP.avatar_url; + if ( + !profile[MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME] || + profile[MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]?.length === 0 + ) + if (!profile[MATRIX_UNSTABLE_COLORS]) delete beeperPMP[MATRIX_UNSTABLE_COLORS]; if (!has_fallback) delete beeperPMP.has_fallback; return beeperPMP; } @@ -102,13 +223,15 @@ export function convertPerMessageProfileToBeeperFormat( */ export function convertBeeperFormatToOurPerMessageProfile( beeperProfile: PerMessageProfileBeeperFormat -): PerMessageProfile { +): PerMessageProfileMsc4461 { return { id: beeperProfile.id, - name: beeperProfile.displayname ?? '', - avatarUrl: beeperProfile.avatar_url, - pronouns: beeperProfile[MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME], - colors: beeperProfile[MATRIX_UNSTABLE_COLORS], + displayname: beeperProfile.displayname ?? '', + avatar_url: beeperProfile.avatar_url, + [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]: + beeperProfile[MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME], + [MATRIX_UNSTABLE_COLORS]: beeperProfile[MATRIX_UNSTABLE_COLORS], + trigger: { prefix: [] }, }; } @@ -335,6 +458,31 @@ function proxyAssociationsMapToObject( } /** + * helper function: getting a profile from the account data where the profile matches a given id + * + * @export + * @param {MatrixClient} mx the matrix client + * @param {string} id the profile id + * @return {*} {(Promise)} the profile, with the profile Id, if it exists + */ +export async function getPerMessageProfileById( + mx: MatrixClient, + id: string +): Promise { + let index = mx.getAccountData( + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME as Parameters< + typeof mx.getAccountData + >[0] + ); + + return index + ? (index.getContent() as PerMessageProfileIndexMsc4461).content.profiles.find((p) => p.id == id) + : undefined; +} + +/** + * @deprecated + * * getting a profile from the account data where the profile matches a given id * * @export @@ -342,7 +490,7 @@ function proxyAssociationsMapToObject( * @param {string} id the profile id * @return {*} {(Promise)} the profile, with the profile Id, if it exists */ -export async function getPerMessageProfileById( +async function getPerMessageProfileByIdDeprecated( mx: MatrixClient, id: string ): Promise { @@ -359,13 +507,56 @@ export async function getPerMessageProfileById( * @param {MatrixClient} mx the matrix client * @return {*} {Promise} a array containing all per-message-profiles saved */ -export async function getAllPerMessageProfiles(mx: MatrixClient): Promise { - const profileData = mx.getAccountData( - `${ACCOUNT_DATA_PREFIX}.index` as Parameters[0] +export async function getAllPerMessageProfiles( + mx: MatrixClient +): Promise { + let profileListIndex = mx.getAccountData( + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME as Parameters< + typeof mx.getAccountData + >[0] ); - const profileIds = (profileData?.getContent() as PerMessageProfileIndex)?.profileIds || []; - const profiles = await Promise.all(profileIds.map((id) => getPerMessageProfileById(mx, id))); - return profiles.filter((profile): profile is PerMessageProfile => profile !== undefined); + + if (!profileListIndex) { + const profileData = mx.getAccountData( + `${ACCOUNT_DATA_PREFIX}.index` as Parameters[0] + ); + if (!profileData) return []; + + const msc4461Index: PerMessageProfileIndexMsc4461 = { + type: 'm.per_message_profiles', + content: { + profiles: [], + }, + }; + + const profileIds = (profileData?.getContent() as PerMessageProfileIndex)?.profileIds || []; + const profiles = await Promise.all( + profileIds.map((id) => getPerMessageProfileByIdDeprecated(mx, id)) + ); + + profiles.forEach((profile) => { + if (!profile) return; + + const migratedProfile = convertPmpToMsc4461(mx, profile); + msc4461Index.content.profiles.push(migratedProfile); + }); + + await mx.setAccountData( + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME as Parameters< + typeof mx.setAccountData + >[0], + msc4461Index as Parameters[1] + ); + + profileListIndex = mx.getAccountData( + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME as Parameters< + typeof mx.getAccountData + >[0] + ); + } + + if (!profileListIndex) return []; + return (profileListIndex.getContent() as PerMessageProfileIndexMsc4461).content.profiles; } /** @@ -374,34 +565,32 @@ export async function getAllPerMessageProfiles(mx: MatrixClient): Promise[0] +export async function addOrUpdatePerMessageProfile( + mx: MatrixClient, + profile: PerMessageProfileMsc4461 +) { + let profileListIndex = mx.getAccountData( + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME as Parameters< + typeof mx.getAccountData + >[0] ); - const profileWithCompat = { - ...profile, - compat: { - version: 1, - compatDate: '2026-03-26', - } satisfies AccountDataCompatVersion, - } satisfies PerMessageProfile; - if (profileListIndex?.getContent()?.profileIds.includes(profile.id)) { - return await mx.setAccountData( - `${ACCOUNT_DATA_PREFIX}.${profile.id}` as Parameters[0], - profileWithCompat as Parameters[1] - ); + + const newIndex = (profileListIndex?.getContent() as + | PerMessageProfileIndexMsc4461 + | undefined) || { type: 'm.per_message_profiles', content: { profiles: [] } }; + + const foundProfile = newIndex.content.profiles.findIndex((p) => p.id === profile.id); + if (foundProfile !== -1) { + newIndex.content.profiles[foundProfile] = profile; + } else { + newIndex.content.profiles.push(profile); } - const newProfileIds = [...(profileListIndex?.getContent()?.profileIds || []), profile.id]; - return await Promise.all([ - mx.setAccountData( - `${ACCOUNT_DATA_PREFIX}.index` as Parameters[0], - { profileIds: newProfileIds } as Parameters[1] - ), - mx.setAccountData( - `${ACCOUNT_DATA_PREFIX}.${profile.id}` as Parameters[0], - profileWithCompat as Parameters[1] - ), - ]); + mx.setAccountData( + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME as Parameters< + typeof mx.getAccountData + >[0], + newIndex as Parameters[1] + ); } /** @@ -508,93 +697,9 @@ export async function setCurrentlyUsedPerMessageProfileIdForAccount( ); } -/** - * - * @param mx the matrix client - * @param profileId the profile id which the prefix should be attached to - * @param proxy the prefix to use as index - * @param proxyRegExp the regex we can use to match the prefix - * @param reset wheather to delete the prefix - */ -export async function associateProxyWithProfile( - mx: MatrixClient, - profileId: string | undefined, - prefix: string | undefined, - suffix: string | undefined, - reset: boolean -) { - if (!(prefix || suffix)) throw new Error('Proxy must have either a prefix, suffix, or both.'); - - const associations = getProxyAssociationMap( - mx - .getAccountData( - `${ACCOUNT_DATA_PREFIX}.proxyassociation` as Parameters[0] - ) - ?.getContent() - ); - - const proxy = createProxyKey(prefix, suffix); - - if (reset) associations.delete(proxy); - - if (!profileId) throw new Error('profileId might not be undefined'); - if (profileId) - associations.set(proxy, { - profileId, - prefix, - suffix, - } satisfies PerMessageProfileProxyAssociationV2); - await mx.setAccountData( - `${ACCOUNT_DATA_PREFIX}.proxyassociation` as Parameters[0], - { associations: proxyAssociationsMapToObject(associations) } as Parameters< - typeof mx.setAccountData - >[1] - ); -} - -/** - * - * - * @export - * @param {MatrixClient} mx the matrix client - * @return {*} {Promise} +/* + * @deprecated in favor of Msc4461 format triggers */ -export async function getAllPerMessageProfileProxies( - mx: MatrixClient -): Promise { - const cont: PerMessageProfileProxyAssociationWrapper | undefined = mx - .getAccountData( - `${ACCOUNT_DATA_PREFIX}.proxyassociation` as Parameters[0] - ) - ?.getContent(); - if (!cont) return []; - const pmap = getProxyAssociationMap(cont); - const parr = new Array(); - let needsMigration = false; - pmap.entries().forEach(([k, v]) => { - if (proxyNeedsMigration(v)) { - needsMigration = true; - v = migratePmpProxyAssociation(k, v) ?? v; - } - return parr.push(v as PerMessageProfileProxyAssociationV2); - }); - - if (needsMigration) { - const newPmap = new Map( - pmap.entries().map(([k, v]) => [k, migratePmpProxyAssociation(k, v) ?? v]) - ); - - await mx.setAccountData( - `${ACCOUNT_DATA_PREFIX}.proxyassociation` as Parameters[0], - { associations: proxyAssociationsMapToObject(newPmap) } as Parameters< - typeof mx.setAccountData - >[1] - ); - } - - return parr; -} - export async function getAllProxiesForPMP( mx: MatrixClient, profileId: string @@ -616,24 +721,6 @@ export async function getAllProxiesForPMP( return parr; } -export async function dropProxyAssociationForPMP(mx: MatrixClient, proxy: string) { - const associations = getProxyAssociationMap( - mx - .getAccountData( - `${ACCOUNT_DATA_PREFIX}.proxyassociation` as Parameters[0] - ) - ?.getContent() - ); - if (!associations) return; - associations.delete(proxy); - await mx.setAccountData( - `${ACCOUNT_DATA_PREFIX}.proxyassociation` as Parameters[0], - { associations: proxyAssociationsMapToObject(associations) } as Parameters< - typeof mx.setAccountData - >[1] - ); -} - /** * * drops all room associations for a profile, used when deleting a profile to make sure there are no dangling associations left that point to a non existing profile, which could cause issues when trying to apply the profile to a message in a room that still has an association for the deleted profile. @@ -701,7 +788,7 @@ export async function renamePerMessageProfile(mx: MatrixClient, oldId: string, n export async function getCurrentlyUsedPerMessageProfileForRoom( mx: MatrixClient, roomId: string -): Promise { +): Promise { const accountData = mx.getAccountData( `${ACCOUNT_DATA_PREFIX}.roomassociation` as Parameters[0] ); @@ -717,7 +804,7 @@ export async function getCurrentlyUsedPerMessageProfileForRoom( */ export async function getCurrentlyUsedPerMessageProfileForAccount( mx: MatrixClient -): Promise { +): Promise { const accountData = mx.getAccountData( `${ACCOUNT_DATA_PREFIX}.globalassociation` as Parameters[0] ); diff --git a/src/app/plugins/pluralkit-handler/PKitCommandMessageHandler.ts b/src/app/plugins/pluralkit-handler/PKitCommandMessageHandler.ts index 0eeb69132a..8315448bbb 100644 --- a/src/app/plugins/pluralkit-handler/PKitCommandMessageHandler.ts +++ b/src/app/plugins/pluralkit-handler/PKitCommandMessageHandler.ts @@ -1,11 +1,10 @@ import type { - PerMessageProfile, + PerMessageProfileMsc4461, PerMessageProfileProxyAssociationV2, + ProfileTrigger, } from '$hooks/usePerMessageProfile'; import { addOrUpdatePerMessageProfile, - associateProxyWithProfile, - dropProxyAssociationForPMP, extractCircumfixProxyTagsFromKey, getAllPerMessageProfiles, getPerMessageProfileById, @@ -13,6 +12,10 @@ import { import { sendFeedback } from '$utils/sendFeedbackToUser'; import type { MatrixClient, Room } from '$types/matrix-sdk'; import { generateShortId } from '$utils/shortIdGen'; +import { + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME, + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME, +} from '$unstable/prefixes'; const pkMemberRenameRegex = /^(pk;member)\s+"?([\w\s]+)"?\s*rename\s+"?([\w\s]+)"?$/; const pkMemberNewRegex = /^(pk;member)\s+new\s+"?([\w\s]+)"?$/; @@ -47,6 +50,27 @@ export function buildProxyRegex({ prefix, suffix }: PerMessageProfileProxyAssoci return new RegExp(`^${pattern}$`); } +export function testTriggers(triggers: ProfileTrigger, input: string): boolean { + const matchesPrefix = triggers.prefix.some((prefix) => input.startsWith(prefix)); + if (matchesPrefix) return true; + + const matchesSuffix = triggers[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]?.some( + (suffix) => input.startsWith(suffix) + ); + if (matchesSuffix) return true; + + const matchesCircumfix = triggers[ + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME + ]?.some(({ prefix, suffix }) => { + const casePrefix = prefix ? input.startsWith(prefix) : true; + const caseSuffix = suffix ? input.endsWith(suffix) : true; + + return casePrefix && caseSuffix; + }); + + return !!matchesCircumfix; +} + export function testProxy( { prefix, suffix }: PerMessageProfileProxyAssociationV2, input: string @@ -56,8 +80,26 @@ export function testProxy( return matchesPrefix && matchesSuffix; } + +export function stripTrigger(triggers: ProfileTrigger, input: string): string { + const prefix = triggers.prefix.find((value) => input.startsWith(value)); + if (prefix !== undefined) return stripProxy({ prefix }, input); + + const suffix = triggers[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]?.find( + (value) => input.endsWith(value) + ); + if (suffix !== undefined) return stripProxy({ suffix }, input); + + const circumfix = triggers[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]?.find( + (trigger) => input.startsWith(trigger.prefix) && input.endsWith(trigger.suffix) + ); + if (circumfix) return stripProxy(circumfix, input); + + return input; +} + export function stripProxy( - { prefix, suffix }: PerMessageProfileProxyAssociationV2, + { prefix, suffix }: { prefix?: string; suffix?: string }, input: string ): string { let message = input; @@ -128,7 +170,8 @@ export class PKitCommandMessageHandler { ); await addOrUpdatePerMessageProfile(this.mx, { id: generatedID, - name: memberName, + displayname: memberName, + trigger: { prefix: [] }, }); sendFeedback( `added new member has been created with id: ${generatedID} and name ${memberName}`, @@ -159,7 +202,7 @@ export class PKitCommandMessageHandler { * The id of the per-message-profile */ const pmpId = (await getAllPerMessageProfiles(this.mx)).find( - (pmp) => pmp.name === oldName + (pmp) => pmp.displayname === oldName )?.id; if (!pmpId) { sendFeedback( @@ -190,7 +233,7 @@ export class PKitCommandMessageHandler { ); return; } - pmp.name = newName; + pmp.displayname = newName; sendFeedback( `renaming your profile ${pmpId} from ${oldName} to ${newName}`, this.room, @@ -204,7 +247,7 @@ export class PKitCommandMessageHandler { const matchAgainst = cmdParts[3]; const pmpId = this.useIdInsteadOfNameWherePossible ? name - : (await getAllPerMessageProfiles(this.mx)).find((pmp) => pmp.name === name)?.id; + : (await getAllPerMessageProfiles(this.mx)).find((pmp) => pmp.displayname === name)?.id; if (!pmpId) { sendFeedback( `Persona with ${this.useIdInsteadOfNameWherePossible ? 'id' : 'name'} "${name}" doesn't exist in your records, ${helpTextPkMemberNew}`, @@ -222,13 +265,33 @@ export class PKitCommandMessageHandler { ); return; } - await dropProxyAssociationForPMP(this.mx, matchAgainst); + const proxyTags = extractCircumfixProxyTagsFromKey(matchAgainst); + const pmp = await getPerMessageProfileById(this.mx, pmpId); - sendFeedback( - `Persona with ${this.useIdInsteadOfNameWherePossible ? 'id' : 'name'} "${name}" (${pmpId}) is now no longer associated with ${matchAgainst}`, - this.room, - this.mx.getSafeUserId() - ); + if (pmp && proxyTags) { + const { prefix, suffix } = proxyTags; + + if (prefix && suffix) { + pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME] ??= []; + pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME] = pmp.trigger[ + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME + ].filter((trigger) => trigger.prefix !== prefix && trigger.suffix !== suffix); + } else if (prefix && !suffix) { + pmp.trigger.prefix = pmp.trigger.prefix.filter((trigger) => trigger !== prefix); + } else if (!prefix && suffix) { + pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME] ??= []; + pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME] = pmp.trigger[ + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME + ].filter((trigger) => trigger !== suffix); + } + await addOrUpdatePerMessageProfile(this.mx, pmp); + + sendFeedback( + `Persona with ${this.useIdInsteadOfNameWherePossible ? 'id' : 'name'} "${name}" (${pmpId}) is now no longer associated with ${matchAgainst}`, + this.room, + this.mx.getSafeUserId() + ); + } } else if (pkMemberNewProxy.test(this.message)) { const cmdParts = pkMemberNewProxy.exec(this.message); if (!cmdParts) return; @@ -236,7 +299,7 @@ export class PKitCommandMessageHandler { const matchAgainst = cmdParts[4]; const pmpId = this.useIdInsteadOfNameWherePossible ? name - : (await getAllPerMessageProfiles(this.mx)).find((pmp) => pmp.name === name)?.id; + : (await getAllPerMessageProfiles(this.mx)).find((pmp) => pmp.displayname === name)?.id; if (!pmpId) { sendFeedback( `Persona with ${this.useIdInsteadOfNameWherePossible ? 'id' : 'name'} "${name}" doesn't exist in your records, ${helpTextPkMemberNew}`, @@ -263,17 +326,37 @@ export class PKitCommandMessageHandler { ); return; } - await associateProxyWithProfile(this.mx, pmpId, proxyTags.prefix, proxyTags.suffix, false); - sendFeedback( - `Persona with ${this.useIdInsteadOfNameWherePossible ? 'id' : 'name'} "${name}" (${pmpId}) is now associated with ${matchAgainst}`, - this.room, - this.mx.getSafeUserId() - ); + let pmp = await getPerMessageProfileById(this.mx, pmpId); + if (pmp) { + const { prefix, suffix } = proxyTags; + + if (prefix && suffix) { + (pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME] ??= []).push({ + prefix: prefix, + suffix: suffix, + }); + } else if (!prefix && suffix) { + (pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME] ??= []).push( + suffix + ); + } else if (prefix && !suffix) { + pmp.trigger.prefix.push(prefix); + } + await addOrUpdatePerMessageProfile(this.mx, pmp); + sendFeedback( + `Persona with ${this.useIdInsteadOfNameWherePossible ? 'id' : 'name'} "${name}" (${pmpId}) is now associated with ${matchAgainst}`, + this.room, + this.mx.getSafeUserId() + ); + } } else { // default to looking up member info - const listOfProfiles: PerMessageProfile[] = await getAllPerMessageProfiles(this.mx); + const listOfProfiles: PerMessageProfileMsc4461[] = await getAllPerMessageProfiles(this.mx); const stringListOfProfiles: string = listOfProfiles - .map((pmp: PerMessageProfile) => `${pmp.id}: ${pmp.name ? pmp.name : '(empty name)'}`) + .map( + (pmp: PerMessageProfileMsc4461) => + `${pmp.id}: ${pmp.displayname ? pmp.displayname : '(empty name)'}` + ) .join('\n'); sendFeedback( `If you see this, you have messed up a command\n\nYou currently have the following persona set up:\n${stringListOfProfiles}\n\n${helpTextPkMember}`, diff --git a/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.test.ts b/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.test.ts index d3e5f4dc79..68f859b4c6 100644 --- a/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.test.ts +++ b/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.test.ts @@ -6,7 +6,7 @@ import type { MatrixClient } from '$types/matrix-sdk'; // Mock the hook module that provides proxy associations + profile lookup vi.mock('$hooks/usePerMessageProfile', () => ({ - getAllPerMessageProfileProxies: vi.fn<() => Promise>(), + getAllPerMessageProfiles: vi.fn<() => Promise>(), getPerMessageProfileById: vi.fn<() => Promise>(), parsePerMessageProfileProxyAssociation: vi.fn<() => unknown>(), })); @@ -24,18 +24,36 @@ describe('PKitProxyMessageHandler', () => { }); it('matches a proxied message, returns pmp, and strips content', async () => { - (mocked.getAllPerMessageProfileProxies as unknown as Mock).mockResolvedValueOnce([ - { profileId: 'p1', prefix: '[', suffix: ']' }, + (mocked.getAllPerMessageProfiles as unknown as Mock).mockResolvedValueOnce([ + { + id: 'p1', + name: 'Test', + trigger: { + prefix: [], + 'net.f0rest.circumfix': [{ prefix: '[', suffix: ']' }], + }, + }, ]); (mocked.getPerMessageProfileById as unknown as Mock).mockResolvedValueOnce({ id: 'p1', name: 'Test', + trigger: { + prefix: [], + 'net.f0rest.circumfix': [{ prefix: '[', suffix: ']' }], + }, }); const handler = new PKitProxyMessageHandler({} as unknown as MatrixClient); const pmp = await handler.getPmpBasedOnMessage('[hello]'); - expect(pmp).toEqual({ id: 'p1', name: 'Test' }); + expect(pmp).toEqual({ + id: 'p1', + name: 'Test', + trigger: { + prefix: [], + 'net.f0rest.circumfix': [{ prefix: '[', suffix: ']' }], + }, + }); // getPmpBasedOnMessage refreshes/init() so we should be inited now expect(handler.isAProxiedMessage('[hello]')).toBe(true); diff --git a/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.ts b/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.ts index df6613e627..bfb3eaa659 100644 --- a/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.ts +++ b/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.ts @@ -1,13 +1,7 @@ -import type { - PerMessageProfile, - PerMessageProfileProxyAssociationV2, -} from '$hooks/usePerMessageProfile'; -import { - getAllPerMessageProfileProxies, - getPerMessageProfileById, -} from '$hooks/usePerMessageProfile'; +import type { PerMessageProfileMsc4461 } from '$hooks/usePerMessageProfile'; +import { getAllPerMessageProfiles, getPerMessageProfileById } from '$hooks/usePerMessageProfile'; import type { MatrixClient } from '$types/matrix-sdk'; -import { stripProxy, testProxy } from './PKitCommandMessageHandler'; +import { stripTrigger, testTriggers } from './PKitCommandMessageHandler'; /** * proxy message handler @@ -24,12 +18,12 @@ export class PKitProxyMessageHandler { private readonly mx: MatrixClient; /** - * a list of proxies; is not initialized in the constructor + * a list of profiles; is not initialized in the constructor * @private - * @type {PerMessageProfileProxyAssociation[]} + * @type {PerMessageProfileMsc4461[]} * @memberof PKitProxyMessageHandler */ - private proxiesAssocs: PerMessageProfileProxyAssociationV2[]; + private profiles: PerMessageProfileMsc4461[]; private succInit: boolean; @@ -39,7 +33,7 @@ export class PKitProxyMessageHandler { */ public constructor(mx: MatrixClient) { this.mx = mx; - this.proxiesAssocs = []; + this.profiles = []; this.succInit = false; } @@ -48,7 +42,7 @@ export class PKitProxyMessageHandler { */ public async init(): Promise { try { - this.proxiesAssocs = await getAllPerMessageProfileProxies(this.mx); + this.profiles = await getAllPerMessageProfiles(this.mx); this.succInit = true; } catch (err) { this.succInit = false; @@ -64,7 +58,7 @@ export class PKitProxyMessageHandler { */ public isAProxiedMessage(message: string): boolean { if (!this.succInit) return false; - return this.proxiesAssocs.some((assoc) => testProxy(assoc, message)); + return this.profiles.some((profile) => testTriggers(profile.trigger, message)); } /** @@ -72,11 +66,13 @@ export class PKitProxyMessageHandler { * @param message the message to look at * @returns the matching Per-Message-Profile, if any */ - public async getPmpBasedOnMessage(message: string): Promise { + public async getPmpBasedOnMessage( + message: string + ): Promise { // Always refresh so newly-added proxies apply immediately. await this.init(); // check if the message matches our formats - const profileId = this.proxiesAssocs.find((assoc) => testProxy(assoc, message))?.profileId; + const profileId = this.profiles.find((profile) => testTriggers(profile.trigger, message))?.id; if (!profileId) return undefined; return getPerMessageProfileById(this.mx, profileId); } @@ -91,8 +87,8 @@ export class PKitProxyMessageHandler { public stripProxyFromMessage(message: string): string | undefined { if (!this.succInit) return undefined; let m; - this.proxiesAssocs.forEach((assoc) => { - if (testProxy(assoc, message)) m = stripProxy(assoc, message); + this.profiles.forEach((profile) => { + if (testTriggers(profile.trigger, message)) m = stripTrigger(profile.trigger, message); }); return m; } diff --git a/src/unstable/prefixes/msc/profile.ts b/src/unstable/prefixes/msc/profile.ts index b7b9698930..94da45319a 100644 --- a/src/unstable/prefixes/msc/profile.ts +++ b/src/unstable/prefixes/msc/profile.ts @@ -24,5 +24,7 @@ export const MATRIX_UNSTABLE_MSC4466_PROPAGATE_TO = 'computer.gingershaped.msc44 /** * Unstable prefix for username colors. defined in https://github.com/matrix-org/matrix-spec-proposals/pull/4522 */ - export const MATRIX_UNSTABLE_COLORS = 'eu.she-a.color'; + +export const MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME = 'net.f0rest.suffix'; +export const MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME = 'net.f0rest.circumfix'; diff --git a/src/unstable/prefixes/sable/accountdata.ts b/src/unstable/prefixes/sable/accountdata.ts index 001404ec5a..7d33034e4b 100644 --- a/src/unstable/prefixes/sable/accountdata.ts +++ b/src/unstable/prefixes/sable/accountdata.ts @@ -11,6 +11,8 @@ export const MATRIX_SABLE_UNSTABLE_ACCOUNT_SETTINGS_PROPERTY_NAME = 'moe.sable.a */ export const MATRIX_SABLE_UNSTABLE_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME = 'fyi.cisnt.permessageprofile'; +export const MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME = + 'fi.mau.msc4461.per_message_profiles.v2'; export const MATRIX_SABLE_UNSTABLE_DISMISSED_INVITES = 'moe.sable.dismissed_invites'; export const MATRIX_SABLE_UNSTABLE_ACCOUNT_ADDED_SERVERS_PROPERTY_NAME = 'moe.sable.added_servers'; From afe64da018bc11159af28ec7fcd3b326d91a7feb Mon Sep 17 00:00:00 2001 From: Josie Date: Mon, 3 Aug 2026 17:55:52 +0100 Subject: [PATCH 2/2] msc4461 delete old records --- src/app/hooks/usePerMessageProfile.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app/hooks/usePerMessageProfile.ts b/src/app/hooks/usePerMessageProfile.ts index 07a4ce53b1..5fdf4d7f49 100644 --- a/src/app/hooks/usePerMessageProfile.ts +++ b/src/app/hooks/usePerMessageProfile.ts @@ -553,6 +553,18 @@ export async function getAllPerMessageProfiles( typeof mx.getAccountData >[0] ); + + // delete old records + await mx.deleteAccountData( + 'fyi.cisnt.permessageprofile.index' as Parameters[0] + ); + + profiles.forEach(async (profile) => { + if (!profile) return; + await mx.deleteAccountData( + `fyi.cisnt.permessageprofile.${profile.id}` as Parameters[0] + ); + }); } if (!profileListIndex) return [];