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

@ -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<string[]>([])
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
const gallery = useMemo(() => new GalleryModel(store), [store])
const gallery = useMemo(() => new GalleryModel(), [])
const onClose = useCallback(() => {
store.shell.closeComposer()
}, [store])
const autocompleteView = useMemo<UserAutocompleteModel>(
() => new UserAutocompleteModel(store),
[store],
)
closeComposer()
}, [closeComposer])
const insets = useSafeAreaInsets()
const viewStyles = useMemo(
@ -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,
]}>
<UserAvatar avatar={store.me.avatar} size={50} />
<UserAvatar avatar={currentProfile?.avatar} size={50} />
<TextInput
ref={textInput}
richtext={richtext}
placeholder={selectTextInputPlaceholder}
suggestedLinks={suggestedLinks}
autocompleteView={autocompleteView}
autoFocus={true}
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}

View file

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

View file

@ -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<string>
autocompleteView: UserAutocompleteModel
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void
onPressPublish: (richtext: RichText) => Promise<void>
@ -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(() => {

View file

@ -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 (
<Animated.View style={topAnimStyle}>
{view.isActive ? (
{isActive ? (
<View style={[pal.view, styles.container, pal.border]}>
{view.suggestions.length > 0 ? (
view.suggestions.slice(0, 5).map(item => {
{suggestions?.length ? (
suggestions.slice(0, 5).map(item => {
// Eventually use an average length
const MAX_CHARS = 40
const MAX_HANDLE_CHARS = 20

View file

@ -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<SuggestionOptions, 'editor'> {
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: () => {

View file

@ -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'