Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

# Impl MSC4461: Storing per-message profiles for users
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions src/app/components/message-preview/MessagePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -415,21 +415,23 @@ 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())
) {
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;
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/message/Reply.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ export const Reply = as<'div', ReplyProps>(
eventType !== EventType.RoomMember && (
<Text size="T300" truncate style={{ fontFamily: usernameFont }}>
<b>
{pmp?.name ??
{pmp?.displayname ??
getMemberDisplayName(room, sender, nicknames) ??
cachedProfiles[sender]?.displayName ??
getMxIdLocalPart(sender)}
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/message/modals/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand Down
12 changes: 6 additions & 6 deletions src/app/features/room/RoomInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ import {
convertPerMessageProfileToBeeperFormat,
getCurrentlyUsedPerMessageProfileForAccount,
getCurrentlyUsedPerMessageProfileForRoom,
type PerMessageProfile,
type PerMessageProfileMsc4461,
setCurrentlyUsedPerMessageProfileIdForRoom,
} from '$hooks/usePerMessageProfile';
import {
Expand Down Expand Up @@ -342,7 +342,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const [pmpLatchingEnable] = useSetting(settingsAtom, 'pmpLatching');
const [pmpPickerEnable] = useSetting(settingsAtom, 'pmpPicker');

const [latchedPersona, setLatchedPersona] = useState<PerMessageProfile>();
const [latchedPersona, setLatchedPersona] = useState<PerMessageProfileMsc4461>();

const emojiBtnRef = useRef<HTMLButtonElement>(null);
const gifBtnRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -1361,12 +1361,12 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
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
Expand All @@ -1376,7 +1376,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
/**
* html escaped version of the display name
*/
const escapedName = sanitizeText(perMessageProfile.name);
const escapedName = sanitizeText(perMessageProfile.displayname);

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

Expand Down
8 changes: 4 additions & 4 deletions src/app/features/room/buildReplacementContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand All @@ -121,7 +121,7 @@ export function buildReplacementPmpContent(
}

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

if (oldContent.formatted_body) {
Expand All @@ -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;
}

Expand Down
8 changes: 4 additions & 4 deletions src/app/features/room/message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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(
Expand All @@ -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');

Expand Down
53 changes: 26 additions & 27 deletions src/app/features/room/persona-picker/PersonaPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useMediaAuthentication } from '$hooks/useMediaAuthentication.ts';
import {
getCurrentlyUsedPerMessageProfileForRoom,
getAllPerMessageProfiles,
type PerMessageProfile,
type PerMessageProfileMsc4461,
setCurrentlyUsedPerMessageProfileIdForRoom,
getCurrentlyUsedPerMessageProfileForAccount,
setCurrentlyUsedPerMessageProfileIdForAccount,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<RectCords | undefined>(anchor);
const [profiles, setProfiles] = useState<PerMessageProfile[] | undefined>(undefined);
const [selectedGlobalPersona, setSelectedGlobalPersona] = useState<PerMessageProfile | null>(
null
);
const [selectedRoomPersona, setSelectedRoomPersona] = useState<PerMessageProfile | null>(
const activeTheme = useActiveTheme();
const [profiles, setProfiles] = useState<PerMessageProfileMsc4461[] | undefined>(undefined);
const [selectedGlobalPersona, setSelectedGlobalPersona] =
useState<PerMessageProfileMsc4461 | null>(null);
const [selectedRoomPersona, setSelectedRoomPersona] = useState<PerMessageProfileMsc4461 | null>(
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 () => {
Expand Down Expand Up @@ -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
)
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -212,7 +211,7 @@ function PersonaPicker({
);

const handleSelect = useCallback(
async (profile: PerMessageProfile | undefined) => {
async (profile: PerMessageProfileMsc4461 | undefined) => {
if (onPersonaSelect) {
onPersonaSelect(profile);
return;
Expand Down Expand Up @@ -320,10 +319,10 @@ function PersonaPicker({
<UserAvatar
userId={profile.id}
src={avatarUrl(profile)}
fallbackColor={profile.colors?.on_light ?? undefined}
fallbackColor={profile['eu.she-a.color']?.on_light ?? undefined}
renderFallback={() => (
<Text as="span" size="H4" aria-label="Avatar fallback">
{nameInitials(profile.name)}
{nameInitials(profile.displayname)}
</Text>
)}
alt={`Avatar for profile ${profile.id}`}
Expand All @@ -335,7 +334,7 @@ function PersonaPicker({
truncate
style={{ color: nameColor(profile) ?? undefined, maxWidth: toRem(150) }}
>
{profile.name}
{profile.displayname}
</Text>
</MenuItem>
))}
Expand Down Expand Up @@ -422,7 +421,7 @@ function PersonaPicker({
src={avatarUrl(defactoPersona()!)}
renderFallback={() => (
<Text as="span" size="H6" aria-label="Avatar fallback">
{nameInitials(defactoPersona()!.name)}
{nameInitials(defactoPersona()!.displayname)}
</Text>
)}
alt={`Avatar for profile ${defactoPersona()!.id}`}
Expand Down
Loading
Loading