From 0a26e78dcbbf48dad5daae73b210e236d706b22c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 14 Nov 2023 10:41:55 -0800 Subject: [PATCH] Composer update (react-query refactor) (#1899) * Move composer state to a context * Rework composer to use RQ --------- Co-authored-by: Eric Bailey --- src/lib/api/index.ts | 13 ++-- src/lib/link-meta/bsky.ts | 2 +- src/lib/media/picker.tsx | 11 +-- src/lib/media/picker.web.tsx | 11 +-- src/state/models/media/gallery.ts | 9 +-- src/state/models/media/image.ts | 9 +-- src/state/models/ui/shell.ts | 52 +------------ src/state/queries/actor-autocomplete.ts | 54 +++++++++++++- src/state/shell/composer.tsx | 74 +++++++++++++++++++ src/state/shell/index.tsx | 5 +- src/view/com/composer/Composer.tsx | 33 +++------ .../com/composer/text-input/TextInput.tsx | 21 +++--- .../com/composer/text-input/TextInput.web.tsx | 20 +++-- .../text-input/mobile/Autocomplete.tsx | 18 +++-- .../composer/text-input/web/Autocomplete.tsx | 11 ++- src/view/com/composer/useExternalLinkFetch.ts | 2 +- src/view/com/feeds/FeedPage.tsx | 6 +- src/view/com/post-thread/PostThreadItem.tsx | 15 ++-- src/view/com/post/Post.tsx | 8 +- src/view/com/posts/FeedItem.tsx | 8 +- src/view/com/util/post-ctrls/PostCtrls.tsx | 8 +- src/view/com/util/post-embeds/QuoteEmbed.tsx | 2 +- src/view/screens/Feeds.tsx | 8 +- src/view/screens/PostThread.tsx | 8 +- src/view/screens/Profile.tsx | 6 +- src/view/screens/ProfileFeed.tsx | 6 +- src/view/screens/ProfileList.tsx | 8 +- src/view/shell/Composer.tsx | 27 +++---- src/view/shell/Composer.web.tsx | 31 +++----- src/view/shell/desktop/LeftNav.tsx | 4 +- src/view/shell/index.tsx | 9 +-- src/view/shell/index.web.tsx | 9 +-- 32 files changed, 269 insertions(+), 239 deletions(-) create mode 100644 src/state/shell/composer.tsx diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index a9883488..92620c45 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -82,12 +82,11 @@ interface PostOpts { extLink?: ExternalEmbedDraft images?: ImageModel[] labels?: string[] - knownHandles?: Set onStateChange?: (state: string) => void langs?: string[] } -export async function post(store: RootStoreModel, opts: PostOpts) { +export async function post(agent: BskyAgent, opts: PostOpts) { let embed: | AppBskyEmbedImages.Main | AppBskyEmbedExternal.Main @@ -103,7 +102,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { ) opts.onStateChange?.('Processing...') - await rt.detectFacets(store.agent) + await rt.detectFacets(agent) rt = shortenLinks(rt) // filter out any mention facets that didn't map to a user @@ -136,7 +135,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { await image.compress() const path = image.compressed?.path ?? image.path const {width, height} = image.compressed || image - const res = await uploadBlob(store.agent, path, 'image/jpeg') + const res = await uploadBlob(agent, path, 'image/jpeg') images.push({ image: res.data.blob, alt: image.altText ?? '', @@ -186,7 +185,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { } if (encoding) { const thumbUploadRes = await uploadBlob( - store.agent, + agent, opts.extLink.localThumb.path, encoding, ) @@ -225,7 +224,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { // add replyTo if post is a reply to another post if (opts.replyTo) { const replyToUrip = new AtUri(opts.replyTo) - const parentPost = await store.agent.getPost({ + const parentPost = await agent.getPost({ repo: replyToUrip.host, rkey: replyToUrip.rkey, }) @@ -258,7 +257,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) { try { opts.onStateChange?.('Posting...') - return await store.agent.post({ + return await agent.post({ text: rt.text, facets: rt.facets, reply, diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts index b052ed04..2134c329 100644 --- a/src/lib/link-meta/bsky.ts +++ b/src/lib/link-meta/bsky.ts @@ -4,7 +4,7 @@ import {LikelyType, LinkMeta} from './link-meta' import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' import {RootStoreModel} from 'state/index' import {PostThreadModel} from 'state/models/content/post-thread' -import {ComposerOptsQuote} from 'state/models/ui/shell' +import {ComposerOptsQuote} from 'state/shell/composer' // TODO // import {Home} from 'view/screens/Home' diff --git a/src/lib/media/picker.tsx b/src/lib/media/picker.tsx index d0ee1ae2..e91a5b8d 100644 --- a/src/lib/media/picker.tsx +++ b/src/lib/media/picker.tsx @@ -3,7 +3,6 @@ import { openCropper as openCropperFn, Image as RNImage, } from 'react-native-image-crop-picker' -import {RootStoreModel} from 'state/index' import {CameraOpts, CropperOptions} from './types' export {openPicker} from './picker.shared' @@ -16,10 +15,7 @@ export {openPicker} from './picker.shared' * -prf */ -export async function openCamera( - _store: RootStoreModel, - opts: CameraOpts, -): Promise { +export async function openCamera(opts: CameraOpts): Promise { const item = await openCameraFn({ width: opts.width, height: opts.height, @@ -39,10 +35,7 @@ export async function openCamera( } } -export async function openCropper( - _store: RootStoreModel, - opts: CropperOptions, -) { +export async function openCropper(opts: CropperOptions) { const item = await openCropperFn({ ...opts, forceJpg: true, // ios only diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx index 50b9c73e..995a0c95 100644 --- a/src/lib/media/picker.web.tsx +++ b/src/lib/media/picker.web.tsx @@ -1,23 +1,16 @@ /// import {CameraOpts, CropperOptions} from './types' -import {RootStoreModel} from 'state/index' import {Image as RNImage} from 'react-native-image-crop-picker' export {openPicker} from './picker.shared' import {unstable__openModal} from '#/state/modals' -export async function openCamera( - _store: RootStoreModel, - _opts: CameraOpts, -): Promise { +export async function openCamera(_opts: CameraOpts): Promise { // const mediaType = opts.mediaType || 'photo' TODO throw new Error('TODO') } -export async function openCropper( - _store: RootStoreModel, - opts: CropperOptions, -): Promise { +export async function openCropper(opts: CropperOptions): Promise { // TODO handle more opts return new Promise((resolve, reject) => { unstable__openModal({ diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts index f9c3efca..04023bf8 100644 --- a/src/state/models/media/gallery.ts +++ b/src/state/models/media/gallery.ts @@ -1,5 +1,4 @@ import {makeAutoObservable, runInAction} from 'mobx' -import {RootStoreModel} from 'state/index' import {ImageModel} from './image' import {Image as RNImage} from 'react-native-image-crop-picker' import {openPicker} from 'lib/media/picker' @@ -8,10 +7,8 @@ import {getImageDim} from 'lib/media/manip' export class GalleryModel { images: ImageModel[] = [] - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this, { - rootStore: false, - }) + constructor() { + makeAutoObservable(this) } get isEmpty() { @@ -33,7 +30,7 @@ export class GalleryModel { // Temporarily enforce uniqueness but can eventually also use index if (!this.images.some(i => i.path === image_.path)) { - const image = new ImageModel(this.rootStore, image_) + const image = new ImageModel(image_) // Initial resize image.manipulate({}) diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index b3796060..6a226484 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -1,5 +1,4 @@ import {Image as RNImage} from 'react-native-image-crop-picker' -import {RootStoreModel} from 'state/index' import {makeAutoObservable, runInAction} from 'mobx' import {POST_IMG_MAX} from 'lib/constants' import * as ImageManipulator from 'expo-image-manipulator' @@ -42,10 +41,8 @@ export class ImageModel implements Omit { } prevAttributes: ImageManipulationAttributes = {} - constructor(public rootStore: RootStoreModel, image: Omit) { - makeAutoObservable(this, { - rootStore: false, - }) + constructor(image: Omit) { + makeAutoObservable(this) this.path = image.path this.width = image.width @@ -178,7 +175,7 @@ export class ImageModel implements Omit { height: this.height, }) - const cropped = await openCropper(this.rootStore, { + const cropped = await openCropper({ mediaType: 'photo', path: this.path, freeStyleCropEnabled: true, diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 9ce9b663..310d4f0f 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -1,4 +1,4 @@ -import {AppBskyEmbedRecord, AppBskyActorDefs} from '@atproto/api' +import {AppBskyActorDefs} from '@atproto/api' import {RootStoreModel} from '../root-store' import {makeAutoObservable, runInAction} from 'mobx' import { @@ -37,41 +37,9 @@ export class ImagesLightbox implements LightboxModel { } } -export interface ComposerOptsPostRef { - uri: string - cid: string - text: string - author: { - handle: string - displayName?: string - avatar?: string - } -} -export interface ComposerOptsQuote { - uri: string - cid: string - text: string - indexedAt: string - author: { - did: string - handle: string - displayName?: string - avatar?: string - } - embeds?: AppBskyEmbedRecord.ViewRecord['embeds'] -} -export interface ComposerOpts { - replyTo?: ComposerOptsPostRef - onPost?: () => void - quote?: ComposerOptsQuote - mention?: string // handle of user to mention -} - export class ShellUiModel { isLightboxActive = false activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null - isComposerActive = false - composerOpts: ComposerOpts | undefined tickEveryMinute = Date.now() constructor(public rootStore: RootStoreModel) { @@ -92,10 +60,6 @@ export class ShellUiModel { this.closeLightbox() return true } - if (this.isComposerActive) { - this.closeComposer() - return true - } return false } @@ -106,9 +70,6 @@ export class ShellUiModel { if (this.isLightboxActive) { this.closeLightbox() } - if (this.isComposerActive) { - this.closeComposer() - } } openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) { @@ -122,17 +83,6 @@ export class ShellUiModel { this.activeLightbox = null } - openComposer(opts: ComposerOpts) { - this.rootStore.emitNavigation() - this.isComposerActive = true - this.composerOpts = opts - } - - closeComposer() { - this.isComposerActive = false - this.composerOpts = undefined - } - setupClock() { setInterval(() => { runInAction(() => { diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts index 18abb631..62c4781c 100644 --- a/src/state/queries/actor-autocomplete.ts +++ b/src/state/queries/actor-autocomplete.ts @@ -1,7 +1,8 @@ -import {AppBskyActorDefs} from '@atproto/api' +import {AppBskyActorDefs, BskyAgent} from '@atproto/api' import {useQuery} from '@tanstack/react-query' import {useSession} from '../session' import {useMyFollowsQuery} from './my-follows' +import AwaitLock from 'await-lock' export const RQKEY = (prefix: string) => ['actor-autocomplete', prefix] @@ -21,6 +22,57 @@ export function useActorAutocompleteQuery(prefix: string) { }) } +export class ActorAutocomplete { + // state + isLoading = false + isActive = false + prefix = '' + lock = new AwaitLock() + + // data + suggestions: AppBskyActorDefs.ProfileViewBasic[] = [] + + constructor( + public agent: BskyAgent, + public follows?: AppBskyActorDefs.ProfileViewBasic[] | undefined, + ) {} + + setFollows(follows: AppBskyActorDefs.ProfileViewBasic[]) { + this.follows = follows + } + + async query(prefix: string) { + const origPrefix = prefix.trim().toLocaleLowerCase() + this.prefix = origPrefix + await this.lock.acquireAsync() + try { + if (this.prefix) { + if (this.prefix !== origPrefix) { + return // another prefix was set before we got our chance + } + + // start with follow results + this.suggestions = computeSuggestions(this.prefix, this.follows) + + // ask backend + const res = await this.agent.searchActorsTypeahead({ + term: this.prefix, + limit: 8, + }) + this.suggestions = computeSuggestions( + this.prefix, + this.follows, + res.data.actors, + ) + } else { + this.suggestions = computeSuggestions(this.prefix, this.follows) + } + } finally { + this.lock.release() + } + } +} + function computeSuggestions( prefix: string, follows: AppBskyActorDefs.ProfileViewBasic[] = [], diff --git a/src/state/shell/composer.tsx b/src/state/shell/composer.tsx new file mode 100644 index 00000000..a350bd7f --- /dev/null +++ b/src/state/shell/composer.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import {AppBskyEmbedRecord} from '@atproto/api' + +export interface ComposerOptsPostRef { + uri: string + cid: string + text: string + author: { + handle: string + displayName?: string + avatar?: string + } +} +export interface ComposerOptsQuote { + uri: string + cid: string + text: string + indexedAt: string + author: { + did: string + handle: string + displayName?: string + avatar?: string + } + embeds?: AppBskyEmbedRecord.ViewRecord['embeds'] +} +export interface ComposerOpts { + replyTo?: ComposerOptsPostRef + onPost?: () => void + quote?: ComposerOptsQuote + mention?: string // handle of user to mention +} + +type StateContext = ComposerOpts | undefined +type ControlsContext = { + openComposer: (opts: ComposerOpts) => void + closeComposer: () => void +} + +const stateContext = React.createContext(undefined) +const controlsContext = React.createContext({ + openComposer(_opts: ComposerOpts) {}, + closeComposer() {}, +}) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState() + const api = React.useMemo( + () => ({ + openComposer(opts: ComposerOpts) { + setState(opts) + }, + closeComposer() { + setState(undefined) + }, + }), + [setState], + ) + return ( + + + {children} + + + ) +} + +export function useComposerState() { + return React.useContext(stateContext) +} + +export function useComposerControls() { + return React.useContext(controlsContext) +} diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx index eb549b9f..63c3763d 100644 --- a/src/state/shell/index.tsx +++ b/src/state/shell/index.tsx @@ -5,6 +5,7 @@ import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled' import {Provider as MinimalModeProvider} from './minimal-mode' import {Provider as ColorModeProvider} from './color-mode' import {Provider as OnboardingProvider} from './onboarding' +import {Provider as ComposerProvider} from './composer' export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' export { @@ -22,7 +23,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { - {children} + + {children} + diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 65c485a2..4db9a3a3 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' @@ -58,6 +56,9 @@ import { useLanguagePrefsApi, toPostLanguages, } from '#/state/preferences/languages' +import {useSession} from '#/state/session' +import {useProfileQuery} from '#/state/queries/profile' +import {useComposerControls} from '#/state/shell/composer' type Props = ComposerOpts export const ComposePost = observer(function ComposePost({ @@ -66,12 +67,14 @@ export const ComposePost = observer(function ComposePost({ quote: initQuote, mention: initMention, }: Props) { + const {agent, 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() @@ -101,15 +104,10 @@ export const ComposePost = observer(function ComposePost({ const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) const [labels, setLabels] = useState([]) const [suggestedLinks, setSuggestedLinks] = useState>(new Set()) - const gallery = useMemo(() => new GalleryModel(store), [store]) + const gallery = useMemo(() => new GalleryModel(), []) const onClose = useCallback(() => { - store.shell.closeComposer() - }, [store]) - - const autocompleteView = useMemo( - () => new UserAutocompleteModel(store), - [store], - ) + closeComposer() + }, [closeComposer]) const insets = useSafeAreaInsets() const viewStyles = useMemo( @@ -162,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) => { @@ -216,7 +209,7 @@ export const ComposePost = observer(function ComposePost({ setIsProcessing(true) try { - await apilib.post(store, { + await apilib.post(agent, { rawText: richtext.text, replyTo: replyTo?.uri, images: gallery.images, @@ -224,7 +217,6 @@ export const ComposePost = observer(function ComposePost({ extLink, labels, onStateChange: setProcessingState, - knownHandles: autocompleteView.knownHandles, langs: toPostLanguages(langPrefs.postLanguage), }) } catch (e: any) { @@ -381,13 +373,12 @@ export const ComposePost = observer(function ComposePost({ styles.textInputLayout, isNative && styles.textInputLayoutMobile, ]}> - + { richtext: RichText placeholder: string suggestedLinks: Set - autocompleteView: UserAutocompleteModel setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void onPressPublish: (richtext: RichText) => Promise @@ -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(null) const textInputSelection = useRef({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 = 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} diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 35482bc7..7690a587 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -11,13 +11,15 @@ 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 {ActorAutocomplete} from '#/state/queries/actor-autocomplete' +import {useSession} from '#/state/session' +import {useMyFollowsQuery} from '#/state/queries/my-follows' export interface TextInputRef { focus: () => void @@ -28,7 +30,6 @@ interface TextInputProps { richtext: RichText placeholder: string suggestedLinks: Set - autocompleteView: UserAutocompleteModel setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void onPressPublish: (richtext: RichText) => Promise @@ -43,7 +44,6 @@ export const TextInput = React.forwardRef(function TextInputImpl( richtext, placeholder, suggestedLinks, - autocompleteView, setRichText, onPhotoPasted, onPressPublish, @@ -52,6 +52,16 @@ export const TextInput = React.forwardRef(function TextInputImpl( TextInputProps, ref, ) { + const {agent} = useSession() + const autocomplete = React.useMemo( + () => new ActorAutocomplete(agent), + [agent], + ) + const {data: follows} = useMyFollowsQuery() + if (follows) { + autocomplete.setFollows(follows) + } + const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') const extensions = React.useMemo( () => [ @@ -61,7 +71,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( HTMLAttributes: { class: 'mention', }, - suggestion: createSuggestion({autocompleteView}), + suggestion: createSuggestion({autocomplete}), }), Paragraph, Placeholder.configure({ @@ -71,7 +81,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 f8335d4b..9ccd717f 100644 --- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx +++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx @@ -1,31 +1,33 @@ import React, {useEffect} 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' export const Autocomplete = observer(function AutocompleteImpl({ - view, + 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} = useActorAutocompleteQuery(prefix) 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 +42,10 @@ export const Autocomplete = observer(function AutocompleteImpl({ return ( - {view.isActive ? ( + {isActive ? ( - {view.suggestions.length > 0 ? ( - view.suggestions.slice(0, 5).map(item => { + {suggestions?.length ? ( + suggestions.slice(0, 5).map(item => { // Eventually use an average length const MAX_CHARS = 40 const MAX_HANDLE_CHARS = 20 diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index bbed26d4..c6b773d8 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 {ActorAutocomplete} 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: ActorAutocomplete }): Omit { return { async items({query}) { - autocompleteView.setActive(true) - await autocompleteView.setPrefix(query) - return autocompleteView.suggestions.slice(0, 8) + await autocomplete.query(query) + return autocomplete.suggestions.slice(0, 8) }, render: () => { diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index eda1a670..9bdd927a 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -14,7 +14,7 @@ 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' diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 8d6a4a3d..562b1c14 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -22,6 +22,7 @@ import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useSession} from '#/state/session' +import {useComposerControls} from '#/state/shell/composer' const POLL_FREQ = 30e3 // 30sec @@ -46,6 +47,7 @@ export function FeedPage({ const {_} = useLingui() const {isDesktop} = useWebMediaQueries() const queryClient = useQueryClient() + const {openComposer} = useComposerControls() const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() const {screen, track} = useAnalytics() const headerOffset = useHeaderOffset() @@ -80,8 +82,8 @@ export function FeedPage({ const onPressCompose = React.useCallback(() => { track('HomeScreen:PressCompose') - store.shell.openComposer({}) - }, [store, track]) + openComposer({}) + }, [openComposer, track]) const onPressLoadLatest = React.useCallback(() => { scrollToTop() diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 88889fd1..c81b762c 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -20,7 +20,6 @@ import {sanitizeHandle} from 'lib/strings/handles' import {countLines, pluralize} from 'lib/strings/helpers' import {isEmbedByEmbedder} from 'lib/embeds' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' -import {useStores} from 'state/index' import {PostMeta} from '../util/PostMeta' import {PostEmbeds} from '../util/post-embeds' import {PostCtrls} from '../util/post-ctrls/PostCtrls' @@ -39,6 +38,8 @@ import {MAX_POST_LINES} from 'lib/constants' import {Trans} from '@lingui/macro' import {useLanguagePrefs} from '#/state/preferences' import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' +import {useComposerControls} from '#/state/shell/composer' +import {useModerationOpts} from '#/state/queries/preferences' export function PostThreadItem({ post, @@ -65,7 +66,7 @@ export function PostThreadItem({ hasPrecedingItem: boolean onPostReply: () => void }) { - const store = useStores() + const moderationOpts = useModerationOpts() const postShadowed = usePostShadow(post, dataUpdatedAt) const richText = useMemo( () => @@ -77,8 +78,8 @@ export function PostThreadItem({ ) const moderation = useMemo( () => - post ? moderatePost(post, store.preferences.moderationOpts) : undefined, - [post, store], + post && moderationOpts ? moderatePost(post, moderationOpts) : undefined, + [post, moderationOpts], ) if (postShadowed === POST_TOMBSTONE) { return @@ -145,8 +146,8 @@ function PostThreadItemLoaded({ onPostReply: () => void }) { const pal = usePalette('default') - const store = useStores() const langPrefs = useLanguagePrefs() + const {openComposer} = useComposerControls() const [limitLines, setLimitLines] = React.useState( countLines(richText?.text) >= MAX_POST_LINES, ) @@ -187,7 +188,7 @@ function PostThreadItemLoaded({ ) const onPressReply = React.useCallback(() => { - store.shell.openComposer({ + openComposer({ replyTo: { uri: post.uri, cid: post.cid, @@ -200,7 +201,7 @@ function PostThreadItemLoaded({ }, onPost: onPostReply, }) - }, [store, post, record, onPostReply]) + }, [openComposer, post, record, onPostReply]) const onPressShowMore = React.useCallback(() => { setLimitLines(false) diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 4a5b8041..09edbe12 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -19,7 +19,6 @@ import {PostAlerts} from '../util/moderation/PostAlerts' import {Text} from '../util/text/Text' import {RichText} from '../util/text/RichText' import {PreviewableUserAvatar} from '../util/UserAvatar' -import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {makeProfileLink} from 'lib/routes/links' @@ -27,6 +26,7 @@ import {MAX_POST_LINES} from 'lib/constants' import {countLines} from 'lib/strings/helpers' import {useModerationOpts} from '#/state/queries/preferences' import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' +import {useComposerControls} from '#/state/shell/composer' export function Post({ post, @@ -97,7 +97,7 @@ function PostInner({ style?: StyleProp }) { const pal = usePalette('default') - const store = useStores() + const {openComposer} = useComposerControls() const [limitLines, setLimitLines] = useState( countLines(richText?.text) >= MAX_POST_LINES, ) @@ -110,7 +110,7 @@ function PostInner({ } const onPressReply = React.useCallback(() => { - store.shell.openComposer({ + openComposer({ replyTo: { uri: post.uri, cid: post.cid, @@ -122,7 +122,7 @@ function PostInner({ }, }, }) - }, [store, post, record]) + }, [openComposer, post, record]) const onPressShowMore = React.useCallback(() => { setLimitLines(false) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index d24a18f0..31981cc5 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -24,7 +24,6 @@ import {RichText} from '../util/text/RichText' import {PostSandboxWarning} from '../util/PostSandboxWarning' import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics/analytics' import {sanitizeDisplayName} from 'lib/strings/display-names' @@ -34,6 +33,7 @@ import {isEmbedByEmbedder} from 'lib/embeds' import {MAX_POST_LINES} from 'lib/constants' import {countLines} from 'lib/strings/helpers' import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' +import {useComposerControls} from '#/state/shell/composer' export function FeedItem({ post, @@ -102,7 +102,7 @@ function FeedItemInner({ isThreadLastChild?: boolean isThreadParent?: boolean }) { - const store = useStores() + const {openComposer} = useComposerControls() const pal = usePalette('default') const {track} = useAnalytics() const [limitLines, setLimitLines] = useState( @@ -124,7 +124,7 @@ function FeedItemInner({ const onPressReply = React.useCallback(() => { track('FeedItem:PostReply') - store.shell.openComposer({ + openComposer({ replyTo: { uri: post.uri, cid: post.cid, @@ -136,7 +136,7 @@ function FeedItemInner({ }, }, }) - }, [post, record, track, store]) + }, [post, record, track, openComposer]) const onPressShowMore = React.useCallback(() => { setLimitLines(false) diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index a764ed52..7e95bde8 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -13,7 +13,6 @@ import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons' import {s, colors} from 'lib/styles' import {pluralize} from 'lib/strings/helpers' import {useTheme} from 'lib/ThemeContext' -import {useStores} from 'state/index' import {RepostButton} from './RepostButton' import {Haptics} from 'lib/haptics' import {HITSLOP_10, HITSLOP_20} from 'lib/constants' @@ -24,6 +23,7 @@ import { usePostRepostMutation, usePostUnrepostMutation, } from '#/state/queries/post' +import {useComposerControls} from '#/state/shell/composer' export function PostCtrls({ big, @@ -38,8 +38,8 @@ export function PostCtrls({ style?: StyleProp onPressReply: () => void }) { - const store = useStores() const theme = useTheme() + const {openComposer} = useComposerControls() const {closeModal} = useModalControls() const postLikeMutation = usePostLikeMutation() const postUnlikeMutation = usePostUnlikeMutation() @@ -90,7 +90,7 @@ export function PostCtrls({ const onQuote = useCallback(() => { closeModal() - store.shell.openComposer({ + openComposer({ quote: { uri: post.uri, cid: post.cid, @@ -100,7 +100,7 @@ export function PostCtrls({ }, }) Haptics.default() - }, [post, record, store.shell, closeModal]) + }, [post, record, openComposer, closeModal]) return ( @@ -90,8 +90,8 @@ type FlatlistSlice = export const FeedsScreen = withAuthRequired(function FeedsScreenImpl( _props: Props, ) { - const store = useStores() const pal = usePalette('default') + const {openComposer} = useComposerControls() const {isMobile, isTabletOrDesktop} = useWebMediaQueries() const [query, setQuery] = React.useState('') const [isPTR, setIsPTR] = React.useState(false) @@ -128,8 +128,8 @@ export const FeedsScreen = withAuthRequired(function FeedsScreenImpl( [search], ) const onPressCompose = React.useCallback(() => { - store.shell.openComposer({}) - }, [store]) + openComposer({}) + }, [openComposer]) const onChangeQuery = React.useCallback( (text: string) => { setQuery(text) diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index c76bf44e..752f78dc 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -10,7 +10,6 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from '../com/util/ViewHeader' import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' import {ComposePrompt} from 'view/com/composer/Prompt' -import {useStores} from 'state/index' import {s} from 'lib/styles' import {useSafeAreaInsets} from 'react-native-safe-area-context' import { @@ -24,14 +23,15 @@ import {useSetMinimalShellMode} from '#/state/shell' import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {ErrorMessage} from '../com/util/error/ErrorMessage' import {CenteredView} from '../com/util/Views' +import {useComposerControls} from '#/state/shell/composer' type Props = NativeStackScreenProps export const PostThreadScreen = withAuthRequired( observer(function PostThreadScreenImpl({route}: Props) { - const store = useStores() const queryClient = useQueryClient() const {fabMinimalShellTransform} = useMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode() + const {openComposer} = useComposerControls() const safeAreaInsets = useSafeAreaInsets() const {name, rkey} = route.params const {isMobile} = useWebMediaQueries() @@ -54,7 +54,7 @@ export const PostThreadScreen = withAuthRequired( if (thread?.type !== 'post') { return } - store.shell.openComposer({ + openComposer({ replyTo: { uri: thread.post.uri, cid: thread.post.cid, @@ -70,7 +70,7 @@ export const PostThreadScreen = withAuthRequired( queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), }), }) - }, [store, queryClient, resolvedUri]) + }, [openComposer, queryClient, resolvedUri]) return ( diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 724c47c9..17ea4498 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -36,6 +36,7 @@ import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' import {cleanError} from '#/lib/strings/errors' import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn' import {useQueryClient} from '@tanstack/react-query' +import {useComposerControls} from '#/state/shell/composer' type Props = NativeStackScreenProps export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({ @@ -128,6 +129,7 @@ function ProfileScreenLoaded({ const store = useStores() const {currentAccount} = useSession() const setMinimalShellMode = useSetMinimalShellMode() + const {openComposer} = useComposerControls() const {screen, track} = useAnalytics() const [currentPage, setCurrentPage] = React.useState(0) const {_} = useLingui() @@ -193,8 +195,8 @@ function ProfileScreenLoaded({ profile.handle === 'handle.invalid' ? undefined : profile.handle - store.shell.openComposer({mention}) - }, [store, currentAccount, track, profile]) + openComposer({mention}) + }, [openComposer, currentAccount, track, profile]) const onPageSelected = React.useCallback( i => { diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 537fe736..f62790be 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -16,7 +16,6 @@ import {CommonNavigatorParams} from 'lib/routes/types' import {makeRecordUri} from 'lib/strings/url-helpers' import {colors, s} from 'lib/styles' import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' import {FeedDescriptor} from '#/state/queries/post-feed' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' @@ -62,6 +61,7 @@ import { } from '#/state/queries/preferences' import {useSession} from '#/state/session' import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' +import {useComposerControls} from '#/state/shell/composer' const SECTION_TITLES = ['Posts', 'About'] @@ -163,9 +163,9 @@ export const ProfileFeedScreenInner = function ProfileFeedScreenInnerImpl({ }) { const {_} = useLingui() const pal = usePalette('default') - const store = useStores() const {currentAccount} = useSession() const {openModal} = useModalControls() + const {openComposer} = useComposerControls() const {track} = useAnalytics() const feedSectionRef = React.useRef(null) @@ -420,7 +420,7 @@ export const ProfileFeedScreenInner = function ProfileFeedScreenInnerImpl({ store.shell.openComposer({})} + onPress={() => openComposer({})} icon={ } diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 42c3741d..594f4907 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -28,7 +28,6 @@ import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {FAB} from 'view/com/util/fab/FAB' import {Haptics} from 'lib/haptics' import {FeedDescriptor} from '#/state/queries/post-feed' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useSetTitle} from 'lib/hooks/useSetTitle' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' @@ -55,6 +54,7 @@ import { } from '#/state/queries/list' import {cleanError} from '#/lib/strings/errors' import {useSession} from '#/state/session' +import {useComposerControls} from '#/state/shell/composer' const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_MOD = ['About'] @@ -106,9 +106,9 @@ function ProfileListScreenLoaded({ uri, list, }: Props & {uri: string; list: AppBskyGraphDefs.ListView}) { - const store = useStores() const {_} = useLingui() const queryClient = useQueryClient() + const {openComposer} = useComposerControls() const setMinimalShellMode = useSetMinimalShellMode() const {rkey} = route.params const feedSectionRef = React.useRef(null) @@ -191,7 +191,7 @@ function ProfileListScreenLoaded({ store.shell.openComposer({})} + onPress={() => openComposer({})} icon={ store.shell.openComposer({})} + onPress={() => openComposer({})} icon={ } diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx index 219a594e..d37ff4fb 100644 --- a/src/view/shell/Composer.tsx +++ b/src/view/shell/Composer.tsx @@ -2,30 +2,21 @@ import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {Animated, Easing, Platform, StyleSheet, View} from 'react-native' import {ComposePost} from '../com/composer/Composer' -import {ComposerOpts} from 'state/models/ui/shell' +import {useComposerState} from 'state/shell/composer' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {usePalette} from 'lib/hooks/usePalette' export const Composer = observer(function ComposerImpl({ - active, winHeight, - replyTo, - onPost, - quote, - mention, }: { - active: boolean winHeight: number - replyTo?: ComposerOpts['replyTo'] - onPost?: ComposerOpts['onPost'] - quote?: ComposerOpts['quote'] - mention?: ComposerOpts['mention'] }) { + const state = useComposerState() const pal = usePalette('default') const initInterp = useAnimatedValue(0) useEffect(() => { - if (active) { + if (state) { Animated.timing(initInterp, { toValue: 1, duration: 300, @@ -35,7 +26,7 @@ export const Composer = observer(function ComposerImpl({ } else { initInterp.setValue(0) } - }, [initInterp, active]) + }, [initInterp, state]) const wrapperAnimStyle = { transform: [ { @@ -50,7 +41,7 @@ export const Composer = observer(function ComposerImpl({ // rendering // = - if (!active) { + if (!state) { return } @@ -60,10 +51,10 @@ export const Composer = observer(function ComposerImpl({ aria-modal accessibilityViewIsModal> ) diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index c3ec37e5..e08c792a 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -1,34 +1,21 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, View} from 'react-native' import {ComposePost} from '../com/composer/Composer' -import {ComposerOpts} from 'state/models/ui/shell' +import {useComposerState} from 'state/shell/composer' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' const BOTTOM_BAR_HEIGHT = 61 -export const Composer = observer(function ComposerImpl({ - active, - replyTo, - quote, - onPost, - mention, -}: { - active: boolean - winHeight: number - replyTo?: ComposerOpts['replyTo'] - quote: ComposerOpts['quote'] - onPost?: ComposerOpts['onPost'] - mention?: ComposerOpts['mention'] -}) { +export function Composer({}: {winHeight: number}) { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() + const state = useComposerState() // rendering // = - if (!active) { + if (!state) { return } @@ -42,15 +29,15 @@ export const Composer = observer(function ComposerImpl({ pal.border, ]}> ) -}) +} const styles = StyleSheet.create({ mask: { diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index d7814cb5..90cf144d 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -44,6 +44,7 @@ import {Trans, msg} from '@lingui/macro' import {useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' import {useUnreadNotifications} from '#/state/queries/notifications/unread' +import {useComposerControls} from '#/state/shell/composer' const ProfileCard = observer(function ProfileCardImpl() { const {currentAccount} = useSession() @@ -195,6 +196,7 @@ const NavItem = observer(function NavItemImpl({ function ComposeBtn() { const store = useStores() const {getState} = useNavigation() + const {openComposer} = useComposerControls() const {_} = useLingui() const {isTablet} = useWebMediaQueries() @@ -224,7 +226,7 @@ function ComposeBtn() { } const onPressCompose = async () => - store.shell.openComposer({mention: await getProfileHandle()}) + openComposer({mention: await getProfileHandle()}) if (isTablet) { return null diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 75ed0747..ff7a7dcd 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -89,14 +89,7 @@ const ShellInner = observer(function ShellInnerImpl() { - + diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index a74cd126..e134358d 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -61,14 +61,7 @@ const ShellInner = observer(function ShellInnerImpl() { )} - + {showBottomBar && }