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_left: {
textAlign: 'left',
},
text_center: {
textAlign: 'center',
},
@@ -242,7 +245,6 @@ export const atoms = {
borderRightWidth: 1,
},
/*
* Shadow
*/
+13 -3
View File
@@ -27,7 +27,7 @@ export type ButtonColor =
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
export type ButtonSize = 'small' | 'large'
export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = {
/**
@@ -277,6 +277,8 @@ export function Button({
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
} else if (size === 'small') {
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') {
if (size === 'large') {
@@ -287,12 +289,18 @@ export function Button({
}
} else if (size === 'small') {
baseStyles.push({height: 40, width: 40})
} else if (size === 'tiny') {
baseStyles.push({height: 20, width: 20})
}
if (shape === 'round') {
baseStyles.push(a.rounded_full)
} 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') {
baseStyles.push(a.text_md, android({paddingBottom: 1}))
} else if (size === 'tiny') {
baseStyles.push(a.text_xs, android({paddingBottom: 1}))
} else {
baseStyles.push(a.text_sm, android({paddingBottom: 1}))
}
@@ -532,7 +542,7 @@ export function ButtonIcon({
},
]}>
<Comp
size={size === 'large' ? 'md' : 'sm'}
size={size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm'}
style={[{color: textStyles.color, pointerEvents: 'none'}]}
/>
</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({
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 {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
// AppBskyEmbedRecord,
// AppBskyEmbedRecordWithMedia,
moderatePost,
} from '@atproto/api'
@@ -13,10 +13,11 @@ export function moderatePost_wrapped(
subject: Parameters<ModeratePost>[0],
opts: Options,
) {
const {hiddenPosts = [], ...options} = opts
const moderations = moderatePost(subject, options)
// const {hiddenPosts = [], ...options} = opts
const moderations = moderatePost(subject, opts) // options)
if (hiddenPosts.includes(subject.uri)) {
// TODO
/*if (hiddenPosts.includes(subject.uri)) {
moderations.content.filter = true
moderations.content.blur = true
if (!moderations.content.cause) {
@@ -52,7 +53,7 @@ export function moderatePost_wrapped(
}
}
}
}
}*/
return moderations
}
+1 -65
View File
@@ -1,68 +1,4 @@
import {
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
}
import {ModerationCause, LABEL_GROUPS, LabelGroupDefinition} from '@atproto/api'
export function getModerationCauseKey(cause: ModerationCause): string {
const source =
@@ -88,7 +88,7 @@ export function SuggestedAccountCard({
<UserAvatar
size={48}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={[a.flex_1]}>
-7
View File
@@ -26,12 +26,6 @@ export interface EditProfileModal {
onUpdate?: () => void
}
export interface ModerationDetailsModal {
name: 'moderation-details'
context: 'account' | 'content'
moderation: ModerationUI
}
export type ReportModal = {
name: 'report'
} & (
@@ -203,7 +197,6 @@ export type Modal =
| PostLanguagesSettingsModal
// Moderation
| ModerationDetailsModal
| ReportModal
| AppealLabelModal
+2 -2
View File
@@ -107,8 +107,8 @@ function computeSuggestions(
}
}
return items.filter(profile => {
const mod = moderateProfile(profile, moderationOpts)
return !mod.account.filter && mod.account.cause?.type !== 'muted'
const mod = moderateProfile(profile, moderationOpts).ui('profileList')
return !mod.filter // TODO && mod.account.cause?.type !== 'muted'
})
}
+2 -6
View File
@@ -97,11 +97,7 @@ function shouldFilterNotif(
return false
}
const profile = moderateProfile(notif.author, moderationOpts)
if (
profile.account.filter ||
profile.profile.filter ||
notif.author.viewer?.muted
) {
if (profile.ui('profileList').filter || notif.author.viewer?.muted) {
return true
}
if (
@@ -111,7 +107,7 @@ function shouldFilterNotif(
) {
// NOTE: the notification overlaps the post enough for this to work
const post = moderatePost(notif, moderationOpts)
if (post.content.filter) {
if (post.ui('contentList').filter) {
return true
}
}
+8 -4
View File
@@ -1,6 +1,10 @@
import React, {useCallback, useEffect, useRef} from 'react'
import {AppState} from 'react-native'
import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
ModerationDecision,
} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
@@ -62,7 +66,7 @@ export interface FeedPostSliceItem {
post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
moderation: PostModeration
moderation: ModerationDecision
}
export interface FeedPostSlice {
@@ -227,7 +231,7 @@ export function usePostFeedQuery(
// apply moderation filter
for (let i = 0; i < slice.items.length; i++) {
if (
moderations[i]?.content.filter &&
moderations[i]?.ui('contentList').filter &&
slice.items[i].post.author.did !== ignoreFilterFor
) {
return undefined
@@ -433,7 +437,7 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
DEFAULT_LOGGED_OUT_PREFERENCES.moderationOpts,
)
if (!moderation.content.filter) {
if (!moderation.ui('contentList').filter) {
// we have a sfw post
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 {LabelPreference, BskyFeedViewPreference} from '@atproto/api'
import {
LabelPreference,
BskyFeedViewPreference,
ModerationOpts,
} from '@atproto/api'
import {track} from '#/lib/analytics/analytics'
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() {
const override = useContext(moderationOptsOverrideContext)
const prefs = usePreferencesQuery()
const hiddenPosts = useHiddenPosts()
const opts = useMemo(() => {
if (override) {
return override
}
if (!prefs.data) {
return
}
@@ -75,7 +88,7 @@ export function useModerationOpts() {
...moderationOpts,
hiddenPosts,
}
}, [prefs.data, hiddenPosts])
}, [override, prefs.data, hiddenPosts])
return opts
}
+2 -1
View File
@@ -46,7 +46,8 @@ export function useSuggestedFollowsQuery() {
res.data.actors = res.data.actors
.filter(
actor => !moderateProfile(actor, moderationOpts!).account.filter,
actor =>
!moderateProfile(actor, moderationOpts!).ui('profileList').filter,
)
.filter(actor => {
const viewer = actor.viewer
+2 -2
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {
AppBskyEmbedRecord,
AppBskyRichtextFacet,
PostModeration,
ModerationDecision,
} from '@atproto/api'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -16,7 +16,7 @@ export interface ComposerOptsPostRef {
avatar?: string
}
embed?: AppBskyEmbedRecord.ViewRecord['embed']
moderation?: PostModeration
moderation?: ModerationDecision
}
export interface ComposerOptsQuote {
uri: string
@@ -1,6 +1,6 @@
import React from 'react'
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 {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
@@ -18,7 +18,7 @@ import {logger} from '#/logger'
type Props = {
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration
moderation: ModerationDecision
onFollowStateChange: (props: {
did: string
following: boolean
@@ -62,7 +62,7 @@ export function ProfileCard({
moderation,
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
moderation: ProfileModeration
moderation: ModerationDecision
onFollowStateChange: (props: {
did: string
following: boolean
@@ -110,7 +110,7 @@ export function ProfileCard({
<UserAvatar
size={40}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={styles.layoutContent}>
@@ -121,7 +121,7 @@ export function ProfileCard({
lineHeight={1.2}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.profile,
moderation.ui('displayName'),
)}
</Text>
<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 {useExternalLinkFetch} from './useExternalLinkFetch'
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 {Gallery} from './photos/Gallery'
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 {UserAvatar} from 'view/com/util/UserAvatar'
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}) {
const pal = usePalette('default')
@@ -86,7 +86,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
<UserAvatar
avatar={replyTo.author.avatar}
size={50}
moderation={replyTo.moderation?.avatar}
moderation={replyTo.moderation?.ui('avatar')}
/>
<View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}>
@@ -103,7 +103,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
{replyTo.text}
</Text>
</View>
{images && !replyTo.moderation?.embed.blur && (
{images && !replyTo.moderation?.ui('contentMedia').blur && (
<ComposerReplyToImages images={images} showFull={showFull} />
)}
</View>
-4
View File
@@ -26,7 +26,6 @@ import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings'
import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail'
@@ -127,9 +126,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'post-languages-settings') {
snapPoints = PostLanguagesSettingsModal.snapPoints
element = <PostLanguagesSettingsModal.Component />
} else if (activeModal?.name === 'moderation-details') {
snapPoints = ModerationDetailsModal.snapPoints
element = <ModerationDetailsModal.Component {...activeModal} />
} else if (activeModal?.name === 'birth-date-settings') {
snapPoints = BirthDateSettingsModal.snapPoints
element = <BirthDateSettingsModal.Component />
-3
View File
@@ -28,7 +28,6 @@ import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as ModerationDetailsModal from './ModerationDetails'
import * as BirthDateSettingsModal from './BirthDateSettings'
import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail'
@@ -121,8 +120,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <AltTextImageModal.Component {...modal} />
} else if (modal.name === 'edit-image') {
element = <EditImageModal.Component {...modal} />
} else if (modal.name === 'moderation-details') {
element = <ModerationDetailsModal.Component {...modal} />
} else if (modal.name === 'birth-date-settings') {
element = <BirthDateSettingsModal.Component />
} 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,
AppBskyFeedPost,
ModerationOpts,
ProfileModeration,
ModerationDecision,
moderateProfile,
AppBskyEmbedRecordWithMedia,
} from '@atproto/api'
@@ -54,7 +54,7 @@ interface Author {
handle: string
displayName?: string
avatar?: string
moderation: ProfileModeration
moderation: ModerationDecision
}
let FeedItem = ({
@@ -336,7 +336,7 @@ function CondensedAuthorsList({
did={authors[0].did}
handle={authors[0].handle}
avatar={authors[0].avatar}
moderation={authors[0].moderation.avatar}
moderation={authors[0].moderation.ui('avatar')}
/>
</View>
)
@@ -354,7 +354,7 @@ function CondensedAuthorsList({
<UserAvatar
size={35}
avatar={author.avatar}
moderation={author.moderation.avatar}
moderation={author.moderation.ui('avatar')}
/>
</View>
))}
@@ -412,7 +412,7 @@ function ExpandedAuthorsList({
<UserAvatar
size={35}
avatar={author.avatar}
moderation={author.moderation.avatar}
moderation={author.moderation.ui('avatar')}
/>
</View>
<View style={s.flex1}>
+6 -5
View File
@@ -90,11 +90,12 @@ export function PostThread({
? moderatePost(rootPost, moderationOpts)
: undefined
const cause = mod?.content.cause
return cause
? cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated'
: false
return !!mod
?.ui('contentList')
.blurs.find(
cause =>
cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated',
)
}, [rootPost, moderationOpts])
useSetTitle(
+11 -34
View File
@@ -5,7 +5,7 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
RichText as RichTextAPI,
PostModeration,
ModerationDecision,
} from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
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 {sanitizeHandle} from 'lib/strings/handles'
import {countLines, pluralize} from 'lib/strings/helpers'
import {isEmbedByEmbedder} from 'lib/embeds'
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/post-embeds'
@@ -144,7 +143,7 @@ let PostThreadItemLoaded = ({
post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record
richText: RichTextAPI
moderation: PostModeration
moderation: ModerationDecision
treeView: boolean
depth: number
prevPost: ThreadPost | undefined
@@ -256,7 +255,7 @@ let PostThreadItemLoaded = ({
did={post.author.did}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={styles.layoutContent}>
@@ -313,12 +312,12 @@ let PostThreadItemLoaded = ({
</View>
<View style={[s.pl10, s.pr10, s.pb10]}>
<ContentHider
moderation={moderation.content}
modui={moderation.ui('contentView')}
ignoreMute
style={styles.contentHider}
childContainerStyle={styles.contentHiderChild}>
<PostAlerts
moderation={moderation.content}
modui={moderation.ui('contentView')}
includeMute
style={styles.alert}
/>
@@ -338,18 +337,7 @@ let PostThreadItemLoaded = ({
</View>
) : undefined}
{post.embed && (
<ContentHider
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>
<PostEmbeds embed={post.embed} moderation={moderation} />
)}
</ContentHider>
<ExpandedPostDetails
@@ -431,7 +419,7 @@ let PostThreadItemLoaded = ({
testID={`postThreadItem-by-${post.author.handle}`}
href={postHref}
style={[pal.view]}
moderation={moderation.content}
modui={moderation.ui('contentList')}
iconSize={isThreadedChild ? 26 : 38}
iconStyles={
isThreadedChild
@@ -482,7 +470,7 @@ let PostThreadItemLoaded = ({
did={post.author.did}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
{showChildReplyLine && (
@@ -507,14 +495,14 @@ let PostThreadItemLoaded = ({
timestamp={post.indexedAt}
postHref={postHref}
showAvatar={isThreadedChild}
avatarModeration={moderation.avatar}
avatarModeration={moderation.ui('avatar')}
avatarSize={28}
displayNameType="md-bold"
displayNameStyle={isThreadedChild && s.ml2}
style={isThreadedChild && s.mb2}
/>
<PostAlerts
moderation={moderation.content}
modui={moderation.ui('contentList')}
style={styles.alert}
/>
{richText?.text ? (
@@ -537,18 +525,7 @@ let PostThreadItemLoaded = ({
/>
) : undefined}
{post.embed && (
<ContentHider
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>
<PostEmbeds embed={post.embed} moderation={moderation} />
)}
<PostCtrls
post={post}
+9 -16
View File
@@ -4,7 +4,7 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
PostModeration,
ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
@@ -92,7 +92,7 @@ function PostInner({
post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record
richText: RichTextAPI
moderation: PostModeration
moderation: ModerationDecision
showReplyLine?: boolean
style?: StyleProp<ViewStyle>
}) {
@@ -141,7 +141,7 @@ function PostInner({
did={post.author.did}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={styles.layoutContent}>
@@ -176,10 +176,13 @@ function PostInner({
</View>
)}
<ContentHider
moderation={moderation.content}
modui={moderation.ui('contentView')}
style={styles.contentHider}
childContainerStyle={styles.contentHiderChild}>
<PostAlerts moderation={moderation.content} style={styles.alert} />
<PostAlerts
modui={moderation.ui('contentView')}
style={styles.alert}
/>
{richText.text ? (
<View style={styles.postTextContainer}>
<RichText
@@ -201,17 +204,7 @@ function PostInner({
/>
) : undefined}
{post.embed ? (
<ContentHider
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
ignoreQuoteDecisions
style={styles.contentHider}>
<PostEmbeds
embed={post.embed}
moderation={moderation.embed}
moderationDecisions={moderation.decisions}
/>
</ContentHider>
<PostEmbeds embed={post.embed} moderation={moderation} />
) : null}
</ContentHider>
<PostCtrls
+14 -24
View File
@@ -4,7 +4,7 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
PostModeration,
ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api'
import {
@@ -28,7 +28,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {isEmbedByEmbedder} from 'lib/embeds'
import {MAX_POST_LINES} from 'lib/constants'
import {countLines} from 'lib/strings/helpers'
import {useComposerControls} from '#/state/shell/composer'
@@ -50,7 +49,7 @@ export function FeedItem({
post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
moderation: PostModeration
moderation: ModerationDecision
isThreadChild?: boolean
isThreadLastChild?: boolean
isThreadParent?: boolean
@@ -98,7 +97,7 @@ let FeedItemInner = ({
record: AppBskyFeedPost.Record
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
richText: RichTextAPI
moderation: PostModeration
moderation: ModerationDecision
isThreadChild?: boolean
isThreadLastChild?: boolean
isThreadParent?: boolean
@@ -111,9 +110,12 @@ let FeedItemInner = ({
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author])
const isModeratedPost =
moderation.decisions.post.cause?.type === 'label' &&
moderation.decisions.post.cause.label.src !== currentAccount?.did
const isModeratedPost = !!moderation.causes.find(
cause =>
cause?.type === 'label' &&
cause.label.src !== currentAccount?.did &&
cause.label.uri === post.uri,
)
const replyAuthorDid = useMemo(() => {
if (!record?.reply) {
@@ -247,7 +249,7 @@ let FeedItemInner = ({
did={post.author.did}
handle={post.author.handle}
avatar={post.author.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
{isThreadParent && (
<View
@@ -324,7 +326,7 @@ let PostContent = ({
postEmbed,
postAuthor,
}: {
moderation: PostModeration
moderation: ModerationDecision
richText: RichTextAPI
postEmbed: AppBskyFeedDefs.PostView['embed']
postAuthor: AppBskyFeedDefs.PostView['author']
@@ -342,10 +344,10 @@ let PostContent = ({
return (
<ContentHider
testID="contentHider-post"
moderation={moderation.content}
modui={moderation.ui('contentList')}
ignoreMute
childContainerStyle={styles.contentHiderChild}>
<PostAlerts moderation={moderation.content} style={styles.alert} />
<PostAlerts modui={moderation.ui('contentList')} style={styles.alert} />
{richText.text ? (
<View style={styles.postTextContainer}>
<RichText
@@ -367,19 +369,7 @@ let PostContent = ({
/>
) : undefined}
{postEmbed ? (
<ContentHider
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>
<PostEmbeds embed={postEmbed} moderation={moderation} />
) : null}
</ContentHider>
)
+18 -17
View File
@@ -4,7 +4,7 @@ import {
AppBskyActorDefs,
moderateProfile,
ModerationCause,
ProfileModeration,
ModerationDecision,
} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
@@ -15,7 +15,7 @@ import {FollowButton} from './FollowButton'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {getProfileModerationCauses, getModerationCauseKey} from 'lib/moderation'
import {getModerationCauseKey} from 'lib/moderation'
import {Shadow} from '#/state/cache/types'
import {useModerationOpts} from '#/state/queries/preferences'
import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -51,11 +51,8 @@ export function ProfileCard({
return null
}
const moderation = moderateProfile(profile, moderationOpts)
if (
!noModFilter &&
moderation.account.filter &&
moderation.account.cause?.type !== 'muted'
) {
const modui = moderation.ui('profileList')
if (!noModFilter && modui.filter /* TODO && modui.type !== 'muted'*/) {
return null
}
@@ -78,7 +75,7 @@ export function ProfileCard({
<UserAvatar
size={40}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
<View style={styles.layoutContent}>
@@ -89,7 +86,7 @@ export function ProfileCard({
lineHeight={1.2}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.profile,
moderation.ui('displayName'),
)}
</Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
@@ -122,12 +119,12 @@ export function ProfileCardPills({
moderation,
}: {
followedBy: boolean
moderation: ProfileModeration
moderation: ModerationDecision
}) {
const pal = usePalette('default')
const causes = getProfileModerationCauses(moderation)
if (!followedBy && !causes.length) {
const informs = moderation.ui('profileList').informs
if (!followedBy && !informs.length) {
return null
}
@@ -140,10 +137,10 @@ export function ProfileCardPills({
</Text>
</View>
)}
{causes.map(cause => (
{informs.map(inform => (
<ProfileCardPillModerationCause
key={getModerationCauseKey(cause)}
cause={cause}
key={getModerationCauseKey(inform)}
cause={inform}
/>
))}
</View>
@@ -183,7 +180,7 @@ function FollowersList({
f,
mod: moderateProfile(f, moderationOpts),
}))
.filter(({mod}) => !mod.account.filter)
.filter(({mod}) => !mod.ui('profileList').filter)
}, [followers, moderationOpts])
if (!followersWithMods?.length) {
@@ -205,7 +202,11 @@ function FollowersList({
{followersWithMods.slice(0, 3).map(({f, mod}) => (
<View key={f.did} style={styles.followedByAviContainer}>
<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>
))}
+9 -8
View File
@@ -134,10 +134,8 @@ let ProfileHeader = ({
}, [navigation])
const onPressAvi = React.useCallback(() => {
if (
profile.avatar &&
!(moderation.avatar.blur && moderation.avatar.noOverride)
) {
const modui = moderation.ui('avatar')
if (profile.avatar && !(modui.blur && modui.noOverride)) {
openLightbox(new ProfileImageLightbox(profile))
}
}, [openLightbox, profile, moderation])
@@ -407,7 +405,10 @@ let ProfileHeader = ({
style={{borderRadius: 0}}
/>
) : (
<UserBanner banner={profile.banner} moderation={moderation.avatar} />
<UserBanner
banner={profile.banner}
moderation={moderation.ui('banner')}
/>
)}
</View>
<View style={styles.content} pointerEvents="box-none">
@@ -538,7 +539,7 @@ let ProfileHeader = ({
style={[pal.text, styles.title]}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.profile,
moderation.ui('displayName'),
)}
</Text>
</View>
@@ -611,7 +612,7 @@ let ProfileHeader = ({
</Text>
</Text>
</View>
{descriptionRT && !moderation.profile.blur ? (
{descriptionRT && !moderation.ui('profileView').blur ? (
<View pointerEvents="auto">
<RichText
testID="profileHeaderDescription"
@@ -671,7 +672,7 @@ let ProfileHeader = ({
<UserAvatar
size={80}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
</TouchableWithoutFeedback>
@@ -214,7 +214,7 @@ function SuggestedFollow({
<UserAvatar
size={60}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
<View style={{width: '100%', paddingVertical: 12}}>
@@ -224,7 +224,7 @@ function SuggestedFollow({
numberOfLines={1}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.profile,
moderation.ui('displayName'),
)}
</Text>
<Text
+80 -89
View File
@@ -1,44 +1,44 @@
import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {usePalette} from 'lib/hooks/usePalette'
import {ModerationUI, PostModeration} from '@atproto/api'
import {Text} from '../text/Text'
import {ShieldExclamation} from 'lib/icons'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
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({
testID,
moderation,
moderationDecisions,
ignoreMute,
ignoreQuoteDecisions,
modui,
// ignoreMute, TODO
style,
childContainerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
moderation: ModerationUI
moderationDecisions?: PostModeration['decisions']
modui: ModerationUI | undefined
ignoreMute?: boolean
ignoreQuoteDecisions?: boolean
style?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle>
}>) {
const pal = usePalette('default')
const t = useTheme()
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(moderation.cause, 'content')
const {gtMobile} = useBreakpoints()
const openDialog = useOpenGlobalDialog()
const blur = modui?.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if (
!moderation.blur ||
(ignoreMute && moderation.cause?.type === 'muted') ||
shouldIgnoreQuote(moderationDecisions, ignoreQuoteDecisions)
!blur
// || (ignoreMute && moderation.cause?.type === 'muted') TODO
) {
return (
<View testID={testID} style={[styles.outer, style]}>
@@ -47,83 +47,74 @@ export function ContentHider({
)
}
const isMute = moderation.cause?.type === 'muted'
return (
<View testID={testID} style={[styles.outer, style]}>
<Pressable
onPress={() => {
if (!moderation.noOverride) {
setOverride(v => !v)
} else {
openModal({
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
<View testID={testID} style={[a.overflow_hidden, style]}>
<View style={[a.flex_col, a.gap_xs]}>
<Button
variant="solid"
color="secondary"
size={gtMobile ? 'large' : 'small'}
shape="default"
onPress={() => {
openModal({
name: 'moderation-details',
context: 'content',
moderation,
})
if (!modui.noOverride) {
setOverride(v => !v)
} else {
openDialog(ModerationDetailsDialog, {
context: 'content',
modcause: blur,
})
}
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
{isMute ? (
<FontAwesomeIcon
icon={['far', 'eye-slash']}
size={18}
color={pal.colors.textLight}
/>
) : (
<ShieldExclamation size={18} style={pal.textLight} />
label={desc.name}
accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}>
<ButtonIcon
icon={blur.type === 'muted' ? EyeSlash : Shield}
position="left"
/>{' '}
<ButtonText style={[a.flex_1, a.text_left]}>{desc.name}</ButtonText>
{!modui.noOverride && (
<ButtonText>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</ButtonText>
)}
</Pressable>
<Text type="md" style={[pal.text, {flex: 1}]} numberOfLines={2}>
{desc.name}
</Text>
<View style={styles.showBtn}>
<Text type="lg" style={pal.link}>
{moderation.noOverride ? (
<Trans>Learn more</Trans>
) : override ? (
<Trans>Hide</Trans>
) : (
<Trans>Show</Trans>
)}
</Text>
</View>
</Pressable>
</Button>
{blur.type === 'label' && !override && (
<Button
variant="ghost"
size="tiny"
onPress={() => {
openDialog(ModerationDetailsDialog, {
context: 'content',
modcause: blur,
})
}}
label={_(msg`Learn more`)}>
<ButtonText
style={[
a.flex_1,
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>}
</View>
)
}
function shouldIgnoreQuote(
decisions: PostModeration['decisions'] | undefined,
ignore: boolean | undefined,
): boolean {
if (!decisions || !ignore) {
return false
}
return !isPostMediaBlurred(decisions)
}
const styles = StyleSheet.create({
outer: {
overflow: 'hidden',
+82 -50
View File
@@ -1,67 +1,99 @@
import React from 'react'
import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationUI, ModerationCause} from '@atproto/api'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {ShieldExclamation} from 'lib/icons'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {Trans} from '@lingui/macro'
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({
moderation,
modui,
style,
}: {
moderation: ModerationUI
modui: ModerationUI
includeMute?: boolean
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(moderation.cause, 'content')
const shouldAlert = !!moderation.cause && moderation.alert
if (!shouldAlert) {
if (!modui.alert && !modui.inform) {
return null
}
return (
<Pressable
onPress={() => {
openModal({
name: 'moderation-details',
context: 'content',
moderation,
})
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint=""
style={[styles.container, pal.viewLight, style]}>
<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>
<View style={[a.flex_col, a.gap_xs, a.mb_sm, style]}>
{modui.inform && (
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{modui.informs.map(cause => (
<PostInform key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)}
{modui.alerts.map(cause => (
<PostAlert key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingVertical: 8,
paddingLeft: 14,
paddingHorizontal: 16,
borderRadius: 8,
},
learnMoreBtn: {
marginLeft: 'auto',
},
})
function PostInform({cause}: {cause: ModerationCause}) {
const openDialog = useOpenGlobalDialog()
const desc = useModerationCauseDescription(cause, 'content')
return (
<Button
label={desc.name}
variant="solid"
color="secondary"
size="tiny"
shape="default"
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 {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
interface Props extends ComponentProps<typeof Link> {
iconSize: number
iconStyles: StyleProp<ViewStyle>
moderation: ModerationUI
modui: ModerationUI
}
export function PostHider({
testID,
href,
moderation,
modui,
style,
children,
iconSize,
@@ -31,10 +33,11 @@ export function PostHider({
const pal = usePalette('default')
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(moderation.cause, 'content')
const openDialog = useOpenGlobalDialog()
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if (!moderation.blur) {
if (!blur) {
return (
<Link
testID={testID}
@@ -48,11 +51,11 @@ export function PostHider({
)
}
const isMute = moderation.cause?.type === 'muted'
const isMute = blur.type === 'muted'
return !override ? (
<Pressable
onPress={() => {
if (!moderation.noOverride) {
if (!modui.noOverride) {
setOverride(v => !v)
}
}}
@@ -68,10 +71,9 @@ export function PostHider({
]}>
<Pressable
onPress={() => {
openModal({
name: 'moderation-details',
openDialog(ModerationDetailsDialog, {
context: 'content',
moderation,
modcause: blur,
})
}}
accessibilityRole="button"
@@ -103,7 +105,7 @@ export function PostHider({
<Text type="sm" style={[{flex: 1}, pal.textLight]} numberOfLines={1}>
{desc.name}
</Text>
{!moderation.noOverride && (
{!modui.noOverride && (
<Text type="sm" style={[styles.showBtn, pal.link]}>
{override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</Text>
@@ -1,101 +1,99 @@
import React from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationCause, ProfileModeration} from '@atproto/api'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationCause, ModerationDecision} from '@atproto/api'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {ShieldExclamation} from 'lib/icons'
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 {getModerationCauseKey} from 'lib/moderation'
import {Trans} from '@lingui/macro'
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({
moderation,
style,
}: {
moderation: ProfileModeration
moderation: ModerationDecision
style?: StyleProp<ViewStyle>
}) {
const causes = getProfileModerationCauses(moderation)
if (!causes.length) {
const modui = moderation.ui('profileView')
if (!modui.alert && !modui.inform) {
return null
}
return (
<View style={styles.grid}>
{causes.map(cause => (
<ProfileHeaderAlert
key={getModerationCauseKey(cause)}
cause={cause}
style={style}
/>
<View style={[a.flex_col, a.gap_xs, a.mb_sm, style]}>
{modui.inform && (
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{modui.informs.map(cause => (
<ProfileInform key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)}
{modui.alerts.map(cause => (
<ProfileAlert key={getModerationCauseKey(cause)} cause={cause} />
))}
</View>
)
}
function ProfileHeaderAlert({
cause,
style,
}: {
cause: ModerationCause
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(cause, 'account')
function ProfileInform({cause}: {cause: ModerationCause}) {
const openDialog = useOpenGlobalDialog()
const desc = useModerationCauseDescription(cause, 'content')
const isMute = cause.type === 'muted'
return (
<Pressable
testID="profileHeaderAlert"
key={desc.name}
<Button
label={desc.name}
variant="solid"
color="secondary"
size="tiny"
shape="default"
onPress={() => {
openModal({
name: 'moderation-details',
openDialog(ModerationDetailsDialog, {
context: 'content',
moderation: {cause},
modcause: cause,
})
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint=""
style={[styles.container, pal.viewLight, style]}>
{isMute ? (
<FontAwesomeIcon
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>
}}>
<ButtonIcon
icon={cause.type === 'muted' ? EyeSlash : CircleInfo}
position="left"
/>{' '}
<ButtonText>{desc.name}</ButtonText>
</Button>
)
}
const styles = StyleSheet.create({
grid: {
gap: 4,
},
container: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
},
learnMoreBtn: {
marginLeft: 'auto',
},
})
function ProfileAlert({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>
)
}
+16 -13
View File
@@ -19,22 +19,24 @@ import {Text} from '../text/Text'
import {Button} from '../forms/Button'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {s} from '#/lib/styles'
import {CenteredView} from '../Views'
import {ModerationDetailsDialog} from '#/components/dialogs/ModerationDetails'
import {useOpenGlobalDialog} from '#/components/dialogs'
export function ScreenHider({
testID,
screenDescription,
moderation,
modui,
style,
containerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
screenDescription: string
moderation: ModerationUI
modui: ModerationUI
style?: StyleProp<ViewStyle>
containerStyle?: StyleProp<ViewStyle>
}>) {
@@ -44,10 +46,11 @@ export function ScreenHider({
const [override, setOverride] = React.useState(false)
const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls()
const desc = useModerationCauseDescription(moderation.cause, 'account')
const openDialog = useOpenGlobalDialog()
const blur = modui.blurs[0]
const desc = useModerationCauseDescription(blur, 'content')
if (!moderation.blur || override) {
if (!blur || override) {
return (
<View testID={testID} style={style}>
{children}
@@ -55,9 +58,10 @@ export function ScreenHider({
)
}
const isNoPwi =
moderation.cause?.type === 'label' &&
moderation.cause?.labelDef.id === '!no-unauthenticated'
const isNoPwi = !!modui.blurs.find(
cause =>
cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated',
)
return (
<CenteredView
style={[styles.container, pal.view, containerStyle]}
@@ -91,10 +95,9 @@ export function ScreenHider({
</Text>
<TouchableWithoutFeedback
onPress={() => {
openModal({
name: 'moderation-details',
openDialog(ModerationDetailsDialog, {
context: 'account',
moderation,
modcause: blur,
})
}}
accessibilityRole="button"
@@ -123,7 +126,7 @@ export function ScreenHider({
<Trans>Go back</Trans>
</Text>
</Button>
{!moderation.noOverride && (
{!modui.noOverride && (
<Button
type="default"
onPress={() => setOverride(v => !v)}
+85 -44
View File
@@ -1,13 +1,15 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {
AppBskyFeedDefs,
AppBskyEmbedRecord,
AppBskyFeedPost,
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
ModerationUI,
AppBskyEmbedExternal,
RichText as RichTextAPI,
moderatePost,
ModerationDecision,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {PostMeta} from '../PostMeta'
@@ -21,14 +23,14 @@ import {makeProfileLink} from 'lib/routes/links'
import {InfoCircleIcon} from 'lib/icons'
import {Trans} from '@lingui/macro'
import {RichText} from 'view/com/util/text/RichText'
import {useModerationOpts} from '#/state/queries/preferences'
import {ContentHider} from '../moderation/ContentHider'
export function MaybeQuoteEmbed({
embed,
moderation,
style,
}: {
embed: AppBskyEmbedRecord.View
moderation: ModerationUI
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
@@ -38,17 +40,9 @@ export function MaybeQuoteEmbed({
AppBskyFeedPost.validateRecord(embed.record.value).success
) {
return (
<QuoteEmbed
quote={{
author: embed.record.author,
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}
<QuoteEmbedModerated
viewRecord={embed.record}
postRecord={embed.record.value}
style={style}
/>
)
@@ -74,19 +68,49 @@ export function MaybeQuoteEmbed({
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({
quote,
moderation,
style,
}: {
quote: ComposerOptsQuote
moderation?: ModerationUI
moderation?: ModerationDecision
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const itemUrip = new AtUri(quote.uri)
const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey)
const itemTitle = `Post by ${quote.author.handle}`
const richText = React.useMemo(
() =>
quote.text.trim()
@@ -94,6 +118,7 @@ export function QuoteEmbed({
: undefined,
[quote.text, quote.facets],
)
const embed = React.useMemo(() => {
const e = quote.embeds?.[0]
@@ -107,39 +132,55 @@ export function QuoteEmbed({
return e.media
}
}, [quote.embeds])
return (
<Link
style={[styles.container, pal.borderDark, style]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
href={itemHref}
title={itemTitle}>
<View pointerEvents="none">
<PostMeta
author={quote.author}
showAvatar
authorHasWarning={false}
postHref={itemHref}
timestamp={quote.indexedAt}
/>
</View>
{moderation ? (
<PostAlerts moderation={moderation} style={styles.alert} />
) : null}
{richText ? (
<RichText
richText={richText}
type="post-text"
style={pal.text}
numberOfLines={20}
noLinks
/>
) : null}
{embed && <PostEmbeds embed={embed} moderation={{}} />}
</Link>
<ContentHider modui={moderation?.ui('contentList')}>
<Link
style={[styles.container, pal.borderDark, style]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
href={itemHref}
title={itemTitle}>
<View pointerEvents="none">
<PostMeta
author={quote.author}
showAvatar
authorHasWarning={false}
postHref={itemHref}
timestamp={quote.indexedAt}
/>
</View>
{moderation ? (
<PostAlerts
modui={moderation.ui('contentView')}
style={styles.alert}
/>
) : null}
{richText ? (
<RichText
richText={richText}
type="post-text"
style={pal.text}
numberOfLines={20}
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({
container: {
+49 -55
View File
@@ -15,8 +15,7 @@ import {
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyGraphDefs,
ModerationUI,
PostModeration,
ModerationDecision,
} from '@atproto/api'
import {Link} from '../Link'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
@@ -26,7 +25,6 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {MaybeQuoteEmbed} from './QuoteEmbed'
import {AutoSizedImage} from '../images/AutoSizedImage'
import {ListEmbed} from './ListEmbed'
import {isCauseALabelOnUri, isQuoteBlurred} from 'lib/moderation'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {ContentHider} from '../moderation/ContentHider'
import {isNative} from '#/platform/detection'
@@ -42,12 +40,10 @@ type Embed =
export function PostEmbeds({
embed,
moderation,
moderationDecisions,
style,
}: {
embed?: Embed
moderation: ModerationUI
moderationDecisions?: PostModeration['decisions']
moderation?: ModerationDecision
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
@@ -66,18 +62,10 @@ export function PostEmbeds({
// quote post with media
// =
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 (
<View style={style}>
<PostEmbeds embed={embed.media} moderation={mediaModeration} />
<ContentHider moderation={quoteModeration}>
<MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} />
</ContentHider>
<PostEmbeds embed={embed.media} moderation={moderation} />
<MaybeQuoteEmbed embed={embed.record} />
</View>
)
}
@@ -86,6 +74,7 @@ export function PostEmbeds({
// custom feed embed (i.e. generator view)
// =
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
// TODO moderation
return (
<FeedSourceCard
feedUri={embed.record.uri}
@@ -97,16 +86,13 @@ export function PostEmbeds({
// list embed
if (AppBskyGraphDefs.isListView(embed.record)) {
// TODO moderation
return <ListEmbed item={embed.record} />
}
// quote post
// =
return (
<ContentHider moderation={moderation}>
<MaybeQuoteEmbed embed={embed} style={style} moderation={moderation} />
</ContentHider>
)
return <MaybeQuoteEmbed embed={embed} style={style} />
}
// image embed
@@ -132,35 +118,41 @@ export function PostEmbeds({
if (images.length === 1) {
const {alt, thumb, aspectRatio} = images[0]
return (
<View style={[styles.imagesContainer, style]}>
<AutoSizedImage
alt={alt}
uri={thumb}
dimensionsHint={aspectRatio}
onPress={() => _openLightbox(0)}
onPressIn={() => onPressIn(0)}
style={[styles.singleImage]}>
{alt === '' ? null : (
<View style={styles.altContainer}>
<Text style={styles.alt} accessible={false}>
ALT
</Text>
</View>
)}
</AutoSizedImage>
</View>
<ContentHider modui={moderation?.ui('contentMedia')}>
<View style={[styles.imagesContainer, style]}>
<AutoSizedImage
alt={alt}
uri={thumb}
dimensionsHint={aspectRatio}
onPress={() => _openLightbox(0)}
onPressIn={() => onPressIn(0)}
style={[styles.singleImage]}>
{alt === '' ? null : (
<View style={styles.altContainer}>
<Text style={styles.alt} accessible={false}>
ALT
</Text>
</View>
)}
</AutoSizedImage>
</View>
</ContentHider>
)
}
return (
<View style={[styles.imagesContainer, style]}>
<ImageLayoutGrid
images={embed.images}
onPress={_openLightbox}
onPressIn={onPressIn}
style={embed.images.length === 1 ? [styles.singleImage] : undefined}
/>
</View>
<ContentHider modui={moderation?.ui('contentMedia')}>
<View style={[styles.imagesContainer, style]}>
<ImageLayoutGrid
images={embed.images}
onPress={_openLightbox}
onPressIn={onPressIn}
style={
embed.images.length === 1 ? [styles.singleImage] : undefined
}
/>
</View>
</ContentHider>
)
}
}
@@ -171,15 +163,17 @@ export function PostEmbeds({
const link = embed.external
return (
<Link
asAnchor
anchorNoUnderline
href={link.uri}
style={[styles.extOuter, pal.view, pal.borderDark, style]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
onLongPress={onShareExternal}>
<ExternalLinkEmbed link={link} />
</Link>
<ContentHider modui={moderation?.ui('contentMedia')}>
<Link
asAnchor
anchorNoUnderline
href={link.uri}
style={[styles.extOuter, pal.view, pal.borderDark, style]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
onLongPress={onShareExternal}>
<ExternalLinkEmbed link={link} />
</Link>
</ContentHider>
)
}
+366 -338
View File
@@ -7,14 +7,16 @@ import {
moderatePost,
moderateProfile,
ModerationOpts,
PostModeration,
ProfileModeration,
ModerationUI,
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost,
LabelTarget,
LabelPreference,
ModerationDecision,
ModerationBehavior,
RichText,
} from '@atproto/api'
import {moderationOptsOverrideContext} from '#/state/queries/preferences'
import {atoms as a, useTheme} from '#/alf'
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 * as Toggle from '#/components/forms/Toggle'
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 {PostHider} from '../com/util/moderation/PostHider'
import {PostAlerts} from '../com/util/moderation/PostAlerts'
import {ContentHider} from '../com/util/moderation/ContentHider'
import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
} from '#/components/icons/Chevron'
import {ScreenHider} from '../com/util/moderation/ScreenHider'
import {ProfileHeader} from '../com/profile/ProfileHeader'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {ProfileCardPills} from '../com/profile/ProfileCard'
import {ProfileCard} 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(
LABELS,
) as (keyof typeof LABELS)[]
const MOCK_MOD_OPTS = {
userDid: 'at://did:web:alice',
userDid: 'did:web:alice.test',
adultContentEnabled: true,
labelGroups: {},
mods: [
@@ -54,36 +57,53 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
'DebugMod'
>) => {
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 [target, setTarget] = React.useState<string[]>(['account'])
const [visibility, setVisiblity] = React.useState<string[]>(['hide'])
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 mockedProfile = mock.profileViewBasic({
handle: `bob.test`,
displayName: 'Bob Robertson',
description: 'User with this as their bio',
labels:
target[0] === 'account'
scenario[0] === 'label' && target[0] === 'account'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
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({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
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,
viewer: mock.actorViewerState({
muted: false,
muted: scenario[0] === 'mute',
mutedByList: undefined,
blockedBy: undefined,
blocking: undefined,
blocking:
scenario[0] === 'block'
? 'did://did:web:alice/app.bsky.actor.block/fake'
: undefined,
blockingByList: undefined,
}),
})
@@ -91,7 +111,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
mockedProfile.banner =
'https://bsky.social/about/images/social-card-default-gradient.png'
return mockedProfile
}, [target, label])
}, [scenario, target, label, isSelfLabel])
const post = React.useMemo(() => {
return mock.postView({
@@ -100,41 +120,63 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
}),
author: profile,
labels:
target[0] === 'post'
scenario[0] === 'label' && target[0] === 'post'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
val: label[0],
uri: `at://bob.test/app.bsky.feed.post/fake`,
}),
]
: undefined,
embed: mock.embedRecordView({
record: mock.post({
text: 'Embed',
}),
labels:
target[0] === 'embed'
? [
mock.label({
val: label[0],
uri: `at://bob.test/app.bsky.feed.post/fake`,
}),
]
: undefined,
author: profile,
}),
embed:
target[0] === 'embed'
? mock.embedRecordView({
record: mock.post({
text: 'Embed',
}),
labels:
scenario[0] === 'label' && target[0] === 'embed'
? [
mock.label({
src: isSelfLabel ? 'did:web:bob.test' : undefined,
val: label[0],
uri: `at://bob.test/app.bsky.feed.post/fake`,
}),
]
: 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(() => {
return {
...MOCK_MOD_OPTS,
userDid: isLoggedOut
? ''
: isTargetMe
? 'did:web:bob.test'
: 'did:web:alice.test',
adultContentEnabled: !noAdult,
labelGroups: {
[LABELS[label[0] as keyof typeof LABELS].groupId]:
visibility[0] as LabelPreference,
},
}
}, [label, visibility])
}, [label, visibility, noAdult, isLoggedOut, isTargetMe])
const profileModeration = React.useMemo(() => {
return moderateProfile(profile, modOpts)
@@ -143,188 +185,201 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
return moderatePost(post, modOpts)
}, [post, modOpts])
console.log(post, profile)
console.log(profileModeration, postModeration)
console.log(post)
return (
<ScrollView>
<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>
<moderationOptsOverrideContext.Provider value={modOpts}>
<ScrollView>
<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="" 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="Config" />
<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" />
<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>
{scenario[0] === 'label' && (
<>
<Toggle.Group
label="Toggle"
type="checkbox"
values={scenarioSwitches}
onChange={setScenarioSwitches}>
<View style={[a.gap_md, a.flex_row, a.pt_md]}>
<Toggle.Item name="targetMe" label="Target is me">
<Toggle.Checkbox />
<Toggle.Label>Target is me</Toggle.Label>
</Toggle.Item>
<Toggle.Item name="selfLabel" label="Self label">
<Toggle.Checkbox />
<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" />
<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>
<View style={{height: 10}} />
<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]} />
<Text style={{fontFamily: 'monospace'}}>
{JSON.stringify(LABELS[label[0]], null, 2)}
</Text>
<View style={{height: 10}} />
<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" />
<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 />
<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" />
<MockPost
label={label[0]}
context="feed"
post={post}
moderation={postModeration}
/>
<Spacer />
<Spacer />
<Heading title="Post" subtitle="in feed" />
<MockPostFeedItem post={post} moderation={postModeration} />
<Heading title="Post" subtitle="viewed directly" />
<MockPost
label={label[0]}
context="view"
post={post}
moderation={postModeration}
/>
<Spacer />
<Spacer />
<Heading title="Post" subtitle="viewed directly" />
<MockPostThreadItem post={post} moderation={postModeration} />
<Heading title="Post" subtitle="reply in thread" />
<MockPost
label={label[0]}
context="reply"
post={post}
moderation={postModeration}
/>
<Spacer />
<Spacer />
<Heading title="Post" subtitle="reply in thread" />
<MockPostThreadItem post={post} moderation={postModeration} reply />
<Heading title="Notification" subtitle="quote or reply" />
<P>TODO</P>
<Spacer />
<Spacer />
<Heading title="Notification" subtitle="quote or reply" />
<P>TODO</P>
{(target[0] === 'account' || target[0] === 'profile') && (
<>
<Heading title="Notification" subtitle="follow or like" />
<P>TODO</P>
<Spacer />
<Spacer />
{(target[0] === 'account' || target[0] === 'profile') && (
<>
<Heading title="Notification" subtitle="follow or like" />
<P>TODO</P>
<Heading title="Account" subtitle="in listing" />
<MockAccountCard
label={label[0]}
profile={profile}
moderation={profileModeration}
/>
<Spacer />
<Spacer />
<Heading title="Account" subtitle="in listing" />
<MockAccountCard
profile={profile}
moderation={profileModeration}
/>
<Heading title="Account" subtitle="viewing directly" />
<MockAccountScreen
label={label[0]}
profile={profile}
moderation={profileModeration}
moderationOpts={modOpts}
/>
</>
)}
<Spacer />
<View style={{height: 400}} />
</CenteredView>
</ScrollView>
<Heading title="Account" subtitle="viewing directly" />
<MockAccountScreen
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() {
return <View style={{height: 40}} />
function Toggler({label, children}: React.PropsWithChildren<{label: string}>) {
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,
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,
moderation,
}: {
label: string
context: 'feed' | 'view' | 'reply'
post: AppBskyFeedDefs.PostView
moderation: PostModeration
moderation: ModerationDecision
}) {
const t = useTheme()
if (context === 'feed' && moderation.content.filter) {
if (moderation.ui('contentList').filter) {
return (
<P style={[t.atoms.bg_contrast_50, a.px_lg, a.py_md]}>
Filtered from the feed
</P>
)
}
return (
<View
style={[t.atoms.border_contrast_medium, a.border, a.rounded_md, a.p_2xs]}>
{' '}
<PostHider
key={label}
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>
<FeedItem
post={post}
record={post.record as AppBskyFeedPost.Record}
moderation={moderation}
reason={undefined}
/>
)
}
<PostAlerts moderation={moderation.content} />
<P style={a.mb_lg}>
This is the body of the post. It's where the text goes. You get
the idea.
</P>
<ContentHider
moderation={moderation.embed}
moderationDecisions={moderation.decisions}>
<View
style={[
t.atoms.border_contrast_medium,
a.border,
a.rounded_md,
a.flex_col,
a.gap_xs,
a.px_xl,
a.py_xl,
]}>
<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 MockPostThreadItem({
post,
reply,
}: {
post: AppBskyFeedDefs.PostView
moderation: ModerationDecision
reply?: boolean
}) {
return (
<PostThreadItem
// @ts-ignore
post={post}
record={post.record as AppBskyFeedPost.Record}
depth={reply ? 1 : 0}
isHighlightedPost={!reply}
treeView={false}
prevPost={undefined}
nextPost={undefined}
hasPrecedingItem={false}
onPostReply={() => {}}
/>
)
}
function MockAccountCard({
label,
profile,
moderation,
}: {
label: string
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration
moderation: ModerationDecision
}) {
const t = useTheme()
if (moderation.account.filter || moderation.profile.filter) {
if (moderation.ui('profileList').filter) {
return (
<P style={[t.atoms.bg_contrast_50, a.px_lg, a.py_md]}>
Filtered from the listing
@@ -446,104 +542,36 @@ function MockAccountCard({
)
}
return (
<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>
)
return <ProfileCard profile={profile} />
}
function MockAccountScreen({
label,
profile,
moderation,
moderationOpts,
}: {
label: string
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration
moderation: ModerationDecision
moderationOpts: ModerationOpts
}) {
const t = useTheme()
return (
<View
key={label}
style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
<View style={[t.atoms.border_contrast_medium, a.border, a.mb_md]}>
<ScreenHider
style={{}}
screenDescription="profile"
moderation={moderation.account}>
modui={moderation.ui('profileView')}>
<ProfileHeader
// @ts-ignore ProfileViewBasic is close enough -prf
profile={profile}
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>
</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}) {
const t = useTheme()
return (
@@ -556,12 +584,12 @@ function Flag({v, label}: {v: boolean | undefined; label: string}) {
a.border,
t.atoms.border_contrast_medium,
{
backgroundColor: v ? t.palette.black : t.palette.white,
backgroundColor: t.palette.contrast_25,
width: 14,
height: 14,
},
]}>
{v && <Check size="xs" fill={t.palette.white} />}
{v && <Check size="xs" fill={t.palette.contrast_900} />}
</View>
<P style={a.text_xs}>{label}</P>
</View>
+1 -1
View File
@@ -298,7 +298,7 @@ function ProfileScreenLoaded({
testID="profileView"
style={styles.container}
screenDescription="profile"
moderation={moderation.account}>
modui={moderation.ui('profileView')}>
<PagerWithHeader
testID="profilePager"
isHeaderReady={!showPlaceholder}
+41
View File
@@ -129,6 +129,15 @@ export function Buttons() {
<ButtonIcon icon={Globe} position="left" />
<ButtonText>Link out</ButtonText>
</Button>
<Button
variant="gradient"
color="gradient_sky"
size="tiny"
label="Link out">
<ButtonIcon icon={Globe} position="left" />
<ButtonText>Link out</ButtonText>
</Button>
</View>
<View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -148,6 +157,14 @@ export function Buttons() {
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="outline"
color="primary"
@@ -164,6 +181,14 @@ export function Buttons() {
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="ghost"
color="primary"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
</View>
<View style={[a.flex_row, a.gap_md, a.align_start]}>
@@ -183,6 +208,14 @@ export function Buttons() {
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="outline"
color="primary"
@@ -199,6 +232,14 @@ export function Buttons() {
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="ghost"
color="primary"
size="tiny"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
</View>
</View>
)
+4 -4
View File
@@ -11,7 +11,7 @@ import {useNavigation, StackActions} from '@react-navigation/native'
import {
AppBskyActorDefs,
moderateProfile,
ProfileModeration,
ModerationDecision,
} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -86,7 +86,7 @@ export function SearchProfileCard({
moderation,
}: {
profile: AppBskyActorDefs.ProfileViewBasic
moderation: ProfileModeration
moderation: ModerationDecision
}) {
const pal = usePalette('default')
@@ -111,7 +111,7 @@ export function SearchProfileCard({
<UserAvatar
size={40}
avatar={profile.avatar}
moderation={moderation.avatar}
moderation={moderation.ui('avatar')}
/>
<View style={{flex: 1}}>
<Text
@@ -121,7 +121,7 @@ export function SearchProfileCard({
lineHeight={1.2}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.profile,
moderation.ui('displayName'),
)}
</Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>