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'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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