From fbf2970bae1030849a39410f026e030d2bf39786 Mon Sep 17 00:00:00 2001
From: Mary <pineapplecreamcheese@skiff.com>
Date: Sat, 20 Jan 2024 10:39:28 +0700
Subject: feat: web composer image drag and drop

---
 src/view/com/composer/text-input/TextInput.web.tsx | 63 ++++++++++++++++++----
 1 file changed, 54 insertions(+), 9 deletions(-)

(limited to 'src/view/com/composer/text-input/TextInput.web.tsx')

diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index ec3a042a3..351ca1d0c 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -18,6 +18,7 @@ import {Emoji} from './web/EmojiPicker.web'
 import {LinkDecorator} from './web/LinkDecorator'
 import {generateJSON} from '@tiptap/html'
 import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
+import {usePalette} from '#/lib/hooks/usePalette'
 
 export interface TextInputRef {
   focus: () => void
@@ -53,7 +54,11 @@ export const TextInput = React.forwardRef(function TextInputImpl(
 ) {
   const autocomplete = useActorAutocompleteFn()
 
+  const pal = usePalette('default')
   const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
+
+  const [isDropping, setIsDropping] = React.useState(false)
+
   const extensions = React.useMemo(
     () => [
       Document,
@@ -112,6 +117,32 @@ export const TextInput = React.forwardRef(function TextInputImpl(
             return true
           }
         },
+        handleDOMEvents: {
+          dragover: (_, event) => {
+            const transfer = event.dataTransfer
+            if (transfer && transfer.types.includes('Files')) {
+              setIsDropping(true)
+            }
+          },
+          dragleave: (_, _event) => {
+            setIsDropping(false)
+          },
+          drop: (_, event) => {
+            const transfer = event.dataTransfer
+            if (transfer) {
+              const items = transfer.items
+
+              if (items.length > 0) {
+                event.preventDefault()
+                getImageFromUri(items, (uri: string) => {
+                  textInputWebEmitter.emit('photo-pasted', uri)
+                })
+              }
+            }
+
+            setIsDropping(false)
+          },
+        },
       },
       content: generateJSON(richtext.text.toString(), extensions),
       autofocus: 'end',
@@ -179,6 +210,10 @@ export const TextInput = React.forwardRef(function TextInputImpl(
   return (
     <View style={styles.container}>
       <EditorContent editor={editor} />
+
+      {isDropping && (
+        <View style={[{borderColor: pal.colors.link}, styles.dropContainer]} />
+      )}
     </View>
   )
 })
@@ -210,6 +245,17 @@ const styles = StyleSheet.create({
     marginLeft: 8,
     marginBottom: 10,
   },
+  dropContainer: {
+    pointerEvents: 'none',
+    position: 'absolute',
+    borderWidth: 4,
+    borderRadius: 8,
+    borderStyle: 'dashed',
+    top: 0,
+    bottom: 0,
+    left: 0,
+    right: 0,
+  },
 })
 
 function getImageFromUri(
@@ -218,25 +264,24 @@ function getImageFromUri(
 ) {
   for (let index = 0; index < items.length; index++) {
     const item = items[index]
-    const {kind, type} = item
+    const type = item.type
 
     if (type === 'text/plain') {
       item.getAsString(async itemString => {
         if (isUriImage(itemString)) {
           const response = await fetch(itemString)
           const blob = await response.blob()
-          blobToDataUri(blob).then(callback, err => console.error(err))
+
+          if (blob.type.startsWith('image/')) {
+            blobToDataUri(blob).then(callback, err => console.error(err))
+          }
         }
       })
-    }
-
-    if (kind === 'file') {
+    } else if (type.startsWith('image/')) {
       const file = item.getAsFile()
 
-      if (file instanceof Blob) {
-        blobToDataUri(new Blob([file], {type: item.type})).then(callback, err =>
-          console.error(err),
-        )
+      if (file) {
+        blobToDataUri(file).then(callback, err => console.error(err))
       }
     }
   }
-- 
cgit 1.4.1


From 90b6cfbb917154c50568e6422ef4dd6bfc77c02a Mon Sep 17 00:00:00 2001
From: Mary <pineapplecreamcheese@skiff.com>
Date: Sat, 20 Jan 2024 21:08:11 +0700
Subject: feat: new design

---
 src/view/com/composer/text-input/TextInput.web.tsx | 116 ++++++++++++++-------
 1 file changed, 81 insertions(+), 35 deletions(-)

(limited to 'src/view/com/composer/text-input/TextInput.web.tsx')

diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 351ca1d0c..b0bc78c15 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -9,7 +9,7 @@ import Hardbreak from '@tiptap/extension-hard-break'
 import {Mention} from '@tiptap/extension-mention'
 import {Paragraph} from '@tiptap/extension-paragraph'
 import {Placeholder} from '@tiptap/extension-placeholder'
-import {Text} from '@tiptap/extension-text'
+import {Text as TiptapText} from '@tiptap/extension-text'
 import isEqual from 'lodash.isequal'
 import {createSuggestion} from './web/Autocomplete'
 import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
@@ -19,6 +19,10 @@ import {LinkDecorator} from './web/LinkDecorator'
 import {generateJSON} from '@tiptap/html'
 import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
 import {usePalette} from '#/lib/hooks/usePalette'
+import {Portal} from '#/components/Portal'
+import {Text} from '../../util/text/Text'
+import {Trans} from '@lingui/macro'
+import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
 
 export interface TextInputRef {
   focus: () => void
@@ -73,7 +77,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
       Placeholder.configure({
         placeholder,
       }),
-      Text,
+      TiptapText,
       History,
       Hardbreak,
     ],
@@ -93,6 +97,41 @@ export const TextInput = React.forwardRef(function TextInputImpl(
     }
   }, [onPhotoPasted])
 
+  React.useEffect(() => {
+    const handleDrop = (event: DragEvent) => {
+      const transfer = event.dataTransfer
+      if (transfer) {
+        const items = transfer.items
+
+        getImageFromUri(items, (uri: string) => {
+          textInputWebEmitter.emit('photo-pasted', uri)
+        })
+      }
+
+      event.preventDefault()
+      setIsDropping(false)
+    }
+    const handleDragOver = (event: DragEvent) => {
+      const transfer = event.dataTransfer
+      if (transfer && transfer.types.includes('Files')) {
+        setIsDropping(true)
+      }
+    }
+    const handleDragLeave = (_event: DragEvent) => {
+      setIsDropping(false)
+    }
+
+    document.body.addEventListener('drop', handleDrop)
+    document.body.addEventListener('dragover', handleDragOver)
+    document.body.addEventListener('dragleave', handleDragLeave)
+
+    return () => {
+      document.body.removeEventListener('drop', handleDrop)
+      document.body.removeEventListener('dragover', handleDragOver)
+      document.body.removeEventListener('dragleave', handleDragLeave)
+    }
+  }, [setIsDropping])
+
   const editor = useEditor(
     {
       extensions,
@@ -117,32 +156,6 @@ export const TextInput = React.forwardRef(function TextInputImpl(
             return true
           }
         },
-        handleDOMEvents: {
-          dragover: (_, event) => {
-            const transfer = event.dataTransfer
-            if (transfer && transfer.types.includes('Files')) {
-              setIsDropping(true)
-            }
-          },
-          dragleave: (_, _event) => {
-            setIsDropping(false)
-          },
-          drop: (_, event) => {
-            const transfer = event.dataTransfer
-            if (transfer) {
-              const items = transfer.items
-
-              if (items.length > 0) {
-                event.preventDefault()
-                getImageFromUri(items, (uri: string) => {
-                  textInputWebEmitter.emit('photo-pasted', uri)
-                })
-              }
-            }
-
-            setIsDropping(false)
-          },
-        },
       },
       content: generateJSON(richtext.text.toString(), extensions),
       autofocus: 'end',
@@ -208,13 +221,32 @@ export const TextInput = React.forwardRef(function TextInputImpl(
   }))
 
   return (
-    <View style={styles.container}>
-      <EditorContent editor={editor} />
+    <>
+      <View style={styles.container}>
+        <EditorContent editor={editor} />
+      </View>
 
       {isDropping && (
-        <View style={[{borderColor: pal.colors.link}, styles.dropContainer]} />
+        <Portal>
+          <Animated.View
+            style={styles.dropContainer}
+            entering={FadeIn.duration(80)}
+            exiting={FadeOut.duration(80)}>
+            <View style={[pal.view, pal.border, styles.dropModal]}>
+              <Text
+                type="lg"
+                style={[
+                  pal.text,
+                  {borderColor: pal.colors.borderDark},
+                  styles.dropText,
+                ]}>
+                <Trans>Drop to add images</Trans>
+              </Text>
+            </View>
+          </Animated.View>
+        </Portal>
       )}
-    </View>
+    </>
   )
 })
 
@@ -246,16 +278,30 @@ const styles = StyleSheet.create({
     marginBottom: 10,
   },
   dropContainer: {
+    backgroundColor: '#0007',
     pointerEvents: 'none',
+    alignItems: 'center',
+    justifyContent: 'center',
     position: 'absolute',
-    borderWidth: 4,
-    borderRadius: 8,
-    borderStyle: 'dashed',
+    padding: 16,
     top: 0,
     bottom: 0,
     left: 0,
     right: 0,
   },
+  dropModal: {
+    // @ts-ignore web only
+    boxShadow: 'rgba(0, 0, 0, 0.3) 0px 5px 20px',
+    padding: 8,
+    borderWidth: 1,
+    borderRadius: 8,
+  },
+  dropText: {
+    padding: 32,
+    borderStyle: 'dashed',
+    borderRadius: 8,
+    borderWidth: 2,
+  },
 })
 
 function getImageFromUri(
-- 
cgit 1.4.1


From 854718c5551139109a0eef60126b9a39117f833f Mon Sep 17 00:00:00 2001
From: Mary <pineapplecreamcheese@skiff.com>
Date: Sat, 20 Jan 2024 21:20:07 +0700
Subject: refactor: use right borderDark

---
 src/view/com/composer/text-input/TextInput.web.tsx | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

(limited to 'src/view/com/composer/text-input/TextInput.web.tsx')

diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index b0bc78c15..3ffe3baaf 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -235,11 +235,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
             <View style={[pal.view, pal.border, styles.dropModal]}>
               <Text
                 type="lg"
-                style={[
-                  pal.text,
-                  {borderColor: pal.colors.borderDark},
-                  styles.dropText,
-                ]}>
+                style={[pal.text, pal.borderDark, styles.dropText]}>
                 <Trans>Drop to add images</Trans>
               </Text>
             </View>
-- 
cgit 1.4.1


From 88f2a73ad6eb0f3f4b9b5a10aaf3d3be090bc5e5 Mon Sep 17 00:00:00 2001
From: Mary <pineapplecreamcheese@skiff.com>
Date: Sun, 21 Jan 2024 04:24:58 +0700
Subject: fix: always call preventDefault

---
 src/view/com/composer/text-input/TextInput.web.tsx | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

(limited to 'src/view/com/composer/text-input/TextInput.web.tsx')

diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 3ffe3baaf..a2344ad3f 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -111,24 +111,32 @@ export const TextInput = React.forwardRef(function TextInputImpl(
       event.preventDefault()
       setIsDropping(false)
     }
-    const handleDragOver = (event: DragEvent) => {
+    const handleDragEnter = (event: DragEvent) => {
       const transfer = event.dataTransfer
+
+      event.preventDefault()
       if (transfer && transfer.types.includes('Files')) {
         setIsDropping(true)
       }
     }
-    const handleDragLeave = (_event: DragEvent) => {
+    const handleDragLeave = (event: DragEvent) => {
+      event.preventDefault()
       setIsDropping(false)
     }
+    const handleDragOver = (event: DragEvent) => {
+      event.preventDefault()
+    }
 
     document.body.addEventListener('drop', handleDrop)
-    document.body.addEventListener('dragover', handleDragOver)
+    document.body.addEventListener('dragenter', handleDragEnter)
     document.body.addEventListener('dragleave', handleDragLeave)
+    document.body.addEventListener('dragover', handleDragOver)
 
     return () => {
       document.body.removeEventListener('drop', handleDrop)
-      document.body.removeEventListener('dragover', handleDragOver)
+      document.body.removeEventListener('dragenter', handleDragEnter)
       document.body.removeEventListener('dragleave', handleDragLeave)
+      document.body.removeEventListener('dragover', handleDragOver)
     }
   }, [setIsDropping])
 
-- 
cgit 1.4.1


From f5356c3d479115103982079a5e76541d3ce4e11c Mon Sep 17 00:00:00 2001
From: Mary <pineapplecreamcheese@skiff.com>
Date: Sun, 21 Jan 2024 04:25:16 +0700
Subject: fix: design adjustments

---
 src/view/com/composer/text-input/TextInput.web.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

(limited to 'src/view/com/composer/text-input/TextInput.web.tsx')

diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index a2344ad3f..db1b6b6f4 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -298,10 +298,11 @@ const styles = StyleSheet.create({
     boxShadow: 'rgba(0, 0, 0, 0.3) 0px 5px 20px',
     padding: 8,
     borderWidth: 1,
-    borderRadius: 8,
+    borderRadius: 16,
   },
   dropText: {
-    padding: 32,
+    paddingVertical: 44,
+    paddingHorizontal: 36,
     borderStyle: 'dashed',
     borderRadius: 8,
     borderWidth: 2,
-- 
cgit 1.4.1


From 526411ab25c66f787e9e702ef26240a165c45a61 Mon Sep 17 00:00:00 2001
From: Mary <pineapplecreamcheese@skiff.com>
Date: Sun, 21 Jan 2024 04:34:29 +0700
Subject: fix: firefox edge-case

---
 src/view/com/composer/text-input/TextInput.web.tsx | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

(limited to 'src/view/com/composer/text-input/TextInput.web.tsx')

diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index db1b6b6f4..8898ab977 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -123,20 +123,17 @@ export const TextInput = React.forwardRef(function TextInputImpl(
       event.preventDefault()
       setIsDropping(false)
     }
-    const handleDragOver = (event: DragEvent) => {
-      event.preventDefault()
-    }
 
     document.body.addEventListener('drop', handleDrop)
     document.body.addEventListener('dragenter', handleDragEnter)
+    document.body.addEventListener('dragover', handleDragEnter)
     document.body.addEventListener('dragleave', handleDragLeave)
-    document.body.addEventListener('dragover', handleDragOver)
 
     return () => {
       document.body.removeEventListener('drop', handleDrop)
       document.body.removeEventListener('dragenter', handleDragEnter)
+      document.body.removeEventListener('dragover', handleDragEnter)
       document.body.removeEventListener('dragleave', handleDragLeave)
-      document.body.removeEventListener('dragover', handleDragOver)
     }
   }, [setIsDropping])
 
-- 
cgit 1.4.1

