Refactor/rewrite the entire moderation-application system

This commit is contained in:
Paul Frazee
2024-02-14 21:48:27 -08:00
parent cc635ae949
commit b009a34a7a
43 changed files with 1094 additions and 1062 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 263 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 6a1 1 0 0 1 .707.293l8 8a1 1 0 0 1-1.414 1.414L12 8.414l-7.293 7.293a1 1 0 0 1-1.414-1.414l8-8A1 1 0 0 1 12 6Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 256 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.675 2.054a1 1 0 0 1 .65 0l8 2.75A1 1 0 0 1 21 5.75v6.162c0 2.807-1.149 4.83-2.813 6.405-1.572 1.488-3.632 2.6-5.555 3.636l-.157.085a1 1 0 0 1-.95 0l-.157-.085c-1.923-1.037-3.983-2.148-5.556-3.636C4.15 16.742 3 14.719 3 11.912V5.75a1 1 0 0 1 .675-.946l8-2.75ZM5 6.464v5.448c0 2.166.851 3.687 2.188 4.952 1.276 1.209 2.964 2.158 4.812 3.157 1.848-1 3.536-1.948 4.813-3.157C18.148 15.6 19 14.078 19 11.912V6.464l-7-2.407-7 2.407Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 572 B

+3 -1
View File
@@ -165,6 +165,9 @@ export const atoms = {
/* /*
* Text * Text
*/ */
text_left: {
textAlign: 'left',
},
text_center: { text_center: {
textAlign: 'center', textAlign: 'center',
}, },
@@ -242,7 +245,6 @@ export const atoms = {
borderRightWidth: 1, borderRightWidth: 1,
}, },
/* /*
* Shadow * Shadow
*/ */
+13 -3
View File
@@ -27,7 +27,7 @@ export type ButtonColor =
| 'gradient_sunset' | 'gradient_sunset'
| 'gradient_nordic' | 'gradient_nordic'
| 'gradient_bonfire' | 'gradient_bonfire'
export type ButtonSize = 'small' | 'large' export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonShape = 'round' | 'square' | 'default' export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = { export type VariantProps = {
/** /**
@@ -277,6 +277,8 @@ export function Button({
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
} else if (size === 'small') { } else if (size === 'small') {
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
} else if (size === 'tiny') {
baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
} }
} else if (shape === 'round' || shape === 'square') { } else if (shape === 'round' || shape === 'square') {
if (size === 'large') { if (size === 'large') {
@@ -287,12 +289,18 @@ export function Button({
} }
} else if (size === 'small') { } else if (size === 'small') {
baseStyles.push({height: 40, width: 40}) baseStyles.push({height: 40, width: 40})
} else if (size === 'tiny') {
baseStyles.push({height: 20, width: 20})
} }
if (shape === 'round') { if (shape === 'round') {
baseStyles.push(a.rounded_full) baseStyles.push(a.rounded_full)
} else if (shape === 'square') { } else if (shape === 'square') {
baseStyles.push(a.rounded_sm) if (size === 'tiny') {
baseStyles.push(a.rounded_xs)
} else {
baseStyles.push(a.rounded_sm)
}
} }
} }
@@ -493,6 +501,8 @@ export function useSharedButtonTextStyles() {
if (size === 'large') { if (size === 'large') {
baseStyles.push(a.text_md, android({paddingBottom: 1})) baseStyles.push(a.text_md, android({paddingBottom: 1}))
} else if (size === 'tiny') {
baseStyles.push(a.text_xs, android({paddingBottom: 1}))
} else { } else {
baseStyles.push(a.text_sm, android({paddingBottom: 1})) baseStyles.push(a.text_sm, android({paddingBottom: 1}))
} }
@@ -532,7 +542,7 @@ export function ButtonIcon({
}, },
]}> ]}>
<Comp <Comp
size={size === 'large' ? 'md' : 'sm'} size={size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm'}
style={[{color: textStyles.color, pointerEvents: 'none'}]} style={[{color: textStyles.color, pointerEvents: 'none'}]}
/> />
</View> </View>
@@ -0,0 +1,128 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ModerationCause} from '@atproto/api'
import {atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {GlobalDialogProps} from '#/components/dialogs'
import {Button} from '#/components/Button'
import {InlineLink} from '#/components/Link'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import {listUriToHref} from '#/lib/strings/url-helpers'
export interface ModerationDetailsDialogProps {
context: 'account' | 'content'
modcause: ModerationCause
}
export function ModerationDetailsDialog({
params,
cleanup,
}: GlobalDialogProps<ModerationDetailsDialogProps>) {
const {_} = useLingui()
const labelStrings = useLabelStrings()
const control = Dialog.useDialogControl()
const {gtMobile} = useBreakpoints()
const {context, modcause} = params
// REQUIRED CLEANUP
const onClose = React.useCallback(() => cleanup(), [cleanup])
let name
let description
if (!modcause) {
name = _(msg`Content Warning`)
description = _(
msg`Moderator has chosen to set a general warning on the content.`,
)
} else if (modcause.type === 'blocking') {
if (modcause.source.type === 'list') {
const list = modcause.source.list
name = _(msg`User Blocked by List`)
description = (
<Trans>
This user is included in the{' '}
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}>
{list.name}
</InlineLink>{' '}
list which you have blocked.
</Trans>
)
} else {
name = _(msg`User Blocked`)
description = _(
msg`You have blocked this user. You cannot view their content.`,
)
}
} else if (modcause.type === 'blocked-by') {
name = _(msg`User Blocks You`)
description = _(
msg`This user has blocked you. You cannot view their content.`,
)
} else if (modcause.type === 'block-other') {
name = _(msg`Content Not Available`)
description = _(
msg`This content is not available because one of the users involved has blocked the other.`,
)
} else if (modcause.type === 'muted') {
if (modcause.source.type === 'list') {
const list = modcause.source.list
name = _(msg`Account Muted by List`)
description = (
<Trans>
This user is included in the{' '}
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}>
{list.name}
</InlineLink>{' '}
list which you have muted.
</Trans>
)
} else {
name = _(msg`Account Muted`)
description = _(msg`You have muted this user.`)
}
} else if (modcause.type === 'label') {
if (modcause.labelDef.id in labelStrings) {
name = labelStrings[modcause.labelDef.id][context].name
description = labelStrings[modcause.labelDef.id][context].description
} else {
name = modcause.labelDef.id
description = _(msg`Labeled ${modcause.labelDef.id}`)
}
} else {
// should never happen
name = ''
description = ''
}
return (
<Dialog.Outer defaultOpen control={control} onClose={onClose}>
<Dialog.Handle />
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
{name}
</Text>
<Text nativeID="dialog-description" style={[a.text_sm]}>
{description}
</Text>
<View style={gtMobile && [a.flex_row, a.justify_end]}>
<Button
testID="doneBtn"
variant="outline"
color="primary"
size="small"
onPress={() => control.close()}
label={_(msg`Done`)}>
{_(msg`Done`)}
</Button>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+8
View File
@@ -7,3 +7,11 @@ export const ChevronLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
export const ChevronRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const ChevronRight_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z', path: 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z',
}) })
export const ChevronTop_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 6a1 1 0 0 1 .707.293l8 8a1 1 0 0 1-1.414 1.414L12 8.414l-7.293 7.293a1 1 0 0 1-1.414-1.414l8-8A1 1 0 0 1 12 6Z',
})
export const ChevronBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Shield_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M11.675 2.054a1 1 0 0 1 .65 0l8 2.75A1 1 0 0 1 21 5.75v6.162c0 2.807-1.149 4.83-2.813 6.405-1.572 1.488-3.632 2.6-5.555 3.636l-.157.085a1 1 0 0 1-.95 0l-.157-.085c-1.923-1.037-3.983-2.148-5.556-3.636C4.15 16.742 3 14.719 3 11.912V5.75a1 1 0 0 1 .675-.946l8-2.75ZM5 6.464v5.448c0 2.166.851 3.687 2.188 4.952 1.276 1.209 2.964 2.158 4.812 3.157 1.848-1 3.536-1.948 4.813-3.157C18.148 15.6 19 14.078 19 11.912V6.464l-7-2.407-7 2.407Z',
})
+7 -6
View File
@@ -1,6 +1,6 @@
import { import {
AppBskyEmbedRecord, // AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia, // AppBskyEmbedRecordWithMedia,
moderatePost, moderatePost,
} from '@atproto/api' } from '@atproto/api'
@@ -13,10 +13,11 @@ export function moderatePost_wrapped(
subject: Parameters<ModeratePost>[0], subject: Parameters<ModeratePost>[0],
opts: Options, opts: Options,
) { ) {
const {hiddenPosts = [], ...options} = opts // const {hiddenPosts = [], ...options} = opts
const moderations = moderatePost(subject, options) const moderations = moderatePost(subject, opts) // options)
if (hiddenPosts.includes(subject.uri)) { // TODO
/*if (hiddenPosts.includes(subject.uri)) {
moderations.content.filter = true moderations.content.filter = true
moderations.content.blur = true moderations.content.blur = true
if (!moderations.content.cause) { if (!moderations.content.cause) {
@@ -52,7 +53,7 @@ export function moderatePost_wrapped(
} }
} }
} }
} }*/
return moderations return moderations
} }
+1 -65
View File
@@ -1,68 +1,4 @@
import { import {ModerationCause, LABEL_GROUPS, LabelGroupDefinition} from '@atproto/api'
ModerationCause,
ProfileModeration,
PostModeration,
LABEL_GROUPS,
LabelGroupDefinition,
} from '@atproto/api'
export function getProfileModerationCauses(
moderation: ProfileModeration,
): ModerationCause[] {
/*
Gather everything on profile and account that blurs or alerts
*/
return [
moderation.decisions.profile.cause,
...moderation.decisions.profile.additionalCauses,
moderation.decisions.account.cause,
...moderation.decisions.account.additionalCauses,
].filter(cause => {
if (!cause) {
return false
}
if (cause?.type === 'label') {
if (
cause.labelDef.onwarn === 'blur' ||
cause.labelDef.onwarn === 'alert'
) {
return true
} else {
return false
}
}
return true
}) as ModerationCause[]
}
export function isPostMediaBlurred(
decisions: PostModeration['decisions'],
): boolean {
return decisions.post.blurMedia
}
export function isQuoteBlurred(
decisions: PostModeration['decisions'],
): boolean {
return (
decisions.quote?.blur ||
decisions.quote?.blurMedia ||
decisions.quote?.filter ||
decisions.quotedAccount?.blur ||
decisions.quotedAccount?.filter ||
false
)
}
export function isCauseALabelOnUri(
cause: ModerationCause | undefined,
uri: string,
): boolean {
if (cause?.type !== 'label') {
return false
}
return cause.label.uri === uri
}
export function getModerationCauseKey(cause: ModerationCause): string { export function getModerationCauseKey(cause: ModerationCause): string {
const source = const source =
@@ -88,7 +88,7 @@ export function SuggestedAccountCard({
<UserAvatar <UserAvatar
size={48} size={48}
avatar={profile.avatar} avatar={profile.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
</View> </View>
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
-7
View File
@@ -26,12 +26,6 @@ export interface EditProfileModal {
onUpdate?: () => void onUpdate?: () => void
} }
export interface ModerationDetailsModal {
name: 'moderation-details'
context: 'account' | 'content'
moderation: ModerationUI
}
export type ReportModal = { export type ReportModal = {
name: 'report' name: 'report'
} & ( } & (
@@ -203,7 +197,6 @@ export type Modal =
| PostLanguagesSettingsModal | PostLanguagesSettingsModal
// Moderation // Moderation
| ModerationDetailsModal
| ReportModal | ReportModal
| AppealLabelModal | AppealLabelModal
+2 -2
View File
@@ -107,8 +107,8 @@ function computeSuggestions(
} }
} }
return items.filter(profile => { return items.filter(profile => {
const mod = moderateProfile(profile, moderationOpts) const mod = moderateProfile(profile, moderationOpts).ui('profileList')
return !mod.account.filter && mod.account.cause?.type !== 'muted' return !mod.filter // TODO && mod.account.cause?.type !== 'muted'
}) })
} }
+2 -6
View File
@@ -97,11 +97,7 @@ function shouldFilterNotif(
return false return false
} }
const profile = moderateProfile(notif.author, moderationOpts) const profile = moderateProfile(notif.author, moderationOpts)
if ( if (profile.ui('profileList').filter || notif.author.viewer?.muted) {
profile.account.filter ||
profile.profile.filter ||
notif.author.viewer?.muted
) {
return true return true
} }
if ( if (
@@ -111,7 +107,7 @@ function shouldFilterNotif(
) { ) {
// NOTE: the notification overlaps the post enough for this to work // NOTE: the notification overlaps the post enough for this to work
const post = moderatePost(notif, moderationOpts) const post = moderatePost(notif, moderationOpts)
if (post.content.filter) { if (post.ui('contentList').filter) {
return true return true
} }
} }
+8 -4
View File
@@ -1,6 +1,10 @@
import React, {useCallback, useEffect, useRef} from 'react' import React, {useCallback, useEffect, useRef} from 'react'
import {AppState} from 'react-native' import {AppState} from 'react-native'
import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api' import {
AppBskyFeedDefs,
AppBskyFeedPost,
ModerationDecision,
} from '@atproto/api'
import { import {
useInfiniteQuery, useInfiniteQuery,
InfiniteData, InfiniteData,
@@ -62,7 +66,7 @@ export interface FeedPostSliceItem {
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
moderation: PostModeration moderation: ModerationDecision
} }
export interface FeedPostSlice { export interface FeedPostSlice {
@@ -227,7 +231,7 @@ export function usePostFeedQuery(
// apply moderation filter // apply moderation filter
for (let i = 0; i < slice.items.length; i++) { for (let i = 0; i < slice.items.length; i++) {
if ( if (
moderations[i]?.content.filter && moderations[i]?.ui('contentList').filter &&
slice.items[i].post.author.did !== ignoreFilterFor slice.items[i].post.author.did !== ignoreFilterFor
) { ) {
return undefined return undefined
@@ -433,7 +437,7 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
DEFAULT_LOGGED_OUT_PREFERENCES.moderationOpts, DEFAULT_LOGGED_OUT_PREFERENCES.moderationOpts,
) )
if (!moderation.content.filter) { if (!moderation.ui('contentList').filter) {
// we have a sfw post // we have a sfw post
somePostsPassModeration = true somePostsPassModeration = true
} }
+16 -3
View File
@@ -1,6 +1,10 @@
import {useMemo} from 'react' import {useMemo, createContext, useContext} from 'react'
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {LabelPreference, BskyFeedViewPreference} from '@atproto/api' import {
LabelPreference,
BskyFeedViewPreference,
ModerationOpts,
} from '@atproto/api'
import {track} from '#/lib/analytics/analytics' import {track} from '#/lib/analytics/analytics'
import {getAge} from '#/lib/strings/time' import {getAge} from '#/lib/strings/time'
@@ -63,10 +67,19 @@ export function usePreferencesQuery() {
}) })
} }
// used in the moderation state devtool
export const moderationOptsOverrideContext = createContext<
ModerationOpts | undefined
>(undefined)
export function useModerationOpts() { export function useModerationOpts() {
const override = useContext(moderationOptsOverrideContext)
const prefs = usePreferencesQuery() const prefs = usePreferencesQuery()
const hiddenPosts = useHiddenPosts() const hiddenPosts = useHiddenPosts()
const opts = useMemo(() => { const opts = useMemo(() => {
if (override) {
return override
}
if (!prefs.data) { if (!prefs.data) {
return return
} }
@@ -75,7 +88,7 @@ export function useModerationOpts() {
...moderationOpts, ...moderationOpts,
hiddenPosts, hiddenPosts,
} }
}, [prefs.data, hiddenPosts]) }, [override, prefs.data, hiddenPosts])
return opts return opts
} }
+2 -1
View File
@@ -46,7 +46,8 @@ export function useSuggestedFollowsQuery() {
res.data.actors = res.data.actors res.data.actors = res.data.actors
.filter( .filter(
actor => !moderateProfile(actor, moderationOpts!).account.filter, actor =>
!moderateProfile(actor, moderationOpts!).ui('profileList').filter,
) )
.filter(actor => { .filter(actor => {
const viewer = actor.viewer const viewer = actor.viewer
+2 -2
View File
@@ -2,7 +2,7 @@ import React from 'react'
import { import {
AppBskyEmbedRecord, AppBskyEmbedRecord,
AppBskyRichtextFacet, AppBskyRichtextFacet,
PostModeration, ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -16,7 +16,7 @@ export interface ComposerOptsPostRef {
avatar?: string avatar?: string
} }
embed?: AppBskyEmbedRecord.ViewRecord['embed'] embed?: AppBskyEmbedRecord.ViewRecord['embed']
moderation?: PostModeration moderation?: ModerationDecision
} }
export interface ComposerOptsQuote { export interface ComposerOptsQuote {
uri: string uri: string
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {View, StyleSheet, ActivityIndicator} from 'react-native' import {View, StyleSheet, ActivityIndicator} from 'react-native'
import {ProfileModeration, AppBskyActorDefs} from '@atproto/api' import {ModerationDecision, AppBskyActorDefs} from '@atproto/api'
import {Button} from '#/view/com/util/forms/Button' import {Button} from '#/view/com/util/forms/Button'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
@@ -18,7 +18,7 @@ import {logger} from '#/logger'
type Props = { type Props = {
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration moderation: ModerationDecision
onFollowStateChange: (props: { onFollowStateChange: (props: {
did: string did: string
following: boolean following: boolean
@@ -62,7 +62,7 @@ export function ProfileCard({
moderation, moderation,
}: { }: {
profile: Shadow<AppBskyActorDefs.ProfileViewBasic> profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
moderation: ProfileModeration moderation: ModerationDecision
onFollowStateChange: (props: { onFollowStateChange: (props: {
did: string did: string
following: boolean following: boolean
@@ -110,7 +110,7 @@ export function ProfileCard({
<UserAvatar <UserAvatar
size={40} size={40}
avatar={profile.avatar} avatar={profile.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
@@ -121,7 +121,7 @@ export function ProfileCard({
lineHeight={1.2}> lineHeight={1.2}>
{sanitizeDisplayName( {sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.profile, moderation.ui('displayName'),
)} )}
</Text> </Text>
<Text type="xl" style={[pal.textLight]} numberOfLines={1}> <Text type="xl" style={[pal.textLight]} numberOfLines={1}>
+1 -1
View File
@@ -39,7 +39,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useExternalLinkFetch} from './useExternalLinkFetch' import {useExternalLinkFetch} from './useExternalLinkFetch'
import {isWeb, isNative, isAndroid, isIOS} from 'platform/detection' import {isWeb, isNative, isAndroid, isIOS} from 'platform/detection'
import QuoteEmbed from '../util/post-embeds/QuoteEmbed' import {QuoteEmbed} from '../util/post-embeds/QuoteEmbed'
import {GalleryModel} from 'state/models/media/gallery' import {GalleryModel} from 'state/models/media/gallery'
import {Gallery} from './photos/Gallery' import {Gallery} from './photos/Gallery'
import {MAX_GRAPHEME_LENGTH} from 'lib/constants' import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
+3 -3
View File
@@ -15,7 +15,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {UserAvatar} from 'view/com/util/UserAvatar' import {UserAvatar} from 'view/com/util/UserAvatar'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import QuoteEmbed from 'view/com/util/post-embeds/QuoteEmbed' import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const pal = usePalette('default') const pal = usePalette('default')
@@ -86,7 +86,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
<UserAvatar <UserAvatar
avatar={replyTo.author.avatar} avatar={replyTo.author.avatar}
size={50} size={50}
moderation={replyTo.moderation?.avatar} moderation={replyTo.moderation?.ui('avatar')}
/> />
<View style={styles.replyToPost}> <View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}> <Text type="xl-medium" style={[pal.text]}>
@@ -103,7 +103,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
{replyTo.text} {replyTo.text}
</Text> </Text>
</View> </View>
{images && !replyTo.moderation?.embed.blur && ( {images && !replyTo.moderation?.ui('contentMedia').blur && (
<ComposerReplyToImages images={images} showFull={showFull} /> <ComposerReplyToImages images={images} showFull={showFull} />
)} )}
</View> </View>
-4
View File
@@ -26,7 +26,6 @@ import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings' import * as BirthDateSettingsModal from './BirthDateSettings'
import * as VerifyEmailModal from './VerifyEmail' import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail' import * as ChangeEmailModal from './ChangeEmail'
@@ -127,9 +126,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'post-languages-settings') { } else if (activeModal?.name === 'post-languages-settings') {
snapPoints = PostLanguagesSettingsModal.snapPoints snapPoints = PostLanguagesSettingsModal.snapPoints
element = <PostLanguagesSettingsModal.Component /> element = <PostLanguagesSettingsModal.Component />
} else if (activeModal?.name === 'moderation-details') {
snapPoints = ModerationDetailsModal.snapPoints
element = <ModerationDetailsModal.Component {...activeModal} />
} else if (activeModal?.name === 'birth-date-settings') { } else if (activeModal?.name === 'birth-date-settings') {
snapPoints = BirthDateSettingsModal.snapPoints snapPoints = BirthDateSettingsModal.snapPoints
element = <BirthDateSettingsModal.Component /> element = <BirthDateSettingsModal.Component />
-3
View File
@@ -28,7 +28,6 @@ import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings' import * as BirthDateSettingsModal from './BirthDateSettings'
import * as VerifyEmailModal from './VerifyEmail' import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail' import * as ChangeEmailModal from './ChangeEmail'
@@ -121,8 +120,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <AltTextImageModal.Component {...modal} /> element = <AltTextImageModal.Component {...modal} />
} else if (modal.name === 'edit-image') { } else if (modal.name === 'edit-image') {
element = <EditImageModal.Component {...modal} /> element = <EditImageModal.Component {...modal} />
} else if (modal.name === 'moderation-details') {
element = <ModerationDetailsModal.Component {...modal} />
} else if (modal.name === 'birth-date-settings') { } else if (modal.name === 'birth-date-settings') {
element = <BirthDateSettingsModal.Component /> element = <BirthDateSettingsModal.Component />
} else if (modal.name === 'verify-email') { } else if (modal.name === 'verify-email') {
-154
View File
@@ -1,154 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles'
import {Text} from '../util/text/Text'
import {TextLink} from '../util/Link'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {listUriToHref} from 'lib/strings/url-helpers'
import {Button} from '../util/forms/Button'
import {useModalControls} from '#/state/modals'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
export const snapPoints = [300]
export function Component({
context,
moderation,
}: {
context: 'account' | 'content'
moderation: ModerationUI
}) {
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
const labelStrings = useLabelStrings()
let name
let description
if (!moderation.cause) {
name = _(msg`Content Warning`)
description = _(
msg`Moderator has chosen to set a general warning on the content.`,
)
} else if (moderation.cause.type === 'blocking') {
if (moderation.cause.source.type === 'list') {
const list = moderation.cause.source.list
name = _(msg`User Blocked by List`)
description = (
<Trans>
This user is included in the{' '}
<TextLink
type="2xl"
href={listUriToHref(list.uri)}
text={list.name}
style={pal.link}
/>{' '}
list which you have blocked.
</Trans>
)
} else {
name = _(msg`User Blocked`)
description = _(
msg`You have blocked this user. You cannot view their content.`,
)
}
} else if (moderation.cause.type === 'blocked-by') {
name = _(msg`User Blocks You`)
description = _(
msg`This user has blocked you. You cannot view their content.`,
)
} else if (moderation.cause.type === 'block-other') {
name = _(msg`Content Not Available`)
description = _(
msg`This content is not available because one of the users involved has blocked the other.`,
)
} else if (moderation.cause.type === 'muted') {
if (moderation.cause.source.type === 'list') {
const list = moderation.cause.source.list
name = _(msg`Account Muted by List`)
description = (
<Trans>
This user is included in the{' '}
<TextLink
type="2xl"
href={listUriToHref(list.uri)}
text={list.name}
style={pal.link}
/>{' '}
list which you have muted.
</Trans>
)
} else {
name = _(msg`Account Muted`)
description = _(msg`You have muted this user.`)
}
} else if (moderation.cause.type === 'label') {
if (moderation.cause.labelDef.id in labelStrings) {
name = labelStrings[moderation.cause.labelDef.id][context].name
description =
labelStrings[moderation.cause.labelDef.id][context].description
} else {
name = moderation.cause.labelDef.id
description = _(msg`Labeled ${moderation.cause.labelDef.id}`)
}
} else {
// should never happen
name = ''
description = ''
}
return (
<View
testID="moderationDetailsModal"
style={[
styles.container,
{
paddingHorizontal: isMobile ? 14 : 0,
},
pal.view,
]}>
<Text type="title-xl" style={[pal.text, styles.title]}>
{name}
</Text>
<Text type="2xl" style={[pal.text, styles.description]}>
{description}
</Text>
<View style={s.flex1} />
<Button
type="primary"
style={styles.btn}
onPress={() => {
closeModal()
}}>
<Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
Okay
</Text>
</Button>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
textAlign: 'center',
fontWeight: 'bold',
marginBottom: 12,
},
description: {
textAlign: 'center',
},
btn: {
paddingVertical: 14,
marginTop: isWeb ? 40 : 0,
marginBottom: isWeb ? 0 : 40,
},
})
+5 -5
View File
@@ -11,7 +11,7 @@ import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
ModerationOpts, ModerationOpts,
ProfileModeration, ModerationDecision,
moderateProfile, moderateProfile,
AppBskyEmbedRecordWithMedia, AppBskyEmbedRecordWithMedia,
} from '@atproto/api' } from '@atproto/api'
@@ -54,7 +54,7 @@ interface Author {
handle: string handle: string
displayName?: string displayName?: string
avatar?: string avatar?: string
moderation: ProfileModeration moderation: ModerationDecision
} }
let FeedItem = ({ let FeedItem = ({
@@ -336,7 +336,7 @@ function CondensedAuthorsList({
did={authors[0].did} did={authors[0].did}
handle={authors[0].handle} handle={authors[0].handle}
avatar={authors[0].avatar} avatar={authors[0].avatar}
moderation={authors[0].moderation.avatar} moderation={authors[0].moderation.ui('avatar')}
/> />
</View> </View>
) )
@@ -354,7 +354,7 @@ function CondensedAuthorsList({
<UserAvatar <UserAvatar
size={35} size={35}
avatar={author.avatar} avatar={author.avatar}
moderation={author.moderation.avatar} moderation={author.moderation.ui('avatar')}
/> />
</View> </View>
))} ))}
@@ -412,7 +412,7 @@ function ExpandedAuthorsList({
<UserAvatar <UserAvatar
size={35} size={35}
avatar={author.avatar} avatar={author.avatar}
moderation={author.moderation.avatar} moderation={author.moderation.ui('avatar')}
/> />
</View> </View>
<View style={s.flex1}> <View style={s.flex1}>
+6 -5
View File
@@ -90,11 +90,12 @@ export function PostThread({
? moderatePost(rootPost, moderationOpts) ? moderatePost(rootPost, moderationOpts)
: undefined : undefined
const cause = mod?.content.cause return !!mod
?.ui('contentList')
return cause .blurs.find(
? cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated' cause =>
: false cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated',
)
}, [rootPost, moderationOpts]) }, [rootPost, moderationOpts])
useSetTitle( useSetTitle(
+11 -34
View File
@@ -5,7 +5,7 @@ import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
RichText as RichTextAPI, RichText as RichTextAPI,
PostModeration, ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -19,7 +19,6 @@ import {niceDate} from 'lib/strings/time'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {countLines, pluralize} from 'lib/strings/helpers' import {countLines, pluralize} from 'lib/strings/helpers'
import {isEmbedByEmbedder} from 'lib/embeds'
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds' import {PostEmbeds} from '../util/post-embeds'
@@ -144,7 +143,7 @@ let PostThreadItemLoaded = ({
post: Shadow<AppBskyFeedDefs.PostView> post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
richText: RichTextAPI richText: RichTextAPI
moderation: PostModeration moderation: ModerationDecision
treeView: boolean treeView: boolean
depth: number depth: number
prevPost: ThreadPost | undefined prevPost: ThreadPost | undefined
@@ -256,7 +255,7 @@ let PostThreadItemLoaded = ({
did={post.author.did} did={post.author.did}
handle={post.author.handle} handle={post.author.handle}
avatar={post.author.avatar} avatar={post.author.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
@@ -313,12 +312,12 @@ let PostThreadItemLoaded = ({
</View> </View>
<View style={[s.pl10, s.pr10, s.pb10]}> <View style={[s.pl10, s.pr10, s.pb10]}>
<ContentHider <ContentHider
moderation={moderation.content} modui={moderation.ui('contentView')}
ignoreMute ignoreMute
style={styles.contentHider} style={styles.contentHider}
childContainerStyle={styles.contentHiderChild}> childContainerStyle={styles.contentHiderChild}>
<PostAlerts <PostAlerts
moderation={moderation.content} modui={moderation.ui('contentView')}
includeMute includeMute
style={styles.alert} style={styles.alert}
/> />
@@ -338,18 +337,7 @@ let PostThreadItemLoaded = ({
</View> </View>
) : undefined} ) : undefined}
{post.embed && ( {post.embed && (
<ContentHider <PostEmbeds embed={post.embed} moderation={moderation} />
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
ignoreQuoteDecisions
style={s.mb10}>
<PostEmbeds
embed={post.embed}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
/>
</ContentHider>
)} )}
</ContentHider> </ContentHider>
<ExpandedPostDetails <ExpandedPostDetails
@@ -431,7 +419,7 @@ let PostThreadItemLoaded = ({
testID={`postThreadItem-by-${post.author.handle}`} testID={`postThreadItem-by-${post.author.handle}`}
href={postHref} href={postHref}
style={[pal.view]} style={[pal.view]}
moderation={moderation.content} modui={moderation.ui('contentList')}
iconSize={isThreadedChild ? 26 : 38} iconSize={isThreadedChild ? 26 : 38}
iconStyles={ iconStyles={
isThreadedChild isThreadedChild
@@ -482,7 +470,7 @@ let PostThreadItemLoaded = ({
did={post.author.did} did={post.author.did}
handle={post.author.handle} handle={post.author.handle}
avatar={post.author.avatar} avatar={post.author.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
{showChildReplyLine && ( {showChildReplyLine && (
@@ -507,14 +495,14 @@ let PostThreadItemLoaded = ({
timestamp={post.indexedAt} timestamp={post.indexedAt}
postHref={postHref} postHref={postHref}
showAvatar={isThreadedChild} showAvatar={isThreadedChild}
avatarModeration={moderation.avatar} avatarModeration={moderation.ui('avatar')}
avatarSize={28} avatarSize={28}
displayNameType="md-bold" displayNameType="md-bold"
displayNameStyle={isThreadedChild && s.ml2} displayNameStyle={isThreadedChild && s.ml2}
style={isThreadedChild && s.mb2} style={isThreadedChild && s.mb2}
/> />
<PostAlerts <PostAlerts
moderation={moderation.content} modui={moderation.ui('contentList')}
style={styles.alert} style={styles.alert}
/> />
{richText?.text ? ( {richText?.text ? (
@@ -537,18 +525,7 @@ let PostThreadItemLoaded = ({
/> />
) : undefined} ) : undefined}
{post.embed && ( {post.embed && (
<ContentHider <PostEmbeds embed={post.embed} moderation={moderation} />
style={styles.contentHider}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
ignoreQuoteDecisions>
<PostEmbeds
embed={post.embed}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
/>
</ContentHider>
)} )}
<PostCtrls <PostCtrls
post={post} post={post}
+9 -16
View File
@@ -4,7 +4,7 @@ import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
PostModeration, ModerationDecision,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
@@ -92,7 +92,7 @@ function PostInner({
post: Shadow<AppBskyFeedDefs.PostView> post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
richText: RichTextAPI richText: RichTextAPI
moderation: PostModeration moderation: ModerationDecision
showReplyLine?: boolean showReplyLine?: boolean
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
@@ -141,7 +141,7 @@ function PostInner({
did={post.author.did} did={post.author.did}
handle={post.author.handle} handle={post.author.handle}
avatar={post.author.avatar} avatar={post.author.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
@@ -176,10 +176,13 @@ function PostInner({
</View> </View>
)} )}
<ContentHider <ContentHider
moderation={moderation.content} modui={moderation.ui('contentView')}
style={styles.contentHider} style={styles.contentHider}
childContainerStyle={styles.contentHiderChild}> childContainerStyle={styles.contentHiderChild}>
<PostAlerts moderation={moderation.content} style={styles.alert} /> <PostAlerts
modui={moderation.ui('contentView')}
style={styles.alert}
/>
{richText.text ? ( {richText.text ? (
<View style={styles.postTextContainer}> <View style={styles.postTextContainer}>
<RichText <RichText
@@ -201,17 +204,7 @@ function PostInner({
/> />
) : undefined} ) : undefined}
{post.embed ? ( {post.embed ? (
<ContentHider <PostEmbeds embed={post.embed} moderation={moderation} />
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
ignoreQuoteDecisions
style={styles.contentHider}>
<PostEmbeds
embed={post.embed}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
/>
</ContentHider>
) : null} ) : null}
</ContentHider> </ContentHider>
<PostCtrls <PostCtrls
+14 -24
View File
@@ -4,7 +4,7 @@ import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
PostModeration, ModerationDecision,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import { import {
@@ -28,7 +28,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {isEmbedByEmbedder} from 'lib/embeds'
import {MAX_POST_LINES} from 'lib/constants' import {MAX_POST_LINES} from 'lib/constants'
import {countLines} from 'lib/strings/helpers' import {countLines} from 'lib/strings/helpers'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
@@ -50,7 +49,7 @@ export function FeedItem({
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
moderation: PostModeration moderation: ModerationDecision
isThreadChild?: boolean isThreadChild?: boolean
isThreadLastChild?: boolean isThreadLastChild?: boolean
isThreadParent?: boolean isThreadParent?: boolean
@@ -98,7 +97,7 @@ let FeedItemInner = ({
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
richText: RichTextAPI richText: RichTextAPI
moderation: PostModeration moderation: ModerationDecision
isThreadChild?: boolean isThreadChild?: boolean
isThreadLastChild?: boolean isThreadLastChild?: boolean
isThreadParent?: boolean isThreadParent?: boolean
@@ -111,9 +110,12 @@ let FeedItemInner = ({
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey) return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author]) }, [post.uri, post.author])
const isModeratedPost = const isModeratedPost = !!moderation.causes.find(
moderation.decisions.post.cause?.type === 'label' && cause =>
moderation.decisions.post.cause.label.src !== currentAccount?.did cause?.type === 'label' &&
cause.label.src !== currentAccount?.did &&
cause.label.uri === post.uri,
)
const replyAuthorDid = useMemo(() => { const replyAuthorDid = useMemo(() => {
if (!record?.reply) { if (!record?.reply) {
@@ -247,7 +249,7 @@ let FeedItemInner = ({
did={post.author.did} did={post.author.did}
handle={post.author.handle} handle={post.author.handle}
avatar={post.author.avatar} avatar={post.author.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
{isThreadParent && ( {isThreadParent && (
<View <View
@@ -324,7 +326,7 @@ let PostContent = ({
postEmbed, postEmbed,
postAuthor, postAuthor,
}: { }: {
moderation: PostModeration moderation: ModerationDecision
richText: RichTextAPI richText: RichTextAPI
postEmbed: AppBskyFeedDefs.PostView['embed'] postEmbed: AppBskyFeedDefs.PostView['embed']
postAuthor: AppBskyFeedDefs.PostView['author'] postAuthor: AppBskyFeedDefs.PostView['author']
@@ -342,10 +344,10 @@ let PostContent = ({
return ( return (
<ContentHider <ContentHider
testID="contentHider-post" testID="contentHider-post"
moderation={moderation.content} modui={moderation.ui('contentList')}
ignoreMute ignoreMute
childContainerStyle={styles.contentHiderChild}> childContainerStyle={styles.contentHiderChild}>
<PostAlerts moderation={moderation.content} style={styles.alert} /> <PostAlerts modui={moderation.ui('contentList')} style={styles.alert} />
{richText.text ? ( {richText.text ? (
<View style={styles.postTextContainer}> <View style={styles.postTextContainer}>
<RichText <RichText
@@ -367,19 +369,7 @@ let PostContent = ({
/> />
) : undefined} ) : undefined}
{postEmbed ? ( {postEmbed ? (
<ContentHider <PostEmbeds embed={postEmbed} moderation={moderation} />
testID="contentHider-embed"
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
ignoreMute={isEmbedByEmbedder(postEmbed, postAuthor.did)}
ignoreQuoteDecisions
style={styles.embed}>
<PostEmbeds
embed={postEmbed}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
/>
</ContentHider>
) : null} ) : null}
</ContentHider> </ContentHider>
) )
+18 -17
View File
@@ -4,7 +4,7 @@ import {
AppBskyActorDefs, AppBskyActorDefs,
moderateProfile, moderateProfile,
ModerationCause, ModerationCause,
ProfileModeration, ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
@@ -15,7 +15,7 @@ import {FollowButton} from './FollowButton'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {getProfileModerationCauses, getModerationCauseKey} from 'lib/moderation' import {getModerationCauseKey} from 'lib/moderation'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -51,11 +51,8 @@ export function ProfileCard({
return null return null
} }
const moderation = moderateProfile(profile, moderationOpts) const moderation = moderateProfile(profile, moderationOpts)
if ( const modui = moderation.ui('profileList')
!noModFilter && if (!noModFilter && modui.filter /* TODO && modui.type !== 'muted'*/) {
moderation.account.filter &&
moderation.account.cause?.type !== 'muted'
) {
return null return null
} }
@@ -78,7 +75,7 @@ export function ProfileCard({
<UserAvatar <UserAvatar
size={40} size={40}
avatar={profile.avatar} avatar={profile.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
@@ -89,7 +86,7 @@ export function ProfileCard({
lineHeight={1.2}> lineHeight={1.2}>
{sanitizeDisplayName( {sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.profile, moderation.ui('displayName'),
)} )}
</Text> </Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}> <Text type="md" style={[pal.textLight]} numberOfLines={1}>
@@ -122,12 +119,12 @@ export function ProfileCardPills({
moderation, moderation,
}: { }: {
followedBy: boolean followedBy: boolean
moderation: ProfileModeration moderation: ModerationDecision
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const causes = getProfileModerationCauses(moderation) const informs = moderation.ui('profileList').informs
if (!followedBy && !causes.length) { if (!followedBy && !informs.length) {
return null return null
} }
@@ -140,10 +137,10 @@ export function ProfileCardPills({
</Text> </Text>
</View> </View>
)} )}
{causes.map(cause => ( {informs.map(inform => (
<ProfileCardPillModerationCause <ProfileCardPillModerationCause
key={getModerationCauseKey(cause)} key={getModerationCauseKey(inform)}
cause={cause} cause={inform}
/> />
))} ))}
</View> </View>
@@ -183,7 +180,7 @@ function FollowersList({
f, f,
mod: moderateProfile(f, moderationOpts), mod: moderateProfile(f, moderationOpts),
})) }))
.filter(({mod}) => !mod.account.filter) .filter(({mod}) => !mod.ui('profileList').filter)
}, [followers, moderationOpts]) }, [followers, moderationOpts])
if (!followersWithMods?.length) { if (!followersWithMods?.length) {
@@ -205,7 +202,11 @@ function FollowersList({
{followersWithMods.slice(0, 3).map(({f, mod}) => ( {followersWithMods.slice(0, 3).map(({f, mod}) => (
<View key={f.did} style={styles.followedByAviContainer}> <View key={f.did} style={styles.followedByAviContainer}>
<View style={[styles.followedByAvi, pal.view]}> <View style={[styles.followedByAvi, pal.view]}>
<UserAvatar avatar={f.avatar} size={32} moderation={mod.avatar} /> <UserAvatar
avatar={f.avatar}
size={32}
moderation={mod.ui('avatar')}
/>
</View> </View>
</View> </View>
))} ))}
+9 -8
View File
@@ -134,10 +134,8 @@ let ProfileHeader = ({
}, [navigation]) }, [navigation])
const onPressAvi = React.useCallback(() => { const onPressAvi = React.useCallback(() => {
if ( const modui = moderation.ui('avatar')
profile.avatar && if (profile.avatar && !(modui.blur && modui.noOverride)) {
!(moderation.avatar.blur && moderation.avatar.noOverride)
) {
openLightbox(new ProfileImageLightbox(profile)) openLightbox(new ProfileImageLightbox(profile))
} }
}, [openLightbox, profile, moderation]) }, [openLightbox, profile, moderation])
@@ -407,7 +405,10 @@ let ProfileHeader = ({
style={{borderRadius: 0}} style={{borderRadius: 0}}
/> />
) : ( ) : (
<UserBanner banner={profile.banner} moderation={moderation.avatar} /> <UserBanner
banner={profile.banner}
moderation={moderation.ui('banner')}
/>
)} )}
</View> </View>
<View style={styles.content} pointerEvents="box-none"> <View style={styles.content} pointerEvents="box-none">
@@ -538,7 +539,7 @@ let ProfileHeader = ({
style={[pal.text, styles.title]}> style={[pal.text, styles.title]}>
{sanitizeDisplayName( {sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.profile, moderation.ui('displayName'),
)} )}
</Text> </Text>
</View> </View>
@@ -611,7 +612,7 @@ let ProfileHeader = ({
</Text> </Text>
</Text> </Text>
</View> </View>
{descriptionRT && !moderation.profile.blur ? ( {descriptionRT && !moderation.ui('profileView').blur ? (
<View pointerEvents="auto"> <View pointerEvents="auto">
<RichText <RichText
testID="profileHeaderDescription" testID="profileHeaderDescription"
@@ -671,7 +672,7 @@ let ProfileHeader = ({
<UserAvatar <UserAvatar
size={80} size={80}
avatar={profile.avatar} avatar={profile.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
@@ -214,7 +214,7 @@ function SuggestedFollow({
<UserAvatar <UserAvatar
size={60} size={60}
avatar={profile.avatar} avatar={profile.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
<View style={{width: '100%', paddingVertical: 12}}> <View style={{width: '100%', paddingVertical: 12}}>
@@ -224,7 +224,7 @@ function SuggestedFollow({
numberOfLines={1}> numberOfLines={1}>
{sanitizeDisplayName( {sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.profile, moderation.ui('displayName'),
)} )}
</Text> </Text>
<Text <Text
+80 -89
View File
@@ -1,44 +1,44 @@
import React from 'react' import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {ModerationUI} from '@atproto/api'
import {usePalette} from 'lib/hooks/usePalette'
import {ModerationUI, PostModeration} from '@atproto/api'
import {Text} from '../text/Text'
import {ShieldExclamation} from 'lib/icons'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {isPostMediaBlurred} from 'lib/moderation'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Text} from '#/components/Typography'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
export function ContentHider({ export function ContentHider({
testID, testID,
moderation, modui,
moderationDecisions, // ignoreMute, TODO
ignoreMute,
ignoreQuoteDecisions,
style, style,
childContainerStyle, childContainerStyle,
children, children,
}: React.PropsWithChildren<{ }: React.PropsWithChildren<{
testID?: string testID?: string
moderation: ModerationUI modui: ModerationUI | undefined
moderationDecisions?: PostModeration['decisions']
ignoreMute?: boolean ignoreMute?: boolean
ignoreQuoteDecisions?: boolean
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle> childContainerStyle?: StyleProp<ViewStyle>
}>) { }>) {
const pal = usePalette('default') const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls() const {gtMobile} = useBreakpoints()
const desc = useModerationCauseDescription(moderation.cause, 'content') const openDialog = useOpenGlobalDialog()
const blur = modui?.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if ( if (
!moderation.blur || !blur
(ignoreMute && moderation.cause?.type === 'muted') || // || (ignoreMute && moderation.cause?.type === 'muted') TODO
shouldIgnoreQuote(moderationDecisions, ignoreQuoteDecisions)
) { ) {
return ( return (
<View testID={testID} style={[styles.outer, style]}> <View testID={testID} style={[styles.outer, style]}>
@@ -47,83 +47,74 @@ export function ContentHider({
) )
} }
const isMute = moderation.cause?.type === 'muted'
return ( return (
<View testID={testID} style={[styles.outer, style]}> <View testID={testID} style={[a.overflow_hidden, style]}>
<Pressable <View style={[a.flex_col, a.gap_xs]}>
onPress={() => { <Button
if (!moderation.noOverride) { variant="solid"
setOverride(v => !v) color="secondary"
} else { size={gtMobile ? 'large' : 'small'}
openModal({ shape="default"
name: 'moderation-details',
context: 'content',
moderation,
})
}
}}
accessibilityRole="button"
accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}
accessibilityLabel=""
style={[
styles.cover,
moderation.noOverride
? {borderWidth: 1, borderColor: pal.colors.borderDark}
: pal.viewLight,
]}>
<Pressable
onPress={() => { onPress={() => {
openModal({ if (!modui.noOverride) {
name: 'moderation-details', setOverride(v => !v)
context: 'content', } else {
moderation, openDialog(ModerationDetailsDialog, {
}) context: 'content',
modcause: blur,
})
}
}} }}
accessibilityRole="button" label={desc.name}
accessibilityLabel={_(msg`Learn more about this warning`)} accessibilityHint={
accessibilityHint=""> override ? _(msg`Hide the content`) : _(msg`Show the content`)
{isMute ? ( }>
<FontAwesomeIcon <ButtonIcon
icon={['far', 'eye-slash']} icon={blur.type === 'muted' ? EyeSlash : Shield}
size={18} position="left"
color={pal.colors.textLight} />{' '}
/> <ButtonText style={[a.flex_1, a.text_left]}>{desc.name}</ButtonText>
) : ( {!modui.noOverride && (
<ShieldExclamation size={18} style={pal.textLight} /> <ButtonText>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</ButtonText>
)} )}
</Pressable> </Button>
<Text type="md" style={[pal.text, {flex: 1}]} numberOfLines={2}> {blur.type === 'label' && !override && (
{desc.name} <Button
</Text> variant="ghost"
<View style={styles.showBtn}> size="tiny"
<Text type="lg" style={pal.link}> onPress={() => {
{moderation.noOverride ? ( openDialog(ModerationDetailsDialog, {
<Trans>Learn more</Trans> context: 'content',
) : override ? ( modcause: blur,
<Trans>Hide</Trans> })
) : ( }}
<Trans>Show</Trans> label={_(msg`Learn more`)}>
)} <ButtonText
</Text> style={[
</View> a.flex_1,
</Pressable> a.text_sm,
a.font_normal,
t.atoms.text_contrast_medium,
a.text_left,
]}>
{/* TODO get actual labeler */}
<Trans>
Labeled by Bluesky Safety.{' '}
<Text style={[{color: t.palette.primary_500}, a.text_sm]}>
Learn more.
</Text>
</Trans>
</ButtonText>
</Button>
)}
</View>
{override && <View style={childContainerStyle}>{children}</View>} {override && <View style={childContainerStyle}>{children}</View>}
</View> </View>
) )
} }
function shouldIgnoreQuote(
decisions: PostModeration['decisions'] | undefined,
ignore: boolean | undefined,
): boolean {
if (!decisions || !ignore) {
return false
}
return !isPostMediaBlurred(decisions)
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
overflow: 'hidden', overflow: 'hidden',
+82 -50
View File
@@ -1,67 +1,99 @@
import React from 'react' import React from 'react'
import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api' import {ModerationUI, ModerationCause} from '@atproto/api'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {Trans} from '@lingui/macro'
import {ShieldExclamation} from 'lib/icons'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {getModerationCauseKey} from '#/lib/moderation'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
export function PostAlerts({ export function PostAlerts({
moderation, modui,
style, style,
}: { }: {
moderation: ModerationUI modui: ModerationUI
includeMute?: boolean includeMute?: boolean
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const pal = usePalette('default') if (!modui.alert && !modui.inform) {
const {_} = useLingui()
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(moderation.cause, 'content')
const shouldAlert = !!moderation.cause && moderation.alert
if (!shouldAlert) {
return null return null
} }
return ( return (
<Pressable <View style={[a.flex_col, a.gap_xs, a.mb_sm, style]}>
onPress={() => { {modui.inform && (
openModal({ <View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
name: 'moderation-details', {modui.informs.map(cause => (
context: 'content', <PostInform key={getModerationCauseKey(cause)} cause={cause} />
moderation, ))}
}) </View>
}} )}
accessibilityRole="button" {modui.alerts.map(cause => (
accessibilityLabel={_(msg`Learn more about this warning`)} <PostAlert key={getModerationCauseKey(cause)} cause={cause} />
accessibilityHint="" ))}
style={[styles.container, pal.viewLight, style]}> </View>
<ShieldExclamation style={pal.text} size={16} />
<Text type="lg" style={[pal.text]}>
{desc.name}{' '}
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
<Trans>Learn More</Trans>
</Text>
</Text>
</Pressable>
) )
} }
const styles = StyleSheet.create({ function PostInform({cause}: {cause: ModerationCause}) {
container: { const openDialog = useOpenGlobalDialog()
flexDirection: 'row', const desc = useModerationCauseDescription(cause, 'content')
alignItems: 'center',
gap: 4, return (
paddingVertical: 8, <Button
paddingLeft: 14, label={desc.name}
paddingHorizontal: 16, variant="solid"
borderRadius: 8, color="secondary"
}, size="tiny"
learnMoreBtn: { shape="default"
marginLeft: 'auto', onPress={() => {
}, openDialog(ModerationDetailsDialog, {
}) context: 'content',
modcause: cause,
})
}}>
<ButtonIcon
icon={cause.type === 'muted' ? EyeSlash : CircleInfo}
position="left"
/>{' '}
<ButtonText>{desc.name}</ButtonText>
</Button>
)
}
function PostAlert({cause}: {cause: ModerationCause}) {
const t = useTheme()
const openDialog = useOpenGlobalDialog()
const desc = useModerationCauseDescription(cause, 'content')
return (
<Button
label={desc.name}
variant="solid"
color="secondary"
size="small"
shape="default"
onPress={() => {
openDialog(ModerationDetailsDialog, {
context: 'content',
modcause: cause,
})
}}>
<ButtonIcon icon={Shield} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}>
{desc.name}
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — ' /* TODO get actual labeler */}
<Trans>Bluesky Safety</Trans>
</Text>
</ButtonText>
</Button>
)
}
+14 -12
View File
@@ -9,19 +9,21 @@ import {addStyle} from 'lib/styles'
import {ShieldExclamation} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
interface Props extends ComponentProps<typeof Link> { interface Props extends ComponentProps<typeof Link> {
iconSize: number iconSize: number
iconStyles: StyleProp<ViewStyle> iconStyles: StyleProp<ViewStyle>
moderation: ModerationUI modui: ModerationUI
} }
export function PostHider({ export function PostHider({
testID, testID,
href, href,
moderation, modui,
style, style,
children, children,
iconSize, iconSize,
@@ -31,10 +33,11 @@ export function PostHider({
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls() const openDialog = useOpenGlobalDialog()
const desc = useModerationCauseDescription(moderation.cause, 'content') const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if (!moderation.blur) { if (!blur) {
return ( return (
<Link <Link
testID={testID} testID={testID}
@@ -48,11 +51,11 @@ export function PostHider({
) )
} }
const isMute = moderation.cause?.type === 'muted' const isMute = blur.type === 'muted'
return !override ? ( return !override ? (
<Pressable <Pressable
onPress={() => { onPress={() => {
if (!moderation.noOverride) { if (!modui.noOverride) {
setOverride(v => !v) setOverride(v => !v)
} }
}} }}
@@ -68,10 +71,9 @@ export function PostHider({
]}> ]}>
<Pressable <Pressable
onPress={() => { onPress={() => {
openModal({ openDialog(ModerationDetailsDialog, {
name: 'moderation-details',
context: 'content', context: 'content',
moderation, modcause: blur,
}) })
}} }}
accessibilityRole="button" accessibilityRole="button"
@@ -103,7 +105,7 @@ export function PostHider({
<Text type="sm" style={[{flex: 1}, pal.textLight]} numberOfLines={1}> <Text type="sm" style={[{flex: 1}, pal.textLight]} numberOfLines={1}>
{desc.name} {desc.name}
</Text> </Text>
{!moderation.noOverride && ( {!modui.noOverride && (
<Text type="sm" style={[styles.showBtn, pal.link]}> <Text type="sm" style={[styles.showBtn, pal.link]}>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>} {override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</Text> </Text>
@@ -1,101 +1,99 @@
import React from 'react' import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationCause, ProfileModeration} from '@atproto/api' import {ModerationCause, ModerationDecision} from '@atproto/api'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {getModerationCauseKey} from 'lib/moderation'
import {ShieldExclamation} from 'lib/icons' import {Trans} from '@lingui/macro'
import {getModerationCauseKey, getProfileModerationCauses} from 'lib/moderation'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {Shield_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
export function ProfileHeaderAlerts({ export function ProfileHeaderAlerts({
moderation, moderation,
style, style,
}: { }: {
moderation: ProfileModeration moderation: ModerationDecision
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const causes = getProfileModerationCauses(moderation) const modui = moderation.ui('profileView')
if (!causes.length) { if (!modui.alert && !modui.inform) {
return null return null
} }
return ( return (
<View style={styles.grid}> <View style={[a.flex_col, a.gap_xs, a.mb_sm, style]}>
{causes.map(cause => ( {modui.inform && (
<ProfileHeaderAlert <View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
key={getModerationCauseKey(cause)} {modui.informs.map(cause => (
cause={cause} <ProfileInform key={getModerationCauseKey(cause)} cause={cause} />
style={style} ))}
/> </View>
)}
{modui.alerts.map(cause => (
<ProfileAlert key={getModerationCauseKey(cause)} cause={cause} />
))} ))}
</View> </View>
) )
} }
function ProfileHeaderAlert({ function ProfileInform({cause}: {cause: ModerationCause}) {
cause, const openDialog = useOpenGlobalDialog()
style, const desc = useModerationCauseDescription(cause, 'content')
}: {
cause: ModerationCause
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(cause, 'account')
const isMute = cause.type === 'muted'
return ( return (
<Pressable <Button
testID="profileHeaderAlert" label={desc.name}
key={desc.name} variant="solid"
color="secondary"
size="tiny"
shape="default"
onPress={() => { onPress={() => {
openModal({ openDialog(ModerationDetailsDialog, {
name: 'moderation-details',
context: 'content', context: 'content',
moderation: {cause}, modcause: cause,
}) })
}} }}>
accessibilityRole="button" <ButtonIcon
accessibilityLabel={_(msg`Learn more about this warning`)} icon={cause.type === 'muted' ? EyeSlash : CircleInfo}
accessibilityHint="" position="left"
style={[styles.container, pal.viewLight, style]}> />{' '}
{isMute ? ( <ButtonText>{desc.name}</ButtonText>
<FontAwesomeIcon </Button>
icon={['far', 'eye-slash']}
size={14}
color={pal.colors.textLight}
/>
) : (
<ShieldExclamation style={pal.text} size={18} />
)}
<Text type="sm" style={[{flex: 1}, pal.text]}>
{desc.name}
</Text>
<Text type="sm" style={[pal.link, styles.learnMoreBtn]}>
<Trans>Learn More</Trans>
</Text>
</Pressable>
) )
} }
const styles = StyleSheet.create({ function ProfileAlert({cause}: {cause: ModerationCause}) {
grid: { const t = useTheme()
gap: 4, const openDialog = useOpenGlobalDialog()
}, const desc = useModerationCauseDescription(cause, 'content')
container: {
flexDirection: 'row', return (
alignItems: 'center', <Button
gap: 8, label={desc.name}
paddingVertical: 12, variant="solid"
paddingHorizontal: 16, color="secondary"
borderRadius: 8, size="small"
}, shape="default"
learnMoreBtn: { onPress={() => {
marginLeft: 'auto', openDialog(ModerationDetailsDialog, {
}, context: 'content',
}) modcause: cause,
})
}}>
<ButtonIcon icon={Shield} position="left" />
<ButtonText style={[a.flex_1, a.text_left]}>
{desc.name}
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{' — ' /* TODO get actual labeler */}
<Trans>Bluesky Safety</Trans>
</Text>
</ButtonText>
</Button>
)
}
+16 -13
View File
@@ -19,22 +19,24 @@ import {Text} from '../text/Text'
import {Button} from '../forms/Button' import {Button} from '../forms/Button'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {CenteredView} from '../Views' import {CenteredView} from '../Views'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
export function ScreenHider({ export function ScreenHider({
testID, testID,
screenDescription, screenDescription,
moderation, modui,
style, style,
containerStyle, containerStyle,
children, children,
}: React.PropsWithChildren<{ }: React.PropsWithChildren<{
testID?: string testID?: string
screenDescription: string screenDescription: string
moderation: ModerationUI modui: ModerationUI
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
containerStyle?: StyleProp<ViewStyle> containerStyle?: StyleProp<ViewStyle>
}>) { }>) {
@@ -44,10 +46,11 @@ export function ScreenHider({
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls() const openDialog = useOpenGlobalDialog()
const desc = useModerationCauseDescription(moderation.cause, 'account') const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if (!moderation.blur || override) { if (!blur || override) {
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
{children} {children}
@@ -55,9 +58,10 @@ export function ScreenHider({
) )
} }
const isNoPwi = const isNoPwi = !!modui.blurs.find(
moderation.cause?.type === 'label' && cause =>
moderation.cause?.labelDef.id === '!no-unauthenticated' cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated',
)
return ( return (
<CenteredView <CenteredView
style={[styles.container, pal.view, containerStyle]} style={[styles.container, pal.view, containerStyle]}
@@ -91,10 +95,9 @@ export function ScreenHider({
</Text> </Text>
<TouchableWithoutFeedback <TouchableWithoutFeedback
onPress={() => { onPress={() => {
openModal({ openDialog(ModerationDetailsDialog, {
name: 'moderation-details',
context: 'account', context: 'account',
moderation, modcause: blur,
}) })
}} }}
accessibilityRole="button" accessibilityRole="button"
@@ -123,7 +126,7 @@ export function ScreenHider({
<Trans>Go back</Trans> <Trans>Go back</Trans>
</Text> </Text>
</Button> </Button>
{!moderation.noOverride && ( {!modui.noOverride && (
<Button <Button
type="default" type="default"
onPress={() => setOverride(v => !v)} onPress={() => setOverride(v => !v)}
+85 -44
View File
@@ -1,13 +1,15 @@
import React from 'react' import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import { import {
AppBskyFeedDefs,
AppBskyEmbedRecord, AppBskyEmbedRecord,
AppBskyFeedPost, AppBskyFeedPost,
AppBskyEmbedImages, AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia, AppBskyEmbedRecordWithMedia,
ModerationUI,
AppBskyEmbedExternal, AppBskyEmbedExternal,
RichText as RichTextAPI, RichText as RichTextAPI,
moderatePost,
ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {PostMeta} from '../PostMeta' import {PostMeta} from '../PostMeta'
@@ -21,14 +23,14 @@ import {makeProfileLink} from 'lib/routes/links'
import {InfoCircleIcon} from 'lib/icons' import {InfoCircleIcon} from 'lib/icons'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {RichText} from 'view/com/util/text/RichText' import {RichText} from 'view/com/util/text/RichText'
import {useModerationOpts} from '#/state/queries/preferences'
import {ContentHider} from '../moderation/ContentHider'
export function MaybeQuoteEmbed({ export function MaybeQuoteEmbed({
embed, embed,
moderation,
style, style,
}: { }: {
embed: AppBskyEmbedRecord.View embed: AppBskyEmbedRecord.View
moderation: ModerationUI
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
@@ -38,17 +40,9 @@ export function MaybeQuoteEmbed({
AppBskyFeedPost.validateRecord(embed.record.value).success AppBskyFeedPost.validateRecord(embed.record.value).success
) { ) {
return ( return (
<QuoteEmbed <QuoteEmbedModerated
quote={{ viewRecord={embed.record}
author: embed.record.author, postRecord={embed.record.value}
cid: embed.record.cid,
uri: embed.record.uri,
indexedAt: embed.record.indexedAt,
text: embed.record.value.text,
facets: embed.record.value.facets,
embeds: embed.record.embeds,
}}
moderation={moderation}
style={style} style={style}
/> />
) )
@@ -74,19 +68,49 @@ export function MaybeQuoteEmbed({
return null return null
} }
function QuoteEmbedModerated({
viewRecord,
postRecord,
style,
}: {
viewRecord: AppBskyEmbedRecord.ViewRecord
postRecord: AppBskyFeedPost.Record
style?: StyleProp<ViewStyle>
}) {
const moderationOpts = useModerationOpts()
const moderation = React.useMemo(() => {
return moderationOpts
? moderatePost(viewRecordToPostView(viewRecord), moderationOpts)
: undefined
}, [viewRecord, moderationOpts])
const quote = {
author: viewRecord.author,
cid: viewRecord.cid,
uri: viewRecord.uri,
indexedAt: viewRecord.indexedAt,
text: postRecord.text,
facets: postRecord.facets,
embeds: viewRecord.embeds,
}
return <QuoteEmbed quote={quote} moderation={moderation} style={style} />
}
export function QuoteEmbed({ export function QuoteEmbed({
quote, quote,
moderation, moderation,
style, style,
}: { }: {
quote: ComposerOptsQuote quote: ComposerOptsQuote
moderation?: ModerationUI moderation?: ModerationDecision
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const itemUrip = new AtUri(quote.uri) const itemUrip = new AtUri(quote.uri)
const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey)
const itemTitle = `Post by ${quote.author.handle}` const itemTitle = `Post by ${quote.author.handle}`
const richText = React.useMemo( const richText = React.useMemo(
() => () =>
quote.text.trim() quote.text.trim()
@@ -94,6 +118,7 @@ export function QuoteEmbed({
: undefined, : undefined,
[quote.text, quote.facets], [quote.text, quote.facets],
) )
const embed = React.useMemo(() => { const embed = React.useMemo(() => {
const e = quote.embeds?.[0] const e = quote.embeds?.[0]
@@ -107,39 +132,55 @@ export function QuoteEmbed({
return e.media return e.media
} }
}, [quote.embeds]) }, [quote.embeds])
return ( return (
<Link <ContentHider modui={moderation?.ui('contentList')}>
style={[styles.container, pal.borderDark, style]} <Link
hoverStyle={{borderColor: pal.colors.borderLinkHover}} style={[styles.container, pal.borderDark, style]}
href={itemHref} hoverStyle={{borderColor: pal.colors.borderLinkHover}}
title={itemTitle}> href={itemHref}
<View pointerEvents="none"> title={itemTitle}>
<PostMeta <View pointerEvents="none">
author={quote.author} <PostMeta
showAvatar author={quote.author}
authorHasWarning={false} showAvatar
postHref={itemHref} authorHasWarning={false}
timestamp={quote.indexedAt} postHref={itemHref}
/> timestamp={quote.indexedAt}
</View> />
{moderation ? ( </View>
<PostAlerts moderation={moderation} style={styles.alert} /> {moderation ? (
) : null} <PostAlerts
{richText ? ( modui={moderation.ui('contentView')}
<RichText style={styles.alert}
richText={richText} />
type="post-text" ) : null}
style={pal.text} {richText ? (
numberOfLines={20} <RichText
noLinks richText={richText}
/> type="post-text"
) : null} style={pal.text}
{embed && <PostEmbeds embed={embed} moderation={{}} />} numberOfLines={20}
</Link> noLinks
/>
) : null}
{embed && <PostEmbeds embed={embed} moderation={moderation} />}
</Link>
</ContentHider>
) )
} }
export default QuoteEmbed function viewRecordToPostView(
viewRecord: AppBskyEmbedRecord.ViewRecord,
): AppBskyFeedDefs.PostView {
const {value, embeds, ...rest} = viewRecord
return {
...rest,
$type: 'app.bsky.feed.defs#postView',
record: value,
embed: embeds?.[0],
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
+49 -55
View File
@@ -15,8 +15,7 @@ import {
AppBskyEmbedRecordWithMedia, AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyGraphDefs, AppBskyGraphDefs,
ModerationUI, ModerationDecision,
PostModeration,
} from '@atproto/api' } from '@atproto/api'
import {Link} from '../Link' import {Link} from '../Link'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
@@ -26,7 +25,6 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {MaybeQuoteEmbed} from './QuoteEmbed' import {MaybeQuoteEmbed} from './QuoteEmbed'
import {AutoSizedImage} from '../images/AutoSizedImage' import {AutoSizedImage} from '../images/AutoSizedImage'
import {ListEmbed} from './ListEmbed' import {ListEmbed} from './ListEmbed'
import {isCauseALabelOnUri, isQuoteBlurred} from 'lib/moderation'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {ContentHider} from '../moderation/ContentHider' import {ContentHider} from '../moderation/ContentHider'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
@@ -42,12 +40,10 @@ type Embed =
export function PostEmbeds({ export function PostEmbeds({
embed, embed,
moderation, moderation,
moderationDecisions,
style, style,
}: { }: {
embed?: Embed embed?: Embed
moderation: ModerationUI moderation?: ModerationDecision
moderationDecisions?: PostModeration['decisions']
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
@@ -66,18 +62,10 @@ export function PostEmbeds({
// quote post with media // quote post with media
// = // =
if (AppBskyEmbedRecordWithMedia.isView(embed)) { if (AppBskyEmbedRecordWithMedia.isView(embed)) {
const isModOnQuote =
(AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
isCauseALabelOnUri(moderation.cause, embed.record.record.uri)) ||
(moderationDecisions && isQuoteBlurred(moderationDecisions))
const mediaModeration = isModOnQuote ? {} : moderation
const quoteModeration = isModOnQuote ? moderation : {}
return ( return (
<View style={style}> <View style={style}>
<PostEmbeds embed={embed.media} moderation={mediaModeration} /> <PostEmbeds embed={embed.media} moderation={moderation} />
<ContentHider moderation={quoteModeration}> <MaybeQuoteEmbed embed={embed.record} />
<MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} />
</ContentHider>
</View> </View>
) )
} }
@@ -86,6 +74,7 @@ export function PostEmbeds({
// custom feed embed (i.e. generator view) // custom feed embed (i.e. generator view)
// = // =
if (AppBskyFeedDefs.isGeneratorView(embed.record)) { if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
// TODO moderation
return ( return (
<FeedSourceCard <FeedSourceCard
feedUri={embed.record.uri} feedUri={embed.record.uri}
@@ -97,16 +86,13 @@ export function PostEmbeds({
// list embed // list embed
if (AppBskyGraphDefs.isListView(embed.record)) { if (AppBskyGraphDefs.isListView(embed.record)) {
// TODO moderation
return <ListEmbed item={embed.record} /> return <ListEmbed item={embed.record} />
} }
// quote post // quote post
// = // =
return ( return <MaybeQuoteEmbed embed={embed} style={style} />
<ContentHider moderation={moderation}>
<MaybeQuoteEmbed embed={embed} style={style} moderation={moderation} />
</ContentHider>
)
} }
// image embed // image embed
@@ -132,35 +118,41 @@ export function PostEmbeds({
if (images.length === 1) { if (images.length === 1) {
const {alt, thumb, aspectRatio} = images[0] const {alt, thumb, aspectRatio} = images[0]
return ( return (
<View style={[styles.imagesContainer, style]}> <ContentHider modui={moderation?.ui('contentMedia')}>
<AutoSizedImage <View style={[styles.imagesContainer, style]}>
alt={alt} <AutoSizedImage
uri={thumb} alt={alt}
dimensionsHint={aspectRatio} uri={thumb}
onPress={() => _openLightbox(0)} dimensionsHint={aspectRatio}
onPressIn={() => onPressIn(0)} onPress={() => _openLightbox(0)}
style={[styles.singleImage]}> onPressIn={() => onPressIn(0)}
{alt === '' ? null : ( style={[styles.singleImage]}>
<View style={styles.altContainer}> {alt === '' ? null : (
<Text style={styles.alt} accessible={false}> <View style={styles.altContainer}>
ALT <Text style={styles.alt} accessible={false}>
</Text> ALT
</View> </Text>
)} </View>
</AutoSizedImage> )}
</View> </AutoSizedImage>
</View>
</ContentHider>
) )
} }
return ( return (
<View style={[styles.imagesContainer, style]}> <ContentHider modui={moderation?.ui('contentMedia')}>
<ImageLayoutGrid <View style={[styles.imagesContainer, style]}>
images={embed.images} <ImageLayoutGrid
onPress={_openLightbox} images={embed.images}
onPressIn={onPressIn} onPress={_openLightbox}
style={embed.images.length === 1 ? [styles.singleImage] : undefined} onPressIn={onPressIn}
/> style={
</View> embed.images.length === 1 ? [styles.singleImage] : undefined
}
/>
</View>
</ContentHider>
) )
} }
} }
@@ -171,15 +163,17 @@ export function PostEmbeds({
const link = embed.external const link = embed.external
return ( return (
<Link <ContentHider modui={moderation?.ui('contentMedia')}>
asAnchor <Link
anchorNoUnderline asAnchor
href={link.uri} anchorNoUnderline
style={[styles.extOuter, pal.view, pal.borderDark, style]} href={link.uri}
hoverStyle={{borderColor: pal.colors.borderLinkHover}} style={[styles.extOuter, pal.view, pal.borderDark, style]}
onLongPress={onShareExternal}> hoverStyle={{borderColor: pal.colors.borderLinkHover}}
<ExternalLinkEmbed link={link} /> onLongPress={onShareExternal}>
</Link> <ExternalLinkEmbed link={link} />
</Link>
</ContentHider>
) )
} }
+366 -338
View File
@@ -7,14 +7,16 @@ import {
moderatePost, moderatePost,
moderateProfile, moderateProfile,
ModerationOpts, ModerationOpts,
PostModeration,
ProfileModeration,
ModerationUI,
AppBskyActorDefs, AppBskyActorDefs,
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost,
LabelTarget, LabelTarget,
LabelPreference, LabelPreference,
ModerationDecision,
ModerationBehavior,
RichText,
} from '@atproto/api' } from '@atproto/api'
import {moderationOptsOverrideContext} from '#/state/queries/preferences'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {CenteredView, ScrollView} from '#/view/com/util/Views' import {CenteredView, ScrollView} from '#/view/com/util/Views'
@@ -22,23 +24,24 @@ import {H1, H3, P, Text} from '#/components/Typography'
import {useLabelStrings} from '#/lib/moderation/useLabelStrings' import {useLabelStrings} from '#/lib/moderation/useLabelStrings'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as ToggleButton from '#/components/forms/ToggleButton'
import {UserAvatar} from '../com/util/UserAvatar' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PostHider} from '../com/util/moderation/PostHider' import {
import {PostAlerts} from '../com/util/moderation/PostAlerts' ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
import {ContentHider} from '../com/util/moderation/ContentHider' ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
} from '#/components/icons/Chevron'
import {ScreenHider} from '../com/util/moderation/ScreenHider' import {ScreenHider} from '../com/util/moderation/ScreenHider'
import {ProfileHeader} from '../com/profile/ProfileHeader' import {ProfileHeader} from '../com/profile/ProfileHeader'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {ProfileCard} from '../com/profile/ProfileCard'
import {ProfileCardPills} from '../com/profile/ProfileCard' import {FeedItem} from '../com/posts/FeedItem'
import {PostThreadItem} from '../com/post-thread/PostThreadItem'
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys( const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
LABELS, LABELS,
) as (keyof typeof LABELS)[] ) as (keyof typeof LABELS)[]
const MOCK_MOD_OPTS = { const MOCK_MOD_OPTS = {
userDid: 'at://did:web:alice', userDid: 'did:web:alice.test',
adultContentEnabled: true, adultContentEnabled: true,
labelGroups: {}, labelGroups: {},
mods: [ mods: [
@@ -54,36 +57,53 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
'DebugMod' 'DebugMod'
>) => { >) => {
const t = useTheme() const t = useTheme()
const [scenario, setScenario] = React.useState<string[]>(['label'])
const [scenarioSwitches, setScenarioSwitches] = React.useState<string[]>([])
const [label, setLabel] = React.useState<string[]>([LABEL_VALUES[0]]) const [label, setLabel] = React.useState<string[]>([LABEL_VALUES[0]])
const [target, setTarget] = React.useState<string[]>(['account']) const [target, setTarget] = React.useState<string[]>(['account'])
const [visibility, setVisiblity] = React.useState<string[]>(['hide']) const [visibility, setVisiblity] = React.useState<string[]>(['hide'])
const labelStrings = useLabelStrings() const labelStrings = useLabelStrings()
const isTargetMe =
scenario[0] === 'label' && scenarioSwitches.includes('targetMe')
const isSelfLabel =
scenario[0] === 'label' && scenarioSwitches.includes('selfLabel')
const noAdult =
scenario[0] === 'label' && scenarioSwitches.includes('noAdult')
const isLoggedOut =
scenario[0] === 'label' && scenarioSwitches.includes('loggedOut')
const profile = React.useMemo(() => { const profile = React.useMemo(() => {
const mockedProfile = mock.profileViewBasic({ const mockedProfile = mock.profileViewBasic({
handle: `bob.test`, handle: `bob.test`,
displayName: 'Bob Robertson', displayName: 'Bob Robertson',
description: 'User with this as their bio',
labels: labels:
target[0] === 'account' scenario[0] === 'label' && target[0] === 'account'
? [ ? [
mock.label({ mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
val: label[0], val: label[0],
uri: `at://did:web:bob/`, uri: `at://did:web:bob.test/`,
}), }),
] ]
: target[0] === 'profile' : scenario[0] === 'label' && target[0] === 'profile'
? [ ? [
mock.label({ mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
val: label[0], val: label[0],
uri: `at://did:web:bob/app.bsky.actor.profile/self`, uri: `at://did:web:bob.test/app.bsky.actor.profile/self`,
}), }),
] ]
: undefined, : undefined,
viewer: mock.actorViewerState({ viewer: mock.actorViewerState({
muted: false, muted: scenario[0] === 'mute',
mutedByList: undefined, mutedByList: undefined,
blockedBy: undefined, blockedBy: undefined,
blocking: undefined, blocking:
scenario[0] === 'block'
? 'did://did:web:alice/app.bsky.actor.block/fake'
: undefined,
blockingByList: undefined, blockingByList: undefined,
}), }),
}) })
@@ -91,7 +111,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
mockedProfile.banner = mockedProfile.banner =
'https://bsky.social/about/images/social-card-default-gradient.png' 'https://bsky.social/about/images/social-card-default-gradient.png'
return mockedProfile return mockedProfile
}, [target, label]) }, [scenario, target, label, isSelfLabel])
const post = React.useMemo(() => { const post = React.useMemo(() => {
return mock.postView({ return mock.postView({
@@ -100,41 +120,63 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
}), }),
author: profile, author: profile,
labels: labels:
target[0] === 'post' scenario[0] === 'label' && target[0] === 'post'
? [ ? [
mock.label({ mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
val: label[0], val: label[0],
uri: `at://bob.test/app.bsky.feed.post/fake`, uri: `at://bob.test/app.bsky.feed.post/fake`,
}), }),
] ]
: undefined, : undefined,
embed: mock.embedRecordView({ embed:
record: mock.post({ target[0] === 'embed'
text: 'Embed', ? mock.embedRecordView({
}), record: mock.post({
labels: text: 'Embed',
target[0] === 'embed' }),
? [ labels:
mock.label({ scenario[0] === 'label' && target[0] === 'embed'
val: label[0], ? [
uri: `at://bob.test/app.bsky.feed.post/fake`, mock.label({
}), src: isSelfLabel ? 'did:web:bob.test' : undefined,
] val: label[0],
: undefined, uri: `at://bob.test/app.bsky.feed.post/fake`,
author: profile, }),
}), ]
: undefined,
author: profile,
})
: {
$type: 'app.bsky.embed.images#view',
images: [
{
thumb:
'https://bsky.social/about/images/social-card-default-gradient.png',
fullsize:
'https://bsky.social/about/images/social-card-default-gradient.png',
alt: '',
},
],
},
}) })
}, [label, target, profile]) }, [scenario, label, target, profile, isSelfLabel])
const modOpts = React.useMemo(() => { const modOpts = React.useMemo(() => {
return { return {
...MOCK_MOD_OPTS, ...MOCK_MOD_OPTS,
userDid: isLoggedOut
? ''
: isTargetMe
? 'did:web:bob.test'
: 'did:web:alice.test',
adultContentEnabled: !noAdult,
labelGroups: { labelGroups: {
[LABELS[label[0] as keyof typeof LABELS].groupId]: [LABELS[label[0] as keyof typeof LABELS].groupId]:
visibility[0] as LabelPreference, visibility[0] as LabelPreference,
}, },
} }
}, [label, visibility]) }, [label, visibility, noAdult, isLoggedOut, isTargetMe])
const profileModeration = React.useMemo(() => { const profileModeration = React.useMemo(() => {
return moderateProfile(profile, modOpts) return moderateProfile(profile, modOpts)
@@ -143,188 +185,201 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
return moderatePost(post, modOpts) return moderatePost(post, modOpts)
}, [post, modOpts]) }, [post, modOpts])
console.log(post, profile) console.log(post)
console.log(profileModeration, postModeration)
return ( return (
<ScrollView> <moderationOptsOverrideContext.Provider value={modOpts}>
<CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}> <ScrollView>
<H1 style={[a.text_5xl, a.font_bold, a.pb_lg]}>Moderation states</H1> <CenteredView style={[t.atoms.bg, a.px_lg, a.py_lg]}>
<H1 style={[a.text_5xl, a.font_bold, a.pb_lg]}>Moderation states</H1>
<Heading title="Config" /> <Heading title="Config" />
<Heading title="" subtitle="Target" />
<ToggleButton.Group label="Target" values={target} onChange={setTarget}>
<ToggleButton.Button name="account" label="Account">
Account
</ToggleButton.Button>
<ToggleButton.Button name="profile" label="Profile">
Profile
</ToggleButton.Button>
<ToggleButton.Button name="post" label="Post">
Post
</ToggleButton.Button>
<ToggleButton.Button name="embed" label="Embed">
Embed
</ToggleButton.Button>
</ToggleButton.Group>
<View style={{height: 10}} /> <Heading title="" subtitle="Scenario" />
<ToggleButton.Group
label="Scenario"
values={scenario}
onChange={setScenario}>
<ToggleButton.Button name="label" label="Label">
Label
</ToggleButton.Button>
<ToggleButton.Button name="block" label="Block">
Block
</ToggleButton.Button>
<ToggleButton.Button name="mute" label="Mute">
Mute
</ToggleButton.Button>
</ToggleButton.Group>
<Heading title="" subtitle="Preference" /> {scenario[0] === 'label' && (
<ToggleButton.Group <>
label="Visiblity" <Toggle.Group
values={visibility} label="Toggle"
onChange={setVisiblity}> type="checkbox"
<ToggleButton.Button name="hide" label="Hide"> values={scenarioSwitches}
Hide onChange={setScenarioSwitches}>
</ToggleButton.Button> <View style={[a.gap_md, a.flex_row, a.pt_md]}>
<ToggleButton.Button name="warn" label="Warn"> <Toggle.Item name="targetMe" label="Target is me">
Warn <Toggle.Checkbox />
</ToggleButton.Button> <Toggle.Label>Target is me</Toggle.Label>
<ToggleButton.Button name="ignore" label="Ignore"> </Toggle.Item>
Ignore <Toggle.Item name="selfLabel" label="Self label">
</ToggleButton.Button> <Toggle.Checkbox />
</ToggleButton.Group> <Toggle.Label>Self label</Toggle.Label>
</Toggle.Item>
<Toggle.Item name="noAdult" label="Adult disabled">
<Toggle.Checkbox />
<Toggle.Label>Adult disabled</Toggle.Label>
</Toggle.Item>
<Toggle.Item name="loggedOut" label="Logged out">
<Toggle.Checkbox />
<Toggle.Label>Logged out</Toggle.Label>
</Toggle.Item>
</View>
</Toggle.Group>
<View style={{height: 10}} />
<View style={{height: 10}} /> <Heading title="" subtitle="Target" />
<ToggleButton.Group
label="Target"
values={target}
onChange={setTarget}>
<ToggleButton.Button name="account" label="Account">
Account
</ToggleButton.Button>
<ToggleButton.Button name="profile" label="Profile">
Profile
</ToggleButton.Button>
<ToggleButton.Button name="post" label="Post">
Post
</ToggleButton.Button>
<ToggleButton.Button name="embed" label="Embed">
Embed
</ToggleButton.Button>
</ToggleButton.Group>
<Heading title="" subtitle="Label" /> <View style={{height: 10}} />
<Toggle.Group
label="Toggle"
type="radio"
values={label}
onChange={setLabel}>
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
{LABEL_VALUES.map(labelValue => {
let targetFixed = target[0]
if (targetFixed !== 'account' && targetFixed !== 'profile') {
targetFixed = 'content'
}
const disabled = !LABELS[labelValue].targets.includes(
targetFixed as LabelTarget,
)
return (
<Toggle.Item
key={labelValue}
name={labelValue}
label={labelStrings[labelValue].general.name}
disabled={disabled}
style={disabled ? {opacity: 0.5} : undefined}>
<Toggle.Radio />
<Toggle.Label>{labelValue}</Toggle.Label>
</Toggle.Item>
)
})}
</View>
</Toggle.Group>
<Spacer /> <Heading title="" subtitle="Preference" />
<ToggleButton.Group
label="Visiblity"
values={visibility}
onChange={setVisiblity}>
<ToggleButton.Button name="hide" label="Hide">
Hide
</ToggleButton.Button>
<ToggleButton.Button name="warn" label="Warn">
Warn
</ToggleButton.Button>
<ToggleButton.Button name="ignore" label="Ignore">
Ignore
</ToggleButton.Button>
</ToggleButton.Group>
<Heading title={label[0]} /> <View style={{height: 10}} />
<Text style={{fontFamily: 'monospace'}}>
{JSON.stringify(LABELS[label[0]], null, 2)}
</Text>
<Spacer /> <Heading title="" subtitle="Label" />
<Toggle.Group
label="Toggle"
type="radio"
values={label}
onChange={setLabel}>
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
{LABEL_VALUES.map(labelValue => {
let targetFixed = target[0]
if (
targetFixed !== 'account' &&
targetFixed !== 'profile'
) {
targetFixed = 'content'
}
const disabled =
!LABELS[labelValue].targets.includes(
targetFixed as LabelTarget,
) ||
(isSelfLabel &&
LABELS[labelValue].flags.includes('no-self'))
return (
<Toggle.Item
key={labelValue}
name={labelValue}
label={labelStrings[labelValue].general.name}
disabled={disabled}
style={disabled ? {opacity: 0.5} : undefined}>
<Toggle.Radio />
<Toggle.Label>{labelValue}</Toggle.Label>
</Toggle.Item>
)
})}
</View>
</Toggle.Group>
</>
)}
<Heading title="Output" /> <Spacer />
<P style={[a.font_bold]}>Post moderation</P>
<View
style={[
t.atoms.border_contrast_low,
a.border,
a.px_md,
a.py_sm,
a.rounded_sm,
a.flex_col,
a.gap_xs,
]}>
<ModerationUIView mod={postModeration.avatar} label="Avatar" />
<ModerationUIView mod={postModeration.content} label="Content" />
<ModerationUIView mod={postModeration.embed} label="Embed" />
</View>
<P style={[a.font_bold, a.mt_md]}>Profile Moderation</P>
<View
style={[
t.atoms.border_contrast_low,
a.border,
a.px_md,
a.py_sm,
a.rounded_sm,
a.flex_col,
a.gap_xs,
]}>
<ModerationUIView mod={profileModeration.avatar} label="Avatar" />
<ModerationUIView mod={profileModeration.account} label="Account" />
<ModerationUIView mod={profileModeration.profile} label="Profile" />
</View>
<Spacer /> <ModerationUIView
label="Profile Moderation UI"
mod={profileModeration}
/>
<ModerationUIView label="Post Moderation UI" mod={postModeration} />
<DataView
label={label[0]}
data={LABELS[label[0] as keyof typeof LABELS]}
/>
<DataView label="Profile Moderation Data" data={profileModeration} />
<DataView label="Post Data" data={postModeration} />
<Heading title="Post" subtitle="in feed" /> <Spacer />
<MockPost
label={label[0]}
context="feed"
post={post}
moderation={postModeration}
/>
<Spacer /> <Heading title="Post" subtitle="in feed" />
<MockPostFeedItem post={post} moderation={postModeration} />
<Heading title="Post" subtitle="viewed directly" /> <Spacer />
<MockPost
label={label[0]}
context="view"
post={post}
moderation={postModeration}
/>
<Spacer /> <Heading title="Post" subtitle="viewed directly" />
<MockPostThreadItem post={post} moderation={postModeration} />
<Heading title="Post" subtitle="reply in thread" /> <Spacer />
<MockPost
label={label[0]}
context="reply"
post={post}
moderation={postModeration}
/>
<Spacer /> <Heading title="Post" subtitle="reply in thread" />
<MockPostThreadItem post={post} moderation={postModeration} reply />
<Heading title="Notification" subtitle="quote or reply" /> <Spacer />
<P>TODO</P>
<Spacer /> <Heading title="Notification" subtitle="quote or reply" />
<P>TODO</P>
{(target[0] === 'account' || target[0] === 'profile') && ( <Spacer />
<>
<Heading title="Notification" subtitle="follow or like" />
<P>TODO</P>
<Spacer /> {(target[0] === 'account' || target[0] === 'profile') && (
<>
<Heading title="Notification" subtitle="follow or like" />
<P>TODO</P>
<Heading title="Account" subtitle="in listing" /> <Spacer />
<MockAccountCard
label={label[0]}
profile={profile}
moderation={profileModeration}
/>
<Spacer /> <Heading title="Account" subtitle="in listing" />
<MockAccountCard
profile={profile}
moderation={profileModeration}
/>
<Heading title="Account" subtitle="viewing directly" /> <Spacer />
<MockAccountScreen
label={label[0]}
profile={profile}
moderation={profileModeration}
moderationOpts={modOpts}
/>
</>
)}
<View style={{height: 400}} /> <Heading title="Account" subtitle="viewing directly" />
</CenteredView> <MockAccountScreen
</ScrollView> profile={profile}
moderation={profileModeration}
moderationOpts={modOpts}
/>
</>
)}
<View style={{height: 400}} />
</CenteredView>
</ScrollView>
</moderationOptsOverrideContext.Provider>
) )
} }
@@ -340,105 +395,146 @@ function Heading({title, subtitle}: {title: string; subtitle?: string}) {
) )
} }
function Spacer() { function Toggler({label, children}: React.PropsWithChildren<{label: string}>) {
return <View style={{height: 40}} /> const t = useTheme()
const [show, setShow] = React.useState(false)
return (
<View style={a.mb_md}>
<View
style={[
t.atoms.border_contrast_medium,
a.border,
a.rounded_sm,
a.p_xs,
]}>
<Button
variant="solid"
color="secondary"
label="Toggle visibility"
size="small"
onPress={() => setShow(!show)}>
<ButtonText>{label}</ButtonText>
<ButtonIcon
icon={show ? ChevronTop : ChevronBottom}
position="right"
/>
</Button>
{show && children}
</View>
</View>
)
} }
function MockPost({ function DataView({label, data}: {label: string; data: any}) {
return (
<Toggler label={label}>
<Text style={[{fontFamily: 'monospace'}, a.p_md]}>
{JSON.stringify(data, null, 2)}
</Text>
</Toggler>
)
}
function ModerationUIView({
mod,
label, label,
context, }: {
mod: ModerationDecision
label: string
}) {
return (
<Toggler label={label}>
<View style={a.p_lg}>
{[
'profileList',
'profileView',
'avatar',
'banner',
'displayName',
'contentList',
'contentView',
'contentMedia',
].map(key => {
const ui = mod.ui(key as keyof ModerationBehavior)
return (
<View key={key} style={[a.flex_row, a.gap_md]}>
<Text style={[a.font_bold, {width: 100}]}>{key}</Text>
<Flag v={ui.filter} label="Filter" />
<Flag v={ui.blur} label="Blur" />
<Flag v={ui.alert} label="Alert" />
<Flag v={ui.inform} label="Inform" />
<Flag v={ui.noOverride} label="No-override" />
</View>
)
})}
</View>
</Toggler>
)
}
function Spacer() {
return <View style={{height: 30}} />
}
function MockPostFeedItem({
post, post,
moderation, moderation,
}: { }: {
label: string
context: 'feed' | 'view' | 'reply'
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
moderation: PostModeration moderation: ModerationDecision
}) { }) {
const t = useTheme() const t = useTheme()
if (moderation.ui('contentList').filter) {
if (context === 'feed' && moderation.content.filter) {
return ( return (
<P style={[t.atoms.bg_contrast_50, a.px_lg, a.py_md]}> <P style={[t.atoms.bg_contrast_50, a.px_lg, a.py_md]}>
Filtered from the feed Filtered from the feed
</P> </P>
) )
} }
return ( return (
<View <FeedItem
style={[t.atoms.border_contrast_medium, a.border, a.rounded_md, a.p_2xs]}> post={post}
{' '} record={post.record as AppBskyFeedPost.Record}
<PostHider moderation={moderation}
key={label} reason={undefined}
href="#" />
moderation={moderation.content} )
iconSize={38} }
iconStyles={{marginLeft: 2, marginRight: 2}}
style={[a.px_lg, a.py_lg, a.rounded_md]}>
<View style={[a.flex_row, a.gap_md]}>
<UserAvatar
size={64}
avatar={post.author.avatar}
moderation={moderation.avatar}
/>
<View style={[a.flex_1]}>
<View style={[a.flex_row]}>
<P style={[a.font_bold]}>Bob Robertson </P>
<P style={t.atoms.text_contrast_medium}>
@bob.bsky.social &middot; 5m
</P>
</View>
<PostAlerts moderation={moderation.content} /> function MockPostThreadItem({
post,
<P style={a.mb_lg}> reply,
This is the body of the post. It's where the text goes. You get }: {
the idea. post: AppBskyFeedDefs.PostView
</P> moderation: ModerationDecision
reply?: boolean
<ContentHider }) {
moderation={moderation.embed} return (
moderationDecisions={moderation.decisions}> <PostThreadItem
<View // @ts-ignore
style={[ post={post}
t.atoms.border_contrast_medium, record={post.record as AppBskyFeedPost.Record}
a.border, depth={reply ? 1 : 0}
a.rounded_md, isHighlightedPost={!reply}
a.flex_col, treeView={false}
a.gap_xs, prevPost={undefined}
a.px_xl, nextPost={undefined}
a.py_xl, hasPrecedingItem={false}
]}> onPostReply={() => {}}
<View style={[a.flex_row]}> />
<P style={[a.font_bold]}>Bob Robertson </P>
<P style={t.atoms.text_contrast_medium}>
@bob.bsky.social &middot; 5m
</P>
</View>
<PostAlerts moderation={moderation.embed} />
<P>Embedded content</P>
</View>
</ContentHider>
</View>
</View>
</PostHider>
</View>
) )
} }
function MockAccountCard({ function MockAccountCard({
label,
profile, profile,
moderation, moderation,
}: { }: {
label: string
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration moderation: ModerationDecision
}) { }) {
const t = useTheme() const t = useTheme()
if (moderation.account.filter || moderation.profile.filter) { if (moderation.ui('profileList').filter) {
return ( return (
<P style={[t.atoms.bg_contrast_50, a.px_lg, a.py_md]}> <P style={[t.atoms.bg_contrast_50, a.px_lg, a.py_md]}>
Filtered from the listing Filtered from the listing
@@ -446,104 +542,36 @@ function MockAccountCard({
) )
} }
return ( return <ProfileCard profile={profile} />
<View
key={label}
style={[
t.atoms.border_contrast_medium,
a.border,
a.rounded_md,
a.flex_row,
a.gap_md,
a.px_lg,
a.py_md,
a.mb_md,
]}>
<UserAvatar
size={64}
avatar={profile.avatar}
moderation={moderation.avatar}
/>
<View style={[a.flex_1]}>
<P style={[a.font_bold]}>
{sanitizeDisplayName('Bob Robertson', moderation.profile)}{' '}
</P>
<P style={t.atoms.text_contrast_medium}>@bob.bsky.social</P>
<P>Thought leader or something.</P>
<ProfileCardPills followedBy={false} moderation={moderation} />
</View>
</View>
)
} }
function MockAccountScreen({ function MockAccountScreen({
label,
profile, profile,
moderation, moderation,
moderationOpts, moderationOpts,
}: { }: {
label: string
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration moderation: ModerationDecision
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}) { }) {
const t = useTheme() const t = useTheme()
return ( return (
<View <View style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
key={label}
style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
<ScreenHider <ScreenHider
style={{}} style={{}}
screenDescription="profile" screenDescription="profile"
moderation={moderation.account}> modui={moderation.ui('profileView')}>
<ProfileHeader <ProfileHeader
// @ts-ignore ProfileViewBasic is close enough -prf // @ts-ignore ProfileViewBasic is close enough -prf
profile={profile} profile={profile}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
descriptionRT={null /*TODO*/} descriptionRT={new RichText({text: profile.description as string})}
/> />
{/*
<UserBanner />
<View
style={[
a.absolute,
t.atoms.bg,
a.rounded_full,
{top: 100, left: 10, width: 100, height: 100, padding: 2},
]}>
<UserAvatar size={96} moderation={moderation.avatar} />
</View>
<View
style={[
a.pb_2xl,
a.px_xl,
t.atoms.border_contrast_medium,
a.border_b,
{paddingTop: 60},
]}>
<H3>Bob Robertson</H3>
<P style={t.atoms.text_contrast_medium}>@bob.bsky.social</P>
<P>Thought leader or something.</P>
</View>
*/}
</ScreenHider> </ScreenHider>
</View> </View>
) )
} }
function ModerationUIView({mod, label}: {mod: ModerationUI; label: string}) {
return (
<View style={[a.flex_row, a.gap_md]}>
<P style={[a.font_bold, a.text_xs, {width: 60}]}>{label}:</P>
<Flag v={mod.filter} label="Filter" />
<Flag v={mod.blur} label="Blur" />
<Flag v={mod.alert} label="Alert" />
<Flag v={mod.noOverride} label="No-override" />
</View>
)
}
function Flag({v, label}: {v: boolean | undefined; label: string}) { function Flag({v, label}: {v: boolean | undefined; label: string}) {
const t = useTheme() const t = useTheme()
return ( return (
@@ -556,12 +584,12 @@ function Flag({v, label}: {v: boolean | undefined; label: string}) {
a.border, a.border,
t.atoms.border_contrast_medium, t.atoms.border_contrast_medium,
{ {
backgroundColor: v ? t.palette.black : t.palette.white, backgroundColor: t.palette.contrast_25,
width: 14, width: 14,
height: 14, height: 14,
}, },
]}> ]}>
{v && <Check size="xs" fill={t.palette.white} />} {v && <Check size="xs" fill={t.palette.contrast_900} />}
</View> </View>
<P style={a.text_xs}>{label}</P> <P style={a.text_xs}>{label}</P>
</View> </View>
+1 -1
View File
@@ -298,7 +298,7 @@ function ProfileScreenLoaded({
testID="profileView" testID="profileView"
style={styles.container} style={styles.container}
screenDescription="profile" screenDescription="profile"
moderation={moderation.account}> modui={moderation.ui('profileView')}>
<PagerWithHeader <PagerWithHeader
testID="profilePager" testID="profilePager"
isHeaderReady={!showPlaceholder} isHeaderReady={!showPlaceholder}
+41
View File
@@ -129,6 +129,15 @@ export function Buttons() {
<ButtonIcon icon={Globe} position="left" /> <ButtonIcon icon={Globe} position="left" />
<ButtonText>Link out</ButtonText> <ButtonText>Link out</ButtonText>
</Button> </Button>
<Button
variant="gradient"
color="gradient_sky"
size="tiny"
label="Link out">
<ButtonIcon icon={Globe} position="left" />
<ButtonText>Link out</ButtonText>
</Button>
</View> </View>
<View style={[a.flex_row, a.gap_md, a.align_start]}> <View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -148,6 +157,14 @@ export function Buttons() {
label="Link out"> label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button <Button
variant="outline" variant="outline"
color="primary" color="primary"
@@ -164,6 +181,14 @@ export function Buttons() {
label="Link out"> label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button
variant="ghost"
color="primary"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
</View> </View>
<View style={[a.flex_row, a.gap_md, a.align_start]}> <View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -183,6 +208,14 @@ export function Buttons() {
label="Link out"> label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button <Button
variant="outline" variant="outline"
color="primary" color="primary"
@@ -199,6 +232,14 @@ export function Buttons() {
label="Link out"> label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button
variant="ghost"
color="primary"
size="tiny"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
</View> </View>
</View> </View>
) )
+4 -4
View File
@@ -11,7 +11,7 @@ import {useNavigation, StackActions} from '@react-navigation/native'
import { import {
AppBskyActorDefs, AppBskyActorDefs,
moderateProfile, moderateProfile,
ProfileModeration, ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -86,7 +86,7 @@ export function SearchProfileCard({
moderation, moderation,
}: { }: {
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration moderation: ModerationDecision
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
@@ -111,7 +111,7 @@ export function SearchProfileCard({
<UserAvatar <UserAvatar
size={40} size={40}
avatar={profile.avatar} avatar={profile.avatar}
moderation={moderation.avatar} moderation={moderation.ui('avatar')}
/> />
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Text <Text
@@ -121,7 +121,7 @@ export function SearchProfileCard({
lineHeight={1.2}> lineHeight={1.2}>
{sanitizeDisplayName( {sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle), profile.displayName || sanitizeHandle(profile.handle),
moderation.profile, moderation.ui('displayName'),
)} )}
</Text> </Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}> <Text type="md" style={[pal.textLight]} numberOfLines={1}>