From 09b0a206c2f0d01a3c55a48bf89deccff89b7083 Mon Sep 17 00:00:00 2001 From: Nate Isern Alvarez Date: Sat, 21 Dec 2024 03:53:10 +0100 Subject: [PATCH] Remove atproto/api and use fake data on profile screen Remove `atproto/api` imports and use fake data on the profile screen. * Replace `AppBskyActorDefs`, `AppBskyLabelerDefs`, `ModerationOpts`, and `RichText` imports with `FAKE_PROFILE_DATA` in `src/screens/Profile/Header/ProfileHeaderLabeler.tsx` and `src/screens/Profile/Header/ProfileHeaderStandard.tsx`. * Add new files `src/screens/Profile/Header/ProfileHeaderShell.tsx`, `src/screens/Profile/Header/ProfileHeaderMetrics.tsx`, `src/screens/Profile/Header/ProfileHeaderHandle.tsx`, and `src/screens/Profile/Header/ProfileHeaderDisplayName.tsx` to use `FAKE_PROFILE_DATA`. * Add `src/screens/Profile/fakeData.ts` to define the `FAKE_PROFILE_DATA` constant with fake profile data. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/OxyHQ/bMention?shareId=XXXX-XXXX-XXXX-XXXX). --- .../Header/ProfileHeaderDisplayName.tsx | 57 +++++ .../Profile/Header/ProfileHeaderHandle.tsx | 56 +++++ .../Profile/Header/ProfileHeaderLabeler.tsx | 18 +- .../Profile/Header/ProfileHeaderMetrics.tsx | 46 ++++ .../Profile/Header/ProfileHeaderShell.tsx | 229 ++++++++++++++++++ .../Profile/Header/ProfileHeaderStandard.tsx | 15 +- src/screens/Profile/fakeData.ts | 52 ++++ 7 files changed, 451 insertions(+), 22 deletions(-) create mode 100644 src/screens/Profile/Header/ProfileHeaderDisplayName.tsx create mode 100644 src/screens/Profile/Header/ProfileHeaderHandle.tsx create mode 100644 src/screens/Profile/Header/ProfileHeaderMetrics.tsx create mode 100644 src/screens/Profile/Header/ProfileHeaderShell.tsx create mode 100644 src/screens/Profile/fakeData.ts diff --git a/src/screens/Profile/Header/ProfileHeaderDisplayName.tsx b/src/screens/Profile/Header/ProfileHeaderDisplayName.tsx new file mode 100644 index 0000000..067a5a1 --- /dev/null +++ b/src/screens/Profile/Header/ProfileHeaderDisplayName.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import {View} from 'react-native' +import {FAKE_PROFILE_DATA} from 'src/screens/Profile/fakeData.ts' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isInvalidHandle} from '#/lib/strings/handles' +import {isIOS} from '#/platform/detection' +import {Shadow} from '#/state/cache/types' +import {atoms as a, useTheme, web} from '#/alf' +import {NewskieDialog} from '#/components/NewskieDialog' +import {Text} from '#/components/Typography' + +export function ProfileHeaderDisplayName({ + profile, + disableTaps, +}: { + profile: Shadow + disableTaps?: boolean +}) { + const t = useTheme() + const {_} = useLingui() + const invalidHandle = isInvalidHandle(profile.handle) + const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy + return ( + + + {profile.viewer?.followedBy && !blockHide ? ( + + + Follows you + + + ) : undefined} + + {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} + + + ) +} diff --git a/src/screens/Profile/Header/ProfileHeaderHandle.tsx b/src/screens/Profile/Header/ProfileHeaderHandle.tsx new file mode 100644 index 0000000..6b36021 --- /dev/null +++ b/src/screens/Profile/Header/ProfileHeaderHandle.tsx @@ -0,0 +1,56 @@ +import {View} from 'react-native' +import {FAKE_PROFILE_DATA} from 'src/screens/Profile/fakeData.ts' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isInvalidHandle} from '#/lib/strings/handles' +import {isIOS} from '#/platform/detection' +import {Shadow} from '#/state/cache/types' +import {atoms as a, useTheme, web} from '#/alf' +import {NewskieDialog} from '#/components/NewskieDialog' +import {Text} from '#/components/Typography' + +export function ProfileHeaderHandle({ + profile, + disableTaps, +}: { + profile: Shadow + disableTaps?: boolean +}) { + const t = useTheme() + const {_} = useLingui() + const invalidHandle = isInvalidHandle(profile.handle) + const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy + return ( + + + {profile.viewer?.followedBy && !blockHide ? ( + + + Follows you + + + ) : undefined} + + {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} + + + ) +} diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 1725c4a..dfa43e8 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -1,12 +1,6 @@ import React, {memo, useMemo} from 'react' import {View} from 'react-native' -import { - AppBskyActorDefs, - AppBskyLabelerDefs, - moderateProfile, - ModerationOpts, - RichText as RichTextAPI, -} from '@atproto/api' +import {FAKE_PROFILE_DATA} from 'src/screens/Profile/fakeData.ts' import {msg, Plural, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -43,10 +37,10 @@ import {ProfileHeaderMetrics} from './Metrics' import {ProfileHeaderShell} from './Shell' interface Props { - profile: AppBskyActorDefs.ProfileViewDetailed - labeler: AppBskyLabelerDefs.LabelerViewDetailed - descriptionRT: RichTextAPI | null - moderationOpts: ModerationOpts + profile: FAKE_PROFILE_DATA.ProfileViewDetailed + labeler: FAKE_PROFILE_DATA.LabelerViewDetailed + descriptionRT: FAKE_PROFILE_DATA.RichText | null + moderationOpts: FAKE_PROFILE_DATA.ModerationOpts hideBackButton?: boolean isPlaceholderProfile?: boolean } @@ -59,7 +53,7 @@ let ProfileHeaderLabeler = ({ hideBackButton = false, isPlaceholderProfile, }: Props): React.ReactNode => { - const profile: Shadow = + const profile: Shadow = useProfileShadow(profileUnshadowed) const t = useTheme() const {_} = useLingui() diff --git a/src/screens/Profile/Header/ProfileHeaderMetrics.tsx b/src/screens/Profile/Header/ProfileHeaderMetrics.tsx new file mode 100644 index 0000000..0ffb5cb --- /dev/null +++ b/src/screens/Profile/Header/ProfileHeaderMetrics.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import {View} from 'react-native' +import {FAKE_PROFILE_DATA} from 'src/screens/Profile/fakeData.ts' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' + +export function ProfileHeaderMetrics({ + profile, +}: { + profile: FAKE_PROFILE_DATA.ProfileViewDetailed +}) { + const t = useTheme() + const {_} = useLingui() + + return ( + + + + {profile.followersCount} + + + Followers + + + + + {profile.followsCount} + + + Following + + + + + {profile.postsCount} + + + Posts + + + + ) +} diff --git a/src/screens/Profile/Header/ProfileHeaderShell.tsx b/src/screens/Profile/Header/ProfileHeaderShell.tsx new file mode 100644 index 0000000..6587911 --- /dev/null +++ b/src/screens/Profile/Header/ProfileHeaderShell.tsx @@ -0,0 +1,229 @@ +import React, {memo} from 'react' +import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' +import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {FAKE_PROFILE_DATA} from 'src/screens/Profile/fakeData.ts' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' + +import {BACK_HITSLOP} from '#/lib/constants' +import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' +import {NavigationProp} from '#/lib/routes/types' +import {isIOS} from '#/platform/detection' +import {Shadow} from '#/state/cache/types' +import {useLightboxControls} from '#/state/lightbox' +import {useSession} from '#/state/session' +import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {UserBanner} from '#/view/com/util/UserBanner' +import {atoms as a, platform, useTheme} from '#/alf' +import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' +import {LabelsOnMe} from '#/components/moderation/LabelsOnMe' +import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts' +import {GrowableAvatar} from './GrowableAvatar' +import {GrowableBanner} from './GrowableBanner' +import {StatusBarShadow} from './StatusBarShadow' + +interface Props { + profile: Shadow + moderation: FAKE_PROFILE_DATA.ModerationDecision + hideBackButton?: boolean + isPlaceholderProfile?: boolean +} + +let ProfileHeaderShell = ({ + children, + profile, + moderation, + hideBackButton = false, + isPlaceholderProfile, +}: React.PropsWithChildren): React.ReactNode => { + const t = useTheme() + const {currentAccount} = useSession() + const {_} = useLingui() + const {openLightbox} = useLightboxControls() + const navigation = useNavigation() + const {top: topInset} = useSafeAreaInsets() + + const aviRef = useHandleRef() + + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, [navigation]) + + const _openLightbox = React.useCallback( + (uri: string, thumbRect: MeasuredDimensions | null) => { + openLightbox({ + images: [ + { + uri, + thumbUri: uri, + thumbRect, + dimensions: { + // It's fine if it's actually smaller but we know it's 1:1. + height: 1000, + width: 1000, + }, + thumbDimensions: null, + type: 'circle-avi', + }, + ], + index: 0, + }) + }, + [openLightbox], + ) + + const onPressAvi = React.useCallback(() => { + const modui = moderation.ui('avatar') + const avatar = profile.avatar + if (avatar && !(modui.blur && modui.noOverride)) { + const aviHandle = aviRef.current + runOnUI(() => { + 'worklet' + const rect = measureHandle(aviHandle) + runOnJS(_openLightbox)(avatar, rect) + })() + } + }, [profile, moderation, _openLightbox, aviRef]) + + const isMe = React.useMemo( + () => currentAccount?.did === profile.did, + [currentAccount, profile], + ) + + return ( + + + + + {!hideBackButton && ( + + + + + + )} + + }> + {isPlaceholderProfile ? ( + + ) : ( + + )} + + + + {children} + + {!isPlaceholderProfile && ( + + {isMe ? ( + + ) : ( + + )} + + )} + + + + + + + + + + + + ) +} +ProfileHeaderShell = memo(ProfileHeaderShell) +export {ProfileHeaderShell} + +const styles = StyleSheet.create({ + backBtnWrapper: { + position: 'absolute', + left: 10, + width: 30, + height: 30, + overflow: 'hidden', + borderRadius: 15, + // @ts-ignore web only + cursor: 'pointer', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + alignItems: 'center', + justifyContent: 'center', + }, + backBtn: { + width: 30, + height: 30, + borderRadius: 15, + alignItems: 'center', + justifyContent: 'center', + }, + aviPosition: { + position: 'absolute', + top: 110, + left: 10, + }, + avi: { + width: 94, + height: 94, + borderRadius: 47, + borderWidth: 2, + }, + aviLabeler: { + borderRadius: 10, + }, +}) diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index b2de785..a391b82 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -1,11 +1,6 @@ import React, {memo, useMemo} from 'react' import {View} from 'react-native' -import { - AppBskyActorDefs, - moderateProfile, - ModerationOpts, - RichText as RichTextAPI, -} from '@atproto/api' +import {FAKE_PROFILE_DATA} from 'src/screens/Profile/fakeData.ts' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -41,9 +36,9 @@ import {ProfileHeaderMetrics} from './Metrics' import {ProfileHeaderShell} from './Shell' interface Props { - profile: AppBskyActorDefs.ProfileViewDetailed - descriptionRT: RichTextAPI | null - moderationOpts: ModerationOpts + profile: FAKE_PROFILE_DATA.ProfileViewDetailed + descriptionRT: FAKE_PROFILE_DATA.RichText | null + moderationOpts: FAKE_PROFILE_DATA.ModerationOpts hideBackButton?: boolean isPlaceholderProfile?: boolean } @@ -55,7 +50,7 @@ let ProfileHeaderStandard = ({ hideBackButton = false, isPlaceholderProfile, }: Props): React.ReactNode => { - const profile: Shadow = + const profile: Shadow = useProfileShadow(profileUnshadowed) const {currentAccount, hasSession} = useSession() const {_} = useLingui() diff --git a/src/screens/Profile/fakeData.ts b/src/screens/Profile/fakeData.ts new file mode 100644 index 0000000..11989fa --- /dev/null +++ b/src/screens/Profile/fakeData.ts @@ -0,0 +1,52 @@ +export const FAKE_PROFILE_DATA = { + ProfileViewDetailed: { + did: 'did:example:123', + handle: 'example.handle', + displayName: 'Example User', + description: 'This is a fake profile description.', + avatar: 'https://example.com/avatar.jpg', + banner: 'https://example.com/banner.jpg', + followersCount: 100, + followsCount: 50, + postsCount: 10, + viewer: { + blocking: false, + blockedBy: false, + following: true, + followedBy: true, + }, + associated: { + labeler: true, + feedgens: 2, + lists: 3, + }, + labels: [], + }, + LabelerViewDetailed: { + uri: 'https://example.com/labeler', + cid: 'cid:example:123', + creator: { + did: 'did:example:labeler', + handle: 'labeler.handle', + }, + likeCount: 10, + viewer: { + like: 'like:example:123', + }, + policies: { + labelValues: ['value1', 'value2'], + }, + }, + ModerationOpts: { + blur: false, + noOverride: false, + }, + RichText: class { + constructor({ text }) { + this.text = text; + } + detectFacets() { + return Promise.resolve(); + } + }, +};