diff --git a/src/App.native.tsx b/src/App.native.tsx
index c6334379f7..96b493af41 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -51,6 +51,7 @@ import {
} from '#/state/session'
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
+import {Provider as ComposerProvider} from '#/state/shell/composer'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
@@ -125,35 +126,37 @@ function InnerApp() {
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 1664812d08..0d500908fd 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -41,6 +41,7 @@ import {
} from '#/state/session'
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
+import {Provider as ComposerProvider} from '#/state/shell/composer'
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
@@ -116,33 +117,35 @@ function InnerApp() {
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index e6e8eea3da..6edb111e6b 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -12,12 +12,17 @@ import {
ComAtprotoRepoStrongRef,
RichText,
} from '@atproto/api'
+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 {writePostgateRecord} from '#/state/queries/postgate'
+import {
+ fetchResolveGifQuery,
+ fetchResolveLinkQuery,
+} from '#/state/queries/resolve-link'
import {
createThreadgateRecord,
ThreadgateAllowUISetting,
@@ -27,7 +32,6 @@ import {
import {ComposerState, EmbedDraft} from '#/view/com/composer/state/composer'
import {createGIFDescription} from '../gif-alt-text'
import {LinkMeta} from '../link-meta/link-meta'
-import {resolveGif, resolveLink} from './resolve'
import {uploadBlob} from './upload-blob'
export {uploadBlob}
@@ -51,7 +55,11 @@ interface PostOpts {
langs?: string[]
}
-export async function post(agent: BskyAgent, opts: PostOpts) {
+export async function post(
+ agent: BskyAgent,
+ queryClient: QueryClient,
+ opts: PostOpts,
+) {
let reply
let rt = new RichText({text: opts.rawText.trimEnd()}, {cleanNewlines: true})
@@ -64,6 +72,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
const embed = await resolveEmbed(
agent,
+ queryClient,
opts.composerState,
opts.onStateChange,
)
@@ -178,6 +187,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
async function resolveEmbed(
agent: BskyAgent,
+ queryClient: QueryClient,
draft: ComposerState,
onStateChange: ((state: string) => void) | undefined,
): Promise<
@@ -190,8 +200,8 @@ async function resolveEmbed(
> {
if (draft.embed.quote) {
const [resolvedMedia, resolvedQuote] = await Promise.all([
- resolveMedia(agent, draft.embed, onStateChange),
- resolveRecord(agent, draft.embed.quote.uri),
+ resolveMedia(agent, queryClient, draft.embed, onStateChange),
+ resolveRecord(agent, queryClient, draft.embed.quote.uri),
])
if (resolvedMedia) {
return {
@@ -208,12 +218,21 @@ async function resolveEmbed(
record: resolvedQuote,
}
}
- const resolvedMedia = await resolveMedia(agent, draft.embed, onStateChange)
+ const resolvedMedia = await resolveMedia(
+ agent,
+ queryClient,
+ draft.embed,
+ onStateChange,
+ )
if (resolvedMedia) {
return resolvedMedia
}
if (draft.embed.link) {
- const resolvedLink = await resolveLink(agent, draft.embed.link.uri)
+ const resolvedLink = await fetchResolveLinkQuery(
+ queryClient,
+ agent,
+ draft.embed.link.uri,
+ )
if (resolvedLink.type === 'record') {
return {
$type: 'app.bsky.embed.record',
@@ -226,6 +245,7 @@ async function resolveEmbed(
async function resolveMedia(
agent: BskyAgent,
+ queryClient: QueryClient,
embedDraft: EmbedDraft,
onStateChange: ((state: string) => void) | undefined,
): Promise<
@@ -286,7 +306,11 @@ async function resolveMedia(
}
if (embedDraft.media?.type === 'gif') {
const gifDraft = embedDraft.media
- const resolvedGif = await resolveGif(agent, gifDraft.gif)
+ const resolvedGif = await fetchResolveGifQuery(
+ queryClient,
+ agent,
+ gifDraft.gif,
+ )
let blob: BlobRef | undefined
if (resolvedGif.thumb) {
onStateChange?.('Uploading link thumbnail...')
@@ -305,7 +329,11 @@ async function resolveMedia(
}
}
if (embedDraft.link) {
- const resolvedLink = await resolveLink(agent, embedDraft.link.uri)
+ const resolvedLink = await fetchResolveLinkQuery(
+ queryClient,
+ agent,
+ embedDraft.link.uri,
+ )
if (resolvedLink.type === 'external') {
let blob: BlobRef | undefined
if (resolvedLink.thumb) {
@@ -330,9 +358,10 @@ async function resolveMedia(
async function resolveRecord(
agent: BskyAgent,
+ queryClient: QueryClient,
uri: string,
): Promise {
- const resolvedLink = await resolveLink(agent, uri)
+ const resolvedLink = await fetchResolveLinkQuery(queryClient, agent, uri)
if (resolvedLink.type !== 'record') {
throw Error('Expected uri to resolve to a record')
}
diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts
index a97a3f31c4..4f409e100f 100644
--- a/src/lib/api/resolve.ts
+++ b/src/lib/api/resolve.ts
@@ -1,4 +1,4 @@
-import {ComAtprotoRepoStrongRef} from '@atproto/api'
+import {AppBskyActorDefs, ComAtprotoRepoStrongRef} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {BskyAgent} from '@atproto/api'
@@ -33,12 +33,32 @@ type ResolvedExternalLink = {
thumb: ComposerImage | undefined
}
-type ResolvedRecord = {
+type ResolvedPostRecord = {
type: 'record'
record: ComAtprotoRepoStrongRef.Main
+ kind: 'post'
+ meta: {
+ text: string
+ indexedAt: string
+ author: AppBskyActorDefs.ProfileViewBasic
+ }
}
-type ResolvedLink = ResolvedExternalLink | ResolvedRecord
+type ResolvedOtherRecord = {
+ type: 'record'
+ record: ComAtprotoRepoStrongRef.Main
+ kind: 'other'
+ meta: {
+ // We should replace this with a hydrated record (e.g. feed, list, starter pack)
+ // and change the composer preview to use the actual post embed components:
+ title: string
+ }
+}
+
+export type ResolvedLink =
+ | ResolvedExternalLink
+ | ResolvedPostRecord
+ | ResolvedOtherRecord
export async function resolveLink(
agent: BskyAgent,
@@ -57,6 +77,8 @@ export async function resolveLink(
cid: result.cid,
uri: result.uri,
},
+ kind: 'post',
+ meta: result,
}
}
if (isBskyCustomFeedUrl(uri)) {
@@ -64,7 +86,12 @@ export async function resolveLink(
const result = await getFeedAsEmbed(agent, fetchDid, uri)
return {
type: 'record',
- record: result.embed!.record, // TODO: Fix types.
+ record: result.embed!.record,
+ kind: 'other',
+ meta: {
+ // TODO: Include hydrated content instead.
+ title: result.meta!.title!,
+ },
}
}
if (isBskyListUrl(uri)) {
@@ -72,7 +99,12 @@ export async function resolveLink(
const result = await getListAsEmbed(agent, fetchDid, uri)
return {
type: 'record',
- record: result.embed!.record, // TODO: Fix types.
+ record: result.embed!.record,
+ kind: 'other',
+ meta: {
+ // TODO: Include hydrated content instead.
+ title: result.meta!.title!,
+ },
}
}
if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) {
@@ -80,7 +112,12 @@ export async function resolveLink(
const result = await getStarterPackAsEmbed(agent, fetchDid, uri)
return {
type: 'record',
- record: result.embed!.record, // TODO: Fix types.
+ record: result.embed!.record,
+ kind: 'other',
+ meta: {
+ // TODO: Include hydrated content instead.
+ title: result.meta!.title!,
+ },
}
}
return resolveExternal(agent, uri)
diff --git a/src/state/queries/resolve-link.ts b/src/state/queries/resolve-link.ts
new file mode 100644
index 0000000000..5856cfb5f7
--- /dev/null
+++ b/src/state/queries/resolve-link.ts
@@ -0,0 +1,70 @@
+import {QueryClient, useQuery} from '@tanstack/react-query'
+
+import {STALE} from '#/state/queries/index'
+import {useAgent} from '../session'
+
+const RQKEY_LINK_ROOT = 'resolve-link'
+export const RQKEY_LINK = (url: string) => [RQKEY_LINK_ROOT, url]
+
+const RQKEY_GIF_ROOT = 'resolve-gif'
+export const RQKEY_GIF = (url: string) => [RQKEY_GIF_ROOT, url]
+
+import {BskyAgent} from '@atproto/api'
+
+import {ResolvedLink, resolveGif, resolveLink} from '#/lib/api/resolve'
+import {Gif} from './tenor'
+
+export function useResolveLinkQuery(url: string) {
+ const agent = useAgent()
+ return useQuery({
+ staleTime: STALE.HOURS.ONE,
+ queryKey: RQKEY_LINK(url),
+ queryFn: async () => {
+ return await resolveLink(agent, url)
+ },
+ })
+}
+export function fetchResolveLinkQuery(
+ queryClient: QueryClient,
+ agent: BskyAgent,
+ url: string,
+) {
+ return queryClient.fetchQuery({
+ staleTime: STALE.HOURS.ONE,
+ queryKey: RQKEY_LINK(url),
+ queryFn: async () => {
+ return await resolveLink(agent, url)
+ },
+ })
+}
+export function precacheResolveLinkQuery(
+ queryClient: QueryClient,
+ url: string,
+ resolvedLink: ResolvedLink,
+) {
+ queryClient.setQueryData(RQKEY_LINK(url), resolvedLink)
+}
+
+export function useResolveGifQuery(gif: Gif) {
+ const agent = useAgent()
+ return useQuery({
+ staleTime: STALE.HOURS.ONE,
+ queryKey: RQKEY_GIF(gif.url),
+ queryFn: async () => {
+ return await resolveGif(agent, gif)
+ },
+ })
+}
+export function fetchResolveGifQuery(
+ queryClient: QueryClient,
+ agent: BskyAgent,
+ gif: Gif,
+) {
+ return queryClient.fetchQuery({
+ staleTime: STALE.HOURS.ONE,
+ queryKey: RQKEY_GIF(gif.url),
+ queryFn: async () => {
+ return await resolveGif(agent, gif)
+ },
+ })
+}
diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx
index 770b0789ed..0969485064 100644
--- a/src/state/shell/composer/index.tsx
+++ b/src/state/shell/composer/index.tsx
@@ -7,9 +7,12 @@ import {
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {postUriToRelativePath, toBskyAppUrl} from '#/lib/strings/url-helpers'
import {purgeTemporaryImageFiles} from '#/state/gallery'
+import {precacheResolveLinkQuery} from '#/state/queries/resolve-link'
import * as Toast from '#/view/com/util/Toast'
export interface ComposerOptsPostRef {
@@ -58,8 +61,28 @@ const controlsContext = React.createContext({
export function Provider({children}: React.PropsWithChildren<{}>) {
const {_} = useLingui()
const [state, setState] = React.useState()
+ const queryClient = useQueryClient()
const openComposer = useNonReactiveCallback((opts: ComposerOpts) => {
+ if (opts.quote) {
+ const path = postUriToRelativePath(opts.quote.uri)
+ if (path) {
+ const appUrl = toBskyAppUrl(path)
+ precacheResolveLinkQuery(queryClient, appUrl, {
+ type: 'record',
+ kind: 'post',
+ record: {
+ cid: opts.quote.cid,
+ uri: opts.quote.uri,
+ },
+ meta: {
+ author: opts.quote.author,
+ indexedAt: opts.quote.indexedAt,
+ text: opts.quote.text,
+ },
+ })
+ }
+ }
const author = opts.replyTo?.author || opts.quote?.author
const isBlocked = Boolean(
author &&
diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx
index 07909c0001..f61dc3c410 100644
--- a/src/state/shell/index.tsx
+++ b/src/state/shell/index.tsx
@@ -1,22 +1,22 @@
import React from 'react'
-import {Provider as ShellLayoutProvder} from './shell-layout'
+
+import {Provider as ColorModeProvider} from './color-mode'
import {Provider as DrawerOpenProvider} from './drawer-open'
import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled'
import {Provider as MinimalModeProvider} from './minimal-mode'
-import {Provider as ColorModeProvider} from './color-mode'
import {Provider as OnboardingProvider} from './onboarding'
-import {Provider as ComposerProvider} from './composer'
+import {Provider as ShellLayoutProvder} from './shell-layout'
import {Provider as TickEveryMinuteProvider} from './tick-every-minute'
+export {useSetThemePrefs, useThemePrefs} from './color-mode'
+export {useComposerControls, useComposerState} from './composer'
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
export {
useIsDrawerSwipeDisabled,
useSetDrawerSwipeDisabled,
} from './drawer-swipe-disabled'
export {useMinimalShellMode, useSetMinimalShellMode} from './minimal-mode'
-export {useThemePrefs, useSetThemePrefs} from './color-mode'
-export {useOnboardingState, useOnboardingDispatch} from './onboarding'
-export {useComposerState, useComposerControls} from './composer'
+export {useOnboardingDispatch, useOnboardingState} from './onboarding'
export {useTickEveryMinute} from './tick-every-minute'
export function Provider({children}: React.PropsWithChildren<{}>) {
@@ -27,9 +27,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
-
- {children}
-
+ {children}
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index a1c4e7656e..ecafea500d 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -46,19 +46,15 @@ import {RichText} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
import * as apilib from '#/lib/api/index'
import {until} from '#/lib/async/until'
import {MAX_GRAPHEME_LENGTH} from '#/lib/constants'
-import {
- createGIFDescription,
- parseAltFromGIFDescription,
-} from '#/lib/gif-alt-text'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {LikelyType} from '#/lib/link-meta/link-meta'
import {logEvent} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
import {insertMentionAt} from '#/lib/strings/mention-manip'
@@ -87,8 +83,11 @@ import {useComposerControls} from '#/state/shell/composer'
import {ComposerOpts} from '#/state/shell/composer'
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import {ComposerReplyTo} from '#/view/com/composer/ComposerReplyTo'
-import {ExternalEmbed} from '#/view/com/composer/ExternalEmbed'
-import {GifAltText} from '#/view/com/composer/GifAltText'
+import {
+ ExternalEmbedGif,
+ ExternalEmbedLink,
+} from '#/view/com/composer/ExternalEmbed'
+import {GifAltTextDialog} from '#/view/com/composer/GifAltText'
import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
import {Gallery} from '#/view/com/composer/photos/Gallery'
import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn'
@@ -100,12 +99,11 @@ import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLa
// due to linting false positives
import {TextInput, TextInputRef} from '#/view/com/composer/text-input/TextInput'
import {ThreadgateBtn} from '#/view/com/composer/threadgate/ThreadgateBtn'
-import {useExternalLinkFetch} from '#/view/com/composer/useExternalLinkFetch'
import {SelectVideoBtn} from '#/view/com/composer/videos/SelectVideoBtn'
import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress'
-import {QuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed'
+import {LazyQuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed'
import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
@@ -117,13 +115,15 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {createPortalGroup} from '#/components/Portal'
import * as Prompt from '#/components/Prompt'
import {Text as NewText} from '#/components/Typography'
-import {composerReducer, createComposerState} from './state/composer'
+import {
+ composerReducer,
+ createComposerState,
+ MAX_IMAGES,
+} from './state/composer'
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
const Portal = createPortalGroup()
-const MAX_IMAGES = 4
-
type CancelRef = {
onPressCancel: () => void
}
@@ -135,7 +135,7 @@ export const ComposePost = ({
replyTo,
onPost,
quote: initQuote,
- quoteCount,
+ quoteCount: initQuoteCount,
mention: initMention,
openEmojiPicker,
text: initText,
@@ -147,6 +147,7 @@ export const ComposePost = ({
}) => {
const {currentAccount} = useSession()
const agent = useAgent()
+ const queryClient = useQueryClient()
const currentDid = currentAccount!.did
const {data: currentProfile} = useProfileQuery({did: currentDid})
const {isModalActive} = useModals()
@@ -183,9 +184,6 @@ export const ComposePost = ({
const graphemeLength = useMemo(() => {
return shortenLinks(richtext).graphemeLength
}, [richtext])
- const [quote, setQuote] = useState(
- initQuote,
- )
// TODO: Move more state here.
const [composerState, dispatch] = useReducer(
@@ -246,8 +244,6 @@ export const ComposePost = ({
const [publishOnUpload, setPublishOnUpload] = useState(false)
- const {extLink, setExtLink} = useExternalLinkFetch({setQuote, setError})
- const [extGif, setExtGif] = useState()
const [labels, setLabels] = useState([])
const [threadgateAllowUISettings, onChangeThreadgateAllowUISettings] =
useState(
@@ -255,10 +251,24 @@ export const ComposePost = ({
)
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()
@@ -335,14 +345,9 @@ export const ComposePost = ({
}
}, [onEscape, isModalActive])
- const onNewLink = useCallback(
- (uri: string) => {
- dispatch({type: 'embed_add_uri', uri})
- if (extLink != null) return
- setExtLink({uri, isLoading: true})
- },
- [extLink, setExtLink],
- )
+ const onNewLink = useCallback((uri: string) => {
+ dispatch({type: 'embed_add_uri', uri})
+ }, [])
const onImageAdd = useCallback(
(next: ComposerImage[]) => {
@@ -371,14 +376,10 @@ export const ComposePost = ({
if (images.some(img => img.alt === '')) return true
- if (extGif) {
- if (!extLink?.meta?.description) return true
+ if (extGif && !extGifAlt) return true
- const parsedAlt = parseAltFromGIFDescription(extLink.meta.description)
- if (!parsedAlt.isPreferred) return true
- }
return false
- }, [images, extLink, extGif, requireAltTextEnabled])
+ }, [images, extGifAlt, extGif, requireAltTextEnabled])
const onPressPublish = React.useCallback(
async (finishedUploading?: boolean) => {
@@ -411,17 +412,13 @@ export const ComposePost = ({
setError(_(msg`Did you want to say anything?`))
return
}
- if (extLink?.isLoading) {
- setError(_(msg`Please wait for your link card to finish loading`))
- return
- }
setIsProcessing(true)
let postUri
try {
postUri = (
- await apilib.post(agent, {
+ await apilib.post(agent, queryClient, {
composerState, // TODO: move more state here.
rawText: richtext.text,
replyTo: replyTo?.uri,
@@ -449,13 +446,6 @@ export const ComposePost = ({
hasImages: images.length > 0,
})
- if (extLink) {
- setExtLink({
- ...extLink,
- isLoading: true,
- localThumb: undefined,
- } as apilib.ExternalEmbedDraft)
- }
let err = cleanError(e.message)
if (err.includes('not locate record')) {
err = _(
@@ -481,13 +471,13 @@ export const ComposePost = ({
emitPostCreated()
}
setLangPrefs.savePostLanguageToHistory()
- if (quote) {
+ if (initQuote && initQuoteCount !== undefined) {
// We want to wait for the quote count to update before we call `onPost`, which will refetch data
- whenAppViewReady(agent, quote.uri, res => {
+ whenAppViewReady(agent, initQuote.uri, res => {
const thread = res.data.thread
if (
AppBskyFeedDefs.isThreadViewPost(thread) &&
- thread.post.quoteCount !== quoteCount
+ thread.post.quoteCount !== initQuoteCount
) {
onPost?.(postUri)
return true
@@ -519,14 +509,15 @@ export const ComposePost = ({
onPost,
postgate,
quote,
- quoteCount,
+ initQuote,
+ initQuoteCount,
replyTo,
richtext.text,
- setExtLink,
setLangPrefs,
threadgateAllowUISettings,
videoState.asset,
videoState.status,
+ queryClient,
],
)
@@ -549,11 +540,9 @@ export const ComposePost = ({
const canSelectImages =
images.length < MAX_IMAGES &&
- !extLink &&
videoState.status === 'idle' &&
!videoState.video
- const hasMedia =
- images.length > 0 || Boolean(extLink) || Boolean(videoState.video)
+ const hasMedia = images.length > 0 || Boolean(videoState.video)
const onEmojiButtonPress = useCallback(() => {
openEmojiPicker?.(textInput.current?.getCursorPosition())
@@ -563,45 +552,13 @@ export const ComposePost = ({
textInput.current?.focus()
}, [])
- const onSelectGif = useCallback(
- (gif: Gif) => {
- dispatch({type: 'embed_add_gif', gif})
- setExtLink({
- uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`,
- isLoading: true,
- meta: {
- url: gif.media_formats.gif.url,
- image: gif.media_formats.preview.url,
- likelyType: LikelyType.HTML,
- title: gif.content_description,
- description: createGIFDescription(gif.content_description),
- },
- })
- setExtGif(gif)
- },
- [setExtLink],
- )
+ const onSelectGif = useCallback((gif: Gif) => {
+ dispatch({type: 'embed_add_gif', gif})
+ }, [])
- const handleChangeGifAltText = useCallback(
- (altText: string) => {
- dispatch({type: 'embed_update_gif', alt: altText})
- setExtLink(ext =>
- ext && ext.meta
- ? {
- ...ext,
- meta: {
- ...ext.meta,
- description: createGIFDescription(
- ext.meta.title ?? '',
- altText,
- ),
- },
- }
- : ext,
- )
- },
- [setExtLink],
- )
+ const handleChangeGifAltText = useCallback((altText: string) => {
+ dispatch({type: 'embed_update_gif', alt: altText})
+ }, [])
const {
scrollHandler,
@@ -660,7 +617,7 @@ export const ComposePost = ({
{canPost ? (
- {images.length === 0 && extLink && (
-
-
+ {
- if (extGif) {
- dispatch({type: 'embed_remove_gif'})
- } else {
- dispatch({type: 'embed_remove_link'})
- }
- setExtLink(undefined)
- setExtGif(undefined)
+ dispatch({type: 'embed_remove_gif'})
}}
/>
-
)}
+
+ {!composerState.embed.media && extLink && (
+
+ {
+ dispatch({type: 'embed_remove_link'})
+ }}
+ />
+
+ )}
+
{hasVideo && (
-
+
- {quote.uri !== initQuote?.uri && (
+ {!initQuote && (
{
dispatch({type: 'embed_remove_quote'})
- setQuote(undefined)
}}
/>
)}
diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx
index f61d410dfc..d7dc32f14e 100644
--- a/src/view/com/composer/ExternalEmbed.tsx
+++ b/src/view/com/composer/ExternalEmbed.tsx
@@ -1,71 +1,112 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
-import {ExternalEmbedDraft} from 'lib/api/index'
-import {Gif} from 'state/queries/tenor'
-import {ExternalEmbedRemoveBtn} from 'view/com/composer/ExternalEmbedRemoveBtn'
-import {ExternalLinkEmbed} from 'view/com/util/post-embeds/ExternalLinkEmbed'
+import {cleanError} from '#/lib/strings/errors'
+import {
+ useResolveGifQuery,
+ useResolveLinkQuery,
+} from '#/state/queries/resolve-link'
+import {Gif} from '#/state/queries/tenor'
+import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
+import {ExternalLinkEmbed} from '#/view/com/util/post-embeds/ExternalLinkEmbed'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
-export const ExternalEmbed = ({
- link,
+export const ExternalEmbedGif = ({
onRemove,
gif,
}: {
- link?: ExternalEmbedDraft
onRemove: () => void
- gif?: Gif
+ gif: Gif
}) => {
const t = useTheme()
-
+ const {data, error} = useResolveGifQuery(gif)
const linkInfo = React.useMemo(
() =>
- link && {
- title: link.meta?.title ?? link.uri,
- uri: link.uri,
- description: link.meta?.description ?? '',
- thumb: link.localThumb?.source.path,
+ data && {
+ title: data.title ?? data.uri,
+ uri: data.uri,
+ description: data.description ?? '',
+ thumb: data.thumb?.source.path,
},
- [link],
+ [data],
)
- if (!link) return null
-
- const loadingStyle: ViewStyle | undefined = gif
- ? {
- aspectRatio:
- gif.media_formats.gif.dims[0] / gif.media_formats.gif.dims[1],
- width: '100%',
- }
- : undefined
+ const loadingStyle: ViewStyle = {
+ aspectRatio: gif.media_formats.gif.dims[0] / gif.media_formats.gif.dims[1],
+ width: '100%',
+ }
return (
-
- {link.isLoading ? (
+
+ {linkInfo ? (
+
+
+
+ ) : error ? (
+
+
+ {gif.url}
+
+
+ {cleanError(error)}
+
+
+ ) : (
- ) : link.meta?.error ? (
+ )}
+
+
+ )
+}
+
+export const ExternalEmbedLink = ({
+ uri,
+ onRemove,
+}: {
+ uri: string
+ onRemove: () => void
+}) => {
+ const t = useTheme()
+ const {data, error} = useResolveLinkQuery(uri)
+ const linkInfo = React.useMemo(
+ () =>
+ data && {
+ title:
+ data.type === 'external'
+ ? data.title
+ : data.kind === 'other'
+ ? data.meta.title
+ : uri,
+ uri,
+ description: data.type === 'external' ? data.description : '',
+ thumb: data.type === 'external' ? data.thumb?.source.path : undefined,
+ },
+ [data, uri],
+ )
+ return (
+
+ {linkInfo ? (
+
+
+
+ ) : error ? (
- {link.uri}
+ {uri}
- {link.meta?.error}
+ {cleanError(error)}
- ) : linkInfo ? (
-
-
-
- ) : null}
+ ) : (
+
+
+
+ )}
)
diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx
index 90d20d94f7..01778c3817 100644
--- a/src/view/com/composer/GifAltText.tsx
+++ b/src/view/com/composer/GifAltText.tsx
@@ -1,10 +1,8 @@
import React, {useState} from 'react'
import {TouchableOpacity, View} from 'react-native'
-import {AppBskyEmbedExternal} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {ExternalEmbedDraft} from '#/lib/api'
import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants'
import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
import {
@@ -12,6 +10,7 @@ import {
parseEmbedPlayerFromUrl,
} from '#/lib/strings/embed-player'
import {isAndroid} from '#/platform/detection'
+import {useResolveGifQuery} from '#/state/queries/resolve-link'
import {Gif} from '#/state/queries/tenor'
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
import {atoms as a, native, useTheme} from '#/alf'
@@ -27,38 +26,54 @@ import {Text} from '#/components/Typography'
import {GifEmbed} from '../util/post-embeds/GifEmbed'
import {AltTextReminder} from './photos/Gallery'
-export function GifAltText({
- link: linkProp,
+export function GifAltTextDialog({
gif,
+ altText,
onSubmit,
Portal,
}: {
- link: ExternalEmbedDraft
- gif?: Gif
+ gif: Gif
+ altText: string
onSubmit: (alt: string) => void
Portal: PortalComponent
+}) {
+ const {data} = useResolveGifQuery(gif)
+ const vendorAltText = parseAltFromGIFDescription(data?.description ?? '').alt
+ const params = data ? parseEmbedPlayerFromUrl(data.uri) : undefined
+ if (!data || !params) {
+ return null
+ }
+ return (
+
+ )
+}
+
+export function GifAltTextDialogLoaded({
+ vendorAltText,
+ altText,
+ onSubmit,
+ params,
+ thumb,
+ Portal,
+}: {
+ vendorAltText: string
+ altText: string
+ onSubmit: (alt: string) => void
+ params: EmbedPlayerParams
+ thumb: string | undefined
+ Portal: PortalComponent
}) {
const control = Dialog.useDialogControl()
const {_} = useLingui()
const t = useTheme()
-
- const {link, params} = React.useMemo(() => {
- return {
- link: {
- title: linkProp.meta?.title ?? linkProp.uri,
- uri: linkProp.uri,
- description: linkProp.meta?.description ?? '',
- thumb: linkProp.localThumb?.source.path,
- },
- params: parseEmbedPlayerFromUrl(linkProp.uri),
- }
- }, [linkProp])
-
- const parsedAlt = parseAltFromGIFDescription(link.description)
- const [altText, setAltText] = useState(parsedAlt.alt)
-
- if (!gif || !params) return null
-
+ const [altTextDraft, setAltTextDraft] = useState(altText || vendorAltText)
return (
<>
- {parsedAlt.isPreferred ? (
+ {altText ? (
) : (
@@ -97,17 +112,17 @@ export function GifAltText({
{
- onSubmit(altText)
+ onSubmit(altTextDraft)
}}
Portal={Portal}>
>
@@ -115,17 +130,19 @@ export function GifAltText({
}
function AltTextInner({
+ vendorAltText,
altText,
- setAltText,
+ onChange,
control,
- link,
params,
+ thumb,
}: {
+ vendorAltText: string
altText: string
- setAltText: (text: string) => void
+ onChange: (text: string) => void
control: DialogControlProps
- link: AppBskyEmbedExternal.ViewExternal
params: EmbedPlayerParams
+ thumb: string | undefined
}) {
const t = useTheme()
const {_, i18n} = useLingui()
@@ -142,10 +159,8 @@ function AltTextInner({
{
- setAltText(text)
- }}
+ placeholder={vendorAltText}
+ onChangeText={onChange}
defaultValue={altText}
multiline
numberOfLines={3}
@@ -200,7 +215,9 @@ function AltTextInner({
void
-}) {
- const agent = useAgent()
- const [extLink, setExtLink] = useState(
- undefined,
- )
-
- useEffect(() => {
- let aborted = false
- const cleanup = () => {
- aborted = true
- }
- if (!extLink) {
- return cleanup
- }
- if (!extLink.meta) {
- getLinkMeta(agent, extLink.uri).then(meta => {
- if (aborted) {
- return
- }
- setExtLink({
- uri: extLink.uri,
- isLoading: !!meta.image,
- meta,
- })
- })
- return cleanup
- }
- if (extLink.isLoading) {
- setExtLink({
- ...extLink,
- isLoading: false, // done
- })
- }
- return cleanup
- }, [extLink, agent])
-
- return {extLink, setExtLink}
-}
diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts
deleted file mode 100644
index 60afadefea..0000000000
--- a/src/view/com/composer/useExternalLinkFetch.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import {useEffect, useState} from 'react'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import * as apilib from '#/lib/api/index'
-import {POST_IMG_MAX} from '#/lib/constants'
-import {
- EmbeddingDisabledError,
- 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 {
- isBskyCustomFeedUrl,
- isBskyListUrl,
- isBskyPostUrl,
- isBskyStarterPackUrl,
- isBskyStartUrl,
- isShortLink,
-} from '#/lib/strings/url-helpers'
-import {logger} from '#/logger'
-import {createComposerImage} from '#/state/gallery'
-import {useFetchDid} from '#/state/queries/handle'
-import {useGetPost} from '#/state/queries/post'
-import {useAgent} from '#/state/session'
-import {ComposerOpts} from '#/state/shell/composer'
-
-export function useExternalLinkFetch({
- setQuote,
- setError,
-}: {
- setQuote: (opts: ComposerOpts['quote']) => void
- setError: (err: string) => void
-}) {
- const {_} = useLingui()
- const [extLink, setExtLink] = useState(
- undefined,
- )
- const getPost = useGetPost()
- const fetchDid = useFetchDid()
- const agent = useAgent()
-
- useEffect(() => {
- let aborted = false
- const cleanup = () => {
- aborted = true
- }
- if (!extLink) {
- return cleanup
- }
- if (!extLink.meta) {
- if (isBskyPostUrl(extLink.uri)) {
- getPostAsQuote(getPost, extLink.uri).then(
- newQuote => {
- if (aborted) {
- return
- }
- setQuote(newQuote)
- setExtLink(undefined)
- },
- err => {
- if (err instanceof EmbeddingDisabledError) {
- setError(_(msg`This post's author has disabled quote posts.`))
- } else {
- logger.error('Failed to fetch post for quote embedding', {
- message: err.toString(),
- })
- }
- setExtLink(undefined)
- },
- )
- } else if (isBskyCustomFeedUrl(extLink.uri)) {
- getFeedAsEmbed(agent, fetchDid, extLink.uri).then(
- ({embed, meta}) => {
- if (aborted) {
- return
- }
- setExtLink({
- uri: extLink.uri,
- isLoading: false,
- meta,
- embed,
- })
- },
- err => {
- logger.error('Failed to fetch feed for embedding', {message: err})
- setExtLink(undefined)
- },
- )
- } else if (isBskyListUrl(extLink.uri)) {
- getListAsEmbed(agent, fetchDid, extLink.uri).then(
- ({embed, meta}) => {
- if (aborted) {
- return
- }
- setExtLink({
- uri: extLink.uri,
- isLoading: false,
- meta,
- embed,
- })
- },
- err => {
- logger.error('Failed to fetch list for embedding', {message: err})
- setExtLink(undefined)
- },
- )
- } else if (
- isBskyStartUrl(extLink.uri) ||
- isBskyStarterPackUrl(extLink.uri)
- ) {
- getStarterPackAsEmbed(agent, fetchDid, extLink.uri).then(
- ({embed, meta}) => {
- if (aborted) {
- return
- }
- setExtLink({
- uri: extLink.uri,
- isLoading: false,
- meta,
- embed,
- })
- },
- )
- } else if (isShortLink(extLink.uri)) {
- if (isShortLink(extLink.uri)) {
- resolveShortLink(extLink.uri).then(res => {
- if (res && res !== extLink.uri) {
- setExtLink({
- uri: res,
- isLoading: true,
- })
- }
- })
- }
- } else {
- getLinkMeta(agent, extLink.uri).then(meta => {
- if (aborted) {
- return
- }
- setExtLink({
- uri: extLink.uri,
- isLoading: !!meta.image,
- meta,
- })
- })
- }
- return cleanup
- }
- if (extLink.isLoading && extLink.meta?.image && !extLink.localThumb) {
- downloadAndResize({
- uri: extLink.meta.image,
- width: POST_IMG_MAX.width,
- height: POST_IMG_MAX.height,
- mode: 'contain',
- maxSize: POST_IMG_MAX.size,
- timeout: 15e3,
- })
- .catch(() => undefined)
- .then(thumb => (thumb ? createComposerImage(thumb) : undefined))
- .then(thumb => {
- if (aborted) {
- return
- }
- setExtLink({
- ...extLink,
- isLoading: false, // done
- localThumb: thumb,
- })
- })
- return cleanup
- }
- if (extLink.isLoading) {
- setExtLink({
- ...extLink,
- isLoading: false, // done
- })
- }
- return cleanup
- }, [_, extLink, setQuote, getPost, fetchDid, agent, setError])
-
- return {extLink, setExtLink}
-}
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 1cad5e0910..8f93538c6a 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -18,6 +18,8 @@ import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {POST_CTRL_HITSLOP} from '#/lib/constants'
+import {CountWheel} from '#/lib/custom-animations/CountWheel'
+import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
import {useHaptics} from '#/lib/haptics'
import {makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing'
@@ -35,8 +37,6 @@ import {
ProgressGuideAction,
useProgressGuideControls,
} from '#/state/shell/progress-guide'
-import {CountWheel} from 'lib/custom-animations/CountWheel'
-import {AnimatedLikeIcon} from 'lib/custom-animations/LikeIcon'
import {atoms as a, useTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
index 98332c33b0..eb03385d0a 100644
--- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
+++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
@@ -5,6 +5,7 @@ import {AppBskyEmbedExternal} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {shareUrl} from '#/lib/sharing'
@@ -55,7 +56,16 @@ export const ExternalLinkEmbed = ({
}, [link.uri, externalEmbedPrefs])
if (embedPlayerParams?.source === 'tenor') {
- return
+ const parsedAlt = parseAltFromGIFDescription(link.description)
+ return (
+
+ )
}
return (
diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx
index a1af6ab26b..fc66278c95 100644
--- a/src/view/com/util/post-embeds/GifEmbed.tsx
+++ b/src/view/com/util/post-embeds/GifEmbed.tsx
@@ -7,12 +7,10 @@ import {
View,
ViewStyle,
} from 'react-native'
-import {AppBskyEmbedExternal} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_20} from '#/lib/constants'
-import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
import {EmbedPlayerParams} from '#/lib/strings/embed-player'
import {isWeb} from '#/platform/detection'
import {useAutoplayDisabled} from '#/state/preferences'
@@ -77,12 +75,16 @@ function PlaybackControls({
export function GifEmbed({
params,
- link,
+ thumb,
+ altText,
+ isPreferredAltText,
hideAlt,
style = {width: '100%'},
}: {
params: EmbedPlayerParams
- link: AppBskyEmbedExternal.ViewExternal
+ thumb: string | undefined
+ altText: string
+ isPreferredAltText: boolean
hideAlt?: boolean
style?: StyleProp
}) {
@@ -111,11 +113,6 @@ export function GifEmbed({
playerRef.current?.toggleAsync()
}, [])
- const parsedAlt = React.useMemo(
- () => parseAltFromGIFDescription(link.description),
- [link],
- )
-
return (
{!playerState.isPlaying && (
)}
- {!hideAlt && parsedAlt.isPreferred && }
+ {!hideAlt && isPreferredAltText && }
)
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index 3b8152c8b8..c44ec3b84d 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -31,6 +31,7 @@ import {makeProfileLink} from '#/lib/routes/links'
import {s} from '#/lib/styles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {precacheProfile} from '#/state/queries/profile'
+import {useResolveLinkQuery} from '#/state/queries/resolve-link'
import {useSession} from '#/state/session'
import {ComposerOptsQuote} from '#/state/shell/composer'
import {atoms as a, useTheme} from '#/alf'
@@ -286,6 +287,24 @@ export function QuoteX({onRemove}: {onRemove: () => void}) {
)
}
+export function LazyQuoteEmbed({uri}: {uri: string}) {
+ const {data} = useResolveLinkQuery(uri)
+ if (!data || data.type !== 'record' || data.kind !== 'post') {
+ return null
+ }
+ return (
+
+ )
+}
+
function viewRecordToPostView(
viewRecord: AppBskyEmbedRecord.ViewRecord,
): AppBskyFeedDefs.PostView {