Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

[APP-1775] Handle labels on actor statuses (#9716)

authored by

Eric Bailey and committed by
GitHub
8c9a11dc d01362a0

+167 -53
+1 -1
package.json
··· 81 81 "icons:optimize": "svgo -f ./assets/icons" 82 82 }, 83 83 "dependencies": { 84 - "@atproto/api": "^0.19.6", 84 + "@atproto/api": "^0.19.8", 85 85 "@bitdrift/react-native": "^0.6.8", 86 86 "@braintree/sanitize-url": "^6.0.2", 87 87 "@bsky.app/alf": "^0.1.7",
+108 -39
src/features/liveNow/components/LiveStatusDialog.tsx
··· 1 - import {useCallback} from 'react' 1 + import {useCallback, useMemo} from 'react' 2 2 import {View} from 'react-native' 3 3 import {Image} from 'expo-image' 4 - import {type AppBskyActorDefs, type AppBskyEmbedExternal} from '@atproto/api' 5 - import {msg} from '@lingui/core/macro' 6 - import {useLingui} from '@lingui/react' 7 - import {Trans} from '@lingui/react/macro' 4 + import { 5 + type AppBskyActorDefs, 6 + type AppBskyEmbedExternal, 7 + moderateStatus, 8 + } from '@atproto/api' 9 + import {Trans, useLingui} from '@lingui/react/macro' 8 10 import {useNavigation} from '@react-navigation/native' 9 11 import {useQueryClient} from '@tanstack/react-query' 10 12 ··· 19 21 import * as Dialog from '#/components/Dialog' 20 22 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' 21 23 import {Globe_Stroke2_Corner0_Rounded} from '#/components/icons/Globe' 24 + import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' 22 25 import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '#/components/icons/SquareArrowTopRight' 23 26 import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' 27 + import * as Hider from '#/components/moderation/Hider' 24 28 import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog' 25 29 import * as ProfileCard from '#/components/ProfileCard' 26 30 import {Text} from '#/components/Typography' ··· 64 68 navigation: NavigationProp 65 69 status: AppBskyActorDefs.StatusView 66 70 }) { 67 - const {_} = useLingui() 71 + const {t: l} = useLingui() 68 72 const control = Dialog.useDialogContext() 69 73 70 74 const onPressOpenProfile = useCallback(() => { ··· 77 81 78 82 return ( 79 83 <Dialog.ScrollableInner 80 - label={_(msg`${sanitizeHandle(profile.handle)} is live`)} 84 + label={l`${sanitizeHandle(profile.handle)} is live`} 81 85 contentContainerStyle={[a.pt_0, a.px_0]} 82 86 style={[web({maxWidth: 420}), a.overflow_hidden]}> 83 87 <LiveStatus ··· 105 109 onPressOpenProfile: () => void 106 110 }) { 107 111 const ax = useAnalytics() 108 - const {_} = useLingui() 112 + const {t: l} = useLingui() 109 113 const t = useTheme() 110 114 const queryClient = useQueryClient() 111 115 const openLink = useOpenLink() 112 116 const moderationOpts = useModerationOpts() 113 117 const reportDialogControl = useGlobalReportDialogControl() 114 118 const dialogContext = Dialog.useDialogContext() 119 + const moderation = useMemo(() => { 120 + return moderateStatus(profile, moderationOpts!) 121 + }, [profile, moderationOpts]) 115 122 116 123 return ( 117 124 <> 118 125 {embed.external.thumb && ( 119 - <View 120 - style={[ 121 - t.atoms.bg_contrast_25, 122 - a.w_full, 123 - a.aspect_card, 124 - android([ 125 - a.overflow_hidden, 126 - { 127 - borderTopLeftRadius: a.rounded_md.borderRadius, 128 - borderTopRightRadius: a.rounded_md.borderRadius, 129 - }, 130 - ]), 131 - ]}> 132 - <Image 133 - source={embed.external.thumb} 134 - contentFit="cover" 135 - style={[a.absolute, a.inset_0]} 136 - accessibilityIgnoresInvertColors 137 - /> 138 - <LiveIndicator 139 - size="large" 140 - style={[ 141 - a.absolute, 142 - {top: tokens.space.lg, left: tokens.space.lg}, 143 - a.align_start, 144 - ]} 145 - /> 146 - </View> 126 + <Hider.Outer modui={moderation.ui('contentMedia')}> 127 + <Hider.Mask> 128 + <ModeratedImage /> 129 + </Hider.Mask> 130 + <Hider.Content> 131 + <View 132 + style={[ 133 + t.atoms.bg_contrast_25, 134 + a.w_full, 135 + a.aspect_card, 136 + android([ 137 + a.overflow_hidden, 138 + { 139 + borderTopLeftRadius: a.rounded_md.borderRadius, 140 + borderTopRightRadius: a.rounded_md.borderRadius, 141 + }, 142 + ]), 143 + ]}> 144 + <Image 145 + source={embed.external.thumb} 146 + contentFit="cover" 147 + style={[a.absolute, a.inset_0]} 148 + accessibilityIgnoresInvertColors 149 + /> 150 + <LiveIndicator 151 + size="large" 152 + style={[ 153 + a.absolute, 154 + {top: tokens.space.lg, left: tokens.space.lg}, 155 + a.align_start, 156 + ]} 157 + /> 158 + </View> 159 + </Hider.Content> 160 + </Hider.Outer> 147 161 )} 148 162 <View 149 163 style={[ ··· 171 185 </View> 172 186 </View> 173 187 <Button 174 - label={_(msg`Watch now`)} 188 + label={l`Watch now`} 175 189 size={platform({native: 'large', web: 'small'})} 176 190 color="primary" 177 191 variant="solid" ··· 200 214 /> 201 215 </View> 202 216 <Button 203 - label={_(msg`Open profile`)} 217 + label={l`Open profile`} 204 218 size="small" 205 219 color="secondary" 206 220 variant="solid" ··· 231 245 </View> 232 246 {status && ( 233 247 <SimpleInlineLinkText 234 - label={_(msg`Report this livestream`)} 248 + label={l`Report this livestream`} 235 249 {...createStaticClick(() => { 236 250 function open() { 237 251 reportDialogControl.open({ ··· 256 270 </> 257 271 ) 258 272 } 273 + 274 + function ModeratedImage() { 275 + const t = useTheme() 276 + const {t: l} = useLingui() 277 + const hider = Hider.useHider() 278 + 279 + return ( 280 + <View 281 + style={[ 282 + a.flex_1, 283 + a.p_lg, 284 + a.py_xl, 285 + a.align_center, 286 + a.justify_center, 287 + t.atoms.bg_contrast_25, 288 + ]}> 289 + <View style={[a.align_center, a.gap_sm, {maxWidth: 200}]}> 290 + <ImageIcon size="lg" fill={t.atoms.text_contrast_medium.color} /> 291 + <Text 292 + style={[ 293 + a.italic, 294 + a.leading_snug, 295 + a.text_center, 296 + t.atoms.text_contrast_medium, 297 + ]}> 298 + {hider.meta.allowOverride ? ( 299 + <Trans comment="Image has been moderated and user has the option of showing it temporarily"> 300 + Image is hidden due to your moderation settings. 301 + </Trans> 302 + ) : ( 303 + /* 304 + * In practice, if `allowOverride` is false, we won't even allow this 305 + * dialog to open. That is handled in 306 + * `#/features/liveNow/index.tsx`. But for clarity, I've included 307 + * this here. 308 + */ 309 + <Trans comment="Image has been moderated and is not visible to the user"> 310 + Image is unavailable. 311 + </Trans> 312 + )} 313 + </Text> 314 + 315 + {hider.meta.allowOverride && ( 316 + <SimpleInlineLinkText 317 + label={l`Show anyway`} 318 + {...createStaticClick(() => { 319 + hider.setIsContentVisible(true) 320 + })}> 321 + <Trans>Show anyway</Trans> 322 + </SimpleInlineLinkText> 323 + )} 324 + </View> 325 + </View> 326 + ) 327 + }
+24 -7
src/features/liveNow/index.tsx
··· 6 6 AppBskyEmbedExternal, 7 7 AtUri, 8 8 ComAtprotoRepoPutRecord, 9 + moderateStatus, 9 10 } from '@atproto/api' 10 11 import {retry} from '@atproto/common-web' 11 12 import {msg} from '@lingui/core/macro' ··· 21 22 updateProfileShadow, 22 23 useMaybeProfileShadow, 23 24 } from '#/state/cache/profile-shadow' 25 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 24 26 import {useAgent, useSession} from '#/state/session' 25 27 import {useTickEveryMinute} from '#/state/shell' 26 28 import {useDialogContext} from '#/components/Dialog' ··· 36 38 'stream.place', 37 39 'bluecast.app', 38 40 ] 41 + 42 + const DEFAULT_STATE = { 43 + status: '', 44 + isDisabled: false, 45 + isActive: false, 46 + record: {}, 47 + } satisfies AppBskyActorDefs.StatusView 39 48 40 49 export type LiveNowConfig = { 41 50 canGoLive: boolean ··· 87 96 const shadowed = useMaybeProfileShadow(actor) 88 97 const tick = useTickEveryMinute() 89 98 const config = useLiveNowConfig() 99 + const moderationOpts = useModerationOpts() 100 + 101 + const moderation = useMemo(() => { 102 + if (!actor || !('status' in actor && actor.status)) return undefined 103 + return moderateStatus(actor, moderationOpts!) 104 + }, [actor, moderationOpts]) 90 105 91 106 return useMemo(() => { 92 107 void tick // revalidate every minute 93 108 109 + /* 110 + * Do not even allow Live Now to show if filtered for `contentList`. 111 + */ 112 + if (moderation && moderation.ui('contentList').filter) { 113 + return DEFAULT_STATE 114 + } 115 + 94 116 if (shadowed && 'status' in shadowed && shadowed.status) { 95 117 const isValid = isStatusValidForViewers(shadowed.status, config) 96 118 const isDisabled = shadowed.status.isDisabled || false ··· 118 140 record: shadowed.status.record, 119 141 } satisfies AppBskyActorDefs.StatusView 120 142 } else { 121 - return { 122 - status: '', 123 - isDisabled: false, 124 - isActive: false, 125 - record: {}, 126 - } satisfies AppBskyActorDefs.StatusView 143 + return DEFAULT_STATE 127 144 } 128 - }, [shadowed, config, tick]) 145 + }, [shadowed, config, tick, moderation]) 129 146 } 130 147 131 148 export function isStatusStillActive(timeStr: string | undefined) {
+34 -6
yarn.lock
··· 20 20 "@jridgewell/gen-mapping" "^0.3.0" 21 21 "@jridgewell/trace-mapping" "^0.3.9" 22 22 23 - "@atproto/api@^0.19.6": 24 - version "0.19.6" 25 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.6.tgz#c8fae3d792fe429c900ac0ba2609d60b9a89e28b" 26 - integrity sha512-8L5dZvGaclB52b8msjtgDNx3uLWUY4PELA7KbFyAWBFVasCceE1txdrscCqDCLN+Fff9+Sm07OIjnjHYJXdETA== 23 + "@atproto/api@^0.19.8": 24 + version "0.19.8" 25 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.8.tgz#ae847abece43f0108535c6305780079e8782ab29" 26 + integrity sha512-b79kuI3AzEmpLLi9afRNq6T0KFEEVL4d+vHFAtWxeDwS7lfwUOIIngMjAVvwmwC5nJRZIrK8L9d4y7LD8zdvsg== 27 27 dependencies: 28 - "@atproto/common-web" "^0.4.19" 28 + "@atproto/common-web" "^0.4.20" 29 29 "@atproto/lexicon" "^0.6.2" 30 30 "@atproto/syntax" "^0.5.3" 31 31 "@atproto/xrpc" "^0.7.7" ··· 34 34 tlds "^1.234.0" 35 35 zod "^3.23.8" 36 36 37 - "@atproto/common-web@^0.4.18", "@atproto/common-web@^0.4.19": 37 + "@atproto/common-web@^0.4.18": 38 38 version "0.4.19" 39 39 resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.19.tgz#bbd7f84f545ebe73ca3bc00314ccf4ee66e7069e" 40 40 integrity sha512-3BTi58p5WpT+9/zb6UZrdsXcfPo5P45UJm0E4iwHLILr+jc37CuBj9JReDSZ4U0i9RTrI3ZkfySyZ9bd+LnMsw== ··· 44 44 "@atproto/syntax" "^0.5.1" 45 45 zod "^3.23.8" 46 46 47 + "@atproto/common-web@^0.4.20": 48 + version "0.4.20" 49 + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.20.tgz#bb455868e674d45ed1044c68ccccae3c08168d47" 50 + integrity sha512-RcsYT28yQgVi/Glb/hHPGpqpzIlKrbMLeldEd7PmmMLWDaJL2j3lb92qytvxjl1yhi2Ssq2TEuMZ2NlWaAbpow== 51 + dependencies: 52 + "@atproto/lex-data" "^0.0.15" 53 + "@atproto/lex-json" "^0.0.15" 54 + "@atproto/syntax" "^0.5.3" 55 + zod "^3.23.8" 56 + 47 57 "@atproto/lex-data@^0.0.14": 48 58 version "0.0.14" 49 59 resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.14.tgz#2f2f3c64699925a0d4785e5afd0e7731ba1d46c0" ··· 54 64 uint8arrays "3.0.0" 55 65 unicode-segmenter "^0.14.0" 56 66 67 + "@atproto/lex-data@^0.0.15": 68 + version "0.0.15" 69 + resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.15.tgz#b9a644d71d4a99b32f452250994b5f12276335ef" 70 + integrity sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw== 71 + dependencies: 72 + multiformats "^9.9.0" 73 + tslib "^2.8.1" 74 + uint8arrays "3.0.0" 75 + unicode-segmenter "^0.14.0" 76 + 57 77 "@atproto/lex-json@^0.0.14": 58 78 version "0.0.14" 59 79 resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.14.tgz#717e533ab583aa5f580acb2a77d9aa3e7eddaa17" 60 80 integrity sha512-6lPkDKqe7teEu4WrN5q7400cvZKgYS3uwUMvzG3F9XkgVYhOwSDCtouV/nSLBbpvo3l9OP0kiigtclcNcyekww== 61 81 dependencies: 62 82 "@atproto/lex-data" "^0.0.14" 83 + tslib "^2.8.1" 84 + 85 + "@atproto/lex-json@^0.0.15": 86 + version "0.0.15" 87 + resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.15.tgz#34d300e5dfd8a0ec76ca7363f264a488e17c1bd9" 88 + integrity sha512-kCLdP629H6GhgPjBTpZibUoqlpmW0hnVfZVwcD4s4Jch1KAqY/QcfL24Ih8wrW0Ok1YvtMIhjk98evdTA2OJcw== 89 + dependencies: 90 + "@atproto/lex-data" "^0.0.15" 63 91 tslib "^2.8.1" 64 92 65 93 "@atproto/lexicon@^0.6.0", "@atproto/lexicon@^0.6.2":