[🐴] Message drafts (#3993)
* drafts * don't throw if no convo ID * Remove labs package --------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
		
							parent
							
								
									f147256fdc
								
							
						
					
					
						commit
						9861494e34
					
				
					 4 changed files with 107 additions and 5 deletions
				
			
		|  | @ -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> | ||||||
|  |       <MessageDraftsProvider> | ||||||
|         <MessagesEventBusProvider>{children}</MessagesEventBusProvider> |         <MessagesEventBusProvider>{children}</MessagesEventBusProvider> | ||||||
|  |       </MessageDraftsProvider> | ||||||
|     </CurrentConvoIdProvider> |     </CurrentConvoIdProvider> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										83
									
								
								src/state/messages/message-drafts.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/state/messages/message-drafts.tsx
									
										
									
									
									
										Normal 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> | ||||||
|  |   ) | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue