[Clipclops] Add screen to view and send clip clops (#3754)
* 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 * [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 * nits --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>zio/stable
parent
5d19f27052
commit
eb8bfd11d1
|
@ -168,6 +168,7 @@
|
||||||
"react-native-get-random-values": "~1.11.0",
|
"react-native-get-random-values": "~1.11.0",
|
||||||
"react-native-image-crop-picker": "^0.38.1",
|
"react-native-image-crop-picker": "^0.38.1",
|
||||||
"react-native-ios-context-menu": "^1.15.3",
|
"react-native-ios-context-menu": "^1.15.3",
|
||||||
|
"react-native-keyboard-controller": "^1.11.7",
|
||||||
"react-native-pager-view": "6.2.3",
|
"react-native-pager-view": "6.2.3",
|
||||||
"react-native-picker-select": "^8.1.0",
|
"react-native-picker-select": "^8.1.0",
|
||||||
"react-native-progress": "bluesky-social/react-native-progress",
|
"react-native-progress": "bluesky-social/react-native-progress",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'view/icons'
|
||||||
|
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||||
|
import {KeyboardProvider} from 'react-native-keyboard-controller'
|
||||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||||
import {
|
import {
|
||||||
initialWindowMetrics,
|
initialWindowMetrics,
|
||||||
|
@ -137,7 +138,9 @@ function App() {
|
||||||
<LightboxStateProvider>
|
<LightboxStateProvider>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<PortalProvider>
|
<PortalProvider>
|
||||||
|
<KeyboardProvider>
|
||||||
<InnerApp />
|
<InnerApp />
|
||||||
|
</KeyboardProvider>
|
||||||
</PortalProvider>
|
</PortalProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</LightboxStateProvider>
|
</LightboxStateProvider>
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {Pressable, TextInput, View} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function MessageInput({
|
||||||
|
onSendMessage,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
}: {
|
||||||
|
onSendMessage: (message: string) => void
|
||||||
|
onFocus: () => void
|
||||||
|
onBlur: () => void
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const [message, setMessage] = React.useState('')
|
||||||
|
|
||||||
|
const inputRef = React.useRef<TextInput>(null)
|
||||||
|
|
||||||
|
const onSubmit = React.useCallback(() => {
|
||||||
|
onSendMessage(message)
|
||||||
|
setMessage('')
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}, 100)
|
||||||
|
}, [message, onSendMessage])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.py_sm,
|
||||||
|
a.px_sm,
|
||||||
|
a.rounded_full,
|
||||||
|
a.mt_sm,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<TextInput
|
||||||
|
accessibilityLabel="Text input field"
|
||||||
|
accessibilityHint="Write a message"
|
||||||
|
value={message}
|
||||||
|
onChangeText={setMessage}
|
||||||
|
placeholder="Write a message"
|
||||||
|
style={[a.flex_1, a.text_sm, a.px_sm]}
|
||||||
|
onSubmitEditing={onSubmit}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
placeholderTextColor={t.palette.contrast_500}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={[
|
||||||
|
a.rounded_full,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
{height: 30, width: 30, backgroundColor: t.palette.primary_500},
|
||||||
|
]}
|
||||||
|
onPress={onSubmit}>
|
||||||
|
<Text style={a.text_md}>🐴</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.py_sm,
|
||||||
|
a.px_md,
|
||||||
|
a.my_xs,
|
||||||
|
a.rounded_md,
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.primary_500,
|
||||||
|
maxWidth: '65%',
|
||||||
|
borderRadius: 17,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_md, {lineHeight: 1.2, color: 'white'}]}>
|
||||||
|
{item.text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
import React, {useCallback, useMemo, useRef, useState} from 'react'
|
||||||
|
import {Alert, FlatList, View, ViewToken} from 'react-native'
|
||||||
|
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
|
||||||
|
|
||||||
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
|
||||||
|
import {MessageItem} from '#/screens/Messages/Conversation/MessageItem'
|
||||||
|
import {
|
||||||
|
useChat,
|
||||||
|
useChatLogQuery,
|
||||||
|
useSendMessageMutation,
|
||||||
|
} from '#/screens/Messages/Temp/query/query'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as TempDmChatDefs from '#/temp/dm/defs'
|
||||||
|
|
||||||
|
function MaybeLoader({isLoading}: {isLoading: boolean}) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
height: 50,
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
{isLoading && <Loader size="xl" />}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem({
|
||||||
|
item,
|
||||||
|
}: {
|
||||||
|
item: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage
|
||||||
|
}) {
|
||||||
|
if (TempDmChatDefs.isMessageView(item)) return <MessageItem item={item} />
|
||||||
|
|
||||||
|
if (TempDmChatDefs.isDeletedMessage(item)) return <Text>Deleted message</Text>
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO rm
|
||||||
|
// TEMP: This is a temporary function to generate unique keys for mutation placeholders
|
||||||
|
const generateUniqueKey = () => `_${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
|
||||||
|
function onScrollToEndFailed() {
|
||||||
|
// Placeholder function. You have to give FlatList something or else it will error.
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MessagesList({chatId}: {chatId: string}) {
|
||||||
|
const flatListRef = useRef<FlatList>(null)
|
||||||
|
|
||||||
|
// Whenever we reach the end (visually the top), we don't want to keep calling it. We will set `isFetching` to true
|
||||||
|
// once the request for new posts starts. Then, we will change it back to false after the content size changes.
|
||||||
|
const isFetching = useRef(false)
|
||||||
|
|
||||||
|
// We use this to know if we should scroll after a new clop is added to the list
|
||||||
|
const isAtBottom = useRef(false)
|
||||||
|
|
||||||
|
// Because the viewableItemsChanged callback won't have access to the updated state, we use a ref to store the
|
||||||
|
// total number of clops
|
||||||
|
// TODO this needs to be set to whatever the initial number of messages is
|
||||||
|
const totalMessages = useRef(10)
|
||||||
|
|
||||||
|
// TODO later
|
||||||
|
const [_, setShowSpinner] = useState(false)
|
||||||
|
|
||||||
|
// Query Data
|
||||||
|
const {data: chat} = useChat(chatId)
|
||||||
|
const {mutate: sendMessage} = useSendMessageMutation(chatId)
|
||||||
|
useChatLogQuery()
|
||||||
|
|
||||||
|
const [onViewableItemsChanged, viewabilityConfig] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
(info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
|
||||||
|
const firstVisibleIndex = info.viewableItems[0]?.index
|
||||||
|
|
||||||
|
isAtBottom.current = Number(firstVisibleIndex) < 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemVisiblePercentThreshold: 50,
|
||||||
|
minimumViewTime: 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onContentSizeChange = useCallback(() => {
|
||||||
|
if (isAtBottom.current) {
|
||||||
|
flatListRef.current?.scrollToOffset({offset: 0, animated: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
isFetching.current = false
|
||||||
|
setShowSpinner(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onEndReached = useCallback(() => {
|
||||||
|
if (isFetching.current) return
|
||||||
|
isFetching.current = true
|
||||||
|
setShowSpinner(true)
|
||||||
|
|
||||||
|
// Eventually we will add more here when we hit the top through RQuery
|
||||||
|
// We wouldn't actually use a timeout, but there would be a delay while loading
|
||||||
|
setTimeout(() => {
|
||||||
|
// Do something
|
||||||
|
setShowSpinner(false)
|
||||||
|
}, 1000)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onInputFocus = useCallback(() => {
|
||||||
|
if (!isAtBottom.current) {
|
||||||
|
flatListRef.current?.scrollToOffset({offset: 0, animated: true})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onSendMessage = useCallback(
|
||||||
|
async (message: string) => {
|
||||||
|
if (!message) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendMessage({
|
||||||
|
message,
|
||||||
|
tempId: generateUniqueKey(),
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
Alert.alert(e.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[sendMessage],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onInputBlur = useCallback(() => {}, [])
|
||||||
|
|
||||||
|
const messages = useMemo(() => {
|
||||||
|
if (!chat) return []
|
||||||
|
|
||||||
|
const filtered = chat.messages.filter(
|
||||||
|
(
|
||||||
|
message,
|
||||||
|
): message is
|
||||||
|
| TempDmChatDefs.MessageView
|
||||||
|
| TempDmChatDefs.DeletedMessage => {
|
||||||
|
return (
|
||||||
|
TempDmChatDefs.isMessageView(message) ||
|
||||||
|
TempDmChatDefs.isDeletedMessage(message)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
totalMessages.current = filtered.length
|
||||||
|
}, [chat])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={{flex: 1, marginBottom: isWeb ? 20 : 85}}
|
||||||
|
behavior="padding"
|
||||||
|
keyboardVerticalOffset={70}
|
||||||
|
contentContainerStyle={{flex: 1}}>
|
||||||
|
<FlatList
|
||||||
|
data={messages}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
renderItem={renderItem}
|
||||||
|
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
|
||||||
|
initialNumToRender={20}
|
||||||
|
// Same with the max to render per batch. Let's be safe for now though.
|
||||||
|
maxToRenderPerBatch={25}
|
||||||
|
inverted={true}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
onScrollToIndexFailed={onScrollToEndFailed}
|
||||||
|
onContentSizeChange={onContentSizeChange}
|
||||||
|
onViewableItemsChanged={onViewableItemsChanged}
|
||||||
|
viewabilityConfig={viewabilityConfig}
|
||||||
|
maintainVisibleContentPosition={{
|
||||||
|
minIndexForVisible: 0,
|
||||||
|
}}
|
||||||
|
// This is actually a header since we are inverted!
|
||||||
|
ListFooterComponent={<MaybeLoader isLoading={false} />}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
ref={flatListRef}
|
||||||
|
keyboardDismissMode="none"
|
||||||
|
/>
|
||||||
|
<View style={{paddingHorizontal: 10}}>
|
||||||
|
<MessageInput
|
||||||
|
onSendMessage={onSendMessage}
|
||||||
|
onFocus={onInputFocus}
|
||||||
|
onBlur={onInputBlur}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
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'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
@ -7,6 +6,8 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||||
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
|
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
|
||||||
import {ClipClopGate} from '../gate'
|
import {ClipClopGate} from '../gate'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<
|
type Props = NativeStackScreenProps<
|
||||||
|
@ -16,17 +17,18 @@ type Props = NativeStackScreenProps<
|
||||||
export function MessagesConversationScreen({route}: Props) {
|
export function MessagesConversationScreen({route}: Props) {
|
||||||
const chatId = route.params.conversation
|
const chatId = route.params.conversation
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
|
|
||||||
if (!gate('dms')) return <ClipClopGate />
|
if (!gate('dms')) return <ClipClopGate />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<CenteredView style={{flex: 1}} sideBorders>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
title={_(msg`Chat with ${chatId}`)}
|
title={_(msg`Chat with ${chatId}`)}
|
||||||
showOnDesktop
|
showOnDesktop
|
||||||
showBorder
|
showBorder
|
||||||
/>
|
/>
|
||||||
</View>
|
<MessagesList chatId={chatId} />
|
||||||
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,7 @@ export function MessagesListScreen({}: Props) {
|
||||||
renderItem={({item}) => {
|
renderItem={({item}) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={`/messages/${item.profile.handle}`}
|
to={`/messages/3kqzb4mytxk2v`}
|
||||||
style={[a.flex_1, a.pl_md, a.py_sm, a.gap_md, a.pr_2xl]}>
|
style={[a.flex_1, a.pl_md, a.py_sm, a.gap_md, a.pr_2xl]}>
|
||||||
<PreviewableUserAvatar profile={item.profile} size={44} />
|
<PreviewableUserAvatar profile={item.profile} size={44} />
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useSession} from 'state/session'
|
||||||
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
|
import * as TempDmChatDefs from '#/temp/dm/defs'
|
||||||
|
import * as TempDmChatGetChat from '#/temp/dm/getChat'
|
||||||
|
import * as TempDmChatGetChatLog from '#/temp/dm/getChatLog'
|
||||||
|
import * as TempDmChatGetChatMessages from '#/temp/dm/getChatMessages'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TEMPORARY, PLEASE DO NOT JUDGE ME REACT QUERY OVERLORDS 🙏
|
||||||
|
* (and do not try this at home)
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createHeaders(did: string) {
|
||||||
|
return {
|
||||||
|
Authorization: did,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chat = {
|
||||||
|
chatId: string
|
||||||
|
messages: TempDmChatGetChatMessages.OutputSchema['messages']
|
||||||
|
lastCursor?: string
|
||||||
|
lastRev?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChat(chatId: string) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const did = currentAccount?.did ?? ''
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['chat', chatId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const currentChat = queryClient.getQueryData(['chat', chatId])
|
||||||
|
|
||||||
|
if (currentChat) {
|
||||||
|
return currentChat as Chat
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesResponse = await fetch(
|
||||||
|
`${serviceUrl}/xrpc/temp.dm.getChatMessages?chatId=${chatId}`,
|
||||||
|
{
|
||||||
|
headers: createHeaders(did),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!messagesResponse.ok) throw new Error('Failed to fetch messages')
|
||||||
|
|
||||||
|
const messagesJson =
|
||||||
|
(await messagesResponse.json()) as TempDmChatGetChatMessages.OutputSchema
|
||||||
|
|
||||||
|
const chatResponse = await fetch(
|
||||||
|
`${serviceUrl}/xrpc/temp.dm.getChat?chatId=${chatId}`,
|
||||||
|
{
|
||||||
|
headers: createHeaders(did),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!chatResponse.ok) throw new Error('Failed to fetch chat')
|
||||||
|
|
||||||
|
const chatJson =
|
||||||
|
(await chatResponse.json()) as TempDmChatGetChat.OutputSchema
|
||||||
|
|
||||||
|
const newChat = {
|
||||||
|
chatId,
|
||||||
|
messages: messagesJson.messages,
|
||||||
|
lastCursor: messagesJson.cursor,
|
||||||
|
lastRev: chatJson.chat.rev,
|
||||||
|
} satisfies Chat
|
||||||
|
|
||||||
|
queryClient.setQueryData(['chat', chatId], newChat)
|
||||||
|
|
||||||
|
return newChat
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SendMessageMutationVariables {
|
||||||
|
message: string
|
||||||
|
tempId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTempId() {
|
||||||
|
return Math.random().toString(36).substring(7).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSendMessageMutation(chatId: string) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const did = currentAccount?.did ?? ''
|
||||||
|
|
||||||
|
return useMutation<
|
||||||
|
TempDmChatDefs.Message,
|
||||||
|
Error,
|
||||||
|
SendMessageMutationVariables,
|
||||||
|
unknown
|
||||||
|
>({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
mutationFn: async ({message, tempId}) => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${serviceUrl}/xrpc/temp.dm.sendMessage?chatId=${chatId}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...createHeaders(did),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
chatId,
|
||||||
|
message: {
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to send message')
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
onMutate: async variables => {
|
||||||
|
queryClient.setQueryData(['chat', chatId], (prev: Chat) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: variables.tempId,
|
||||||
|
text: variables.message,
|
||||||
|
},
|
||||||
|
...prev.messages,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: (result, variables) => {
|
||||||
|
queryClient.setQueryData(['chat', chatId], (prev: Chat) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
messages: prev.messages.map(m =>
|
||||||
|
m.id === variables.tempId
|
||||||
|
? {
|
||||||
|
...m,
|
||||||
|
id: result.id,
|
||||||
|
}
|
||||||
|
: m,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (_, variables) => {
|
||||||
|
console.log(_)
|
||||||
|
queryClient.setQueryData(['chat', chatId], (prev: Chat) => ({
|
||||||
|
...prev,
|
||||||
|
messages: prev.messages.filter(m => m.id !== variables.tempId),
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatLogQuery() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const did = currentAccount?.did ?? ''
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['chatLog'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const prevLog = queryClient.getQueryData([
|
||||||
|
'chatLog',
|
||||||
|
]) as TempDmChatGetChatLog.OutputSchema
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${serviceUrl}/xrpc/temp.dm.getChatLog?cursor=${
|
||||||
|
prevLog?.cursor ?? ''
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
headers: createHeaders(did),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch chat log')
|
||||||
|
|
||||||
|
const json =
|
||||||
|
(await response.json()) as TempDmChatGetChatLog.OutputSchema
|
||||||
|
|
||||||
|
for (const log of json.logs) {
|
||||||
|
if (TempDmChatDefs.isLogDeleteMessage(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
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
messages: [log.message, ...prev.messages],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchInterval: 5000,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {useAsyncStorage} from '@react-native-async-storage/async-storage'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TEMP: REMOVE BEFORE RELEASE
|
||||||
|
*
|
||||||
|
* Clip clop trivia:
|
||||||
|
*
|
||||||
|
* A little known fact about the term "clip clop" is that it may refer to a unit of time. It is unknown what the exact
|
||||||
|
* length of a clip clop is, but it is generally agreed that it is approximately 9 minutes and 30 seconds, or 570
|
||||||
|
* seconds.
|
||||||
|
*
|
||||||
|
* The term "clip clop" may also be used in other contexts, although it is unknown what all of these contexts may be.
|
||||||
|
* Recently, the term has been used among many young adults to refer to a type of social media functionality, although
|
||||||
|
* the exact nature of this functionality is also unknown. It is believed that the term may have originated from a
|
||||||
|
* popular video game, but this has not been confirmed.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DmServiceUrlStorageContext = React.createContext<{
|
||||||
|
serviceUrl: string
|
||||||
|
setServiceUrl: (value: string) => void
|
||||||
|
}>({
|
||||||
|
serviceUrl: '',
|
||||||
|
setServiceUrl: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useDmServiceUrlStorage = () =>
|
||||||
|
React.useContext(DmServiceUrlStorageContext)
|
||||||
|
|
||||||
|
export function DmServiceUrlProvider({children}: {children: React.ReactNode}) {
|
||||||
|
const [serviceUrl, setServiceUrl] = React.useState<string>('')
|
||||||
|
const {getItem, setItem: setItemInner} = useAsyncStorage('dmServiceUrl')
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
const v = await getItem()
|
||||||
|
console.log(v)
|
||||||
|
setServiceUrl(v ?? '')
|
||||||
|
})()
|
||||||
|
}, [getItem])
|
||||||
|
|
||||||
|
const setItem = React.useCallback(
|
||||||
|
(v: string) => {
|
||||||
|
setItemInner(v)
|
||||||
|
setServiceUrl(v)
|
||||||
|
},
|
||||||
|
[setItemInner],
|
||||||
|
)
|
||||||
|
|
||||||
|
const value = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
serviceUrl,
|
||||||
|
setServiceUrl: setItem,
|
||||||
|
}),
|
||||||
|
[serviceUrl, setItem],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DmServiceUrlStorageContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</DmServiceUrlStorageContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import {DmServiceUrlProvider} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
import {Provider as AltTextRequiredProvider} from './alt-text-required'
|
import {Provider as AltTextRequiredProvider} from './alt-text-required'
|
||||||
import {Provider as AutoplayProvider} from './autoplay'
|
import {Provider as AutoplayProvider} from './autoplay'
|
||||||
import {Provider as DisableHapticsProvider} from './disable-haptics'
|
import {Provider as DisableHapticsProvider} from './disable-haptics'
|
||||||
|
@ -30,7 +31,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
<HiddenPostsProvider>
|
<HiddenPostsProvider>
|
||||||
<InAppBrowserProvider>
|
<InAppBrowserProvider>
|
||||||
<DisableHapticsProvider>
|
<DisableHapticsProvider>
|
||||||
<AutoplayProvider>{children}</AutoplayProvider>
|
<AutoplayProvider>
|
||||||
|
<DmServiceUrlProvider>{children}</DmServiceUrlProvider>
|
||||||
|
</AutoplayProvider>
|
||||||
</DisableHapticsProvider>
|
</DisableHapticsProvider>
|
||||||
</InAppBrowserProvider>
|
</InAppBrowserProvider>
|
||||||
</HiddenPostsProvider>
|
</HiddenPostsProvider>
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
import {
|
||||||
|
AppBskyActorDefs,
|
||||||
|
AppBskyEmbedRecord,
|
||||||
|
AppBskyRichtextFacet,
|
||||||
|
} from '@atproto/api'
|
||||||
|
import {ValidationResult} from '@atproto/lexicon'
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
id?: string
|
||||||
|
text: string
|
||||||
|
/** Annotations of text (mentions, URLs, hashtags, etc) */
|
||||||
|
facets?: AppBskyRichtextFacet.Main[]
|
||||||
|
embed?: AppBskyEmbedRecord.Main | {$type: string; [k: string]: unknown}
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMessage(v: unknown): v is Message {
|
||||||
|
return isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#message'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMessage(v: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageView {
|
||||||
|
id: string
|
||||||
|
rev: string
|
||||||
|
text: string
|
||||||
|
/** Annotations of text (mentions, URLs, hashtags, etc) */
|
||||||
|
facets?: AppBskyRichtextFacet.Main[]
|
||||||
|
embed?: AppBskyEmbedRecord.Main | {$type: string; [k: string]: unknown}
|
||||||
|
sender?: MessageViewSender
|
||||||
|
sentAt: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMessageView(v: unknown): v is MessageView {
|
||||||
|
return (
|
||||||
|
isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#messageView'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMessageView(v: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeletedMessage {
|
||||||
|
id: string
|
||||||
|
rev?: string
|
||||||
|
sender?: MessageViewSender
|
||||||
|
sentAt: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDeletedMessage(v: unknown): v is DeletedMessage {
|
||||||
|
return (
|
||||||
|
isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#deletedMessage'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDeletedMessage(v: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageViewSender {
|
||||||
|
did: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMessageViewSender(v: unknown): v is MessageViewSender {
|
||||||
|
return (
|
||||||
|
isObj(v) &&
|
||||||
|
hasProp(v, '$type') &&
|
||||||
|
v.$type === 'temp.dm.defs#messageViewSender'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMessageViewSender(v: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatView {
|
||||||
|
id: string
|
||||||
|
rev: string
|
||||||
|
members: AppBskyActorDefs.ProfileViewBasic[]
|
||||||
|
lastMessage?:
|
||||||
|
| MessageView
|
||||||
|
| DeletedMessage
|
||||||
|
| {$type: string; [k: string]: unknown}
|
||||||
|
unreadCount: number
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChatView(v: unknown): v is ChatView {
|
||||||
|
return isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#chatView'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateChatView(v: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IncomingMessageSetting =
|
||||||
|
| 'all'
|
||||||
|
| 'none'
|
||||||
|
| 'following'
|
||||||
|
| (string & {})
|
||||||
|
|
||||||
|
export interface LogBeginChat {
|
||||||
|
rev: string
|
||||||
|
chatId: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLogBeginChat(v: unknown): v is LogBeginChat {
|
||||||
|
return (
|
||||||
|
isObj(v) && hasProp(v, '$type') && v.$type === 'temp.dm.defs#logBeginChat'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLogBeginChat(v: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogCreateMessage {
|
||||||
|
rev: string
|
||||||
|
chatId: string
|
||||||
|
message: MessageView | DeletedMessage | {$type: string; [k: string]: unknown}
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLogCreateMessage(v: unknown): v is LogCreateMessage {
|
||||||
|
return (
|
||||||
|
isObj(v) &&
|
||||||
|
hasProp(v, '$type') &&
|
||||||
|
v.$type === 'temp.dm.defs#logCreateMessage'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLogCreateMessage(v: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogDeleteMessage {
|
||||||
|
rev: string
|
||||||
|
chatId: string
|
||||||
|
message: MessageView | DeletedMessage | {$type: string; [k: string]: unknown}
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLogDeleteMessage(v: unknown): v is LogDeleteMessage {
|
||||||
|
return (
|
||||||
|
isObj(v) &&
|
||||||
|
hasProp(v, '$type') &&
|
||||||
|
v.$type === 'temp.dm.defs#logDeleteMessage'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLogDeleteMessage(v: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObj(v: unknown): v is Record<string, unknown> {
|
||||||
|
return typeof v === 'object' && v !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasProp<K extends PropertyKey>(
|
||||||
|
data: object,
|
||||||
|
prop: K,
|
||||||
|
): data is Record<K, unknown> {
|
||||||
|
return prop in data
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {}
|
||||||
|
|
||||||
|
export interface InputSchema {
|
||||||
|
chatId: string
|
||||||
|
messageId: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutputSchema = TempDmDefs.DeletedMessage
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
qp?: QueryParams
|
||||||
|
encoding: 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {
|
||||||
|
chatId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputSchema = undefined
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
chat: TempDmDefs.ChatView
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {
|
||||||
|
members: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputSchema = undefined
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
chat: TempDmDefs.ChatView
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {
|
||||||
|
cursor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputSchema = undefined
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
cursor?: string
|
||||||
|
logs: (
|
||||||
|
| TempDmDefs.LogBeginChat
|
||||||
|
| TempDmDefs.LogCreateMessage
|
||||||
|
| TempDmDefs.LogDeleteMessage
|
||||||
|
| {$type: string; [k: string]: unknown}
|
||||||
|
)[]
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {
|
||||||
|
chatId: string
|
||||||
|
limit?: number
|
||||||
|
cursor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputSchema = undefined
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
cursor?: string
|
||||||
|
messages: (
|
||||||
|
| TempDmDefs.MessageView
|
||||||
|
| TempDmDefs.DeletedMessage
|
||||||
|
| {$type: string; [k: string]: unknown}
|
||||||
|
)[]
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {}
|
||||||
|
|
||||||
|
export type InputSchema = undefined
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
allowIncoming: TempDmDefs.IncomingMessageSetting
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
export interface QueryParams {}
|
||||||
|
|
||||||
|
export interface InputSchema {
|
||||||
|
chatId: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
qp?: QueryParams
|
||||||
|
encoding: 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {
|
||||||
|
limit?: number
|
||||||
|
cursor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputSchema = undefined
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
cursor?: string
|
||||||
|
chats: TempDmDefs.ChatView[]
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
export interface QueryParams {}
|
||||||
|
|
||||||
|
export interface InputSchema {
|
||||||
|
chatId: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
qp?: QueryParams
|
||||||
|
encoding: 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {}
|
||||||
|
|
||||||
|
export interface InputSchema {
|
||||||
|
chatId: string
|
||||||
|
message: TempDmDefs.Message
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutputSchema = TempDmDefs.MessageView
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
qp?: QueryParams
|
||||||
|
encoding: 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import {ValidationResult} from '@atproto/lexicon'
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {}
|
||||||
|
|
||||||
|
export interface InputSchema {
|
||||||
|
items: BatchItem[]
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
items: TempDmDefs.MessageView[]
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
qp?: QueryParams
|
||||||
|
encoding: 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchItem {
|
||||||
|
chatId: string
|
||||||
|
message: TempDmDefs.Message
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBatchItem(v: unknown): v is BatchItem {
|
||||||
|
return (
|
||||||
|
isObj(v) &&
|
||||||
|
hasProp(v, '$type') &&
|
||||||
|
v.$type === 'temp.dm.sendMessageBatch#batchItem'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateBatchItem(v: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObj(v: unknown): v is Record<string, unknown> {
|
||||||
|
return typeof v === 'object' && v !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasProp<K extends PropertyKey>(
|
||||||
|
data: object,
|
||||||
|
prop: K,
|
||||||
|
): data is Record<K, unknown> {
|
||||||
|
return prop in data
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
export interface QueryParams {}
|
||||||
|
|
||||||
|
export interface InputSchema {
|
||||||
|
chatId: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
qp?: QueryParams
|
||||||
|
encoding: 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {}
|
||||||
|
|
||||||
|
export interface InputSchema {
|
||||||
|
chatId: string
|
||||||
|
messageId?: string
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutputSchema = TempDmDefs.ChatView
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
qp?: QueryParams
|
||||||
|
encoding: 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {Headers, XRPCError} from '@atproto/xrpc'
|
||||||
|
|
||||||
|
import * as TempDmDefs from './defs'
|
||||||
|
|
||||||
|
export interface QueryParams {}
|
||||||
|
|
||||||
|
export interface InputSchema {
|
||||||
|
allowIncoming?: TempDmDefs.IncomingMessageSetting
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutputSchema {
|
||||||
|
allowIncoming: TempDmDefs.IncomingMessageSetting
|
||||||
|
[k: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallOptions {
|
||||||
|
headers?: Headers
|
||||||
|
qp?: QueryParams
|
||||||
|
encoding: 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
success: boolean
|
||||||
|
headers: Headers
|
||||||
|
data: OutputSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toKnownErr(e: any) {
|
||||||
|
if (e instanceof XRPCError) {
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ import {HandIcon, HashtagIcon} from 'lib/icons'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
import {useGate} from 'lib/statsig/statsig'
|
||||||
import {colors, s} from 'lib/styles'
|
import {colors, s} from 'lib/styles'
|
||||||
import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
|
import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
|
||||||
import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
|
import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
|
||||||
|
@ -61,8 +62,10 @@ import {Text} from 'view/com/util/text/Text'
|
||||||
import * as Toast from 'view/com/util/Toast'
|
import * as Toast from 'view/com/util/Toast'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {ScrollView} from 'view/com/util/Views'
|
import {ScrollView} from 'view/com/util/Views'
|
||||||
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
||||||
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {navigate, resetToTab} from '#/Navigation'
|
import {navigate, resetToTab} from '#/Navigation'
|
||||||
import {Email2FAToggle} from './Email2FAToggle'
|
import {Email2FAToggle} from './Email2FAToggle'
|
||||||
import {ExportCarDialog} from './ExportCarDialog'
|
import {ExportCarDialog} from './ExportCarDialog'
|
||||||
|
@ -169,6 +172,11 @@ export function SettingsScreen({}: Props) {
|
||||||
const exportCarControl = useDialogControl()
|
const exportCarControl = useDialogControl()
|
||||||
const birthdayControl = useDialogControl()
|
const birthdayControl = useDialogControl()
|
||||||
|
|
||||||
|
// TODO: TEMP REMOVE WHEN CLOPS ARE RELEASED
|
||||||
|
const gate = useGate()
|
||||||
|
const {serviceUrl: dmServiceUrl, setServiceUrl: setDmServiceUrl} =
|
||||||
|
useDmServiceUrlStorage()
|
||||||
|
|
||||||
// const primaryBg = useCustomPalette<ViewStyle>({
|
// const primaryBg = useCustomPalette<ViewStyle>({
|
||||||
// light: {backgroundColor: colors.blue0},
|
// light: {backgroundColor: colors.blue0},
|
||||||
// dark: {backgroundColor: colors.blue6},
|
// dark: {backgroundColor: colors.blue6},
|
||||||
|
@ -778,6 +786,22 @@ export function SettingsScreen({}: Props) {
|
||||||
<Trans>System log</Trans>
|
<Trans>System log</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
{gate('dms') && (
|
||||||
|
<TextField.Root>
|
||||||
|
<TextField.Input
|
||||||
|
value={dmServiceUrl}
|
||||||
|
onChangeText={(text: string) => {
|
||||||
|
if (text.endsWith('/')) {
|
||||||
|
text = text.slice(0, -1)
|
||||||
|
}
|
||||||
|
setDmServiceUrl(text)
|
||||||
|
}}
|
||||||
|
autoCapitalize="none"
|
||||||
|
keyboardType="url"
|
||||||
|
label="🐴"
|
||||||
|
/>
|
||||||
|
</TextField.Root>
|
||||||
|
)}
|
||||||
{__DEV__ ? (
|
{__DEV__ ? (
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
|
@ -122,6 +122,16 @@ export function BottomBarWeb() {
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
<NavItem routeName="Messages" href="/messages">
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Envelope
|
||||||
|
size="lg"
|
||||||
|
style={[styles.ctrlIcon, pal.text, styles.messagesIcon]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</NavItem>
|
||||||
{gate('dms') && (
|
{gate('dms') && (
|
||||||
<NavItem routeName="Messages" href="/messages">
|
<NavItem routeName="Messages" href="/messages">
|
||||||
{({isActive}) => {
|
{({isActive}) => {
|
||||||
|
|
|
@ -18695,6 +18695,11 @@ react-native-ios-context-menu@^1.15.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@dominicstop/ts-event-emitter" "^1.1.0"
|
"@dominicstop/ts-event-emitter" "^1.1.0"
|
||||||
|
|
||||||
|
react-native-keyboard-controller@^1.11.7:
|
||||||
|
version "1.11.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.11.7.tgz#85640374e4c3627c3b667256a1d308698ff80393"
|
||||||
|
integrity sha512-K2zlqVyWX4QO7r+dHMQgZT41G2dSEWtDYgBdht1WVyTaMQmwTMalZcHCWBVOnzyGaJq/hMKhF1kSPqJP1xqSFA==
|
||||||
|
|
||||||
react-native-pager-view@6.2.3:
|
react-native-pager-view@6.2.3:
|
||||||
version "6.2.3"
|
version "6.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775"
|
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775"
|
||||||
|
|
Loading…
Reference in New Issue