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

(cherry picked from commit 8c9a11dc3f)
This commit is contained in:
Eric Bailey
2026-04-09 15:52:45 -05:00
parent 18844b57ea
commit feb2b76265
4 changed files with 167 additions and 53 deletions
+1 -1
View File
@@ -81,7 +81,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.19.6",
"@atproto/api": "^0.19.8",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.7",
@@ -1,10 +1,12 @@
import {useCallback} from 'react'
import {useCallback, useMemo} from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
import {type AppBskyActorDefs, type AppBskyEmbedExternal} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {
type AppBskyActorDefs,
type AppBskyEmbedExternal,
moderateStatus,
} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
@@ -19,8 +21,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {Globe_Stroke2_Corner0_Rounded} from '#/components/icons/Globe'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '#/components/icons/SquareArrowTopRight'
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
import * as Hider from '#/components/moderation/Hider'
import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
@@ -64,7 +68,7 @@ function DialogInner({
navigation: NavigationProp
status: AppBskyActorDefs.StatusView
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const control = Dialog.useDialogContext()
const onPressOpenProfile = useCallback(() => {
@@ -77,7 +81,7 @@ function DialogInner({
return (
<Dialog.ScrollableInner
label={_(msg`${sanitizeHandle(profile.handle)} is live`)}
label={l`${sanitizeHandle(profile.handle)} is live`}
contentContainerStyle={[a.pt_0, a.px_0]}
style={[web({maxWidth: 420}), a.overflow_hidden]}>
<LiveStatus
@@ -105,45 +109,55 @@ export function LiveStatus({
onPressOpenProfile: () => void
}) {
const ax = useAnalytics()
const {_} = useLingui()
const {t: l} = useLingui()
const t = useTheme()
const queryClient = useQueryClient()
const openLink = useOpenLink()
const moderationOpts = useModerationOpts()
const reportDialogControl = useGlobalReportDialogControl()
const dialogContext = Dialog.useDialogContext()
const moderation = useMemo(() => {
return moderateStatus(profile, moderationOpts!)
}, [profile, moderationOpts])
return (
<>
{embed.external.thumb && (
<View
style={[
t.atoms.bg_contrast_25,
a.w_full,
a.aspect_card,
android([
a.overflow_hidden,
{
borderTopLeftRadius: a.rounded_md.borderRadius,
borderTopRightRadius: a.rounded_md.borderRadius,
},
]),
]}>
<Image
source={embed.external.thumb}
contentFit="cover"
style={[a.absolute, a.inset_0]}
accessibilityIgnoresInvertColors
/>
<LiveIndicator
size="large"
style={[
a.absolute,
{top: tokens.space.lg, left: tokens.space.lg},
a.align_start,
]}
/>
</View>
<Hider.Outer modui={moderation.ui('contentMedia')}>
<Hider.Mask>
<ModeratedImage />
</Hider.Mask>
<Hider.Content>
<View
style={[
t.atoms.bg_contrast_25,
a.w_full,
a.aspect_card,
android([
a.overflow_hidden,
{
borderTopLeftRadius: a.rounded_md.borderRadius,
borderTopRightRadius: a.rounded_md.borderRadius,
},
]),
]}>
<Image
source={embed.external.thumb}
contentFit="cover"
style={[a.absolute, a.inset_0]}
accessibilityIgnoresInvertColors
/>
<LiveIndicator
size="large"
style={[
a.absolute,
{top: tokens.space.lg, left: tokens.space.lg},
a.align_start,
]}
/>
</View>
</Hider.Content>
</Hider.Outer>
)}
<View
style={[
@@ -171,7 +185,7 @@ export function LiveStatus({
</View>
</View>
<Button
label={_(msg`Watch now`)}
label={l`Watch now`}
size={platform({native: 'large', web: 'small'})}
color="primary"
variant="solid"
@@ -200,7 +214,7 @@ export function LiveStatus({
/>
</View>
<Button
label={_(msg`Open profile`)}
label={l`Open profile`}
size="small"
color="secondary"
variant="solid"
@@ -231,7 +245,7 @@ export function LiveStatus({
</View>
{status && (
<SimpleInlineLinkText
label={_(msg`Report this livestream`)}
label={l`Report this livestream`}
{...createStaticClick(() => {
function open() {
reportDialogControl.open({
@@ -256,3 +270,58 @@ export function LiveStatus({
</>
)
}
function ModeratedImage() {
const t = useTheme()
const {t: l} = useLingui()
const hider = Hider.useHider()
return (
<View
style={[
a.flex_1,
a.p_lg,
a.py_xl,
a.align_center,
a.justify_center,
t.atoms.bg_contrast_25,
]}>
<View style={[a.align_center, a.gap_sm, {maxWidth: 200}]}>
<ImageIcon size="lg" fill={t.atoms.text_contrast_medium.color} />
<Text
style={[
a.italic,
a.leading_snug,
a.text_center,
t.atoms.text_contrast_medium,
]}>
{hider.meta.allowOverride ? (
<Trans comment="Image has been moderated and user has the option of showing it temporarily">
Image is hidden due to your moderation settings.
</Trans>
) : (
/*
* In practice, if `allowOverride` is false, we won't even allow this
* dialog to open. That is handled in
* `#/features/liveNow/index.tsx`. But for clarity, I've included
* this here.
*/
<Trans comment="Image has been moderated and is not visible to the user">
Image is unavailable.
</Trans>
)}
</Text>
{hider.meta.allowOverride && (
<SimpleInlineLinkText
label={l`Show anyway`}
{...createStaticClick(() => {
hider.setIsContentVisible(true)
})}>
<Trans>Show anyway</Trans>
</SimpleInlineLinkText>
)}
</View>
</View>
)
}
+24 -7
View File
@@ -6,6 +6,7 @@ import {
AppBskyEmbedExternal,
AtUri,
ComAtprotoRepoPutRecord,
moderateStatus,
} from '@atproto/api'
import {retry} from '@atproto/common-web'
import {msg} from '@lingui/core/macro'
@@ -21,6 +22,7 @@ import {
updateProfileShadow,
useMaybeProfileShadow,
} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAgent, useSession} from '#/state/session'
import {useTickEveryMinute} from '#/state/shell'
import {useDialogContext} from '#/components/Dialog'
@@ -37,6 +39,13 @@ export const DEFAULT_ALLOWED_DOMAINS = [
'bluecast.app',
]
const DEFAULT_STATE = {
status: '',
isDisabled: false,
isActive: false,
record: {},
} satisfies AppBskyActorDefs.StatusView
export type LiveNowConfig = {
canGoLive: boolean
currentAccountAllowedHosts: Set<string>
@@ -87,10 +96,23 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
const shadowed = useMaybeProfileShadow(actor)
const tick = useTickEveryMinute()
const config = useLiveNowConfig()
const moderationOpts = useModerationOpts()
const moderation = useMemo(() => {
if (!actor || !('status' in actor && actor.status)) return undefined
return moderateStatus(actor, moderationOpts!)
}, [actor, moderationOpts])
return useMemo(() => {
void tick // revalidate every minute
/*
* Do not even allow Live Now to show if filtered for `contentList`.
*/
if (moderation && moderation.ui('contentList').filter) {
return DEFAULT_STATE
}
if (shadowed && 'status' in shadowed && shadowed.status) {
const isValid = isStatusValidForViewers(shadowed.status, config)
const isDisabled = shadowed.status.isDisabled || false
@@ -118,14 +140,9 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
record: shadowed.status.record,
} satisfies AppBskyActorDefs.StatusView
} else {
return {
status: '',
isDisabled: false,
isActive: false,
record: {},
} satisfies AppBskyActorDefs.StatusView
return DEFAULT_STATE
}
}, [shadowed, config, tick])
}, [shadowed, config, tick, moderation])
}
export function isStatusStillActive(timeStr: string | undefined) {
+34 -6
View File
@@ -20,12 +20,12 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@atproto/api@^0.19.6":
version "0.19.6"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.6.tgz#c8fae3d792fe429c900ac0ba2609d60b9a89e28b"
integrity sha512-8L5dZvGaclB52b8msjtgDNx3uLWUY4PELA7KbFyAWBFVasCceE1txdrscCqDCLN+Fff9+Sm07OIjnjHYJXdETA==
"@atproto/api@^0.19.8":
version "0.19.8"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.8.tgz#ae847abece43f0108535c6305780079e8782ab29"
integrity sha512-b79kuI3AzEmpLLi9afRNq6T0KFEEVL4d+vHFAtWxeDwS7lfwUOIIngMjAVvwmwC5nJRZIrK8L9d4y7LD8zdvsg==
dependencies:
"@atproto/common-web" "^0.4.19"
"@atproto/common-web" "^0.4.20"
"@atproto/lexicon" "^0.6.2"
"@atproto/syntax" "^0.5.3"
"@atproto/xrpc" "^0.7.7"
@@ -34,7 +34,7 @@
tlds "^1.234.0"
zod "^3.23.8"
"@atproto/common-web@^0.4.18", "@atproto/common-web@^0.4.19":
"@atproto/common-web@^0.4.18":
version "0.4.19"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.19.tgz#bbd7f84f545ebe73ca3bc00314ccf4ee66e7069e"
integrity sha512-3BTi58p5WpT+9/zb6UZrdsXcfPo5P45UJm0E4iwHLILr+jc37CuBj9JReDSZ4U0i9RTrI3ZkfySyZ9bd+LnMsw==
@@ -44,6 +44,16 @@
"@atproto/syntax" "^0.5.1"
zod "^3.23.8"
"@atproto/common-web@^0.4.20":
version "0.4.20"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.20.tgz#bb455868e674d45ed1044c68ccccae3c08168d47"
integrity sha512-RcsYT28yQgVi/Glb/hHPGpqpzIlKrbMLeldEd7PmmMLWDaJL2j3lb92qytvxjl1yhi2Ssq2TEuMZ2NlWaAbpow==
dependencies:
"@atproto/lex-data" "^0.0.15"
"@atproto/lex-json" "^0.0.15"
"@atproto/syntax" "^0.5.3"
zod "^3.23.8"
"@atproto/lex-data@^0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.14.tgz#2f2f3c64699925a0d4785e5afd0e7731ba1d46c0"
@@ -54,6 +64,16 @@
uint8arrays "3.0.0"
unicode-segmenter "^0.14.0"
"@atproto/lex-data@^0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.15.tgz#b9a644d71d4a99b32f452250994b5f12276335ef"
integrity sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw==
dependencies:
multiformats "^9.9.0"
tslib "^2.8.1"
uint8arrays "3.0.0"
unicode-segmenter "^0.14.0"
"@atproto/lex-json@^0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.14.tgz#717e533ab583aa5f580acb2a77d9aa3e7eddaa17"
@@ -62,6 +82,14 @@
"@atproto/lex-data" "^0.0.14"
tslib "^2.8.1"
"@atproto/lex-json@^0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.15.tgz#34d300e5dfd8a0ec76ca7363f264a488e17c1bd9"
integrity sha512-kCLdP629H6GhgPjBTpZibUoqlpmW0hnVfZVwcD4s4Jch1KAqY/QcfL24Ih8wrW0Ok1YvtMIhjk98evdTA2OJcw==
dependencies:
"@atproto/lex-data" "^0.0.15"
tslib "^2.8.1"
"@atproto/lexicon@^0.6.0", "@atproto/lexicon@^0.6.2":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.6.2.tgz#f6152a2119df953236ca127c4b30e332265e81e7"