about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Error.tsx26
-rw-r--r--src/components/Lists.tsx4
-rw-r--r--src/components/dms/NewChat.tsx233
3 files changed, 252 insertions, 11 deletions
diff --git a/src/components/Error.tsx b/src/components/Error.tsx
index bf689fc07..ee479cca9 100644
--- a/src/components/Error.tsx
+++ b/src/components/Error.tsx
@@ -17,12 +17,14 @@ export function Error({
   message,
   onRetry,
   onGoBack: onGoBackProp,
+  hideBackButton,
   sideBorders = true,
 }: {
   title?: string
   message?: string
   onRetry?: () => unknown
   onGoBack?: () => unknown
+  hideBackButton?: boolean
   sideBorders?: boolean
 }) {
   const navigation = useNavigation<NavigationProp>()
@@ -89,17 +91,19 @@ export function Error({
             </ButtonText>
           </Button>
         )}
-        <Button
-          variant="solid"
-          color={onRetry ? 'secondary' : 'primary'}
-          label={_(msg`Return to previous page`)}
-          onPress={onGoBack}
-          size="large"
-          style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
-          <ButtonText>
-            <Trans>Go Back</Trans>
-          </ButtonText>
-        </Button>
+        {!hideBackButton && (
+          <Button
+            variant="solid"
+            color={onRetry ? 'secondary' : 'primary'}
+            label={_(msg`Return to previous page`)}
+            onPress={onGoBack}
+            size="large"
+            style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
+            <ButtonText>
+              <Trans>Go Back</Trans>
+            </ButtonText>
+          </Button>
+        )}
       </View>
     </CenteredView>
   )
diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx
index b5419697b..721e877be 100644
--- a/src/components/Lists.tsx
+++ b/src/components/Lists.tsx
@@ -134,6 +134,7 @@ let ListMaybePlaceholder = ({
   emptyType = 'page',
   onRetry,
   onGoBack,
+  hideBackButton,
   sideBorders,
 }: {
   isLoading: boolean
@@ -146,6 +147,7 @@ let ListMaybePlaceholder = ({
   emptyType?: 'page' | 'results'
   onRetry?: () => Promise<unknown>
   onGoBack?: () => void
+  hideBackButton?: boolean
   sideBorders?: boolean
 }): React.ReactNode => {
   const t = useTheme()
@@ -179,6 +181,7 @@ let ListMaybePlaceholder = ({
         onRetry={onRetry}
         onGoBack={onGoBack}
         sideBorders={sideBorders}
+        hideBackButton={hideBackButton}
       />
     )
   }
@@ -198,6 +201,7 @@ let ListMaybePlaceholder = ({
         }
         onRetry={onRetry}
         onGoBack={onGoBack}
+        hideBackButton={hideBackButton}
         sideBorders={sideBorders}
       />
     )
diff --git a/src/components/dms/NewChat.tsx b/src/components/dms/NewChat.tsx
new file mode 100644
index 000000000..bbe118f04
--- /dev/null
+++ b/src/components/dms/NewChat.tsx
@@ -0,0 +1,233 @@
+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/queries/preferences'
+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 {useGetChatFromMembers} from '../../screens/Messages/Temp/query/query'
+import {Button} from '../Button'
+import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope'
+import {ListMaybePlaceholder} from '../Lists'
+import {Text} from '../Typography'
+
+export function NewChat({onNewChat}: {onNewChat: (chatId: string) => void}) {
+  const control = Dialog.useDialogControl()
+  const t = useTheme()
+  const {_} = useLingui()
+
+  const {mutate: createChat} = useGetChatFromMembers({
+    onSuccess: data => {
+      onNewChat(data.chat.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={<Envelope size="xl" 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 [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)
+      return (
+        <Button
+          label={profile.displayName || sanitizeHandle(profile.handle)}
+          onPress={() => onCreateChat(profile.did)}>
+          {({hovered, pressed}) => (
+            <View
+              style={[
+                a.flex_1,
+                a.px_md,
+                a.py_sm,
+                a.gap_md,
+                a.align_center,
+                a.flex_row,
+                a.rounded_sm,
+                pressed
+                  ? 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={1}>
+                  {sanitizeHandle(profile.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,
+          ]}
+        />
+        <Dialog.Close />
+        <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} />
+          <TextField.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"
+          />
+        </TextField.Root>
+      </View>
+    )
+  }, [t.atoms.bg, _, control, searchText])
+
+  return (
+    <Dialog.InnerFlatList
+      ref={listRef}
+      data={actorAutocompleteData}
+      renderItem={renderItem}
+      ListHeaderComponent={
+        <>
+          {listHeader}
+          {searchText.length > 0 && !actorAutocompleteData?.length && (
+            <ListMaybePlaceholder
+              isLoading={isFetching}
+              isError={isError}
+              onRetry={refetch}
+              hideBackButton={true}
+              emptyType="results"
+              sideBorders={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()}
+    />
+  )
+}