Move remaining composer state into reducer (#5623)

Co-authored-by: Mary <git@mary.my.id>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
dan
2024-10-08 09:07:40 +09:00
committed by GitHub
parent c06040cc20
commit e1ca3ae40e
7 changed files with 203 additions and 364 deletions
+17 -28
View File
@@ -4,7 +4,6 @@ import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
AppBskyFeedPostgate,
AtUri,
BlobRef,
BskyAgent,
@@ -17,7 +16,7 @@ import {QueryClient} from '@tanstack/react-query'
import {isNetworkError} from '#/lib/strings/errors'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {logger} from '#/logger'
import {ComposerImage, compressImage} from '#/state/gallery'
import {compressImage} from '#/state/gallery'
import {writePostgateRecord} from '#/state/queries/postgate'
import {
fetchResolveGifQuery,
@@ -25,32 +24,18 @@ import {
} from '#/state/queries/resolve-link'
import {
createThreadgateRecord,
ThreadgateAllowUISetting,
threadgateAllowUISettingToAllowRecordValue,
writeThreadgateRecord,
} from '#/state/queries/threadgate'
import {ComposerState, EmbedDraft} from '#/view/com/composer/state/composer'
import {ComposerDraft, EmbedDraft} from '#/view/com/composer/state/composer'
import {createGIFDescription} from '../gif-alt-text'
import {LinkMeta} from '../link-meta/link-meta'
import {uploadBlob} from './upload-blob'
export {uploadBlob}
export interface ExternalEmbedDraft {
uri: string
isLoading: boolean
meta?: LinkMeta
embed?: AppBskyEmbedRecord.Main
localThumb?: ComposerImage
}
interface PostOpts {
composerState: ComposerState // TODO: Not used yet.
rawText: string
draft: ComposerDraft
replyTo?: string
labels?: string[]
threadgate: ThreadgateAllowUISetting[]
postgate: AppBskyFeedPostgate.Record
onStateChange?: (state: string) => void
langs?: string[]
}
@@ -60,8 +45,12 @@ export async function post(
queryClient: QueryClient,
opts: PostOpts,
) {
const draft = opts.draft
let reply
let rt = new RichText({text: opts.rawText.trimEnd()}, {cleanNewlines: true})
let rt = new RichText(
{text: draft.richtext.text.trimEnd()},
{cleanNewlines: true},
)
opts.onStateChange?.('Processing...')
@@ -73,7 +62,7 @@ export async function post(
const embed = await resolveEmbed(
agent,
queryClient,
opts.composerState,
draft,
opts.onStateChange,
)
@@ -98,10 +87,10 @@ export async function post(
// set labels
let labels: ComAtprotoLabelDefs.SelfLabels | undefined
if (opts.labels?.length) {
if (draft.labels.length) {
labels = {
$type: 'com.atproto.label.defs#selfLabels',
values: opts.labels.map(val => ({val})),
values: draft.labels.map(val => ({val})),
}
}
@@ -135,7 +124,7 @@ export async function post(
}
}
if (opts.threadgate.some(tg => tg.type !== 'everybody')) {
if (draft.threadgate.some(tg => tg.type !== 'everybody')) {
try {
// TODO: this needs to be batch-created with the post!
await writeThreadgateRecord({
@@ -143,7 +132,7 @@ export async function post(
postUri: res.uri,
threadgate: createThreadgateRecord({
post: res.uri,
allow: threadgateAllowUISettingToAllowRecordValue(opts.threadgate),
allow: threadgateAllowUISettingToAllowRecordValue(draft.threadgate),
}),
})
} catch (e: any) {
@@ -158,8 +147,8 @@ export async function post(
}
if (
opts.postgate.embeddingRules?.length ||
opts.postgate.detachedEmbeddingUris?.length
draft.postgate.embeddingRules?.length ||
draft.postgate.detachedEmbeddingUris?.length
) {
try {
// TODO: this needs to be batch-created with the post!
@@ -167,7 +156,7 @@ export async function post(
agent,
postUri: res.uri,
postgate: {
...opts.postgate,
...draft.postgate,
post: res.uri,
},
})
@@ -188,7 +177,7 @@ export async function post(
async function resolveEmbed(
agent: BskyAgent,
queryClient: QueryClient,
draft: ComposerState,
draft: ComposerDraft,
onStateChange: ((state: string) => void) | undefined,
): Promise<
| AppBskyEmbedImages.Main
+68 -25
View File
@@ -1,17 +1,20 @@
import {AppBskyActorDefs, ComAtprotoRepoStrongRef} from '@atproto/api'
import {
AppBskyActorDefs,
AppBskyFeedPost,
AppBskyGraphStarterpack,
ComAtprotoRepoStrongRef,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {BskyAgent} from '@atproto/api'
import {POST_IMG_MAX} from '#/lib/constants'
import {
getFeedAsEmbed,
getListAsEmbed,
getPostAsQuote,
getStarterPackAsEmbed,
} from '#/lib/link-meta/bsky'
import {getLinkMeta} from '#/lib/link-meta/link-meta'
import {resolveShortLink} from '#/lib/link-meta/resolve-short-link'
import {downloadAndResize} from '#/lib/media/manip'
import {
createStarterPackUri,
parseStarterPackUri,
} from '#/lib/strings/starter-pack'
import {
isBskyCustomFeedUrl,
isBskyListUrl,
@@ -24,6 +27,7 @@ import {ComposerImage} from '#/state/gallery'
import {createComposerImage} from '#/state/gallery'
import {Gif} from '#/state/queries/tenor'
import {createGIFDescription} from '../gif-alt-text'
import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
type ResolvedExternalLink = {
type: 'external'
@@ -60,6 +64,12 @@ export type ResolvedLink =
| ResolvedPostRecord
| ResolvedOtherRecord
class EmbeddingDisabledError extends Error {
constructor() {
super('Embedding is disabled for this record')
}
}
export async function resolveLink(
agent: BskyAgent,
uri: string,
@@ -68,55 +78,88 @@ export async function resolveLink(
uri = await resolveShortLink(uri)
}
if (isBskyPostUrl(uri)) {
// TODO: Remove this abstraction.
// TODO: Nice error messages (e.g. EmbeddingDisabledError).
const result = await getPostAsQuote(getPost, uri)
uri = convertBskyAppUrlIfNeeded(uri)
const [_0, user, _1, rkey] = uri.split('/').filter(Boolean)
const recordUri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
const post = await getPost({uri: recordUri})
if (post.viewer?.embeddingDisabled) {
throw new EmbeddingDisabledError()
}
return {
type: 'record',
record: {
cid: result.cid,
uri: result.uri,
cid: post.cid,
uri: post.uri,
},
kind: 'post',
meta: result,
meta: {
text: AppBskyFeedPost.isRecord(post.record) ? post.record.text : '',
indexedAt: post.indexedAt,
author: post.author,
},
}
}
if (isBskyCustomFeedUrl(uri)) {
// TODO: Remove this abstraction.
const result = await getFeedAsEmbed(agent, fetchDid, uri)
uri = convertBskyAppUrlIfNeeded(uri)
const [_0, handleOrDid, _1, rkey] = uri.split('/').filter(Boolean)
const did = await fetchDid(handleOrDid)
const feed = makeRecordUri(did, 'app.bsky.feed.generator', rkey)
const res = await agent.app.bsky.feed.getFeedGenerator({feed})
return {
type: 'record',
record: result.embed!.record,
record: {
uri: res.data.view.uri,
cid: res.data.view.cid,
},
kind: 'other',
meta: {
// TODO: Include hydrated content instead.
title: result.meta!.title!,
title: res.data.view.displayName,
},
}
}
if (isBskyListUrl(uri)) {
// TODO: Remove this abstraction.
const result = await getListAsEmbed(agent, fetchDid, uri)
uri = convertBskyAppUrlIfNeeded(uri)
const [_0, handleOrDid, _1, rkey] = uri.split('/').filter(Boolean)
const did = await fetchDid(handleOrDid)
const list = makeRecordUri(did, 'app.bsky.graph.list', rkey)
const res = await agent.app.bsky.graph.getList({list})
return {
type: 'record',
record: result.embed!.record,
record: {
uri: res.data.list.uri,
cid: res.data.list.cid,
},
kind: 'other',
meta: {
// TODO: Include hydrated content instead.
title: result.meta!.title!,
title: res.data.list.name,
},
}
}
if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) {
// TODO: Remove this abstraction.
const result = await getStarterPackAsEmbed(agent, fetchDid, uri)
const parsed = parseStarterPackUri(uri)
if (!parsed) {
throw new Error(
'Unexpectedly called getStarterPackAsEmbed with a non-starterpack url',
)
}
const did = await fetchDid(parsed.name)
const starterPack = createStarterPackUri({did, rkey: parsed.rkey})
const res = await agent.app.bsky.graph.getStarterPack({starterPack})
const record = res.data.starterPack.record
return {
type: 'record',
record: result.embed!.record,
record: {
uri: res.data.starterPack.uri,
cid: res.data.starterPack.cid,
},
kind: 'other',
meta: {
// TODO: Include hydrated content instead.
title: result.meta!.title!,
title: AppBskyGraphStarterpack.isRecord(record)
? record.name
: 'Starter Pack',
},
}
}
-225
View File
@@ -1,225 +0,0 @@
import {AppBskyFeedPost, AppBskyGraphStarterpack, BskyAgent} from '@atproto/api'
import {useFetchDid} from '#/state/queries/handle'
import {useGetPost} from '#/state/queries/post'
import * as apilib from 'lib/api/index'
import {
createStarterPackUri,
parseStarterPackUri,
} from 'lib/strings/starter-pack'
import {ComposerOptsQuote} from 'state/shell/composer'
// import {match as matchRoute} from 'view/routes'
import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
import {LikelyType, LinkMeta} from './link-meta'
// TODO
// import {Home} from 'view/screens/Home'
// import {Search} from 'view/screens/Search'
// import {Notifications} from 'view/screens/Notifications'
// import {PostThread} from 'view/screens/PostThread'
// import {PostUpvotedBy} from 'view/screens/PostUpvotedBy'
// import {PostRepostedBy} from 'view/screens/PostRepostedBy'
// import {Profile} from 'view/screens/Profile'
// import {ProfileFollowers} from 'view/screens/ProfileFollowers'
// import {ProfileFollows} from 'view/screens/ProfileFollows'
// NOTE
// this is a hack around the lack of hosted social metadata
// remove once that's implemented
// -prf
export async function extractBskyMeta(
agent: BskyAgent,
url: string,
): Promise<LinkMeta> {
url = convertBskyAppUrlIfNeeded(url)
// const route = matchRoute(url)
let meta: LinkMeta = {
likelyType: LikelyType.AtpData,
url,
// title: route.defaultTitle,
}
// if (route.Com === Home) {
// meta = {
// ...meta,
// title: 'Bluesky',
// description: 'A new kind of social network',
// }
// } else if (route.Com === Search) {
// meta = {
// ...meta,
// title: 'Search - Bluesky',
// description: 'A new kind of social network',
// }
// } else if (route.Com === Notifications) {
// meta = {
// ...meta,
// title: 'Notifications - Bluesky',
// description: 'A new kind of social network',
// }
// } else if (
// route.Com === PostThread ||
// route.Com === PostUpvotedBy ||
// route.Com === PostRepostedBy
// ) {
// // post and post-related screens
// const threadUri = makeRecordUri(
// route.params.name,
// 'app.bsky.feed.post',
// route.params.rkey,
// )
// const threadView = new PostThreadViewModel(store, {
// uri: threadUri,
// depth: 0,
// })
// await threadView.setup().catch(_err => undefined)
// const title = [
// route.Com === PostUpvotedBy
// ? 'Likes on a post by'
// : route.Com === PostRepostedBy
// ? 'Reposts of a post by'
// : 'Post by',
// threadView.thread?.post.author.displayName ||
// threadView.thread?.post.author.handle ||
// 'a bluesky user',
// ].join(' ')
// meta = {
// ...meta,
// title,
// description: threadView.thread?.postRecord?.text,
// }
// } else if (
// route.Com === Profile ||
// route.Com === ProfileFollowers ||
// route.Com === ProfileFollows
// ) {
// // profile and profile-related screens
// const profile = await store.profiles.getProfile(route.params.name)
// if (profile?.data) {
// meta = {
// ...meta,
// title: profile.data.displayName || profile.data.handle,
// description: profile.data.description,
// }
// }
// }
return meta
}
export class EmbeddingDisabledError extends Error {
constructor() {
super('Embedding is disabled for this record')
}
}
export async function getPostAsQuote(
getPost: ReturnType<typeof useGetPost>,
url: string,
): Promise<ComposerOptsQuote> {
url = convertBskyAppUrlIfNeeded(url)
const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
const post = await getPost({uri: uri})
if (post.viewer?.embeddingDisabled) {
throw new EmbeddingDisabledError()
}
return {
uri: post.uri,
cid: post.cid,
text: AppBskyFeedPost.isRecord(post.record) ? post.record.text : '',
indexedAt: post.indexedAt,
author: post.author,
}
}
export async function getFeedAsEmbed(
agent: BskyAgent,
fetchDid: ReturnType<typeof useFetchDid>,
url: string,
): Promise<apilib.ExternalEmbedDraft> {
url = convertBskyAppUrlIfNeeded(url)
const [_0, handleOrDid, _1, rkey] = url.split('/').filter(Boolean)
const did = await fetchDid(handleOrDid)
const feed = makeRecordUri(did, 'app.bsky.feed.generator', rkey)
const res = await agent.app.bsky.feed.getFeedGenerator({feed})
return {
isLoading: false,
uri: feed,
meta: {
url: feed,
likelyType: LikelyType.AtpData,
title: res.data.view.displayName,
},
embed: {
$type: 'app.bsky.embed.record',
record: {
uri: res.data.view.uri,
cid: res.data.view.cid,
},
},
}
}
export async function getListAsEmbed(
agent: BskyAgent,
fetchDid: ReturnType<typeof useFetchDid>,
url: string,
): Promise<apilib.ExternalEmbedDraft> {
url = convertBskyAppUrlIfNeeded(url)
const [_0, handleOrDid, _1, rkey] = url.split('/').filter(Boolean)
const did = await fetchDid(handleOrDid)
const list = makeRecordUri(did, 'app.bsky.graph.list', rkey)
const res = await agent.app.bsky.graph.getList({list})
return {
isLoading: false,
uri: list,
meta: {
url: list,
likelyType: LikelyType.AtpData,
title: res.data.list.name,
},
embed: {
$type: 'app.bsky.embed.record',
record: {
uri: res.data.list.uri,
cid: res.data.list.cid,
},
},
}
}
export async function getStarterPackAsEmbed(
agent: BskyAgent,
fetchDid: ReturnType<typeof useFetchDid>,
url: string,
): Promise<apilib.ExternalEmbedDraft> {
const parsed = parseStarterPackUri(url)
if (!parsed) {
throw new Error(
'Unexepectedly called getStarterPackAsEmbed with a non-starterpack url',
)
}
const did = await fetchDid(parsed.name)
const starterPack = createStarterPackUri({did, rkey: parsed.rkey})
const res = await agent.app.bsky.graph.getStarterPack({starterPack})
const record = res.data.starterPack.record
return {
isLoading: false,
uri: starterPack,
meta: {
url: starterPack,
likelyType: LikelyType.AtpData,
// Validation here should never fail
title: AppBskyGraphStarterpack.isRecord(record)
? record.name
: 'Starter Pack',
},
embed: {
$type: 'app.bsky.embed.record',
record: {
uri: res.data.starterPack.uri,
cid: res.data.starterPack.cid,
},
},
}
}
+7 -5
View File
@@ -1,10 +1,9 @@
import {BskyAgent} from '@atproto/api'
import {LINK_META_PROXY} from 'lib/constants'
import {getGiphyMetaUri} from 'lib/strings/embed-player'
import {parseStarterPackUri} from 'lib/strings/starter-pack'
import {LINK_META_PROXY} from '#/lib/constants'
import {getGiphyMetaUri} from '#/lib/strings/embed-player'
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
import {isBskyAppUrl} from '../strings/url-helpers'
import {extractBskyMeta} from './bsky'
export enum LikelyType {
HTML,
@@ -31,7 +30,10 @@ export async function getLinkMeta(
timeout = 15e3,
): Promise<LinkMeta> {
if (isBskyAppUrl(url) && !parseStarterPackUri(url)) {
return extractBskyMeta(agent, url)
return {
likelyType: LikelyType.AtpData,
url,
}
}
let urlp
+50 -75
View File
@@ -42,7 +42,6 @@ import {
AppBskyFeedGetPostThread,
BskyAgent,
} from '@atproto/api'
import {RichText} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -57,7 +56,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {logEvent} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
import {insertMentionAt} from '#/lib/strings/mention-manip'
import {shortenLinks} from '#/lib/strings/rich-text-manip'
import {colors, s} from '#/lib/styles'
import {logger} from '#/logger'
@@ -73,11 +71,8 @@ import {
useLanguagePrefs,
useLanguagePrefsApi,
} from '#/state/preferences/languages'
import {createPostgateRecord} from '#/state/queries/postgate/util'
import {useProfileQuery} from '#/state/queries/profile'
import {Gif} from '#/state/queries/tenor'
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util'
import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {ComposerOpts} from '#/state/shell/composer'
@@ -168,35 +163,40 @@ export const ComposePost = ({
const [isProcessing, setIsProcessing] = useState(false)
const [processingState, setProcessingState] = useState('')
const [error, setError] = useState('')
const [richtext, setRichText] = useState(
new RichText({
text: initText
? initText
: initMention
? insertMentionAt(
`@${initMention}`,
initMention.length + 1,
`${initMention}`,
) // insert mention if passed in
: '',
}),
const [draft, dispatch] = useReducer(
composerReducer,
{initImageUris, initQuoteUri: initQuote?.uri, initText, initMention},
createComposerState,
)
const richtext = draft.richtext
let quote: string | undefined
if (draft.embed.quote) {
quote = draft.embed.quote.uri
}
let images = NO_IMAGES
if (draft.embed.media?.type === 'images') {
images = draft.embed.media.images
}
let videoState: VideoState | NoVideoState = NO_VIDEO
if (draft.embed.media?.type === 'video') {
videoState = draft.embed.media.video
}
let extGif: Gif | undefined
let extGifAlt: string | undefined
if (draft.embed.media?.type === 'gif') {
extGif = draft.embed.media.gif
extGifAlt = draft.embed.media.alt
}
let extLink: string | undefined
if (draft.embed.link) {
extLink = draft.embed.link.uri
}
const graphemeLength = useMemo(() => {
return shortenLinks(richtext).graphemeLength
}, [richtext])
// TODO: Move more state here.
const [composerState, dispatch] = useReducer(
composerReducer,
{initImageUris, initQuoteUri: initQuote?.uri},
createComposerState,
)
let videoState: VideoState | NoVideoState = NO_VIDEO
if (composerState.embed.media?.type === 'video') {
videoState = composerState.embed.media.video
}
const selectVideo = React.useCallback(
(asset: ImagePickerAsset) => {
const abortController = new AbortController()
@@ -241,35 +241,8 @@ export const ComposePost = ({
)
const hasVideo = Boolean(videoState.asset || videoState.video)
const [publishOnUpload, setPublishOnUpload] = useState(false)
const [labels, setLabels] = useState<string[]>([])
const [threadgateAllowUISettings, onChangeThreadgateAllowUISettings] =
useState<ThreadgateAllowUISetting[]>(
threadgateViewToAllowUISetting(undefined),
)
const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
let quote: string | undefined
if (composerState.embed.quote) {
quote = composerState.embed.quote.uri
}
let images = NO_IMAGES
if (composerState.embed.media?.type === 'images') {
images = composerState.embed.media.images
}
let extGif: Gif | undefined
let extGifAlt: string | undefined
if (composerState.embed.media?.type === 'gif') {
extGif = composerState.embed.media.gif
extGifAlt = composerState.embed.media.alt
}
let extLink: string | undefined
if (composerState.embed.link) {
extLink = composerState.embed.link.uri
}
const onClose = useCallback(() => {
closeComposer()
}, [closeComposer])
@@ -419,12 +392,8 @@ export const ComposePost = ({
try {
postUri = (
await apilib.post(agent, queryClient, {
composerState, // TODO: move more state here.
rawText: richtext.text,
draft: draft,
replyTo: replyTo?.uri,
labels,
threadgate: threadgateAllowUISettings,
postgate,
onStateChange: setProcessingState,
langs: toPostLanguages(langPrefs.postLanguage),
})
@@ -497,24 +466,21 @@ export const ComposePost = ({
[
_,
agent,
composerState,
draft,
extLink,
images,
graphemeLength,
isAltTextRequiredAndMissing,
isProcessing,
labels,
langPrefs.postLanguage,
onClose,
onPost,
postgate,
quote,
initQuote,
initQuoteCount,
replyTo,
richtext.text,
setLangPrefs,
threadgateAllowUISettings,
videoState.asset,
videoState.status,
queryClient,
@@ -615,8 +581,10 @@ export const ComposePost = ({
) : (
<View style={[styles.postBtnWrapper]}>
<LabelsBtn
labels={labels}
onChange={setLabels}
labels={draft.labels}
onChange={nextLabels => {
dispatch({type: 'update_labels', labels: nextLabels})
}}
hasMedia={hasMedia || Boolean(extLink)}
/>
{canPost ? (
@@ -698,7 +666,9 @@ export const ComposePost = ({
richtext={richtext}
placeholder={selectTextInputPlaceholder}
autoFocus
setRichText={setRichText}
setRichText={rt => {
dispatch({type: 'update_richtext', richtext: rt})
}}
onPhotoPasted={onPhotoPasted}
onPressPublish={() => onPressPublish()}
onNewLink={onNewLink}
@@ -734,7 +704,7 @@ export const ComposePost = ({
</View>
)}
{!composerState.embed.media && extLink && (
{!draft.embed.media && extLink && (
<View style={a.relative} key={extLink}>
<ExternalEmbedLink
uri={extLink}
@@ -815,12 +785,17 @@ export const ComposePost = ({
{replyTo ? null : (
<ThreadgateBtn
postgate={postgate}
onChangePostgate={setPostgate}
threadgateAllowUISettings={threadgateAllowUISettings}
onChangeThreadgateAllowUISettings={
onChangeThreadgateAllowUISettings
}
postgate={draft.postgate}
onChangePostgate={nextPostgate => {
dispatch({type: 'update_postgate', postgate: nextPostgate})
}}
threadgateAllowUISettings={draft.threadgate}
onChangeThreadgateAllowUISettings={nextThreadgate => {
dispatch({
type: 'update_threadgate',
threadgate: nextThreadgate,
})
}}
style={bottomBarAnimatedStyle}
Portal={Portal.Portal}
/>
+60 -5
View File
@@ -1,12 +1,17 @@
import {ImagePickerAsset} from 'expo-image-picker'
import {AppBskyFeedPostgate, RichText} from '@atproto/api'
import {insertMentionAt} from '#/lib/strings/mention-manip'
import {
isBskyPostUrl,
postUriToRelativePath,
toBskyAppUrl,
} from '#/lib/strings/url-helpers'
import {ComposerImage, createInitialImages} from '#/state/gallery'
import {createPostgateRecord} from '#/state/queries/postgate/util'
import {Gif} from '#/state/queries/tenor'
import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate'
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
import {ComposerOpts} from '#/state/shell/composer'
import {createVideoState, VideoAction, videoReducer, VideoState} from './video'
@@ -41,12 +46,19 @@ export type EmbedDraft = {
link: Link | undefined
}
export type ComposerState = {
// TODO: Other draft data.
export type ComposerDraft = {
richtext: RichText
labels: string[]
postgate: AppBskyFeedPostgate.Record
threadgate: ThreadgateAllowUISetting[]
embed: EmbedDraft
}
export type ComposerAction =
| {type: 'update_richtext'; richtext: RichText}
| {type: 'update_labels'; labels: string[]}
| {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record}
| {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]}
| {type: 'embed_add_images'; images: ComposerImage[]}
| {type: 'embed_update_image'; image: ComposerImage}
| {type: 'embed_remove_image'; image: ComposerImage}
@@ -67,10 +79,34 @@ export type ComposerAction =
export const MAX_IMAGES = 4
export function composerReducer(
state: ComposerState,
state: ComposerDraft,
action: ComposerAction,
): ComposerState {
): ComposerDraft {
switch (action.type) {
case 'update_richtext': {
return {
...state,
richtext: action.richtext,
}
}
case 'update_labels': {
return {
...state,
labels: action.labels,
}
}
case 'update_postgate': {
return {
...state,
postgate: action.postgate,
}
}
case 'update_threadgate': {
return {
...state,
threadgate: action.threadgate,
}
}
case 'embed_add_images': {
if (action.images.length === 0) {
return state
@@ -293,12 +329,16 @@ export function composerReducer(
}
export function createComposerState({
initText,
initMention,
initImageUris,
initQuoteUri,
}: {
initText: string | undefined
initMention: string | undefined
initImageUris: ComposerOpts['imageUris']
initQuoteUri: string | undefined
}): ComposerState {
}): ComposerDraft {
let media: ImagesMedia | undefined
if (initImageUris?.length) {
media = {
@@ -317,7 +357,22 @@ export function createComposerState({
}
}
}
const initRichText = new RichText({
text: initText
? initText
: initMention
? insertMentionAt(
`@${initMention}`,
initMention.length + 1,
`${initMention}`,
)
: '',
})
return {
richtext: initRichText,
labels: [],
postgate: createPostgateRecord({post: ''}),
threadgate: threadgateViewToAllowUISetting(undefined),
embed: {
quote,
media,
@@ -43,7 +43,7 @@ export interface TextInputRef {
interface TextInputProps extends ComponentProps<typeof RNTextInput> {
richtext: RichText
placeholder: string
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
setRichText: (v: RichText) => void
onPhotoPasted: (uri: string) => void
onPressPublish: (richtext: RichText) => Promise<void>
onNewLink: (uri: string) => void