diff --git a/src/components/moderation/PostHider.tsx b/src/components/moderation/PostHider.tsx index 464ee207..05cb8464 100644 --- a/src/components/moderation/PostHider.tsx +++ b/src/components/moderation/PostHider.tsx @@ -1,25 +1,27 @@ import React, {ComponentProps} from 'react' -import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native' -import {ModerationUI} from '@atproto/api' +import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import {AppBskyActorDefs, ModerationUI} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' +import {useQueryClient} from '@tanstack/react-query' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {addStyle} from 'lib/styles' - -import {useTheme, atoms as a} from '#/alf' +import {precacheProfile} from 'state/queries/profile' +// import {Link} from '#/components/Link' TODO this imposes some styles that screw things up +import {Link} from '#/view/com/util/Link' +import {atoms as a, useTheme} from '#/alf' import { ModerationDetailsDialog, useModerationDetailsDialogControl, } from '#/components/moderation/ModerationDetailsDialog' import {Text} from '#/components/Typography' -// import {Link} from '#/components/Link' TODO this imposes some styles that screw things up -import {Link} from '#/view/com/util/Link' interface Props extends ComponentProps { iconSize: number iconStyles: StyleProp modui: ModerationUI + profile: AppBskyActorDefs.ProfileViewBasic } export function PostHider({ @@ -30,8 +32,10 @@ export function PostHider({ children, iconSize, iconStyles, + profile, ...props }: Props) { + const queryClient = useQueryClient() const t = useTheme() const {_} = useLingui() const [override, setOverride] = React.useState(false) @@ -39,6 +43,10 @@ export function PostHider({ const blur = modui.blurs[0] const desc = useModerationCauseDescription(blur) + const onBeforePress = React.useCallback(() => { + precacheProfile(queryClient, profile) + }, [queryClient, profile]) + if (!blur) { return ( {children} diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx index 56dfb76c..c4490aa5 100644 --- a/src/screens/Messages/List/index.tsx +++ b/src/screens/Messages/List/index.tsx @@ -113,12 +113,7 @@ export function MessagesListScreen({}: Props) { - + [RQKEY_ROOT, did] @@ -477,56 +473,6 @@ export function precacheProfile( queryClient.setQueryData(profileBasicQueryKey(profile.did), profile) } -export function precacheFeedPostProfiles( - queryClient: QueryClient, - posts: AppBskyFeedDefs.FeedViewPost[], -) { - for (const post of posts) { - // Save the author of the post every time - precacheProfile(queryClient, post.post.author) - precachePostEmbedProfile(queryClient, post.post.embed) - - // Cache parent author and embeds - const parent = post.reply?.parent - if (AppBskyFeedDefs.isPostView(parent)) { - precacheProfile(queryClient, parent.author) - precachePostEmbedProfile(queryClient, parent.embed) - } - } -} - -function precachePostEmbedProfile( - queryClient: QueryClient, - embed: AppBskyFeedDefs.PostView['embed'], -) { - if (AppBskyEmbedRecord.isView(embed)) { - if (AppBskyEmbedRecord.isViewRecord(embed.record)) { - precacheProfile(queryClient, embed.record.author) - } - } else if (AppBskyEmbedRecordWithMedia.isView(embed)) { - if (AppBskyEmbedRecord.isViewRecord(embed.record.record)) { - precacheProfile(queryClient, embed.record.record.author) - } - } -} - -export function precacheThreadPostProfiles( - queryClient: QueryClient, - node: ThreadNode, -) { - if (node.type === 'post') { - precacheProfile(queryClient, node.post.author) - if (node.parent) { - precacheThreadPostProfiles(queryClient, node.parent) - } - if (node.replies?.length) { - for (const reply of node.replies) { - precacheThreadPostProfiles(queryClient, reply) - } - } - } -} - async function whenAppViewReady( getAgent: () => BskyAgent, actor: string, diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index 24a2373f..7dc17fd4 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -86,9 +86,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { )}> diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 3c9c6406..94844cb1 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -24,6 +24,7 @@ import { } from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' import {FeedNotification} from '#/state/queries/notifications/feed' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' @@ -36,6 +37,7 @@ import {pluralize} from 'lib/strings/helpers' import {niceDate} from 'lib/strings/time' import {colors, s} from 'lib/styles' import {isWeb} from 'platform/detection' +import {precacheProfile} from 'state/queries/profile' import {Link as NewLink} from '#/components/Link' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {FeedSourceCard} from '../feeds/FeedSourceCard' @@ -52,13 +54,9 @@ const MAX_AUTHORS = 5 const EXPANDED_AUTHOR_EL_HEIGHT = 35 interface Author { + profile: AppBskyActorDefs.ProfileViewBasic href: string - did: string - handle: string - displayName?: string - avatar?: string moderation: ModerationDecision - associated?: AppBskyActorDefs.ProfileAssociated } let FeedItem = ({ @@ -68,6 +66,7 @@ let FeedItem = ({ item: FeedNotification moderationOpts: ModerationOpts }): React.ReactNode => { + const queryClient = useQueryClient() const pal = usePalette('default') const {_} = useLingui() const [isAuthorsExpanded, setAuthorsExpanded] = useState(false) @@ -95,28 +94,22 @@ let FeedItem = ({ setAuthorsExpanded(currentlyExpanded => !currentlyExpanded) } + const onBeforePress = React.useCallback(() => { + precacheProfile(queryClient, item.notification.author) + }, [queryClient, item.notification.author]) + const authors: Author[] = useMemo(() => { return [ { + profile: item.notification.author, href: makeProfileLink(item.notification.author), - did: item.notification.author.did, - handle: item.notification.author.handle, - displayName: item.notification.author.displayName, - avatar: item.notification.author.avatar, moderation: moderateProfile(item.notification.author, moderationOpts), - associated: item.notification.author.associated, }, - ...(item.additional?.map(({author}) => { - return { - href: makeProfileLink(author), - did: author.did, - handle: author.handle, - displayName: author.displayName, - avatar: author.avatar, - moderation: moderateProfile(author, moderationOpts), - associated: author.associated, - } - }) || []), + ...(item.additional?.map(({author}) => ({ + profile: author, + href: makeProfileLink(author), + moderation: moderateProfile(author, moderationOpts), + })) || []), ] }, [item, moderationOpts]) @@ -201,7 +194,8 @@ let FeedItem = ({ accessible={ (item.type === 'post-like' && authors.length === 1) || item.type === 'repost' - }> + } + onBeforePress={onBeforePress}> {/* TODO: Prevent conditional rendering and move toward composable notifications for clearer accessibility labeling */} @@ -231,7 +225,7 @@ let FeedItem = ({ style={[pal.text, s.bold]} href={authors[0].href} text={sanitizeDisplayName( - authors[0].displayName || authors[0].handle, + authors[0].profile.displayName || authors[0].profile.handle, )} disableMismatchWarning /> @@ -339,11 +333,9 @@ function CondensedAuthorsList({ ) @@ -360,11 +352,9 @@ function CondensedAuthorsList({ ))} @@ -415,20 +405,20 @@ function ExpandedAuthorsList({ ]}> {authors.map(author => ( - + @@ -438,10 +428,12 @@ function ExpandedAuthorsList({ numberOfLines={1} style={pal.text} lineHeight={1.2}> - {sanitizeDisplayName(author.displayName || author.handle)} + {sanitizeDisplayName( + author.profile.displayName || author.profile.handle, + )}   - {sanitizeHandle(author.handle)} + {sanitizeHandle(author.profile.handle)} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 4c11fdff..564e37e7 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -249,9 +249,7 @@ let PostThreadItemLoaded = ({ @@ -399,7 +397,8 @@ let PostThreadItemLoaded = ({ isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2} - }> + } + profile={post.author}> diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index b4658694..546eb282 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -21,7 +21,7 @@ import {usePalette} from 'lib/hooks/usePalette' import {makeProfileLink} from 'lib/routes/links' import {countLines} from 'lib/strings/helpers' import {colors, s} from 'lib/styles' -import {RQKEY as RQKEY_URI} from 'state/queries/resolve-uri' +import {precacheProfile} from 'state/queries/profile' import {atoms as a} from '#/alf' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichText} from '#/components/RichText' @@ -135,8 +135,8 @@ function PostInner({ }, [setLimitLines]) const onBeforePress = React.useCallback(() => { - queryClient.setQueryData(RQKEY_URI(post.author.handle), post.author.did) - }, [queryClient, post.author.handle, post.author.did]) + precacheProfile(queryClient, post.author) + }, [queryClient, post.author]) return ( diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 7694b502..605dffde 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -13,6 +13,7 @@ import { } from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {useComposerControls} from '#/state/shell/composer' @@ -24,6 +25,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {countLines} from 'lib/strings/helpers' import {s} from 'lib/styles' +import {precacheProfile} from 'state/queries/profile' import {atoms as a} from '#/alf' import {ContentHider} from '#/components/moderation/ContentHider' import {ProfileHoverCard} from '#/components/ProfileHoverCard' @@ -106,6 +108,7 @@ let FeedItemInner = ({ isThreadLastChild?: boolean isThreadParent?: boolean }): React.ReactNode => { + const queryClient = useQueryClient() const {openComposer} = useComposerControls() const pal = usePalette('default') const {_} = useLingui() @@ -135,6 +138,10 @@ let FeedItemInner = ({ }) }, [post, record, openComposer, moderation]) + const onBeforePress = React.useCallback(() => { + precacheProfile(queryClient, post.author) + }, [queryClient, post.author]) + const outerStyles = [ styles.outer, { @@ -153,7 +160,8 @@ let FeedItemInner = ({ style={outerStyles} href={href} noFeedback - accessible={false}> + accessible={false} + onBeforePress={onBeforePress}> {isThreadChild && ( @@ -240,9 +248,7 @@ let FeedItemInner = ({ diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index b52573a0..90ab9b73 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -20,8 +20,7 @@ import {makeProfileLink} from 'lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {s} from 'lib/styles' -import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from 'state/queries/profile' -import {RQKEY as RQKEY_URI} from 'state/queries/resolve-uri' +import {precacheProfile} from 'state/queries/profile' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {PreviewableUserAvatar} from '../util/UserAvatar' @@ -58,9 +57,7 @@ export function ProfileCard({ const onBeforePress = React.useCallback(() => { onPress?.() - - queryClient.setQueryData(RQKEY_URI(profile.handle), profile.did) - queryClient.setQueryData(RQKEY_PROFILE_BASIC(profile.did), profile) + precacheProfile(queryClient, profile) }, [onPress, profile, queryClient]) if (!moderationOpts) { @@ -91,9 +88,7 @@ export function ProfileCard({ @@ -238,9 +233,7 @@ function FollowersList({ diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx index cf35885c..4c9d164f 100644 --- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx +++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx @@ -220,8 +220,7 @@ function SuggestedFollow({ ]}> diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index d35d0fcc..78d995ee 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -148,6 +148,7 @@ export const TextLink = memo(function TextLink({ dataSet, title, onPress, + onBeforePress, disableMismatchWarning, navigationAction, anchorNoUnderline, @@ -165,6 +166,7 @@ export const TextLink = memo(function TextLink({ disableMismatchWarning?: boolean navigationAction?: 'push' | 'replace' | 'navigate' anchorNoUnderline?: boolean + onBeforePress?: () => void } & TextProps) { const {...props} = useLinkProps({to: sanitizeUrl(href)}) const navigation = useNavigationDeduped() @@ -202,6 +204,7 @@ export const TextLink = memo(function TextLink({ // Let the browser handle opening in new tab etc. return } + onBeforePress?.() if (onPress) { e?.preventDefault?.() // @ts-ignore function signature differs by platform -prf @@ -226,6 +229,7 @@ export const TextLink = memo(function TextLink({ disableMismatchWarning, navigationAction, openLink, + onBeforePress, ], ) const hrefAttrs = useMemo(() => { @@ -274,6 +278,7 @@ interface TextLinkOnWebOnlyProps extends TextProps { title?: string navigationAction?: 'push' | 'replace' | 'navigate' disableMismatchWarning?: boolean + onBeforePress?: () => void onPointerEnter?: () => void anchorNoUnderline?: boolean } @@ -287,6 +292,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ lineHeight, navigationAction, disableMismatchWarning, + onBeforePress, ...props }: TextLinkOnWebOnlyProps) { if (isWeb) { @@ -302,6 +308,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ title={props.title} navigationAction={navigationAction} disableMismatchWarning={disableMismatchWarning} + onBeforePress={onBeforePress} {...props} /> ) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index ed3d3e5b..db16ff06 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -1,8 +1,9 @@ -import React, {memo} from 'react' +import React, {memo, useCallback} from 'react' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' +import {useQueryClient} from '@tanstack/react-query' -import {usePrefetchProfileQuery} from '#/state/queries/profile' +import {precacheProfile, usePrefetchProfileQuery} from '#/state/queries/profile' import {usePalette} from 'lib/hooks/usePalette' import {makeProfileLink} from 'lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' @@ -40,15 +41,18 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { ? () => prefetchProfileQuery(opts.author.did) : undefined + const queryClient = useQueryClient() + const onBeforePress = useCallback(() => { + precacheProfile(queryClient, opts.author) + }, [queryClient, opts.author]) + return ( {opts.showAvatar && ( @@ -71,6 +75,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { } href={profileLink} + onBeforePress={onBeforePress} onPointerEnter={onPointerEnter} /> { style={[pal.textLight, {flexShrink: 4}]} text={'\xa0' + sanitizeHandle(handle, '@')} href={profileLink} + onBeforePress={onBeforePress} onPointerEnter={onPointerEnter} anchorNoUnderline /> @@ -103,6 +109,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { title={niceDate(opts.timestamp)} accessibilityHint="" href={opts.postHref} + onBeforePress={onBeforePress} /> )} diff --git a/src/view/com/util/TimeElapsed.tsx b/src/view/com/util/TimeElapsed.tsx index aa3a0922..6ea41b82 100644 --- a/src/view/com/util/TimeElapsed.tsx +++ b/src/view/com/util/TimeElapsed.tsx @@ -1,6 +1,7 @@ import React from 'react' -import {ago} from 'lib/strings/time' + import {useTickEveryMinute} from '#/state/shell' +import {ago} from 'lib/strings/time' // FIXME(dan): Figure out why the false positives @@ -12,7 +13,7 @@ export function TimeElapsed({ children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element }) { const tick = useTickEveryMinute() - const [timeElapsed, setTimeAgo] = React.useState(ago(timestamp)) + const [timeElapsed, setTimeAgo] = React.useState(() => ago(timestamp)) React.useEffect(() => { setTimeAgo(ago(timestamp)) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 89aa56b7..118e2ce2 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -2,10 +2,11 @@ import React, {memo, useMemo} from 'react' import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' import {Image as RNImage} from 'react-native-image-crop-picker' import Svg, {Circle, Path, Rect} from 'react-native-svg' -import {ModerationUI} from '@atproto/api' +import {AppBskyActorDefs, ModerationUI} 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 {usePalette} from 'lib/hooks/usePalette' import { @@ -15,6 +16,7 @@ import { import {makeProfileLink} from 'lib/routes/links' import {colors} from 'lib/styles' import {isAndroid, isNative, isWeb} from 'platform/detection' +import {precacheProfile} from 'state/queries/profile' import {HighPriorityImage} from 'view/com/util/images/Image' import {tokens, useTheme} from '#/alf' import { @@ -47,8 +49,7 @@ interface EditableUserAvatarProps extends BaseUserAvatarProps { interface PreviewableUserAvatarProps extends BaseUserAvatarProps { moderation?: ModerationUI - did: string - handle: string + profile: AppBskyActorDefs.ProfileViewBasic } const BLUR_AMOUNT = isWeb ? 5 : 100 @@ -371,19 +372,28 @@ let EditableUserAvatar = ({ EditableUserAvatar = memo(EditableUserAvatar) export {EditableUserAvatar} -let PreviewableUserAvatar = ( - props: PreviewableUserAvatarProps, -): React.ReactNode => { +let PreviewableUserAvatar = ({ + moderation, + profile, + ...rest +}: PreviewableUserAvatarProps): React.ReactNode => { const {_} = useLingui() + const queryClient = useQueryClient() + + const onPress = React.useCallback(() => { + precacheProfile(queryClient, profile) + }, [profile, queryClient]) + return ( - + - + did: profile.did, + handle: profile.handle, + })} + onPress={onPress}> + ) diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 935696ab..e0178f34 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -26,10 +26,10 @@ import {useQueryClient} from '@tanstack/react-query' import {HITSLOP_20} from '#/lib/constants' import {s} from '#/lib/styles' import {useModerationOpts} from '#/state/queries/preferences' -import {RQKEY as RQKEY_URI} from '#/state/queries/resolve-uri' import {usePalette} from 'lib/hooks/usePalette' import {InfoCircleIcon} from 'lib/icons' import {makeProfileLink} from 'lib/routes/links' +import {precacheProfile} from 'state/queries/profile' import {ComposerOptsQuote} from 'state/shell/composer' import {atoms as a} from '#/alf' import {RichText} from '#/components/RichText' @@ -149,8 +149,8 @@ export function QuoteEmbed({ }, [quote.embeds]) const onBeforePress = React.useCallback(() => { - queryClient.setQueryData(RQKEY_URI(quote.author.handle), quote.author.did) - }, [queryClient, quote.author.did, quote.author.handle]) + precacheProfile(queryClient, quote.author) + }, [queryClient, quote.author]) return (