From 9b51b8f0f0c8dbf91d1c91616cce9998e8df0133 Mon Sep 17 00:00:00 2001 From: trevor-signal <131492920+trevor-signal@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:29:17 -0500 Subject: [PATCH] Keep mention repository up to date --- ts/components/CompositionInput.dom.tsx | 20 ++++++++++++-------- ts/quill/memberRepository.std.ts | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/ts/components/CompositionInput.dom.tsx b/ts/components/CompositionInput.dom.tsx index 66741e893a..b2628f4e10 100644 --- a/ts/components/CompositionInput.dom.tsx +++ b/ts/components/CompositionInput.dom.tsx @@ -51,6 +51,7 @@ import { getDeltaToRestartEmoji, insertEmojiOps, insertFormattingAndMentionsOps, + isInsertMentionOp, } from '../quill/util.dom.js'; import { SignalClipboard } from '../quill/signal-clipboard/index.dom.js'; import { DirectionalBlot } from '../quill/block/blot.dom.js'; @@ -754,6 +755,9 @@ export function CompositionInput(props: Props): React.ReactElement { if (ops === undefined) { return; } + if (!ops.some(isInsertMentionOp)) { + return; + } const currentMemberAcis = currentMembers .map(m => m.serviceId) @@ -766,17 +770,17 @@ export function CompositionInput(props: Props): React.ReactElement { quill.updateContents(newDelta as any); }; - const memberIds = sortedGroupMembers ? sortedGroupMembers.map(m => m.id) : []; + const memberIdList = React.useMemo(() => { + return JSON.stringify(sortedGroupMembers?.map(mem => mem.id)); + }, [sortedGroupMembers]); + const previousMemberIdList = usePrevious(undefined, memberIdList); React.useEffect(() => { memberRepositoryRef.current.updateMembers(sortedGroupMembers || []); - removeStaleMentions(sortedGroupMembers || []); - // We are still depending on members, but ESLint can't tell - // Comparing the actual members list does not work for a couple reasons: - // * Arrays with the same objects are not "equal" to React - // * We only care about added/removed members, ignoring other attributes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(memberIds)]); + if (memberIdList !== previousMemberIdList) { + removeStaleMentions(sortedGroupMembers || []); + } + }, [sortedGroupMembers, memberIdList, previousMemberIdList]); // Placing all of these callbacks inside of a ref since Quill is not able // to re-render. We want to make sure that all these callbacks are fresh diff --git a/ts/quill/memberRepository.std.ts b/ts/quill/memberRepository.std.ts index 696f675056..65146b7e36 100644 --- a/ts/quill/memberRepository.std.ts +++ b/ts/quill/memberRepository.std.ts @@ -70,20 +70,26 @@ const FUSE_OPTIONS = { }; export class MemberRepository { - #members: ReadonlyArray; + #rawMembers: ReadonlyArray; + #members: ReadonlyArray | null = null; #isFuseReady = false; #fuse = new Fuse([], FUSE_OPTIONS); constructor(conversations: ReadonlyArray = []) { - this.#members = _toMembers(conversations); + this.#rawMembers = conversations; } updateMembers(conversations: ReadonlyArray): void { - this.#members = _toMembers(conversations); + this.#rawMembers = conversations; + this.#members = null; this.#isFuseReady = false; } getMembers(omitId?: string): ReadonlyArray { + if (!this.#members) { + this.#members = _toMembers(this.#rawMembers); + } + if (omitId) { return this.#members.filter(({ id }) => id !== omitId); } @@ -93,19 +99,19 @@ export class MemberRepository { getMemberById(id?: string): MemberType | undefined { return id - ? this.#members.find(({ id: memberId }) => memberId === id) + ? this.getMembers().find(({ id: memberId }) => memberId === id) : undefined; } getMemberByAci(aci?: AciString): MemberType | undefined { return aci - ? this.#members.find(({ aci: memberAci }) => memberAci === aci) + ? this.getMembers().find(({ aci: memberAci }) => memberAci === aci) : undefined; } search(pattern: string, omitId?: string): ReadonlyArray { if (!this.#isFuseReady) { - this.#fuse.setCollection(this.#members); + this.#fuse.setCollection(this.getMembers()); this.#isFuseReady = true; }