diff options
Diffstat (limited to 'src/view/com/composer')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 8 | ||||
-rw-r--r-- | src/view/com/composer/select-language/SuggestedLanguage.tsx | 101 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.web.tsx | 123 | ||||
-rw-r--r-- | src/view/com/composer/text-input/web/EmojiPicker.web.tsx | 3 | ||||
-rw-r--r-- | src/view/com/composer/useExternalLinkFetch.ts | 8 |
5 files changed, 222 insertions, 21 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index e24fdcf3e..1ed6b98a5 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -45,6 +45,7 @@ import {Gallery} from './photos/Gallery' import {MAX_GRAPHEME_LENGTH} from 'lib/constants' import {LabelsBtn} from './labels/LabelsBtn' import {SelectLangBtn} from './select-language/SelectLangBtn' +import {SuggestedLanguage} from './select-language/SuggestedLanguage' import {insertMentionAt} from 'lib/strings/mention-manip' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -73,7 +74,7 @@ export const ComposePost = observer(function ComposePost({ }: Props) { const {currentAccount} = useSession() const {data: currentProfile} = useProfileQuery({did: currentAccount!.did}) - const {activeModals} = useModals() + const {isModalActive, activeModals} = useModals() const {openModal, closeModal} = useModalControls() const {closeComposer} = useComposerControls() const {track} = useAnalytics() @@ -175,11 +176,11 @@ export const ComposePost = observer(function ComposePost({ [onPressCancel], ) useEffect(() => { - if (isWeb) { + if (isWeb && !isModalActive) { window.addEventListener('keydown', onEscape) return () => window.removeEventListener('keydown', onEscape) } - }, [onEscape]) + }, [onEscape, isModalActive]) const onPressAddLinkCard = useCallback( (uri: string) => { @@ -454,6 +455,7 @@ export const ComposePost = observer(function ComposePost({ ))} </View> ) : null} + <SuggestedLanguage text={richtext.text} /> <View style={[pal.border, styles.bottomBar]}> {canSelectImages ? ( <> diff --git a/src/view/com/composer/select-language/SuggestedLanguage.tsx b/src/view/com/composer/select-language/SuggestedLanguage.tsx new file mode 100644 index 000000000..987d89d36 --- /dev/null +++ b/src/view/com/composer/select-language/SuggestedLanguage.tsx @@ -0,0 +1,101 @@ +import React, {useEffect, useState} from 'react' +import {StyleSheet, View} from 'react-native' +import lande from 'lande' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {Text} from '../../util/text/Text' +import {Button} from '../../util/forms/Button' +import {code3ToCode2Strict, codeToLanguageName} from '#/locale/helpers' +import { + toPostLanguages, + useLanguagePrefs, + useLanguagePrefsApi, +} from '#/state/preferences/languages' +import {usePalette} from '#/lib/hooks/usePalette' +import {s} from '#/lib/styles' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' + +// fallbacks for safari +const onIdle = globalThis.requestIdleCallback || (cb => setTimeout(cb, 1)) +const cancelIdle = globalThis.cancelIdleCallback || clearTimeout + +export function SuggestedLanguage({text}: {text: string}) { + const [suggestedLanguage, setSuggestedLanguage] = useState<string>() + const langPrefs = useLanguagePrefs() + const setLangPrefs = useLanguagePrefsApi() + const pal = usePalette('default') + const {_} = useLingui() + + useEffect(() => { + const textTrimmed = text.trim() + + // Don't run the language model on small posts, the results are likely + // to be inaccurate anyway. + if (textTrimmed.length < 40) { + setSuggestedLanguage(undefined) + return + } + + const idle = onIdle(() => { + // Only select languages that have a high confidence and convert to code2 + const result = lande(textTrimmed).filter( + ([lang, value]) => value >= 0.97 && code3ToCode2Strict(lang), + ) + + setSuggestedLanguage( + result.length > 0 ? code3ToCode2Strict(result[0][0]) : undefined, + ) + }) + + return () => cancelIdle(idle) + }, [text]) + + return suggestedLanguage && + !toPostLanguages(langPrefs.postLanguage).includes(suggestedLanguage) ? ( + <View style={[pal.border, styles.infoBar]}> + <FontAwesomeIcon + icon="language" + style={pal.text as FontAwesomeIconStyle} + size={24} + /> + <Text style={[pal.text, s.flex1]}> + <Trans> + Are you writing in{' '} + <Text type="sm-bold" style={pal.text}> + {codeToLanguageName(suggestedLanguage)} + </Text> + ? + </Trans> + </Text> + + <Button + type="default" + onPress={() => setLangPrefs.setPostLanguage(suggestedLanguage)} + accessibilityLabel={_( + msg`Change post language to ${codeToLanguageName(suggestedLanguage)}`, + )} + accessibilityHint=""> + <Text type="button" style={[pal.link, s.fw600]}> + <Trans>Yes</Trans> + </Text> + </Button> + </View> + ) : null +} + +const styles = StyleSheet.create({ + infoBar: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 16, + paddingVertical: 12, + marginHorizontal: 10, + marginBottom: 10, + }, +}) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index ec3a042a3..f2012a630 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' @@ -18,6 +18,11 @@ 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' +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 @@ -53,7 +58,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, @@ -68,7 +77,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( Placeholder.configure({ placeholder, }), - Text, + TiptapText, History, Hardbreak, ], @@ -88,6 +97,46 @@ 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 handleDragEnter = (event: DragEvent) => { + const transfer = event.dataTransfer + + event.preventDefault() + if (transfer && transfer.types.includes('Files')) { + setIsDropping(true) + } + } + const handleDragLeave = (event: DragEvent) => { + event.preventDefault() + setIsDropping(false) + } + + document.body.addEventListener('drop', handleDrop) + document.body.addEventListener('dragenter', handleDragEnter) + document.body.addEventListener('dragover', handleDragEnter) + document.body.addEventListener('dragleave', handleDragLeave) + + return () => { + document.body.removeEventListener('drop', handleDrop) + document.body.removeEventListener('dragenter', handleDragEnter) + document.body.removeEventListener('dragover', handleDragEnter) + document.body.removeEventListener('dragleave', handleDragLeave) + } + }, [setIsDropping]) + const editor = useEditor( { extensions, @@ -177,9 +226,28 @@ export const TextInput = React.forwardRef(function TextInputImpl( })) return ( - <View style={styles.container}> - <EditorContent editor={editor} /> - </View> + <> + <View style={styles.container}> + <EditorContent editor={editor} /> + </View> + + {isDropping && ( + <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, pal.borderDark, styles.dropText]}> + <Trans>Drop to add images</Trans> + </Text> + </View> + </Animated.View> + </Portal> + )} + </> ) }) @@ -210,6 +278,33 @@ const styles = StyleSheet.create({ marginLeft: 8, marginBottom: 10, }, + dropContainer: { + backgroundColor: '#0007', + pointerEvents: 'none', + alignItems: 'center', + justifyContent: 'center', + // @ts-ignore web only -prf + position: 'fixed', + 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: 16, + }, + dropText: { + paddingVertical: 44, + paddingHorizontal: 36, + borderStyle: 'dashed', + borderRadius: 8, + borderWidth: 2, + }, }) function getImageFromUri( @@ -218,25 +313,25 @@ 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') { + console.log('hit') 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)) } } } diff --git a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx index 6d16403ff..149362116 100644 --- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx +++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx @@ -121,7 +121,8 @@ export function EmojiPicker({state, close}: IProps) { const styles = StyleSheet.create({ mask: { - position: 'absolute', + // @ts-ignore web ony + position: 'fixed', top: 0, left: 0, right: 0, diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index ef3958c9d..fc7218d5d 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -18,6 +18,7 @@ import {POST_IMG_MAX} from 'lib/constants' import {logger} from '#/logger' import {getAgent} from '#/state/session' import {useGetPost} from '#/state/queries/post' +import {useFetchDid} from '#/state/queries/handle' export function useExternalLinkFetch({ setQuote, @@ -28,6 +29,7 @@ export function useExternalLinkFetch({ undefined, ) const getPost = useGetPost() + const fetchDid = useFetchDid() useEffect(() => { let aborted = false @@ -55,7 +57,7 @@ export function useExternalLinkFetch({ }, ) } else if (isBskyCustomFeedUrl(extLink.uri)) { - getFeedAsEmbed(getAgent(), extLink.uri).then( + getFeedAsEmbed(getAgent(), fetchDid, extLink.uri).then( ({embed, meta}) => { if (aborted) { return @@ -73,7 +75,7 @@ export function useExternalLinkFetch({ }, ) } else if (isBskyListUrl(extLink.uri)) { - getListAsEmbed(getAgent(), extLink.uri).then( + getListAsEmbed(getAgent(), fetchDid, extLink.uri).then( ({embed, meta}) => { if (aborted) { return @@ -133,7 +135,7 @@ export function useExternalLinkFetch({ }) } return cleanup - }, [extLink, setQuote, getPost]) + }, [extLink, setQuote, getPost, fetchDid]) return {extLink, setExtLink} } |