Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Unify label pills (#4676)

* New label pills

* Fix type errors, add default case

* Remove negative margin, only works in some places

* Fix alignment edge case

* Add a bit of padding

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>

authored by

Eric Bailey
Dan Abramov
and committed by
GitHub
14c2d75d c1336617

+226 -234
+169
src/components/Pills.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {BSKY_LABELER_DID, ModerationCause} from '@atproto/api' 4 + import {Trans} from '@lingui/macro' 5 + 6 + import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 7 + import {UserAvatar} from '#/view/com/util/UserAvatar' 8 + import {atoms as a, useTheme, ViewStyleProp} from '#/alf' 9 + import {Button} from '#/components/Button' 10 + import { 11 + ModerationDetailsDialog, 12 + useModerationDetailsDialogControl, 13 + } from '#/components/moderation/ModerationDetailsDialog' 14 + import {Text} from '#/components/Typography' 15 + 16 + export type CommonProps = { 17 + size?: 'sm' | 'lg' 18 + } 19 + 20 + export function Row({ 21 + children, 22 + style, 23 + size = 'sm', 24 + }: {children: React.ReactNode | React.ReactNode[]} & CommonProps & 25 + ViewStyleProp) { 26 + const styles = React.useMemo(() => { 27 + switch (size) { 28 + case 'lg': 29 + return [{gap: 5}] 30 + case 'sm': 31 + default: 32 + return [{gap: 3}] 33 + } 34 + }, [size]) 35 + return ( 36 + <View style={[a.flex_row, a.flex_wrap, a.gap_xs, styles, style]}> 37 + {children} 38 + </View> 39 + ) 40 + } 41 + 42 + export type LabelProps = { 43 + cause: ModerationCause 44 + disableDetailsDialog?: boolean 45 + noBg?: boolean 46 + } & CommonProps 47 + 48 + export function Label({ 49 + cause, 50 + size = 'sm', 51 + disableDetailsDialog, 52 + noBg, 53 + }: LabelProps) { 54 + const t = useTheme() 55 + const control = useModerationDetailsDialogControl() 56 + const desc = useModerationCauseDescription(cause) 57 + const isLabeler = Boolean(desc.sourceType && desc.sourceDid) 58 + const isBlueskyLabel = 59 + desc.sourceType === 'labeler' && desc.sourceDid === BSKY_LABELER_DID 60 + 61 + const {outer, avi, text} = React.useMemo(() => { 62 + switch (size) { 63 + case 'lg': { 64 + return { 65 + outer: [ 66 + t.atoms.bg_contrast_25, 67 + { 68 + gap: 5, 69 + paddingHorizontal: 5, 70 + paddingVertical: 5, 71 + }, 72 + ], 73 + avi: 16, 74 + text: [a.text_sm], 75 + } 76 + } 77 + case 'sm': 78 + default: { 79 + return { 80 + outer: [ 81 + !noBg && t.atoms.bg_contrast_25, 82 + { 83 + gap: 3, 84 + paddingHorizontal: 3, 85 + paddingVertical: 3, 86 + }, 87 + ], 88 + avi: 12, 89 + text: [a.text_xs], 90 + } 91 + } 92 + } 93 + }, [t, size, noBg]) 94 + 95 + return ( 96 + <> 97 + <Button 98 + disabled={disableDetailsDialog} 99 + label={desc.name} 100 + onPress={e => { 101 + e.preventDefault() 102 + e.stopPropagation() 103 + control.open() 104 + }}> 105 + {({hovered, pressed}) => ( 106 + <View 107 + style={[ 108 + a.flex_row, 109 + a.align_center, 110 + a.rounded_full, 111 + outer, 112 + (hovered || pressed) && t.atoms.bg_contrast_50, 113 + ]}> 114 + {isBlueskyLabel || !isLabeler ? ( 115 + <desc.icon 116 + width={avi} 117 + fill={t.atoms.text_contrast_medium.color} 118 + /> 119 + ) : ( 120 + <UserAvatar avatar={desc.sourceAvi} size={avi} /> 121 + )} 122 + 123 + <Text 124 + style={[ 125 + text, 126 + a.font_semibold, 127 + a.leading_tight, 128 + t.atoms.text_contrast_medium, 129 + {paddingRight: 3}, 130 + ]}> 131 + {desc.name} 132 + </Text> 133 + </View> 134 + )} 135 + </Button> 136 + 137 + {!disableDetailsDialog && ( 138 + <ModerationDetailsDialog control={control} modcause={cause} /> 139 + )} 140 + </> 141 + ) 142 + } 143 + 144 + export function FollowsYou({size = 'sm'}: CommonProps) { 145 + const t = useTheme() 146 + 147 + const variantStyles = React.useMemo(() => { 148 + switch (size) { 149 + case 'sm': 150 + case 'lg': 151 + default: 152 + return [ 153 + { 154 + paddingHorizontal: 6, 155 + paddingVertical: 3, 156 + borderRadius: 4, 157 + }, 158 + ] 159 + } 160 + }, [size]) 161 + 162 + return ( 163 + <View style={[variantStyles, a.justify_center, t.atoms.bg_contrast_25]}> 164 + <Text style={[a.text_xs, a.leading_tight]}> 165 + <Trans>Follows You</Trans> 166 + </Text> 167 + </View> 168 + ) 169 + }
+3 -2
src/components/ProfileHoverCard/index.web.tsx
··· 29 29 } from '#/components/KnownFollowers' 30 30 import {InlineLinkText, Link} from '#/components/Link' 31 31 import {Loader} from '#/components/Loader' 32 + import * as Pills from '#/components/Pills' 32 33 import {Portal} from '#/components/Portal' 33 34 import {RichText} from '#/components/RichText' 34 35 import {Text} from '#/components/Typography' 35 - import {ProfileLabel} from '../moderation/ProfileHeaderAlerts' 36 36 import {ProfileHoverCardProps} from './types' 37 37 38 38 const floatingMiddlewares = [ ··· 476 476 {isBlockedUser && ( 477 477 <View style={[a.flex_row, a.flex_wrap, a.gap_xs]}> 478 478 {moderation.ui('profileView').alerts.map(cause => ( 479 - <ProfileLabel 479 + <Pills.Label 480 480 key={getModerationCauseKey(cause)} 481 + size="lg" 481 482 cause={cause} 482 483 disableDetailsDialog 483 484 />
+1 -1
src/components/dms/MessagesListHeader.tsx
··· 214 214 ]}> 215 215 <PostAlerts 216 216 modui={moderation.ui('contentList')} 217 - size="large" 217 + size="lg" 218 218 style={[a.pt_xs]} 219 219 /> 220 220 </View>
+1 -3
src/components/moderation/ContentHider.tsx
··· 165 165 } 166 166 167 167 const styles = StyleSheet.create({ 168 - outer: { 169 - overflow: 'hidden', 170 - }, 168 + outer: {}, 171 169 cover: { 172 170 flexDirection: 'row', 173 171 alignItems: 'center',
+23 -98
src/components/moderation/PostAlerts.tsx
··· 1 1 import React from 'react' 2 - import {StyleProp, View, ViewStyle} from 'react-native' 3 - import {BSKY_LABELER_DID, ModerationCause, ModerationUI} from '@atproto/api' 2 + import {StyleProp, ViewStyle} from 'react-native' 3 + import {ModerationUI} from '@atproto/api' 4 4 5 5 import {getModerationCauseKey} from '#/lib/moderation' 6 - import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 7 - import {UserAvatar} from '#/view/com/util/UserAvatar' 8 - import {atoms as a, useTheme} from '#/alf' 9 - import {Button} from '#/components/Button' 10 - import { 11 - ModerationDetailsDialog, 12 - useModerationDetailsDialogControl, 13 - } from '#/components/moderation/ModerationDetailsDialog' 14 - import {Text} from '#/components/Typography' 6 + import * as Pills from '#/components/Pills' 15 7 16 8 export function PostAlerts({ 17 9 modui, 18 - size, 10 + size = 'sm', 19 11 style, 20 12 }: { 21 13 modui: ModerationUI 22 - size?: 'medium' | 'large' 14 + size?: Pills.CommonProps['size'] 23 15 includeMute?: boolean 24 16 style?: StyleProp<ViewStyle> 25 17 }) { ··· 28 20 } 29 21 30 22 return ( 31 - <View style={[a.flex_col, a.gap_xs, style]}> 32 - <View style={[a.flex_row, a.flex_wrap, a.gap_xs]}> 33 - {modui.alerts.map(cause => ( 34 - <PostLabel 35 - key={getModerationCauseKey(cause)} 36 - cause={cause} 37 - size={size} 38 - /> 39 - ))} 40 - {modui.informs.map(cause => ( 41 - <PostLabel 42 - key={getModerationCauseKey(cause)} 43 - cause={cause} 44 - size={size} 45 - /> 46 - ))} 47 - </View> 48 - </View> 49 - ) 50 - } 51 - 52 - function PostLabel({ 53 - cause, 54 - size, 55 - }: { 56 - cause: ModerationCause 57 - size?: 'medium' | 'large' 58 - }) { 59 - const control = useModerationDetailsDialogControl() 60 - const desc = useModerationCauseDescription(cause) 61 - const t = useTheme() 62 - 63 - return ( 64 - <> 65 - <Button 66 - label={desc.name} 67 - onPress={e => { 68 - e.preventDefault() 69 - e.stopPropagation() 70 - control.open() 71 - }}> 72 - {({hovered, pressed}) => ( 73 - <View 74 - style={[ 75 - a.flex_row, 76 - a.align_center, 77 - a.gap_xs, 78 - a.rounded_sm, 79 - hovered || pressed 80 - ? size === 'large' 81 - ? t.atoms.bg_contrast_50 82 - : t.atoms.bg_contrast_25 83 - : size === 'large' 84 - ? t.atoms.bg_contrast_25 85 - : undefined, 86 - size === 'large' 87 - ? {paddingLeft: 4, paddingRight: 6, paddingVertical: 2} 88 - : {paddingRight: 4, paddingVertical: 1}, 89 - ]}> 90 - {desc.sourceType === 'labeler' && 91 - desc.sourceDid !== BSKY_LABELER_DID ? ( 92 - <UserAvatar 93 - avatar={desc.sourceAvi} 94 - size={size === 'large' ? 16 : 12} 95 - type="labeler" 96 - shape="circle" 97 - /> 98 - ) : ( 99 - <desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} /> 100 - )} 101 - <Text 102 - style={[ 103 - a.text_left, 104 - a.leading_snug, 105 - size === 'large' ? {fontSize: 13} : a.text_xs, 106 - size === 'large' ? t.atoms.text : t.atoms.text_contrast_high, 107 - ]}> 108 - {desc.name} 109 - </Text> 110 - </View> 111 - )} 112 - </Button> 113 - 114 - <ModerationDetailsDialog control={control} modcause={cause} /> 115 - </> 23 + <Pills.Row size={size} style={[size === 'sm' && {marginLeft: -3}, style]}> 24 + {modui.alerts.map(cause => ( 25 + <Pills.Label 26 + key={getModerationCauseKey(cause)} 27 + cause={cause} 28 + size={size} 29 + noBg={size === 'sm'} 30 + /> 31 + ))} 32 + {modui.informs.map(cause => ( 33 + <Pills.Label 34 + key={getModerationCauseKey(cause)} 35 + cause={cause} 36 + size={size} 37 + noBg={size === 'sm'} 38 + /> 39 + ))} 40 + </Pills.Row> 116 41 ) 117 42 }
+19 -84
src/components/moderation/ProfileHeaderAlerts.tsx
··· 1 1 import React from 'react' 2 - import {StyleProp, View, ViewStyle} from 'react-native' 3 - import { 4 - BSKY_LABELER_DID, 5 - ModerationCause, 6 - ModerationDecision, 7 - } from '@atproto/api' 2 + import {StyleProp, ViewStyle} from 'react-native' 3 + import {ModerationDecision} from '@atproto/api' 8 4 9 - import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 10 5 import {getModerationCauseKey} from 'lib/moderation' 11 - import {UserAvatar} from '#/view/com/util/UserAvatar' 12 - import {atoms as a, useTheme} from '#/alf' 13 - import {Button} from '#/components/Button' 14 - import { 15 - ModerationDetailsDialog, 16 - useModerationDetailsDialogControl, 17 - } from '#/components/moderation/ModerationDetailsDialog' 18 - import {Text} from '#/components/Typography' 6 + import * as Pills from '#/components/Pills' 19 7 20 8 export function ProfileHeaderAlerts({ 21 9 moderation, 22 - style, 23 10 }: { 24 11 moderation: ModerationDecision 25 12 style?: StyleProp<ViewStyle> ··· 30 17 } 31 18 32 19 return ( 33 - <View style={[a.flex_col, a.gap_xs, style]}> 34 - <View style={[a.flex_row, a.flex_wrap, a.gap_xs]}> 35 - {modui.alerts.map(cause => ( 36 - <ProfileLabel key={getModerationCauseKey(cause)} cause={cause} /> 37 - ))} 38 - {modui.informs.map(cause => ( 39 - <ProfileLabel key={getModerationCauseKey(cause)} cause={cause} /> 40 - ))} 41 - </View> 42 - </View> 43 - ) 44 - } 45 - 46 - export function ProfileLabel({ 47 - cause, 48 - disableDetailsDialog, 49 - }: { 50 - cause: ModerationCause 51 - disableDetailsDialog?: boolean 52 - }) { 53 - const t = useTheme() 54 - const control = useModerationDetailsDialogControl() 55 - const desc = useModerationCauseDescription(cause) 56 - 57 - return ( 58 - <> 59 - <Button 60 - disabled={disableDetailsDialog} 61 - label={desc.name} 62 - onPress={() => { 63 - control.open() 64 - }}> 65 - {({hovered, pressed}) => ( 66 - <View 67 - style={[ 68 - a.flex_row, 69 - a.align_center, 70 - {paddingLeft: 6, paddingRight: 8, paddingVertical: 4}, 71 - a.gap_xs, 72 - a.rounded_md, 73 - hovered || pressed 74 - ? t.atoms.bg_contrast_50 75 - : t.atoms.bg_contrast_25, 76 - ]}> 77 - {desc.sourceType === 'labeler' && 78 - desc.sourceDid !== BSKY_LABELER_DID ? ( 79 - <UserAvatar avatar={desc.sourceAvi} size={16} /> 80 - ) : ( 81 - <desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} /> 82 - )} 83 - <Text 84 - style={[ 85 - a.text_left, 86 - a.leading_snug, 87 - a.text_sm, 88 - t.atoms.text_contrast_medium, 89 - a.font_semibold, 90 - ]}> 91 - {desc.name} 92 - </Text> 93 - </View> 94 - )} 95 - </Button> 96 - 97 - {!disableDetailsDialog && ( 98 - <ModerationDetailsDialog control={control} modcause={cause} /> 99 - )} 100 - </> 20 + <Pills.Row size="lg"> 21 + {modui.alerts.map(cause => ( 22 + <Pills.Label 23 + size="lg" 24 + key={getModerationCauseKey(cause)} 25 + cause={cause} 26 + /> 27 + ))} 28 + {modui.informs.map(cause => ( 29 + <Pills.Label 30 + size="lg" 31 + key={getModerationCauseKey(cause)} 32 + cause={cause} 33 + /> 34 + ))} 35 + </Pills.Row> 101 36 ) 102 37 }
+1 -1
src/screens/Messages/List/ChatListItem.tsx
··· 315 315 316 316 <PostAlerts 317 317 modui={moderation.ui('contentList')} 318 - size="large" 318 + size="lg" 319 319 style={[a.pt_xs]} 320 320 /> 321 321 </View>
+1 -1
src/view/com/post-thread/PostThreadItem.tsx
··· 313 313 childContainerStyle={styles.contentHiderChild}> 314 314 <PostAlerts 315 315 modui={moderation.ui('contentView')} 316 - size="large" 316 + size="lg" 317 317 includeMute 318 318 style={[a.pt_2xs, a.pb_sm]} 319 319 />
+8 -44
src/view/com/profile/ProfileCard.tsx
··· 3 3 import { 4 4 AppBskyActorDefs, 5 5 moderateProfile, 6 - ModerationCause, 7 6 ModerationDecision, 8 7 } from '@atproto/api' 9 8 import {Trans} from '@lingui/macro' 10 9 import {useQueryClient} from '@tanstack/react-query' 11 10 12 - import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 13 11 import {useProfileShadow} from '#/state/cache/profile-shadow' 14 12 import {Shadow} from '#/state/cache/types' 15 13 import {useModerationOpts} from '#/state/preferences/moderation-opts' ··· 26 24 import {PreviewableUserAvatar} from '../util/UserAvatar' 27 25 import {FollowButton} from './FollowButton' 28 26 import hairlineWidth = StyleSheet.hairlineWidth 27 + import {atoms as a} from '#/alf' 28 + import * as Pills from '#/components/Pills' 29 29 30 30 export function ProfileCard({ 31 31 testID, ··· 137 137 followedBy: boolean 138 138 moderation: ModerationDecision 139 139 }) { 140 - const pal = usePalette('default') 141 - 142 140 const modui = moderation.ui('profileList') 143 141 if (!followedBy && !modui.inform && !modui.alert) { 144 142 return null 145 143 } 146 144 147 145 return ( 148 - <View style={styles.pills}> 149 - {followedBy && ( 150 - <View style={[s.mt5, pal.btn, styles.pill]}> 151 - <Text type="xs" style={pal.text}> 152 - <Trans>Follows You</Trans> 153 - </Text> 154 - </View> 155 - )} 146 + <Pills.Row style={[a.pt_xs]}> 147 + {followedBy && <Pills.FollowsYou />} 156 148 {modui.alerts.map(alert => ( 157 - <ProfileCardPillModerationCause 158 - key={getModerationCauseKey(alert)} 159 - cause={alert} 160 - severity="alert" 161 - /> 149 + <Pills.Label key={getModerationCauseKey(alert)} cause={alert} /> 162 150 ))} 163 151 {modui.informs.map(inform => ( 164 - <ProfileCardPillModerationCause 165 - key={getModerationCauseKey(inform)} 166 - cause={inform} 167 - severity="inform" 168 - /> 152 + <Pills.Label key={getModerationCauseKey(inform)} cause={inform} /> 169 153 ))} 170 - </View> 171 - ) 172 - } 173 - 174 - function ProfileCardPillModerationCause({ 175 - cause, 176 - severity, 177 - }: { 178 - cause: ModerationCause 179 - severity: 'alert' | 'inform' 180 - }) { 181 - const pal = usePalette('default') 182 - const {name} = useModerationCauseDescription(cause) 183 - return ( 184 - <View 185 - style={[s.mt5, pal.btn, styles.pill]} 186 - key={getModerationCauseKey(cause)}> 187 - <Text type="xs" style={pal.text}> 188 - {severity === 'alert' ? '⚠ ' : ''} 189 - {name} 190 - </Text> 191 - </View> 154 + </Pills.Row> 192 155 ) 193 156 } 194 157 ··· 322 285 paddingBottom: 10, 323 286 }, 324 287 pills: { 288 + alignItems: 'flex-start', 325 289 flexDirection: 'row', 326 290 flexWrap: 'wrap', 327 291 columnGap: 6,