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:
parent
c687172de9
commit
0a26e78dcb
32 changed files with 269 additions and 239 deletions
|
@ -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({})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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[] = [],
|
||||
|
|
74
src/state/shell/composer.tsx
Normal file
74
src/state/shell/composer.tsx
Normal 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)
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue