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..5fdf4d7f49 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,68 @@ 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]
+ );
+
+ // 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 [];
+ return (profileListIndex.getContent() as PerMessageProfileIndexMsc4461).content.profiles;
}
/**
@@ -374,34 +577,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 +709,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 +733,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 +800,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 +816,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';