Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/pronoun-pills.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 29 additions & 10 deletions src/app/features/room/message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -423,8 +418,32 @@ function MessageInternal(

const [mobileOptionsOpen, setMobileOptionsOpen] = useState(false);
const optionsRef = useRef<HTMLDivElement>(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;
Expand Down Expand Up @@ -457,11 +476,11 @@ function MessageInternal(
onClick={onUsernameClick}
>
<Text as="span" size={messageLayout === MessageLayout.Bubble ? 'T300' : 'T400'} truncate>
<UsernameBold>{displayName}</UsernameBold>
<UsernameBold>{cleanedDisplayName}</UsernameBold>
</Text>
</Username>
{showPronouns && (
<Pronouns pronouns={profile.pronouns} tagColor={usernameColor ?? 'currentColor'} />
<Pronouns pronouns={mergedPronouns} tagColor={usernameColor ?? 'currentColor'} />
)}
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
Expand Down Expand Up @@ -500,7 +519,7 @@ function MessageInternal(
<UserAvatar
userId={senderId}
src={cachedAvatar}
alt={displayName}
alt={cleanedDisplayName}
renderFallback={() => <Icon size="200" src={Icons.User} filled />}
/>
</Avatar>
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/app/features/settings/cosmetics/Cosmetics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -196,6 +197,13 @@ function IdentityCosmetics() {
after={<Switch variant="Primary" value={showPronouns} onChange={setShowPronouns} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Pronoun Pills for All"
description="Attempts to convert pronouns in names into pills (e.g. [they/them] or (it/its) turns into a pill)."
after={<Switch variant="Primary" value={parsePronouns} onChange={setParsePronouns} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Render Global Username Colors"
Expand Down
2 changes: 2 additions & 0 deletions src/app/state/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface Settings {
privacyBlurAvatars: boolean;
privacyBlurEmotes: boolean;
showPronouns: boolean;
parsePronouns: boolean;
renderGlobalNameColors: boolean;
filterPronounsBasedOnLanguage?: boolean;
filterPronounsLanguages?: string[];
Expand Down Expand Up @@ -154,6 +155,7 @@ const defaultSettings: Settings = {
privacyBlurAvatars: false,
privacyBlurEmotes: false,
showPronouns: true,
parsePronouns: true,
renderGlobalNameColors: true,
renderRoomColors: true,
renderRoomFonts: true,
Expand Down
42 changes: 42 additions & 0 deletions src/app/utils/pronouns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,45 @@ export function filterPronounsByLanguage(

return filteredPronouns;
}

const pronounParseCache = new Map<
string,
{ cleanedDisplayName: string; inlinePronoun: string | null }
>();

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;
}
Loading