diff --git a/.changeset/pronoun-pills.md b/.changeset/pronoun-pills.md new file mode 100644 index 0000000000..416f3764f3 --- /dev/null +++ b/.changeset/pronoun-pills.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Added a setting to Appearance that attempts to convert text in names like (it/its) into a pronoun pill, enlabed by default. diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 9dd200ed58..7ce5adbe33 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -79,7 +79,7 @@ import { MessageSourceCodeItem } from '$components/message/modals/MessageSource' import { MessageForwardItem } from '$components/message/modals/MessageForward'; import { MessageDeleteItem } from '$components/message/modals/MessageDelete'; import { MessageReportItem } from '$components/message/modals/MessageReport'; -import { filterPronounsByLanguage } from '$utils/pronouns'; +import { filterPronounsByLanguage, getParsedPronouns } from '$utils/pronouns'; import { useMentionClickHandler } from '$hooks/useMentionClickHandler'; import { addStickerToDefaultPack, @@ -390,11 +390,6 @@ function MessageInternal( return mxc ? mxcUrlToHttp(mx, mxc, useAuthentication, 48, 48, 'crop') : undefined; }, [pmp, collapse, profile.avatarUrl, senderId, mx, room, useAuthentication]); - const displayName = useMemo( - () => pmp?.displayname || senderDisplayName, - [pmp, senderDisplayName] - ); - const cachedAvatar = useBlobCache(avatarUrl ?? undefined); // UI State @@ -423,8 +418,32 @@ function MessageInternal( const [mobileOptionsOpen, setMobileOptionsOpen] = useState(false); const optionsRef = useRef(null); + const [showPronouns] = useSetting(settingsAtom, 'showPronouns'); + const [parsePronouns] = useSetting(settingsAtom, 'parsePronouns'); + const [useRightBubbles] = useSetting(settingsAtom, 'useRightBubbles'); + const { cleanedDisplayName, inlinePronoun } = useMemo(() => { + const rawName = pmp?.displayname || senderDisplayName || ''; + return getParsedPronouns(rawName, parsePronouns); + }, [pmp, senderDisplayName, parsePronouns]); + + const mergedPronouns = useMemo(() => { + const existing = profile.pronouns ? [...profile.pronouns] : []; + + if (inlinePronoun) { + const isDupe = existing.some((p) => p.summary?.toLowerCase() === inlinePronoun); + + if (!isDupe) { + existing.push({ + summary: inlinePronoun, + language: 'en', + }); + } + } + + return existing; + }, [profile.pronouns, inlinePronoun]); useEffect(() => { if (!mobileOptionsOpen) return undefined; @@ -457,11 +476,11 @@ function MessageInternal( onClick={onUsernameClick} > - {displayName} + {cleanedDisplayName} {showPronouns && ( - + )} {tagIconSrc && } @@ -500,7 +519,7 @@ function MessageInternal( } /> @@ -976,7 +995,7 @@ function MessageInternal( autoFocus value={nickDraft} onChange={(e) => setNickDraft(e.target.value)} - placeholder={displayName} + placeholder={cleanedDisplayName} onKeyDown={(e) => { if (e.key === 'Enter') { setNickname(senderId, nickDraft || undefined, mx); diff --git a/src/app/features/settings/cosmetics/Cosmetics.tsx b/src/app/features/settings/cosmetics/Cosmetics.tsx index d1a65b6baa..f0bf1d8fc5 100644 --- a/src/app/features/settings/cosmetics/Cosmetics.tsx +++ b/src/app/features/settings/cosmetics/Cosmetics.tsx @@ -165,6 +165,7 @@ function IdentityCosmetics() { 'legacyUsernameColor' ); const [showPronouns, setShowPronouns] = useSetting(settingsAtom, 'showPronouns'); + const [parsePronouns, setParsePronouns] = useSetting(settingsAtom, 'parsePronouns'); const [renderGlobalColors, setRenderGlobalColors] = useSetting( settingsAtom, 'renderGlobalNameColors' @@ -196,6 +197,13 @@ function IdentityCosmetics() { after={} /> + + } + /> + (); + +export function getParsedPronouns(rawName: string, parseSetting: boolean) { + if (!parseSetting || !rawName) { + return { cleanedDisplayName: rawName, inlinePronoun: null }; + } + + if (pronounParseCache.has(rawName)) { + return pronounParseCache.get(rawName)!; + } + + // match text like (she/her) or [he/they/them] but not (hi) + const regex = /[([]?([a-zA-Z]+\/[a-zA-Z]+(?:\/[a-zA-Z]+)?)[)\]]?/; + const match = rawName.match(regex); + + let result: { cleanedDisplayName: string; inlinePronoun: string | null } = { + cleanedDisplayName: rawName, + inlinePronoun: null, + }; + + if (match) { + let strippedName = rawName.replace(match[0], '').trim(); + if (!strippedName || strippedName === '') { + strippedName = rawName; + } + result = { + cleanedDisplayName: strippedName, + inlinePronoun: match[1].toLowerCase().slice(0, 16), + }; + } + + if (pronounParseCache.size > 1000) { + pronounParseCache.clear(); + } + + pronounParseCache.set(rawName, result); + return result; +}