my fork of the bluesky client

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>

authored by danabra.mov

Mary
surfdude29
Hailey
and committed by
GitHub
e1ca3ae4 c06040cc

+201 -362
+17 -28
src/lib/api/index.ts
··· 4 AppBskyEmbedRecord, 5 AppBskyEmbedRecordWithMedia, 6 AppBskyEmbedVideo, 7 - AppBskyFeedPostgate, 8 AtUri, 9 BlobRef, 10 BskyAgent, ··· 17 import {isNetworkError} from '#/lib/strings/errors' 18 import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 19 import {logger} from '#/logger' 20 - import {ComposerImage, compressImage} from '#/state/gallery' 21 import {writePostgateRecord} from '#/state/queries/postgate' 22 import { 23 fetchResolveGifQuery, ··· 25 } from '#/state/queries/resolve-link' 26 import { 27 createThreadgateRecord, 28 - ThreadgateAllowUISetting, 29 threadgateAllowUISettingToAllowRecordValue, 30 writeThreadgateRecord, 31 } from '#/state/queries/threadgate' 32 - import {ComposerState, EmbedDraft} from '#/view/com/composer/state/composer' 33 import {createGIFDescription} from '../gif-alt-text' 34 - import {LinkMeta} from '../link-meta/link-meta' 35 import {uploadBlob} from './upload-blob' 36 37 export {uploadBlob} 38 39 - export interface ExternalEmbedDraft { 40 - uri: string 41 - isLoading: boolean 42 - meta?: LinkMeta 43 - embed?: AppBskyEmbedRecord.Main 44 - localThumb?: ComposerImage 45 - } 46 - 47 interface PostOpts { 48 - composerState: ComposerState // TODO: Not used yet. 49 - rawText: string 50 replyTo?: string 51 - labels?: string[] 52 - threadgate: ThreadgateAllowUISetting[] 53 - postgate: AppBskyFeedPostgate.Record 54 onStateChange?: (state: string) => void 55 langs?: string[] 56 } ··· 60 queryClient: QueryClient, 61 opts: PostOpts, 62 ) { 63 let reply 64 - let rt = new RichText({text: opts.rawText.trimEnd()}, {cleanNewlines: true}) 65 66 opts.onStateChange?.('Processing...') 67 ··· 73 const embed = await resolveEmbed( 74 agent, 75 queryClient, 76 - opts.composerState, 77 opts.onStateChange, 78 ) 79 ··· 98 99 // set labels 100 let labels: ComAtprotoLabelDefs.SelfLabels | undefined 101 - if (opts.labels?.length) { 102 labels = { 103 $type: 'com.atproto.label.defs#selfLabels', 104 - values: opts.labels.map(val => ({val})), 105 } 106 } 107 ··· 135 } 136 } 137 138 - if (opts.threadgate.some(tg => tg.type !== 'everybody')) { 139 try { 140 // TODO: this needs to be batch-created with the post! 141 await writeThreadgateRecord({ ··· 143 postUri: res.uri, 144 threadgate: createThreadgateRecord({ 145 post: res.uri, 146 - allow: threadgateAllowUISettingToAllowRecordValue(opts.threadgate), 147 }), 148 }) 149 } catch (e: any) { ··· 158 } 159 160 if ( 161 - opts.postgate.embeddingRules?.length || 162 - opts.postgate.detachedEmbeddingUris?.length 163 ) { 164 try { 165 // TODO: this needs to be batch-created with the post! ··· 167 agent, 168 postUri: res.uri, 169 postgate: { 170 - ...opts.postgate, 171 post: res.uri, 172 }, 173 }) ··· 188 async function resolveEmbed( 189 agent: BskyAgent, 190 queryClient: QueryClient, 191 - draft: ComposerState, 192 onStateChange: ((state: string) => void) | undefined, 193 ): Promise< 194 | AppBskyEmbedImages.Main
··· 4 AppBskyEmbedRecord, 5 AppBskyEmbedRecordWithMedia, 6 AppBskyEmbedVideo, 7 AtUri, 8 BlobRef, 9 BskyAgent, ··· 16 import {isNetworkError} from '#/lib/strings/errors' 17 import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 18 import {logger} from '#/logger' 19 + import {compressImage} from '#/state/gallery' 20 import {writePostgateRecord} from '#/state/queries/postgate' 21 import { 22 fetchResolveGifQuery, ··· 24 } from '#/state/queries/resolve-link' 25 import { 26 createThreadgateRecord, 27 threadgateAllowUISettingToAllowRecordValue, 28 writeThreadgateRecord, 29 } from '#/state/queries/threadgate' 30 + import {ComposerDraft, EmbedDraft} from '#/view/com/composer/state/composer' 31 import {createGIFDescription} from '../gif-alt-text' 32 import {uploadBlob} from './upload-blob' 33 34 export {uploadBlob} 35 36 interface PostOpts { 37 + draft: ComposerDraft 38 replyTo?: string 39 onStateChange?: (state: string) => void 40 langs?: string[] 41 } ··· 45 queryClient: QueryClient, 46 opts: PostOpts, 47 ) { 48 + const draft = opts.draft 49 let reply 50 + let rt = new RichText( 51 + {text: draft.richtext.text.trimEnd()}, 52 + {cleanNewlines: true}, 53 + ) 54 55 opts.onStateChange?.('Processing...') 56 ··· 62 const embed = await resolveEmbed( 63 agent, 64 queryClient, 65 + draft, 66 opts.onStateChange, 67 ) 68 ··· 87 88 // set labels 89 let labels: ComAtprotoLabelDefs.SelfLabels | undefined 90 + if (draft.labels.length) { 91 labels = { 92 $type: 'com.atproto.label.defs#selfLabels', 93 + values: draft.labels.map(val => ({val})), 94 } 95 } 96 ··· 124 } 125 } 126 127 + if (draft.threadgate.some(tg => tg.type !== 'everybody')) { 128 try { 129 // TODO: this needs to be batch-created with the post! 130 await writeThreadgateRecord({ ··· 132 postUri: res.uri, 133 threadgate: createThreadgateRecord({ 134 post: res.uri, 135 + allow: threadgateAllowUISettingToAllowRecordValue(draft.threadgate), 136 }), 137 }) 138 } catch (e: any) { ··· 147 } 148 149 if ( 150 + draft.postgate.embeddingRules?.length || 151 + draft.postgate.detachedEmbeddingUris?.length 152 ) { 153 try { 154 // TODO: this needs to be batch-created with the post! ··· 156 agent, 157 postUri: res.uri, 158 postgate: { 159 + ...draft.postgate, 160 post: res.uri, 161 }, 162 }) ··· 177 async function resolveEmbed( 178 agent: BskyAgent, 179 queryClient: QueryClient, 180 + draft: ComposerDraft, 181 onStateChange: ((state: string) => void) | undefined, 182 ): Promise< 183 | AppBskyEmbedImages.Main
+68 -25
src/lib/api/resolve.ts
··· 1 - import {AppBskyActorDefs, ComAtprotoRepoStrongRef} from '@atproto/api' 2 import {AtUri} from '@atproto/api' 3 import {BskyAgent} from '@atproto/api' 4 5 import {POST_IMG_MAX} from '#/lib/constants' 6 - import { 7 - getFeedAsEmbed, 8 - getListAsEmbed, 9 - getPostAsQuote, 10 - getStarterPackAsEmbed, 11 - } from '#/lib/link-meta/bsky' 12 import {getLinkMeta} from '#/lib/link-meta/link-meta' 13 import {resolveShortLink} from '#/lib/link-meta/resolve-short-link' 14 import {downloadAndResize} from '#/lib/media/manip' 15 import { 16 isBskyCustomFeedUrl, 17 isBskyListUrl, ··· 24 import {createComposerImage} from '#/state/gallery' 25 import {Gif} from '#/state/queries/tenor' 26 import {createGIFDescription} from '../gif-alt-text' 27 28 type ResolvedExternalLink = { 29 type: 'external' ··· 59 | ResolvedExternalLink 60 | ResolvedPostRecord 61 | ResolvedOtherRecord 62 63 export async function resolveLink( 64 agent: BskyAgent, ··· 68 uri = await resolveShortLink(uri) 69 } 70 if (isBskyPostUrl(uri)) { 71 - // TODO: Remove this abstraction. 72 - // TODO: Nice error messages (e.g. EmbeddingDisabledError). 73 - const result = await getPostAsQuote(getPost, uri) 74 return { 75 type: 'record', 76 record: { 77 - cid: result.cid, 78 - uri: result.uri, 79 }, 80 kind: 'post', 81 - meta: result, 82 } 83 } 84 if (isBskyCustomFeedUrl(uri)) { 85 - // TODO: Remove this abstraction. 86 - const result = await getFeedAsEmbed(agent, fetchDid, uri) 87 return { 88 type: 'record', 89 - record: result.embed!.record, 90 kind: 'other', 91 meta: { 92 // TODO: Include hydrated content instead. 93 - title: result.meta!.title!, 94 }, 95 } 96 } 97 if (isBskyListUrl(uri)) { 98 - // TODO: Remove this abstraction. 99 - const result = await getListAsEmbed(agent, fetchDid, uri) 100 return { 101 type: 'record', 102 - record: result.embed!.record, 103 kind: 'other', 104 meta: { 105 // TODO: Include hydrated content instead. 106 - title: result.meta!.title!, 107 }, 108 } 109 } 110 if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) { 111 - // TODO: Remove this abstraction. 112 - const result = await getStarterPackAsEmbed(agent, fetchDid, uri) 113 return { 114 type: 'record', 115 - record: result.embed!.record, 116 kind: 'other', 117 meta: { 118 // TODO: Include hydrated content instead. 119 - title: result.meta!.title!, 120 }, 121 } 122 }
··· 1 + import { 2 + AppBskyActorDefs, 3 + AppBskyFeedPost, 4 + AppBskyGraphStarterpack, 5 + ComAtprotoRepoStrongRef, 6 + } from '@atproto/api' 7 import {AtUri} from '@atproto/api' 8 import {BskyAgent} from '@atproto/api' 9 10 import {POST_IMG_MAX} from '#/lib/constants' 11 import {getLinkMeta} from '#/lib/link-meta/link-meta' 12 import {resolveShortLink} from '#/lib/link-meta/resolve-short-link' 13 import {downloadAndResize} from '#/lib/media/manip' 14 + import { 15 + createStarterPackUri, 16 + parseStarterPackUri, 17 + } from '#/lib/strings/starter-pack' 18 import { 19 isBskyCustomFeedUrl, 20 isBskyListUrl, ··· 27 import {createComposerImage} from '#/state/gallery' 28 import {Gif} from '#/state/queries/tenor' 29 import {createGIFDescription} from '../gif-alt-text' 30 + import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' 31 32 type ResolvedExternalLink = { 33 type: 'external' ··· 63 | ResolvedExternalLink 64 | ResolvedPostRecord 65 | ResolvedOtherRecord 66 + 67 + class EmbeddingDisabledError extends Error { 68 + constructor() { 69 + super('Embedding is disabled for this record') 70 + } 71 + } 72 73 export async function resolveLink( 74 agent: BskyAgent, ··· 78 uri = await resolveShortLink(uri) 79 } 80 if (isBskyPostUrl(uri)) { 81 + uri = convertBskyAppUrlIfNeeded(uri) 82 + const [_0, user, _1, rkey] = uri.split('/').filter(Boolean) 83 + const recordUri = makeRecordUri(user, 'app.bsky.feed.post', rkey) 84 + const post = await getPost({uri: recordUri}) 85 + if (post.viewer?.embeddingDisabled) { 86 + throw new EmbeddingDisabledError() 87 + } 88 return { 89 type: 'record', 90 record: { 91 + cid: post.cid, 92 + uri: post.uri, 93 }, 94 kind: 'post', 95 + meta: { 96 + text: AppBskyFeedPost.isRecord(post.record) ? post.record.text : '', 97 + indexedAt: post.indexedAt, 98 + author: post.author, 99 + }, 100 } 101 } 102 if (isBskyCustomFeedUrl(uri)) { 103 + uri = convertBskyAppUrlIfNeeded(uri) 104 + const [_0, handleOrDid, _1, rkey] = uri.split('/').filter(Boolean) 105 + const did = await fetchDid(handleOrDid) 106 + const feed = makeRecordUri(did, 'app.bsky.feed.generator', rkey) 107 + const res = await agent.app.bsky.feed.getFeedGenerator({feed}) 108 return { 109 type: 'record', 110 + record: { 111 + uri: res.data.view.uri, 112 + cid: res.data.view.cid, 113 + }, 114 kind: 'other', 115 meta: { 116 // TODO: Include hydrated content instead. 117 + title: res.data.view.displayName, 118 }, 119 } 120 } 121 if (isBskyListUrl(uri)) { 122 + uri = convertBskyAppUrlIfNeeded(uri) 123 + const [_0, handleOrDid, _1, rkey] = uri.split('/').filter(Boolean) 124 + const did = await fetchDid(handleOrDid) 125 + const list = makeRecordUri(did, 'app.bsky.graph.list', rkey) 126 + const res = await agent.app.bsky.graph.getList({list}) 127 return { 128 type: 'record', 129 + record: { 130 + uri: res.data.list.uri, 131 + cid: res.data.list.cid, 132 + }, 133 kind: 'other', 134 meta: { 135 // TODO: Include hydrated content instead. 136 + title: res.data.list.name, 137 }, 138 } 139 } 140 if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) { 141 + const parsed = parseStarterPackUri(uri) 142 + if (!parsed) { 143 + throw new Error( 144 + 'Unexpectedly called getStarterPackAsEmbed with a non-starterpack url', 145 + ) 146 + } 147 + const did = await fetchDid(parsed.name) 148 + const starterPack = createStarterPackUri({did, rkey: parsed.rkey}) 149 + const res = await agent.app.bsky.graph.getStarterPack({starterPack}) 150 + const record = res.data.starterPack.record 151 return { 152 type: 'record', 153 + record: { 154 + uri: res.data.starterPack.uri, 155 + cid: res.data.starterPack.cid, 156 + }, 157 kind: 'other', 158 meta: { 159 // TODO: Include hydrated content instead. 160 + title: AppBskyGraphStarterpack.isRecord(record) 161 + ? record.name 162 + : 'Starter Pack', 163 }, 164 } 165 }
+48 -73
src/view/com/composer/Composer.tsx
··· 42 AppBskyFeedGetPostThread, 43 BskyAgent, 44 } from '@atproto/api' 45 - import {RichText} from '@atproto/api' 46 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 47 import {msg, Trans} from '@lingui/macro' 48 import {useLingui} from '@lingui/react' ··· 57 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 58 import {logEvent} from '#/lib/statsig/statsig' 59 import {cleanError} from '#/lib/strings/errors' 60 - import {insertMentionAt} from '#/lib/strings/mention-manip' 61 import {shortenLinks} from '#/lib/strings/rich-text-manip' 62 import {colors, s} from '#/lib/styles' 63 import {logger} from '#/logger' ··· 73 useLanguagePrefs, 74 useLanguagePrefsApi, 75 } from '#/state/preferences/languages' 76 - import {createPostgateRecord} from '#/state/queries/postgate/util' 77 import {useProfileQuery} from '#/state/queries/profile' 78 import {Gif} from '#/state/queries/tenor' 79 - import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' 80 - import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util' 81 import {useAgent, useSession} from '#/state/session' 82 import {useComposerControls} from '#/state/shell/composer' 83 import {ComposerOpts} from '#/state/shell/composer' ··· 168 const [isProcessing, setIsProcessing] = useState(false) 169 const [processingState, setProcessingState] = useState('') 170 const [error, setError] = useState('') 171 - const [richtext, setRichText] = useState( 172 - new RichText({ 173 - text: initText 174 - ? initText 175 - : initMention 176 - ? insertMentionAt( 177 - `@${initMention}`, 178 - initMention.length + 1, 179 - `${initMention}`, 180 - ) // insert mention if passed in 181 - : '', 182 - }), 183 - ) 184 - const graphemeLength = useMemo(() => { 185 - return shortenLinks(richtext).graphemeLength 186 - }, [richtext]) 187 188 - // TODO: Move more state here. 189 - const [composerState, dispatch] = useReducer( 190 composerReducer, 191 - {initImageUris, initQuoteUri: initQuote?.uri}, 192 createComposerState, 193 ) 194 - 195 let videoState: VideoState | NoVideoState = NO_VIDEO 196 - if (composerState.embed.media?.type === 'video') { 197 - videoState = composerState.embed.media.video 198 } 199 200 const selectVideo = React.useCallback( 201 (asset: ImagePickerAsset) => { ··· 241 ) 242 243 const hasVideo = Boolean(videoState.asset || videoState.video) 244 - 245 const [publishOnUpload, setPublishOnUpload] = useState(false) 246 247 - const [labels, setLabels] = useState<string[]>([]) 248 - const [threadgateAllowUISettings, onChangeThreadgateAllowUISettings] = 249 - useState<ThreadgateAllowUISetting[]>( 250 - threadgateViewToAllowUISetting(undefined), 251 - ) 252 - const [postgate, setPostgate] = useState(createPostgateRecord({post: ''})) 253 - 254 - let quote: string | undefined 255 - if (composerState.embed.quote) { 256 - quote = composerState.embed.quote.uri 257 - } 258 - let images = NO_IMAGES 259 - if (composerState.embed.media?.type === 'images') { 260 - images = composerState.embed.media.images 261 - } 262 - let extGif: Gif | undefined 263 - let extGifAlt: string | undefined 264 - if (composerState.embed.media?.type === 'gif') { 265 - extGif = composerState.embed.media.gif 266 - extGifAlt = composerState.embed.media.alt 267 - } 268 - let extLink: string | undefined 269 - if (composerState.embed.link) { 270 - extLink = composerState.embed.link.uri 271 - } 272 - 273 const onClose = useCallback(() => { 274 closeComposer() 275 }, [closeComposer]) ··· 419 try { 420 postUri = ( 421 await apilib.post(agent, queryClient, { 422 - composerState, // TODO: move more state here. 423 - rawText: richtext.text, 424 replyTo: replyTo?.uri, 425 - labels, 426 - threadgate: threadgateAllowUISettings, 427 - postgate, 428 onStateChange: setProcessingState, 429 langs: toPostLanguages(langPrefs.postLanguage), 430 }) ··· 497 [ 498 _, 499 agent, 500 - composerState, 501 extLink, 502 images, 503 graphemeLength, 504 isAltTextRequiredAndMissing, 505 isProcessing, 506 - labels, 507 langPrefs.postLanguage, 508 onClose, 509 onPost, 510 - postgate, 511 quote, 512 initQuote, 513 initQuoteCount, 514 replyTo, 515 richtext.text, 516 setLangPrefs, 517 - threadgateAllowUISettings, 518 videoState.asset, 519 videoState.status, 520 queryClient, ··· 615 ) : ( 616 <View style={[styles.postBtnWrapper]}> 617 <LabelsBtn 618 - labels={labels} 619 - onChange={setLabels} 620 hasMedia={hasMedia || Boolean(extLink)} 621 /> 622 {canPost ? ( ··· 698 richtext={richtext} 699 placeholder={selectTextInputPlaceholder} 700 autoFocus 701 - setRichText={setRichText} 702 onPhotoPasted={onPhotoPasted} 703 onPressPublish={() => onPressPublish()} 704 onNewLink={onNewLink} ··· 734 </View> 735 )} 736 737 - {!composerState.embed.media && extLink && ( 738 <View style={a.relative} key={extLink}> 739 <ExternalEmbedLink 740 uri={extLink} ··· 815 816 {replyTo ? null : ( 817 <ThreadgateBtn 818 - postgate={postgate} 819 - onChangePostgate={setPostgate} 820 - threadgateAllowUISettings={threadgateAllowUISettings} 821 - onChangeThreadgateAllowUISettings={ 822 - onChangeThreadgateAllowUISettings 823 - } 824 style={bottomBarAnimatedStyle} 825 Portal={Portal.Portal} 826 />
··· 42 AppBskyFeedGetPostThread, 43 BskyAgent, 44 } from '@atproto/api' 45 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 46 import {msg, Trans} from '@lingui/macro' 47 import {useLingui} from '@lingui/react' ··· 56 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 57 import {logEvent} from '#/lib/statsig/statsig' 58 import {cleanError} from '#/lib/strings/errors' 59 import {shortenLinks} from '#/lib/strings/rich-text-manip' 60 import {colors, s} from '#/lib/styles' 61 import {logger} from '#/logger' ··· 71 useLanguagePrefs, 72 useLanguagePrefsApi, 73 } from '#/state/preferences/languages' 74 import {useProfileQuery} from '#/state/queries/profile' 75 import {Gif} from '#/state/queries/tenor' 76 import {useAgent, useSession} from '#/state/session' 77 import {useComposerControls} from '#/state/shell/composer' 78 import {ComposerOpts} from '#/state/shell/composer' ··· 163 const [isProcessing, setIsProcessing] = useState(false) 164 const [processingState, setProcessingState] = useState('') 165 const [error, setError] = useState('') 166 167 + const [draft, dispatch] = useReducer( 168 composerReducer, 169 + {initImageUris, initQuoteUri: initQuote?.uri, initText, initMention}, 170 createComposerState, 171 ) 172 + const richtext = draft.richtext 173 + let quote: string | undefined 174 + if (draft.embed.quote) { 175 + quote = draft.embed.quote.uri 176 + } 177 + let images = NO_IMAGES 178 + if (draft.embed.media?.type === 'images') { 179 + images = draft.embed.media.images 180 + } 181 let videoState: VideoState | NoVideoState = NO_VIDEO 182 + if (draft.embed.media?.type === 'video') { 183 + videoState = draft.embed.media.video 184 + } 185 + let extGif: Gif | undefined 186 + let extGifAlt: string | undefined 187 + if (draft.embed.media?.type === 'gif') { 188 + extGif = draft.embed.media.gif 189 + extGifAlt = draft.embed.media.alt 190 + } 191 + let extLink: string | undefined 192 + if (draft.embed.link) { 193 + extLink = draft.embed.link.uri 194 } 195 + 196 + const graphemeLength = useMemo(() => { 197 + return shortenLinks(richtext).graphemeLength 198 + }, [richtext]) 199 200 const selectVideo = React.useCallback( 201 (asset: ImagePickerAsset) => { ··· 241 ) 242 243 const hasVideo = Boolean(videoState.asset || videoState.video) 244 const [publishOnUpload, setPublishOnUpload] = useState(false) 245 246 const onClose = useCallback(() => { 247 closeComposer() 248 }, [closeComposer]) ··· 392 try { 393 postUri = ( 394 await apilib.post(agent, queryClient, { 395 + draft: draft, 396 replyTo: replyTo?.uri, 397 onStateChange: setProcessingState, 398 langs: toPostLanguages(langPrefs.postLanguage), 399 }) ··· 466 [ 467 _, 468 agent, 469 + draft, 470 extLink, 471 images, 472 graphemeLength, 473 isAltTextRequiredAndMissing, 474 isProcessing, 475 langPrefs.postLanguage, 476 onClose, 477 onPost, 478 quote, 479 initQuote, 480 initQuoteCount, 481 replyTo, 482 richtext.text, 483 setLangPrefs, 484 videoState.asset, 485 videoState.status, 486 queryClient, ··· 581 ) : ( 582 <View style={[styles.postBtnWrapper]}> 583 <LabelsBtn 584 + labels={draft.labels} 585 + onChange={nextLabels => { 586 + dispatch({type: 'update_labels', labels: nextLabels}) 587 + }} 588 hasMedia={hasMedia || Boolean(extLink)} 589 /> 590 {canPost ? ( ··· 666 richtext={richtext} 667 placeholder={selectTextInputPlaceholder} 668 autoFocus 669 + setRichText={rt => { 670 + dispatch({type: 'update_richtext', richtext: rt}) 671 + }} 672 onPhotoPasted={onPhotoPasted} 673 onPressPublish={() => onPressPublish()} 674 onNewLink={onNewLink} ··· 704 </View> 705 )} 706 707 + {!draft.embed.media && extLink && ( 708 <View style={a.relative} key={extLink}> 709 <ExternalEmbedLink 710 uri={extLink} ··· 785 786 {replyTo ? null : ( 787 <ThreadgateBtn 788 + postgate={draft.postgate} 789 + onChangePostgate={nextPostgate => { 790 + dispatch({type: 'update_postgate', postgate: nextPostgate}) 791 + }} 792 + threadgateAllowUISettings={draft.threadgate} 793 + onChangeThreadgateAllowUISettings={nextThreadgate => { 794 + dispatch({ 795 + type: 'update_threadgate', 796 + threadgate: nextThreadgate, 797 + }) 798 + }} 799 style={bottomBarAnimatedStyle} 800 Portal={Portal.Portal} 801 />
+60 -5
src/view/com/composer/state/composer.ts
··· 1 import {ImagePickerAsset} from 'expo-image-picker' 2 3 import { 4 isBskyPostUrl, 5 postUriToRelativePath, 6 toBskyAppUrl, 7 } from '#/lib/strings/url-helpers' 8 import {ComposerImage, createInitialImages} from '#/state/gallery' 9 import {Gif} from '#/state/queries/tenor' 10 import {ComposerOpts} from '#/state/shell/composer' 11 import {createVideoState, VideoAction, videoReducer, VideoState} from './video' 12 ··· 41 link: Link | undefined 42 } 43 44 - export type ComposerState = { 45 - // TODO: Other draft data. 46 embed: EmbedDraft 47 } 48 49 export type ComposerAction = 50 | {type: 'embed_add_images'; images: ComposerImage[]} 51 | {type: 'embed_update_image'; image: ComposerImage} 52 | {type: 'embed_remove_image'; image: ComposerImage} ··· 67 export const MAX_IMAGES = 4 68 69 export function composerReducer( 70 - state: ComposerState, 71 action: ComposerAction, 72 - ): ComposerState { 73 switch (action.type) { 74 case 'embed_add_images': { 75 if (action.images.length === 0) { 76 return state ··· 293 } 294 295 export function createComposerState({ 296 initImageUris, 297 initQuoteUri, 298 }: { 299 initImageUris: ComposerOpts['imageUris'] 300 initQuoteUri: string | undefined 301 - }): ComposerState { 302 let media: ImagesMedia | undefined 303 if (initImageUris?.length) { 304 media = { ··· 317 } 318 } 319 } 320 return { 321 embed: { 322 quote, 323 media,
··· 1 import {ImagePickerAsset} from 'expo-image-picker' 2 + import {AppBskyFeedPostgate, RichText} from '@atproto/api' 3 4 + import {insertMentionAt} from '#/lib/strings/mention-manip' 5 import { 6 isBskyPostUrl, 7 postUriToRelativePath, 8 toBskyAppUrl, 9 } from '#/lib/strings/url-helpers' 10 import {ComposerImage, createInitialImages} from '#/state/gallery' 11 + import {createPostgateRecord} from '#/state/queries/postgate/util' 12 import {Gif} from '#/state/queries/tenor' 13 + import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate' 14 + import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' 15 import {ComposerOpts} from '#/state/shell/composer' 16 import {createVideoState, VideoAction, videoReducer, VideoState} from './video' 17 ··· 46 link: Link | undefined 47 } 48 49 + export type ComposerDraft = { 50 + richtext: RichText 51 + labels: string[] 52 + postgate: AppBskyFeedPostgate.Record 53 + threadgate: ThreadgateAllowUISetting[] 54 embed: EmbedDraft 55 } 56 57 export type ComposerAction = 58 + | {type: 'update_richtext'; richtext: RichText} 59 + | {type: 'update_labels'; labels: string[]} 60 + | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} 61 + | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} 62 | {type: 'embed_add_images'; images: ComposerImage[]} 63 | {type: 'embed_update_image'; image: ComposerImage} 64 | {type: 'embed_remove_image'; image: ComposerImage} ··· 79 export const MAX_IMAGES = 4 80 81 export function composerReducer( 82 + state: ComposerDraft, 83 action: ComposerAction, 84 + ): ComposerDraft { 85 switch (action.type) { 86 + case 'update_richtext': { 87 + return { 88 + ...state, 89 + richtext: action.richtext, 90 + } 91 + } 92 + case 'update_labels': { 93 + return { 94 + ...state, 95 + labels: action.labels, 96 + } 97 + } 98 + case 'update_postgate': { 99 + return { 100 + ...state, 101 + postgate: action.postgate, 102 + } 103 + } 104 + case 'update_threadgate': { 105 + return { 106 + ...state, 107 + threadgate: action.threadgate, 108 + } 109 + } 110 case 'embed_add_images': { 111 if (action.images.length === 0) { 112 return state ··· 329 } 330 331 export function createComposerState({ 332 + initText, 333 + initMention, 334 initImageUris, 335 initQuoteUri, 336 }: { 337 + initText: string | undefined 338 + initMention: string | undefined 339 initImageUris: ComposerOpts['imageUris'] 340 initQuoteUri: string | undefined 341 + }): ComposerDraft { 342 let media: ImagesMedia | undefined 343 if (initImageUris?.length) { 344 media = { ··· 357 } 358 } 359 } 360 + const initRichText = new RichText({ 361 + text: initText 362 + ? initText 363 + : initMention 364 + ? insertMentionAt( 365 + `@${initMention}`, 366 + initMention.length + 1, 367 + `${initMention}`, 368 + ) 369 + : '', 370 + }) 371 return { 372 + richtext: initRichText, 373 + labels: [], 374 + postgate: createPostgateRecord({post: ''}), 375 + threadgate: threadgateViewToAllowUISetting(undefined), 376 embed: { 377 quote, 378 media,
+1 -1
src/view/com/composer/text-input/TextInput.tsx
··· 43 interface TextInputProps extends ComponentProps<typeof RNTextInput> { 44 richtext: RichText 45 placeholder: string 46 - setRichText: (v: RichText | ((v: RichText) => RichText)) => void 47 onPhotoPasted: (uri: string) => void 48 onPressPublish: (richtext: RichText) => Promise<void> 49 onNewLink: (uri: string) => void
··· 43 interface TextInputProps extends ComponentProps<typeof RNTextInput> { 44 richtext: RichText 45 placeholder: string 46 + setRichText: (v: RichText) => void 47 onPhotoPasted: (uri: string) => void 48 onPressPublish: (richtext: RichText) => Promise<void> 49 onNewLink: (uri: string) => void