diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index d93c7bb91a..d533d70f03 100644 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -29,6 +29,7 @@ import { Autocomplete as AutocompleteBase, AutocompleteItemEmoji, AutocompleteItemProfile, + type LocalSource, parseAutocompleteItemType, useAutocomplete, } from '#/components/Autocomplete' @@ -101,6 +102,8 @@ export type ComposerProps = Omit< undefined >['placement'] disableEmojiFacets?: boolean + /** Local datasets weighted above remote results in the mention typeahead. */ + localSources?: LocalSource[] } export function Composer({ @@ -117,6 +120,7 @@ export function Composer({ autocompletePlacement, defaultValue, disableEmojiFacets = !IS_WEB, + localSources, ...rest }: ComposerProps) { const {theme: t, fonts} = useAlf() @@ -377,6 +381,7 @@ export function Composer({ inverted={autocompletePlacement?.startsWith('top')} sift={sift} activeFacet={activeFacet} + localSources={localSources} onDismiss={() => setActiveFacet(null)} /> )} @@ -392,16 +397,20 @@ function AutocompleteInner({ inverted, sift, activeFacet, + localSources, onDismiss, }: { inverted?: boolean sift: UseSiftReturn activeFacet: TapperActiveFacet + localSources?: LocalSource[] onDismiss: () => void }) { + const itemType = parseAutocompleteItemType(activeFacet.type) const {items} = useAutocomplete({ - type: parseAutocompleteItemType(activeFacet.type), + type: itemType, query: activeFacet.value, + sources: itemType === 'profile' ? localSources : undefined, }) useEffect(() => { diff --git a/src/features/autocomplete/useConvoMembersSource.ts b/src/features/autocomplete/useConvoMembersSource.ts new file mode 100644 index 0000000000..567a21369c --- /dev/null +++ b/src/features/autocomplete/useConvoMembersSource.ts @@ -0,0 +1,26 @@ +import {useConvo} from '#/state/messages/convo' +import {useSession} from '#/state/session' +import {type LocalSource} from '#/components/Autocomplete/types' + +/** + * Members of the active conversation as an autocomplete LocalSource, so the + * mention typeahead in a chat prioritizes the people in it. Must be used + * within a ConvoProvider. + */ +export function useConvoMembersSource(): LocalSource { + const convo = useConvo() + const {currentAccount} = useSession() + const members = convo.convo?.members ?? [] + + return { + key: 'convo-members', + items: members + .filter(member => member.did !== currentAccount?.did) + .map(member => ({ + key: member.did, + type: 'profile' as const, + value: '@' + member.handle, + profile: member, + })), + } +} diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx index c5976fb688..5725716469 100644 --- a/src/screens/Messages/components/MessageComposer.tsx +++ b/src/screens/Messages/components/MessageComposer.tsx @@ -37,6 +37,8 @@ import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} fro import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' +import {useConvoMembersSource} from '#/features/autocomplete/useConvoMembersSource' +import {useFollowsSource} from '#/features/autocomplete/useFollowsSource' import {type MessageEmbedState} from './MessageInputEmbed' const MIN_HEIGHT = 40 @@ -71,6 +73,8 @@ export function MessageComposer({ const [text, setText] = useState(getDraft) useSaveMessageDraft(text) + const convoMembers = useConvoMembersSource() + const follows = useFollowsSource() useEffect(() => { if (!replyTo) return @@ -255,6 +259,7 @@ export function MessageComposer({ : l({message: 'Message', context: 'description'}) } autocompletePlacement="top-start" + localSources={[convoMembers, follows]} internalApiRef={composerInternalApiRef} defaultValue={text} editable={editable}