Precache basic profile from posts for instant future navigations (#2795)
* skeleton for caching * modify some existing logic * refactor uri resolution query * add precache feed posts * adjustments * remove prefetch on hover (maybe revert, just example) * fix * change arg name to match what we want * optional infinite stale time * use `ProfileViewDetailed` * Revert "remove prefetch on hover (maybe revert, just example)" This reverts commit 08609deb0defa7cea040438bc37dd3488ddc56f4. * add warning comment back for stale time * remove comment * store profile with both the handle and did for query key * remove extra block from revert * clarify argument name * remove QT cache * structure queries the same (put `enabled` at bottom) * use both `ProfileViewDetailed` and `ProfileView` for the query return type * placeholder profile header * remove logs * remove a few other things we don't need * add placeholder * refactor * refactor * we don't need this height adjustment now * use gray banner while loading * set background color of image to the loading placeholder color * reorg imports * add border to header on loading * Fix style * Rm radius * oops * Undo edit * Back out type changes * Tighten some types and moderate shadow * Move precaching fns to profile where the cache is * Rename functions to match what they do now * Remove anys --------- Co-authored-by: Dan Abramov <dan.abramov@gmail.com>zio/stable
parent
d9b62955b5
commit
de28626001
|
@ -12,7 +12,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import chunk from 'lodash.chunk'
|
import chunk from 'lodash.chunk'
|
||||||
import {QueryClient} from '@tanstack/react-query'
|
import {QueryClient} from '@tanstack/react-query'
|
||||||
import {getAgent} from '../../session'
|
import {getAgent} from '../../session'
|
||||||
import {precacheProfile as precacheResolvedUri} from '../resolve-uri'
|
import {precacheProfile} from '../profile'
|
||||||
import {NotificationType, FeedNotification, FeedPage} from './types'
|
import {NotificationType, FeedNotification, FeedPage} from './types'
|
||||||
|
|
||||||
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
|
const GROUPABLE_REASONS = ['like', 'repost', 'follow']
|
||||||
|
@ -59,7 +59,7 @@ export async function fetchPage({
|
||||||
if (notif.subjectUri) {
|
if (notif.subjectUri) {
|
||||||
notif.subject = subjects.get(notif.subjectUri)
|
notif.subject = subjects.get(notif.subjectUri)
|
||||||
if (notif.subject) {
|
if (notif.subject) {
|
||||||
precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution
|
precacheProfile(queryClient, notif.subject.author)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {MergeFeedAPI} from 'lib/api/feed/merge'
|
||||||
import {HomeFeedAPI} from '#/lib/api/feed/home'
|
import {HomeFeedAPI} from '#/lib/api/feed/home'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
|
import {precacheFeedPostProfiles} from './profile'
|
||||||
import {getAgent} from '#/state/session'
|
import {getAgent} from '#/state/session'
|
||||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
|
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
|
||||||
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
||||||
|
@ -138,7 +138,7 @@ export function usePostFeedQuery(
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api.fetch({cursor, limit: PAGE_SIZE})
|
const res = await api.fetch({cursor, limit: PAGE_SIZE})
|
||||||
precacheResolvedUris(queryClient, res.feed) // precache the handle->did resolution
|
precacheFeedPostProfiles(queryClient, res.feed)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If this is a public view, we need to check if posts fail moderation.
|
* If this is a public view, we need to check if posts fail moderation.
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {getAgent} from '#/state/session'
|
||||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||||
import {findPostInQueryData as findPostInFeedQueryData} from './post-feed'
|
import {findPostInQueryData as findPostInFeedQueryData} from './post-feed'
|
||||||
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
|
import {findPostInQueryData as findPostInNotifsQueryData} from './notifications/feed'
|
||||||
import {precacheThreadPosts as precacheResolvedUris} from './resolve-uri'
|
import {precacheThreadPostProfiles} from './profile'
|
||||||
import {getEmbeddedPost} from './util'
|
import {getEmbeddedPost} from './util'
|
||||||
|
|
||||||
export const RQKEY = (uri: string) => ['post-thread', uri]
|
export const RQKEY = (uri: string) => ['post-thread', uri]
|
||||||
|
@ -71,7 +71,7 @@ export function usePostThreadQuery(uri: string | undefined) {
|
||||||
const res = await getAgent().getPostThread({uri: uri!})
|
const res = await getAgent().getPostThread({uri: uri!})
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
const nodes = responseToThreadNodes(res.data.thread)
|
const nodes = responseToThreadNodes(res.data.thread)
|
||||||
precacheResolvedUris(queryClient, nodes) // precache the handle->did resolution
|
precacheThreadPostProfiles(queryClient, nodes)
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
return {type: 'unknown', uri: uri!}
|
return {type: 'unknown', uri: uri!}
|
||||||
|
|
|
@ -4,6 +4,9 @@ import {
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
AppBskyActorProfile,
|
AppBskyActorProfile,
|
||||||
AppBskyActorGetProfile,
|
AppBskyActorGetProfile,
|
||||||
|
AppBskyFeedDefs,
|
||||||
|
AppBskyEmbedRecord,
|
||||||
|
AppBskyEmbedRecordWithMedia,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
useQuery,
|
useQuery,
|
||||||
|
@ -23,9 +26,14 @@ import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
|
||||||
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
|
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {track} from '#/lib/analytics/analytics'
|
import {track} from '#/lib/analytics/analytics'
|
||||||
|
import {ThreadNode} from './post-thread'
|
||||||
|
|
||||||
export const RQKEY = (did: string) => ['profile', did]
|
export const RQKEY = (did: string) => ['profile', did]
|
||||||
export const profilesQueryKey = (handles: string[]) => ['profiles', handles]
|
export const profilesQueryKey = (handles: string[]) => ['profiles', handles]
|
||||||
|
export const profileBasicQueryKey = (didOrHandle: string) => [
|
||||||
|
'profileBasic',
|
||||||
|
didOrHandle,
|
||||||
|
]
|
||||||
|
|
||||||
export function useProfileQuery({
|
export function useProfileQuery({
|
||||||
did,
|
did,
|
||||||
|
@ -34,18 +42,26 @@ export function useProfileQuery({
|
||||||
did: string | undefined
|
did: string | undefined
|
||||||
staleTime?: number
|
staleTime?: number
|
||||||
}) {
|
}) {
|
||||||
return useQuery({
|
const queryClient = useQueryClient()
|
||||||
|
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
|
||||||
// WARNING
|
// WARNING
|
||||||
// this staleTime is load-bearing
|
// this staleTime is load-bearing
|
||||||
// if you remove it, the UI infinite-loops
|
// if you remove it, the UI infinite-loops
|
||||||
// -prf
|
// -prf
|
||||||
staleTime,
|
staleTime,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
queryKey: RQKEY(did || ''),
|
queryKey: RQKEY(did ?? ''),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await getAgent().getProfile({actor: did || ''})
|
const res = await getAgent().getProfile({actor: did ?? ''})
|
||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
|
placeholderData: () => {
|
||||||
|
if (!did) return
|
||||||
|
|
||||||
|
return queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
|
||||||
|
profileBasicQueryKey(did),
|
||||||
|
)
|
||||||
|
},
|
||||||
enabled: !!did,
|
enabled: !!did,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -405,6 +421,64 @@ function useProfileUnblockMutation() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function precacheProfile(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
profile: AppBskyActorDefs.ProfileViewBasic,
|
||||||
|
) {
|
||||||
|
queryClient.setQueryData(profileBasicQueryKey(profile.handle), profile)
|
||||||
|
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(
|
async function whenAppViewReady(
|
||||||
actor: string,
|
actor: string,
|
||||||
fn: (res: AppBskyActorGetProfile.Response) => boolean,
|
fn: (res: AppBskyActorGetProfile.Response) => boolean,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {QueryClient, useQuery, UseQueryResult} from '@tanstack/react-query'
|
import {useQuery, useQueryClient, UseQueryResult} from '@tanstack/react-query'
|
||||||
import {AtUri, AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
|
import {AtUri, AppBskyActorDefs} from '@atproto/api'
|
||||||
|
|
||||||
|
import {profileBasicQueryKey as RQKEY_PROFILE_BASIC} from './profile'
|
||||||
import {getAgent} from '#/state/session'
|
import {getAgent} from '#/state/session'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {ThreadNode} from './post-thread'
|
|
||||||
|
|
||||||
export const RQKEY = (didOrHandle: string) => ['resolved-did', didOrHandle]
|
export const RQKEY = (didOrHandle: string) => ['resolved-did', didOrHandle]
|
||||||
|
|
||||||
|
@ -22,55 +22,29 @@ export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useResolveDidQuery(didOrHandle: string | undefined) {
|
export function useResolveDidQuery(didOrHandle: string | undefined) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useQuery<string, Error>({
|
return useQuery<string, Error>({
|
||||||
staleTime: STALE.HOURS.ONE,
|
staleTime: STALE.HOURS.ONE,
|
||||||
queryKey: RQKEY(didOrHandle || ''),
|
queryKey: RQKEY(didOrHandle ?? ''),
|
||||||
async queryFn() {
|
queryFn: async () => {
|
||||||
if (!didOrHandle) {
|
if (!didOrHandle) return ''
|
||||||
return ''
|
// Just return the did if it's already one
|
||||||
}
|
if (didOrHandle.startsWith('did:')) return didOrHandle
|
||||||
if (!didOrHandle.startsWith('did:')) {
|
|
||||||
const res = await getAgent().resolveHandle({handle: didOrHandle})
|
const res = await getAgent().resolveHandle({handle: didOrHandle})
|
||||||
didOrHandle = res.data.did
|
return res.data.did
|
||||||
}
|
},
|
||||||
return didOrHandle
|
initialData: () => {
|
||||||
|
// Return undefined if no did or handle
|
||||||
|
if (!didOrHandle) return
|
||||||
|
|
||||||
|
const profile =
|
||||||
|
queryClient.getQueryData<AppBskyActorDefs.ProfileViewBasic>(
|
||||||
|
RQKEY_PROFILE_BASIC(didOrHandle),
|
||||||
|
)
|
||||||
|
return profile?.did
|
||||||
},
|
},
|
||||||
enabled: !!didOrHandle,
|
enabled: !!didOrHandle,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function precacheProfile(
|
|
||||||
queryClient: QueryClient,
|
|
||||||
profile:
|
|
||||||
| AppBskyActorDefs.ProfileView
|
|
||||||
| AppBskyActorDefs.ProfileViewBasic
|
|
||||||
| AppBskyActorDefs.ProfileViewDetailed,
|
|
||||||
) {
|
|
||||||
queryClient.setQueryData(RQKEY(profile.handle), profile.did)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function precacheFeedPosts(
|
|
||||||
queryClient: QueryClient,
|
|
||||||
posts: AppBskyFeedDefs.FeedViewPost[],
|
|
||||||
) {
|
|
||||||
for (const post of posts) {
|
|
||||||
precacheProfile(queryClient, post.post.author)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function precacheThreadPosts(
|
|
||||||
queryClient: QueryClient,
|
|
||||||
node: ThreadNode,
|
|
||||||
) {
|
|
||||||
if (node.type === 'post') {
|
|
||||||
precacheProfile(queryClient, node.post.author)
|
|
||||||
if (node.parent) {
|
|
||||||
precacheThreadPosts(queryClient, node.parent)
|
|
||||||
}
|
|
||||||
if (node.replies?.length) {
|
|
||||||
for (const reply of node.replies) {
|
|
||||||
precacheThreadPosts(queryClient, reply)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {memo} from 'react'
|
import React, {memo, useMemo} from 'react'
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
@ -10,7 +10,8 @@ import {useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
ProfileModeration,
|
ModerationOpts,
|
||||||
|
moderateProfile,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
@ -42,12 +43,11 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {BACK_HITSLOP} from 'lib/constants'
|
import {BACK_HITSLOP} from 'lib/constants'
|
||||||
import {isInvalidHandle} from 'lib/strings/handles'
|
import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {pluralize} from 'lib/strings/helpers'
|
import {pluralize} from 'lib/strings/helpers'
|
||||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
|
||||||
import {shareUrl} from 'lib/sharing'
|
import {shareUrl} from 'lib/sharing'
|
||||||
import {s, colors} from 'lib/styles'
|
import {s, colors} from 'lib/styles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
@ -55,17 +55,19 @@ import {useSession, getAgent} from '#/state/session'
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {useRequireAuth} from '#/state/session'
|
import {useRequireAuth} from '#/state/session'
|
||||||
import {LabelInfo} from '../util/moderation/LabelInfo'
|
import {LabelInfo} from '../util/moderation/LabelInfo'
|
||||||
|
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> | null
|
profile: AppBskyActorDefs.ProfileView | null
|
||||||
moderation: ProfileModeration | null
|
placeholderData?: AppBskyActorDefs.ProfileView | null
|
||||||
|
moderationOpts: ModerationOpts | null
|
||||||
hideBackButton?: boolean
|
hideBackButton?: boolean
|
||||||
isProfilePreview?: boolean
|
isProfilePreview?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileHeader({
|
export function ProfileHeader({
|
||||||
profile,
|
profile,
|
||||||
moderation,
|
moderationOpts,
|
||||||
hideBackButton = false,
|
hideBackButton = false,
|
||||||
isProfilePreview,
|
isProfilePreview,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
@ -73,10 +75,14 @@ export function ProfileHeader({
|
||||||
|
|
||||||
// loading
|
// loading
|
||||||
// =
|
// =
|
||||||
if (!profile || !moderation) {
|
if (!profile || !moderationOpts) {
|
||||||
return (
|
return (
|
||||||
<View style={pal.view}>
|
<View style={pal.view}>
|
||||||
<LoadingPlaceholder width="100%" height={153} />
|
<LoadingPlaceholder
|
||||||
|
width="100%"
|
||||||
|
height={150}
|
||||||
|
style={{borderRadius: 0}}
|
||||||
|
/>
|
||||||
<View
|
<View
|
||||||
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
|
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
|
||||||
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
|
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
|
||||||
|
@ -95,7 +101,7 @@ export function ProfileHeader({
|
||||||
return (
|
return (
|
||||||
<ProfileHeaderLoaded
|
<ProfileHeaderLoaded
|
||||||
profile={profile}
|
profile={profile}
|
||||||
moderation={moderation}
|
moderationOpts={moderationOpts}
|
||||||
hideBackButton={hideBackButton}
|
hideBackButton={hideBackButton}
|
||||||
isProfilePreview={isProfilePreview}
|
isProfilePreview={isProfilePreview}
|
||||||
/>
|
/>
|
||||||
|
@ -103,18 +109,20 @@ export function ProfileHeader({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadedProps {
|
interface LoadedProps {
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
moderation: ProfileModeration
|
moderationOpts: ModerationOpts
|
||||||
hideBackButton?: boolean
|
hideBackButton?: boolean
|
||||||
isProfilePreview?: boolean
|
isProfilePreview?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let ProfileHeaderLoaded = ({
|
let ProfileHeaderLoaded = ({
|
||||||
profile,
|
profile: profileUnshadowed,
|
||||||
moderation,
|
moderationOpts,
|
||||||
hideBackButton = false,
|
hideBackButton = false,
|
||||||
isProfilePreview,
|
isProfilePreview,
|
||||||
}: LoadedProps): React.ReactNode => {
|
}: LoadedProps): React.ReactNode => {
|
||||||
|
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
|
||||||
|
useProfileShadow(profileUnshadowed)
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palInverted = usePalette('inverted')
|
const palInverted = usePalette('inverted')
|
||||||
const {currentAccount, hasSession} = useSession()
|
const {currentAccount, hasSession} = useSession()
|
||||||
|
@ -131,6 +139,10 @@ let ProfileHeaderLoaded = ({
|
||||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
||||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const moderation = useMemo(
|
||||||
|
() => moderateProfile(profile, moderationOpts),
|
||||||
|
[profile, moderationOpts],
|
||||||
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* BEGIN handle bio facet resolution
|
* BEGIN handle bio facet resolution
|
||||||
|
@ -442,9 +454,22 @@ let ProfileHeaderLoaded = ({
|
||||||
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
|
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={pal.view} pointerEvents="box-none">
|
<View
|
||||||
|
style={[
|
||||||
|
pal.view,
|
||||||
|
isProfilePreview && isDesktop && styles.loadingBorderStyle,
|
||||||
|
]}
|
||||||
|
pointerEvents="box-none">
|
||||||
<View pointerEvents="none">
|
<View pointerEvents="none">
|
||||||
|
{isProfilePreview ? (
|
||||||
|
<LoadingPlaceholder
|
||||||
|
width="100%"
|
||||||
|
height={150}
|
||||||
|
style={{borderRadius: 0}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<UserBanner banner={profile.banner} moderation={moderation.avatar} />
|
<UserBanner banner={profile.banner} moderation={moderation.avatar} />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.content} pointerEvents="box-none">
|
<View style={styles.content} pointerEvents="box-none">
|
||||||
<View style={[styles.buttonsLine]} pointerEvents="box-none">
|
<View style={[styles.buttonsLine]} pointerEvents="box-none">
|
||||||
|
@ -478,7 +503,7 @@ let ProfileHeaderLoaded = ({
|
||||||
)
|
)
|
||||||
) : !profile.viewer?.blockedBy ? (
|
) : !profile.viewer?.blockedBy ? (
|
||||||
<>
|
<>
|
||||||
{!isProfilePreview && hasSession && (
|
{hasSession && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="suggestedFollowsBtn"
|
testID="suggestedFollowsBtn"
|
||||||
onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
|
onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
|
||||||
|
@ -597,7 +622,7 @@ let ProfileHeaderLoaded = ({
|
||||||
{invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
|
{invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
{!blockHide && (
|
{!isProfilePreview && !blockHide && (
|
||||||
<>
|
<>
|
||||||
<View style={styles.metricsLine} pointerEvents="box-none">
|
<View style={styles.metricsLine} pointerEvents="box-none">
|
||||||
<Link
|
<Link
|
||||||
|
@ -665,7 +690,7 @@ let ProfileHeaderLoaded = ({
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{!isProfilePreview && showSuggestedFollows && (
|
{showSuggestedFollows && (
|
||||||
<ProfileHeaderSuggestedFollows
|
<ProfileHeaderSuggestedFollows
|
||||||
actorDid={profile.did}
|
actorDid={profile.did}
|
||||||
requestDismiss={() => {
|
requestDismiss={() => {
|
||||||
|
@ -820,4 +845,9 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
br40: {borderRadius: 40},
|
br40: {borderRadius: 40},
|
||||||
br50: {borderRadius: 50},
|
br50: {borderRadius: 50},
|
||||||
|
|
||||||
|
loadingBorderStyle: {
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,10 @@ import {StyleSheet, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {ModerationUI} from '@atproto/api'
|
import {ModerationUI} from '@atproto/api'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||||
import {
|
import {
|
||||||
usePhotoLibraryPermission,
|
usePhotoLibraryPermission,
|
||||||
|
@ -13,8 +16,6 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {isWeb, isAndroid} from 'platform/detection'
|
import {isWeb, isAndroid} from 'platform/detection'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
|
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {msg} from '@lingui/macro'
|
|
||||||
|
|
||||||
export function UserBanner({
|
export function UserBanner({
|
||||||
banner,
|
banner,
|
||||||
|
@ -26,6 +27,7 @@ export function UserBanner({
|
||||||
onSelectNewBanner?: (img: RNImage | null) => void
|
onSelectNewBanner?: (img: RNImage | null) => void
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const theme = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
const {requestCameraAccessIfNeeded} = useCameraPermission()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
|
@ -142,7 +144,10 @@ export function UserBanner({
|
||||||
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
|
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
|
||||||
<Image
|
<Image
|
||||||
testID="userBannerImage"
|
testID="userBannerImage"
|
||||||
style={styles.bannerImage}
|
style={[
|
||||||
|
styles.bannerImage,
|
||||||
|
{backgroundColor: theme.palette.default.backgroundLight},
|
||||||
|
]}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
source={{uri: banner}}
|
source={{uri: banner}}
|
||||||
blurRadius={moderation?.blur ? 100 : 0}
|
blurRadius={moderation?.blur ? 100 : 0}
|
||||||
|
|
|
@ -66,6 +66,7 @@ export function ProfileScreen({route}: Props) {
|
||||||
error: profileError,
|
error: profileError,
|
||||||
refetch: refetchProfile,
|
refetch: refetchProfile,
|
||||||
isLoading: isLoadingProfile,
|
isLoading: isLoadingProfile,
|
||||||
|
isPlaceholderData: isPlaceholderProfile,
|
||||||
} = useProfileQuery({
|
} = useProfileQuery({
|
||||||
did: resolvedDid,
|
did: resolvedDid,
|
||||||
})
|
})
|
||||||
|
@ -85,12 +86,13 @@ export function ProfileScreen({route}: Props) {
|
||||||
}
|
}
|
||||||
}, [profile?.viewer?.blockedBy, resolvedDid])
|
}, [profile?.viewer?.blockedBy, resolvedDid])
|
||||||
|
|
||||||
if (isLoadingDid || isLoadingProfile || !moderationOpts) {
|
// Most pushes will happen here, since we will have only placeholder data
|
||||||
|
if (isLoadingDid || isLoadingProfile || isPlaceholderProfile) {
|
||||||
return (
|
return (
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<ProfileHeader
|
<ProfileHeader
|
||||||
profile={null}
|
profile={profile ?? null}
|
||||||
moderation={null}
|
moderationOpts={moderationOpts ?? null}
|
||||||
isProfilePreview={true}
|
isProfilePreview={true}
|
||||||
/>
|
/>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
|
@ -268,11 +270,11 @@ function ProfileScreenLoaded({
|
||||||
return (
|
return (
|
||||||
<ProfileHeader
|
<ProfileHeader
|
||||||
profile={profile}
|
profile={profile}
|
||||||
moderation={moderation}
|
moderationOpts={moderationOpts}
|
||||||
hideBackButton={hideBackButton}
|
hideBackButton={hideBackButton}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}, [profile, moderation, hideBackButton])
|
}, [profile, moderationOpts, hideBackButton])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenHider
|
<ScreenHider
|
||||||
|
|
Loading…
Reference in New Issue