Refactor/rewrite the entire moderation-application system
This commit is contained in:
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
@@ -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
@@ -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]}>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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}>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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}>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
@@ -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 · 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 · 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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user