mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 343 lines 12 kB view raw
1import React, {memo, useMemo} from 'react' 2import {View} from 'react-native' 3import { 4 AppBskyActorDefs, 5 AppBskyLabelerDefs, 6 moderateProfile, 7 ModerationOpts, 8 RichText as RichTextAPI, 9} from '@atproto/api' 10import {msg, Plural, plural, Trans} from '@lingui/macro' 11import {useLingui} from '@lingui/react' 12 13// eslint-disable-next-line @typescript-eslint/no-unused-vars 14import {MAX_LABELERS} from '#/lib/constants' 15import {isAppLabeler} from '#/lib/moderation' 16import {logger} from '#/logger' 17import {Shadow} from '#/state/cache/types' 18import {useModalControls} from '#/state/modals' 19import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' 20import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' 21import {usePreferencesQuery} from '#/state/queries/preferences' 22import {useRequireAuth, useSession} from '#/state/session' 23import {useAnalytics} from 'lib/analytics/analytics' 24import {useHaptics} from 'lib/haptics' 25import {isIOS} from 'platform/detection' 26import {useProfileShadow} from 'state/cache/profile-shadow' 27import {ProfileMenu} from '#/view/com/profile/ProfileMenu' 28import * as Toast from '#/view/com/util/Toast' 29import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' 30import {Button, ButtonText} from '#/components/Button' 31import {DialogOuterProps} from '#/components/Dialog' 32import { 33 Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, 34 Heart2_Stroke2_Corner0_Rounded as Heart, 35} from '#/components/icons/Heart2' 36import {Link} from '#/components/Link' 37import * as Prompt from '#/components/Prompt' 38import {RichText} from '#/components/RichText' 39import {Text} from '#/components/Typography' 40import {ProfileHeaderDisplayName} from './DisplayName' 41import {ProfileHeaderHandle} from './Handle' 42import {ProfileHeaderMetrics} from './Metrics' 43import {ProfileHeaderShell} from './Shell' 44 45interface Props { 46 profile: AppBskyActorDefs.ProfileViewDetailed 47 labeler: AppBskyLabelerDefs.LabelerViewDetailed 48 descriptionRT: RichTextAPI | null 49 moderationOpts: ModerationOpts 50 hideBackButton?: boolean 51 isPlaceholderProfile?: boolean 52} 53 54let ProfileHeaderLabeler = ({ 55 profile: profileUnshadowed, 56 labeler, 57 descriptionRT, 58 moderationOpts, 59 hideBackButton = false, 60 isPlaceholderProfile, 61}: Props): React.ReactNode => { 62 const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> = 63 useProfileShadow(profileUnshadowed) 64 const t = useTheme() 65 const {gtMobile} = useBreakpoints() 66 const {_} = useLingui() 67 const {currentAccount, hasSession} = useSession() 68 const {openModal} = useModalControls() 69 const {track} = useAnalytics() 70 const requireAuth = useRequireAuth() 71 const playHaptic = useHaptics() 72 const cantSubscribePrompt = Prompt.usePromptControl() 73 const isSelf = currentAccount?.did === profile.did 74 75 const moderation = useMemo( 76 () => moderateProfile(profile, moderationOpts), 77 [profile, moderationOpts], 78 ) 79 const {data: preferences} = usePreferencesQuery() 80 const { 81 mutateAsync: toggleSubscription, 82 variables, 83 reset, 84 } = useLabelerSubscriptionMutation() 85 const isSubscribed = 86 variables?.subscribe ?? 87 preferences?.moderationPrefs.labelers.find(l => l.did === profile.did) 88 const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation() 89 const {mutateAsync: unlikeMod, isPending: isUnlikePending} = 90 useUnlikeMutation() 91 const [likeUri, setLikeUri] = React.useState<string>( 92 labeler.viewer?.like || '', 93 ) 94 const [likeCount, setLikeCount] = React.useState(labeler.likeCount || 0) 95 96 const onToggleLiked = React.useCallback(async () => { 97 if (!labeler) { 98 return 99 } 100 try { 101 playHaptic() 102 103 if (likeUri) { 104 await unlikeMod({uri: likeUri}) 105 track('CustomFeed:Unlike') 106 setLikeCount(c => c - 1) 107 setLikeUri('') 108 } else { 109 const res = await likeMod({uri: labeler.uri, cid: labeler.cid}) 110 track('CustomFeed:Like') 111 setLikeCount(c => c + 1) 112 setLikeUri(res.uri) 113 } 114 } catch (e: any) { 115 Toast.show( 116 _( 117 msg`There was an an issue contacting the server, please check your internet connection and try again.`, 118 ), 119 'xmark', 120 ) 121 logger.error(`Failed to toggle labeler like`, {message: e.message}) 122 } 123 }, [labeler, playHaptic, likeUri, unlikeMod, track, likeMod, _]) 124 125 const onPressEditProfile = React.useCallback(() => { 126 track('ProfileHeader:EditProfileButtonClicked') 127 openModal({ 128 name: 'edit-profile', 129 profile, 130 }) 131 }, [track, openModal, profile]) 132 133 const onPressSubscribe = React.useCallback( 134 () => 135 requireAuth(async (): Promise<void> => { 136 try { 137 await toggleSubscription({ 138 did: profile.did, 139 subscribe: !isSubscribed, 140 }) 141 } catch (e: any) { 142 reset() 143 if (e.message === 'MAX_LABELERS') { 144 cantSubscribePrompt.open() 145 return 146 } 147 logger.error(`Failed to subscribe to labeler`, {message: e.message}) 148 } 149 }), 150 [ 151 requireAuth, 152 toggleSubscription, 153 isSubscribed, 154 profile, 155 cantSubscribePrompt, 156 reset, 157 ], 158 ) 159 160 const isMe = React.useMemo( 161 () => currentAccount?.did === profile.did, 162 [currentAccount, profile], 163 ) 164 165 return ( 166 <ProfileHeaderShell 167 profile={profile} 168 moderation={moderation} 169 hideBackButton={hideBackButton} 170 isPlaceholderProfile={isPlaceholderProfile}> 171 <View 172 style={[a.px_lg, a.pt_md, a.pb_sm]} 173 pointerEvents={isIOS ? 'auto' : 'box-none'}> 174 <View 175 style={[a.flex_row, a.justify_end, a.gap_sm, a.pb_lg]} 176 pointerEvents={isIOS ? 'auto' : 'box-none'}> 177 {isMe ? ( 178 <Button 179 testID="profileHeaderEditProfileButton" 180 size="small" 181 color="secondary" 182 variant="solid" 183 onPress={onPressEditProfile} 184 label={_(msg`Edit profile`)} 185 style={a.rounded_full}> 186 <ButtonText> 187 <Trans>Edit Profile</Trans> 188 </ButtonText> 189 </Button> 190 ) : !isAppLabeler(profile.did) ? ( 191 <> 192 <Button 193 testID="toggleSubscribeBtn" 194 label={ 195 isSubscribed 196 ? _(msg`Unsubscribe from this labeler`) 197 : _(msg`Subscribe to this labeler`) 198 } 199 onPress={onPressSubscribe}> 200 {state => ( 201 <View 202 style={[ 203 { 204 paddingVertical: gtMobile ? 12 : 10, 205 backgroundColor: isSubscribed 206 ? state.hovered || state.pressed 207 ? t.palette.contrast_50 208 : t.palette.contrast_25 209 : state.hovered || state.pressed 210 ? tokens.color.temp_purple_dark 211 : tokens.color.temp_purple, 212 }, 213 a.px_lg, 214 a.rounded_sm, 215 a.gap_sm, 216 ]}> 217 <Text 218 style={[ 219 { 220 color: isSubscribed 221 ? t.palette.contrast_700 222 : t.palette.white, 223 }, 224 a.font_bold, 225 a.text_center, 226 ]}> 227 {isSubscribed ? ( 228 <Trans>Unsubscribe</Trans> 229 ) : ( 230 <Trans>Subscribe to Labeler</Trans> 231 )} 232 </Text> 233 </View> 234 )} 235 </Button> 236 </> 237 ) : null} 238 <ProfileMenu profile={profile} /> 239 </View> 240 <View style={[a.flex_col, a.gap_xs, a.pb_md]}> 241 <ProfileHeaderDisplayName profile={profile} moderation={moderation} /> 242 <ProfileHeaderHandle profile={profile} /> 243 </View> 244 {!isPlaceholderProfile && ( 245 <> 246 {isSelf && <ProfileHeaderMetrics profile={profile} />} 247 {descriptionRT && !moderation.ui('profileView').blur ? ( 248 <View pointerEvents="auto"> 249 <RichText 250 testID="profileHeaderDescription" 251 style={[a.text_md]} 252 numberOfLines={15} 253 value={descriptionRT} 254 enableTags 255 authorHandle={profile.handle} 256 /> 257 </View> 258 ) : undefined} 259 {!isAppLabeler(profile.did) && ( 260 <View style={[a.flex_row, a.gap_xs, a.align_center, a.pt_lg]}> 261 <Button 262 testID="toggleLikeBtn" 263 size="small" 264 color="secondary" 265 variant="solid" 266 shape="round" 267 label={_(msg`Like this feed`)} 268 disabled={!hasSession || isLikePending || isUnlikePending} 269 onPress={onToggleLiked}> 270 {likeUri ? ( 271 <HeartFilled fill={t.palette.negative_400} /> 272 ) : ( 273 <Heart fill={t.atoms.text_contrast_medium.color} /> 274 )} 275 </Button> 276 277 {typeof likeCount === 'number' && ( 278 <Link 279 to={{ 280 screen: 'ProfileLabelerLikedBy', 281 params: { 282 name: labeler.creator.handle || labeler.creator.did, 283 }, 284 }} 285 size="tiny" 286 label={plural(likeCount, { 287 one: 'Liked by # user', 288 other: 'Liked by # users', 289 })}> 290 {({hovered, focused, pressed}) => ( 291 <Text 292 style={[ 293 a.font_bold, 294 a.text_sm, 295 t.atoms.text_contrast_medium, 296 (hovered || focused || pressed) && 297 t.atoms.text_contrast_high, 298 ]}> 299 <Plural 300 value={likeCount} 301 one="Liked by # user" 302 other="Liked by # users" 303 /> 304 </Text> 305 )} 306 </Link> 307 )} 308 </View> 309 )} 310 </> 311 )} 312 </View> 313 <CantSubscribePrompt control={cantSubscribePrompt} /> 314 </ProfileHeaderShell> 315 ) 316} 317ProfileHeaderLabeler = memo(ProfileHeaderLabeler) 318export {ProfileHeaderLabeler} 319 320/** 321 * Keep this in sync with the value of {@link MAX_LABELERS} 322 */ 323function CantSubscribePrompt({ 324 control, 325}: { 326 control: DialogOuterProps['control'] 327}) { 328 const {_} = useLingui() 329 return ( 330 <Prompt.Outer control={control}> 331 <Prompt.TitleText>Unable to subscribe</Prompt.TitleText> 332 <Prompt.DescriptionText> 333 <Trans> 334 We're sorry! You can only subscribe to twenty labelers, and you've 335 reached your limit of twenty. 336 </Trans> 337 </Prompt.DescriptionText> 338 <Prompt.Actions> 339 <Prompt.Action onPress={() => control.close()} cta={_(msg`OK`)} /> 340 </Prompt.Actions> 341 </Prompt.Outer> 342 ) 343}