diff options
author | Eric Bailey <git@esb.lol> | 2024-05-17 17:03:50 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-17 15:03:50 -0700 |
commit | 1cdcb3e6c333b7ad5aa53676163643d7f43d1528 (patch) | |
tree | bd512fbaf93009bd00d60b34a7b97bb4e1469177 /src/components/dms/NewChat.tsx | |
parent | d02e0884c40adebe3799254395d933205b104a86 (diff) | |
download | voidsky-1cdcb3e6c333b7ad5aa53676163643d7f43d1528.tar.zst |
[🐴] New chat dialog refresh (#4071)
* Checkpoint, header styled, empty * Checkpoint, styles * Show recent follows in initial state, finesse some styles * Add skeleton * Add some limits * Fix autofocus on web, use bottom sheet input on native * Ignore type * Clean up edits * Format * Tweak icon placement * Fix type * use prop for dismissing keyboard --------- Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/components/dms/NewChat.tsx')
-rw-r--r-- | src/components/dms/NewChat.tsx | 278 |
1 files changed, 0 insertions, 278 deletions
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()} - /> - ) -} |