diff options
Diffstat (limited to 'src/view/com/composer')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 102 | ||||
-rw-r--r-- | src/view/com/composer/ExternalEmbed.tsx | 5 | ||||
-rw-r--r-- | src/view/com/composer/Prompt.tsx | 15 | ||||
-rw-r--r-- | src/view/com/composer/labels/LabelsBtn.tsx | 16 | ||||
-rw-r--r-- | src/view/com/composer/photos/Gallery.tsx | 47 | ||||
-rw-r--r-- | src/view/com/composer/photos/OpenCameraBtn.tsx | 11 | ||||
-rw-r--r-- | src/view/com/composer/photos/SelectPhotoBtn.tsx | 5 | ||||
-rw-r--r-- | src/view/com/composer/select-language/SelectLangBtn.tsx | 46 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.tsx | 21 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.web.tsx | 10 | ||||
-rw-r--r-- | src/view/com/composer/text-input/mobile/Autocomplete.tsx | 39 | ||||
-rw-r--r-- | src/view/com/composer/text-input/web/Autocomplete.tsx | 11 | ||||
-rw-r--r-- | src/view/com/composer/useExternalLinkFetch.ts | 23 |
13 files changed, 206 insertions, 145 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index e44a0ce01..6f058d39e 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -16,7 +16,6 @@ import LinearGradient from 'react-native-linear-gradient' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {RichText} from '@atproto/api' import {useAnalytics} from 'lib/analytics/analytics' -import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible' import {ExternalEmbed} from './ExternalEmbed' import {Text} from '../util/text/Text' @@ -26,9 +25,8 @@ import * as Toast from '../util/Toast' import {TextInput, TextInputRef} from './text-input/TextInput' import {CharProgress} from './char-progress/CharProgress' import {UserAvatar} from '../util/UserAvatar' -import {useStores} from 'state/index' import * as apilib from 'lib/api/index' -import {ComposerOpts} from 'state/models/ui/shell' +import {ComposerOpts} from 'state/shell/composer' import {s, colors, gradients} from 'lib/styles' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' @@ -49,6 +47,18 @@ import {LabelsBtn} from './labels/LabelsBtn' import {SelectLangBtn} from './select-language/SelectLangBtn' import {EmojiPickerButton} from './text-input/web/EmojiPicker.web' import {insertMentionAt} from 'lib/strings/mention-manip' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useModals, useModalControls} from '#/state/modals' +import {useRequireAltTextEnabled} from '#/state/preferences' +import { + useLanguagePrefs, + useLanguagePrefsApi, + toPostLanguages, +} from '#/state/preferences/languages' +import {useSession, getAgent} from '#/state/session' +import {useProfileQuery} from '#/state/queries/profile' +import {useComposerControls} from '#/state/shell/composer' type Props = ComposerOpts export const ComposePost = observer(function ComposePost({ @@ -57,10 +67,18 @@ export const ComposePost = observer(function ComposePost({ quote: initQuote, mention: initMention, }: Props) { + const {currentAccount} = useSession() + const {data: currentProfile} = useProfileQuery({did: currentAccount!.did}) + const {activeModals} = useModals() + const {openModal, closeModal} = useModalControls() + const {closeComposer} = useComposerControls() const {track} = useAnalytics() const pal = usePalette('default') const {isDesktop, isMobile} = useWebMediaQueries() - const store = useStores() + const {_} = useLingui() + const requireAltTextEnabled = useRequireAltTextEnabled() + const langPrefs = useLanguagePrefs() + const setLangPrefs = useLanguagePrefsApi() const textInput = useRef<TextInputRef>(null) const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true}) const [isProcessing, setIsProcessing] = useState(false) @@ -86,15 +104,10 @@ export const ComposePost = observer(function ComposePost({ 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]) + const gallery = useMemo(() => new GalleryModel(), []) const onClose = useCallback(() => { - store.shell.closeComposer() - }, [store]) - - const autocompleteView = useMemo<UserAutocompleteModel>( - () => new UserAutocompleteModel(store), - [store], - ) + closeComposer() + }, [closeComposer]) const insets = useSafeAreaInsets() const viewStyles = useMemo( @@ -108,27 +121,27 @@ export const ComposePost = observer(function ComposePost({ const onPressCancel = useCallback(() => { if (graphemeLength > 0 || !gallery.isEmpty) { - if (store.shell.activeModals.some(modal => modal.name === 'confirm')) { - store.shell.closeModal() + if (activeModals.some(modal => modal.name === 'confirm')) { + closeModal() } if (Keyboard) { Keyboard.dismiss() } - store.shell.openModal({ + openModal({ name: 'confirm', - title: 'Discard draft', + title: _(msg`Discard draft`), onPressConfirm: onClose, onPressCancel: () => { - store.shell.closeModal() + closeModal() }, - message: "Are you sure you'd like to discard this draft?", - confirmBtnText: 'Discard', + message: _(msg`Are you sure you'd like to discard this draft?`), + confirmBtnText: _(msg`Discard`), confirmBtnStyle: {backgroundColor: colors.red4}, }) } else { onClose() } - }, [store, onClose, graphemeLength, gallery]) + }, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery, _]) // android back button useEffect(() => { if (!isAndroid) { @@ -147,11 +160,6 @@ export const ComposePost = observer(function ComposePost({ } }, [onPressCancel]) - // initial setup - useEffect(() => { - autocompleteView.setup() - }, [autocompleteView]) - // listen to escape key on desktop web const onEscape = useCallback( (e: KeyboardEvent) => { @@ -187,7 +195,7 @@ export const ComposePost = observer(function ComposePost({ if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { return } - if (store.preferences.requireAltTextEnabled && gallery.needsAltText) { + if (requireAltTextEnabled && gallery.needsAltText) { return } @@ -201,7 +209,7 @@ export const ComposePost = observer(function ComposePost({ setIsProcessing(true) try { - await apilib.post(store, { + await apilib.post(getAgent(), { rawText: richtext.text, replyTo: replyTo?.uri, images: gallery.images, @@ -209,8 +217,7 @@ export const ComposePost = observer(function ComposePost({ extLink, labels, onStateChange: setProcessingState, - knownHandles: autocompleteView.knownHandles, - langs: store.preferences.postLanguages, + langs: toPostLanguages(langPrefs.postLanguage), }) } catch (e: any) { if (extLink) { @@ -230,9 +237,9 @@ export const ComposePost = observer(function ComposePost({ if (replyTo && replyTo.uri) track('Post:Reply') } if (!replyTo) { - store.me.mainFeed.onPostCreated() + // TODO onPostCreated } - store.preferences.savePostLanguageToHistory() + setLangPrefs.savePostLanguageToHistory() onPost?.() onClose() Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) @@ -241,12 +248,8 @@ export const ComposePost = observer(function ComposePost({ const canPost = useMemo( () => graphemeLength <= MAX_GRAPHEME_LENGTH && - (!store.preferences.requireAltTextEnabled || !gallery.needsAltText), - [ - graphemeLength, - store.preferences.requireAltTextEnabled, - gallery.needsAltText, - ], + (!requireAltTextEnabled || !gallery.needsAltText), + [graphemeLength, requireAltTextEnabled, gallery.needsAltText], ) const selectTextInputPlaceholder = replyTo ? 'Write your reply' : `What's up?` @@ -265,9 +268,11 @@ export const ComposePost = observer(function ComposePost({ onPress={onPressCancel} onAccessibilityEscape={onPressCancel} accessibilityRole="button" - accessibilityLabel="Cancel" + accessibilityLabel={_(msg`Cancel`)} accessibilityHint="Closes post composer and discards post draft"> - <Text style={[pal.link, s.f18]}>Cancel</Text> + <Text style={[pal.link, s.f18]}> + <Trans>Cancel</Trans> + </Text> </TouchableOpacity> <View style={s.flex1} /> {isProcessing ? ( @@ -308,13 +313,15 @@ export const ComposePost = observer(function ComposePost({ </TouchableOpacity> ) : ( <View style={[styles.postBtn, pal.btn]}> - <Text style={[pal.textLight, s.f16, s.bold]}>Post</Text> + <Text style={[pal.textLight, s.f16, s.bold]}> + <Trans>Post</Trans> + </Text> </View> )} </> )} </View> - {store.preferences.requireAltTextEnabled && gallery.needsAltText && ( + {requireAltTextEnabled && gallery.needsAltText && ( <View style={[styles.reminderLine, pal.viewLight]}> <View style={styles.errorIcon}> <FontAwesomeIcon @@ -324,7 +331,7 @@ export const ComposePost = observer(function ComposePost({ /> </View> <Text style={[pal.text, s.flex1]}> - One or more images is missing alt text. + <Trans>One or more images is missing alt text.</Trans> </Text> </View> )} @@ -366,13 +373,12 @@ export const ComposePost = observer(function ComposePost({ styles.textInputLayout, isNative && styles.textInputLayoutMobile, ]}> - <UserAvatar avatar={store.me.avatar} size={50} /> + <UserAvatar avatar={currentProfile?.avatar} size={50} /> <TextInput ref={textInput} richtext={richtext} placeholder={selectTextInputPlaceholder} suggestedLinks={suggestedLinks} - autocompleteView={autocompleteView} autoFocus={true} setRichText={setRichText} onPhotoPasted={onPhotoPasted} @@ -380,7 +386,7 @@ export const ComposePost = observer(function ComposePost({ onSuggestedLinksChanged={setSuggestedLinks} onError={setError} accessible={true} - accessibilityLabel="Write post" + accessibilityLabel={_(msg`Write post`)} accessibilityHint={`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`} /> </View> @@ -409,11 +415,11 @@ export const ComposePost = observer(function ComposePost({ style={[pal.borderDark, styles.addExtLinkBtn]} onPress={() => onPressAddLinkCard(url)} accessibilityRole="button" - accessibilityLabel="Add link card" + accessibilityLabel={_(msg`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> + <Trans>Add link card:</Trans> + <Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text> </Text> </TouchableOpacity> ))} diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index c9200ec63..502e4b4d2 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -11,6 +11,8 @@ import {Text} from '../util/text/Text' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {ExternalEmbedDraft} from 'lib/api/index' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' export const ExternalEmbed = ({ link, @@ -21,6 +23,7 @@ export const ExternalEmbed = ({ }) => { const pal = usePalette('default') const palError = usePalette('error') + const {_} = useLingui() if (!link) { return <View /> } @@ -64,7 +67,7 @@ export const ExternalEmbed = ({ style={styles.removeBtn} onPress={onRemove} accessibilityRole="button" - accessibilityLabel="Remove image preview" + accessibilityLabel={_(msg`Remove image preview`)} accessibilityHint={`Removes default thumbnail from ${link.uri}`} onAccessibilityEscape={onRemove}> <FontAwesomeIcon size={18} icon="xmark" style={s.white} /> diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx index e54404f52..ae055f9ac 100644 --- a/src/view/com/composer/Prompt.tsx +++ b/src/view/com/composer/Prompt.tsx @@ -3,12 +3,17 @@ import {StyleSheet, TouchableOpacity} from 'react-native' import {UserAvatar} from '../util/UserAvatar' import {Text} from '../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useSession} from '#/state/session' +import {useProfileQuery} from '#/state/queries/profile' export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) { - const store = useStores() + const {currentAccount} = useSession() + const {data: profile} = useProfileQuery({did: currentAccount?.did}) const pal = usePalette('default') + const {_} = useLingui() const {isDesktop} = useWebMediaQueries() return ( <TouchableOpacity @@ -16,16 +21,16 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) { style={[pal.view, pal.border, styles.prompt]} onPress={() => onPressCompose()} accessibilityRole="button" - accessibilityLabel="Compose reply" + accessibilityLabel={_(msg`Compose reply`)} accessibilityHint="Opens composer"> - <UserAvatar avatar={store.me.avatar} size={38} /> + <UserAvatar avatar={profile?.avatar} size={38} /> <Text type="xl" style={[ pal.text, isDesktop ? styles.labelDesktopWeb : styles.labelMobile, ]}> - Write your reply + <Trans>Write your reply</Trans> </Text> </TouchableOpacity> ) diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx index 96908d47f..a10684691 100644 --- a/src/view/com/composer/labels/LabelsBtn.tsx +++ b/src/view/com/composer/labels/LabelsBtn.tsx @@ -1,15 +1,16 @@ 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' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {useModalControls} from '#/state/modals' -export const LabelsBtn = observer(function LabelsBtn({ +export function LabelsBtn({ labels, hasMedia, onChange, @@ -19,14 +20,15 @@ export const LabelsBtn = observer(function LabelsBtn({ onChange: (v: string[]) => void }) { const pal = usePalette('default') - const store = useStores() + const {_} = useLingui() + const {openModal} = useModalControls() return ( <Button type="default-light" testID="labelsBtn" style={[styles.button, !hasMedia && styles.dimmed]} - accessibilityLabel="Content warnings" + accessibilityLabel={_(msg`Content warnings`)} accessibilityHint="" onPress={() => { if (isNative) { @@ -34,7 +36,7 @@ export const LabelsBtn = observer(function LabelsBtn({ Keyboard.dismiss() } } - store.shell.openModal({name: 'self-label', labels, hasMedia, onChange}) + openModal({name: 'self-label', labels, hasMedia, onChange}) }}> <ShieldExclamation style={pal.link} size={26} /> {labels.length > 0 ? ( @@ -46,7 +48,7 @@ export const LabelsBtn = observer(function LabelsBtn({ ) : null} </Button> ) -}) +} const styles = StyleSheet.create({ button: { diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index fcd99842a..69c8debb0 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -7,11 +7,13 @@ import {s, colors} from 'lib/styles' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {Image} from 'expo-image' import {Text} from 'view/com/util/text/Text' -import {openAltTextModal} from 'lib/media/alt-text' import {Dimensions} from 'lib/media/types' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useModalControls} from '#/state/modals' +import {isNative} from 'platform/detection' const IMAGE_GAP = 8 @@ -47,9 +49,10 @@ const GalleryInner = observer(function GalleryImpl({ gallery, containerInfo, }: GalleryInnerProps) { - const store = useStores() const pal = usePalette('default') + const {_} = useLingui() const {isMobile} = useWebMediaQueries() + const {openModal} = useModalControls() let side: number @@ -113,15 +116,18 @@ const GalleryInner = observer(function GalleryImpl({ <TouchableOpacity testID="altTextButton" accessibilityRole="button" - accessibilityLabel="Add alt text" + accessibilityLabel={_(msg`Add alt text`)} accessibilityHint="" onPress={() => { Keyboard.dismiss() - openAltTextModal(store, image) + openModal({ + name: 'alt-text-image', + image, + }) }} style={[styles.altTextControl, altTextControlStyle]}> <Text style={styles.altTextControlLabel} accessible={false}> - ALT + <Trans>ALT</Trans> </Text> {image.altText.length > 0 ? ( <FontAwesomeIcon @@ -135,9 +141,19 @@ const GalleryInner = observer(function GalleryImpl({ <TouchableOpacity testID="editPhotoButton" accessibilityRole="button" - accessibilityLabel="Edit image" + accessibilityLabel={_(msg`Edit image`)} accessibilityHint="" - onPress={() => gallery.edit(image)} + onPress={() => { + if (isNative) { + gallery.crop(image) + } else { + openModal({ + name: 'edit-image', + image, + gallery, + }) + } + }} style={styles.imageControl}> <FontAwesomeIcon icon="pen" @@ -148,7 +164,7 @@ const GalleryInner = observer(function GalleryImpl({ <TouchableOpacity testID="removePhotoButton" accessibilityRole="button" - accessibilityLabel="Remove image" + accessibilityLabel={_(msg`Remove image`)} accessibilityHint="" onPress={() => gallery.remove(image)} style={styles.imageControl}> @@ -161,11 +177,14 @@ const GalleryInner = observer(function GalleryImpl({ </View> <TouchableOpacity accessibilityRole="button" - accessibilityLabel="Add alt text" + accessibilityLabel={_(msg`Add alt text`)} accessibilityHint="" onPress={() => { Keyboard.dismiss() - openAltTextModal(store, image) + openModal({ + name: 'alt-text-image', + image, + }) }} style={styles.altTextHiddenRegion} /> @@ -187,8 +206,10 @@ const GalleryInner = observer(function GalleryImpl({ <FontAwesomeIcon icon="info" size={12} color={pal.colors.text} /> </View> <Text type="sm" style={[pal.textLight, s.flex1]}> - Alt text describes images for blind and low-vision users, and helps - give context to everyone. + <Trans> + Alt text describes images for blind and low-vision users, and helps + give context to everyone. + </Trans> </Text> </View> </> diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index 99e820d51..69f63c55f 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -6,13 +6,14 @@ import { } from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics/analytics' -import {useStores} from 'state/index' import {openCamera} from 'lib/media/picker' import {useCameraPermission} from 'lib/hooks/usePermissions' import {HITSLOP_10, POST_IMG_MAX} from 'lib/constants' import {GalleryModel} from 'state/models/media/gallery' import {isMobileWeb, isNative} from 'platform/detection' import {logger} from '#/logger' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' type Props = { gallery: GalleryModel @@ -21,7 +22,7 @@ type Props = { export function OpenCameraBtn({gallery}: Props) { const pal = usePalette('default') const {track} = useAnalytics() - const store = useStores() + const {_} = useLingui() const {requestCameraAccessIfNeeded} = useCameraPermission() const onPressTakePicture = useCallback(async () => { @@ -31,7 +32,7 @@ export function OpenCameraBtn({gallery}: Props) { return } - const img = await openCamera(store, { + const img = await openCamera({ width: POST_IMG_MAX.width, height: POST_IMG_MAX.height, freeStyleCropEnabled: true, @@ -42,7 +43,7 @@ export function OpenCameraBtn({gallery}: Props) { // ignore logger.warn('Error using camera', {error: err}) } - }, [gallery, track, store, requestCameraAccessIfNeeded]) + }, [gallery, track, requestCameraAccessIfNeeded]) const shouldShowCameraButton = isNative || isMobileWeb if (!shouldShowCameraButton) { @@ -56,7 +57,7 @@ export function OpenCameraBtn({gallery}: Props) { style={styles.button} hitSlop={HITSLOP_10} accessibilityRole="button" - accessibilityLabel="Camera" + accessibilityLabel={_(msg`Camera`)} accessibilityHint="Opens camera on device"> <FontAwesomeIcon icon="camera" diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx index a6826eb98..af0a22b01 100644 --- a/src/view/com/composer/photos/SelectPhotoBtn.tsx +++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx @@ -10,6 +10,8 @@ import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' import {GalleryModel} from 'state/models/media/gallery' import {HITSLOP_10} from 'lib/constants' import {isNative} from 'platform/detection' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' type Props = { gallery: GalleryModel @@ -18,6 +20,7 @@ type Props = { export function SelectPhotoBtn({gallery}: Props) { const pal = usePalette('default') const {track} = useAnalytics() + const {_} = useLingui() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() const onPressSelectPhotos = useCallback(async () => { @@ -37,7 +40,7 @@ export function SelectPhotoBtn({gallery}: Props) { style={styles.button} hitSlop={HITSLOP_10} accessibilityRole="button" - accessibilityLabel="Gallery" + accessibilityLabel={_(msg`Gallery`)} accessibilityHint="Opens device photo gallery"> <FontAwesomeIcon icon={['far', 'image']} diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx index 4faac3750..78b1e9ba2 100644 --- a/src/view/com/composer/select-language/SelectLangBtn.tsx +++ b/src/view/com/composer/select-language/SelectLangBtn.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useMemo} from 'react' import {StyleSheet, Keyboard} from 'react-native' -import {observer} from 'mobx-react-lite' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -12,13 +11,24 @@ import { DropdownItemButton, } from 'view/com/util/forms/DropdownButton' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {isNative} from 'platform/detection' import {codeToLanguageName} from '../../../../locale/helpers' +import {useModalControls} from '#/state/modals' +import { + useLanguagePrefs, + useLanguagePrefsApi, + toPostLanguages, + hasPostLanguage, +} from '#/state/preferences/languages' +import {t, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' -export const SelectLangBtn = observer(function SelectLangBtn() { +export function SelectLangBtn() { const pal = usePalette('default') - const store = useStores() + const {_} = useLingui() + const {openModal} = useModalControls() + const langPrefs = useLanguagePrefs() + const setLangPrefs = useLanguagePrefsApi() const onPressMore = useCallback(async () => { if (isNative) { @@ -26,11 +36,10 @@ export const SelectLangBtn = observer(function SelectLangBtn() { Keyboard.dismiss() } } - store.shell.openModal({name: 'post-languages-settings'}) - }, [store]) + openModal({name: 'post-languages-settings'}) + }, [openModal]) - const postLanguagesPref = store.preferences.postLanguages - const postLanguagePref = store.preferences.postLanguage + const postLanguagesPref = toPostLanguages(langPrefs.postLanguage) const items: DropdownItem[] = useMemo(() => { let arr: DropdownItemButton[] = [] @@ -49,13 +58,14 @@ export const SelectLangBtn = observer(function SelectLangBtn() { arr.push({ icon: - langCodes.every(code => store.preferences.hasPostLanguage(code)) && - langCodes.length === postLanguagesPref.length + langCodes.every(code => + hasPostLanguage(langPrefs.postLanguage, code), + ) && langCodes.length === postLanguagesPref.length ? ['fas', 'circle-dot'] : ['far', 'circle'], label: langName, onPress() { - store.preferences.setPostLanguage(commaSeparatedLangCodes) + setLangPrefs.setPostLanguage(commaSeparatedLangCodes) }, }) } @@ -65,24 +75,24 @@ export const SelectLangBtn = observer(function SelectLangBtn() { * Re-join here after sanitization bc postLanguageHistory is an array of * comma-separated strings too */ - add(postLanguagePref) + add(langPrefs.postLanguage) } // comma-separted strings of lang codes that have been used in the past - for (const lang of store.preferences.postLanguageHistory) { + for (const lang of langPrefs.postLanguageHistory) { add(lang) } return [ - {heading: true, label: 'Post language'}, + {heading: true, label: t`Post language`}, ...arr.slice(0, 6), {sep: true}, { - label: 'Other...', + label: t`Other...`, onPress: onPressMore, }, ] - }, [store.preferences, onPressMore, postLanguagePref, postLanguagesPref]) + }, [onPressMore, langPrefs, setLangPrefs, postLanguagesPref]) return ( <DropdownButton @@ -91,7 +101,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() { items={items} openUpwards style={styles.button} - accessibilityLabel="Language selection" + accessibilityLabel={_(msg`Language selection`)} accessibilityHint=""> {postLanguagesPref.length > 0 ? ( <Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}> @@ -106,7 +116,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() { )} </DropdownButton> ) -}) +} const styles = StyleSheet.create({ button: { diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 2810129f6..13fe3a0b3 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useRef, useMemo, + useState, ComponentProps, } from 'react' import { @@ -18,7 +19,6 @@ import PasteInput, { } from '@mattermost/react-native-paste-input' import {AppBskyRichtextFacet, RichText} from '@atproto/api' import isEqual from 'lodash.isequal' -import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' import {Autocomplete} from './mobile/Autocomplete' import {Text} from 'view/com/util/text/Text' import {cleanError} from 'lib/strings/errors' @@ -38,7 +38,6 @@ interface TextInputProps extends ComponentProps<typeof RNTextInput> { richtext: RichText placeholder: string suggestedLinks: Set<string> - autocompleteView: UserAutocompleteModel setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void onPressPublish: (richtext: RichText) => Promise<void> @@ -56,7 +55,6 @@ export const TextInput = forwardRef(function TextInputImpl( richtext, placeholder, suggestedLinks, - autocompleteView, setRichText, onPhotoPasted, onSuggestedLinksChanged, @@ -69,6 +67,7 @@ export const TextInput = forwardRef(function TextInputImpl( const textInput = useRef<PasteInputRef>(null) const textInputSelection = useRef<Selection>({start: 0, end: 0}) const theme = useTheme() + const [autocompletePrefix, setAutocompletePrefix] = useState('') React.useImperativeHandle(ref, () => ({ focus: () => textInput.current?.focus(), @@ -99,10 +98,9 @@ export const TextInput = forwardRef(function TextInputImpl( textInputSelection.current?.start || 0, ) if (prefix) { - autocompleteView.setActive(true) - autocompleteView.setPrefix(prefix.value) - } else { - autocompleteView.setActive(false) + setAutocompletePrefix(prefix.value) + } else if (autocompletePrefix) { + setAutocompletePrefix('') } const set: Set<string> = new Set() @@ -139,7 +137,8 @@ export const TextInput = forwardRef(function TextInputImpl( }, [ setRichText, - autocompleteView, + autocompletePrefix, + setAutocompletePrefix, suggestedLinks, onSuggestedLinksChanged, onPhotoPasted, @@ -179,9 +178,9 @@ export const TextInput = forwardRef(function TextInputImpl( item, ), ) - autocompleteView.setActive(false) + setAutocompletePrefix('') }, - [onChangeText, richtext, autocompleteView], + [onChangeText, richtext, setAutocompletePrefix], ) const textDecorated = useMemo(() => { @@ -221,7 +220,7 @@ export const TextInput = forwardRef(function TextInputImpl( {textDecorated} </PasteInput> <Autocomplete - view={autocompleteView} + prefix={autocompletePrefix} onSelect={onSelectAutocompleteItem} /> </View> diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 35482bc70..4c31da338 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -11,13 +11,13 @@ import {Paragraph} from '@tiptap/extension-paragraph' import {Placeholder} from '@tiptap/extension-placeholder' import {Text} from '@tiptap/extension-text' import isEqual from 'lodash.isequal' -import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' import {createSuggestion} from './web/Autocomplete' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {isUriImage, blobToDataUri} from 'lib/media/util' import {Emoji} from './web/EmojiPicker.web' import {LinkDecorator} from './web/LinkDecorator' import {generateJSON} from '@tiptap/html' +import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' export interface TextInputRef { focus: () => void @@ -28,7 +28,6 @@ interface TextInputProps { richtext: RichText placeholder: string suggestedLinks: Set<string> - autocompleteView: UserAutocompleteModel setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void onPressPublish: (richtext: RichText) => Promise<void> @@ -43,7 +42,6 @@ export const TextInput = React.forwardRef(function TextInputImpl( richtext, placeholder, suggestedLinks, - autocompleteView, setRichText, onPhotoPasted, onPressPublish, @@ -52,6 +50,8 @@ export const TextInput = React.forwardRef(function TextInputImpl( TextInputProps, ref, ) { + const autocomplete = useActorAutocompleteFn() + const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') const extensions = React.useMemo( () => [ @@ -61,7 +61,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( HTMLAttributes: { class: 'mention', }, - suggestion: createSuggestion({autocompleteView}), + suggestion: createSuggestion({autocomplete}), }), Paragraph, Placeholder.configure({ @@ -71,7 +71,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( History, Hardbreak, ], - [autocompleteView, placeholder], + [autocomplete, placeholder], ) React.useEffect(() => { diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx index f8335d4b9..c400aa48d 100644 --- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx +++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx @@ -1,31 +1,40 @@ -import React, {useEffect} from 'react' +import React, {useEffect, useRef} from 'react' import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' -import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {usePalette} from 'lib/hooks/usePalette' import {Text} from 'view/com/util/text/Text' import {UserAvatar} from 'view/com/util/UserAvatar' import {useGrapheme} from '../hooks/useGrapheme' +import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' +import {Trans} from '@lingui/macro' +import {AppBskyActorDefs} from '@atproto/api' -export const Autocomplete = observer(function AutocompleteImpl({ - view, +export function Autocomplete({ + prefix, onSelect, }: { - view: UserAutocompleteModel + prefix: string onSelect: (item: string) => void }) { const pal = usePalette('default') const positionInterp = useAnimatedValue(0) const {getGraphemeString} = useGrapheme() + const isActive = !!prefix + const {data: suggestions, isFetching} = useActorAutocompleteQuery(prefix) + const suggestionsRef = useRef< + AppBskyActorDefs.ProfileViewBasic[] | undefined + >(undefined) + if (suggestions) { + suggestionsRef.current = suggestions + } useEffect(() => { Animated.timing(positionInterp, { - toValue: view.isActive ? 1 : 0, + toValue: isActive ? 1 : 0, duration: 200, useNativeDriver: true, }).start() - }, [positionInterp, view.isActive]) + }, [positionInterp, isActive]) const topAnimStyle = { transform: [ @@ -40,10 +49,10 @@ export const Autocomplete = observer(function AutocompleteImpl({ return ( <Animated.View style={topAnimStyle}> - {view.isActive ? ( + {isActive ? ( <View style={[pal.view, styles.container, pal.border]}> - {view.suggestions.length > 0 ? ( - view.suggestions.slice(0, 5).map(item => { + {suggestionsRef.current?.length ? ( + suggestionsRef.current.slice(0, 5).map(item => { // Eventually use an average length const MAX_CHARS = 40 const MAX_HANDLE_CHARS = 20 @@ -82,14 +91,18 @@ export const Autocomplete = observer(function AutocompleteImpl({ }) ) : ( <Text type="sm" style={[pal.text, pal.border, styles.noResults]}> - No result + {isFetching ? ( + <Trans>Loading...</Trans> + ) : ( + <Trans>No result</Trans> + )} </Text> )} </View> ) : null} </Animated.View> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index bbed26d48..1f7412561 100644 --- a/src/view/com/composer/text-input/web/Autocomplete.tsx +++ b/src/view/com/composer/text-input/web/Autocomplete.tsx @@ -12,7 +12,7 @@ import { SuggestionProps, SuggestionKeyDownProps, } from '@tiptap/suggestion' -import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' +import {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete' import {usePalette} from 'lib/hooks/usePalette' import {Text} from 'view/com/util/text/Text' import {UserAvatar} from 'view/com/util/UserAvatar' @@ -23,15 +23,14 @@ interface MentionListRef { } export function createSuggestion({ - autocompleteView, + autocomplete, }: { - autocompleteView: UserAutocompleteModel + autocomplete: ActorAutocompleteFn }): Omit<SuggestionOptions, 'editor'> { return { async items({query}) { - autocompleteView.setActive(true) - await autocompleteView.setPrefix(query) - return autocompleteView.suggestions.slice(0, 8) + const suggestions = await autocomplete({query}) + return suggestions.slice(0, 8) }, render: () => { diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index eda1a6704..ef3958c9d 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -1,5 +1,4 @@ import {useState, useEffect} from 'react' -import {useStores} from 'state/index' import {ImageModel} from 'state/models/media/image' import * as apilib from 'lib/api/index' import {getLinkMeta} from 'lib/link-meta/link-meta' @@ -14,19 +13,21 @@ import { isBskyCustomFeedUrl, isBskyListUrl, } from 'lib/strings/url-helpers' -import {ComposerOpts} from 'state/models/ui/shell' +import {ComposerOpts} from 'state/shell/composer' import {POST_IMG_MAX} from 'lib/constants' import {logger} from '#/logger' +import {getAgent} from '#/state/session' +import {useGetPost} from '#/state/queries/post' export function useExternalLinkFetch({ setQuote, }: { setQuote: (opts: ComposerOpts['quote']) => void }) { - const store = useStores() const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>( undefined, ) + const getPost = useGetPost() useEffect(() => { let aborted = false @@ -38,7 +39,7 @@ export function useExternalLinkFetch({ } if (!extLink.meta) { if (isBskyPostUrl(extLink.uri)) { - getPostAsQuote(store, extLink.uri).then( + getPostAsQuote(getPost, extLink.uri).then( newQuote => { if (aborted) { return @@ -48,13 +49,13 @@ export function useExternalLinkFetch({ }, err => { logger.error('Failed to fetch post for quote embedding', { - error: err, + error: err.toString(), }) setExtLink(undefined) }, ) } else if (isBskyCustomFeedUrl(extLink.uri)) { - getFeedAsEmbed(store, extLink.uri).then( + getFeedAsEmbed(getAgent(), extLink.uri).then( ({embed, meta}) => { if (aborted) { return @@ -72,7 +73,7 @@ export function useExternalLinkFetch({ }, ) } else if (isBskyListUrl(extLink.uri)) { - getListAsEmbed(store, extLink.uri).then( + getListAsEmbed(getAgent(), extLink.uri).then( ({embed, meta}) => { if (aborted) { return @@ -90,7 +91,7 @@ export function useExternalLinkFetch({ }, ) } else { - getLinkMeta(store, extLink.uri).then(meta => { + getLinkMeta(getAgent(), extLink.uri).then(meta => { if (aborted) { return } @@ -120,9 +121,7 @@ export function useExternalLinkFetch({ setExtLink({ ...extLink, isLoading: false, // done - localThumb: localThumb - ? new ImageModel(store, localThumb) - : undefined, + localThumb: localThumb ? new ImageModel(localThumb) : undefined, }) }) return cleanup @@ -134,7 +133,7 @@ export function useExternalLinkFetch({ }) } return cleanup - }, [store, extLink, setQuote]) + }, [extLink, setQuote, getPost]) return {extLink, setExtLink} } |