From bcd3678067c2b807582794ce72f26c3af25d3a75 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 30 Apr 2024 17:43:57 +0100 Subject: [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 --- src/components/Error.tsx | 26 ++- src/components/Lists.tsx | 4 + src/components/dms/NewChat.tsx | 233 +++++++++++++++++++++ src/screens/Messages/Conversation/MessageItem.tsx | 12 +- src/screens/Messages/Conversation/MessagesList.tsx | 9 +- src/screens/Messages/List/index.tsx | 43 ++-- src/screens/Messages/Temp/query/query.ts | 79 +++++-- src/view/screens/Settings/index.tsx | 2 +- 8 files changed, 352 insertions(+), 56 deletions(-) create mode 100644 src/components/dms/NewChat.tsx (limited to 'src') 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() @@ -89,17 +91,19 @@ export function Error({ )} - + {!hideBackButton && ( + + )} ) 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 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 ( + <> + } + accessibilityRole="button" + accessibilityLabel={_(msg`New chat`)} + accessibilityHint="" + /> + + + + + + + ) +} + +function SearchablePeopleList({ + onCreateChat, +}: { + onCreateChat: (did: string) => void +}) { + const t = useTheme() + const {_} = useLingui() + const moderationOpts = useModerationOpts() + const control = Dialog.useDialogContext() + const listRef = useRef(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 ( + + ) + }, + [ + 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 ( + + {/* cover top corners */} + + + + Start a new chat + + + + { + 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" + /> + + + ) + }, [t.atoms.bg, _, control, searchText]) + + return ( + + {listHeader} + {searchText.length > 0 && !actorAutocompleteData?.length && ( + + )} + + } + stickyHeaderIndices={[0]} + keyExtractor={(item: AppBskyActorDefs.ProfileView) => item.did} + // @ts-expect-error web only + style={isWeb && {minHeight: '100vh'}} + onScrollBeginDrag={() => Keyboard.dismiss()} + /> + ) +} diff --git a/src/screens/Messages/Conversation/MessageItem.tsx b/src/screens/Messages/Conversation/MessageItem.tsx index 74e65488e..822b17804 100644 --- a/src/screens/Messages/Conversation/MessageItem.tsx +++ b/src/screens/Messages/Conversation/MessageItem.tsx @@ -1,12 +1,16 @@ import React from 'react' import {View} from 'react-native' +import {useAgent} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' import * as TempDmChatDefs from '#/temp/dm/defs' export function MessageItem({item}: {item: TempDmChatDefs.MessageView}) { const t = useTheme() + const {getAgent} = useAgent() + + const fromMe = item.sender?.did === getAgent().session?.did return ( - + {item.text} diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index aafed42af..e3b518f65 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -1,5 +1,6 @@ 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 {isWeb} from 'platform/detection' @@ -64,6 +65,7 @@ export function MessagesList({chatId}: {chatId: string}) { const totalMessages = useRef(10) // TODO later + const [_, setShowSpinner] = useState(false) // Query Data @@ -147,6 +149,8 @@ export function MessagesList({chatId}: {chatId: string}) { }, ) totalMessages.current = filtered.length + + return filtered }, [chat]) return ( @@ -162,7 +166,7 @@ export function MessagesList({chatId}: {chatId: string}) { contentContainerStyle={{paddingHorizontal: 10}} // 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 - // this...probably + // this...probably. initialNumToRender={20} // Same with the max to render per batch. Let's be safe for now though. maxToRenderPerBatch={25} @@ -175,7 +179,6 @@ export function MessagesList({chatId}: {chatId: string}) { maintainVisibleContentPosition={{ minIndexForVisible: 0, }} - // This is actually a header since we are inverted! ListFooterComponent={} removeClippedSubviews={true} ref={flatListRef} diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx index b13ddd291..ff4e8e83e 100644 --- a/src/screens/Messages/List/index.tsx +++ b/src/screens/Messages/List/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react' +import React, {useCallback, useMemo, useState} from 'react' import {View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -20,10 +20,11 @@ import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from ' import {Link} from '#/components/Link' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' import {Text} from '#/components/Typography' +import {NewChat} from '../../../components/dms/NewChat' import {ClipClopGate} from '../gate' type Props = NativeStackScreenProps -export function MessagesListScreen({}: Props) { +export function MessagesListScreen({navigation}: Props) { const {_} = useLingui() const t = useTheme() @@ -53,14 +54,14 @@ export function MessagesListScreen({}: Props) { const isError = !!error - const conversations = React.useMemo(() => { + const conversations = useMemo(() => { if (data?.pages) { return data.pages.flat() } return [] }, [data]) - const onRefresh = React.useCallback(async () => { + const onRefresh = useCallback(async () => { setIsPTRing(true) try { await refetch() @@ -70,7 +71,7 @@ export function MessagesListScreen({}: Props) { setIsPTRing(false) }, [refetch, setIsPTRing]) - const onEndReached = React.useCallback(async () => { + const onEndReached = useCallback(async () => { if (isFetchingNextPage || !hasNextPage || isError) return try { await fetchNextPage() @@ -79,26 +80,35 @@ export function MessagesListScreen({}: Props) { } }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) + const onNewChat = useCallback( + (conversation: string) => + navigation.navigate('MessagesConversation', {conversation}), + [navigation], + ) + const gate = useGate() if (!gate('dms')) return if (conversations.length < 1) { return ( - + <> + + + ) } return ( - + + { diff --git a/src/screens/Messages/Temp/query/query.ts b/src/screens/Messages/Temp/query/query.ts index 2477dc569..26f9e625f 100644 --- a/src/screens/Messages/Temp/query/query.ts +++ b/src/screens/Messages/Temp/query/query.ts @@ -1,20 +1,24 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' -import {useSession} from 'state/session' -import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage' +import {useAgent} from '#/state/session' import * as TempDmChatDefs from '#/temp/dm/defs' import * as TempDmChatGetChat from '#/temp/dm/getChat' +import * as TempDmChatGetChatForMembers from '#/temp/dm/getChatForMembers' import * as TempDmChatGetChatLog from '#/temp/dm/getChatLog' import * as TempDmChatGetChatMessages from '#/temp/dm/getChatMessages' +import {useDmServiceUrlStorage} from '../useDmServiceUrlStorage' /** * TEMPORARY, PLEASE DO NOT JUDGE ME REACT QUERY OVERLORDS 🙏 * (and do not try this at home) */ -function createHeaders(did: string) { +const useHeaders = () => { + const {getAgent} = useAgent() return { - Authorization: did, + get Authorization() { + return getAgent().session!.did + }, } } @@ -27,10 +31,8 @@ type Chat = { export function useChat(chatId: string) { const queryClient = useQueryClient() - + const headers = useHeaders() const {serviceUrl} = useDmServiceUrlStorage() - const {currentAccount} = useSession() - const did = currentAccount?.did ?? '' return useQuery({ queryKey: ['chat', chatId], @@ -44,7 +46,7 @@ export function useChat(chatId: string) { const messagesResponse = await fetch( `${serviceUrl}/xrpc/temp.dm.getChatMessages?chatId=${chatId}`, { - headers: createHeaders(did), + headers, }, ) @@ -56,7 +58,7 @@ export function useChat(chatId: string) { const chatResponse = await fetch( `${serviceUrl}/xrpc/temp.dm.getChat?chatId=${chatId}`, { - headers: createHeaders(did), + headers, }, ) @@ -90,10 +92,8 @@ export function createTempId() { export function useSendMessageMutation(chatId: string) { const queryClient = useQueryClient() - + const headers = useHeaders() const {serviceUrl} = useDmServiceUrlStorage() - const {currentAccount} = useSession() - const did = currentAccount?.did ?? '' return useMutation< TempDmChatDefs.Message, @@ -108,7 +108,7 @@ export function useSendMessageMutation(chatId: string) { { method: 'POST', headers: { - ...createHeaders(did), + ...headers, 'Content-Type': 'application/json', }, body: JSON.stringify({ @@ -130,8 +130,10 @@ export function useSendMessageMutation(chatId: string) { ...prev, messages: [ { + $type: 'temp.dm.defs#messageView', id: variables.tempId, text: variables.message, + sender: {did: headers.Authorization}, // TODO a real DID get }, ...prev.messages, ], @@ -165,10 +167,8 @@ export function useSendMessageMutation(chatId: string) { export function useChatLogQuery() { const queryClient = useQueryClient() - + const headers = useHeaders() const {serviceUrl} = useDmServiceUrlStorage() - const {currentAccount} = useSession() - const did = currentAccount?.did ?? '' return useQuery({ queryKey: ['chatLog'], @@ -183,7 +183,7 @@ export function useChatLogQuery() { prevLog?.cursor ?? '' }`, { - headers: createHeaders(did), + headers, }, ) @@ -193,13 +193,10 @@ export function useChatLogQuery() { (await response.json()) as TempDmChatGetChatLog.OutputSchema for (const log of json.logs) { - if (TempDmChatDefs.isLogDeleteMessage(log)) { + if (TempDmChatDefs.isLogCreateMessage(log)) { queryClient.setQueryData(['chat', log.chatId], (prev: Chat) => { - // What to do in this case - if (!prev) 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 + // TODO hack filter out duplicates + if (prev?.messages.find(m => m.id === log.message.id)) return return { ...prev, @@ -217,3 +214,39 @@ export function useChatLogQuery() { 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, + }) +} diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index 470bace87..a0e4ff60f 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -791,7 +791,7 @@ export function SettingsScreen({}: Props) { { - if (text.endsWith('/')) { + if (text.length > 9 && text.endsWith('/')) { text = text.slice(0, -1) } setDmServiceUrl(text) -- cgit 1.4.1