[🐴] Message drafts (#3993)
* drafts * don't throw if no convo ID * Remove labs package --------- Co-authored-by: Eric Bailey <git@esb.lol>zio/stable
parent
f147256fdc
commit
9861494e34
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
<MessagesEventBusProvider>{children}</MessagesEventBusProvider>
|
<MessageDraftsProvider>
|
||||||
|
<MessagesEventBusProvider>{children}</MessagesEventBusProvider>
|
||||||
|
</MessageDraftsProvider>
|
||||||
</CurrentConvoIdProvider>
|
</CurrentConvoIdProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue