diff options
-rw-r--r-- | src/components/Dialog/index.tsx | 7 | ||||
-rw-r--r-- | src/components/Dialog/index.web.tsx | 21 | ||||
-rw-r--r-- | src/components/dms/NewChat.tsx | 278 | ||||
-rw-r--r-- | src/components/dms/NewChatDialog/TextInput.tsx | 1 | ||||
-rw-r--r-- | src/components/dms/NewChatDialog/TextInput.web.tsx | 1 | ||||
-rw-r--r-- | src/components/dms/NewChatDialog/index.tsx | 496 | ||||
-rw-r--r-- | src/screens/Messages/List/index.tsx | 2 | ||||
-rw-r--r-- | src/state/queries/actor-autocomplete.ts | 3 | ||||
-rw-r--r-- | src/state/queries/profile-follows.ts | 13 |
9 files changed, 530 insertions, 292 deletions
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index b5258c02b..b88159613 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -1,5 +1,5 @@ import React, {useImperativeHandle} from 'react' -import {Dimensions, Pressable, View} from 'react-native' +import {Dimensions, Pressable, StyleProp, View, ViewStyle} from 'react-native' import Animated, {useAnimatedStyle} from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import BottomSheet, { @@ -257,9 +257,10 @@ export const ScrollableInner = React.forwardRef< export const InnerFlatList = React.forwardRef< BottomSheetFlatListMethods, - BottomSheetFlatListProps<any> + BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>} >(function InnerFlatList({style, contentContainerStyle, ...props}, ref) { const insets = useSafeAreaInsets() + return ( <BottomSheetFlatList keyboardShouldPersistTaps="handled" @@ -276,6 +277,8 @@ export const InnerFlatList = React.forwardRef< a.h_full, { marginTop: 40, + borderTopLeftRadius: 40, + borderTopRightRadius: 40, }, flatten(style), ]} diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 4cb4e7570..35d807b4b 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -2,8 +2,10 @@ import React, {useImperativeHandle} from 'react' import { FlatList, FlatListProps, + StyleProp, TouchableWithoutFeedback, View, + ViewStyle, } from 'react-native' import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated' import {msg} from '@lingui/macro' @@ -199,18 +201,21 @@ export const ScrollableInner = Inner export const InnerFlatList = React.forwardRef< FlatList, - FlatListProps<any> & {label: string} ->(function InnerFlatList({label, style, ...props}, ref) { + FlatListProps<any> & {label: string} & {webInnerStyle?: StyleProp<ViewStyle>} +>(function InnerFlatList({label, style, webInnerStyle, ...props}, ref) { const {gtMobile} = useBreakpoints() return ( <Inner label={label} - // @ts-ignore web only -sfn - style={{ - paddingHorizontal: 0, - maxHeight: 'calc(-36px + 100vh)', - overflow: 'hidden', - }}> + style={[ + // @ts-ignore web only -sfn + { + paddingHorizontal: 0, + maxHeight: 'calc(-36px + 100vh)', + overflow: 'hidden', + }, + webInnerStyle, + ]}> <FlatList ref={ref} style={[gtMobile ? a.px_2xl : a.px_xl, flatten(style)]} diff --git a/src/components/dms/NewChat.tsx b/src/components/dms/NewChat.tsx deleted file mode 100644 index 3975c0c5d..000000000 --- a/src/components/dms/NewChat.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import React, {useCallback, useMemo, useRef, useState} from 'react' -import {Keyboard, View} from 'react-native' -import {AppBskyActorDefs, moderateProfile} from '@atproto/api' -import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {sanitizeDisplayName} from '#/lib/strings/display-names' -import {sanitizeHandle} from '#/lib/strings/handles' -import {isWeb} from '#/platform/detection' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' -import {useSession} from '#/state/session' -import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' -import {FAB} from '#/view/com/util/fab/FAB' -import * as Toast from '#/view/com/util/Toast' -import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, useTheme, web} from '#/alf' -import * as Dialog from '#/components/Dialog' -import * as TextField from '#/components/forms/TextField' -import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {Button} from '../Button' -import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope' -import {ListMaybePlaceholder} from '../Lists' -import {Text} from '../Typography' -import {canBeMessaged} from './util' - -export function NewChat({ - control, - onNewChat, -}: { - control: Dialog.DialogControlProps - onNewChat: (chatId: string) => void -}) { - const t = useTheme() - const {_} = useLingui() - - const {mutate: createChat} = useGetConvoForMembers({ - onSuccess: data => { - onNewChat(data.convo.id) - }, - onError: error => { - Toast.show(error.message) - }, - }) - - const onCreateChat = useCallback( - (did: string) => { - control.close(() => createChat([did])) - }, - [control, createChat], - ) - - return ( - <> - <FAB - testID="newChatFAB" - onPress={control.open} - icon={<Plus size="lg" fill={t.palette.white} />} - accessibilityRole="button" - accessibilityLabel={_(msg`New chat`)} - accessibilityHint="" - /> - - <Dialog.Outer - control={control} - testID="newChatDialog" - nativeOptions={{sheet: {snapPoints: ['100%']}}}> - <Dialog.Handle /> - <SearchablePeopleList onCreateChat={onCreateChat} /> - </Dialog.Outer> - </> - ) -} - -function SearchablePeopleList({ - onCreateChat, -}: { - onCreateChat: (did: string) => void -}) { - const t = useTheme() - const {_} = useLingui() - const moderationOpts = useModerationOpts() - const control = Dialog.useDialogContext() - const listRef = useRef<BottomSheetFlatListMethods>(null) - const {currentAccount} = useSession() - - const [searchText, setSearchText] = useState('') - - const { - data: actorAutocompleteData, - isFetching, - isError, - refetch, - } = useActorAutocompleteQuery(searchText, true) - - const renderItem = useCallback( - ({item: profile}: {item: AppBskyActorDefs.ProfileView}) => { - if (!moderationOpts) return null - - const moderation = moderateProfile(profile, moderationOpts) - - const disabled = !canBeMessaged(profile) - const handle = sanitizeHandle(profile.handle, '@') - - return ( - <Button - label={profile.displayName || sanitizeHandle(profile.handle)} - onPress={() => !disabled && onCreateChat(profile.did)}> - {({hovered, pressed, focused}) => ( - <View - style={[ - a.flex_1, - a.px_md, - a.py_sm, - a.gap_md, - a.align_center, - a.flex_row, - a.rounded_sm, - disabled - ? {opacity: 0.5} - : pressed || focused - ? t.atoms.bg_contrast_25 - : hovered - ? t.atoms.bg_contrast_50 - : t.atoms.bg, - ]}> - <UserAvatar - size={40} - avatar={profile.avatar} - moderation={moderation.ui('avatar')} - type={profile.associated?.labeler ? 'labeler' : 'user'} - /> - <View style={{flex: 1}}> - <Text - style={[t.atoms.text, a.font_bold, a.leading_snug]} - numberOfLines={1}> - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} - </Text> - <Text style={t.atoms.text_contrast_high} numberOfLines={2}> - {disabled ? ( - <Trans>{handle} can't be messaged</Trans> - ) : ( - handle - )} - </Text> - </View> - </View> - )} - </Button> - ) - }, - [ - moderationOpts, - onCreateChat, - t.atoms.bg_contrast_25, - t.atoms.bg_contrast_50, - t.atoms.bg, - t.atoms.text, - t.atoms.text_contrast_high, - ], - ) - - const listHeader = useMemo(() => { - return ( - <View style={[a.relative, a.mb_lg]}> - {/* cover top corners */} - <View - style={[ - a.absolute, - a.inset_0, - { - borderBottomLeftRadius: 8, - borderBottomRightRadius: 8, - }, - t.atoms.bg, - ]} - /> - <Text - style={[ - a.text_2xl, - a.font_bold, - a.leading_tight, - a.pb_lg, - web(a.pt_lg), - ]}> - <Trans>Start a new chat</Trans> - </Text> - <TextField.Root> - <TextField.Icon icon={Search} /> - <Dialog.Input - label={_(msg`Search profiles`)} - placeholder={_(msg`Search`)} - value={searchText} - onChangeText={text => { - setSearchText(text) - listRef.current?.scrollToOffset({offset: 0, animated: false}) - }} - returnKeyType="search" - clearButtonMode="while-editing" - maxLength={50} - onKeyPress={({nativeEvent}) => { - if (nativeEvent.key === 'Escape') { - control.close() - } - }} - autoCorrect={false} - autoComplete="off" - autoCapitalize="none" - autoFocus - /> - </TextField.Root> - <Dialog.Close /> - </View> - ) - }, [t.atoms.bg, _, control, searchText]) - - const dataWithoutSelf = useMemo(() => { - return ( - actorAutocompleteData?.filter( - profile => profile.did !== currentAccount?.did, - ) ?? [] - ) - }, [actorAutocompleteData, currentAccount?.did]) - - return ( - <Dialog.InnerFlatList - ref={listRef} - data={dataWithoutSelf} - renderItem={renderItem} - ListHeaderComponent={ - <> - {listHeader} - {searchText.length === 0 ? ( - <View style={[a.pt_4xl, a.align_center, a.px_lg]}> - <Envelope width={64} fill={t.palette.contrast_200} /> - <Text - style={[ - a.text_lg, - a.text_center, - a.mt_md, - t.atoms.text_contrast_low, - ]}> - <Trans>Search for someone to start a conversation with.</Trans> - </Text> - </View> - ) : ( - !actorAutocompleteData?.length && ( - <ListMaybePlaceholder - isLoading={isFetching} - isError={isError} - onRetry={refetch} - hideBackButton={true} - emptyType="results" - sideBorders={false} - topBorder={false} - emptyMessage={ - isError - ? _(msg`No search results found for "${searchText}".`) - : _(msg`Could not load profiles. Please try again later.`) - } - /> - ) - )} - </> - } - stickyHeaderIndices={[0]} - keyExtractor={(item: AppBskyActorDefs.ProfileView) => item.did} - // @ts-expect-error web only - style={isWeb && {minHeight: '100vh'}} - onScrollBeginDrag={() => Keyboard.dismiss()} - /> - ) -} diff --git a/src/components/dms/NewChatDialog/TextInput.tsx b/src/components/dms/NewChatDialog/TextInput.tsx new file mode 100644 index 000000000..b4e77e3e0 --- /dev/null +++ b/src/components/dms/NewChatDialog/TextInput.tsx @@ -0,0 +1 @@ +export {BottomSheetTextInput as TextInput} from '@discord/bottom-sheet/src' diff --git a/src/components/dms/NewChatDialog/TextInput.web.tsx b/src/components/dms/NewChatDialog/TextInput.web.tsx new file mode 100644 index 000000000..5371a534f --- /dev/null +++ b/src/components/dms/NewChatDialog/TextInput.web.tsx @@ -0,0 +1 @@ +export {TextInput} from 'react-native' diff --git a/src/components/dms/NewChatDialog/index.tsx b/src/components/dms/NewChatDialog/index.tsx new file mode 100644 index 000000000..99572fd5c --- /dev/null +++ b/src/components/dms/NewChatDialog/index.tsx @@ -0,0 +1,496 @@ +import React, {useCallback, useMemo, useRef, useState} from 'react' +import type {TextInput as TextInputType} from 'react-native' +import {View} from 'react-native' +import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' +import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {isWeb} from '#/platform/detection' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' +import {useProfileFollowsQuery} from '#/state/queries/profile-follows' +import {useSession} from '#/state/session' +import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' +import {FAB} from '#/view/com/util/fab/FAB' +import * as Toast from '#/view/com/util/Toast' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, native, useTheme, web} from '#/alf' +import {Button} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {TextInput} from '#/components/dms/NewChatDialog/TextInput' +import {canBeMessaged} from '#/components/dms/util' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {Text} from '#/components/Typography' + +type Item = + | { + type: 'profile' + key: string + enabled: boolean + profile: AppBskyActorDefs.ProfileView + } + | { + type: 'empty' + key: string + message: string + } + | { + type: 'placeholder' + key: string + } + | { + type: 'error' + key: string + } + +export function NewChat({ + control, + onNewChat, +}: { + control: Dialog.DialogControlProps + onNewChat: (chatId: string) => void +}) { + const t = useTheme() + const {_} = useLingui() + + const {mutate: createChat} = useGetConvoForMembers({ + onSuccess: data => { + onNewChat(data.convo.id) + }, + onError: error => { + Toast.show(error.message) + }, + }) + + const onCreateChat = useCallback( + (did: string) => { + control.close(() => createChat([did])) + }, + [control, createChat], + ) + + return ( + <> + <FAB + testID="newChatFAB" + onPress={control.open} + icon={<Plus size="lg" fill={t.palette.white} />} + accessibilityRole="button" + accessibilityLabel={_(msg`New chat`)} + accessibilityHint="" + /> + + <Dialog.Outer + control={control} + testID="newChatDialog" + nativeOptions={{sheet: {snapPoints: ['100%']}}}> + <SearchablePeopleList onCreateChat={onCreateChat} /> + </Dialog.Outer> + </> + ) +} + +function ProfileCard({ + enabled, + profile, + moderationOpts, + onPress, +}: { + enabled: boolean + profile: AppBskyActorDefs.ProfileView + moderationOpts: ModerationOpts + onPress: (did: string) => void +}) { + const t = useTheme() + const {_} = useLingui() + const moderation = moderateProfile(profile, moderationOpts) + const handle = sanitizeHandle(profile.handle, '@') + const displayName = sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + moderation.ui('displayName'), + ) + + const handleOnPress = useCallback(() => { + onPress(profile.did) + }, [onPress, profile.did]) + + return ( + <Button + disabled={!enabled} + label={_(msg`Start chat with ${displayName}`)} + onPress={handleOnPress}> + {({hovered, pressed, focused}) => ( + <View + style={[ + a.flex_1, + a.py_md, + a.px_lg, + a.gap_md, + a.align_center, + a.flex_row, + !enabled + ? {opacity: 0.5} + : pressed || focused + ? t.atoms.bg_contrast_25 + : hovered + ? t.atoms.bg_contrast_50 + : t.atoms.bg, + ]}> + <UserAvatar + size={42} + avatar={profile.avatar} + moderation={moderation.ui('avatar')} + type={profile.associated?.labeler ? 'labeler' : 'user'} + /> + <View style={[a.flex_1, a.gap_2xs]}> + <Text + style={[t.atoms.text, a.font_bold, a.leading_snug]} + numberOfLines={1}> + {displayName} + </Text> + <Text style={t.atoms.text_contrast_high} numberOfLines={2}> + {!enabled ? <Trans>{handle} can't be messaged</Trans> : handle} + </Text> + </View> + </View> + )} + </Button> + ) +} + +function ProfileCardSkeleton() { + const t = useTheme() + + return ( + <View + style={[ + a.flex_1, + a.py_md, + a.px_lg, + a.gap_md, + a.align_center, + a.flex_row, + ]}> + <View + style={[ + a.rounded_full, + {width: 42, height: 42}, + t.atoms.bg_contrast_25, + ]} + /> + + <View style={[a.flex_1, a.gap_sm]}> + <View + style={[ + a.rounded_xs, + {width: 80, height: 14}, + t.atoms.bg_contrast_25, + ]} + /> + <View + style={[ + a.rounded_xs, + {width: 120, height: 10}, + t.atoms.bg_contrast_25, + ]} + /> + </View> + </View> + ) +} + +function Empty({message}: {message: string}) { + const t = useTheme() + return ( + <View style={[a.p_lg, a.py_xl, a.align_center, a.gap_md]}> + <Text style={[a.text_sm, a.italic, t.atoms.text_contrast_high]}> + {message} + </Text> + + <Text style={[a.text_xs, t.atoms.text_contrast_low]}>(╯°□°)╯︵ ┻━┻</Text> + </View> + ) +} + +function SearchInput({ + value, + onChangeText, + onEscape, + inputRef, +}: { + value: string + onChangeText: (text: string) => void + onEscape: () => void + inputRef: React.RefObject<TextInputType> +}) { + const t = useTheme() + const {_} = useLingui() + const { + state: hovered, + onIn: onMouseEnter, + onOut: onMouseLeave, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + const interacted = hovered || focused + + return ( + <View + {...web({ + onMouseEnter, + onMouseLeave, + })} + style={[a.flex_row, a.align_center, a.gap_sm]}> + <Search + size="md" + fill={interacted ? t.palette.primary_500 : t.palette.contrast_300} + /> + + <TextInput + // @ts-ignore bottom sheet input types issue — esb + ref={inputRef} + placeholder={_(msg`Search`)} + value={value} + onChangeText={onChangeText} + onFocus={onFocus} + onBlur={onBlur} + style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]} + placeholderTextColor={t.palette.contrast_500} + keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} + returnKeyType="search" + clearButtonMode="while-editing" + maxLength={50} + onKeyPress={({nativeEvent}) => { + if (nativeEvent.key === 'Escape') { + onEscape() + } + }} + autoCorrect={false} + autoComplete="off" + autoCapitalize="none" + autoFocus + accessibilityLabel={_(msg`Search profiles`)} + accessibilityHint={_(msg`Search profiles`)} + /> + </View> + ) +} + +function SearchablePeopleList({ + onCreateChat, +}: { + onCreateChat: (did: string) => void +}) { + const t = useTheme() + const {_} = useLingui() + const moderationOpts = useModerationOpts() + const control = Dialog.useDialogContext() + const listRef = useRef<BottomSheetFlatListMethods>(null) + const {currentAccount} = useSession() + const inputRef = React.useRef<TextInputType>(null) + + const [searchText, setSearchText] = useState('') + + const { + data: results, + isError, + isFetching, + } = useActorAutocompleteQuery(searchText, true, 12) + const {data: follows} = useProfileFollowsQuery(currentAccount?.did, { + limit: 12, + }) + + const items = React.useMemo(() => { + let _items: Item[] = [] + + if (isError) { + _items.push({ + type: 'empty', + key: 'empty', + message: _(msg`We're having network issues, try again`), + }) + } else if (searchText.length) { + if (results?.length) { + for (const profile of results) { + if (profile.did === currentAccount?.did) continue + _items.push({ + type: 'profile', + key: profile.did, + enabled: canBeMessaged(profile), + profile, + }) + } + + _items = _items.sort(a => { + // @ts-ignore + return a.enabled ? -1 : 1 + }) + } + } else { + if (follows) { + for (const page of follows.pages) { + for (const profile of page.follows) { + _items.push({ + type: 'profile', + key: profile.did, + enabled: canBeMessaged(profile), + profile, + }) + } + } + + _items = _items.sort(a => { + // @ts-ignore + return a.enabled ? -1 : 1 + }) + } else { + Array(10) + .fill(0) + .forEach((_, i) => { + _items.push({ + type: 'placeholder', + key: i + '', + }) + }) + } + } + + return _items + }, [_, searchText, results, isError, currentAccount?.did, follows]) + + if (searchText && !isFetching && !items.length && !isError) { + items.push({type: 'empty', key: 'empty', message: _(msg`No results`)}) + } + + const renderItems = React.useCallback( + ({item}: {item: Item}) => { + switch (item.type) { + case 'profile': { + return ( + <ProfileCard + key={item.key} + enabled={item.enabled} + profile={item.profile} + moderationOpts={moderationOpts!} + onPress={onCreateChat} + /> + ) + } + case 'placeholder': { + return <ProfileCardSkeleton key={item.key} /> + } + case 'empty': { + return <Empty key={item.key} message={item.message} /> + } + default: + return null + } + }, + [moderationOpts, onCreateChat], + ) + + React.useLayoutEffect(() => { + if (isWeb) { + setImmediate(() => { + inputRef?.current?.focus() + }) + } + }, []) + + const listHeader = useMemo(() => { + return ( + <View + style={[ + a.relative, + a.pt_md, + a.pb_xs, + a.px_lg, + a.border_b, + t.atoms.border_contrast_low, + t.atoms.bg, + native([a.pt_lg]), + ]}> + <View + style={[ + a.relative, + native(a.align_center), + a.justify_center, + {height: 32}, + ]}> + <Button + label={_(msg`Close`)} + size="small" + shape="round" + variant="ghost" + color="secondary" + style={[ + a.absolute, + a.z_20, + native({ + left: -7, + }), + web({ + right: -4, + }), + ]} + onPress={() => control.close()}> + {isWeb ? ( + <X size="md" fill={t.palette.contrast_500} /> + ) : ( + <ChevronLeft size="md" fill={t.palette.contrast_500} /> + )} + </Button> + <Text + style={[ + a.z_10, + a.text_lg, + a.font_bold, + a.leading_tight, + t.atoms.text_contrast_high, + ]}> + <Trans>Start a new chat</Trans> + </Text> + </View> + + <View style={[native([a.pt_sm]), web([a.pt_xs])]}> + <SearchInput + inputRef={inputRef} + value={searchText} + onChangeText={text => { + setSearchText(text) + listRef.current?.scrollToOffset({offset: 0, animated: false}) + }} + onEscape={control.close} + /> + </View> + </View> + ) + }, [t, _, control, searchText]) + + return ( + <Dialog.InnerFlatList + ref={listRef} + data={items} + renderItem={renderItems} + ListHeaderComponent={listHeader} + stickyHeaderIndices={[0]} + keyExtractor={(item: Item) => item.key} + style={[ + web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), + native({ + paddingHorizontal: 0, + marginTop: 0, + paddingTop: 0, + }), + ]} + webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} + keyboardDismissMode="on-drag" + /> + ) +} diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx index e36d1edf2..c198d44c4 100644 --- a/src/screens/Messages/List/index.tsx +++ b/src/screens/Messages/List/index.tsx @@ -18,7 +18,7 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {DialogControlProps, useDialogControl} from '#/components/Dialog' import {MessagesNUX} from '#/components/dms/MessagesNUX' -import {NewChat} from '#/components/dms/NewChat' +import {NewChat} from '#/components/dms/NewChatDialog' import {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts index 8708a244b..17b00dc26 100644 --- a/src/state/queries/actor-autocomplete.ts +++ b/src/state/queries/actor-autocomplete.ts @@ -20,6 +20,7 @@ export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix] export function useActorAutocompleteQuery( prefix: string, maintainData?: boolean, + limit?: number, ) { const moderationOpts = useModerationOpts() const {getAgent} = useAgent() @@ -37,7 +38,7 @@ export function useActorAutocompleteQuery( const res = prefix ? await getAgent().searchActorsTypeahead({ q: prefix, - limit: 8, + limit: limit || 8, }) : undefined return res?.data.actors || [] diff --git a/src/state/queries/profile-follows.ts b/src/state/queries/profile-follows.ts index 23c0dce3e..1919409c7 100644 --- a/src/state/queries/profile-follows.ts +++ b/src/state/queries/profile-follows.ts @@ -16,7 +16,16 @@ type RQPageParam = string | undefined const RQKEY_ROOT = 'profile-follows' export const RQKEY = (did: string) => [RQKEY_ROOT, did] -export function useProfileFollowsQuery(did: string | undefined) { +export function useProfileFollowsQuery( + did: string | undefined, + { + limit, + }: { + limit?: number + } = { + limit: PAGE_SIZE, + }, +) { const {getAgent} = useAgent() return useInfiniteQuery< AppBskyGraphGetFollows.OutputSchema, @@ -30,7 +39,7 @@ export function useProfileFollowsQuery(did: string | undefined) { async queryFn({pageParam}: {pageParam: RQPageParam}) { const res = await getAgent().app.bsky.graph.getFollows({ actor: did || '', - limit: PAGE_SIZE, + limit: limit || PAGE_SIZE, cursor: pageParam, }) return res.data |