[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
Hailey 2024-04-29 23:34:26 -07:00 committed by GitHub
parent 5d19f27052
commit eb8bfd11d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1295 additions and 7 deletions

View File

@ -168,6 +168,7 @@
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "^0.38.1",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.11.7",
"react-native-pager-view": "6.2.3",
"react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress",

View File

@ -4,6 +4,7 @@ import 'view/icons'
import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {KeyboardProvider} from 'react-native-keyboard-controller'
import {RootSiblingParent} from 'react-native-root-siblings'
import {
initialWindowMetrics,
@ -137,7 +138,9 @@ function App() {
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
<KeyboardProvider>
<InnerApp />
</KeyboardProvider>
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -1,5 +1,4 @@
import React from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
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 {useGate} from '#/lib/statsig/statsig'
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'
type Props = NativeStackScreenProps<
@ -16,17 +17,18 @@ type Props = NativeStackScreenProps<
export function MessagesConversationScreen({route}: Props) {
const chatId = route.params.conversation
const {_} = useLingui()
const gate = useGate()
if (!gate('dms')) return <ClipClopGate />
return (
<View>
<CenteredView style={{flex: 1}} sideBorders>
<ViewHeader
title={_(msg`Chat with ${chatId}`)}
showOnDesktop
showBorder
/>
</View>
<MessagesList chatId={chatId} />
</CenteredView>
)
}

View File

@ -111,7 +111,7 @@ export function MessagesListScreen({}: Props) {
renderItem={({item}) => {
return (
<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]}>
<PreviewableUserAvatar profile={item.profile} size={44} />
<View style={[a.flex_1]}>

View File

@ -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,
})
}

View File

@ -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>
)
}

View File

@ -1,5 +1,6 @@
import React from 'react'
import {DmServiceUrlProvider} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
import {Provider as AltTextRequiredProvider} from './alt-text-required'
import {Provider as AutoplayProvider} from './autoplay'
import {Provider as DisableHapticsProvider} from './disable-haptics'
@ -30,7 +31,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
<HiddenPostsProvider>
<InAppBrowserProvider>
<DisableHapticsProvider>
<AutoplayProvider>{children}</AutoplayProvider>
<AutoplayProvider>
<DmServiceUrlProvider>{children}</DmServiceUrlProvider>
</AutoplayProvider>
</DisableHapticsProvider>
</InAppBrowserProvider>
</HiddenPostsProvider>

195
src/temp/dm/defs.ts 100644
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -51,6 +51,7 @@ import {HandIcon, HashtagIcon} from 'lib/icons'
import {makeProfileLink} from 'lib/routes/links'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {NavigationProp} from 'lib/routes/types'
import {useGate} from 'lib/statsig/statsig'
import {colors, s} from 'lib/styles'
import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
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 {UserAvatar} from 'view/com/util/UserAvatar'
import {ScrollView} from 'view/com/util/Views'
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import * as TextField from '#/components/forms/TextField'
import {navigate, resetToTab} from '#/Navigation'
import {Email2FAToggle} from './Email2FAToggle'
import {ExportCarDialog} from './ExportCarDialog'
@ -169,6 +172,11 @@ export function SettingsScreen({}: Props) {
const exportCarControl = useDialogControl()
const birthdayControl = useDialogControl()
// TODO: TEMP REMOVE WHEN CLOPS ARE RELEASED
const gate = useGate()
const {serviceUrl: dmServiceUrl, setServiceUrl: setDmServiceUrl} =
useDmServiceUrlStorage()
// const primaryBg = useCustomPalette<ViewStyle>({
// light: {backgroundColor: colors.blue0},
// dark: {backgroundColor: colors.blue6},
@ -778,6 +786,22 @@ export function SettingsScreen({}: Props) {
<Trans>System log</Trans>
</Text>
</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__ ? (
<>
<TouchableOpacity

View File

@ -122,6 +122,16 @@ 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}) => {

View File

@ -18695,6 +18695,11 @@ react-native-ios-context-menu@^1.15.3:
dependencies:
"@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:
version "6.2.3"
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775"