[🐴] Message drafts (#3993)

* drafts

* don't throw if no convo ID

* Remove labs package

---------

Co-authored-by: Eric Bailey <git@esb.lol>
zio/stable
Samuel Newman 2024-05-14 18:55:43 +01:00 committed by GitHub
parent f147256fdc
commit 9861494e34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 107 additions and 5 deletions

View File

@ -15,6 +15,10 @@ import Graphemer from 'graphemer'
import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {
useMessageDraft,
useSaveMessageDraft,
} from '#/state/messages/message-drafts'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
@ -29,7 +33,8 @@ export function MessageInput({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const playHaptic = useHaptics() const playHaptic = useHaptics()
const [message, setMessage] = React.useState('') const {getDraft, clearDraft} = useMessageDraft()
const [message, setMessage] = React.useState(getDraft)
const [maxHeight, setMaxHeight] = React.useState<number | undefined>() const [maxHeight, setMaxHeight] = React.useState<number | undefined>()
const [isInputScrollable, setIsInputScrollable] = React.useState(false) const [isInputScrollable, setIsInputScrollable] = React.useState(false)
@ -45,13 +50,14 @@ export function MessageInput({
Toast.show(_(msg`Message is too long`)) Toast.show(_(msg`Message is too long`))
return return
} }
clearDraft()
onSendMessage(message.trimEnd()) onSendMessage(message.trimEnd())
playHaptic() playHaptic()
setMessage('') setMessage('')
setTimeout(() => { setTimeout(() => {
inputRef.current?.focus() inputRef.current?.focus()
}, 100) }, 100)
}, [message, onSendMessage, playHaptic, _]) }, [message, onSendMessage, playHaptic, _, clearDraft])
const onInputLayout = React.useCallback( const onInputLayout = React.useCallback(
(e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => { (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
@ -69,6 +75,8 @@ export function MessageInput({
[scrollToEnd, topInset], [scrollToEnd, topInset],
) )
useSaveMessageDraft(message)
return ( return (
<View style={a.p_sm}> <View style={a.p_sm}>
<View <View

View File

@ -6,6 +6,10 @@ import Graphemer from 'graphemer'
import TextareaAutosize from 'react-textarea-autosize' import TextareaAutosize from 'react-textarea-autosize'
import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
import {
useMessageDraft,
useSaveMessageDraft,
} from '#/state/messages/message-drafts'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
@ -18,7 +22,8 @@ export function MessageInput({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const [message, setMessage] = React.useState('') const {getDraft, clearDraft} = useMessageDraft()
const [message, setMessage] = React.useState(getDraft)
const onSubmit = React.useCallback(() => { const onSubmit = React.useCallback(() => {
if (message.trim() === '') { if (message.trim() === '') {
@ -28,9 +33,10 @@ export function MessageInput({
Toast.show(_(msg`Message is too long`)) Toast.show(_(msg`Message is too long`))
return return
} }
clearDraft()
onSendMessage(message.trimEnd()) onSendMessage(message.trimEnd())
setMessage('') setMessage('')
}, [message, onSendMessage, _]) }, [message, onSendMessage, _, clearDraft])
const onKeyDown = React.useCallback( const onKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => { (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@ -50,6 +56,8 @@ export function MessageInput({
[], [],
) )
useSaveMessageDraft(message)
return ( return (
<View style={a.p_sm}> <View style={a.p_sm}>
<View <View

View File

@ -2,11 +2,14 @@ import React from 'react'
import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id' import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id'
import {MessagesEventBusProvider} from '#/state/messages/events' import {MessagesEventBusProvider} from '#/state/messages/events'
import {MessageDraftsProvider} from './message-drafts'
export function MessagesProvider({children}: {children: React.ReactNode}) { export function MessagesProvider({children}: {children: React.ReactNode}) {
return ( return (
<CurrentConvoIdProvider> <CurrentConvoIdProvider>
<MessageDraftsProvider>
<MessagesEventBusProvider>{children}</MessagesEventBusProvider> <MessagesEventBusProvider>{children}</MessagesEventBusProvider>
</MessageDraftsProvider>
</CurrentConvoIdProvider> </CurrentConvoIdProvider>
) )
} }

View File

@ -0,0 +1,83 @@
import React, {useEffect, useMemo, useReducer, useRef} from 'react'
import {useCurrentConvoId} from './current-convo-id'
const MessageDraftsContext = React.createContext<{
state: State
dispatch: React.Dispatch<Actions>
} | null>(null)
function useMessageDraftsContext() {
const ctx = React.useContext(MessageDraftsContext)
if (!ctx) {
throw new Error(
'useMessageDrafts must be used within a MessageDraftsContext',
)
}
return ctx
}
export function useMessageDraft() {
const {currentConvoId} = useCurrentConvoId()
const {state, dispatch} = useMessageDraftsContext()
return useMemo(
() => ({
getDraft: () => (currentConvoId && state[currentConvoId]) || '',
clearDraft: () => {
if (currentConvoId) {
dispatch({type: 'clear', convoId: currentConvoId})
}
},
}),
[state, dispatch, currentConvoId],
)
}
export function useSaveMessageDraft(message: string) {
const {currentConvoId} = useCurrentConvoId()
const {dispatch} = useMessageDraftsContext()
const messageRef = useRef(message)
messageRef.current = message
useEffect(() => {
return () => {
if (currentConvoId) {
dispatch({
type: 'set',
convoId: currentConvoId,
draft: messageRef.current,
})
}
}
}, [currentConvoId, dispatch])
}
type State = {[convoId: string]: string}
type Actions =
| {type: 'set'; convoId: string; draft: string}
| {type: 'clear'; convoId: string}
function reducer(state: State, action: Actions): State {
switch (action.type) {
case 'set':
return {...state, [action.convoId]: action.draft}
case 'clear':
return {...state, [action.convoId]: ''}
default:
return state
}
}
export function MessageDraftsProvider({children}: {children: React.ReactNode}) {
const [state, dispatch] = useReducer(reducer, {})
const ctx = useMemo(() => {
return {state, dispatch}
}, [state])
return (
<MessageDraftsContext.Provider value={ctx}>
{children}
</MessageDraftsContext.Provider>
)
}