diff options
Diffstat (limited to 'src/view/com/composer')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 160 | ||||
-rw-r--r-- | src/view/com/composer/labels/LabelsBtn.tsx | 64 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.web.tsx | 32 |
3 files changed, 164 insertions, 92 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 0fae996ff..ecfef3ecd 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -32,6 +32,8 @@ import {s, colors, gradients} from 'lib/styles' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {cleanError} from 'lib/strings/errors' +import {shortenLinks} from 'lib/strings/rich-text-manip' +import {toShortUrl} from 'lib/strings/url-helpers' import {SelectPhotoBtn} from './photos/SelectPhotoBtn' import {OpenCameraBtn} from './photos/OpenCameraBtn' import {usePalette} from 'lib/hooks/usePalette' @@ -41,6 +43,7 @@ import {isDesktopWeb, isAndroid, isIOS} from 'platform/detection' import {GalleryModel} from 'state/models/media/gallery' import {Gallery} from './photos/Gallery' import {MAX_GRAPHEME_LENGTH} from 'lib/constants' +import {LabelsBtn} from './labels/LabelsBtn' import {SelectLangBtn} from './select-language/SelectLangBtn' type Props = ComposerOpts & { @@ -62,11 +65,14 @@ export const ComposePost = observer(function ComposePost({ const [processingState, setProcessingState] = useState('') const [error, setError] = useState('') const [richtext, setRichText] = useState(new RichText({text: ''})) - const graphemeLength = useMemo(() => richtext.graphemeLength, [richtext]) + const graphemeLength = useMemo(() => { + return shortenLinks(richtext).graphemeLength + }, [richtext]) const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>( initQuote, ) const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) + const [labels, setLabels] = useState<string[]>([]) const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set()) const gallery = useMemo(() => new GalleryModel(store), [store]) @@ -145,76 +151,59 @@ export const ComposePost = observer(function ComposePost({ [gallery, track], ) - const onPressPublish = useCallback( - async (rt: RichText) => { - if (isProcessing || rt.graphemeLength > MAX_GRAPHEME_LENGTH) { - return - } - if (store.preferences.requireAltTextEnabled && gallery.needsAltText) { - return - } + const onPressPublish = async () => { + if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { + return + } + if (store.preferences.requireAltTextEnabled && gallery.needsAltText) { + return + } - setError('') + setError('') - if (rt.text.trim().length === 0 && gallery.isEmpty) { - setError('Did you want to say anything?') - return - } + if (richtext.text.trim().length === 0 && gallery.isEmpty) { + setError('Did you want to say anything?') + return + } - setIsProcessing(true) + setIsProcessing(true) - let createdPost - try { - createdPost = await apilib.post(store, { - rawText: rt.text, - replyTo: replyTo?.uri, - images: gallery.images, - quote: quote, - extLink: extLink, - onStateChange: setProcessingState, - knownHandles: autocompleteView.knownHandles, - langs: store.preferences.postLanguages, - }) - } catch (e: any) { - if (extLink) { - setExtLink({ - ...extLink, - isLoading: true, - localThumb: undefined, - } as apilib.ExternalEmbedDraft) - } - setError(cleanError(e.message)) - setIsProcessing(false) - return - } finally { - track('Create Post', { - imageCount: gallery.size, - }) - if (replyTo && replyTo.uri) track('Post:Reply') - } - if (!replyTo) { - await store.me.mainFeed.addPostToTop(createdPost.uri) + try { + await apilib.post(store, { + rawText: richtext.text, + replyTo: replyTo?.uri, + images: gallery.images, + quote, + extLink, + labels, + onStateChange: setProcessingState, + knownHandles: autocompleteView.knownHandles, + langs: store.preferences.postLanguages, + }) + } catch (e: any) { + if (extLink) { + setExtLink({ + ...extLink, + isLoading: true, + localThumb: undefined, + } as apilib.ExternalEmbedDraft) } - onPost?.() - onClose() - Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) - }, - [ - isProcessing, - setError, - setIsProcessing, - replyTo, - autocompleteView.knownHandles, - extLink, - onClose, - onPost, - quote, - setExtLink, - store, - track, - gallery, - ], - ) + setError(cleanError(e.message)) + setIsProcessing(false) + return + } finally { + track('Create Post', { + imageCount: gallery.size, + }) + if (replyTo && replyTo.uri) track('Post:Reply') + } + if (!replyTo) { + store.me.mainFeed.onPostCreated() + } + onPost?.() + onClose() + Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) + } const canPost = useMemo( () => @@ -229,6 +218,7 @@ export const ComposePost = observer(function ComposePost({ const selectTextInputPlaceholder = replyTo ? 'Write your reply' : `What's up?` const canSelectImages = useMemo(() => gallery.size < 4, [gallery.size]) + const hasMedia = gallery.size > 0 || Boolean(extLink) return ( <KeyboardAvoidingView @@ -247,6 +237,7 @@ export const ComposePost = observer(function ComposePost({ <Text style={[pal.link, s.f18]}>Cancel</Text> </TouchableOpacity> <View style={s.flex1} /> + <LabelsBtn labels={labels} onChange={setLabels} hasMedia={hasMedia} /> {isProcessing ? ( <View style={styles.postBtn}> <ActivityIndicator /> @@ -254,9 +245,7 @@ export const ComposePost = observer(function ComposePost({ ) : canPost ? ( <TouchableOpacity testID="composerPublishBtn" - onPress={() => { - onPressPublish(richtext) - }} + onPress={onPressPublish} accessibilityRole="button" accessibilityLabel={replyTo ? 'Publish reply' : 'Publish post'} accessibilityHint={ @@ -366,20 +355,23 @@ export const ComposePost = observer(function ComposePost({ </ScrollView> {!extLink && suggestedLinks.size > 0 ? ( <View style={s.mb5}> - {Array.from(suggestedLinks).map(url => ( - <TouchableOpacity - key={`suggested-${url}`} - testID="addLinkCardBtn" - style={[pal.borderDark, styles.addExtLinkBtn]} - onPress={() => onPressAddLinkCard(url)} - accessibilityRole="button" - accessibilityLabel="Add link card" - accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}> - <Text style={pal.text}> - Add link card: <Text style={pal.link}>{url}</Text> - </Text> - </TouchableOpacity> - ))} + {Array.from(suggestedLinks) + .slice(0, 3) + .map(url => ( + <TouchableOpacity + key={`suggested-${url}`} + testID="addLinkCardBtn" + style={[pal.borderDark, styles.addExtLinkBtn]} + onPress={() => onPressAddLinkCard(url)} + accessibilityRole="button" + accessibilityLabel="Add link card" + accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}> + <Text style={pal.text}> + Add link card:{' '} + <Text style={pal.link}>{toShortUrl(url)}</Text> + </Text> + </TouchableOpacity> + ))} </View> ) : null} <View style={[pal.border, styles.bottomBar]}> @@ -408,7 +400,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', paddingTop: isDesktopWeb ? 10 : undefined, - paddingBottom: 10, + paddingBottom: isDesktopWeb ? 10 : 4, paddingHorizontal: 20, height: 55, }, diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx new file mode 100644 index 000000000..96908d47f --- /dev/null +++ b/src/view/com/composer/labels/LabelsBtn.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import {Keyboard, StyleSheet} from 'react-native' +import {observer} from 'mobx-react-lite' +import {Button} from 'view/com/util/forms/Button' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' +import {ShieldExclamation} from 'lib/icons' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' +import {isNative} from 'platform/detection' + +export const LabelsBtn = observer(function LabelsBtn({ + labels, + hasMedia, + onChange, +}: { + labels: string[] + hasMedia: boolean + onChange: (v: string[]) => void +}) { + const pal = usePalette('default') + const store = useStores() + + return ( + <Button + type="default-light" + testID="labelsBtn" + style={[styles.button, !hasMedia && styles.dimmed]} + accessibilityLabel="Content warnings" + accessibilityHint="" + onPress={() => { + if (isNative) { + if (Keyboard.isVisible()) { + Keyboard.dismiss() + } + } + store.shell.openModal({name: 'self-label', labels, hasMedia, onChange}) + }}> + <ShieldExclamation style={pal.link} size={26} /> + {labels.length > 0 ? ( + <FontAwesomeIcon + icon="check" + size={16} + style={pal.link as FontAwesomeIconStyle} + /> + ) : null} + </Button> + ) +}) + +const styles = StyleSheet.create({ + button: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 14, + marginRight: 4, + }, + dimmed: { + opacity: 0.4, + }, + label: { + maxWidth: 100, + }, +}) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 245c17b9c..f64880e15 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -1,6 +1,7 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {RichText} from '@atproto/api' +import EventEmitter from 'eventemitter3' import {useEditor, EditorContent, JSONContent} from '@tiptap/react' import {Document} from '@tiptap/extension-document' import History from '@tiptap/extension-history' @@ -53,6 +54,22 @@ export const TextInput = React.forwardRef( 'ProseMirror-dark', ) + // we use a memoized emitter to propagate events out of tiptap + // without triggering re-runs of the useEditor hook + const emitter = React.useMemo(() => new EventEmitter(), []) + React.useEffect(() => { + emitter.addListener('publish', onPressPublish) + return () => { + emitter.removeListener('publish', onPressPublish) + } + }, [emitter, onPressPublish]) + React.useEffect(() => { + emitter.addListener('photo-pasted', onPhotoPasted) + return () => { + emitter.removeListener('photo-pasted', onPhotoPasted) + } + }, [emitter, onPhotoPasted]) + const editor = useEditor( { extensions: [ @@ -60,6 +77,7 @@ export const TextInput = React.forwardRef( Link.configure({ protocols: ['http', 'https'], autolink: true, + linkOnPaste: false, }), Mention.configure({ HTMLAttributes: { @@ -86,16 +104,13 @@ export const TextInput = React.forwardRef( return } - getImageFromUri(items, onPhotoPasted) + getImageFromUri(items, (uri: string) => { + emitter.emit('photo-pasted', uri) + }) }, handleKeyDown: (_, event) => { if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') { - // Workaround relying on previous state from `setRichText` to - // get the updated text content during editor initialization - setRichText((state: RichText) => { - onPressPublish(state) - return state - }) + emitter.emit('publish') } }, }, @@ -107,6 +122,7 @@ export const TextInput = React.forwardRef( const json = editorProp.getJSON() const newRt = new RichText({text: editorJsonToText(json).trim()}) + newRt.detectFacetsWithoutResolution() setRichText(newRt) const newSuggestedLinks = new Set(editorJsonToLinks(json)) @@ -115,7 +131,7 @@ export const TextInput = React.forwardRef( } }, }, - [modeClass], + [modeClass, emitter], ) React.useImperativeHandle(ref, () => ({ |