An ATproto social media client -- with an independent Appview.

Replace old ProfileCard with new (#8195)

* Replace usages of old ProfileCard

* Replace Pills with Labels component

* Replace impl of ProfileCardWithFollowButton

* Remove never-used LikesDialog

* Handle missing mod opts

* Add missing profile hover

* use modern button in listmembers

* remove follow button from muted accounts list

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

authored by

Eric Bailey
Samuel Newman
and committed by
GitHub
f46336e3 32c2b69b

+189 -509
-129
src/components/LikesDialog.tsx
··· 1 - import {useCallback, useMemo} from 'react' 2 - import {ActivityIndicator, FlatList, View} from 'react-native' 3 - import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' 4 - import {msg, Trans} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 - 7 - import {cleanError} from '#/lib/strings/errors' 8 - import {logger} from '#/logger' 9 - import {useLikedByQuery} from '#/state/queries/post-liked-by' 10 - import {useResolveUriQuery} from '#/state/queries/resolve-uri' 11 - import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' 12 - import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 13 - import {atoms as a, useTheme} from '#/alf' 14 - import * as Dialog from '#/components/Dialog' 15 - import {Loader} from '#/components/Loader' 16 - import {Text} from '#/components/Typography' 17 - 18 - interface LikesDialogProps { 19 - control: Dialog.DialogOuterProps['control'] 20 - uri: string 21 - } 22 - 23 - export function LikesDialog(props: LikesDialogProps) { 24 - return ( 25 - <Dialog.Outer control={props.control}> 26 - <Dialog.Handle /> 27 - <LikesDialogInner {...props} /> 28 - </Dialog.Outer> 29 - ) 30 - } 31 - 32 - export function LikesDialogInner({control, uri}: LikesDialogProps) { 33 - const {_} = useLingui() 34 - const t = useTheme() 35 - 36 - const { 37 - data: resolvedUri, 38 - error: resolveError, 39 - isFetched: hasFetchedResolvedUri, 40 - } = useResolveUriQuery(uri) 41 - const { 42 - data, 43 - isFetching: isFetchingLikedBy, 44 - isFetched: hasFetchedLikedBy, 45 - isFetchingNextPage, 46 - hasNextPage, 47 - fetchNextPage, 48 - isError, 49 - error: likedByError, 50 - } = useLikedByQuery(resolvedUri?.uri) 51 - 52 - const isLoading = !hasFetchedResolvedUri || !hasFetchedLikedBy 53 - const likes = useMemo(() => { 54 - if (data?.pages) { 55 - return data.pages.flatMap(page => page.likes) 56 - } 57 - return [] 58 - }, [data]) 59 - 60 - const onEndReached = useCallback(async () => { 61 - if (isFetchingLikedBy || !hasNextPage || isError) return 62 - try { 63 - await fetchNextPage() 64 - } catch (err) { 65 - logger.error('Failed to load more likes', {message: err}) 66 - } 67 - }, [isFetchingLikedBy, hasNextPage, isError, fetchNextPage]) 68 - 69 - const renderItem = useCallback( 70 - ({item}: {item: GetLikes.Like}) => { 71 - return ( 72 - <ProfileCardWithFollowBtn 73 - key={item.actor.did} 74 - profile={item.actor} 75 - onPress={() => control.close()} 76 - /> 77 - ) 78 - }, 79 - [control], 80 - ) 81 - 82 - return ( 83 - <Dialog.Inner label={_(msg`Users that have liked this content or profile`)}> 84 - <Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_lg]}> 85 - <Trans>Liked by</Trans> 86 - </Text> 87 - 88 - {isLoading ? ( 89 - <View style={{minHeight: 300}}> 90 - <Loader size="xl" /> 91 - </View> 92 - ) : resolveError || likedByError || !data ? ( 93 - <ErrorMessage message={cleanError(resolveError || likedByError)} /> 94 - ) : likes.length === 0 ? ( 95 - <View style={[t.atoms.bg_contrast_50, a.px_md, a.py_xl, a.rounded_md]}> 96 - <Text style={[a.text_center]}> 97 - <Trans> 98 - Nobody has liked this yet. Maybe you should be the first! 99 - </Trans> 100 - </Text> 101 - </View> 102 - ) : ( 103 - <FlatList 104 - data={likes} 105 - keyExtractor={item => item.actor.did} 106 - onEndReached={onEndReached} 107 - renderItem={renderItem} 108 - initialNumToRender={15} 109 - ListFooterComponent={ 110 - <ListFooterComponent isFetching={isFetchingNextPage} /> 111 - } 112 - /> 113 - )} 114 - 115 - <Dialog.Close /> 116 - </Dialog.Inner> 117 - ) 118 - } 119 - 120 - function ListFooterComponent({isFetching}: {isFetching: boolean}) { 121 - if (isFetching) { 122 - return ( 123 - <View style={a.pt_lg}> 124 - <ActivityIndicator /> 125 - </View> 126 - ) 127 - } 128 - return null 129 - }
+39 -14
src/components/ProfileCard.tsx
··· 8 8 import {msg} from '@lingui/macro' 9 9 import {useLingui} from '@lingui/react' 10 10 11 + import {getModerationCauseKey} from '#/lib/moderation' 11 12 import {type LogEvents} from '#/lib/statsig/statsig' 12 13 import {sanitizeDisplayName} from '#/lib/strings/display-names' 13 14 import {sanitizeHandle} from '#/lib/strings/handles' 14 15 import {useProfileShadow} from '#/state/cache/profile-shadow' 15 16 import {useProfileFollowMutationQueue} from '#/state/queries/profile' 16 17 import {useSession} from '#/state/session' 17 - import {ProfileCardPills} from '#/view/com/profile/ProfileCard' 18 18 import * as Toast from '#/view/com/util/Toast' 19 - import {UserAvatar} from '#/view/com/util/UserAvatar' 19 + import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 20 20 import {atoms as a, useTheme} from '#/alf' 21 21 import { 22 22 Button, ··· 27 27 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 28 28 import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 29 29 import {Link as InternalLink, type LinkProps} from '#/components/Link' 30 + import * as Pills from '#/components/Pills' 30 31 import {RichText} from '#/components/RichText' 31 32 import {Text} from '#/components/Typography' 32 33 import type * as bsky from '#/types/bsky' ··· 35 36 profile, 36 37 moderationOpts, 37 38 logContext = 'ProfileCard', 39 + testID, 38 40 }: { 39 41 profile: bsky.profile.AnyProfileView 40 42 moderationOpts: ModerationOpts 41 43 logContext?: 'ProfileCard' | 'StarterPackProfilesList' 44 + testID?: string 42 45 }) { 43 46 return ( 44 - <Link profile={profile}> 47 + <Link testID={testID} profile={profile}> 45 48 <Card 46 49 profile={profile} 47 50 moderationOpts={moderationOpts} ··· 60 63 moderationOpts: ModerationOpts 61 64 logContext?: 'ProfileCard' | 'StarterPackProfilesList' 62 65 }) { 63 - const moderation = moderateProfile(profile, moderationOpts) 64 - 65 66 return ( 66 67 <Outer> 67 68 <Header> ··· 74 75 /> 75 76 </Header> 76 77 77 - <ProfileCardPills 78 - followedBy={Boolean(profile.viewer?.followedBy)} 79 - moderation={moderation} 80 - /> 78 + <Labels profile={profile} moderationOpts={moderationOpts} /> 81 79 82 80 <Description profile={profile} /> 83 81 </Outer> ··· 87 85 export function Outer({ 88 86 children, 89 87 }: { 90 - children: React.ReactElement | React.ReactElement[] 88 + children: React.ReactNode | React.ReactNode[] 91 89 }) { 92 90 return <View style={[a.w_full, a.flex_1, a.gap_xs]}>{children}</View> 93 91 } ··· 95 93 export function Header({ 96 94 children, 97 95 }: { 98 - children: React.ReactElement | React.ReactElement[] 96 + children: React.ReactNode | React.ReactNode[] 99 97 }) { 100 98 return <View style={[a.flex_row, a.align_center, a.gap_sm]}>{children}</View> 101 99 } ··· 137 135 const moderation = moderateProfile(profile, moderationOpts) 138 136 139 137 return ( 140 - <UserAvatar 138 + <PreviewableUserAvatar 141 139 size={40} 142 - avatar={profile.avatar} 143 - type={profile.associated?.labeler ? 'labeler' : 'user'} 140 + profile={profile} 144 141 moderation={moderation.ui('avatar')} 145 142 /> 146 143 ) ··· 415 412 </View> 416 413 ) 417 414 } 415 + 416 + export function Labels({ 417 + profile, 418 + moderationOpts, 419 + }: { 420 + profile: bsky.profile.AnyProfileView 421 + moderationOpts: ModerationOpts 422 + }) { 423 + const moderation = moderateProfile(profile, moderationOpts) 424 + const modui = moderation.ui('profileList') 425 + const followedBy = profile.viewer?.followedBy 426 + 427 + if (!followedBy && !modui.inform && !modui.alert) { 428 + return null 429 + } 430 + 431 + return ( 432 + <Pills.Row style={[a.pt_xs]}> 433 + {followedBy && <Pills.FollowsYou />} 434 + {modui.alerts.map(alert => ( 435 + <Pills.Label key={getModerationCauseKey(alert)} cause={alert} /> 436 + ))} 437 + {modui.informs.map(inform => ( 438 + <Pills.Label key={getModerationCauseKey(inform)} cause={inform} /> 439 + ))} 440 + </Pills.Row> 441 + ) 442 + }
+1 -3
src/screens/Search/SearchResults.tsx
··· 275 275 {results.length ? ( 276 276 <List 277 277 data={results} 278 - renderItem={({item}) => ( 279 - <ProfileCardWithFollowBtn profile={item} noBg /> 280 - )} 278 + renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />} 281 279 keyExtractor={item => item.did} 282 280 desktopFixedHeight 283 281 contentContainerStyle={{paddingBottom: 100}}
+52 -33
src/view/com/lists/ListMembers.tsx
··· 1 1 import React, {useCallback} from 'react' 2 2 import {Dimensions, type StyleProp, View, type ViewStyle} from 'react-native' 3 3 import {type AppBskyGraphDefs} from '@atproto/api' 4 - import {msg} from '@lingui/macro' 4 + import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 7 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 7 import {cleanError} from '#/lib/strings/errors' 9 8 import {logger} from '#/logger' 10 9 import {useModalControls} from '#/state/modals' 10 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 11 11 import {useListMembersQuery} from '#/state/queries/list-members' 12 12 import {useSession} from '#/state/session' 13 - import {ProfileCard} from '#/view/com/profile/ProfileCard' 14 13 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 15 - import {Button} from '#/view/com/util/forms/Button' 16 14 import {List, type ListRef} from '#/view/com/util/List' 17 15 import {ProfileCardFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 18 16 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 17 + import {atoms as a, useTheme} from '#/alf' 18 + import {Button, ButtonText} from '#/components/Button' 19 19 import {ListFooter} from '#/components/Lists' 20 + import * as ProfileCard from '#/components/ProfileCard' 20 21 import type * as bsky from '#/types/bsky' 21 22 22 23 const LOADING_ITEM = {_reactKey: '__loading__'} ··· 47 48 headerOffset?: number 48 49 desktopFixedHeightOffset?: number 49 50 }) { 51 + const t = useTheme() 50 52 const {_} = useLingui() 51 53 const [isRefreshing, setIsRefreshing] = React.useState(false) 52 - const {isMobile} = useWebMediaQueries() 53 54 const {openModal} = useModalControls() 54 55 const {currentAccount} = useSession() 56 + const moderationOpts = useModerationOpts() 55 57 56 58 const { 57 59 data, ··· 131 133 // rendering 132 134 // = 133 135 134 - const renderMemberButton = React.useCallback( 135 - (profile: bsky.profile.AnyProfileView) => { 136 - if (!isOwner) { 137 - return null 138 - } 139 - return ( 140 - <Button 141 - testID={`user-${profile.handle}-editBtn`} 142 - type="default" 143 - label={_(msg({message: 'Edit', context: 'action'}))} 144 - onPress={() => onPressEditMembership(profile)} 145 - /> 146 - ) 147 - }, 148 - [isOwner, onPressEditMembership, _], 149 - ) 150 - 151 136 const renderItem = React.useCallback( 152 137 ({item}: {item: any}) => { 153 138 if (item === EMPTY_ITEM) { ··· 171 156 } else if (item === LOADING_ITEM) { 172 157 return <ProfileCardFeedLoadingPlaceholder /> 173 158 } 159 + 160 + const profile = (item as AppBskyGraphDefs.ListItemView).subject 161 + if (!moderationOpts) return null 162 + 174 163 return ( 175 - <ProfileCard 176 - testID={`user-${ 177 - (item as AppBskyGraphDefs.ListItemView).subject.handle 178 - }`} 179 - profile={(item as AppBskyGraphDefs.ListItemView).subject} 180 - renderButton={renderMemberButton} 181 - style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}} 182 - noModFilter 183 - /> 164 + <View 165 + style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}> 166 + <ProfileCard.Link profile={profile}> 167 + <ProfileCard.Outer> 168 + <ProfileCard.Header> 169 + <ProfileCard.Avatar 170 + profile={profile} 171 + moderationOpts={moderationOpts} 172 + /> 173 + <ProfileCard.NameAndHandle 174 + profile={profile} 175 + moderationOpts={moderationOpts} 176 + /> 177 + {isOwner && ( 178 + <Button 179 + testID={`user-${profile.handle}-editBtn`} 180 + label={_(msg({message: 'Edit', context: 'action'}))} 181 + onPress={() => onPressEditMembership(profile)} 182 + size="small" 183 + variant="solid" 184 + color="secondary"> 185 + <ButtonText> 186 + <Trans context="action">Edit</Trans> 187 + </ButtonText> 188 + </Button> 189 + )} 190 + </ProfileCard.Header> 191 + 192 + <ProfileCard.Labels 193 + profile={profile} 194 + moderationOpts={moderationOpts} 195 + /> 196 + 197 + <ProfileCard.Description profile={profile} /> 198 + </ProfileCard.Outer> 199 + </ProfileCard.Link> 200 + </View> 184 201 ) 185 202 }, 186 203 [ 187 - renderMemberButton, 188 204 renderEmptyState, 189 205 error, 190 206 onPressTryAgain, 191 207 onPressRetryLoadMore, 192 - isMobile, 208 + moderationOpts, 209 + isOwner, 210 + onPressEditMembership, 193 211 _, 212 + t, 194 213 ], 195 214 ) 196 215
+20 -291
src/view/com/profile/ProfileCard.tsx
··· 1 - import React from 'react' 2 - import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3 - import { 4 - AppBskyActorDefs, 5 - moderateProfile, 6 - ModerationDecision, 7 - } from '@atproto/api' 8 - import {useQueryClient} from '@tanstack/react-query' 1 + import {View} from 'react-native' 2 + import {type AppBskyActorDefs} from '@atproto/api' 9 3 10 - import {usePalette} from '#/lib/hooks/usePalette' 11 - import {getModerationCauseKey, isJustAMute} from '#/lib/moderation' 12 - import {makeProfileLink} from '#/lib/routes/links' 13 - import {sanitizeDisplayName} from '#/lib/strings/display-names' 14 - import {sanitizeHandle} from '#/lib/strings/handles' 15 - import {s} from '#/lib/styles' 16 - import {useProfileShadow} from '#/state/cache/profile-shadow' 17 - import {Shadow} from '#/state/cache/types' 18 4 import {useModerationOpts} from '#/state/preferences/moderation-opts' 19 - import {precacheProfile} from '#/state/queries/profile' 20 - import {useSession} from '#/state/session' 21 - import {atoms as a} from '#/alf' 22 - import { 23 - KnownFollowers, 24 - shouldShowKnownFollowers, 25 - } from '#/components/KnownFollowers' 26 - import * as Pills from '#/components/Pills' 27 - import * as bsky from '#/types/bsky' 28 - import {Link} from '../util/Link' 29 - import {Text} from '../util/text/Text' 30 - import {PreviewableUserAvatar} from '../util/UserAvatar' 31 - import {FollowButton} from './FollowButton' 32 - 33 - export function ProfileCard({ 34 - testID, 35 - profile: profileUnshadowed, 36 - noModFilter, 37 - noBg, 38 - noBorder, 39 - renderButton, 40 - onPress, 41 - style, 42 - showKnownFollowers, 43 - }: { 44 - testID?: string 45 - profile: bsky.profile.AnyProfileView 46 - noModFilter?: boolean 47 - noBg?: boolean 48 - noBorder?: boolean 49 - renderButton?: ( 50 - profile: Shadow<bsky.profile.AnyProfileView>, 51 - ) => React.ReactNode 52 - onPress?: () => void 53 - style?: StyleProp<ViewStyle> 54 - showKnownFollowers?: boolean 55 - }) { 56 - const queryClient = useQueryClient() 57 - const pal = usePalette('default') 58 - const profile = useProfileShadow(profileUnshadowed) 59 - const moderationOpts = useModerationOpts() 60 - const isLabeler = profile?.associated?.labeler 61 - 62 - const onBeforePress = React.useCallback(() => { 63 - onPress?.() 64 - precacheProfile(queryClient, profile) 65 - }, [onPress, profile, queryClient]) 66 - 67 - if (!moderationOpts) { 68 - return null 69 - } 70 - const moderation = moderateProfile(profile, moderationOpts) 71 - const modui = moderation.ui('profileList') 72 - if (!noModFilter && modui.filter && !isJustAMute(modui)) { 73 - return null 74 - } 75 - 76 - const knownFollowersVisible = 77 - showKnownFollowers && 78 - shouldShowKnownFollowers(profile.viewer?.knownFollowers) && 79 - moderationOpts 80 - const hasDescription = 'description' in profile 81 - 82 - return ( 83 - <Link 84 - testID={testID} 85 - style={[ 86 - styles.outer, 87 - pal.border, 88 - noBorder && styles.outerNoBorder, 89 - !noBg && pal.view, 90 - style, 91 - ]} 92 - href={makeProfileLink(profile)} 93 - title={profile.handle} 94 - asAnchor 95 - onBeforePress={onBeforePress} 96 - anchorNoUnderline> 97 - <View style={styles.layout}> 98 - <View style={styles.layoutAvi}> 99 - <PreviewableUserAvatar 100 - size={40} 101 - profile={profile} 102 - moderation={moderation.ui('avatar')} 103 - type={isLabeler ? 'labeler' : 'user'} 104 - /> 105 - </View> 106 - <View style={styles.layoutContent}> 107 - <Text 108 - emoji 109 - type="lg" 110 - style={[s.bold, pal.text, a.self_start]} 111 - numberOfLines={1} 112 - lineHeight={1.2}> 113 - {sanitizeDisplayName( 114 - profile.displayName || sanitizeHandle(profile.handle), 115 - moderation.ui('displayName'), 116 - )} 117 - </Text> 118 - <Text emoji type="md" style={[pal.textLight]} numberOfLines={1}> 119 - {sanitizeHandle(profile.handle, '@')} 120 - </Text> 121 - <ProfileCardPills 122 - followedBy={!!profile.viewer?.followedBy} 123 - moderation={moderation} 124 - /> 125 - {!!profile.viewer?.followedBy && <View style={s.flexRow} />} 126 - </View> 127 - {renderButton && !isLabeler ? ( 128 - <View style={styles.layoutButton}>{renderButton(profile)}</View> 129 - ) : undefined} 130 - </View> 131 - {hasDescription || knownFollowersVisible ? ( 132 - <View style={styles.details}> 133 - {hasDescription && profile.description ? ( 134 - <Text emoji style={pal.text} numberOfLines={4}> 135 - {profile.description as string} 136 - </Text> 137 - ) : null} 138 - {knownFollowersVisible ? ( 139 - <View 140 - style={[ 141 - a.flex_row, 142 - a.align_center, 143 - a.gap_sm, 144 - !!hasDescription && a.mt_md, 145 - ]}> 146 - <KnownFollowers 147 - minimal 148 - profile={profile} 149 - moderationOpts={moderationOpts} 150 - /> 151 - </View> 152 - ) : null} 153 - </View> 154 - ) : null} 155 - </Link> 156 - ) 157 - } 158 - 159 - export function ProfileCardPills({ 160 - followedBy, 161 - moderation, 162 - }: { 163 - followedBy: boolean 164 - moderation: ModerationDecision 165 - }) { 166 - const modui = moderation.ui('profileList') 167 - if (!followedBy && !modui.inform && !modui.alert) { 168 - return null 169 - } 170 - 171 - return ( 172 - <Pills.Row style={[a.pt_xs]}> 173 - {followedBy && <Pills.FollowsYou />} 174 - {modui.alerts.map(alert => ( 175 - <Pills.Label key={getModerationCauseKey(alert)} cause={alert} /> 176 - ))} 177 - {modui.informs.map(inform => ( 178 - <Pills.Label key={getModerationCauseKey(inform)} cause={inform} /> 179 - ))} 180 - </Pills.Row> 181 - ) 182 - } 5 + import {atoms as a, useTheme} from '#/alf' 6 + import * as ProfileCard from '#/components/ProfileCard' 183 7 184 8 export function ProfileCardWithFollowBtn({ 185 9 profile, 186 - noBg, 187 10 noBorder, 188 - onPress, 189 - onFollow, 190 11 logContext = 'ProfileCard', 191 - showKnownFollowers, 192 12 }: { 193 13 profile: AppBskyActorDefs.ProfileView 194 - noBg?: boolean 195 14 noBorder?: boolean 196 - onPress?: () => void 197 - onFollow?: () => void 198 15 logContext?: 'ProfileCard' | 'StarterPackProfilesList' 199 - showKnownFollowers?: boolean 200 16 }) { 201 - const {currentAccount} = useSession() 202 - const isMe = profile.did === currentAccount?.did 17 + const t = useTheme() 18 + const moderationOpts = useModerationOpts() 19 + 20 + if (!moderationOpts) return null 203 21 204 22 return ( 205 - <ProfileCard 206 - profile={profile} 207 - noBg={noBg} 208 - noBorder={noBorder} 209 - renderButton={ 210 - isMe 211 - ? undefined 212 - : profileShadow => ( 213 - <FollowButton 214 - profile={profileShadow} 215 - logContext={logContext} 216 - onFollow={onFollow} 217 - /> 218 - ) 219 - } 220 - onPress={onPress} 221 - showKnownFollowers={!isMe && showKnownFollowers} 222 - /> 23 + <View 24 + style={[ 25 + a.py_md, 26 + a.px_xl, 27 + !noBorder && [a.border_t, t.atoms.border_contrast_low], 28 + ]}> 29 + <ProfileCard.Default 30 + profile={profile} 31 + moderationOpts={moderationOpts} 32 + logContext={logContext} 33 + /> 34 + </View> 223 35 ) 224 36 } 225 - 226 - const styles = StyleSheet.create({ 227 - outer: { 228 - borderTopWidth: StyleSheet.hairlineWidth, 229 - paddingHorizontal: 6, 230 - paddingVertical: 4, 231 - }, 232 - outerNoBorder: { 233 - borderTopWidth: 0, 234 - }, 235 - layout: { 236 - flexDirection: 'row', 237 - alignItems: 'center', 238 - }, 239 - layoutAvi: { 240 - alignSelf: 'flex-start', 241 - width: 54, 242 - paddingLeft: 4, 243 - paddingTop: 10, 244 - }, 245 - avi: { 246 - width: 40, 247 - height: 40, 248 - borderRadius: 20, 249 - resizeMode: 'cover', 250 - }, 251 - layoutContent: { 252 - flex: 1, 253 - paddingRight: 10, 254 - paddingTop: 10, 255 - paddingBottom: 10, 256 - }, 257 - layoutButton: { 258 - paddingRight: 10, 259 - }, 260 - details: { 261 - justifyContent: 'center', 262 - paddingLeft: 54, 263 - paddingRight: 10, 264 - paddingBottom: 10, 265 - }, 266 - pills: { 267 - alignItems: 'flex-start', 268 - flexDirection: 'row', 269 - flexWrap: 'wrap', 270 - columnGap: 6, 271 - rowGap: 2, 272 - }, 273 - pill: { 274 - borderRadius: 4, 275 - paddingHorizontal: 6, 276 - paddingVertical: 2, 277 - justifyContent: 'center', 278 - }, 279 - btn: { 280 - paddingVertical: 7, 281 - borderRadius: 50, 282 - marginLeft: 6, 283 - paddingHorizontal: 14, 284 - }, 285 - 286 - followedBy: { 287 - flexDirection: 'row', 288 - paddingLeft: 54, 289 - paddingRight: 20, 290 - marginBottom: 10, 291 - marginTop: -6, 292 - }, 293 - followedByAviContainer: { 294 - width: 24, 295 - height: 36, 296 - }, 297 - followedByAvi: { 298 - width: 36, 299 - height: 36, 300 - borderRadius: 18, 301 - padding: 2, 302 - }, 303 - followsByDesc: { 304 - flex: 1, 305 - paddingRight: 10, 306 - }, 307 - })
+19 -13
src/view/screens/DebugMod.tsx
··· 1 - /* eslint-disable no-restricted-imports */ 2 1 import React from 'react' 3 2 import {View} from 'react-native' 4 3 import { 5 - AppBskyActorDefs, 6 - AppBskyFeedDefs, 7 - AppBskyFeedPost, 8 - ComAtprotoLabelDefs, 4 + type AppBskyActorDefs, 5 + type AppBskyFeedDefs, 6 + type AppBskyFeedPost, 7 + type ComAtprotoLabelDefs, 9 8 interpretLabelValueDefinition, 10 - LabelPreference, 9 + type LabelPreference, 11 10 LABELS, 12 11 mock, 13 12 moderatePost, 14 13 moderateProfile, 15 - ModerationBehavior, 16 - ModerationDecision, 17 - ModerationOpts, 14 + type ModerationBehavior, 15 + type ModerationDecision, 16 + type ModerationOpts, 18 17 RichText, 19 18 } from '@atproto/api' 20 19 import {msg} from '@lingui/macro' 21 20 import {useLingui} from '@lingui/react' 22 21 23 22 import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings' 24 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 23 + import { 24 + type CommonNavigatorParams, 25 + type NativeStackScreenProps, 26 + } from '#/lib/routes/types' 27 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 25 28 import {moderationOptsOverrideContext} from '#/state/preferences/moderation-opts' 26 - import {FeedNotification} from '#/state/queries/notifications/types' 29 + import {type FeedNotification} from '#/state/queries/notifications/types' 27 30 import { 28 31 groupNotifications, 29 32 shouldFilterNotif, ··· 42 45 ChevronTop_Stroke2_Corner0_Rounded as ChevronTop, 43 46 } from '#/components/icons/Chevron' 44 47 import * as Layout from '#/components/Layout' 48 + import * as ProfileCard from '#/components/ProfileCard' 45 49 import {H1, H3, P, Text} from '#/components/Typography' 46 50 import {ScreenHider} from '../../components/moderation/ScreenHider' 47 51 import {NotificationFeedItem} from '../com/notifications/NotificationFeedItem' 48 52 import {PostThreadItem} from '../com/post-thread/PostThreadItem' 49 53 import {PostFeedItem} from '../com/posts/PostFeedItem' 50 - import {ProfileCard} from '../com/profile/ProfileCard' 51 54 52 55 const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys( 53 56 LABELS, ··· 890 893 moderation: ModerationDecision 891 894 }) { 892 895 const t = useTheme() 896 + const moderationOpts = useModerationOpts() 897 + 898 + if (!moderationOpts) return null 893 899 894 900 if (moderation.ui('profileList').filter) { 895 901 return ( ··· 899 905 ) 900 906 } 901 907 902 - return <ProfileCard profile={profile} /> 908 + return <ProfileCard.Card profile={profile} moderationOpts={moderationOpts} /> 903 909 } 904 910 905 911 function MockAccountScreen({
+22 -13
src/view/screens/ModerationBlockedAccounts.tsx
··· 6 6 StyleSheet, 7 7 View, 8 8 } from 'react-native' 9 - import {AppBskyActorDefs as ActorDefs} from '@atproto/api' 9 + import {type AppBskyActorDefs as ActorDefs} from '@atproto/api' 10 10 import {msg, Trans} from '@lingui/macro' 11 11 import {useLingui} from '@lingui/react' 12 12 import {useFocusEffect} from '@react-navigation/native' 13 - import {NativeStackScreenProps} from '@react-navigation/native-stack' 13 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 14 14 15 15 import {usePalette} from '#/lib/hooks/usePalette' 16 16 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 - import {CommonNavigatorParams} from '#/lib/routes/types' 17 + import {type CommonNavigatorParams} from '#/lib/routes/types' 18 18 import {cleanError} from '#/lib/strings/errors' 19 19 import {logger} from '#/logger' 20 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 20 21 import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts' 21 22 import {useSetMinimalShellMode} from '#/state/shell' 22 - import {ProfileCard} from '#/view/com/profile/ProfileCard' 23 23 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 24 24 import {Text} from '#/view/com/util/text/Text' 25 25 import {ViewHeader} from '#/view/com/util/ViewHeader' 26 - import {atoms as a} from '#/alf' 26 + import {atoms as a, useTheme} from '#/alf' 27 27 import * as Layout from '#/components/Layout' 28 + import * as ProfileCard from '#/components/ProfileCard' 28 29 29 30 type Props = NativeStackScreenProps< 30 31 CommonNavigatorParams, 31 32 'ModerationBlockedAccounts' 32 33 > 33 34 export function ModerationBlockedAccounts({}: Props) { 35 + const t = useTheme() 34 36 const pal = usePalette('default') 35 37 const {_} = useLingui() 36 38 const setMinimalShellMode = useSetMinimalShellMode() 37 39 const {isTabletOrDesktop} = useWebMediaQueries() 40 + const moderationOpts = useModerationOpts() 38 41 39 42 const [isPTRing, setIsPTRing] = React.useState(false) 40 43 const { ··· 87 90 }: { 88 91 item: ActorDefs.ProfileView 89 92 index: number 90 - }) => ( 91 - <ProfileCard 92 - testID={`blockedAccount-${index}`} 93 - key={item.did} 94 - profile={item} 95 - noModFilter 96 - /> 97 - ) 93 + }) => { 94 + if (!moderationOpts) return null 95 + return ( 96 + <View 97 + style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]} 98 + key={item.did}> 99 + <ProfileCard.Default 100 + testID={`blockedAccount-${index}`} 101 + profile={item} 102 + moderationOpts={moderationOpts} 103 + /> 104 + </View> 105 + ) 106 + } 98 107 return ( 99 108 <Layout.Screen testID="blockedAccountsScreen"> 100 109 <Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
+36 -13
src/view/screens/ModerationMutedAccounts.tsx
··· 6 6 StyleSheet, 7 7 View, 8 8 } from 'react-native' 9 - import {AppBskyActorDefs as ActorDefs} from '@atproto/api' 9 + import {type AppBskyActorDefs as ActorDefs} from '@atproto/api' 10 10 import {msg, Trans} from '@lingui/macro' 11 11 import {useLingui} from '@lingui/react' 12 12 import {useFocusEffect} from '@react-navigation/native' 13 - import {NativeStackScreenProps} from '@react-navigation/native-stack' 13 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 14 14 15 15 import {usePalette} from '#/lib/hooks/usePalette' 16 16 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 - import {CommonNavigatorParams} from '#/lib/routes/types' 17 + import {type CommonNavigatorParams} from '#/lib/routes/types' 18 18 import {cleanError} from '#/lib/strings/errors' 19 19 import {logger} from '#/logger' 20 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 20 21 import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts' 21 22 import {useSetMinimalShellMode} from '#/state/shell' 22 - import {ProfileCard} from '#/view/com/profile/ProfileCard' 23 23 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 24 24 import {Text} from '#/view/com/util/text/Text' 25 25 import {ViewHeader} from '#/view/com/util/ViewHeader' 26 - import {atoms as a} from '#/alf' 26 + import {atoms as a, useTheme} from '#/alf' 27 27 import * as Layout from '#/components/Layout' 28 + import * as ProfileCard from '#/components/ProfileCard' 28 29 29 30 type Props = NativeStackScreenProps< 30 31 CommonNavigatorParams, 31 32 'ModerationMutedAccounts' 32 33 > 33 34 export function ModerationMutedAccounts({}: Props) { 35 + const t = useTheme() 34 36 const pal = usePalette('default') 35 37 const {_} = useLingui() 36 38 const setMinimalShellMode = useSetMinimalShellMode() 37 39 const {isTabletOrDesktop} = useWebMediaQueries() 40 + const moderationOpts = useModerationOpts() 38 41 39 42 const [isPTRing, setIsPTRing] = React.useState(false) 40 43 const { ··· 87 90 }: { 88 91 item: ActorDefs.ProfileView 89 92 index: number 90 - }) => ( 91 - <ProfileCard 92 - testID={`mutedAccount-${index}`} 93 - key={item.did} 94 - profile={item} 95 - noModFilter 96 - /> 97 - ) 93 + }) => { 94 + if (!moderationOpts) return null 95 + return ( 96 + <View 97 + style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]} 98 + key={item.did}> 99 + <ProfileCard.Link profile={item} testID={`mutedAccount-${index}`}> 100 + <ProfileCard.Outer> 101 + <ProfileCard.Header> 102 + <ProfileCard.Avatar 103 + profile={item} 104 + moderationOpts={moderationOpts} 105 + /> 106 + <ProfileCard.NameAndHandle 107 + profile={item} 108 + moderationOpts={moderationOpts} 109 + /> 110 + </ProfileCard.Header> 111 + <ProfileCard.Labels 112 + profile={item} 113 + moderationOpts={moderationOpts} 114 + /> 115 + <ProfileCard.Description profile={item} /> 116 + </ProfileCard.Outer> 117 + </ProfileCard.Link> 118 + </View> 119 + ) 120 + } 98 121 return ( 99 122 <Layout.Screen testID="mutedAccountsScreen"> 100 123 <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />