diff options
author | dan <dan.abramov@gmail.com> | 2024-10-08 09:07:40 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-07 17:07:40 -0700 |
commit | e1ca3ae40e8a208ab2ab0b89a96b8e314042c75b (patch) | |
tree | 3552743fa299b00b5f13e31721fa219ec50c136e /src | |
parent | c06040cc209338fc37980648b31d4d64cc0c5c09 (diff) | |
download | voidsky-e1ca3ae40e8a208ab2ab0b89a96b8e314042c75b.tar.zst |
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>
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/api/index.ts | 45 | ||||
-rw-r--r-- | src/lib/api/resolve.ts | 93 | ||||
-rw-r--r-- | src/lib/link-meta/bsky.ts | 225 | ||||
-rw-r--r-- | src/lib/link-meta/link-meta.ts | 12 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 121 | ||||
-rw-r--r-- | src/view/com/composer/state/composer.ts | 65 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.tsx | 2 |
7 files changed, 201 insertions, 362 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 6edb111e6..4b203d28b 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -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 diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index 4f409e100..1115a4fb0 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -1,18 +1,21 @@ -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, isBskyPostUrl, @@ -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', }, } } diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts deleted file mode 100644 index 3d49f5237..000000000 --- a/src/lib/link-meta/bsky.ts +++ /dev/null @@ -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, - }, - }, - } -} diff --git a/src/lib/link-meta/link-meta.ts b/src/lib/link-meta/link-meta.ts index 6416df2b7..ea9190b6e 100644 --- a/src/lib/link-meta/link-meta.ts +++ b/src/lib/link-meta/link-meta.ts @@ -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 diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index ecafea500..87f72ce42 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -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 graphemeLength = useMemo(() => { - return shortenLinks(richtext).graphemeLength - }, [richtext]) - // TODO: Move more state here. - const [composerState, dispatch] = useReducer( + const [draft, dispatch] = useReducer( composerReducer, - {initImageUris, initQuoteUri: initQuote?.uri}, + {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 (composerState.embed.media?.type === 'video') { - videoState = composerState.embed.media.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]) + 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} /> diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 6156d3cfa..e37690342 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -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, diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 39baa2cb6..43074fa5b 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -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 |