mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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