hoist composer state
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import React, {
|
import {
|
||||||
|
Fragment,
|
||||||
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
useReducer,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
@@ -84,7 +85,6 @@ import {
|
|||||||
createComposerImage,
|
createComposerImage,
|
||||||
pasteImage,
|
pasteImage,
|
||||||
} from '#/state/gallery'
|
} from '#/state/gallery'
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {useRequireAltTextEnabled} from '#/state/preferences'
|
import {useRequireAltTextEnabled} from '#/state/preferences'
|
||||||
import {
|
import {
|
||||||
fromPostLanguages,
|
fromPostLanguages,
|
||||||
@@ -92,7 +92,6 @@ import {
|
|||||||
useLanguagePrefs,
|
useLanguagePrefs,
|
||||||
useLanguagePrefsApi,
|
useLanguagePrefsApi,
|
||||||
} from '#/state/preferences/languages'
|
} from '#/state/preferences/languages'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {type Gif} from '#/state/queries/tenor'
|
import {type Gif} from '#/state/queries/tenor'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
@@ -139,8 +138,7 @@ import {
|
|||||||
} from './SelectMediaButton'
|
} from './SelectMediaButton'
|
||||||
import {
|
import {
|
||||||
type ComposerAction,
|
type ComposerAction,
|
||||||
composerReducer,
|
type ComposerState,
|
||||||
createComposerState,
|
|
||||||
type EmbedDraft,
|
type EmbedDraft,
|
||||||
MAX_IMAGES,
|
MAX_IMAGES,
|
||||||
type PostAction,
|
type PostAction,
|
||||||
@@ -167,16 +165,17 @@ export const ComposePost = ({
|
|||||||
onPost,
|
onPost,
|
||||||
onPostSuccess,
|
onPostSuccess,
|
||||||
quote: initQuote,
|
quote: initQuote,
|
||||||
mention: initMention,
|
|
||||||
openEmojiPicker,
|
openEmojiPicker,
|
||||||
text: initText,
|
|
||||||
imageUris: initImageUris,
|
|
||||||
videoUri: initVideoUri,
|
videoUri: initVideoUri,
|
||||||
|
composerState,
|
||||||
|
composerDispatch,
|
||||||
cancelRef,
|
cancelRef,
|
||||||
setIsDirty,
|
isDirty,
|
||||||
}: Props & {
|
}: Props & {
|
||||||
|
composerState: ComposerState
|
||||||
|
composerDispatch: React.Dispatch<ComposerAction>
|
||||||
cancelRef?: React.RefObject<CancelRef | null>
|
cancelRef?: React.RefObject<CancelRef | null>
|
||||||
setIsDirty?: React.Dispatch<React.SetStateAction<boolean>>
|
isDirty: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
@@ -190,8 +189,6 @@ export const ComposePost = ({
|
|||||||
const textInput = useRef<TextInputRef>(null)
|
const textInput = useRef<TextInputRef>(null)
|
||||||
const discardPromptControl = Prompt.usePromptControl()
|
const discardPromptControl = Prompt.usePromptControl()
|
||||||
const {closeAllDialogs} = useDialogStateControlContext()
|
const {closeAllDialogs} = useDialogStateControlContext()
|
||||||
const {closeAllModals} = useModalControls()
|
|
||||||
const {data: preferences} = usePreferencesQuery()
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
|
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
|
||||||
@@ -237,18 +234,6 @@ export const ComposePost = ({
|
|||||||
setReplyToLanguages([])
|
setReplyToLanguages([])
|
||||||
}
|
}
|
||||||
|
|
||||||
const [composerState, composerDispatch] = useReducer(
|
|
||||||
composerReducer,
|
|
||||||
{
|
|
||||||
initImageUris,
|
|
||||||
initQuoteUri: initQuote?.uri,
|
|
||||||
initText,
|
|
||||||
initMention,
|
|
||||||
initInteractionSettings: preferences?.postInteractionSettings,
|
|
||||||
},
|
|
||||||
createComposerState,
|
|
||||||
)
|
|
||||||
|
|
||||||
const thread = composerState.thread
|
const thread = composerState.thread
|
||||||
const activePost = thread.posts[composerState.activePostIndex]
|
const activePost = thread.posts[composerState.activePostIndex]
|
||||||
const nextPost: PostDraft | undefined =
|
const nextPost: PostDraft | undefined =
|
||||||
@@ -261,10 +246,10 @@ export const ComposePost = ({
|
|||||||
postAction,
|
postAction,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[activePost.id],
|
[activePost.id, composerDispatch],
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectVideo = React.useCallback(
|
const selectVideo = useCallback(
|
||||||
(postId: string, asset: ImagePickerAsset) => {
|
(postId: string, asset: ImagePickerAsset) => {
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
composerDispatch({
|
composerDispatch({
|
||||||
@@ -307,7 +292,7 @@ export const ComposePost = ({
|
|||||||
onInitVideo()
|
onInitVideo()
|
||||||
}, [onInitVideo])
|
}, [onInitVideo])
|
||||||
|
|
||||||
const clearVideo = React.useCallback(
|
const clearVideo = useCallback(
|
||||||
(postId: string) => {
|
(postId: string) => {
|
||||||
composerDispatch({
|
composerDispatch({
|
||||||
type: 'update_post',
|
type: 'update_post',
|
||||||
@@ -344,26 +329,7 @@ export const ComposePost = ({
|
|||||||
[insets, isKeyboardVisible],
|
[insets, isKeyboardVisible],
|
||||||
)
|
)
|
||||||
|
|
||||||
const isDirty = thread.posts.some(
|
|
||||||
post =>
|
|
||||||
post.shortenedGraphemeLength > 0 || post.embed.media || post.embed.link,
|
|
||||||
)
|
|
||||||
|
|
||||||
// very unfortunate, but we need to pass state back up to the parent on iOS
|
|
||||||
//
|
|
||||||
// WARNING - if the Modal on iOS thinks it's not dirty,
|
|
||||||
// `allowSwipeDismissal` will be true, and if we don't then close the composer
|
|
||||||
// when `onPressCancel` is called it will be bad (might even softlock)
|
|
||||||
// so we need to keep the parent state and the behaviour of onPressCancel in
|
|
||||||
// tight sync. do NOT force the modal to stay open without marking it as dirty! -sfn
|
|
||||||
useEffect(() => {
|
|
||||||
if (isIOS) {
|
|
||||||
setIsDirty?.(isDirty)
|
|
||||||
}
|
|
||||||
}, [isDirty, setIsDirty])
|
|
||||||
|
|
||||||
const onPressCancel = useNonReactiveCallback(() => {
|
const onPressCancel = useNonReactiveCallback(() => {
|
||||||
// web only, so it's fine w.r.t. the Modal
|
|
||||||
const didCloseAutocomplete = textInput.current?.maybeClosePopup()
|
const didCloseAutocomplete = textInput.current?.maybeClosePopup()
|
||||||
if (isWeb && didCloseAutocomplete) {
|
if (isWeb && didCloseAutocomplete) {
|
||||||
return
|
return
|
||||||
@@ -386,7 +352,7 @@ export const ComposePost = ({
|
|||||||
const backHandler = BackHandler.addEventListener(
|
const backHandler = BackHandler.addEventListener(
|
||||||
'hardwareBackPress',
|
'hardwareBackPress',
|
||||||
() => {
|
() => {
|
||||||
if (closeAllDialogs() || closeAllModals()) {
|
if (closeAllDialogs()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
onPressCancel()
|
onPressCancel()
|
||||||
@@ -396,7 +362,7 @@ export const ComposePost = ({
|
|||||||
return () => {
|
return () => {
|
||||||
backHandler.remove()
|
backHandler.remove()
|
||||||
}
|
}
|
||||||
}, [onPressCancel, closeAllDialogs, closeAllModals])
|
}, [onPressCancel, closeAllDialogs])
|
||||||
|
|
||||||
const missingAltError = useMemo(() => {
|
const missingAltError = useMemo(() => {
|
||||||
if (!requireAltTextEnabled) {
|
if (!requireAltTextEnabled) {
|
||||||
@@ -434,7 +400,7 @@ export const ComposePost = ({
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPressPublish = React.useCallback(async () => {
|
const onPressPublish = useCallback(async () => {
|
||||||
if (isPublishing) {
|
if (isPublishing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -629,7 +595,7 @@ export const ComposePost = ({
|
|||||||
onPressPublish()
|
onPressPublish()
|
||||||
})
|
})
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (publishOnUpload) {
|
if (publishOnUpload) {
|
||||||
let erroredVideos = 0
|
let erroredVideos = 0
|
||||||
let uploadingVideos = 0
|
let uploadingVideos = 0
|
||||||
@@ -673,8 +639,7 @@ export const ComposePost = ({
|
|||||||
if (rect) {
|
if (rect) {
|
||||||
openEmojiPicker?.({
|
openEmojiPicker?.({
|
||||||
...rect,
|
...rect,
|
||||||
nextFocusRef:
|
nextFocusRef: textInput as unknown as React.RefObject<HTMLElement>,
|
||||||
textInput as unknown as React.MutableRefObject<HTMLElement>,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [openEmojiPicker])
|
}, [openEmojiPicker])
|
||||||
@@ -786,7 +751,7 @@ export const ComposePost = ({
|
|||||||
onLayout={onScrollViewLayout}>
|
onLayout={onScrollViewLayout}>
|
||||||
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
||||||
{thread.posts.map((post, index) => (
|
{thread.posts.map((post, index) => (
|
||||||
<React.Fragment key={post.id}>
|
<Fragment key={post.id}>
|
||||||
<ComposerPost
|
<ComposerPost
|
||||||
post={post}
|
post={post}
|
||||||
dispatch={composerDispatch}
|
dispatch={composerDispatch}
|
||||||
@@ -806,7 +771,7 @@ export const ComposePost = ({
|
|||||||
{isWebFooterSticky && post.id === activePost.id && (
|
{isWebFooterSticky && post.id === activePost.id && (
|
||||||
<View style={styles.stickyFooterWeb}>{footer}</View>
|
<View style={styles.stickyFooterWeb}>{footer}</View>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
{!isWebFooterSticky && footer}
|
{!isWebFooterSticky && footer}
|
||||||
@@ -825,7 +790,7 @@ export const ComposePost = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let ComposerPost = React.memo(function ComposerPost({
|
let ComposerPost = memo(function ComposerPost({
|
||||||
post,
|
post,
|
||||||
dispatch,
|
dispatch,
|
||||||
textInput,
|
textInput,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {useReducer, useState} from 'react'
|
||||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||||
import {
|
import {
|
||||||
type AppBskyFeedPostgate,
|
type AppBskyFeedPostgate,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
} from '#/lib/strings/url-helpers'
|
} from '#/lib/strings/url-helpers'
|
||||||
import {type ComposerImage, createInitialImages} from '#/state/gallery'
|
import {type ComposerImage, createInitialImages} from '#/state/gallery'
|
||||||
import {createPostgateRecord} from '#/state/queries/postgate/util'
|
import {createPostgateRecord} from '#/state/queries/postgate/util'
|
||||||
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {type Gif} from '#/state/queries/tenor'
|
import {type Gif} from '#/state/queries/tenor'
|
||||||
import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate'
|
import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate'
|
||||||
import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||||
@@ -104,6 +106,8 @@ export type ComposerState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ComposerAction =
|
export type ComposerAction =
|
||||||
|
| {type: 'init'; initialState: InitialState}
|
||||||
|
| {type: 'clear'}
|
||||||
| {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record}
|
| {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record}
|
||||||
| {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]}
|
| {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]}
|
||||||
| {
|
| {
|
||||||
@@ -125,11 +129,63 @@ export type ComposerAction =
|
|||||||
|
|
||||||
export const MAX_IMAGES = 4
|
export const MAX_IMAGES = 4
|
||||||
|
|
||||||
export function composerReducer(
|
const EMPTY_STATE: ComposerState = {
|
||||||
|
thread: {
|
||||||
|
posts: [],
|
||||||
|
threadgate: [],
|
||||||
|
postgate: createPostgateRecord({post: ''}),
|
||||||
|
},
|
||||||
|
activePostIndex: 0,
|
||||||
|
mutableNeedsFocusActive: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the internal state of the composer
|
||||||
|
*/
|
||||||
|
export function useComposerReducer(composerOpts: ComposerOpts | undefined) {
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(composerReducer, EMPTY_STATE)
|
||||||
|
|
||||||
|
const open = !!composerOpts
|
||||||
|
const [prevOpen, setPrevOpen] = useState(open)
|
||||||
|
if (open !== prevOpen) {
|
||||||
|
setPrevOpen(open)
|
||||||
|
if (open) {
|
||||||
|
dispatch({
|
||||||
|
type: 'init',
|
||||||
|
initialState: {
|
||||||
|
initImageUris: composerOpts.imageUris,
|
||||||
|
initQuoteUri: composerOpts.quote?.uri,
|
||||||
|
initText: composerOpts.text,
|
||||||
|
initMention: composerOpts.mention,
|
||||||
|
initInteractionSettings: preferences?.postInteractionSettings,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
dispatch({type: 'clear'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirty = state.thread.posts.some(
|
||||||
|
post =>
|
||||||
|
post.shortenedGraphemeLength > 0 || post.embed.media || post.embed.link,
|
||||||
|
)
|
||||||
|
|
||||||
|
return [state, dispatch, isDirty] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
function composerReducer(
|
||||||
state: ComposerState,
|
state: ComposerState,
|
||||||
action: ComposerAction,
|
action: ComposerAction,
|
||||||
): ComposerState {
|
): ComposerState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case 'init': {
|
||||||
|
return createComposerState(action.initialState)
|
||||||
|
}
|
||||||
|
case 'clear': {
|
||||||
|
return state
|
||||||
|
}
|
||||||
case 'update_postgate': {
|
case 'update_postgate': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -482,13 +538,7 @@ function postReducer(state: PostDraft, action: PostAction): PostDraft {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createComposerState({
|
type InitialState = {
|
||||||
initText,
|
|
||||||
initMention,
|
|
||||||
initImageUris,
|
|
||||||
initQuoteUri,
|
|
||||||
initInteractionSettings,
|
|
||||||
}: {
|
|
||||||
initText: string | undefined
|
initText: string | undefined
|
||||||
initMention: string | undefined
|
initMention: string | undefined
|
||||||
initImageUris: ComposerOpts['imageUris']
|
initImageUris: ComposerOpts['imageUris']
|
||||||
@@ -496,7 +546,15 @@ export function createComposerState({
|
|||||||
initInteractionSettings:
|
initInteractionSettings:
|
||||||
| BskyPreferences['postInteractionSettings']
|
| BskyPreferences['postInteractionSettings']
|
||||||
| undefined
|
| undefined
|
||||||
}): ComposerState {
|
}
|
||||||
|
|
||||||
|
function createComposerState({
|
||||||
|
initText,
|
||||||
|
initMention,
|
||||||
|
initImageUris,
|
||||||
|
initQuoteUri,
|
||||||
|
initInteractionSettings,
|
||||||
|
}: InitialState): ComposerState {
|
||||||
let media: ImagesMedia | undefined
|
let media: ImagesMedia | undefined
|
||||||
if (initImageUris?.length) {
|
if (initImageUris?.length) {
|
||||||
media = {
|
media = {
|
||||||
|
|||||||
@@ -1,34 +1,31 @@
|
|||||||
import {useEffect, useRef, useState} from 'react'
|
import {useEffect} from 'react'
|
||||||
import {Modal, View} from 'react-native'
|
import {Modal, View} from 'react-native'
|
||||||
|
import {SystemBars} from 'react-native-edge-to-edge'
|
||||||
|
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
|
||||||
import {useComposerState} from '#/state/shell/composer'
|
import {useComposerState} from '#/state/shell/composer'
|
||||||
|
import {useComposerReducer} from '#/view/com/composer/state/composer'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
|
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
|
||||||
|
|
||||||
export function Composer({}: {winHeight: number}) {
|
export function Composer({}: {winHeight: number}) {
|
||||||
const {setFullyExpandedCount} = useDialogStateControlContext()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const state = useComposerState()
|
const state = useComposerState()
|
||||||
const ref = useComposerCancelRef()
|
const ref = useComposerCancelRef()
|
||||||
const [isDirty, setIsDirty] = useState(
|
|
||||||
!!state?.text ||
|
|
||||||
!!state?.imageUris ||
|
|
||||||
!!state?.videoUri ||
|
|
||||||
!!state?.mention,
|
|
||||||
)
|
|
||||||
|
|
||||||
const open = !!state
|
const open = !!state
|
||||||
const prevOpen = useRef(open)
|
|
||||||
|
const [composerState, composerDispatch, isDirty] = useComposerReducer(state)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && !prevOpen.current) {
|
if (open) {
|
||||||
setFullyExpandedCount(c => c + 1)
|
const entry = SystemBars.pushStackEntry({
|
||||||
} else if (!open && prevOpen.current) {
|
style: {statusBar: 'light'},
|
||||||
setFullyExpandedCount(c => c - 1)
|
})
|
||||||
|
return () => {
|
||||||
|
SystemBars.popStackEntry(entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
prevOpen.current = open
|
}, [open])
|
||||||
}, [open, setFullyExpandedCount])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -50,7 +47,9 @@ export function Composer({}: {winHeight: number}) {
|
|||||||
text={state?.text}
|
text={state?.text}
|
||||||
imageUris={state?.imageUris}
|
imageUris={state?.imageUris}
|
||||||
videoUri={state?.videoUri}
|
videoUri={state?.videoUri}
|
||||||
setIsDirty={setIsDirty}
|
composerState={composerState}
|
||||||
|
composerDispatch={composerDispatch}
|
||||||
|
isDirty={isDirty}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
import {Animated, Easing} from 'react-native'
|
import {Animated, Easing} from 'react-native'
|
||||||
|
import {SystemBars} from 'react-native-edge-to-edge'
|
||||||
|
|
||||||
import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue'
|
import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue'
|
||||||
import {useComposerState} from '#/state/shell/composer'
|
import {useComposerState} from '#/state/shell/composer'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ComposePost} from '../com/composer/Composer'
|
import {ComposePost} from '../com/composer/Composer'
|
||||||
|
import {useComposerReducer} from '../com/composer/state/composer'
|
||||||
|
|
||||||
export function Composer({winHeight}: {winHeight: number}) {
|
export function Composer({winHeight}: {winHeight: number}) {
|
||||||
const state = useComposerState()
|
const state = useComposerState()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const initInterp = useAnimatedValue(0)
|
const initInterp = useAnimatedValue(0)
|
||||||
|
|
||||||
|
const open = !!state
|
||||||
|
|
||||||
|
const [composerState, composerDispatch, isDirty] = useComposerReducer(state)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state) {
|
if (open) {
|
||||||
|
const entry = SystemBars.pushStackEntry({
|
||||||
|
style: {statusBar: t.scheme === 'light' ? 'dark' : 'light'},
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
SystemBars.popStackEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [open, t.scheme])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
Animated.timing(initInterp, {
|
Animated.timing(initInterp, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
@@ -22,7 +39,7 @@ export function Composer({winHeight}: {winHeight: number}) {
|
|||||||
} else {
|
} else {
|
||||||
initInterp.setValue(0)
|
initInterp.setValue(0)
|
||||||
}
|
}
|
||||||
}, [initInterp, state])
|
}, [initInterp, open])
|
||||||
const wrapperAnimStyle = {
|
const wrapperAnimStyle = {
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
@@ -37,7 +54,7 @@ export function Composer({winHeight}: {winHeight: number}) {
|
|||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
|
||||||
if (!state) {
|
if (!open) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +72,9 @@ export function Composer({winHeight}: {winHeight: number}) {
|
|||||||
text={state.text}
|
text={state.text}
|
||||||
imageUris={state.imageUris}
|
imageUris={state.imageUris}
|
||||||
videoUri={state.videoUri}
|
videoUri={state.videoUri}
|
||||||
|
composerState={composerState}
|
||||||
|
composerDispatch={composerDispatch}
|
||||||
|
isDirty={isDirty}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import {DismissableLayer, FocusGuards, FocusScope} from 'radix-ui/internal'
|
|||||||
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||||
|
|
||||||
import {useA11y} from '#/state/a11y'
|
import {useA11y} from '#/state/a11y'
|
||||||
import {useModals} from '#/state/modals'
|
|
||||||
import {type ComposerOpts, useComposerState} from '#/state/shell/composer'
|
import {type ComposerOpts, useComposerState} from '#/state/shell/composer'
|
||||||
|
import {ComposePost, useComposerCancelRef} from '#/view/com/composer/Composer'
|
||||||
|
import {
|
||||||
|
type ComposerAction,
|
||||||
|
type ComposerState,
|
||||||
|
useComposerReducer,
|
||||||
|
} from '#/view/com/composer/state/composer'
|
||||||
import {
|
import {
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
type EmojiPickerPosition,
|
type EmojiPickerPosition,
|
||||||
type EmojiPickerState,
|
type EmojiPickerState,
|
||||||
} from '#/view/com/composer/text-input/web/EmojiPicker'
|
} from '#/view/com/composer/text-input/web/EmojiPicker'
|
||||||
import {atoms as a, flatten, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, flatten, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
|
|
||||||
|
|
||||||
const BOTTOM_BAR_HEIGHT = 61
|
const BOTTOM_BAR_HEIGHT = 61
|
||||||
|
|
||||||
@@ -20,6 +24,8 @@ export function Composer({}: {winHeight: number}) {
|
|||||||
const state = useComposerState()
|
const state = useComposerState()
|
||||||
const isActive = !!state
|
const isActive = !!state
|
||||||
|
|
||||||
|
const [composerState, composerDispatch, isDirty] = useComposerReducer(state)
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
|
||||||
@@ -30,14 +36,28 @@ export function Composer({}: {winHeight: number}) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RemoveScrollBar />
|
<RemoveScrollBar />
|
||||||
<Inner state={state} />
|
<Inner
|
||||||
|
state={state}
|
||||||
|
composerState={composerState}
|
||||||
|
composerDispatch={composerDispatch}
|
||||||
|
isDirty={isDirty}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Inner({state}: {state: ComposerOpts}) {
|
function Inner({
|
||||||
|
state,
|
||||||
|
composerState,
|
||||||
|
composerDispatch,
|
||||||
|
isDirty,
|
||||||
|
}: {
|
||||||
|
state: ComposerOpts
|
||||||
|
composerState: ComposerState
|
||||||
|
composerDispatch: React.Dispatch<ComposerAction>
|
||||||
|
isDirty: boolean
|
||||||
|
}) {
|
||||||
const ref = useComposerCancelRef()
|
const ref = useComposerCancelRef()
|
||||||
const {isModalActive} = useModals()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {reduceMotionEnabled} = useA11y()
|
const {reduceMotionEnabled} = useA11y()
|
||||||
@@ -82,12 +102,7 @@ function Inner({state}: {state: ComposerOpts}) {
|
|||||||
])}
|
])}
|
||||||
onFocusOutside={evt => evt.preventDefault()}
|
onFocusOutside={evt => evt.preventDefault()}
|
||||||
onInteractOutside={evt => evt.preventDefault()}
|
onInteractOutside={evt => evt.preventDefault()}
|
||||||
onDismiss={() => {
|
onDismiss={() => ref.current?.onPressCancel()}>
|
||||||
// TEMP: remove when all modals are ALF'd -sfn
|
|
||||||
if (!isModalActive) {
|
|
||||||
ref.current?.onPressCancel()
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
@@ -110,6 +125,9 @@ function Inner({state}: {state: ComposerOpts}) {
|
|||||||
openEmojiPicker={onOpenPicker}
|
openEmojiPicker={onOpenPicker}
|
||||||
text={state.text}
|
text={state.text}
|
||||||
imageUris={state.imageUris}
|
imageUris={state.imageUris}
|
||||||
|
composerState={composerState}
|
||||||
|
composerDispatch={composerDispatch}
|
||||||
|
isDirty={isDirty}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<EmojiPicker state={pickerState} close={onClosePicker} />
|
<EmojiPicker state={pickerState} close={onClosePicker} />
|
||||||
|
|||||||
Reference in New Issue
Block a user