Composer update (react-query refactor) (#1899)

* Move composer state to a context

* Rework composer to use RQ

---------

Co-authored-by: Eric Bailey <git@esb.lol>
zio/stable
Paul Frazee 2023-11-14 10:41:55 -08:00 committed by GitHub
parent c687172de9
commit 0a26e78dcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 269 additions and 239 deletions

View File

@ -82,12 +82,11 @@ interface PostOpts {
extLink?: ExternalEmbedDraft extLink?: ExternalEmbedDraft
images?: ImageModel[] images?: ImageModel[]
labels?: string[] labels?: string[]
knownHandles?: Set<string>
onStateChange?: (state: string) => void onStateChange?: (state: string) => void
langs?: string[] langs?: string[]
} }
export async function post(store: RootStoreModel, opts: PostOpts) { export async function post(agent: BskyAgent, opts: PostOpts) {
let embed: let embed:
| AppBskyEmbedImages.Main | AppBskyEmbedImages.Main
| AppBskyEmbedExternal.Main | AppBskyEmbedExternal.Main
@ -103,7 +102,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
) )
opts.onStateChange?.('Processing...') opts.onStateChange?.('Processing...')
await rt.detectFacets(store.agent) await rt.detectFacets(agent)
rt = shortenLinks(rt) rt = shortenLinks(rt)
// filter out any mention facets that didn't map to a user // 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() await image.compress()
const path = image.compressed?.path ?? image.path const path = image.compressed?.path ?? image.path
const {width, height} = image.compressed || image 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({ images.push({
image: res.data.blob, image: res.data.blob,
alt: image.altText ?? '', alt: image.altText ?? '',
@ -186,7 +185,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
} }
if (encoding) { if (encoding) {
const thumbUploadRes = await uploadBlob( const thumbUploadRes = await uploadBlob(
store.agent, agent,
opts.extLink.localThumb.path, opts.extLink.localThumb.path,
encoding, encoding,
) )
@ -225,7 +224,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
// add replyTo if post is a reply to another post // add replyTo if post is a reply to another post
if (opts.replyTo) { if (opts.replyTo) {
const replyToUrip = new AtUri(opts.replyTo) const replyToUrip = new AtUri(opts.replyTo)
const parentPost = await store.agent.getPost({ const parentPost = await agent.getPost({
repo: replyToUrip.host, repo: replyToUrip.host,
rkey: replyToUrip.rkey, rkey: replyToUrip.rkey,
}) })
@ -258,7 +257,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
try { try {
opts.onStateChange?.('Posting...') opts.onStateChange?.('Posting...')
return await store.agent.post({ return await agent.post({
text: rt.text, text: rt.text,
facets: rt.facets, facets: rt.facets,
reply, reply,

View File

@ -4,7 +4,7 @@ import {LikelyType, LinkMeta} from './link-meta'
import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
import {RootStoreModel} from 'state/index' import {RootStoreModel} from 'state/index'
import {PostThreadModel} from 'state/models/content/post-thread' import {PostThreadModel} from 'state/models/content/post-thread'
import {ComposerOptsQuote} from 'state/models/ui/shell' import {ComposerOptsQuote} from 'state/shell/composer'
// TODO // TODO
// import {Home} from 'view/screens/Home' // import {Home} from 'view/screens/Home'

View File

@ -3,7 +3,6 @@ import {
openCropper as openCropperFn, openCropper as openCropperFn,
Image as RNImage, Image as RNImage,
} from 'react-native-image-crop-picker' } from 'react-native-image-crop-picker'
import {RootStoreModel} from 'state/index'
import {CameraOpts, CropperOptions} from './types' import {CameraOpts, CropperOptions} from './types'
export {openPicker} from './picker.shared' export {openPicker} from './picker.shared'
@ -16,10 +15,7 @@ export {openPicker} from './picker.shared'
* -prf * -prf
*/ */
export async function openCamera( export async function openCamera(opts: CameraOpts): Promise<RNImage> {
_store: RootStoreModel,
opts: CameraOpts,
): Promise<RNImage> {
const item = await openCameraFn({ const item = await openCameraFn({
width: opts.width, width: opts.width,
height: opts.height, height: opts.height,
@ -39,10 +35,7 @@ export async function openCamera(
} }
} }
export async function openCropper( export async function openCropper(opts: CropperOptions) {
_store: RootStoreModel,
opts: CropperOptions,
) {
const item = await openCropperFn({ const item = await openCropperFn({
...opts, ...opts,
forceJpg: true, // ios only forceJpg: true, // ios only

View File

@ -1,23 +1,16 @@
/// <reference lib="dom" /> /// <reference lib="dom" />
import {CameraOpts, CropperOptions} from './types' import {CameraOpts, CropperOptions} from './types'
import {RootStoreModel} from 'state/index'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
export {openPicker} from './picker.shared' export {openPicker} from './picker.shared'
import {unstable__openModal} from '#/state/modals' import {unstable__openModal} from '#/state/modals'
export async function openCamera( export async function openCamera(_opts: CameraOpts): Promise<RNImage> {
_store: RootStoreModel,
_opts: CameraOpts,
): Promise<RNImage> {
// const mediaType = opts.mediaType || 'photo' TODO // const mediaType = opts.mediaType || 'photo' TODO
throw new Error('TODO') throw new Error('TODO')
} }
export async function openCropper( export async function openCropper(opts: CropperOptions): Promise<RNImage> {
_store: RootStoreModel,
opts: CropperOptions,
): Promise<RNImage> {
// TODO handle more opts // TODO handle more opts
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
unstable__openModal({ unstable__openModal({

View File

@ -1,5 +1,4 @@
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {RootStoreModel} from 'state/index'
import {ImageModel} from './image' import {ImageModel} from './image'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {openPicker} from 'lib/media/picker' import {openPicker} from 'lib/media/picker'
@ -8,10 +7,8 @@ import {getImageDim} from 'lib/media/manip'
export class GalleryModel { export class GalleryModel {
images: ImageModel[] = [] images: ImageModel[] = []
constructor(public rootStore: RootStoreModel) { constructor() {
makeAutoObservable(this, { makeAutoObservable(this)
rootStore: false,
})
} }
get isEmpty() { get isEmpty() {
@ -33,7 +30,7 @@ export class GalleryModel {
// Temporarily enforce uniqueness but can eventually also use index // Temporarily enforce uniqueness but can eventually also use index
if (!this.images.some(i => i.path === image_.path)) { if (!this.images.some(i => i.path === image_.path)) {
const image = new ImageModel(this.rootStore, image_) const image = new ImageModel(image_)
// Initial resize // Initial resize
image.manipulate({}) image.manipulate({})

View File

@ -1,5 +1,4 @@
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {RootStoreModel} from 'state/index'
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {POST_IMG_MAX} from 'lib/constants' import {POST_IMG_MAX} from 'lib/constants'
import * as ImageManipulator from 'expo-image-manipulator' import * as ImageManipulator from 'expo-image-manipulator'
@ -42,10 +41,8 @@ export class ImageModel implements Omit<RNImage, 'size'> {
} }
prevAttributes: ImageManipulationAttributes = {} prevAttributes: ImageManipulationAttributes = {}
constructor(public rootStore: RootStoreModel, image: Omit<RNImage, 'size'>) { constructor(image: Omit<RNImage, 'size'>) {
makeAutoObservable(this, { makeAutoObservable(this)
rootStore: false,
})
this.path = image.path this.path = image.path
this.width = image.width this.width = image.width
@ -178,7 +175,7 @@ export class ImageModel implements Omit<RNImage, 'size'> {
height: this.height, height: this.height,
}) })
const cropped = await openCropper(this.rootStore, { const cropped = await openCropper({
mediaType: 'photo', mediaType: 'photo',
path: this.path, path: this.path,
freeStyleCropEnabled: true, freeStyleCropEnabled: true,

View File

@ -1,4 +1,4 @@
import {AppBskyEmbedRecord, AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs} from '@atproto/api'
import {RootStoreModel} from '../root-store' import {RootStoreModel} from '../root-store'
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import { 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 { export class ShellUiModel {
isLightboxActive = false isLightboxActive = false
activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null
isComposerActive = false
composerOpts: ComposerOpts | undefined
tickEveryMinute = Date.now() tickEveryMinute = Date.now()
constructor(public rootStore: RootStoreModel) { constructor(public rootStore: RootStoreModel) {
@ -92,10 +60,6 @@ export class ShellUiModel {
this.closeLightbox() this.closeLightbox()
return true return true
} }
if (this.isComposerActive) {
this.closeComposer()
return true
}
return false return false
} }
@ -106,9 +70,6 @@ export class ShellUiModel {
if (this.isLightboxActive) { if (this.isLightboxActive) {
this.closeLightbox() this.closeLightbox()
} }
if (this.isComposerActive) {
this.closeComposer()
}
} }
openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) { openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) {
@ -122,17 +83,6 @@ export class ShellUiModel {
this.activeLightbox = null this.activeLightbox = null
} }
openComposer(opts: ComposerOpts) {
this.rootStore.emitNavigation()
this.isComposerActive = true
this.composerOpts = opts
}
closeComposer() {
this.isComposerActive = false
this.composerOpts = undefined
}
setupClock() { setupClock() {
setInterval(() => { setInterval(() => {
runInAction(() => { runInAction(() => {

View File

@ -1,7 +1,8 @@
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs, BskyAgent} from '@atproto/api'
import {useQuery} from '@tanstack/react-query' import {useQuery} from '@tanstack/react-query'
import {useSession} from '../session' import {useSession} from '../session'
import {useMyFollowsQuery} from './my-follows' import {useMyFollowsQuery} from './my-follows'
import AwaitLock from 'await-lock'
export const RQKEY = (prefix: string) => ['actor-autocomplete', prefix] 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( function computeSuggestions(
prefix: string, prefix: string,
follows: AppBskyActorDefs.ProfileViewBasic[] = [], follows: AppBskyActorDefs.ProfileViewBasic[] = [],

View File

@ -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<StateContext>(undefined)
const controlsContext = React.createContext<ControlsContext>({
openComposer(_opts: ComposerOpts) {},
closeComposer() {},
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState<StateContext>()
const api = React.useMemo(
() => ({
openComposer(opts: ComposerOpts) {
setState(opts)
},
closeComposer() {
setState(undefined)
},
}),
[setState],
)
return (
<stateContext.Provider value={state}>
<controlsContext.Provider value={api}>
{children}
</controlsContext.Provider>
</stateContext.Provider>
)
}
export function useComposerState() {
return React.useContext(stateContext)
}
export function useComposerControls() {
return React.useContext(controlsContext)
}

View File

@ -5,6 +5,7 @@ import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled'
import {Provider as MinimalModeProvider} from './minimal-mode' import {Provider as MinimalModeProvider} from './minimal-mode'
import {Provider as ColorModeProvider} from './color-mode' import {Provider as ColorModeProvider} from './color-mode'
import {Provider as OnboardingProvider} from './onboarding' import {Provider as OnboardingProvider} from './onboarding'
import {Provider as ComposerProvider} from './composer'
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
export { export {
@ -22,7 +23,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
<DrawerSwipableProvider> <DrawerSwipableProvider>
<MinimalModeProvider> <MinimalModeProvider>
<ColorModeProvider> <ColorModeProvider>
<OnboardingProvider>{children}</OnboardingProvider> <OnboardingProvider>
<ComposerProvider>{children}</ComposerProvider>
</OnboardingProvider>
</ColorModeProvider> </ColorModeProvider>
</MinimalModeProvider> </MinimalModeProvider>
</DrawerSwipableProvider> </DrawerSwipableProvider>

View File

@ -16,7 +16,6 @@ import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {RichText} from '@atproto/api' import {RichText} from '@atproto/api'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible' import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
import {ExternalEmbed} from './ExternalEmbed' import {ExternalEmbed} from './ExternalEmbed'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
@ -26,9 +25,8 @@ import * as Toast from '../util/Toast'
import {TextInput, TextInputRef} from './text-input/TextInput' import {TextInput, TextInputRef} from './text-input/TextInput'
import {CharProgress} from './char-progress/CharProgress' import {CharProgress} from './char-progress/CharProgress'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {useStores} from 'state/index'
import * as apilib from 'lib/api/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 {s, colors, gradients} from 'lib/styles'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
@ -58,6 +56,9 @@ import {
useLanguagePrefsApi, useLanguagePrefsApi,
toPostLanguages, toPostLanguages,
} from '#/state/preferences/languages' } from '#/state/preferences/languages'
import {useSession} from '#/state/session'
import {useProfileQuery} from '#/state/queries/profile'
import {useComposerControls} from '#/state/shell/composer'
type Props = ComposerOpts type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({ export const ComposePost = observer(function ComposePost({
@ -66,12 +67,14 @@ export const ComposePost = observer(function ComposePost({
quote: initQuote, quote: initQuote,
mention: initMention, mention: initMention,
}: Props) { }: Props) {
const {agent, currentAccount} = useSession()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
const {activeModals} = useModals() const {activeModals} = useModals()
const {openModal, closeModal} = useModalControls() const {openModal, closeModal} = useModalControls()
const {closeComposer} = useComposerControls()
const {track} = useAnalytics() const {track} = useAnalytics()
const pal = usePalette('default') const pal = usePalette('default')
const {isDesktop, isMobile} = useWebMediaQueries() const {isDesktop, isMobile} = useWebMediaQueries()
const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled() const requireAltTextEnabled = useRequireAltTextEnabled()
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
@ -101,15 +104,10 @@ export const ComposePost = observer(function ComposePost({
const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
const [labels, setLabels] = useState<string[]>([]) const [labels, setLabels] = useState<string[]>([])
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set()) const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
const gallery = useMemo(() => new GalleryModel(store), [store]) const gallery = useMemo(() => new GalleryModel(), [])
const onClose = useCallback(() => { const onClose = useCallback(() => {
store.shell.closeComposer() closeComposer()
}, [store]) }, [closeComposer])
const autocompleteView = useMemo<UserAutocompleteModel>(
() => new UserAutocompleteModel(store),
[store],
)
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const viewStyles = useMemo( const viewStyles = useMemo(
@ -162,11 +160,6 @@ export const ComposePost = observer(function ComposePost({
} }
}, [onPressCancel]) }, [onPressCancel])
// initial setup
useEffect(() => {
autocompleteView.setup()
}, [autocompleteView])
// listen to escape key on desktop web // listen to escape key on desktop web
const onEscape = useCallback( const onEscape = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@ -216,7 +209,7 @@ export const ComposePost = observer(function ComposePost({
setIsProcessing(true) setIsProcessing(true)
try { try {
await apilib.post(store, { await apilib.post(agent, {
rawText: richtext.text, rawText: richtext.text,
replyTo: replyTo?.uri, replyTo: replyTo?.uri,
images: gallery.images, images: gallery.images,
@ -224,7 +217,6 @@ export const ComposePost = observer(function ComposePost({
extLink, extLink,
labels, labels,
onStateChange: setProcessingState, onStateChange: setProcessingState,
knownHandles: autocompleteView.knownHandles,
langs: toPostLanguages(langPrefs.postLanguage), langs: toPostLanguages(langPrefs.postLanguage),
}) })
} catch (e: any) { } catch (e: any) {
@ -381,13 +373,12 @@ export const ComposePost = observer(function ComposePost({
styles.textInputLayout, styles.textInputLayout,
isNative && styles.textInputLayoutMobile, isNative && styles.textInputLayoutMobile,
]}> ]}>
<UserAvatar avatar={store.me.avatar} size={50} /> <UserAvatar avatar={currentProfile?.avatar} size={50} />
<TextInput <TextInput
ref={textInput} ref={textInput}
richtext={richtext} richtext={richtext}
placeholder={selectTextInputPlaceholder} placeholder={selectTextInputPlaceholder}
suggestedLinks={suggestedLinks} suggestedLinks={suggestedLinks}
autocompleteView={autocompleteView}
autoFocus={true} autoFocus={true}
setRichText={setRichText} setRichText={setRichText}
onPhotoPasted={onPhotoPasted} onPhotoPasted={onPhotoPasted}

View File

@ -3,6 +3,7 @@ import React, {
useCallback, useCallback,
useRef, useRef,
useMemo, useMemo,
useState,
ComponentProps, ComponentProps,
} from 'react' } from 'react'
import { import {
@ -18,7 +19,6 @@ import PasteInput, {
} from '@mattermost/react-native-paste-input' } from '@mattermost/react-native-paste-input'
import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {Autocomplete} from './mobile/Autocomplete' import {Autocomplete} from './mobile/Autocomplete'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
@ -38,7 +38,6 @@ interface TextInputProps extends ComponentProps<typeof RNTextInput> {
richtext: RichText richtext: RichText
placeholder: string placeholder: string
suggestedLinks: Set<string> suggestedLinks: Set<string>
autocompleteView: UserAutocompleteModel
setRichText: (v: RichText | ((v: RichText) => RichText)) => void setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void onPhotoPasted: (uri: string) => void
onPressPublish: (richtext: RichText) => Promise<void> onPressPublish: (richtext: RichText) => Promise<void>
@ -56,7 +55,6 @@ export const TextInput = forwardRef(function TextInputImpl(
richtext, richtext,
placeholder, placeholder,
suggestedLinks, suggestedLinks,
autocompleteView,
setRichText, setRichText,
onPhotoPasted, onPhotoPasted,
onSuggestedLinksChanged, onSuggestedLinksChanged,
@ -69,6 +67,7 @@ export const TextInput = forwardRef(function TextInputImpl(
const textInput = useRef<PasteInputRef>(null) const textInput = useRef<PasteInputRef>(null)
const textInputSelection = useRef<Selection>({start: 0, end: 0}) const textInputSelection = useRef<Selection>({start: 0, end: 0})
const theme = useTheme() const theme = useTheme()
const [autocompletePrefix, setAutocompletePrefix] = useState('')
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
focus: () => textInput.current?.focus(), focus: () => textInput.current?.focus(),
@ -99,10 +98,9 @@ export const TextInput = forwardRef(function TextInputImpl(
textInputSelection.current?.start || 0, textInputSelection.current?.start || 0,
) )
if (prefix) { if (prefix) {
autocompleteView.setActive(true) setAutocompletePrefix(prefix.value)
autocompleteView.setPrefix(prefix.value) } else if (autocompletePrefix) {
} else { setAutocompletePrefix('')
autocompleteView.setActive(false)
} }
const set: Set<string> = new Set() const set: Set<string> = new Set()
@ -139,7 +137,8 @@ export const TextInput = forwardRef(function TextInputImpl(
}, },
[ [
setRichText, setRichText,
autocompleteView, autocompletePrefix,
setAutocompletePrefix,
suggestedLinks, suggestedLinks,
onSuggestedLinksChanged, onSuggestedLinksChanged,
onPhotoPasted, onPhotoPasted,
@ -179,9 +178,9 @@ export const TextInput = forwardRef(function TextInputImpl(
item, item,
), ),
) )
autocompleteView.setActive(false) setAutocompletePrefix('')
}, },
[onChangeText, richtext, autocompleteView], [onChangeText, richtext, setAutocompletePrefix],
) )
const textDecorated = useMemo(() => { const textDecorated = useMemo(() => {
@ -221,7 +220,7 @@ export const TextInput = forwardRef(function TextInputImpl(
{textDecorated} {textDecorated}
</PasteInput> </PasteInput>
<Autocomplete <Autocomplete
view={autocompleteView} prefix={autocompletePrefix}
onSelect={onSelectAutocompleteItem} onSelect={onSelectAutocompleteItem}
/> />
</View> </View>

View File

@ -11,13 +11,15 @@ import {Paragraph} from '@tiptap/extension-paragraph'
import {Placeholder} from '@tiptap/extension-placeholder' import {Placeholder} from '@tiptap/extension-placeholder'
import {Text} from '@tiptap/extension-text' import {Text} from '@tiptap/extension-text'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {createSuggestion} from './web/Autocomplete' import {createSuggestion} from './web/Autocomplete'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {isUriImage, blobToDataUri} from 'lib/media/util' import {isUriImage, blobToDataUri} from 'lib/media/util'
import {Emoji} from './web/EmojiPicker.web' import {Emoji} from './web/EmojiPicker.web'
import {LinkDecorator} from './web/LinkDecorator' import {LinkDecorator} from './web/LinkDecorator'
import {generateJSON} from '@tiptap/html' 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 { export interface TextInputRef {
focus: () => void focus: () => void
@ -28,7 +30,6 @@ interface TextInputProps {
richtext: RichText richtext: RichText
placeholder: string placeholder: string
suggestedLinks: Set<string> suggestedLinks: Set<string>
autocompleteView: UserAutocompleteModel
setRichText: (v: RichText | ((v: RichText) => RichText)) => void setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void onPhotoPasted: (uri: string) => void
onPressPublish: (richtext: RichText) => Promise<void> onPressPublish: (richtext: RichText) => Promise<void>
@ -43,7 +44,6 @@ export const TextInput = React.forwardRef(function TextInputImpl(
richtext, richtext,
placeholder, placeholder,
suggestedLinks, suggestedLinks,
autocompleteView,
setRichText, setRichText,
onPhotoPasted, onPhotoPasted,
onPressPublish, onPressPublish,
@ -52,6 +52,16 @@ export const TextInput = React.forwardRef(function TextInputImpl(
TextInputProps, TextInputProps,
ref, 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 modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
const extensions = React.useMemo( const extensions = React.useMemo(
() => [ () => [
@ -61,7 +71,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
HTMLAttributes: { HTMLAttributes: {
class: 'mention', class: 'mention',
}, },
suggestion: createSuggestion({autocompleteView}), suggestion: createSuggestion({autocomplete}),
}), }),
Paragraph, Paragraph,
Placeholder.configure({ Placeholder.configure({
@ -71,7 +81,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
History, History,
Hardbreak, Hardbreak,
], ],
[autocompleteView, placeholder], [autocomplete, placeholder],
) )
React.useEffect(() => { React.useEffect(() => {

View File

@ -1,31 +1,33 @@
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native' import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar' import {UserAvatar} from 'view/com/util/UserAvatar'
import {useGrapheme} from '../hooks/useGrapheme' import {useGrapheme} from '../hooks/useGrapheme'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
export const Autocomplete = observer(function AutocompleteImpl({ export const Autocomplete = observer(function AutocompleteImpl({
view, prefix,
onSelect, onSelect,
}: { }: {
view: UserAutocompleteModel prefix: string
onSelect: (item: string) => void onSelect: (item: string) => void
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const positionInterp = useAnimatedValue(0) const positionInterp = useAnimatedValue(0)
const {getGraphemeString} = useGrapheme() const {getGraphemeString} = useGrapheme()
const isActive = !!prefix
const {data: suggestions} = useActorAutocompleteQuery(prefix)
useEffect(() => { useEffect(() => {
Animated.timing(positionInterp, { Animated.timing(positionInterp, {
toValue: view.isActive ? 1 : 0, toValue: isActive ? 1 : 0,
duration: 200, duration: 200,
useNativeDriver: true, useNativeDriver: true,
}).start() }).start()
}, [positionInterp, view.isActive]) }, [positionInterp, isActive])
const topAnimStyle = { const topAnimStyle = {
transform: [ transform: [
@ -40,10 +42,10 @@ export const Autocomplete = observer(function AutocompleteImpl({
return ( return (
<Animated.View style={topAnimStyle}> <Animated.View style={topAnimStyle}>
{view.isActive ? ( {isActive ? (
<View style={[pal.view, styles.container, pal.border]}> <View style={[pal.view, styles.container, pal.border]}>
{view.suggestions.length > 0 ? ( {suggestions?.length ? (
view.suggestions.slice(0, 5).map(item => { suggestions.slice(0, 5).map(item => {
// Eventually use an average length // Eventually use an average length
const MAX_CHARS = 40 const MAX_CHARS = 40
const MAX_HANDLE_CHARS = 20 const MAX_HANDLE_CHARS = 20

View File

@ -12,7 +12,7 @@ import {
SuggestionProps, SuggestionProps,
SuggestionKeyDownProps, SuggestionKeyDownProps,
} from '@tiptap/suggestion' } 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 {usePalette} from 'lib/hooks/usePalette'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar' import {UserAvatar} from 'view/com/util/UserAvatar'
@ -23,15 +23,14 @@ interface MentionListRef {
} }
export function createSuggestion({ export function createSuggestion({
autocompleteView, autocomplete,
}: { }: {
autocompleteView: UserAutocompleteModel autocomplete: ActorAutocomplete
}): Omit<SuggestionOptions, 'editor'> { }): Omit<SuggestionOptions, 'editor'> {
return { return {
async items({query}) { async items({query}) {
autocompleteView.setActive(true) await autocomplete.query(query)
await autocompleteView.setPrefix(query) return autocomplete.suggestions.slice(0, 8)
return autocompleteView.suggestions.slice(0, 8)
}, },
render: () => { render: () => {

View File

@ -14,7 +14,7 @@ import {
isBskyCustomFeedUrl, isBskyCustomFeedUrl,
isBskyListUrl, isBskyListUrl,
} from 'lib/strings/url-helpers' } 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 {POST_IMG_MAX} from 'lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'

View File

@ -22,6 +22,7 @@ import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
const POLL_FREQ = 30e3 // 30sec const POLL_FREQ = 30e3 // 30sec
@ -46,6 +47,7 @@ export function FeedPage({
const {_} = useLingui() const {_} = useLingui()
const {isDesktop} = useWebMediaQueries() const {isDesktop} = useWebMediaQueries()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {openComposer} = useComposerControls()
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
const {screen, track} = useAnalytics() const {screen, track} = useAnalytics()
const headerOffset = useHeaderOffset() const headerOffset = useHeaderOffset()
@ -80,8 +82,8 @@ export function FeedPage({
const onPressCompose = React.useCallback(() => { const onPressCompose = React.useCallback(() => {
track('HomeScreen:PressCompose') track('HomeScreen:PressCompose')
store.shell.openComposer({}) openComposer({})
}, [store, track]) }, [openComposer, track])
const onPressLoadLatest = React.useCallback(() => { const onPressLoadLatest = React.useCallback(() => {
scrollToTop() scrollToTop()

View File

@ -20,7 +20,6 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {countLines, pluralize} from 'lib/strings/helpers' import {countLines, pluralize} from 'lib/strings/helpers'
import {isEmbedByEmbedder} from 'lib/embeds' import {isEmbedByEmbedder} from 'lib/embeds'
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
import {useStores} from 'state/index'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds' import {PostEmbeds} from '../util/post-embeds'
import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostCtrls} from '../util/post-ctrls/PostCtrls'
@ -39,6 +38,8 @@ import {MAX_POST_LINES} from 'lib/constants'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {useComposerControls} from '#/state/shell/composer'
import {useModerationOpts} from '#/state/queries/preferences'
export function PostThreadItem({ export function PostThreadItem({
post, post,
@ -65,7 +66,7 @@ export function PostThreadItem({
hasPrecedingItem: boolean hasPrecedingItem: boolean
onPostReply: () => void onPostReply: () => void
}) { }) {
const store = useStores() const moderationOpts = useModerationOpts()
const postShadowed = usePostShadow(post, dataUpdatedAt) const postShadowed = usePostShadow(post, dataUpdatedAt)
const richText = useMemo( const richText = useMemo(
() => () =>
@ -77,8 +78,8 @@ export function PostThreadItem({
) )
const moderation = useMemo( const moderation = useMemo(
() => () =>
post ? moderatePost(post, store.preferences.moderationOpts) : undefined, post && moderationOpts ? moderatePost(post, moderationOpts) : undefined,
[post, store], [post, moderationOpts],
) )
if (postShadowed === POST_TOMBSTONE) { if (postShadowed === POST_TOMBSTONE) {
return <PostThreadItemDeleted /> return <PostThreadItemDeleted />
@ -145,8 +146,8 @@ function PostThreadItemLoaded({
onPostReply: () => void onPostReply: () => void
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const {openComposer} = useComposerControls()
const [limitLines, setLimitLines] = React.useState( const [limitLines, setLimitLines] = React.useState(
countLines(richText?.text) >= MAX_POST_LINES, countLines(richText?.text) >= MAX_POST_LINES,
) )
@ -187,7 +188,7 @@ function PostThreadItemLoaded({
) )
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {
store.shell.openComposer({ openComposer({
replyTo: { replyTo: {
uri: post.uri, uri: post.uri,
cid: post.cid, cid: post.cid,
@ -200,7 +201,7 @@ function PostThreadItemLoaded({
}, },
onPost: onPostReply, onPost: onPostReply,
}) })
}, [store, post, record, onPostReply]) }, [openComposer, post, record, onPostReply])
const onPressShowMore = React.useCallback(() => { const onPressShowMore = React.useCallback(() => {
setLimitLines(false) setLimitLines(false)

View File

@ -19,7 +19,6 @@ import {PostAlerts} from '../util/moderation/PostAlerts'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText' import {RichText} from '../util/text/RichText'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
@ -27,6 +26,7 @@ import {MAX_POST_LINES} from 'lib/constants'
import {countLines} from 'lib/strings/helpers' import {countLines} from 'lib/strings/helpers'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {useComposerControls} from '#/state/shell/composer'
export function Post({ export function Post({
post, post,
@ -97,7 +97,7 @@ function PostInner({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const {openComposer} = useComposerControls()
const [limitLines, setLimitLines] = useState( const [limitLines, setLimitLines] = useState(
countLines(richText?.text) >= MAX_POST_LINES, countLines(richText?.text) >= MAX_POST_LINES,
) )
@ -110,7 +110,7 @@ function PostInner({
} }
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {
store.shell.openComposer({ openComposer({
replyTo: { replyTo: {
uri: post.uri, uri: post.uri,
cid: post.cid, cid: post.cid,
@ -122,7 +122,7 @@ function PostInner({
}, },
}, },
}) })
}, [store, post, record]) }, [openComposer, post, record])
const onPressShowMore = React.useCallback(() => { const onPressShowMore = React.useCallback(() => {
setLimitLines(false) setLimitLines(false)

View File

@ -24,7 +24,6 @@ import {RichText} from '../util/text/RichText'
import {PostSandboxWarning} from '../util/PostSandboxWarning' import {PostSandboxWarning} from '../util/PostSandboxWarning'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
@ -34,6 +33,7 @@ import {isEmbedByEmbedder} from 'lib/embeds'
import {MAX_POST_LINES} from 'lib/constants' import {MAX_POST_LINES} from 'lib/constants'
import {countLines} from 'lib/strings/helpers' import {countLines} from 'lib/strings/helpers'
import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {useComposerControls} from '#/state/shell/composer'
export function FeedItem({ export function FeedItem({
post, post,
@ -102,7 +102,7 @@ function FeedItemInner({
isThreadLastChild?: boolean isThreadLastChild?: boolean
isThreadParent?: boolean isThreadParent?: boolean
}) { }) {
const store = useStores() const {openComposer} = useComposerControls()
const pal = usePalette('default') const pal = usePalette('default')
const {track} = useAnalytics() const {track} = useAnalytics()
const [limitLines, setLimitLines] = useState( const [limitLines, setLimitLines] = useState(
@ -124,7 +124,7 @@ function FeedItemInner({
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {
track('FeedItem:PostReply') track('FeedItem:PostReply')
store.shell.openComposer({ openComposer({
replyTo: { replyTo: {
uri: post.uri, uri: post.uri,
cid: post.cid, cid: post.cid,
@ -136,7 +136,7 @@ function FeedItemInner({
}, },
}, },
}) })
}, [post, record, track, store]) }, [post, record, track, openComposer])
const onPressShowMore = React.useCallback(() => { const onPressShowMore = React.useCallback(() => {
setLimitLines(false) setLimitLines(false)

View File

@ -13,7 +13,6 @@ import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {pluralize} from 'lib/strings/helpers' import {pluralize} from 'lib/strings/helpers'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {useStores} from 'state/index'
import {RepostButton} from './RepostButton' import {RepostButton} from './RepostButton'
import {Haptics} from 'lib/haptics' import {Haptics} from 'lib/haptics'
import {HITSLOP_10, HITSLOP_20} from 'lib/constants' import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
@ -24,6 +23,7 @@ import {
usePostRepostMutation, usePostRepostMutation,
usePostUnrepostMutation, usePostUnrepostMutation,
} from '#/state/queries/post' } from '#/state/queries/post'
import {useComposerControls} from '#/state/shell/composer'
export function PostCtrls({ export function PostCtrls({
big, big,
@ -38,8 +38,8 @@ export function PostCtrls({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
onPressReply: () => void onPressReply: () => void
}) { }) {
const store = useStores()
const theme = useTheme() const theme = useTheme()
const {openComposer} = useComposerControls()
const {closeModal} = useModalControls() const {closeModal} = useModalControls()
const postLikeMutation = usePostLikeMutation() const postLikeMutation = usePostLikeMutation()
const postUnlikeMutation = usePostUnlikeMutation() const postUnlikeMutation = usePostUnlikeMutation()
@ -90,7 +90,7 @@ export function PostCtrls({
const onQuote = useCallback(() => { const onQuote = useCallback(() => {
closeModal() closeModal()
store.shell.openComposer({ openComposer({
quote: { quote: {
uri: post.uri, uri: post.uri,
cid: post.cid, cid: post.cid,
@ -100,7 +100,7 @@ export function PostCtrls({
}, },
}) })
Haptics.default() Haptics.default()
}, [post, record, store.shell, closeModal]) }, [post, record, openComposer, closeModal])
return ( return (
<View style={[styles.ctrls, style]}> <View style={[styles.ctrls, style]}>
<TouchableOpacity <TouchableOpacity

View File

@ -12,7 +12,7 @@ import {PostMeta} from '../PostMeta'
import {Link} from '../Link' import {Link} from '../Link'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {ComposerOptsQuote} from 'state/models/ui/shell' import {ComposerOptsQuote} from 'state/shell/composer'
import {PostEmbeds} from '.' import {PostEmbeds} from '.'
import {PostAlerts} from '../moderation/PostAlerts' import {PostAlerts} from '../moderation/PostAlerts'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'

View File

@ -8,7 +8,6 @@ import {FAB} from 'view/com/util/fab/FAB'
import {Link} from 'view/com/util/Link' import {Link} from 'view/com/util/Link'
import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ComposeIcon2, CogIcon} from 'lib/icons' import {ComposeIcon2, CogIcon} from 'lib/icons'
import {s} from 'lib/styles' import {s} from 'lib/styles'
@ -34,6 +33,7 @@ import {
useSearchPopularFeedsMutation, useSearchPopularFeedsMutation,
} from '#/state/queries/feed' } from '#/state/queries/feed'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {useComposerControls} from '#/state/shell/composer'
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'> type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
@ -90,8 +90,8 @@ type FlatlistSlice =
export const FeedsScreen = withAuthRequired(function FeedsScreenImpl( export const FeedsScreen = withAuthRequired(function FeedsScreenImpl(
_props: Props, _props: Props,
) { ) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {openComposer} = useComposerControls()
const {isMobile, isTabletOrDesktop} = useWebMediaQueries() const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
const [query, setQuery] = React.useState('') const [query, setQuery] = React.useState('')
const [isPTR, setIsPTR] = React.useState(false) const [isPTR, setIsPTR] = React.useState(false)
@ -128,8 +128,8 @@ export const FeedsScreen = withAuthRequired(function FeedsScreenImpl(
[search], [search],
) )
const onPressCompose = React.useCallback(() => { const onPressCompose = React.useCallback(() => {
store.shell.openComposer({}) openComposer({})
}, [store]) }, [openComposer])
const onChangeQuery = React.useCallback( const onChangeQuery = React.useCallback(
(text: string) => { (text: string) => {
setQuery(text) setQuery(text)

View File

@ -10,7 +10,6 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
import {ComposePrompt} from 'view/com/composer/Prompt' import {ComposePrompt} from 'view/com/composer/Prompt'
import {useStores} from 'state/index'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import { import {
@ -24,14 +23,15 @@ import {useSetMinimalShellMode} from '#/state/shell'
import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {ErrorMessage} from '../com/util/error/ErrorMessage' import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {CenteredView} from '../com/util/Views' import {CenteredView} from '../com/util/Views'
import {useComposerControls} from '#/state/shell/composer'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
export const PostThreadScreen = withAuthRequired( export const PostThreadScreen = withAuthRequired(
observer(function PostThreadScreenImpl({route}: Props) { observer(function PostThreadScreenImpl({route}: Props) {
const store = useStores()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {fabMinimalShellTransform} = useMinimalShellMode() const {fabMinimalShellTransform} = useMinimalShellMode()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls()
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
const {name, rkey} = route.params const {name, rkey} = route.params
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
@ -54,7 +54,7 @@ export const PostThreadScreen = withAuthRequired(
if (thread?.type !== 'post') { if (thread?.type !== 'post') {
return return
} }
store.shell.openComposer({ openComposer({
replyTo: { replyTo: {
uri: thread.post.uri, uri: thread.post.uri,
cid: thread.post.cid, cid: thread.post.cid,
@ -70,7 +70,7 @@ export const PostThreadScreen = withAuthRequired(
queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''),
}), }),
}) })
}, [store, queryClient, resolvedUri]) }, [openComposer, queryClient, resolvedUri])
return ( return (
<View style={s.hContentRegion}> <View style={s.hContentRegion}>

View File

@ -36,6 +36,7 @@ import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useComposerControls} from '#/state/shell/composer'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({ export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({
@ -128,6 +129,7 @@ function ProfileScreenLoaded({
const store = useStores() const store = useStores()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls()
const {screen, track} = useAnalytics() const {screen, track} = useAnalytics()
const [currentPage, setCurrentPage] = React.useState(0) const [currentPage, setCurrentPage] = React.useState(0)
const {_} = useLingui() const {_} = useLingui()
@ -193,8 +195,8 @@ function ProfileScreenLoaded({
profile.handle === 'handle.invalid' profile.handle === 'handle.invalid'
? undefined ? undefined
: profile.handle : profile.handle
store.shell.openComposer({mention}) openComposer({mention})
}, [store, currentAccount, track, profile]) }, [openComposer, currentAccount, track, profile])
const onPageSelected = React.useCallback( const onPageSelected = React.useCallback(
i => { i => {

View File

@ -16,7 +16,6 @@ import {CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
import {colors, s} from 'lib/styles' import {colors, s} from 'lib/styles'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {useStores} from 'state/index'
import {FeedDescriptor} from '#/state/queries/post-feed' import {FeedDescriptor} from '#/state/queries/post-feed'
import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
@ -62,6 +61,7 @@ import {
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
import {useComposerControls} from '#/state/shell/composer'
const SECTION_TITLES = ['Posts', 'About'] const SECTION_TITLES = ['Posts', 'About']
@ -163,9 +163,9 @@ export const ProfileFeedScreenInner = function ProfileFeedScreenInnerImpl({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const {openComposer} = useComposerControls()
const {track} = useAnalytics() const {track} = useAnalytics()
const feedSectionRef = React.useRef<SectionRef>(null) const feedSectionRef = React.useRef<SectionRef>(null)
@ -420,7 +420,7 @@ export const ProfileFeedScreenInner = function ProfileFeedScreenInnerImpl({
</PagerWithHeader> </PagerWithHeader>
<FAB <FAB
testID="composeFAB" testID="composeFAB"
onPress={() => store.shell.openComposer({})} onPress={() => openComposer({})}
icon={ icon={
<ComposeIcon2 strokeWidth={1.5} size={29} style={{color: 'white'}} /> <ComposeIcon2 strokeWidth={1.5} size={29} style={{color: 'white'}} />
} }

View File

@ -28,7 +28,6 @@ import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {FAB} from 'view/com/util/fab/FAB' import {FAB} from 'view/com/util/fab/FAB'
import {Haptics} from 'lib/haptics' import {Haptics} from 'lib/haptics'
import {FeedDescriptor} from '#/state/queries/post-feed' import {FeedDescriptor} from '#/state/queries/post-feed'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useSetTitle} from 'lib/hooks/useSetTitle' import {useSetTitle} from 'lib/hooks/useSetTitle'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@ -55,6 +54,7 @@ import {
} from '#/state/queries/list' } from '#/state/queries/list'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_CURATE = ['Posts', 'About']
const SECTION_TITLES_MOD = ['About'] const SECTION_TITLES_MOD = ['About']
@ -106,9 +106,9 @@ function ProfileListScreenLoaded({
uri, uri,
list, list,
}: Props & {uri: string; list: AppBskyGraphDefs.ListView}) { }: Props & {uri: string; list: AppBskyGraphDefs.ListView}) {
const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {openComposer} = useComposerControls()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {rkey} = route.params const {rkey} = route.params
const feedSectionRef = React.useRef<SectionRef>(null) const feedSectionRef = React.useRef<SectionRef>(null)
@ -191,7 +191,7 @@ function ProfileListScreenLoaded({
</PagerWithHeader> </PagerWithHeader>
<FAB <FAB
testID="composeFAB" testID="composeFAB"
onPress={() => store.shell.openComposer({})} onPress={() => openComposer({})}
icon={ icon={
<ComposeIcon2 <ComposeIcon2
strokeWidth={1.5} strokeWidth={1.5}
@ -227,7 +227,7 @@ function ProfileListScreenLoaded({
</PagerWithHeader> </PagerWithHeader>
<FAB <FAB
testID="composeFAB" testID="composeFAB"
onPress={() => store.shell.openComposer({})} onPress={() => openComposer({})}
icon={ icon={
<ComposeIcon2 strokeWidth={1.5} size={29} style={{color: 'white'}} /> <ComposeIcon2 strokeWidth={1.5} size={29} style={{color: 'white'}} />
} }

View File

@ -2,30 +2,21 @@ import React, {useEffect} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Animated, Easing, Platform, StyleSheet, View} from 'react-native' import {Animated, Easing, Platform, StyleSheet, View} from 'react-native'
import {ComposePost} from '../com/composer/Composer' 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 {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
export const Composer = observer(function ComposerImpl({ export const Composer = observer(function ComposerImpl({
active,
winHeight, winHeight,
replyTo,
onPost,
quote,
mention,
}: { }: {
active: boolean
winHeight: number winHeight: number
replyTo?: ComposerOpts['replyTo']
onPost?: ComposerOpts['onPost']
quote?: ComposerOpts['quote']
mention?: ComposerOpts['mention']
}) { }) {
const state = useComposerState()
const pal = usePalette('default') const pal = usePalette('default')
const initInterp = useAnimatedValue(0) const initInterp = useAnimatedValue(0)
useEffect(() => { useEffect(() => {
if (active) { if (state) {
Animated.timing(initInterp, { Animated.timing(initInterp, {
toValue: 1, toValue: 1,
duration: 300, duration: 300,
@ -35,7 +26,7 @@ export const Composer = observer(function ComposerImpl({
} else { } else {
initInterp.setValue(0) initInterp.setValue(0)
} }
}, [initInterp, active]) }, [initInterp, state])
const wrapperAnimStyle = { const wrapperAnimStyle = {
transform: [ transform: [
{ {
@ -50,7 +41,7 @@ export const Composer = observer(function ComposerImpl({
// rendering // rendering
// = // =
if (!active) { if (!state) {
return <View /> return <View />
} }
@ -60,10 +51,10 @@ export const Composer = observer(function ComposerImpl({
aria-modal aria-modal
accessibilityViewIsModal> accessibilityViewIsModal>
<ComposePost <ComposePost
replyTo={replyTo} replyTo={state.replyTo}
onPost={onPost} onPost={state.onPost}
quote={quote} quote={state.quote}
mention={mention} mention={state.mention}
/> />
</Animated.View> </Animated.View>
) )

View File

@ -1,34 +1,21 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {ComposePost} from '../com/composer/Composer' 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 {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
const BOTTOM_BAR_HEIGHT = 61 const BOTTOM_BAR_HEIGHT = 61
export const Composer = observer(function ComposerImpl({ export function Composer({}: {winHeight: number}) {
active,
replyTo,
quote,
onPost,
mention,
}: {
active: boolean
winHeight: number
replyTo?: ComposerOpts['replyTo']
quote: ComposerOpts['quote']
onPost?: ComposerOpts['onPost']
mention?: ComposerOpts['mention']
}) {
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const state = useComposerState()
// rendering // rendering
// = // =
if (!active) { if (!state) {
return <View /> return <View />
} }
@ -42,15 +29,15 @@ export const Composer = observer(function ComposerImpl({
pal.border, pal.border,
]}> ]}>
<ComposePost <ComposePost
replyTo={replyTo} replyTo={state.replyTo}
quote={quote} quote={state.quote}
onPost={onPost} onPost={state.onPost}
mention={mention} mention={state.mention}
/> />
</View> </View>
</View> </View>
) )
}) }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
mask: { mask: {

View File

@ -44,6 +44,7 @@ import {Trans, msg} from '@lingui/macro'
import {useProfileQuery} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useComposerControls} from '#/state/shell/composer'
const ProfileCard = observer(function ProfileCardImpl() { const ProfileCard = observer(function ProfileCardImpl() {
const {currentAccount} = useSession() const {currentAccount} = useSession()
@ -195,6 +196,7 @@ const NavItem = observer(function NavItemImpl({
function ComposeBtn() { function ComposeBtn() {
const store = useStores() const store = useStores()
const {getState} = useNavigation() const {getState} = useNavigation()
const {openComposer} = useComposerControls()
const {_} = useLingui() const {_} = useLingui()
const {isTablet} = useWebMediaQueries() const {isTablet} = useWebMediaQueries()
@ -224,7 +226,7 @@ function ComposeBtn() {
} }
const onPressCompose = async () => const onPressCompose = async () =>
store.shell.openComposer({mention: await getProfileHandle()}) openComposer({mention: await getProfileHandle()})
if (isTablet) { if (isTablet) {
return null return null

View File

@ -89,14 +89,7 @@ const ShellInner = observer(function ShellInnerImpl() {
</Drawer> </Drawer>
</ErrorBoundary> </ErrorBoundary>
</View> </View>
<Composer <Composer winHeight={winDim.height} />
active={store.shell.isComposerActive}
winHeight={winDim.height}
replyTo={store.shell.composerOpts?.replyTo}
onPost={store.shell.composerOpts?.onPost}
quote={store.shell.composerOpts?.quote}
mention={store.shell.composerOpts?.mention}
/>
<ModalsContainer /> <ModalsContainer />
<Lightbox /> <Lightbox />
</> </>

View File

@ -61,14 +61,7 @@ const ShellInner = observer(function ShellInnerImpl() {
<DesktopRightNav /> <DesktopRightNav />
</> </>
)} )}
<Composer <Composer winHeight={0} />
active={store.shell.isComposerActive}
winHeight={0}
replyTo={store.shell.composerOpts?.replyTo}
quote={store.shell.composerOpts?.quote}
onPost={store.shell.composerOpts?.onPost}
mention={store.shell.composerOpts?.mention}
/>
{showBottomBar && <BottomBarWeb />} {showBottomBar && <BottomBarWeb />}
<ModalsContainer /> <ModalsContainer />
<Lightbox /> <Lightbox />