[Clipclops] Use API data for clipclop list (#3769)

* use real API

* remove extra tab icon

* messages list web layout + style improvements

* use style's text color for input

* make new chat button way more obvious

---------

Co-authored-by: Hailey <me@haileyok.com>
zio/stable
Samuel Newman 2024-04-30 18:15:48 +01:00 committed by GitHub
parent bcd3678067
commit 7b694fd860
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 204 additions and 124 deletions

View File

@ -23,8 +23,13 @@ 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()
export function NewChat({
control,
onNewChat,
}: {
control: Dialog.DialogControlProps
onNewChat: (chatId: string) => void
}) {
const t = useTheme()
const {_} = useLingui()

View File

@ -42,7 +42,7 @@ export function MessageInput({
value={message}
onChangeText={setMessage}
placeholder="Write a message"
style={[a.flex_1, a.text_sm, a.px_sm]}
style={[a.flex_1, a.text_sm, a.px_sm, t.atoms.text]}
onSubmitEditing={onSubmit}
onFocus={onFocus}
onBlur={onBlur}

View File

@ -1,9 +1,10 @@
/* eslint-disable react/prop-types */
import React, {useCallback, useMemo, useState} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useInfiniteQuery} from '@tanstack/react-query'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {MessagesTabNavigatorParams} from '#/lib/routes/types'
@ -14,19 +15,26 @@ import {useAgent} from '#/state/session'
import {List} from '#/view/com/util/List'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {useTheme} from '#/alf'
import {useBreakpoints, useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {DialogControlProps, useDialogControl} from '#/components/Dialog'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider'
import {Link} from '#/components/Link'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {Text} from '#/components/Typography'
import * as TempDmChatDefs from '#/temp/dm/defs'
import {NewChat} from '../../../components/dms/NewChat'
import {ClipClopGate} from '../gate'
import {useListChats} from '../Temp/query/query'
type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'MessagesList'>
export function MessagesListScreen({navigation}: Props) {
const {_} = useLingui()
const t = useTheme()
const newChatControl = useDialogControl()
const {gtMobile} = useBreakpoints()
const renderButton = useCallback(() => {
return (
@ -50,13 +58,13 @@ export function MessagesListScreen({navigation}: Props) {
fetchNextPage,
error,
refetch,
} = usePlaceholderConversations()
} = useListChats()
const isError = !!error
const conversations = useMemo(() => {
if (data?.pages) {
return data.pages.flat()
return data.pages.flatMap(page => page.chats)
}
return []
}, [data])
@ -86,6 +94,14 @@ export function MessagesListScreen({navigation}: Props) {
[navigation],
)
const onNavigateToSettings = useCallback(() => {
navigation.navigate('MessagesSettings')
}, [navigation])
const renderItem = useCallback(({item}: {item: TempDmChatDefs.ChatView}) => {
return <ChatListItem key={item.id} chat={item} />
}, [])
const gate = useGate()
if (!gate('dms')) return <ClipClopGate />
@ -102,73 +118,35 @@ export function MessagesListScreen({navigation}: Props) {
errorMessage={cleanError(error)}
onRetry={isError ? refetch : undefined}
/>
<NewChat onNewChat={onNewChat} />
<NewChat onNewChat={onNewChat} control={newChatControl} />
</>
)
}
return (
<View style={a.flex_1}>
<ViewHeader
title={_(msg`Messages`)}
showOnDesktop
renderButton={renderButton}
showBorder
canGoBack={false}
/>
<NewChat onNewChat={onNewChat} />
{!gtMobile && (
<ViewHeader
title={_(msg`Messages`)}
renderButton={renderButton}
showBorder
canGoBack={false}
/>
)}
<NewChat onNewChat={onNewChat} control={newChatControl} />
<List
data={conversations}
renderItem={({item}) => {
return (
<Link
to={`/messages/3kqzb4mytxk2v`}
style={[a.flex_1, a.pl_md, a.py_sm, a.gap_md, a.pr_2xl]}>
<PreviewableUserAvatar profile={item.profile} size={44} />
<View style={[a.flex_1]}>
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.flex_1,
]}>
<Text numberOfLines={1}>
<Text style={item.unread && a.font_bold}>
{item.profile.displayName || item.profile.handle}
</Text>{' '}
<Text style={t.atoms.text_contrast_medium}>
@{item.profile.handle}
</Text>
</Text>
{item.unread && (
<View
style={[
a.ml_2xl,
{backgroundColor: t.palette.primary_500},
a.rounded_full,
{height: 7, width: 7},
]}
/>
)}
</View>
<Text
numberOfLines={2}
style={[
a.text_sm,
item.unread ? a.font_bold : t.atoms.text_contrast_medium,
]}>
{item.lastMessage}
</Text>
</View>
</Link>
)
}}
keyExtractor={item => item.profile.did}
renderItem={renderItem}
keyExtractor={item => item.id}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
ListHeaderComponent={
<DesktopHeader
newChatControl={newChatControl}
onNavigateToSettings={onNavigateToSettings}
/>
}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
@ -180,61 +158,139 @@ export function MessagesListScreen({navigation}: Props) {
onEndReachedThreshold={3}
initialNumToRender={initialNumToRender}
windowSize={11}
// @ts-ignore our .web version only -sfn
desktopFixedHeight
/>
</View>
)
}
function usePlaceholderConversations() {
function ChatListItem({chat}: {chat: TempDmChatDefs.ChatView}) {
const t = useTheme()
const {_} = useLingui()
const {getAgent} = useAgent()
return useInfiniteQuery({
queryKey: ['messages'],
queryFn: async () => {
const people = await getAgent().getProfiles({actors: PLACEHOLDER_PEOPLE})
return people.data.profiles.map(profile => ({
profile,
unread: Math.random() > 0.5,
lastMessage: getRandomPost(),
}))
},
initialPageParam: undefined,
getNextPageParam: () => undefined,
})
}
const PLACEHOLDER_PEOPLE = [
'pfrazee.com',
'haileyok.com',
'danabra.mov',
'esb.lol',
'samuel.bsky.team',
]
function getRandomPost() {
const num = Math.floor(Math.random() * 10)
switch (num) {
case 0:
return 'hello'
case 1:
return 'lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'
case 2:
return 'banger post'
case 3:
return 'lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'
case 4:
return 'lol look at this bug'
case 5:
return 'wow'
case 6:
return "that's pretty cool, wow!"
case 7:
return 'I think this is a bug'
case 8:
return 'Hello World!'
case 9:
return 'DMs when???'
default:
return 'this is unlikely'
let lastMessage = _(msg`No messages yet`)
if (TempDmChatDefs.isMessageView(chat.lastMessage)) {
lastMessage = chat.lastMessage.text
}
const otherUser = chat.members.find(
member => member.did !== getAgent().session?.did,
)
if (!otherUser) {
return null
}
return (
<Link to={`/messages/${chat.id}`} style={a.flex_1}>
{({hovered, pressed}) => (
<View
style={[
a.flex_row,
a.flex_1,
a.pl_md,
a.py_sm,
a.gap_md,
a.pr_2xl,
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<View pointerEvents="none">
<PreviewableUserAvatar profile={otherUser} size={42} />
</View>
<View style={[a.flex_1]}>
<Text numberOfLines={1} style={a.leading_snug}>
<Text style={[t.atoms.text, chat.unreadCount > 0 && a.font_bold]}>
{otherUser.displayName || otherUser.handle}
</Text>{' '}
<Text style={t.atoms.text_contrast_medium}>
@{otherUser.handle}
</Text>
</Text>
<Text
numberOfLines={2}
style={[
a.text_sm,
chat.unread ? a.font_bold : t.atoms.text_contrast_medium,
]}>
{lastMessage}
</Text>
</View>
{chat.unreadCount > 0 && (
<View
style={[
a.flex_0,
a.ml_2xl,
a.mt_xs,
{backgroundColor: t.palette.primary_500},
a.rounded_full,
{height: 7, width: 7},
]}
/>
)}
</View>
)}
</Link>
)
}
function DesktopHeader({
newChatControl,
onNavigateToSettings,
}: {
newChatControl: DialogControlProps
onNavigateToSettings: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile, gtTablet} = useBreakpoints()
if (!gtMobile) {
return null
}
return (
<View
style={[
t.atoms.bg,
t.atoms.border_contrast_low,
a.border_b,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.px_lg,
a.py_sm,
]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Messages</Trans>
</Text>
<View style={[a.flex_row, a.align_center, a.gap_md]}>
<Button
label={_(msg`Message settings`)}
color="secondary"
size="large"
variant="ghost"
style={[{height: 'auto', width: 'auto'}, a.px_sm, a.py_sm]}
onPress={onNavigateToSettings}>
<ButtonIcon icon={SettingsSlider} />
</Button>
{gtTablet && (
<Button
label={_(msg`New chat`)}
color="primary"
size="large"
variant="solid"
style={[{height: 'auto', width: 'auto'}, a.px_md, a.py_sm]}
onPress={newChatControl.open}>
<ButtonIcon icon={Envelope} position="right" />
<ButtonText>
<Trans>New chat</Trans>
</ButtonText>
</Button>
)}
</View>
</View>
)
}

View File

@ -1,4 +1,9 @@
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import * as TempDmChatDefs from '#/temp/dm/defs'
@ -6,6 +11,7 @@ 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 * as TempDmChatListChats from '#/temp/dm/listChats'
import {useDmServiceUrlStorage} from '../useDmServiceUrlStorage'
/**
@ -250,3 +256,26 @@ export function useGetChatFromMembers({
onError,
})
}
export function useListChats() {
const headers = useHeaders()
const {serviceUrl} = useDmServiceUrlStorage()
return useInfiniteQuery({
queryKey: ['chats'],
queryFn: async ({pageParam}) => {
const response = await fetch(
`${serviceUrl}/xrpc/temp.dm.listChats${
pageParam ? `?cursor=${pageParam}` : ''
}`,
{headers},
)
if (!response.ok) throw new Error('Failed to fetch chats')
return (await response.json()) as TempDmChatListChats.OutputSchema
},
initialPageParam: undefined as string | undefined,
getNextPageParam: lastPage => lastPage.cursor,
})
}

View File

@ -122,16 +122,6 @@ export function BottomBarWeb() {
)
}}
</NavItem>
<NavItem routeName="Messages" href="/messages">
{() => {
return (
<Envelope
size="lg"
style={[styles.ctrlIcon, pal.text, styles.messagesIcon]}
/>
)
}}
</NavItem>
{gate('dms') && (
<NavItem routeName="Messages" href="/messages">
{({isActive}) => {