Delete some dead code and do some refactoring
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {ListRef} from 'view/com/util/List'
|
||||
import {Feed} from 'view/com/posts/Feed'
|
||||
import {EmptyState} from 'view/com/util/EmptyState'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {SectionRef} from './types'
|
||||
|
||||
interface FeedSectionProps {
|
||||
feed: FeedDescriptor
|
||||
headerHeight: number
|
||||
isFocused: boolean
|
||||
scrollElRef: ListRef
|
||||
ignoreFilterFor?: string
|
||||
}
|
||||
export const ProfileFeedSection = React.forwardRef<
|
||||
SectionRef,
|
||||
FeedSectionProps
|
||||
>(function FeedSectionImpl(
|
||||
{feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
|
||||
ref,
|
||||
) {
|
||||
const {_} = useLingui()
|
||||
const queryClient = useQueryClient()
|
||||
const [hasNew, setHasNew] = React.useState(false)
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||
|
||||
const onScrollToTop = React.useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({
|
||||
animated: isNative,
|
||||
offset: -headerHeight,
|
||||
})
|
||||
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
||||
setHasNew(false)
|
||||
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
scrollToTop: onScrollToTop,
|
||||
}))
|
||||
|
||||
const renderPostsEmpty = React.useCallback(() => {
|
||||
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
|
||||
}, [_])
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Feed
|
||||
testID="postsFeed"
|
||||
enabled={isFocused}
|
||||
feed={feed}
|
||||
scrollElRef={scrollElRef}
|
||||
onHasNew={setHasNew}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
renderEmptyState={renderPostsEmpty}
|
||||
headerOffset={headerHeight}
|
||||
renderEndOfFeed={ProfileEndOfFeed}
|
||||
ignoreFilterFor={ignoreFilterFor}
|
||||
/>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onScrollToTop}
|
||||
label={_(msg`Load new posts`)}
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
function ProfileEndOfFeed() {
|
||||
const pal = usePalette('default')
|
||||
|
||||
return (
|
||||
<View style={[pal.border, {paddingTop: 32, borderTopWidth: 1}]}>
|
||||
<Text style={[pal.textLight, pal.border, {textAlign: 'center'}]}>
|
||||
<Trans>End of feed</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface SectionRef {
|
||||
scrollToTop: () => void
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {getAgent} from '#/state/session'
|
||||
import {STALE} from '#/state/queries'
|
||||
|
||||
// TODO refactor invalidate on mutate?
|
||||
export const RQKEY = (did: string) => ['profile-extra-info', did]
|
||||
|
||||
/**
|
||||
* Fetches some additional information for the profile screen which
|
||||
* is not available in the API's ProfileView
|
||||
*/
|
||||
export function useProfileExtraInfoQuery(did: string) {
|
||||
return useQuery({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: RQKEY(did),
|
||||
async queryFn() {
|
||||
const [listsRes, feedsRes] = await Promise.all([
|
||||
getAgent().app.bsky.graph.getLists({
|
||||
actor: did,
|
||||
limit: 1,
|
||||
}),
|
||||
getAgent().app.bsky.feed.getActorFeeds({
|
||||
actor: did,
|
||||
limit: 1,
|
||||
}),
|
||||
])
|
||||
return {
|
||||
hasLists: listsRes.data.lists.length > 0,
|
||||
hasFeedgens: feedsRes.data.feeds.length > 0,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,798 +0,0 @@
|
||||
import React, {memo, useMemo} from 'react'
|
||||
import {
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
ModerationOpts,
|
||||
moderateProfile,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {isNative, isWeb} from 'platform/detection'
|
||||
import {BlurView} from '../util/BlurView'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ThemedText} from '../util/text/ThemedText'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {UserBanner} from '../util/UserBanner'
|
||||
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
|
||||
import {formatCount} from '../util/numeric/format'
|
||||
import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
|
||||
import {Link} from '../util/Link'
|
||||
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
|
||||
import {
|
||||
RQKEY as profileQueryKey,
|
||||
useProfileMuteMutationQueue,
|
||||
useProfileBlockMutationQueue,
|
||||
useProfileFollowMutationQueue,
|
||||
} from '#/state/queries/profile'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {BACK_HITSLOP} from 'lib/constants'
|
||||
import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {useSession} from '#/state/session'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
import {LabelsOnMe} from '../util/moderation/LabelsOnMe'
|
||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||
import {NEW_REPORT_DIALOG_ENABLED} from '#/lib/build-flags'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View style={pal.view}>
|
||||
<LoadingPlaceholder width="100%" height={150} style={{borderRadius: 0}} />
|
||||
<View
|
||||
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
|
||||
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<View style={[styles.buttonsLine]}>
|
||||
<LoadingPlaceholder width={167} height={31} style={styles.br50} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
ProfileHeaderLoading = memo(ProfileHeaderLoading)
|
||||
export {ProfileHeaderLoading}
|
||||
|
||||
interface Props {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
descriptionRT: RichTextAPI | null
|
||||
moderationOpts: ModerationOpts
|
||||
hideBackButton?: boolean
|
||||
isPlaceholderProfile?: boolean
|
||||
}
|
||||
|
||||
let ProfileHeader = ({
|
||||
profile: profileUnshadowed,
|
||||
descriptionRT,
|
||||
moderationOpts,
|
||||
hideBackButton = false,
|
||||
isPlaceholderProfile,
|
||||
}: Props): React.ReactNode => {
|
||||
const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
|
||||
useProfileShadow(profileUnshadowed)
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const {currentAccount, hasSession} = useSession()
|
||||
const requireAuth = useRequireAuth()
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
const {openLightbox} = useLightboxControls()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
const invalidHandle = isInvalidHandle(profile.handle)
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
|
||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
|
||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||
const queryClient = useQueryClient()
|
||||
const moderation = useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
[profile, moderationOpts],
|
||||
)
|
||||
const control = useReportDialogControl()
|
||||
|
||||
const invalidateProfileQuery = React.useCallback(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: profileQueryKey(profile.did),
|
||||
})
|
||||
}, [queryClient, profile.did])
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
const modui = moderation.ui('avatar')
|
||||
if (profile.avatar && !(modui.blur && modui.noOverride)) {
|
||||
openLightbox(new ProfileImageLightbox(profile))
|
||||
}
|
||||
}, [openLightbox, profile, moderation])
|
||||
|
||||
const onPressFollow = () => {
|
||||
requireAuth(async () => {
|
||||
try {
|
||||
track('ProfileHeader:FollowButtonClicked')
|
||||
await queueFollow()
|
||||
Toast.show(
|
||||
_(
|
||||
msg`Following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to follow', {message: String(e)})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onPressUnfollow = () => {
|
||||
requireAuth(async () => {
|
||||
try {
|
||||
track('ProfileHeader:UnfollowButtonClicked')
|
||||
await queueUnfollow()
|
||||
Toast.show(
|
||||
_(
|
||||
msg`No longer following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unfollow', {message: String(e)})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onPressEditProfile = React.useCallback(() => {
|
||||
track('ProfileHeader:EditProfileButtonClicked')
|
||||
openModal({
|
||||
name: 'edit-profile',
|
||||
profile,
|
||||
})
|
||||
}, [track, openModal, profile])
|
||||
|
||||
const onPressShare = React.useCallback(() => {
|
||||
track('ProfileHeader:ShareButtonClicked')
|
||||
shareUrl(toShareUrl(makeProfileLink(profile)))
|
||||
}, [track, profile])
|
||||
|
||||
const onPressAddRemoveLists = React.useCallback(() => {
|
||||
track('ProfileHeader:AddToListsButtonClicked')
|
||||
openModal({
|
||||
name: 'user-add-remove-lists',
|
||||
subject: profile.did,
|
||||
handle: profile.handle,
|
||||
displayName: profile.displayName || profile.handle,
|
||||
onAdd: invalidateProfileQuery,
|
||||
onRemove: invalidateProfileQuery,
|
||||
})
|
||||
}, [track, profile, openModal, invalidateProfileQuery])
|
||||
|
||||
const onPressMuteAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:MuteAccountButtonClicked')
|
||||
try {
|
||||
await queueMute()
|
||||
Toast.show(_(msg`Account muted`))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to mute account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||
}
|
||||
}
|
||||
}, [track, queueMute, _])
|
||||
|
||||
const onPressUnmuteAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:UnmuteAccountButtonClicked')
|
||||
try {
|
||||
await queueUnmute()
|
||||
Toast.show(_(msg`Account unmuted`))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unmute account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||
}
|
||||
}
|
||||
}, [track, queueUnmute, _])
|
||||
|
||||
const onPressBlockAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:BlockAccountButtonClicked')
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Block Account`),
|
||||
message: _(
|
||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||
),
|
||||
onPressConfirm: async () => {
|
||||
try {
|
||||
await queueBlock()
|
||||
Toast.show(_(msg`Account blocked`))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to block account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [track, queueBlock, openModal, _])
|
||||
|
||||
const onPressUnblockAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:UnblockAccountButtonClicked')
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Unblock Account`),
|
||||
message: _(
|
||||
msg`The account will be able to interact with you after unblocking.`,
|
||||
),
|
||||
onPressConfirm: async () => {
|
||||
try {
|
||||
await queueUnblock()
|
||||
Toast.show(_(msg`Account unblocked`))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unblock account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [track, queueUnblock, openModal, _])
|
||||
|
||||
const onPressReportAccount = React.useCallback(() => {
|
||||
track('ProfileHeader:ReportAccountButtonClicked')
|
||||
if (NEW_REPORT_DIALOG_ENABLED) {
|
||||
control.open()
|
||||
} else {
|
||||
openModal({
|
||||
name: 'report',
|
||||
did: profile.did,
|
||||
})
|
||||
}
|
||||
}, [track, openModal, profile, control])
|
||||
|
||||
const isMe = React.useMemo(
|
||||
() => currentAccount?.did === profile.did,
|
||||
[currentAccount, profile],
|
||||
)
|
||||
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
||||
let items: DropdownItem[] = [
|
||||
{
|
||||
testID: 'profileHeaderDropdownShareBtn',
|
||||
label: isWeb ? _(msg`Copy link to profile`) : _(msg`Share`),
|
||||
onPress: onPressShare,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'square.and.arrow.up',
|
||||
},
|
||||
android: 'ic_menu_share',
|
||||
web: 'share',
|
||||
},
|
||||
},
|
||||
]
|
||||
if (hasSession) {
|
||||
items.push({label: 'separator'})
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
||||
label: _(msg`Add to Lists`),
|
||||
onPress: onPressAddRemoveLists,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'list.bullet',
|
||||
},
|
||||
android: 'ic_menu_add',
|
||||
web: 'list',
|
||||
},
|
||||
})
|
||||
if (!isMe) {
|
||||
if (!profile.viewer?.blocking) {
|
||||
if (!profile.viewer?.mutedByList) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownMuteBtn',
|
||||
label: profile.viewer?.muted
|
||||
? _(msg`Unmute Account`)
|
||||
: _(msg`Mute Account`),
|
||||
onPress: profile.viewer?.muted
|
||||
? onPressUnmuteAccount
|
||||
: onPressMuteAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'speaker.slash',
|
||||
},
|
||||
android: 'ic_lock_silent_mode',
|
||||
web: 'comment-slash',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (!profile.viewer?.blockingByList) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownBlockBtn',
|
||||
label: profile.viewer?.blocking
|
||||
? _(msg`Unblock Account`)
|
||||
: _(msg`Block Account`),
|
||||
onPress: profile.viewer?.blocking
|
||||
? onPressUnblockAccount
|
||||
: onPressBlockAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'person.fill.xmark',
|
||||
},
|
||||
android: 'ic_menu_close_clear_cancel',
|
||||
web: 'user-slash',
|
||||
},
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownReportBtn',
|
||||
label: _(msg`Report Account`),
|
||||
onPress: onPressReportAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return items
|
||||
}, [
|
||||
isMe,
|
||||
hasSession,
|
||||
profile.viewer?.muted,
|
||||
profile.viewer?.mutedByList,
|
||||
profile.viewer?.blocking,
|
||||
profile.viewer?.blockingByList,
|
||||
onPressShare,
|
||||
onPressUnmuteAccount,
|
||||
onPressMuteAccount,
|
||||
onPressUnblockAccount,
|
||||
onPressBlockAccount,
|
||||
onPressReportAccount,
|
||||
onPressAddRemoveLists,
|
||||
_,
|
||||
])
|
||||
|
||||
const blockHide =
|
||||
!isMe && (profile.viewer?.blocking || profile.viewer?.blockedBy)
|
||||
const following = formatCount(profile.followsCount || 0)
|
||||
const followers = formatCount(profile.followersCount || 0)
|
||||
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
|
||||
|
||||
return (
|
||||
<View style={[pal.view]} pointerEvents="box-none">
|
||||
<ReportDialog
|
||||
control={control}
|
||||
params={{type: 'profile', did: profile.did}}
|
||||
/>
|
||||
|
||||
<View pointerEvents="none">
|
||||
{isPlaceholderProfile ? (
|
||||
<LoadingPlaceholder
|
||||
width="100%"
|
||||
height={150}
|
||||
style={{borderRadius: 0}}
|
||||
/>
|
||||
) : (
|
||||
<UserBanner
|
||||
banner={profile.banner}
|
||||
moderation={moderation.ui('banner')}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.content} pointerEvents="box-none">
|
||||
<View style={[styles.buttonsLine]} pointerEvents="box-none">
|
||||
{isMe ? (
|
||||
<TouchableOpacity
|
||||
testID="profileHeaderEditProfileButton"
|
||||
onPress={onPressEditProfile}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Edit profile`)}
|
||||
accessibilityHint={_(
|
||||
msg`Opens editor for profile display name, avatar, background image, and description`,
|
||||
)}>
|
||||
<Text type="button" style={pal.text}>
|
||||
<Trans>Edit Profile</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : profile.viewer?.blocking ? (
|
||||
profile.viewer?.blockingByList ? null : (
|
||||
<TouchableOpacity
|
||||
testID="unblockBtn"
|
||||
onPress={onPressUnblockAccount}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Unblock`)}
|
||||
accessibilityHint="">
|
||||
<Text type="button" style={[pal.text, s.bold]}>
|
||||
<Trans context="action">Unblock</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
) : !profile.viewer?.blockedBy ? (
|
||||
<>
|
||||
{hasSession && (
|
||||
<TouchableOpacity
|
||||
testID="suggestedFollowsBtn"
|
||||
onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
|
||||
style={[
|
||||
styles.btn,
|
||||
styles.mainBtn,
|
||||
pal.btn,
|
||||
{
|
||||
paddingHorizontal: 10,
|
||||
backgroundColor: showSuggestedFollows
|
||||
? pal.colors.text
|
||||
: pal.colors.backgroundLight,
|
||||
},
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(
|
||||
msg`Show follows similar to ${profile.handle}`,
|
||||
)}
|
||||
accessibilityHint={_(
|
||||
msg`Shows a list of users similar to this user.`,
|
||||
)}>
|
||||
<FontAwesomeIcon
|
||||
icon="user-plus"
|
||||
style={[
|
||||
pal.text,
|
||||
{
|
||||
color: showSuggestedFollows
|
||||
? pal.textInverted.color
|
||||
: pal.text.color,
|
||||
},
|
||||
]}
|
||||
size={14}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{profile.viewer?.following ? (
|
||||
<TouchableOpacity
|
||||
testID="unfollowBtn"
|
||||
onPress={onPressUnfollow}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Unfollow ${profile.handle}`)}
|
||||
accessibilityHint={_(
|
||||
msg`Hides posts from ${profile.handle} in your feed`,
|
||||
)}>
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
style={[pal.text, s.mr5]}
|
||||
size={14}
|
||||
/>
|
||||
<Text type="button" style={pal.text}>
|
||||
<Trans>Following</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="followBtn"
|
||||
onPress={onPressFollow}
|
||||
style={[styles.btn, styles.mainBtn, palInverted.view]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Follow ${profile.handle}`)}
|
||||
accessibilityHint={_(
|
||||
msg`Shows posts from ${profile.handle} in your feed`,
|
||||
)}>
|
||||
<FontAwesomeIcon
|
||||
icon="plus"
|
||||
style={[palInverted.text, s.mr5]}
|
||||
/>
|
||||
<Text type="button" style={[palInverted.text, s.bold]}>
|
||||
<Trans>Follow</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{dropdownItems?.length ? (
|
||||
<NativeDropdown
|
||||
testID="profileHeaderDropdownBtn"
|
||||
items={dropdownItems}
|
||||
accessibilityLabel={_(msg`More options`)}
|
||||
accessibilityHint="">
|
||||
<View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
|
||||
<FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
|
||||
</View>
|
||||
</NativeDropdown>
|
||||
) : undefined}
|
||||
</View>
|
||||
<View pointerEvents="none">
|
||||
<Text
|
||||
testID="profileHeaderDisplayName"
|
||||
type="title-2xl"
|
||||
style={[pal.text, styles.title]}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.handleLine} pointerEvents="none">
|
||||
{profile.viewer?.followedBy && !blockHide ? (
|
||||
<View style={[styles.pill, pal.btn, s.mr5]}>
|
||||
<Text type="xs" style={[pal.text]}>
|
||||
<Trans>Follows you</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
<ThemedText
|
||||
type={invalidHandle ? 'xs' : 'md'}
|
||||
fg={invalidHandle ? 'error' : 'light'}
|
||||
border={invalidHandle ? 'error' : undefined}
|
||||
style={[
|
||||
invalidHandle ? styles.invalidHandle : undefined,
|
||||
styles.handle,
|
||||
]}>
|
||||
{invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
|
||||
</ThemedText>
|
||||
</View>
|
||||
{!isPlaceholderProfile && !blockHide && (
|
||||
<>
|
||||
<View style={styles.metricsLine} pointerEvents="box-none">
|
||||
<Link
|
||||
testID="profileHeaderFollowersButton"
|
||||
style={[s.flexRow, s.mr10]}
|
||||
href={makeProfileLink(profile, 'followers')}
|
||||
onPressOut={() =>
|
||||
track(`ProfileHeader:FollowersButtonClicked`, {
|
||||
handle: profile.handle,
|
||||
})
|
||||
}
|
||||
asAnchor
|
||||
accessibilityLabel={`${followers} ${pluralizedFollowers}`}
|
||||
accessibilityHint={_(msg`Opens followers list`)}>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
{followers}{' '}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
{pluralizedFollowers}
|
||||
</Text>
|
||||
</Link>
|
||||
<Link
|
||||
testID="profileHeaderFollowsButton"
|
||||
style={[s.flexRow, s.mr10]}
|
||||
href={makeProfileLink(profile, 'follows')}
|
||||
onPressOut={() =>
|
||||
track(`ProfileHeader:FollowsButtonClicked`, {
|
||||
handle: profile.handle,
|
||||
})
|
||||
}
|
||||
asAnchor
|
||||
accessibilityLabel={_(msg`${following} following`)}
|
||||
accessibilityHint={_(msg`Opens following list`)}>
|
||||
<Trans>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
{following}{' '}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
following
|
||||
</Text>
|
||||
</Trans>
|
||||
</Link>
|
||||
<Text type="md" style={[s.bold, pal.text]}>
|
||||
{formatCount(profile.postsCount || 0)}{' '}
|
||||
<Text type="md" style={[pal.textLight]}>
|
||||
{pluralize(profile.postsCount || 0, 'post')}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
{descriptionRT && !moderation.ui('profileView').blur ? (
|
||||
<View pointerEvents="auto" style={[styles.description]}>
|
||||
<RichText
|
||||
testID="profileHeaderDescription"
|
||||
style={[a.text_md]}
|
||||
numberOfLines={15}
|
||||
value={descriptionRT}
|
||||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
</>
|
||||
)}
|
||||
<ProfileHeaderAlerts moderation={moderation} />
|
||||
{isMe && (
|
||||
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{showSuggestedFollows && (
|
||||
<ProfileHeaderSuggestedFollows
|
||||
actorDid={profile.did}
|
||||
requestDismiss={() => {
|
||||
if (showSuggestedFollows) {
|
||||
setShowSuggestedFollows(false)
|
||||
} else {
|
||||
track('ProfileHeader:SuggestedFollowsOpened')
|
||||
setShowSuggestedFollows(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isDesktop && !hideBackButton && (
|
||||
<TouchableWithoutFeedback
|
||||
testID="profileHeaderBackBtn"
|
||||
onPress={onPressBack}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
<View style={styles.backBtnWrapper}>
|
||||
<BlurView style={styles.backBtn} blurType="dark">
|
||||
<FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
|
||||
</BlurView>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
<TouchableWithoutFeedback
|
||||
testID="profileHeaderAviButton"
|
||||
onPress={onPressAvi}
|
||||
accessibilityRole="image"
|
||||
accessibilityLabel={_(msg`View ${profile.handle}'s avatar`)}
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
|
||||
<UserAvatar
|
||||
size={80}
|
||||
avatar={profile.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
ProfileHeader = memo(ProfileHeader)
|
||||
export {ProfileHeader}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
banner: {
|
||||
width: '100%',
|
||||
height: 120,
|
||||
},
|
||||
backBtnWrapper: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
width: 30,
|
||||
height: 30,
|
||||
overflow: 'hidden',
|
||||
borderRadius: 15,
|
||||
// @ts-ignore web only
|
||||
cursor: 'pointer',
|
||||
},
|
||||
backBtn: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
avi: {
|
||||
position: 'absolute',
|
||||
top: 110,
|
||||
left: 10,
|
||||
width: 84,
|
||||
height: 84,
|
||||
borderRadius: 42,
|
||||
borderWidth: 2,
|
||||
},
|
||||
content: {
|
||||
paddingTop: 8,
|
||||
paddingHorizontal: 14,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
|
||||
buttonsLine: {
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
marginBottom: 12,
|
||||
},
|
||||
primaryBtn: {
|
||||
backgroundColor: colors.blue3,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
mainBtn: {
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
secondaryBtn: {
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 7,
|
||||
borderRadius: 50,
|
||||
marginLeft: 6,
|
||||
},
|
||||
title: {lineHeight: 38},
|
||||
|
||||
// Word wrapping appears fine on
|
||||
// mobile but overflows on desktop
|
||||
handle: isNative
|
||||
? {}
|
||||
: {
|
||||
// @ts-ignore web only -prf
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
invalidHandle: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
|
||||
handleLine: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
metricsLine: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
description: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
detailLine: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 5,
|
||||
},
|
||||
|
||||
pill: {
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
|
||||
br40: {borderRadius: 40},
|
||||
br50: {borderRadius: 50},
|
||||
})
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
|
||||
} from '#/components/icons/Chevron'
|
||||
import {ScreenHider} from '../com/util/moderation/ScreenHider'
|
||||
import {ProfileHeader} from '../com/profile/ProfileHeader'
|
||||
import {ProfileHeader} from '#/screens/Profile/Header'
|
||||
import {ProfileCard} from '../com/profile/ProfileCard'
|
||||
import {FeedItem} from '../com/posts/FeedItem'
|
||||
import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem'
|
||||
|
||||
+12
-440
@@ -1,5 +1,5 @@
|
||||
import React, {useMemo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
@@ -7,54 +7,38 @@ import {
|
||||
ModerationOpts,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {CenteredView, ScrollView} from '../com/util/Views'
|
||||
import {CenteredView} from '../com/util/Views'
|
||||
import {ListRef} from '../com/util/List'
|
||||
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
|
||||
import {Feed} from 'view/com/posts/Feed'
|
||||
import {ProfileLists} from '../com/lists/ProfileLists'
|
||||
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
|
||||
import {ProfileHeader, ProfileHeaderLoading} from '../com/profile/ProfileHeader'
|
||||
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
|
||||
import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
||||
import {EmptyState} from '../com/util/EmptyState'
|
||||
import {FAB} from '../com/util/fab/FAB'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {ComposeIcon2} from 'lib/icons'
|
||||
import {useSetTitle} from 'lib/hooks/useSetTitle'
|
||||
import {combinedDisplayName} from 'lib/strings/display-names'
|
||||
import {
|
||||
FeedDescriptor,
|
||||
resetProfilePostsQueries,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
|
||||
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useSession, getAgent} from '#/state/session'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {useProfileExtraInfoQuery} from '#/state/queries/profile-extra-info'
|
||||
import {useLabelerInfoQuery} from '#/state/queries/labeler'
|
||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||
|
||||
import {useTheme, atoms as a} from '#/alf'
|
||||
import * as ModerationServiceCard from '#/components/ModerationServiceCard'
|
||||
import {RaisingHande4Finger_Stroke2_Corner0_Rounded as RaisingHand} from '#/components/icons/RaisingHand'
|
||||
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
|
||||
import {ProfileContentFiltersSection} from '#/screens/Profile/Sections/ContentFilters'
|
||||
import {ProfileHeader as ProfileHeaderV2} from '#/screens/Profile/Header'
|
||||
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
|
||||
|
||||
interface SectionRef {
|
||||
scrollToTop: () => void
|
||||
@@ -119,7 +103,7 @@ export function ProfileScreen({route}: Props) {
|
||||
}
|
||||
if (profile && moderationOpts) {
|
||||
return (
|
||||
<ProfileScreenLoadedV2
|
||||
<ProfileScreenLoaded
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
isPlaceholderProfile={isPlaceholderProfile}
|
||||
@@ -149,298 +133,6 @@ function ProfileScreenLoaded({
|
||||
moderationOpts: ModerationOpts
|
||||
hideBackButton: boolean
|
||||
isPlaceholderProfile: boolean
|
||||
}) {
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {openComposer} = useComposerControls()
|
||||
const {screen, track} = useAnalytics()
|
||||
const [currentPage, setCurrentPage] = React.useState(0)
|
||||
const {_} = useLingui()
|
||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
const extraInfoQuery = useProfileExtraInfoQuery(profile.did)
|
||||
const postsSectionRef = React.useRef<SectionRef>(null)
|
||||
const repliesSectionRef = React.useRef<SectionRef>(null)
|
||||
const mediaSectionRef = React.useRef<SectionRef>(null)
|
||||
const likesSectionRef = React.useRef<SectionRef>(null)
|
||||
const feedsSectionRef = React.useRef<SectionRef>(null)
|
||||
const listsSectionRef = React.useRef<SectionRef>(null)
|
||||
const moderationSectionRef = React.useRef<SectionRef>(null)
|
||||
|
||||
useSetTitle(combinedDisplayName(profile))
|
||||
|
||||
const description = profile.description ?? ''
|
||||
const hasDescription = description !== ''
|
||||
const [descriptionRT, isResolvingDescriptionRT] = useRichText(description)
|
||||
const showPlaceholder = isPlaceholderProfile || isResolvingDescriptionRT
|
||||
const moderation = useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
[profile, moderationOpts],
|
||||
)
|
||||
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
const showRepliesTab = hasSession
|
||||
const showLikesTab = isMe
|
||||
const showFeedsTab = hasSession && (isMe || extraInfoQuery.data?.hasFeedgens)
|
||||
const showListsTab = hasSession && (isMe || extraInfoQuery.data?.hasLists)
|
||||
const showModerationTab =
|
||||
hasSession && (isMe || true) /* TODO true should be associated modservice */
|
||||
|
||||
const sectionTitles = useMemo<string[]>(() => {
|
||||
return [
|
||||
_(msg`Posts`),
|
||||
showRepliesTab ? _(msg`Replies`) : undefined,
|
||||
_(msg`Media`),
|
||||
showLikesTab ? _(msg`Likes`) : undefined,
|
||||
showFeedsTab ? _(msg`Feeds`) : undefined,
|
||||
showListsTab ? _(msg`Lists`) : undefined,
|
||||
showModerationTab ? _(msg`Moderation`) : undefined,
|
||||
].filter(Boolean) as string[]
|
||||
}, [
|
||||
showRepliesTab,
|
||||
showLikesTab,
|
||||
showFeedsTab,
|
||||
showListsTab,
|
||||
showModerationTab,
|
||||
_,
|
||||
])
|
||||
|
||||
let nextIndex = 0
|
||||
const postsIndex = nextIndex++
|
||||
let repliesIndex: number | null = null
|
||||
if (showRepliesTab) {
|
||||
repliesIndex = nextIndex++
|
||||
}
|
||||
const mediaIndex = nextIndex++
|
||||
let likesIndex: number | null = null
|
||||
if (showLikesTab) {
|
||||
likesIndex = nextIndex++
|
||||
}
|
||||
let feedsIndex: number | null = null
|
||||
if (showFeedsTab) {
|
||||
feedsIndex = nextIndex++
|
||||
}
|
||||
let listsIndex: number | null = null
|
||||
if (showListsTab) {
|
||||
listsIndex = nextIndex++
|
||||
}
|
||||
let moderationIndex: number | null = null
|
||||
if (showModerationTab) {
|
||||
moderationIndex = nextIndex++
|
||||
}
|
||||
|
||||
const scrollSectionToTop = React.useCallback(
|
||||
(index: number) => {
|
||||
if (index === postsIndex) {
|
||||
postsSectionRef.current?.scrollToTop()
|
||||
} else if (index === repliesIndex) {
|
||||
repliesSectionRef.current?.scrollToTop()
|
||||
} else if (index === mediaIndex) {
|
||||
mediaSectionRef.current?.scrollToTop()
|
||||
} else if (index === likesIndex) {
|
||||
likesSectionRef.current?.scrollToTop()
|
||||
} else if (index === feedsIndex) {
|
||||
feedsSectionRef.current?.scrollToTop()
|
||||
} else if (index === listsIndex) {
|
||||
listsSectionRef.current?.scrollToTop()
|
||||
} else if (index === moderationIndex) {
|
||||
moderationSectionRef.current?.scrollToTop()
|
||||
}
|
||||
},
|
||||
[
|
||||
postsIndex,
|
||||
repliesIndex,
|
||||
mediaIndex,
|
||||
likesIndex,
|
||||
feedsIndex,
|
||||
listsIndex,
|
||||
moderationIndex,
|
||||
],
|
||||
)
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
screen('Profile')
|
||||
return listenSoftReset(() => {
|
||||
scrollSectionToTop(currentPage)
|
||||
})
|
||||
}, [setMinimalShellMode, screen, currentPage, scrollSectionToTop]),
|
||||
)
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setDrawerSwipeDisabled(currentPage > 0)
|
||||
return () => {
|
||||
setDrawerSwipeDisabled(false)
|
||||
}
|
||||
}, [setDrawerSwipeDisabled, currentPage]),
|
||||
)
|
||||
|
||||
// events
|
||||
// =
|
||||
|
||||
const onPressCompose = React.useCallback(() => {
|
||||
track('ProfileScreen:PressCompose')
|
||||
const mention =
|
||||
profile.handle === currentAccount?.handle ||
|
||||
isInvalidHandle(profile.handle)
|
||||
? undefined
|
||||
: profile.handle
|
||||
openComposer({mention})
|
||||
}, [openComposer, currentAccount, track, profile])
|
||||
|
||||
const onPageSelected = React.useCallback(
|
||||
(i: number) => {
|
||||
setCurrentPage(i)
|
||||
},
|
||||
[setCurrentPage],
|
||||
)
|
||||
|
||||
const onCurrentPageSelected = React.useCallback(
|
||||
(index: number) => {
|
||||
scrollSectionToTop(index)
|
||||
},
|
||||
[scrollSectionToTop],
|
||||
)
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
const renderHeader = React.useCallback(() => {
|
||||
return (
|
||||
<ProfileHeader
|
||||
profile={profile}
|
||||
descriptionRT={hasDescription ? descriptionRT : null}
|
||||
moderationOpts={moderationOpts}
|
||||
hideBackButton={hideBackButton}
|
||||
isPlaceholderProfile={showPlaceholder}
|
||||
/>
|
||||
)
|
||||
}, [
|
||||
profile,
|
||||
descriptionRT,
|
||||
hasDescription,
|
||||
moderationOpts,
|
||||
hideBackButton,
|
||||
showPlaceholder,
|
||||
])
|
||||
|
||||
return (
|
||||
<ScreenHider
|
||||
testID="profileView"
|
||||
style={styles.container}
|
||||
screenDescription="profile"
|
||||
modui={moderation.ui('profileView')}>
|
||||
<PagerWithHeader
|
||||
testID="profilePager"
|
||||
isHeaderReady={!showPlaceholder}
|
||||
items={sectionTitles}
|
||||
onPageSelected={onPageSelected}
|
||||
onCurrentPageSelected={onCurrentPageSelected}
|
||||
renderHeader={renderHeader}>
|
||||
{({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={postsSectionRef}
|
||||
feed={`author|${profile.did}|posts_and_author_threads`}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
/>
|
||||
)}
|
||||
{showRepliesTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={repliesSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_replies`}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={mediaSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_media`}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
/>
|
||||
)}
|
||||
{showLikesTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={likesSectionRef}
|
||||
feed={`likes|${profile.did}`}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
ignoreFilterFor={profile.did}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{showFeedsTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileFeedgens
|
||||
ref={feedsSectionRef}
|
||||
did={profile.did}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
headerOffset={headerHeight}
|
||||
enabled={isFocused}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{showListsTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileLists
|
||||
ref={listsSectionRef}
|
||||
did={profile.did}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
headerOffset={headerHeight}
|
||||
enabled={isFocused}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{showModerationTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<ModerationSection
|
||||
// ref={moderationSectionRef}
|
||||
did={profile.did}
|
||||
scrollElRef={scrollElRef as ListRef}
|
||||
headerOffset={headerHeight}
|
||||
enabled={isFocused}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</PagerWithHeader>
|
||||
{hasSession && (
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
)}
|
||||
</ScreenHider>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileScreenLoadedV2({
|
||||
profile: profileUnshadowed,
|
||||
isPlaceholderProfile,
|
||||
moderationOpts,
|
||||
hideBackButton,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
moderationOpts: ModerationOpts
|
||||
hideBackButton: boolean
|
||||
isPlaceholderProfile: boolean
|
||||
}) {
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
@@ -618,7 +310,7 @@ function ProfileScreenLoadedV2({
|
||||
|
||||
const renderHeader = React.useCallback(() => {
|
||||
return (
|
||||
<ProfileHeaderV2
|
||||
<ProfileHeader
|
||||
profile={profile}
|
||||
modservice={modServiceQuery.data}
|
||||
descriptionRT={hasDescription ? descriptionRT : null}
|
||||
@@ -675,7 +367,7 @@ function ProfileScreenLoadedV2({
|
||||
: null}
|
||||
{showPostsTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
<ProfileFeedSection
|
||||
ref={postsSectionRef}
|
||||
feed={`author|${profile.did}|posts_and_author_threads`}
|
||||
headerHeight={headerHeight}
|
||||
@@ -687,7 +379,7 @@ function ProfileScreenLoadedV2({
|
||||
: null}
|
||||
{showRepliesTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
<ProfileFeedSection
|
||||
ref={repliesSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_replies`}
|
||||
headerHeight={headerHeight}
|
||||
@@ -699,7 +391,7 @@ function ProfileScreenLoadedV2({
|
||||
: null}
|
||||
{showMediaTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
<ProfileFeedSection
|
||||
ref={mediaSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_media`}
|
||||
headerHeight={headerHeight}
|
||||
@@ -711,7 +403,7 @@ function ProfileScreenLoadedV2({
|
||||
: null}
|
||||
{showLikesTab
|
||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||
<FeedSection
|
||||
<ProfileFeedSection
|
||||
ref={likesSectionRef}
|
||||
feed={`likes|${profile.did}`}
|
||||
headerHeight={headerHeight}
|
||||
@@ -758,126 +450,6 @@ function ProfileScreenLoadedV2({
|
||||
)
|
||||
}
|
||||
|
||||
interface FeedSectionProps {
|
||||
feed: FeedDescriptor
|
||||
headerHeight: number
|
||||
isFocused: boolean
|
||||
scrollElRef: ListRef
|
||||
ignoreFilterFor?: string
|
||||
}
|
||||
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
|
||||
function FeedSectionImpl(
|
||||
{feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
|
||||
ref,
|
||||
) {
|
||||
const {_} = useLingui()
|
||||
const queryClient = useQueryClient()
|
||||
const [hasNew, setHasNew] = React.useState(false)
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||
|
||||
const onScrollToTop = React.useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({
|
||||
animated: isNative,
|
||||
offset: -headerHeight,
|
||||
})
|
||||
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
||||
setHasNew(false)
|
||||
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
scrollToTop: onScrollToTop,
|
||||
}))
|
||||
|
||||
const renderPostsEmpty = React.useCallback(() => {
|
||||
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
|
||||
}, [_])
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Feed
|
||||
testID="postsFeed"
|
||||
enabled={isFocused}
|
||||
feed={feed}
|
||||
scrollElRef={scrollElRef}
|
||||
onHasNew={setHasNew}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
renderEmptyState={renderPostsEmpty}
|
||||
headerOffset={headerHeight}
|
||||
renderEndOfFeed={ProfileEndOfFeed}
|
||||
ignoreFilterFor={ignoreFilterFor}
|
||||
/>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onScrollToTop}
|
||||
label={_(msg`Load new posts`)}
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function ProfileEndOfFeed() {
|
||||
const pal = usePalette('default')
|
||||
|
||||
return (
|
||||
<View style={[pal.border, {paddingTop: 32, borderTopWidth: 1}]}>
|
||||
<Text style={[pal.textLight, pal.border, {textAlign: 'center'}]}>
|
||||
<Trans>End of feed</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ModerationSection({did}: {did: string}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<ScrollView>
|
||||
<ModerationServiceCard.Loader
|
||||
did={did}
|
||||
component={({modservice}) => (
|
||||
<ModerationServiceCard.Link modservice={modservice}>
|
||||
{ctx => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_md,
|
||||
a.p_md,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
...(ctx.focused || ctx.hovered
|
||||
? [t.atoms.bg_contrast_25]
|
||||
: []),
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
{backgroundColor: t.palette.negative_25},
|
||||
a.p_lg,
|
||||
a.rounded_sm,
|
||||
]}>
|
||||
<RaisingHand
|
||||
width={36}
|
||||
style={[a.z_10]}
|
||||
fill={t.palette.negative_500}
|
||||
/>
|
||||
</View>
|
||||
<ModerationServiceCard.Card.Content
|
||||
title="Moderation service"
|
||||
description={modservice.description}
|
||||
handle={modservice.creator.handle}
|
||||
likeCount={modservice.likeCount}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ModerationServiceCard.Link>
|
||||
)}
|
||||
/>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
function useRichText(text: string): [RichTextAPI, boolean] {
|
||||
const [prevText, setPrevText] = React.useState(text)
|
||||
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
|
||||
|
||||
Reference in New Issue
Block a user