Refactor composer state for threads (#5945)
* Refactor composer state for threads * Remove unnecessary default case TS can see it's exhaustive.
This commit is contained in:
+14
-9
@@ -30,14 +30,18 @@ import {
|
|||||||
createThreadgateRecord,
|
createThreadgateRecord,
|
||||||
threadgateAllowUISettingToAllowRecordValue,
|
threadgateAllowUISettingToAllowRecordValue,
|
||||||
} from '#/state/queries/threadgate'
|
} from '#/state/queries/threadgate'
|
||||||
import {ComposerDraft, EmbedDraft} from '#/view/com/composer/state/composer'
|
import {
|
||||||
|
EmbedDraft,
|
||||||
|
PostDraft,
|
||||||
|
ThreadDraft,
|
||||||
|
} from '#/view/com/composer/state/composer'
|
||||||
import {createGIFDescription} from '../gif-alt-text'
|
import {createGIFDescription} from '../gif-alt-text'
|
||||||
import {uploadBlob} from './upload-blob'
|
import {uploadBlob} from './upload-blob'
|
||||||
|
|
||||||
export {uploadBlob}
|
export {uploadBlob}
|
||||||
|
|
||||||
interface PostOpts {
|
interface PostOpts {
|
||||||
draft: ComposerDraft
|
thread: ThreadDraft
|
||||||
replyTo?: string
|
replyTo?: string
|
||||||
onStateChange?: (state: string) => void
|
onStateChange?: (state: string) => void
|
||||||
langs?: string[]
|
langs?: string[]
|
||||||
@@ -48,7 +52,8 @@ export async function post(
|
|||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
opts: PostOpts,
|
opts: PostOpts,
|
||||||
) {
|
) {
|
||||||
const draft = opts.draft
|
const thread = opts.thread
|
||||||
|
const draft = thread.posts[0] // TODO: Support threads.
|
||||||
|
|
||||||
opts.onStateChange?.(t`Processing...`)
|
opts.onStateChange?.(t`Processing...`)
|
||||||
// NB -- Do not await anything here to avoid waterfalls!
|
// NB -- Do not await anything here to avoid waterfalls!
|
||||||
@@ -111,11 +116,11 @@ export async function post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create threadgate record
|
// Create threadgate record
|
||||||
if (draft.threadgate.some(tg => tg.type !== 'everybody')) {
|
if (thread.threadgate.some(tg => tg.type !== 'everybody')) {
|
||||||
const record = createThreadgateRecord({
|
const record = createThreadgateRecord({
|
||||||
createdAt: date,
|
createdAt: date,
|
||||||
post: uri,
|
post: uri,
|
||||||
allow: threadgateAllowUISettingToAllowRecordValue(draft.threadgate),
|
allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate),
|
||||||
})
|
})
|
||||||
|
|
||||||
writes.push({
|
writes.push({
|
||||||
@@ -128,11 +133,11 @@ export async function post(
|
|||||||
|
|
||||||
// Create postgate record
|
// Create postgate record
|
||||||
if (
|
if (
|
||||||
draft.postgate.embeddingRules?.length ||
|
thread.postgate.embeddingRules?.length ||
|
||||||
draft.postgate.detachedEmbeddingUris?.length
|
thread.postgate.detachedEmbeddingUris?.length
|
||||||
) {
|
) {
|
||||||
const record: AppBskyFeedPostgate.Record = {
|
const record: AppBskyFeedPostgate.Record = {
|
||||||
...draft.postgate,
|
...thread.postgate,
|
||||||
$type: 'app.bsky.feed.postgate',
|
$type: 'app.bsky.feed.postgate',
|
||||||
createdAt: date,
|
createdAt: date,
|
||||||
post: uri,
|
post: uri,
|
||||||
@@ -198,7 +203,7 @@ async function resolveReply(agent: BskyAgent, replyTo: string) {
|
|||||||
async function resolveEmbed(
|
async function resolveEmbed(
|
||||||
agent: BskyAgent,
|
agent: BskyAgent,
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
draft: ComposerDraft,
|
draft: PostDraft,
|
||||||
onStateChange: ((state: string) => void) | undefined,
|
onStateChange: ((state: string) => void) | undefined,
|
||||||
): Promise<
|
): Promise<
|
||||||
| AppBskyEmbedImages.Main
|
| AppBskyEmbedImages.Main
|
||||||
|
|||||||
@@ -114,11 +114,13 @@ import {Text as NewText} from '#/components/Typography'
|
|||||||
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
|
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
|
||||||
import {
|
import {
|
||||||
ComposerAction,
|
ComposerAction,
|
||||||
ComposerDraft,
|
|
||||||
composerReducer,
|
composerReducer,
|
||||||
createComposerState,
|
createComposerState,
|
||||||
EmbedDraft,
|
EmbedDraft,
|
||||||
MAX_IMAGES,
|
MAX_IMAGES,
|
||||||
|
PostAction,
|
||||||
|
PostDraft,
|
||||||
|
ThreadDraft,
|
||||||
} from './state/composer'
|
} from './state/composer'
|
||||||
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
|
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
|
||||||
|
|
||||||
@@ -161,11 +163,21 @@ export const ComposePost = ({
|
|||||||
const [publishingStage, setPublishingStage] = useState('')
|
const [publishingStage, setPublishingStage] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const [draft, dispatch] = useReducer(
|
const [composerState, composerDispatch] = useReducer(
|
||||||
composerReducer,
|
composerReducer,
|
||||||
{initImageUris, initQuoteUri: initQuote?.uri, initText, initMention},
|
{initImageUris, initQuoteUri: initQuote?.uri, initText, initMention},
|
||||||
createComposerState,
|
createComposerState,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: Display drafts for other posts in the thread.
|
||||||
|
const draft = composerState.thread.posts[composerState.activePostIndex]
|
||||||
|
const dispatch = useCallback((postAction: PostAction) => {
|
||||||
|
composerDispatch({
|
||||||
|
type: 'update_post',
|
||||||
|
postAction,
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const richtext = draft.richtext
|
const richtext = draft.richtext
|
||||||
let quote: string | undefined
|
let quote: string | undefined
|
||||||
if (draft.embed.quote) {
|
if (draft.embed.quote) {
|
||||||
@@ -207,7 +219,7 @@ export const ComposePost = ({
|
|||||||
_,
|
_,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[_, agent, currentDid],
|
[_, agent, currentDid, dispatch],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Whenever we receive an initial video uri, we should immediately run compression if necessary
|
// Whenever we receive an initial video uri, we should immediately run compression if necessary
|
||||||
@@ -333,7 +345,7 @@ export const ComposePost = ({
|
|||||||
try {
|
try {
|
||||||
postUri = (
|
postUri = (
|
||||||
await apilib.post(agent, queryClient, {
|
await apilib.post(agent, queryClient, {
|
||||||
draft: draft,
|
thread: composerState.thread,
|
||||||
replyTo: replyTo?.uri,
|
replyTo: replyTo?.uri,
|
||||||
onStateChange: setPublishingStage,
|
onStateChange: setPublishingStage,
|
||||||
langs: toPostLanguages(langPrefs.postLanguage),
|
langs: toPostLanguages(langPrefs.postLanguage),
|
||||||
@@ -409,7 +421,7 @@ export const ComposePost = ({
|
|||||||
[
|
[
|
||||||
_,
|
_,
|
||||||
agent,
|
agent,
|
||||||
draft,
|
composerState.thread,
|
||||||
extLink,
|
extLink,
|
||||||
images,
|
images,
|
||||||
canPost,
|
canPost,
|
||||||
@@ -504,8 +516,9 @@ export const ComposePost = ({
|
|||||||
|
|
||||||
<ComposerPills
|
<ComposerPills
|
||||||
isReply={!!replyTo}
|
isReply={!!replyTo}
|
||||||
draft={draft}
|
post={draft}
|
||||||
dispatch={dispatch}
|
thread={composerState.thread}
|
||||||
|
dispatch={composerDispatch}
|
||||||
bottomBarAnimatedStyle={bottomBarAnimatedStyle}
|
bottomBarAnimatedStyle={bottomBarAnimatedStyle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -543,8 +556,8 @@ function ComposerPost({
|
|||||||
onError,
|
onError,
|
||||||
onPublish,
|
onPublish,
|
||||||
}: {
|
}: {
|
||||||
draft: ComposerDraft
|
draft: PostDraft
|
||||||
dispatch: (action: ComposerAction) => void
|
dispatch: (action: PostAction) => void
|
||||||
textInput: React.Ref<TextInputRef>
|
textInput: React.Ref<TextInputRef>
|
||||||
isReply: boolean
|
isReply: boolean
|
||||||
canRemoveQuote: boolean
|
canRemoveQuote: boolean
|
||||||
@@ -736,7 +749,7 @@ function ComposerEmbeds({
|
|||||||
canRemoveQuote,
|
canRemoveQuote,
|
||||||
}: {
|
}: {
|
||||||
embed: EmbedDraft
|
embed: EmbedDraft
|
||||||
dispatch: (action: ComposerAction) => void
|
dispatch: (action: PostAction) => void
|
||||||
clearVideo: () => void
|
clearVideo: () => void
|
||||||
canRemoveQuote: boolean
|
canRemoveQuote: boolean
|
||||||
}) {
|
}) {
|
||||||
@@ -850,19 +863,21 @@ function ComposerEmbeds({
|
|||||||
|
|
||||||
function ComposerPills({
|
function ComposerPills({
|
||||||
isReply,
|
isReply,
|
||||||
draft,
|
thread,
|
||||||
|
post,
|
||||||
dispatch,
|
dispatch,
|
||||||
bottomBarAnimatedStyle,
|
bottomBarAnimatedStyle,
|
||||||
}: {
|
}: {
|
||||||
isReply: boolean
|
isReply: boolean
|
||||||
draft: ComposerDraft
|
thread: ThreadDraft
|
||||||
|
post: PostDraft
|
||||||
dispatch: (action: ComposerAction) => void
|
dispatch: (action: ComposerAction) => void
|
||||||
bottomBarAnimatedStyle: StyleProp<ViewStyle>
|
bottomBarAnimatedStyle: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const media = draft.embed.media
|
const media = post.embed.media
|
||||||
const hasMedia = media?.type === 'images' || media?.type === 'video'
|
const hasMedia = media?.type === 'images' || media?.type === 'video'
|
||||||
const hasLink = !!draft.embed.link
|
const hasLink = !!post.embed.link
|
||||||
|
|
||||||
// Don't render anything if no pills are going to be displayed
|
// Don't render anything if no pills are going to be displayed
|
||||||
if (isReply && !hasMedia && !hasLink) {
|
if (isReply && !hasMedia && !hasLink) {
|
||||||
@@ -879,11 +894,11 @@ function ComposerPills({
|
|||||||
showsHorizontalScrollIndicator={false}>
|
showsHorizontalScrollIndicator={false}>
|
||||||
{isReply ? null : (
|
{isReply ? null : (
|
||||||
<ThreadgateBtn
|
<ThreadgateBtn
|
||||||
postgate={draft.postgate}
|
postgate={thread.postgate}
|
||||||
onChangePostgate={nextPostgate => {
|
onChangePostgate={nextPostgate => {
|
||||||
dispatch({type: 'update_postgate', postgate: nextPostgate})
|
dispatch({type: 'update_postgate', postgate: nextPostgate})
|
||||||
}}
|
}}
|
||||||
threadgateAllowUISettings={draft.threadgate}
|
threadgateAllowUISettings={thread.threadgate}
|
||||||
onChangeThreadgateAllowUISettings={nextThreadgate => {
|
onChangeThreadgateAllowUISettings={nextThreadgate => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'update_threadgate',
|
type: 'update_threadgate',
|
||||||
@@ -895,9 +910,15 @@ function ComposerPills({
|
|||||||
)}
|
)}
|
||||||
{hasMedia || hasLink ? (
|
{hasMedia || hasLink ? (
|
||||||
<LabelsBtn
|
<LabelsBtn
|
||||||
labels={draft.labels}
|
labels={post.labels}
|
||||||
onChange={nextLabels => {
|
onChange={nextLabels => {
|
||||||
dispatch({type: 'update_labels', labels: nextLabels})
|
dispatch({
|
||||||
|
type: 'update_post',
|
||||||
|
postAction: {
|
||||||
|
type: 'update_labels',
|
||||||
|
labels: nextLabels,
|
||||||
|
},
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -914,8 +935,8 @@ function ComposerFooter({
|
|||||||
onError,
|
onError,
|
||||||
onSelectVideo,
|
onSelectVideo,
|
||||||
}: {
|
}: {
|
||||||
draft: ComposerDraft
|
draft: PostDraft
|
||||||
dispatch: (action: ComposerAction) => void
|
dispatch: (action: PostAction) => void
|
||||||
graphemeLength: number
|
graphemeLength: number
|
||||||
onEmojiButtonPress: () => void
|
onEmojiButtonPress: () => void
|
||||||
onError: (error: string) => void
|
onError: (error: string) => void
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
|
|||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {ComposerAction} from '../state/composer'
|
import {PostAction} from '../state/composer'
|
||||||
import {EditImageDialog} from './EditImageDialog'
|
import {EditImageDialog} from './EditImageDialog'
|
||||||
import {ImageAltTextDialog} from './ImageAltTextDialog'
|
import {ImageAltTextDialog} from './ImageAltTextDialog'
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ const IMAGE_GAP = 8
|
|||||||
|
|
||||||
interface GalleryProps {
|
interface GalleryProps {
|
||||||
images: ComposerImage[]
|
images: ComposerImage[]
|
||||||
dispatch: (action: ComposerAction) => void
|
dispatch: (action: PostAction) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export let Gallery = (props: GalleryProps): React.ReactNode => {
|
export let Gallery = (props: GalleryProps): React.ReactNode => {
|
||||||
|
|||||||
@@ -47,19 +47,15 @@ export type EmbedDraft = {
|
|||||||
link: Link | undefined
|
link: Link | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComposerDraft = {
|
export type PostDraft = {
|
||||||
richtext: RichText
|
richtext: RichText
|
||||||
labels: SelfLabel[]
|
labels: SelfLabel[]
|
||||||
postgate: AppBskyFeedPostgate.Record
|
|
||||||
threadgate: ThreadgateAllowUISetting[]
|
|
||||||
embed: EmbedDraft
|
embed: EmbedDraft
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComposerAction =
|
export type PostAction =
|
||||||
| {type: 'update_richtext'; richtext: RichText}
|
| {type: 'update_richtext'; richtext: RichText}
|
||||||
| {type: 'update_labels'; labels: SelfLabel[]}
|
| {type: 'update_labels'; labels: SelfLabel[]}
|
||||||
| {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record}
|
|
||||||
| {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]}
|
|
||||||
| {type: 'embed_add_images'; images: ComposerImage[]}
|
| {type: 'embed_add_images'; images: ComposerImage[]}
|
||||||
| {type: 'embed_update_image'; image: ComposerImage}
|
| {type: 'embed_update_image'; image: ComposerImage}
|
||||||
| {type: 'embed_remove_image'; image: ComposerImage}
|
| {type: 'embed_remove_image'; image: ComposerImage}
|
||||||
@@ -77,12 +73,65 @@ export type ComposerAction =
|
|||||||
| {type: 'embed_update_gif'; alt: string}
|
| {type: 'embed_update_gif'; alt: string}
|
||||||
| {type: 'embed_remove_gif'}
|
| {type: 'embed_remove_gif'}
|
||||||
|
|
||||||
|
export type ThreadDraft = {
|
||||||
|
posts: PostDraft[]
|
||||||
|
postgate: AppBskyFeedPostgate.Record
|
||||||
|
threadgate: ThreadgateAllowUISetting[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComposerState = {
|
||||||
|
thread: ThreadDraft
|
||||||
|
activePostIndex: number // TODO: Add actions to update this.
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComposerAction =
|
||||||
|
| {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record}
|
||||||
|
| {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]}
|
||||||
|
| {type: 'update_post'; postAction: PostAction}
|
||||||
|
|
||||||
export const MAX_IMAGES = 4
|
export const MAX_IMAGES = 4
|
||||||
|
|
||||||
export function composerReducer(
|
export function composerReducer(
|
||||||
state: ComposerDraft,
|
state: ComposerState,
|
||||||
action: ComposerAction,
|
action: ComposerAction,
|
||||||
): ComposerDraft {
|
): ComposerState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'update_postgate': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
thread: {
|
||||||
|
...state.thread,
|
||||||
|
postgate: action.postgate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'update_threadgate': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
thread: {
|
||||||
|
...state.thread,
|
||||||
|
threadgate: action.threadgate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'update_post': {
|
||||||
|
const nextPosts = [...state.thread.posts]
|
||||||
|
nextPosts[state.activePostIndex] = postReducer(
|
||||||
|
state.thread.posts[state.activePostIndex],
|
||||||
|
action.postAction,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
thread: {
|
||||||
|
...state.thread,
|
||||||
|
posts: nextPosts,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function postReducer(state: PostDraft, action: PostAction): PostDraft {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'update_richtext': {
|
case 'update_richtext': {
|
||||||
return {
|
return {
|
||||||
@@ -96,18 +145,6 @@ export function composerReducer(
|
|||||||
labels: action.labels,
|
labels: action.labels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'update_postgate': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
postgate: action.postgate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'update_threadgate': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
threadgate: action.threadgate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'embed_add_images': {
|
case 'embed_add_images': {
|
||||||
if (action.images.length === 0) {
|
if (action.images.length === 0) {
|
||||||
return state
|
return state
|
||||||
@@ -339,8 +376,6 @@ export function composerReducer(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +389,7 @@ export function createComposerState({
|
|||||||
initMention: string | undefined
|
initMention: string | undefined
|
||||||
initImageUris: ComposerOpts['imageUris']
|
initImageUris: ComposerOpts['imageUris']
|
||||||
initQuoteUri: string | undefined
|
initQuoteUri: string | undefined
|
||||||
}): ComposerDraft {
|
}): ComposerState {
|
||||||
let media: ImagesMedia | undefined
|
let media: ImagesMedia | undefined
|
||||||
if (initImageUris?.length) {
|
if (initImageUris?.length) {
|
||||||
media = {
|
media = {
|
||||||
@@ -385,14 +420,21 @@ export function createComposerState({
|
|||||||
: '',
|
: '',
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
|
activePostIndex: 0,
|
||||||
|
thread: {
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
richtext: initRichText,
|
richtext: initRichText,
|
||||||
labels: [],
|
labels: [],
|
||||||
postgate: createPostgateRecord({post: ''}),
|
|
||||||
threadgate: threadgateViewToAllowUISetting(undefined),
|
|
||||||
embed: {
|
embed: {
|
||||||
quote,
|
quote,
|
||||||
media,
|
media,
|
||||||
link: undefined,
|
link: undefined,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
postgate: createPostgateRecord({post: ''}),
|
||||||
|
threadgate: threadgateViewToAllowUISetting(undefined),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user