[Clipclops] New clipclop dialog (#3750)
* add new routes with placeholder screens * add clops list * add a clop input * add some better padding to the clops * some more adjustments * add rnkc * implement rnkc * implement rnkc * be a little less weird about it * rename clop stuff * rename more clop * one more * add codegenerated lexicon * replace hailey's types * use codegen'd types in components * fix error + throw if fetch failed * remove bad imports * update messageslist and messageitem * import useState * replace hailey's types * use codegen'd types in components * add FAB * new chat dialog * error + default search term * fix typo * fix web styles * optimistically set chat data * use cursor instead of last rev * [Clipclops] Temp codegenerated lexicon (#3749) * add codegenerated lexicon * replace hailey's types * use codegen'd types in components * fix error + throw if fetch failed * remove bad imports * update messageslist and messageitem * import useState * add clop service URL hook * add dm service url storage * use context * use context for service url (temp) * remove log * cleanup merge * fix merge error * disable hack * sender-based message styles * temporary filter * merge cleanup * add `hideBackButton` * rm unneeded return * tried to be smart * hide go back button * use `searchActorTypeahead` instead --------- Co-authored-by: Hailey <me@haileyok.com>zio/stable
parent
2b7d796ca9
commit
bcd3678067
|
@ -17,12 +17,14 @@ export function Error({
|
||||||
message,
|
message,
|
||||||
onRetry,
|
onRetry,
|
||||||
onGoBack: onGoBackProp,
|
onGoBack: onGoBackProp,
|
||||||
|
hideBackButton,
|
||||||
sideBorders = true,
|
sideBorders = true,
|
||||||
}: {
|
}: {
|
||||||
title?: string
|
title?: string
|
||||||
message?: string
|
message?: string
|
||||||
onRetry?: () => unknown
|
onRetry?: () => unknown
|
||||||
onGoBack?: () => unknown
|
onGoBack?: () => unknown
|
||||||
|
hideBackButton?: boolean
|
||||||
sideBorders?: boolean
|
sideBorders?: boolean
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
@ -89,17 +91,19 @@ export function Error({
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
{!hideBackButton && (
|
||||||
variant="solid"
|
<Button
|
||||||
color={onRetry ? 'secondary' : 'primary'}
|
variant="solid"
|
||||||
label={_(msg`Return to previous page`)}
|
color={onRetry ? 'secondary' : 'primary'}
|
||||||
onPress={onGoBack}
|
label={_(msg`Return to previous page`)}
|
||||||
size="large"
|
onPress={onGoBack}
|
||||||
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
|
size="large"
|
||||||
<ButtonText>
|
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
|
||||||
<Trans>Go Back</Trans>
|
<ButtonText>
|
||||||
</ButtonText>
|
<Trans>Go Back</Trans>
|
||||||
</Button>
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
|
|
|
@ -134,6 +134,7 @@ let ListMaybePlaceholder = ({
|
||||||
emptyType = 'page',
|
emptyType = 'page',
|
||||||
onRetry,
|
onRetry,
|
||||||
onGoBack,
|
onGoBack,
|
||||||
|
hideBackButton,
|
||||||
sideBorders,
|
sideBorders,
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
|
@ -146,6 +147,7 @@ let ListMaybePlaceholder = ({
|
||||||
emptyType?: 'page' | 'results'
|
emptyType?: 'page' | 'results'
|
||||||
onRetry?: () => Promise<unknown>
|
onRetry?: () => Promise<unknown>
|
||||||
onGoBack?: () => void
|
onGoBack?: () => void
|
||||||
|
hideBackButton?: boolean
|
||||||
sideBorders?: boolean
|
sideBorders?: boolean
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
@ -179,6 +181,7 @@ let ListMaybePlaceholder = ({
|
||||||
onRetry={onRetry}
|
onRetry={onRetry}
|
||||||
onGoBack={onGoBack}
|
onGoBack={onGoBack}
|
||||||
sideBorders={sideBorders}
|
sideBorders={sideBorders}
|
||||||
|
hideBackButton={hideBackButton}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -198,6 +201,7 @@ let ListMaybePlaceholder = ({
|
||||||
}
|
}
|
||||||
onRetry={onRetry}
|
onRetry={onRetry}
|
||||||
onGoBack={onGoBack}
|
onGoBack={onGoBack}
|
||||||
|
hideBackButton={hideBackButton}
|
||||||
sideBorders={sideBorders}
|
sideBorders={sideBorders}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,12 +1,16 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as TempDmChatDefs from '#/temp/dm/defs'
|
import * as TempDmChatDefs from '#/temp/dm/defs'
|
||||||
|
|
||||||
export function MessageItem({item}: {item: TempDmChatDefs.MessageView}) {
|
export function MessageItem({item}: {item: TempDmChatDefs.MessageView}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
|
const fromMe = item.sender?.did === getAgent().session?.did
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
@ -15,13 +19,17 @@ export function MessageItem({item}: {item: TempDmChatDefs.MessageView}) {
|
||||||
a.px_md,
|
a.px_md,
|
||||||
a.my_xs,
|
a.my_xs,
|
||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
|
fromMe ? a.self_end : a.self_start,
|
||||||
{
|
{
|
||||||
backgroundColor: t.palette.primary_500,
|
backgroundColor: fromMe
|
||||||
|
? t.palette.primary_500
|
||||||
|
: t.palette.contrast_50,
|
||||||
maxWidth: '65%',
|
maxWidth: '65%',
|
||||||
borderRadius: 17,
|
borderRadius: 17,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<Text style={[a.text_md, {lineHeight: 1.2, color: 'white'}]}>
|
<Text
|
||||||
|
style={[a.text_md, a.leading_snug, fromMe && {color: t.palette.white}]}>
|
||||||
{item.text}
|
{item.text}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, {useCallback, useMemo, useRef, useState} from 'react'
|
import React, {useCallback, useMemo, useRef, useState} from 'react'
|
||||||
import {Alert, FlatList, View, ViewToken} from 'react-native'
|
import {FlatList, View, ViewToken} from 'react-native'
|
||||||
|
import {Alert} from 'react-native'
|
||||||
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
|
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
|
||||||
|
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
@ -64,6 +65,7 @@ export function MessagesList({chatId}: {chatId: string}) {
|
||||||
const totalMessages = useRef(10)
|
const totalMessages = useRef(10)
|
||||||
|
|
||||||
// TODO later
|
// TODO later
|
||||||
|
|
||||||
const [_, setShowSpinner] = useState(false)
|
const [_, setShowSpinner] = useState(false)
|
||||||
|
|
||||||
// Query Data
|
// Query Data
|
||||||
|
@ -147,6 +149,8 @@ export function MessagesList({chatId}: {chatId: string}) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
totalMessages.current = filtered.length
|
totalMessages.current = filtered.length
|
||||||
|
|
||||||
|
return filtered
|
||||||
}, [chat])
|
}, [chat])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -162,7 +166,7 @@ export function MessagesList({chatId}: {chatId: string}) {
|
||||||
contentContainerStyle={{paddingHorizontal: 10}}
|
contentContainerStyle={{paddingHorizontal: 10}}
|
||||||
// In the future, we might want to adjust this value. Not very concerning right now as long as we are only
|
// In the future, we might want to adjust this value. Not very concerning right now as long as we are only
|
||||||
// dealing with text. But whenever we have images or other media and things are taller, we will want to lower
|
// dealing with text. But whenever we have images or other media and things are taller, we will want to lower
|
||||||
// this...probably
|
// this...probably.
|
||||||
initialNumToRender={20}
|
initialNumToRender={20}
|
||||||
// Same with the max to render per batch. Let's be safe for now though.
|
// Same with the max to render per batch. Let's be safe for now though.
|
||||||
maxToRenderPerBatch={25}
|
maxToRenderPerBatch={25}
|
||||||
|
@ -175,7 +179,6 @@ export function MessagesList({chatId}: {chatId: string}) {
|
||||||
maintainVisibleContentPosition={{
|
maintainVisibleContentPosition={{
|
||||||
minIndexForVisible: 0,
|
minIndexForVisible: 0,
|
||||||
}}
|
}}
|
||||||
// This is actually a header since we are inverted!
|
|
||||||
ListFooterComponent={<MaybeLoader isLoading={false} />}
|
ListFooterComponent={<MaybeLoader isLoading={false} />}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {useCallback, useState} from 'react'
|
import React, {useCallback, useMemo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
@ -20,10 +20,11 @@ import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {NewChat} from '../../../components/dms/NewChat'
|
||||||
import {ClipClopGate} from '../gate'
|
import {ClipClopGate} from '../gate'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'MessagesList'>
|
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'MessagesList'>
|
||||||
export function MessagesListScreen({}: Props) {
|
export function MessagesListScreen({navigation}: Props) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
|
@ -53,14 +54,14 @@ export function MessagesListScreen({}: Props) {
|
||||||
|
|
||||||
const isError = !!error
|
const isError = !!error
|
||||||
|
|
||||||
const conversations = React.useMemo(() => {
|
const conversations = useMemo(() => {
|
||||||
if (data?.pages) {
|
if (data?.pages) {
|
||||||
return data.pages.flat()
|
return data.pages.flat()
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
const onRefresh = React.useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
try {
|
try {
|
||||||
await refetch()
|
await refetch()
|
||||||
|
@ -70,7 +71,7 @@ export function MessagesListScreen({}: Props) {
|
||||||
setIsPTRing(false)
|
setIsPTRing(false)
|
||||||
}, [refetch, setIsPTRing])
|
}, [refetch, setIsPTRing])
|
||||||
|
|
||||||
const onEndReached = React.useCallback(async () => {
|
const onEndReached = useCallback(async () => {
|
||||||
if (isFetchingNextPage || !hasNextPage || isError) return
|
if (isFetchingNextPage || !hasNextPage || isError) return
|
||||||
try {
|
try {
|
||||||
await fetchNextPage()
|
await fetchNextPage()
|
||||||
|
@ -79,26 +80,35 @@ export function MessagesListScreen({}: Props) {
|
||||||
}
|
}
|
||||||
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
||||||
|
|
||||||
|
const onNewChat = useCallback(
|
||||||
|
(conversation: string) =>
|
||||||
|
navigation.navigate('MessagesConversation', {conversation}),
|
||||||
|
[navigation],
|
||||||
|
)
|
||||||
|
|
||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
if (!gate('dms')) return <ClipClopGate />
|
if (!gate('dms')) return <ClipClopGate />
|
||||||
|
|
||||||
if (conversations.length < 1) {
|
if (conversations.length < 1) {
|
||||||
return (
|
return (
|
||||||
<ListMaybePlaceholder
|
<>
|
||||||
isLoading={isLoading}
|
<ListMaybePlaceholder
|
||||||
isError={isError}
|
isLoading={isLoading}
|
||||||
emptyType="results"
|
isError={isError}
|
||||||
emptyMessage={_(
|
emptyType="results"
|
||||||
msg`You have no messages yet. Start a conversation with someone!`,
|
emptyMessage={_(
|
||||||
)}
|
msg`You have no messages yet. Start a conversation with someone!`,
|
||||||
errorMessage={cleanError(error)}
|
)}
|
||||||
onRetry={isError ? refetch : undefined}
|
errorMessage={cleanError(error)}
|
||||||
/>
|
onRetry={isError ? refetch : undefined}
|
||||||
|
/>
|
||||||
|
<NewChat onNewChat={onNewChat} />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={a.flex_1}>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
title={_(msg`Messages`)}
|
title={_(msg`Messages`)}
|
||||||
showOnDesktop
|
showOnDesktop
|
||||||
|
@ -106,6 +116,7 @@ export function MessagesListScreen({}: Props) {
|
||||||
showBorder
|
showBorder
|
||||||
canGoBack={false}
|
canGoBack={false}
|
||||||
/>
|
/>
|
||||||
|
<NewChat onNewChat={onNewChat} />
|
||||||
<List
|
<List
|
||||||
data={conversations}
|
data={conversations}
|
||||||
renderItem={({item}) => {
|
renderItem={({item}) => {
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useSession} from 'state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
|
||||||
import * as TempDmChatDefs from '#/temp/dm/defs'
|
import * as TempDmChatDefs from '#/temp/dm/defs'
|
||||||
import * as TempDmChatGetChat from '#/temp/dm/getChat'
|
import * as TempDmChatGetChat from '#/temp/dm/getChat'
|
||||||
|
import * as TempDmChatGetChatForMembers from '#/temp/dm/getChatForMembers'
|
||||||
import * as TempDmChatGetChatLog from '#/temp/dm/getChatLog'
|
import * as TempDmChatGetChatLog from '#/temp/dm/getChatLog'
|
||||||
import * as TempDmChatGetChatMessages from '#/temp/dm/getChatMessages'
|
import * as TempDmChatGetChatMessages from '#/temp/dm/getChatMessages'
|
||||||
|
import {useDmServiceUrlStorage} from '../useDmServiceUrlStorage'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TEMPORARY, PLEASE DO NOT JUDGE ME REACT QUERY OVERLORDS 🙏
|
* TEMPORARY, PLEASE DO NOT JUDGE ME REACT QUERY OVERLORDS 🙏
|
||||||
* (and do not try this at home)
|
* (and do not try this at home)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function createHeaders(did: string) {
|
const useHeaders = () => {
|
||||||
|
const {getAgent} = useAgent()
|
||||||
return {
|
return {
|
||||||
Authorization: did,
|
get Authorization() {
|
||||||
|
return getAgent().session!.did
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,10 +31,8 @@ type Chat = {
|
||||||
|
|
||||||
export function useChat(chatId: string) {
|
export function useChat(chatId: string) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const headers = useHeaders()
|
||||||
const {serviceUrl} = useDmServiceUrlStorage()
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
const {currentAccount} = useSession()
|
|
||||||
const did = currentAccount?.did ?? ''
|
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['chat', chatId],
|
queryKey: ['chat', chatId],
|
||||||
|
@ -44,7 +46,7 @@ export function useChat(chatId: string) {
|
||||||
const messagesResponse = await fetch(
|
const messagesResponse = await fetch(
|
||||||
`${serviceUrl}/xrpc/temp.dm.getChatMessages?chatId=${chatId}`,
|
`${serviceUrl}/xrpc/temp.dm.getChatMessages?chatId=${chatId}`,
|
||||||
{
|
{
|
||||||
headers: createHeaders(did),
|
headers,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,7 +58,7 @@ export function useChat(chatId: string) {
|
||||||
const chatResponse = await fetch(
|
const chatResponse = await fetch(
|
||||||
`${serviceUrl}/xrpc/temp.dm.getChat?chatId=${chatId}`,
|
`${serviceUrl}/xrpc/temp.dm.getChat?chatId=${chatId}`,
|
||||||
{
|
{
|
||||||
headers: createHeaders(did),
|
headers,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -90,10 +92,8 @@ export function createTempId() {
|
||||||
|
|
||||||
export function useSendMessageMutation(chatId: string) {
|
export function useSendMessageMutation(chatId: string) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const headers = useHeaders()
|
||||||
const {serviceUrl} = useDmServiceUrlStorage()
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
const {currentAccount} = useSession()
|
|
||||||
const did = currentAccount?.did ?? ''
|
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<
|
||||||
TempDmChatDefs.Message,
|
TempDmChatDefs.Message,
|
||||||
|
@ -108,7 +108,7 @@ export function useSendMessageMutation(chatId: string) {
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...createHeaders(did),
|
...headers,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
@ -130,8 +130,10 @@ export function useSendMessageMutation(chatId: string) {
|
||||||
...prev,
|
...prev,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
|
$type: 'temp.dm.defs#messageView',
|
||||||
id: variables.tempId,
|
id: variables.tempId,
|
||||||
text: variables.message,
|
text: variables.message,
|
||||||
|
sender: {did: headers.Authorization}, // TODO a real DID get
|
||||||
},
|
},
|
||||||
...prev.messages,
|
...prev.messages,
|
||||||
],
|
],
|
||||||
|
@ -165,10 +167,8 @@ export function useSendMessageMutation(chatId: string) {
|
||||||
|
|
||||||
export function useChatLogQuery() {
|
export function useChatLogQuery() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const headers = useHeaders()
|
||||||
const {serviceUrl} = useDmServiceUrlStorage()
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
const {currentAccount} = useSession()
|
|
||||||
const did = currentAccount?.did ?? ''
|
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['chatLog'],
|
queryKey: ['chatLog'],
|
||||||
|
@ -183,7 +183,7 @@ export function useChatLogQuery() {
|
||||||
prevLog?.cursor ?? ''
|
prevLog?.cursor ?? ''
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
headers: createHeaders(did),
|
headers,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -193,13 +193,10 @@ export function useChatLogQuery() {
|
||||||
(await response.json()) as TempDmChatGetChatLog.OutputSchema
|
(await response.json()) as TempDmChatGetChatLog.OutputSchema
|
||||||
|
|
||||||
for (const log of json.logs) {
|
for (const log of json.logs) {
|
||||||
if (TempDmChatDefs.isLogDeleteMessage(log)) {
|
if (TempDmChatDefs.isLogCreateMessage(log)) {
|
||||||
queryClient.setQueryData(['chat', log.chatId], (prev: Chat) => {
|
queryClient.setQueryData(['chat', log.chatId], (prev: Chat) => {
|
||||||
// What to do in this case
|
// TODO hack filter out duplicates
|
||||||
if (!prev) return
|
if (prev?.messages.find(m => m.id === log.message.id)) return
|
||||||
|
|
||||||
// HACK we don't know who the creator of a message is, so just filter by id for now
|
|
||||||
if (prev.messages.find(m => m.id === log.message.id)) return prev
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
|
@ -217,3 +214,39 @@ export function useChatLogQuery() {
|
||||||
refetchInterval: 5000,
|
refetchInterval: 5000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useGetChatFromMembers({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess?: (data: TempDmChatGetChatForMembers.OutputSchema) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
}) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const headers = useHeaders()
|
||||||
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (members: string[]) => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${serviceUrl}/xrpc/temp.dm.getChatForMembers?members=${members.join(
|
||||||
|
',',
|
||||||
|
)}`,
|
||||||
|
{headers},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch chat')
|
||||||
|
|
||||||
|
return (await response.json()) as TempDmChatGetChatForMembers.OutputSchema
|
||||||
|
},
|
||||||
|
onSuccess: data => {
|
||||||
|
queryClient.setQueryData(['chat', data.chat.id], {
|
||||||
|
chatId: data.chat.id,
|
||||||
|
messages: [],
|
||||||
|
lastRev: data.chat.rev,
|
||||||
|
})
|
||||||
|
onSuccess?.(data)
|
||||||
|
},
|
||||||
|
onError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -791,7 +791,7 @@ export function SettingsScreen({}: Props) {
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
value={dmServiceUrl}
|
value={dmServiceUrl}
|
||||||
onChangeText={(text: string) => {
|
onChangeText={(text: string) => {
|
||||||
if (text.endsWith('/')) {
|
if (text.length > 9 && text.endsWith('/')) {
|
||||||
text = text.slice(0, -1)
|
text = text.slice(0, -1)
|
||||||
}
|
}
|
||||||
setDmServiceUrl(text)
|
setDmServiceUrl(text)
|
||||||
|
|
Loading…
Reference in New Issue