my fork of the bluesky client

[🐴] Message drafts (#3993)

* drafts

* don't throw if no convo ID

* Remove labs package

---------

Co-authored-by: Eric Bailey <git@esb.lol>

authored by samuel.fm

Eric Bailey and committed by
GitHub
9861494e f147256f

+107 -5
+10 -2
src/screens/Messages/Conversation/MessageInput.tsx
··· 15 15 16 16 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 17 17 import {useHaptics} from '#/lib/haptics' 18 + import { 19 + useMessageDraft, 20 + useSaveMessageDraft, 21 + } from '#/state/messages/message-drafts' 18 22 import * as Toast from '#/view/com/util/Toast' 19 23 import {atoms as a, useTheme} from '#/alf' 20 24 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' ··· 29 33 const {_} = useLingui() 30 34 const t = useTheme() 31 35 const playHaptic = useHaptics() 32 - const [message, setMessage] = React.useState('') 36 + const {getDraft, clearDraft} = useMessageDraft() 37 + const [message, setMessage] = React.useState(getDraft) 33 38 const [maxHeight, setMaxHeight] = React.useState<number | undefined>() 34 39 const [isInputScrollable, setIsInputScrollable] = React.useState(false) 35 40 ··· 45 50 Toast.show(_(msg`Message is too long`)) 46 51 return 47 52 } 53 + clearDraft() 48 54 onSendMessage(message.trimEnd()) 49 55 playHaptic() 50 56 setMessage('') 51 57 setTimeout(() => { 52 58 inputRef.current?.focus() 53 59 }, 100) 54 - }, [message, onSendMessage, playHaptic, _]) 60 + }, [message, onSendMessage, playHaptic, _, clearDraft]) 55 61 56 62 const onInputLayout = React.useCallback( 57 63 (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => { ··· 68 74 }, 69 75 [scrollToEnd, topInset], 70 76 ) 77 + 78 + useSaveMessageDraft(message) 71 79 72 80 return ( 73 81 <View style={a.p_sm}>
+10 -2
src/screens/Messages/Conversation/MessageInput.web.tsx
··· 6 6 import TextareaAutosize from 'react-textarea-autosize' 7 7 8 8 import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 9 + import { 10 + useMessageDraft, 11 + useSaveMessageDraft, 12 + } from '#/state/messages/message-drafts' 9 13 import * as Toast from '#/view/com/util/Toast' 10 14 import {atoms as a, useTheme} from '#/alf' 11 15 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' ··· 18 22 }) { 19 23 const {_} = useLingui() 20 24 const t = useTheme() 21 - const [message, setMessage] = React.useState('') 25 + const {getDraft, clearDraft} = useMessageDraft() 26 + const [message, setMessage] = React.useState(getDraft) 22 27 23 28 const onSubmit = React.useCallback(() => { 24 29 if (message.trim() === '') { ··· 28 33 Toast.show(_(msg`Message is too long`)) 29 34 return 30 35 } 36 + clearDraft() 31 37 onSendMessage(message.trimEnd()) 32 38 setMessage('') 33 - }, [message, onSendMessage, _]) 39 + }, [message, onSendMessage, _, clearDraft]) 34 40 35 41 const onKeyDown = React.useCallback( 36 42 (e: React.KeyboardEvent<HTMLTextAreaElement>) => { ··· 49 55 }, 50 56 [], 51 57 ) 58 + 59 + useSaveMessageDraft(message) 52 60 53 61 return ( 54 62 <View style={a.p_sm}>
+4 -1
src/state/messages/index.tsx
··· 2 2 3 3 import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id' 4 4 import {MessagesEventBusProvider} from '#/state/messages/events' 5 + import {MessageDraftsProvider} from './message-drafts' 5 6 6 7 export function MessagesProvider({children}: {children: React.ReactNode}) { 7 8 return ( 8 9 <CurrentConvoIdProvider> 9 - <MessagesEventBusProvider>{children}</MessagesEventBusProvider> 10 + <MessageDraftsProvider> 11 + <MessagesEventBusProvider>{children}</MessagesEventBusProvider> 12 + </MessageDraftsProvider> 10 13 </CurrentConvoIdProvider> 11 14 ) 12 15 }
+83
src/state/messages/message-drafts.tsx
··· 1 + import React, {useEffect, useMemo, useReducer, useRef} from 'react' 2 + 3 + import {useCurrentConvoId} from './current-convo-id' 4 + 5 + const MessageDraftsContext = React.createContext<{ 6 + state: State 7 + dispatch: React.Dispatch<Actions> 8 + } | null>(null) 9 + 10 + function useMessageDraftsContext() { 11 + const ctx = React.useContext(MessageDraftsContext) 12 + if (!ctx) { 13 + throw new Error( 14 + 'useMessageDrafts must be used within a MessageDraftsContext', 15 + ) 16 + } 17 + return ctx 18 + } 19 + 20 + export function useMessageDraft() { 21 + const {currentConvoId} = useCurrentConvoId() 22 + const {state, dispatch} = useMessageDraftsContext() 23 + return useMemo( 24 + () => ({ 25 + getDraft: () => (currentConvoId && state[currentConvoId]) || '', 26 + clearDraft: () => { 27 + if (currentConvoId) { 28 + dispatch({type: 'clear', convoId: currentConvoId}) 29 + } 30 + }, 31 + }), 32 + [state, dispatch, currentConvoId], 33 + ) 34 + } 35 + 36 + export function useSaveMessageDraft(message: string) { 37 + const {currentConvoId} = useCurrentConvoId() 38 + const {dispatch} = useMessageDraftsContext() 39 + const messageRef = useRef(message) 40 + messageRef.current = message 41 + 42 + useEffect(() => { 43 + return () => { 44 + if (currentConvoId) { 45 + dispatch({ 46 + type: 'set', 47 + convoId: currentConvoId, 48 + draft: messageRef.current, 49 + }) 50 + } 51 + } 52 + }, [currentConvoId, dispatch]) 53 + } 54 + 55 + type State = {[convoId: string]: string} 56 + type Actions = 57 + | {type: 'set'; convoId: string; draft: string} 58 + | {type: 'clear'; convoId: string} 59 + 60 + function reducer(state: State, action: Actions): State { 61 + switch (action.type) { 62 + case 'set': 63 + return {...state, [action.convoId]: action.draft} 64 + case 'clear': 65 + return {...state, [action.convoId]: ''} 66 + default: 67 + return state 68 + } 69 + } 70 + 71 + export function MessageDraftsProvider({children}: {children: React.ReactNode}) { 72 + const [state, dispatch] = useReducer(reducer, {}) 73 + 74 + const ctx = useMemo(() => { 75 + return {state, dispatch} 76 + }, [state]) 77 + 78 + return ( 79 + <MessageDraftsContext.Provider value={ctx}> 80 + {children} 81 + </MessageDraftsContext.Provider> 82 + ) 83 + }