Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'mary-ext-feat/image-dnd' into main

+109 -14
+109 -14
src/view/com/composer/text-input/TextInput.web.tsx
··· 9 9 import {Mention} from '@tiptap/extension-mention' 10 10 import {Paragraph} from '@tiptap/extension-paragraph' 11 11 import {Placeholder} from '@tiptap/extension-placeholder' 12 - import {Text} from '@tiptap/extension-text' 12 + import {Text as TiptapText} from '@tiptap/extension-text' 13 13 import isEqual from 'lodash.isequal' 14 14 import {createSuggestion} from './web/Autocomplete' 15 15 import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' ··· 18 18 import {LinkDecorator} from './web/LinkDecorator' 19 19 import {generateJSON} from '@tiptap/html' 20 20 import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' 21 + import {usePalette} from '#/lib/hooks/usePalette' 22 + import {Portal} from '#/components/Portal' 23 + import {Text} from '../../util/text/Text' 24 + import {Trans} from '@lingui/macro' 25 + import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 21 26 22 27 export interface TextInputRef { 23 28 focus: () => void ··· 53 58 ) { 54 59 const autocomplete = useActorAutocompleteFn() 55 60 61 + const pal = usePalette('default') 56 62 const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') 63 + 64 + const [isDropping, setIsDropping] = React.useState(false) 65 + 57 66 const extensions = React.useMemo( 58 67 () => [ 59 68 Document, ··· 68 77 Placeholder.configure({ 69 78 placeholder, 70 79 }), 71 - Text, 80 + TiptapText, 72 81 History, 73 82 Hardbreak, 74 83 ], ··· 88 97 } 89 98 }, [onPhotoPasted]) 90 99 100 + React.useEffect(() => { 101 + const handleDrop = (event: DragEvent) => { 102 + const transfer = event.dataTransfer 103 + if (transfer) { 104 + const items = transfer.items 105 + 106 + getImageFromUri(items, (uri: string) => { 107 + textInputWebEmitter.emit('photo-pasted', uri) 108 + }) 109 + } 110 + 111 + event.preventDefault() 112 + setIsDropping(false) 113 + } 114 + const handleDragEnter = (event: DragEvent) => { 115 + const transfer = event.dataTransfer 116 + 117 + event.preventDefault() 118 + if (transfer && transfer.types.includes('Files')) { 119 + setIsDropping(true) 120 + } 121 + } 122 + const handleDragLeave = (event: DragEvent) => { 123 + event.preventDefault() 124 + setIsDropping(false) 125 + } 126 + 127 + document.body.addEventListener('drop', handleDrop) 128 + document.body.addEventListener('dragenter', handleDragEnter) 129 + document.body.addEventListener('dragover', handleDragEnter) 130 + document.body.addEventListener('dragleave', handleDragLeave) 131 + 132 + return () => { 133 + document.body.removeEventListener('drop', handleDrop) 134 + document.body.removeEventListener('dragenter', handleDragEnter) 135 + document.body.removeEventListener('dragover', handleDragEnter) 136 + document.body.removeEventListener('dragleave', handleDragLeave) 137 + } 138 + }, [setIsDropping]) 139 + 91 140 const editor = useEditor( 92 141 { 93 142 extensions, ··· 177 226 })) 178 227 179 228 return ( 180 - <View style={styles.container}> 181 - <EditorContent editor={editor} /> 182 - </View> 229 + <> 230 + <View style={styles.container}> 231 + <EditorContent editor={editor} /> 232 + </View> 233 + 234 + {isDropping && ( 235 + <Portal> 236 + <Animated.View 237 + style={styles.dropContainer} 238 + entering={FadeIn.duration(80)} 239 + exiting={FadeOut.duration(80)}> 240 + <View style={[pal.view, pal.border, styles.dropModal]}> 241 + <Text 242 + type="lg" 243 + style={[pal.text, pal.borderDark, styles.dropText]}> 244 + <Trans>Drop to add images</Trans> 245 + </Text> 246 + </View> 247 + </Animated.View> 248 + </Portal> 249 + )} 250 + </> 183 251 ) 184 252 }) 185 253 ··· 210 278 marginLeft: 8, 211 279 marginBottom: 10, 212 280 }, 281 + dropContainer: { 282 + backgroundColor: '#0007', 283 + pointerEvents: 'none', 284 + alignItems: 'center', 285 + justifyContent: 'center', 286 + // @ts-ignore web only -prf 287 + position: 'fixed', 288 + padding: 16, 289 + top: 0, 290 + bottom: 0, 291 + left: 0, 292 + right: 0, 293 + }, 294 + dropModal: { 295 + // @ts-ignore web only 296 + boxShadow: 'rgba(0, 0, 0, 0.3) 0px 5px 20px', 297 + padding: 8, 298 + borderWidth: 1, 299 + borderRadius: 16, 300 + }, 301 + dropText: { 302 + paddingVertical: 44, 303 + paddingHorizontal: 36, 304 + borderStyle: 'dashed', 305 + borderRadius: 8, 306 + borderWidth: 2, 307 + }, 213 308 }) 214 309 215 310 function getImageFromUri( ··· 218 313 ) { 219 314 for (let index = 0; index < items.length; index++) { 220 315 const item = items[index] 221 - const {kind, type} = item 316 + const type = item.type 222 317 223 318 if (type === 'text/plain') { 319 + console.log('hit') 224 320 item.getAsString(async itemString => { 225 321 if (isUriImage(itemString)) { 226 322 const response = await fetch(itemString) 227 323 const blob = await response.blob() 228 - blobToDataUri(blob).then(callback, err => console.error(err)) 324 + 325 + if (blob.type.startsWith('image/')) { 326 + blobToDataUri(blob).then(callback, err => console.error(err)) 327 + } 229 328 } 230 329 }) 231 - } 232 - 233 - if (kind === 'file') { 330 + } else if (type.startsWith('image/')) { 234 331 const file = item.getAsFile() 235 332 236 - if (file instanceof Blob) { 237 - blobToDataUri(new Blob([file], {type: item.type})).then(callback, err => 238 - console.error(err), 239 - ) 333 + if (file) { 334 + blobToDataUri(file).then(callback, err => console.error(err)) 240 335 } 241 336 } 242 337 }