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>
This commit is contained in:
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

@ -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({})

View file

@ -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<RNImage, 'size'> {
}
prevAttributes: ImageManipulationAttributes = {}
constructor(public rootStore: RootStoreModel, image: Omit<RNImage, 'size'>) {
makeAutoObservable(this, {
rootStore: false,
})
constructor(image: Omit<RNImage, 'size'>) {
makeAutoObservable(this)
this.path = image.path
this.width = image.width
@ -178,7 +175,7 @@ export class ImageModel implements Omit<RNImage, 'size'> {
height: this.height,
})
const cropped = await openCropper(this.rootStore, {
const cropped = await openCropper({
mediaType: 'photo',
path: this.path,
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 {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(() => {

View file

@ -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[] = [],

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 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<{}>) {
<DrawerSwipableProvider>
<MinimalModeProvider>
<ColorModeProvider>
<OnboardingProvider>{children}</OnboardingProvider>
<OnboardingProvider>
<ComposerProvider>{children}</ComposerProvider>
</OnboardingProvider>
</ColorModeProvider>
</MinimalModeProvider>
</DrawerSwipableProvider>