From 79cd9bd2bf2844dab23ef0bba7d9cf1c977f77b8 Mon Sep 17 00:00:00 2001 From: 7w1 Date: Sun, 24 May 2026 22:29:03 -0500 Subject: [PATCH 1/2] feat: pronoun pill hover details --- .changeset/add-hover-pronouns-list.md | 5 ++ src/app/features/room/message/Message.tsx | 75 ++++++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 .changeset/add-hover-pronouns-list.md diff --git a/.changeset/add-hover-pronouns-list.md b/.changeset/add-hover-pronouns-list.md new file mode 100644 index 000000000..a302a53be --- /dev/null +++ b/.changeset/add-hover-pronouns-list.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Adds the ability to hover/tap over `...` to see the rest of someone's pronouns. diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index ca1517de8..94b51f1dc 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -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'; @@ -274,6 +277,72 @@ 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; +}; + +function MorePronounsPill({ pronouns, tagColor }: MorePronounsPillProps) { + const [anchor, setAnchor] = useState(); + + const toggleAnchor = (target: HTMLElement) => { + setAnchor((prev) => (prev ? undefined : target.getBoundingClientRect())); + }; + + const handleClick: MouseEventHandler = (e) => { + e.stopPropagation(); + toggleAnchor(e.currentTarget); + }; + + const handleKeyDown: KeyboardEventHandler = (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, 16)).join(', '); + + const tooltipContent = ( + + {tooltipText} + + ); + + return ( + <> + + {(triggerRef) => ( + } + style={{ color: tagColor, cursor: 'help' }} + onClick={handleClick} + onKeyDown={handleKeyDown} + role="button" + tabIndex={0} + > + ... + + )} + + {anchor && ( + + {null} + + )} + + ); +} + /** * Component to render pronouns in the chat timeline. * It also filters them. @@ -320,7 +389,9 @@ const Pronouns = as< {clamp(p.summary, 16)} ))} - {visiblePronouns.length > limit && ...} + {visiblePronouns.length > limit && ( + + )} ); }); From a2f2707a3722a7dd70af9f2529c5dfcbe5446584 Mon Sep 17 00:00:00 2001 From: 7w1 Date: Sun, 24 May 2026 22:54:54 -0500 Subject: [PATCH 2/2] feat: customizable pronoun pill limits --- ...add-ability-to-customize-pronoun-limits.md | 5 + .changeset/add-hover-pronouns-list.md | 2 +- src/app/features/room/message/Message.tsx | 16 ++- .../features/settings/cosmetics/Cosmetics.tsx | 119 +++++++++++++++++- src/app/features/settings/settingsLink.ts | 2 + src/app/state/settings.ts | 12 ++ 6 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 .changeset/add-ability-to-customize-pronoun-limits.md diff --git a/.changeset/add-ability-to-customize-pronoun-limits.md b/.changeset/add-ability-to-customize-pronoun-limits.md new file mode 100644 index 000000000..7b5c04bdf --- /dev/null +++ b/.changeset/add-ability-to-customize-pronoun-limits.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Added configurable max pronoun pill count and max pill length rendering settings. diff --git a/.changeset/add-hover-pronouns-list.md b/.changeset/add-hover-pronouns-list.md index a302a53be..4d71893f6 100644 --- a/.changeset/add-hover-pronouns-list.md +++ b/.changeset/add-hover-pronouns-list.md @@ -1,5 +1,5 @@ --- -default: patch +default: minor --- Adds the ability to hover/tap over `...` to see the rest of someone's pronouns. diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 94b51f1dc..a0bc3e1d1 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -280,9 +280,10 @@ const clamp = (str: string, len: number) => (str.length > len ? `${str.slice(0, type MorePronounsPillProps = { pronouns: PronounSet[]; tagColor: string; + maxPillLength: number; }; -function MorePronounsPill({ pronouns, tagColor }: MorePronounsPillProps) { +function MorePronounsPill({ pronouns, tagColor, maxPillLength }: MorePronounsPillProps) { const [anchor, setAnchor] = useState(); const toggleAnchor = (target: HTMLElement) => { @@ -310,7 +311,7 @@ function MorePronounsPill({ pronouns, tagColor }: MorePronounsPillProps) { return () => document.removeEventListener('click', dismiss); }, [anchor]); - const tooltipText = pronouns.map((p) => clamp(p.summary, 16)).join(', '); + const tooltipText = pronouns.map((p) => clamp(p.summary, maxPillLength)).join(', '); const tooltipContent = ( @@ -375,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) { @@ -386,11 +388,15 @@ const Pronouns = as< {visiblePronouns.slice(0, limit).map((p) => ( - {clamp(p.summary, 16)} + {clamp(p.summary, maxPillLength)} ))} {visiblePronouns.length > limit && ( - + )} ); diff --git a/src/app/features/settings/cosmetics/Cosmetics.tsx b/src/app/features/settings/cosmetics/Cosmetics.tsx index 49ab59374..bb938a855 100644 --- a/src/app/features/settings/cosmetics/Cosmetics.tsx +++ b/src/app/features/settings/cosmetics/Cosmetics.tsx @@ -1,5 +1,5 @@ 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, @@ -7,13 +7,16 @@ import { 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'; @@ -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 = (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 = (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 ( + + ); +} + +function PronounPillMaxLengthInput({ disabled }: { disabled: boolean }) { + const [maxLength, setMaxLength] = useSetting(settingsAtom, 'pronounPillMaxLength'); + const [inputValue, setInputValue] = useState(maxLength.toString()); + + const handleChange: ChangeEventHandler = (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 = (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 ( + + ); +} + const emojiSizeItems = [ { id: 'none', name: 'None (Same size as text)' }, { id: 'extraSmall', name: 'Extra Small' }, @@ -282,6 +373,32 @@ function IdentityCosmetics() { after={} /> + + } + /> + + + } + /> + = 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':