Skip to content

Commit 4a735be

Browse files
authored
Impl MSC4461: Storing per-message profiles for users (#1589)
<!-- Please read https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md before submitting your pull request --> ### Description <!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. --> Migrates PMP account data to the format described in [MSC4461](matrix-org/matrix-spec-proposals#4461). Extensions to the format are defined in my informal [MSCF002](https://github.com/j0lol/matrix-spec-proposals/blob/mscf/proposals/F0002-PK-proxy-tags.md). (Though ambiguity behavior defined in this spec have not been implemented just yet.) Part of #1279 #### Type of change breaking change on an experimental feature, with migrations - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings ### AI disclosure: - [ ] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). <!-- Write any explanation required here, but do not generate the explanation using AI!! You must prove you understand what the code in this PR does. -->
2 parents 900d810 + afe64da commit 4a735be

21 files changed

Lines changed: 663 additions & 522 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: minor
3+
---
4+
5+
# Impl MSC4461: Storing per-message profiles for users

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
"marked": "^18.0.5",
100100
"matrix-js-sdk": "42.0.0",
101101
"matrix-widget-api": "^1.17.0",
102+
"nanoid": "^6.0.0",
102103
"pdfjs-dist": "^6.1.200",
103104
"react": "^18.3.1",
104105
"react-aria": "^3.50.0",

‎pnpm-lock.yaml‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/app/components/message-preview/MessagePreview.tsx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -415,21 +415,23 @@ export function MessagePreview({
415415
const [showPronouns] = useSetting(settingsAtom, 'showPronouns');
416416
const [parsePronouns] = useSetting(settingsAtom, 'parsePronouns');
417417
const { cleanedDisplayName: displayName, inlinePronoun } = useMemo(
418-
() => getParsedPronouns(perMessageProfile?.name || fallbackDisplayName, parsePronouns),
419-
[perMessageProfile?.name, fallbackDisplayName, parsePronouns]
418+
() => getParsedPronouns(perMessageProfile?.displayname || fallbackDisplayName, parsePronouns),
419+
[perMessageProfile?.displayname, fallbackDisplayName, parsePronouns]
420420
);
421421
const pronouns = useMemo(() => {
422-
const resolved = [...(perMessageProfile?.pronouns ?? userProfile.pronouns ?? [])];
422+
const resolved = [
423+
...(perMessageProfile?.['io.fsky.nyx.pronouns'] ?? userProfile.pronouns ?? []),
424+
];
423425
if (
424426
inlinePronoun &&
425427
!resolved.some((item) => item.summary?.toLowerCase() === inlinePronoun.toLowerCase())
426428
) {
427429
resolved.push({ summary: inlinePronoun, language: 'en' });
428430
}
429431
return resolved;
430-
}, [perMessageProfile?.pronouns, userProfile.pronouns, inlinePronoun]);
432+
}, [perMessageProfile, userProfile.pronouns, inlinePronoun]);
431433
const avatarMxc =
432-
perMessageProfile?.avatarUrl ??
434+
perMessageProfile?.avatar_url ??
433435
getMemberAvatarMxc(room, sender) ??
434436
userProfile.avatarUrl ??
435437
profile?.avatarUrl;

‎src/app/components/message/Reply.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ export const Reply = as<'div', ReplyProps>(
512512
eventType !== EventType.RoomMember && (
513513
<Text size="T300" truncate style={{ fontFamily: usernameFont }}>
514514
<b>
515-
{pmp?.name ??
515+
{pmp?.displayname ??
516516
getMemberDisplayName(room, sender, nicknames) ??
517517
cachedProfiles[sender]?.displayName ??
518518
getMxIdLocalPart(sender)}

‎src/app/components/message/modals/Options.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import { useFavoriteGifs } from '$hooks/useFavoriteGifs';
6767
import type { IImageInfo } from '$types/matrix/common';
6868
import { getIncomingMediaMxcUrl } from '../MsgTypeRenderers';
6969
import { TemporaryPersonaPicker } from '$features/room/persona-picker/PersonaPicker';
70-
import { type PerMessageProfile } from '$hooks/usePerMessageProfile';
70+
import { type PerMessageProfileMsc4461 } from '$hooks/usePerMessageProfile';
7171
import { buildReplacementPmpContent } from '$features/room/buildReplacementContent';
7272
import { settingsAtom } from '$state/settings';
7373
import { useSetting } from '$state/hooks/settings';
@@ -414,7 +414,7 @@ function OptionsReproxyPersonaPicker({
414414
closeMenu,
415415
anchor,
416416
}: OptionsReproxyPersonaPickerProps) {
417-
const reproxyMessage = async (profile: PerMessageProfile | undefined) => {
417+
const reproxyMessage = async (profile: PerMessageProfileMsc4461 | undefined) => {
418418
const content = buildReplacementPmpContent(mEvent.getContent(), mEvent.getId()!, profile);
419419
await mx.sendMessage(roomId, content as RoomMessageEventContent);
420420

‎src/app/features/room/RoomInput.tsx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ import {
150150
convertPerMessageProfileToBeeperFormat,
151151
getCurrentlyUsedPerMessageProfileForAccount,
152152
getCurrentlyUsedPerMessageProfileForRoom,
153-
type PerMessageProfile,
153+
type PerMessageProfileMsc4461,
154154
setCurrentlyUsedPerMessageProfileIdForRoom,
155155
} from '$hooks/usePerMessageProfile';
156156
import {
@@ -342,7 +342,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
342342
const [pmpLatchingEnable] = useSetting(settingsAtom, 'pmpLatching');
343343
const [pmpPickerEnable] = useSetting(settingsAtom, 'pmpPicker');
344344

345-
const [latchedPersona, setLatchedPersona] = useState<PerMessageProfile>();
345+
const [latchedPersona, setLatchedPersona] = useState<PerMessageProfileMsc4461>();
346346

347347
const emojiBtnRef = useRef<HTMLButtonElement>(null);
348348
const gifBtnRef = useRef<HTMLButtonElement>(null);
@@ -1361,12 +1361,12 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
13611361
content[prefix.MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME] =
13621362
convertPerMessageProfileToBeeperFormat(
13631363
perMessageProfile,
1364-
perMessageProfile.name.trim() !== ''
1364+
perMessageProfile.displayname.trim() !== ''
13651365
);
13661366

1367-
if (perMessageProfile.name.trim() !== '') {
1367+
if (perMessageProfile.displayname.trim() !== '') {
13681368
// if a per-message profile is used, it must per spec include a fallback
1369-
const pmpPrefix = `${perMessageProfile.name}: `;
1369+
const pmpPrefix = `${perMessageProfile.displayname}: `;
13701370

13711371
if (!content.body.startsWith(pmpPrefix)) {
13721372
// to prevent double-prefixing when the fallback is already present
@@ -1376,7 +1376,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
13761376
/**
13771377
* html escaped version of the display name
13781378
*/
1379-
const escapedName = sanitizeText(perMessageProfile.name);
1379+
const escapedName = sanitizeText(perMessageProfile.displayname);
13801380

13811381
const htmlPrefix = `<strong data-mx-profile-fallback>${escapedName}: </strong>`;
13821382

‎src/app/features/room/buildReplacementContent.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { IContent, IMentions } from '$types/matrix-sdk';
22
import { MsgType, RelationType } from '$types/matrix-sdk';
33
import { customHtmlEqualsPlainText } from '$components/editor';
44
import { sanitizeText } from '$utils/sanitize';
5-
import type { PerMessageProfile } from '$hooks/usePerMessageProfile';
5+
import type { PerMessageProfileMsc4461 } from '$hooks/usePerMessageProfile';
66
import { convertPerMessageProfileToBeeperFormat } from '$hooks/usePerMessageProfile';
77
import { MATRIX_UNSTABLE_PER_MESSAGE_PROFILE_PROPERTY_NAME } from '$unstable/prefixes';
88

@@ -100,7 +100,7 @@ export function buildReplacementContent(
100100
export function buildReplacementPmpContent(
101101
oldContent: IContent,
102102
eventId: string,
103-
newProfile: PerMessageProfile | undefined
103+
newProfile: PerMessageProfileMsc4461 | undefined
104104
) {
105105
const profileBeeperFormat =
106106
newProfile && convertPerMessageProfileToBeeperFormat(newProfile, true);
@@ -121,7 +121,7 @@ export function buildReplacementPmpContent(
121121
}
122122

123123
if (newProfile) {
124-
const escapedName = sanitizeText(newProfile.name);
124+
const escapedName = sanitizeText(newProfile.displayname);
125125
const htmlPrefix = `<strong data-mx-profile-fallback>${escapedName}: </strong>`;
126126

127127
if (oldContent.formatted_body) {
@@ -133,7 +133,7 @@ export function buildReplacementPmpContent(
133133
oldContent.formatted_body = `${htmlPrefix}${escapedBody}`;
134134
}
135135

136-
const pmpPrefix = `${newProfile.name}: `;
136+
const pmpPrefix = `${newProfile.displayname}: `;
137137
oldContent.body = pmpPrefix + oldContent.body;
138138
}
139139

‎src/app/features/room/message/Message.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,8 @@ function MessageInternal(
458458

459459
const pmpNameColor = useMemo(() => {
460460
if (!renderPersonaColors) return undefined;
461-
const pmpNameColorLight = parsedPMPContent?.colors?.on_light;
462-
const pmpNameColorDark = parsedPMPContent?.colors?.on_dark;
461+
const pmpNameColorLight = parsedPMPContent?.['eu.she-a.color']?.on_light;
462+
const pmpNameColorDark = parsedPMPContent?.['eu.she-a.color']?.on_dark;
463463

464464
return activeTheme.kind === ThemeKind.Dark ? pmpNameColorDark : pmpNameColorLight;
465465
}, [parsedPMPContent, activeTheme, renderPersonaColors]);
@@ -468,7 +468,7 @@ function MessageInternal(
468468
* boolean to indicate wheather we should indicate to the user that it is a pmp
469469
* We want to not show it, when the name is unset, or whitespace only
470470
*/
471-
const showPmPInfo = parsedPMPContent?.name && parsedPMPContent.name?.trim() !== '';
471+
const showPmPInfo = parsedPMPContent?.displayname && parsedPMPContent.displayname?.trim() !== '';
472472
// Profiles and Colors
473473
const profile = useUserProfile(senderId, room, undefined, true, isVisible);
474474
const { color: usernameColor, font: usernameFont } = useSableCosmetics(
@@ -488,7 +488,7 @@ function MessageInternal(
488488
* otherwise we fall back to the profile pronouns.
489489
* 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.
490490
*/
491-
const pronouns = parsedPMPContent?.pronouns ?? profile.pronouns;
491+
const pronouns = parsedPMPContent?.['io.fsky.nyx.pronouns'] ?? profile.pronouns;
492492

493493
const [highlightMentions] = useSetting(settingsAtom, 'highlightMentions');
494494

‎src/app/features/room/persona-picker/PersonaPicker.tsx‎

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useMediaAuthentication } from '$hooks/useMediaAuthentication.ts';
1010
import {
1111
getCurrentlyUsedPerMessageProfileForRoom,
1212
getAllPerMessageProfiles,
13-
type PerMessageProfile,
13+
type PerMessageProfileMsc4461,
1414
setCurrentlyUsedPerMessageProfileIdForRoom,
1515
getCurrentlyUsedPerMessageProfileForAccount,
1616
setCurrentlyUsedPerMessageProfileIdForAccount,
@@ -58,9 +58,9 @@ type PersonaPickerProps = {
5858
roomId?: string;
5959
suppressEditorRefocus?: () => void;
6060
onTabChange?: (tab: PersonaPickerTab) => void;
61-
latchedPersona?: PerMessageProfile;
61+
latchedPersona?: PerMessageProfileMsc4461;
6262
hideTabs?: boolean;
63-
onPersonaSelect?: (persona: PerMessageProfile | undefined) => void;
63+
onPersonaSelect?: (persona: PerMessageProfileMsc4461 | undefined) => void;
6464
requestClose?: () => void;
6565
showNoneOption?: boolean;
6666
hideButton?: boolean;
@@ -126,22 +126,26 @@ function PersonaPicker({
126126
onTabChange,
127127
}: PersonaPickerProps & { presentation: PersonaPickerPresentation }) {
128128
const useAuthentication = useMediaAuthentication();
129-
130129
const persistent = presentation === PersonaPickerPresentation.PersistentPicker;
131-
132130
const [tab, setTab] = useState(tabProp);
133131
const [AddPersonaMenuAnchor, setAddPersonaMenuAnchor] = useState<RectCords | undefined>(anchor);
134-
const [profiles, setProfiles] = useState<PerMessageProfile[] | undefined>(undefined);
135-
const [selectedGlobalPersona, setSelectedGlobalPersona] = useState<PerMessageProfile | null>(
136-
null
137-
);
138-
const [selectedRoomPersona, setSelectedRoomPersona] = useState<PerMessageProfile | null>(
132+
const activeTheme = useActiveTheme();
133+
const [profiles, setProfiles] = useState<PerMessageProfileMsc4461[] | undefined>(undefined);
134+
const [selectedGlobalPersona, setSelectedGlobalPersona] =
135+
useState<PerMessageProfileMsc4461 | null>(null);
136+
const [selectedRoomPersona, setSelectedRoomPersona] = useState<PerMessageProfileMsc4461 | null>(
139137
latchedPersona ?? null
140138
);
141139

142-
const defactoPersona = () => selectedRoomPersona ?? selectedGlobalPersona;
140+
const nameColor = useCallback(
141+
(persona: PerMessageProfileMsc4461) =>
142+
activeTheme.kind === ThemeKind.Dark
143+
? persona['eu.she-a.color']?.on_dark
144+
: persona['eu.she-a.color']?.on_light,
145+
[activeTheme]
146+
);
143147

144-
const activeTheme = useActiveTheme();
148+
const defactoPersona = () => selectedRoomPersona ?? selectedGlobalPersona;
145149

146150
useEffect(() => {
147151
const syncProfile = async () => {
@@ -172,7 +176,7 @@ function PersonaPicker({
172176
const filtered = term
173177
? profiles?.filter((profile) =>
174178
searchInputRef.current
175-
? profile.name.toLocaleLowerCase().includes(searchInputRef.current?.value) ||
179+
? profile.displayname.toLocaleLowerCase().includes(searchInputRef.current?.value) ||
176180
profile.id.toLocaleLowerCase().includes(searchInputRef.current?.value)
177181
: true
178182
)
@@ -182,13 +186,8 @@ function PersonaPicker({
182186
},
183187
[profiles]
184188
);
185-
const nameColor = useCallback(
186-
(persona: PerMessageProfile) =>
187-
activeTheme.kind === ThemeKind.Dark ? persona.colors?.on_dark : persona.colors?.on_light,
188-
[activeTheme]
189-
);
190189

191-
const isSelected = (persona: PerMessageProfile | undefined) => {
190+
const isSelected = (persona: PerMessageProfileMsc4461 | undefined) => {
192191
if (!persona) return undefined;
193192
const selected = tab === PersonaPickerTab.Global ? selectedGlobalPersona : selectedRoomPersona;
194193
return persona.id === selected?.id ? true : undefined;
@@ -201,9 +200,9 @@ function PersonaPicker({
201200
}, [mx]);
202201

203202
const avatarUrl = useCallback(
204-
(profile: PerMessageProfile) => {
205-
if (profile.avatarUrl !== undefined) {
206-
return mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined;
203+
(profile: PerMessageProfileMsc4461) => {
204+
if (profile.avatar_url !== undefined) {
205+
return mxcUrlToHttp(mx, profile.avatar_url, useAuthentication, 96, 96, 'crop') ?? undefined;
207206
} else {
208207
return undefined;
209208
}
@@ -212,7 +211,7 @@ function PersonaPicker({
212211
);
213212

214213
const handleSelect = useCallback(
215-
async (profile: PerMessageProfile | undefined) => {
214+
async (profile: PerMessageProfileMsc4461 | undefined) => {
216215
if (onPersonaSelect) {
217216
onPersonaSelect(profile);
218217
return;
@@ -320,10 +319,10 @@ function PersonaPicker({
320319
<UserAvatar
321320
userId={profile.id}
322321
src={avatarUrl(profile)}
323-
fallbackColor={profile.colors?.on_light ?? undefined}
322+
fallbackColor={profile['eu.she-a.color']?.on_light ?? undefined}
324323
renderFallback={() => (
325324
<Text as="span" size="H4" aria-label="Avatar fallback">
326-
{nameInitials(profile.name)}
325+
{nameInitials(profile.displayname)}
327326
</Text>
328327
)}
329328
alt={`Avatar for profile ${profile.id}`}
@@ -335,7 +334,7 @@ function PersonaPicker({
335334
truncate
336335
style={{ color: nameColor(profile) ?? undefined, maxWidth: toRem(150) }}
337336
>
338-
{profile.name}
337+
{profile.displayname}
339338
</Text>
340339
</MenuItem>
341340
))}
@@ -422,7 +421,7 @@ function PersonaPicker({
422421
src={avatarUrl(defactoPersona()!)}
423422
renderFallback={() => (
424423
<Text as="span" size="H6" aria-label="Avatar fallback">
425-
{nameInitials(defactoPersona()!.name)}
424+
{nameInitials(defactoPersona()!.displayname)}
426425
</Text>
427426
)}
428427
alt={`Avatar for profile ${defactoPersona()!.id}`}

0 commit comments

Comments
 (0)