about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/screens/Messages/Conversation/MessageInput.tsx12
-rw-r--r--src/screens/Messages/Conversation/MessageInput.web.tsx12
-rw-r--r--src/state/messages/index.tsx5
-rw-r--r--src/state/messages/message-drafts.tsx83
4 files changed, 107 insertions, 5 deletions
diff --git a/src/screens/Messages/Conversation/MessageInput.tsx b/src/screens/Messages/Conversation/MessageInput.tsx
index 926d66e7d..d05d6109e 100644
--- a/src/screens/Messages/Conversation/MessageInput.tsx
+++ b/src/screens/Messages/Conversation/MessageInput.tsx
@@ -15,6 +15,10 @@ import Graphemer from 'graphemer'
 
 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
 import {useHaptics} from '#/lib/haptics'
+import {
+  useMessageDraft,
+  useSaveMessageDraft,
+} from '#/state/messages/message-drafts'
 import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useTheme} from '#/alf'
 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
@@ -29,7 +33,8 @@ export function MessageInput({
   const {_} = useLingui()
   const t = useTheme()
   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 [isInputScrollable, setIsInputScrollable] = React.useState(false)
 
@@ -45,13 +50,14 @@ export function MessageInput({
       Toast.show(_(msg`Message is too long`))
       return
     }
+    clearDraft()
     onSendMessage(message.trimEnd())
     playHaptic()
     setMessage('')
     setTimeout(() => {
       inputRef.current?.focus()
     }, 100)
-  }, [message, onSendMessage, playHaptic, _])
+  }, [message, onSendMessage, playHaptic, _, clearDraft])
 
   const onInputLayout = React.useCallback(
     (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
@@ -69,6 +75,8 @@ export function MessageInput({
     [scrollToEnd, topInset],
   )
 
+  useSaveMessageDraft(message)
+
   return (
     <View style={a.p_sm}>
       <View
diff --git a/src/screens/Messages/Conversation/MessageInput.web.tsx b/src/screens/Messages/Conversation/MessageInput.web.tsx
index 2ee03bb31..b9506b5bb 100644
--- a/src/screens/Messages/Conversation/MessageInput.web.tsx
+++ b/src/screens/Messages/Conversation/MessageInput.web.tsx
@@ -6,6 +6,10 @@ import Graphemer from 'graphemer'
 import TextareaAutosize from 'react-textarea-autosize'
 
 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 {atoms as a, useTheme} from '#/alf'
 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
@@ -18,7 +22,8 @@ export function MessageInput({
 }) {
   const {_} = useLingui()
   const t = useTheme()
-  const [message, setMessage] = React.useState('')
+  const {getDraft, clearDraft} = useMessageDraft()
+  const [message, setMessage] = React.useState(getDraft)
 
   const onSubmit = React.useCallback(() => {
     if (message.trim() === '') {
@@ -28,9 +33,10 @@ export function MessageInput({
       Toast.show(_(msg`Message is too long`))
       return
     }
+    clearDraft()
     onSendMessage(message.trimEnd())
     setMessage('')
-  }, [message, onSendMessage, _])
+  }, [message, onSendMessage, _, clearDraft])
 
   const onKeyDown = React.useCallback(
     (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -50,6 +56,8 @@ export function MessageInput({
     [],
   )
 
+  useSaveMessageDraft(message)
+
   return (
     <View style={a.p_sm}>
       <View
diff --git a/src/state/messages/index.tsx b/src/state/messages/index.tsx
index 205d17e8c..04ace8d60 100644
--- a/src/state/messages/index.tsx
+++ b/src/state/messages/index.tsx
@@ -2,11 +2,14 @@ import React from 'react'
 
 import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id'
 import {MessagesEventBusProvider} from '#/state/messages/events'
+import {MessageDraftsProvider} from './message-drafts'
 
 export function MessagesProvider({children}: {children: React.ReactNode}) {
   return (
     <CurrentConvoIdProvider>
-      <MessagesEventBusProvider>{children}</MessagesEventBusProvider>
+      <MessageDraftsProvider>
+        <MessagesEventBusProvider>{children}</MessagesEventBusProvider>
+      </MessageDraftsProvider>
     </CurrentConvoIdProvider>
   )
 }
diff --git a/src/state/messages/message-drafts.tsx b/src/state/messages/message-drafts.tsx
new file mode 100644
index 000000000..132e85967
--- /dev/null
+++ b/src/state/messages/message-drafts.tsx
@@ -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>
+  )
+}