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/add-ability-to-customize-pronoun-limits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Added configurable max pronoun pill count and max pill length rendering settings.
5 changes: 5 additions & 0 deletions .changeset/add-hover-pronouns-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Adds the ability to hover/tap over `...` to see the rest of someone's pronouns.
85 changes: 81 additions & 4 deletions src/app/features/room/message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import {
MenuItem,
PopOut,
Text,
Tooltip,
TooltipProvider,
as,
config,
toRem,
} from 'folds';
import type { MouseEventHandler, MouseEvent, ReactNode } from 'react';
import type { KeyboardEventHandler, MouseEventHandler, MouseEvent, ReactNode } from 'react';
import { memo, useCallback, useRef, useState, useEffect, useMemo } from 'react';
import FocusTrap from 'focus-trap-react';
import { useHover, useFocusWithin } from 'react-aria';
Expand Down Expand Up @@ -274,6 +277,73 @@ function useMobileDoubleTap(callback: () => void, delay = 300) {

const clamp = (str: string, len: number) => (str.length > len ? `${str.slice(0, len)}...` : str);

type MorePronounsPillProps = {
pronouns: PronounSet[];
tagColor: string;
maxPillLength: number;
};

function MorePronounsPill({ pronouns, tagColor, maxPillLength }: MorePronounsPillProps) {
const [anchor, setAnchor] = useState<RectCords | undefined>();

const toggleAnchor = (target: HTMLElement) => {
setAnchor((prev) => (prev ? undefined : target.getBoundingClientRect()));
};

const handleClick: MouseEventHandler<HTMLElement> = (e) => {
e.stopPropagation();
toggleAnchor(e.currentTarget);
};

const handleKeyDown: KeyboardEventHandler<HTMLElement> = (e) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
e.preventDefault();
e.stopPropagation();
toggleAnchor(e.currentTarget);
};

// On mobile, tapping the pill pins the tooltip open.
// Tapping anywhere else dismisses it.
useEffect(() => {
if (!anchor) return undefined;
const dismiss = () => setAnchor(undefined);
document.addEventListener('click', dismiss, { once: true });
return () => document.removeEventListener('click', dismiss);
}, [anchor]);

const tooltipText = pronouns.map((p) => clamp(p.summary, maxPillLength)).join(', ');

const tooltipContent = (
<Tooltip style={{ maxWidth: toRem(250) }}>
<Text size="T200">{tooltipText}</Text>
</Tooltip>
);

return (
<>
<TooltipProvider position="Top" tooltip={tooltipContent}>
{(triggerRef) => (
<PronounPill
ref={triggerRef as React.Ref<HTMLSpanElement>}
style={{ color: tagColor, cursor: 'help' }}
onClick={handleClick}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
>
...
</PronounPill>
)}
</TooltipProvider>
{anchor && (
<PopOut anchor={anchor} position="Top" align="Center" content={tooltipContent}>
{null}
</PopOut>
)}
</>
);
}

/**
* Component to render pronouns in the chat timeline.
* It also filters them.
Expand Down Expand Up @@ -306,7 +376,8 @@ const Pronouns = as<
selectedLanguages
);

const limit = mobileOrTablet() ? 1 : 3;
const limit = getSettings().pronounPillMaxCount ?? 3;
const maxPillLength = getSettings().pronounPillMaxLength ?? 16;

// if language specific pronouns can't be found matching the filter return unfiltered
if (visiblePronouns.length === 0) {
Expand All @@ -317,10 +388,16 @@ const Pronouns = as<
<AsPronouns {...props} ref={ref}>
{visiblePronouns.slice(0, limit).map((p) => (
<PronounPill key={p.summary} style={{ color: tagColor }}>
{clamp(p.summary, 16)}
{clamp(p.summary, maxPillLength)}
</PronounPill>
))}
{visiblePronouns.length > limit && <PronounPill style={{ color: tagColor }}>...</PronounPill>}
{visiblePronouns.length > limit && (
<MorePronounsPill
pronouns={visiblePronouns.slice(limit)}
tagColor={tagColor}
maxPillLength={maxPillLength}
/>
)}
</AsPronouns>
);
});
Expand Down
119 changes: 118 additions & 1 deletion src/app/features/settings/cosmetics/Cosmetics.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { useEffect, useRef, useState } from 'react';
import type { MouseEventHandler } from 'react';
import type { ChangeEventHandler, KeyboardEventHandler, MouseEventHandler } from 'react';
import type { RectCords } from 'folds';
import {
Box,
Button,
config,
Icon,
Icons,
Input,
Menu,
MenuItem,
PopOut,
Scroll,
Switch,
Text,
toRem,
} from 'folds';
import { isKeyHotkey } from 'is-hotkey';
import FocusTrap from 'focus-trap-react';
import { PageContent } from '$components/page';
import { SequenceCard } from '$components/sequence-card';
Expand All @@ -27,6 +30,94 @@ import { SettingsSectionPage } from '../SettingsSectionPage';
import { Appearance } from './Themes';
import { LanguageSpecificPronouns } from './LanguageSpecificPronouns';

function PronounPillMaxCountInput({ disabled }: { disabled: boolean }) {
const [maxCount, setMaxCount] = useSetting(settingsAtom, 'pronounPillMaxCount');
const [inputValue, setInputValue] = useState(maxCount.toString());

const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
const val = evt.target.value;
setInputValue(val);

const parsed = Number.parseInt(val, 10);
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 10) {
setMaxCount(parsed);
}
};

const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => {
if (isKeyHotkey('escape', evt)) {
evt.stopPropagation();
setInputValue(maxCount.toString());
(evt.target as HTMLInputElement).blur();
}

if (isKeyHotkey('enter', evt)) {
(evt.target as HTMLInputElement).blur();
}
};

return (
<Input
style={{ width: toRem(80) }}
variant={Number.parseInt(inputValue, 10) === maxCount ? 'Secondary' : 'Success'}
size="300"
radii="300"
type="number"
min="1"
max="10"
value={inputValue}
onChange={handleChange}
onKeyDown={handleKeyDown}
disabled={disabled}
outlined
/>
);
}

function PronounPillMaxLengthInput({ disabled }: { disabled: boolean }) {
const [maxLength, setMaxLength] = useSetting(settingsAtom, 'pronounPillMaxLength');
const [inputValue, setInputValue] = useState(maxLength.toString());

const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
const val = evt.target.value;
setInputValue(val);

const parsed = Number.parseInt(val, 10);
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 64) {
setMaxLength(parsed);
}
};

const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => {
if (isKeyHotkey('escape', evt)) {
evt.stopPropagation();
setInputValue(maxLength.toString());
(evt.target as HTMLInputElement).blur();
}

if (isKeyHotkey('enter', evt)) {
(evt.target as HTMLInputElement).blur();
}
};

return (
<Input
style={{ width: toRem(80) }}
variant={Number.parseInt(inputValue, 10) === maxLength ? 'Secondary' : 'Success'}
size="300"
radii="300"
type="number"
min="1"
max="64"
value={inputValue}
onChange={handleChange}
onKeyDown={handleKeyDown}
disabled={disabled}
outlined
/>
);
}

const emojiSizeItems = [
{ id: 'none', name: 'None (Same size as text)' },
{ id: 'extraSmall', name: 'Extra Small' },
Expand Down Expand Up @@ -282,6 +373,32 @@ function IdentityCosmetics() {
after={<Switch variant="Primary" value={showPronouns} onChange={setShowPronouns} />}
/>
</SequenceCard>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
style={{ opacity: showPronouns ? 1 : 0.5 }}
>
<SettingTile
title="Max Pronoun Pills"
focusId="pronoun-pill-max-count"
description="Maximum number of pronoun pills shown per user in the timeline. Additional pronouns appear behind the ... pill."
after={<PronounPillMaxCountInput disabled={!showPronouns} />}
/>
</SequenceCard>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
style={{ opacity: showPronouns ? 1 : 0.5 }}
>
<SettingTile
title="Max Pronoun Pill Length"
focusId="pronoun-pill-max-length"
description="Maximum characters shown in each pronoun pill before truncation."
after={<PronounPillMaxLengthInput disabled={!showPronouns} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Pronoun Pills for All"
Expand Down
2 changes: 2 additions & 0 deletions src/app/features/settings/settingsLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ const settingsLinkFocusIdsBySection: Record<SettingsSectionId, readonly string[]
'theme-chat-auto-any',
'theme-import-open',
'theme-local-sync-system',
'pronoun-pill-max-count',
'pronoun-pill-max-length',
'pronoun-pills-for-all',
'reduced-motion',
'render-global-username-colors',
Expand Down
12 changes: 12 additions & 0 deletions src/app/state/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ export interface Settings {
privacyBlurEmotes: boolean;
showPronouns: boolean;
parsePronouns: boolean;
pronounPillMaxCount: number;
pronounPillMaxLength: number;
renderGlobalNameColors: boolean;
renderUserCards: RenderUserCardsMode;
filterPronounsBasedOnLanguage?: boolean;
Expand Down Expand Up @@ -273,6 +275,8 @@ export const defaultSettings: Settings = {
privacyBlurEmotes: false,
showPronouns: true,
parsePronouns: true,
pronounPillMaxCount: 3,
pronounPillMaxLength: 16,
renderGlobalNameColors: true,
renderUserCards: 'both',
renderRoomColors: true,
Expand Down Expand Up @@ -516,6 +520,14 @@ function sanitizeSettingsKey(key: keyof Settings, val: unknown): unknown {
return typeof val === 'string' && JUMBO_EMOJI_VALUES.has(val as JumboEmojiSize)
? val
: undefined;
case 'pronounPillMaxCount':
return typeof val === 'number' && Number.isInteger(val) && val >= 1 && val <= 10
? val
: undefined;
case 'pronounPillMaxLength':
return typeof val === 'number' && Number.isInteger(val) && val >= 1 && val <= 64
? val
: undefined;
case 'themeRemoteManualKind':
case 'themeRemoteLightKind':
case 'themeRemoteDarkKind':
Expand Down
Loading