Add self-labeling controls (#1141)
* Add self-label modal * Use the shield-exclamation icon consistently on post moderation * Wire up self-labeling * Bump @atproto/api@0.6.0 * Bump @atproto/dev-env@^0.2.3 * Add e2e test for self-labeling * Fix typeszio/stable
parent
48813a96d6
commit
03d152675e
|
@ -0,0 +1,34 @@
|
|||
/* eslint-env detox/detox */
|
||||
|
||||
import {openApp, login, createServer, sleep} from '../util'
|
||||
|
||||
describe('Self-labeling', () => {
|
||||
let service: string
|
||||
beforeAll(async () => {
|
||||
service = await createServer('?users')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await login(service, 'alice', 'hunter2')
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
})
|
||||
|
||||
it('Post an image with the porn label', async () => {
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Post with an image')
|
||||
await element(by.id('openGalleryBtn')).tap()
|
||||
await sleep(1e3)
|
||||
await element(by.id('labelsBtn')).tap()
|
||||
await element(by.id('pornLabelBtn')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
const posts = by.id('feedItem-by-alice.test')
|
||||
await expect(
|
||||
element(by.id('contentHider-embed').withAncestor(posts)).atIndex(0),
|
||||
).toExist()
|
||||
})
|
||||
})
|
|
@ -24,7 +24,7 @@
|
|||
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.5.4",
|
||||
"@atproto/api": "^0.6.0",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@expo/html-elements": "^0.4.2",
|
||||
|
@ -146,7 +146,7 @@
|
|||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.2.2",
|
||||
"@atproto/dev-env": "^0.2.3",
|
||||
"@atproto/pds": "^0.2.0-beta.2",
|
||||
"@babel/core": "^7.20.0",
|
||||
"@babel/preset-env": "^7.20.0",
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyRichtextFacet,
|
||||
ComAtprotoLabelDefs,
|
||||
ComAtprotoRepoUploadBlob,
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
|
@ -77,6 +78,7 @@ interface PostOpts {
|
|||
}
|
||||
extLink?: ExternalEmbedDraft
|
||||
images?: ImageModel[]
|
||||
labels?: string[]
|
||||
knownHandles?: Set<string>
|
||||
onStateChange?: (state: string) => void
|
||||
langs?: string[]
|
||||
|
@ -234,6 +236,15 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
|||
}
|
||||
}
|
||||
|
||||
// set labels
|
||||
let labels: ComAtprotoLabelDefs.SelfLabels | undefined
|
||||
if (opts.labels?.length) {
|
||||
labels = {
|
||||
$type: 'com.atproto.label.defs#selfLabels',
|
||||
values: opts.labels.map(val => ({val})),
|
||||
}
|
||||
}
|
||||
|
||||
// add top 3 languages from user preferences if langs is provided
|
||||
let langs = opts.langs
|
||||
if (opts.langs) {
|
||||
|
@ -248,6 +259,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
|||
reply,
|
||||
embed,
|
||||
langs,
|
||||
labels,
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.error(`Failed to create post: ${e.toString()}`)
|
||||
|
|
|
@ -957,3 +957,41 @@ export function SatelliteDishIcon({
|
|||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Copyright (c) 2020 Refactoring UI Inc.
|
||||
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
|
||||
export function ShieldExclamation({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<TextStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
let color = 'currentColor'
|
||||
if (
|
||||
style &&
|
||||
typeof style === 'object' &&
|
||||
'color' in style &&
|
||||
typeof style.color === 'string'
|
||||
) {
|
||||
color = style.color
|
||||
}
|
||||
return (
|
||||
<Svg
|
||||
width={size}
|
||||
height={size}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={strokeWidth || 1.5}
|
||||
stroke={color}
|
||||
style={style}>
|
||||
<Path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 9v3.75m0-10.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.249-8.25-3.286zm0 13.036h.008v.008H12v-.008z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -29,12 +29,7 @@ export function describeModerationCause(
|
|||
}
|
||||
}
|
||||
if (cause.type === 'muted') {
|
||||
if (cause.source.type === 'user') {
|
||||
return {
|
||||
name: context === 'account' ? 'Muted User' : 'Post by muted user',
|
||||
description: 'You have muted this user',
|
||||
}
|
||||
} else {
|
||||
if (cause.source.type === 'list') {
|
||||
return {
|
||||
name:
|
||||
context === 'account'
|
||||
|
@ -42,6 +37,11 @@ export function describeModerationCause(
|
|||
: `Post by muted user ("${cause.source.list.name}")`,
|
||||
description: 'You have muted this user',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
name: context === 'account' ? 'Muted User' : 'Post by muted user',
|
||||
description: 'You have muted this user',
|
||||
}
|
||||
}
|
||||
}
|
||||
return cause.labelDef.strings[context].en
|
||||
|
|
|
@ -418,34 +418,35 @@ export class PreferencesModel {
|
|||
return {
|
||||
userDid: this.rootStore.session.currentSession?.did || '',
|
||||
adultContentEnabled: this.adultContentEnabled,
|
||||
labelerSettings: [
|
||||
labels: {
|
||||
// TEMP translate old settings until this UI can be migrated -prf
|
||||
porn: tempfixLabelPref(this.contentLabels.nsfw),
|
||||
sexual: tempfixLabelPref(this.contentLabels.suggestive),
|
||||
nudity: tempfixLabelPref(this.contentLabels.nudity),
|
||||
nsfl: tempfixLabelPref(this.contentLabels.gore),
|
||||
corpse: tempfixLabelPref(this.contentLabels.gore),
|
||||
gore: tempfixLabelPref(this.contentLabels.gore),
|
||||
torture: tempfixLabelPref(this.contentLabels.gore),
|
||||
'self-harm': tempfixLabelPref(this.contentLabels.gore),
|
||||
'intolerant-race': tempfixLabelPref(this.contentLabels.hate),
|
||||
'intolerant-gender': tempfixLabelPref(this.contentLabels.hate),
|
||||
'intolerant-sexual-orientation': tempfixLabelPref(
|
||||
this.contentLabels.hate,
|
||||
),
|
||||
'intolerant-religion': tempfixLabelPref(this.contentLabels.hate),
|
||||
intolerant: tempfixLabelPref(this.contentLabels.hate),
|
||||
'icon-intolerant': tempfixLabelPref(this.contentLabels.hate),
|
||||
spam: tempfixLabelPref(this.contentLabels.spam),
|
||||
impersonation: tempfixLabelPref(this.contentLabels.impersonation),
|
||||
scam: 'warn',
|
||||
},
|
||||
labelers: [
|
||||
{
|
||||
labeler: {
|
||||
did: '',
|
||||
displayName: 'Bluesky Social',
|
||||
},
|
||||
settings: {
|
||||
// TEMP translate old settings until this UI can be migrated -prf
|
||||
porn: tempfixLabelPref(this.contentLabels.nsfw),
|
||||
sexual: tempfixLabelPref(this.contentLabels.suggestive),
|
||||
nudity: tempfixLabelPref(this.contentLabels.nudity),
|
||||
nsfl: tempfixLabelPref(this.contentLabels.gore),
|
||||
corpse: tempfixLabelPref(this.contentLabels.gore),
|
||||
gore: tempfixLabelPref(this.contentLabels.gore),
|
||||
torture: tempfixLabelPref(this.contentLabels.gore),
|
||||
'self-harm': tempfixLabelPref(this.contentLabels.gore),
|
||||
'intolerant-race': tempfixLabelPref(this.contentLabels.hate),
|
||||
'intolerant-gender': tempfixLabelPref(this.contentLabels.hate),
|
||||
'intolerant-sexual-orientation': tempfixLabelPref(
|
||||
this.contentLabels.hate,
|
||||
),
|
||||
'intolerant-religion': tempfixLabelPref(this.contentLabels.hate),
|
||||
intolerant: tempfixLabelPref(this.contentLabels.hate),
|
||||
'icon-intolerant': tempfixLabelPref(this.contentLabels.hate),
|
||||
spam: tempfixLabelPref(this.contentLabels.spam),
|
||||
impersonation: tempfixLabelPref(this.contentLabels.impersonation),
|
||||
scam: 'warn',
|
||||
},
|
||||
labels: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -100,6 +100,12 @@ export interface RepostModal {
|
|||
isReposted: boolean
|
||||
}
|
||||
|
||||
export interface SelfLabelModal {
|
||||
name: 'self-label'
|
||||
labels: string[]
|
||||
onChange: (labels: string[]) => void
|
||||
}
|
||||
|
||||
export interface ChangeHandleModal {
|
||||
name: 'change-handle'
|
||||
onChanged: () => void
|
||||
|
@ -164,6 +170,7 @@ export type Modal =
|
|||
| EditImageModal
|
||||
| ServerInputModal
|
||||
| RepostModal
|
||||
| SelfLabelModal
|
||||
|
||||
// Bluesky access
|
||||
| WaitlistModal
|
||||
|
|
|
@ -41,6 +41,7 @@ import {isDesktopWeb, isAndroid, isIOS} from 'platform/detection'
|
|||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {Gallery} from './photos/Gallery'
|
||||
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
|
||||
import {LabelsBtn} from './labels/LabelsBtn'
|
||||
import {SelectLangBtn} from './select-language/SelectLangBtn'
|
||||
|
||||
type Props = ComposerOpts & {
|
||||
|
@ -67,6 +68,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
initQuote,
|
||||
)
|
||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||
const [labels, setLabels] = useState<string[]>([])
|
||||
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
|
||||
const gallery = useMemo(() => new GalleryModel(store), [store])
|
||||
|
||||
|
@ -145,75 +147,59 @@ export const ComposePost = observer(function ComposePost({
|
|||
[gallery, track],
|
||||
)
|
||||
|
||||
const onPressPublish = useCallback(
|
||||
async (rt: RichText) => {
|
||||
if (isProcessing || rt.graphemeLength > MAX_GRAPHEME_LENGTH) {
|
||||
return
|
||||
}
|
||||
if (store.preferences.requireAltTextEnabled && gallery.needsAltText) {
|
||||
return
|
||||
}
|
||||
const onPressPublish = async (rt: RichText) => {
|
||||
if (isProcessing || rt.graphemeLength > MAX_GRAPHEME_LENGTH) {
|
||||
return
|
||||
}
|
||||
if (store.preferences.requireAltTextEnabled && gallery.needsAltText) {
|
||||
return
|
||||
}
|
||||
|
||||
setError('')
|
||||
setError('')
|
||||
|
||||
if (rt.text.trim().length === 0 && gallery.isEmpty) {
|
||||
setError('Did you want to say anything?')
|
||||
return
|
||||
}
|
||||
if (rt.text.trim().length === 0 && gallery.isEmpty) {
|
||||
setError('Did you want to say anything?')
|
||||
return
|
||||
}
|
||||
|
||||
setIsProcessing(true)
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
await apilib.post(store, {
|
||||
rawText: rt.text,
|
||||
replyTo: replyTo?.uri,
|
||||
images: gallery.images,
|
||||
quote: quote,
|
||||
extLink: extLink,
|
||||
onStateChange: setProcessingState,
|
||||
knownHandles: autocompleteView.knownHandles,
|
||||
langs: store.preferences.postLanguages,
|
||||
})
|
||||
} catch (e: any) {
|
||||
if (extLink) {
|
||||
setExtLink({
|
||||
...extLink,
|
||||
isLoading: true,
|
||||
localThumb: undefined,
|
||||
} as apilib.ExternalEmbedDraft)
|
||||
}
|
||||
setError(cleanError(e.message))
|
||||
setIsProcessing(false)
|
||||
return
|
||||
} finally {
|
||||
track('Create Post', {
|
||||
imageCount: gallery.size,
|
||||
})
|
||||
if (replyTo && replyTo.uri) track('Post:Reply')
|
||||
try {
|
||||
await apilib.post(store, {
|
||||
rawText: rt.text,
|
||||
replyTo: replyTo?.uri,
|
||||
images: gallery.images,
|
||||
quote,
|
||||
extLink,
|
||||
labels,
|
||||
onStateChange: setProcessingState,
|
||||
knownHandles: autocompleteView.knownHandles,
|
||||
langs: store.preferences.postLanguages,
|
||||
})
|
||||
} catch (e: any) {
|
||||
if (extLink) {
|
||||
setExtLink({
|
||||
...extLink,
|
||||
isLoading: true,
|
||||
localThumb: undefined,
|
||||
} as apilib.ExternalEmbedDraft)
|
||||
}
|
||||
if (!replyTo) {
|
||||
store.me.mainFeed.onPostCreated()
|
||||
}
|
||||
onPost?.()
|
||||
onClose()
|
||||
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
|
||||
},
|
||||
[
|
||||
isProcessing,
|
||||
setError,
|
||||
setIsProcessing,
|
||||
replyTo,
|
||||
autocompleteView.knownHandles,
|
||||
extLink,
|
||||
onClose,
|
||||
onPost,
|
||||
quote,
|
||||
setExtLink,
|
||||
store,
|
||||
track,
|
||||
gallery,
|
||||
],
|
||||
)
|
||||
setError(cleanError(e.message))
|
||||
setIsProcessing(false)
|
||||
return
|
||||
} finally {
|
||||
track('Create Post', {
|
||||
imageCount: gallery.size,
|
||||
})
|
||||
if (replyTo && replyTo.uri) track('Post:Reply')
|
||||
}
|
||||
if (!replyTo) {
|
||||
store.me.mainFeed.onPostCreated()
|
||||
}
|
||||
onPost?.()
|
||||
onClose()
|
||||
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
|
||||
}
|
||||
|
||||
const canPost = useMemo(
|
||||
() =>
|
||||
|
@ -246,6 +232,7 @@ export const ComposePost = observer(function ComposePost({
|
|||
<Text style={[pal.link, s.f18]}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
<LabelsBtn labels={labels} onChange={setLabels} />
|
||||
{isProcessing ? (
|
||||
<View style={styles.postBtn}>
|
||||
<ActivityIndicator />
|
||||
|
@ -407,7 +394,7 @@ const styles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingTop: isDesktopWeb ? 10 : undefined,
|
||||
paddingBottom: 10,
|
||||
paddingBottom: isDesktopWeb ? 10 : 4,
|
||||
paddingHorizontal: 20,
|
||||
height: 55,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useStores} from 'state/index'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
||||
|
||||
export const LabelsBtn = observer(function LabelsBtn({
|
||||
labels,
|
||||
onChange,
|
||||
}: {
|
||||
labels: string[]
|
||||
onChange: (v: string[]) => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="default-light"
|
||||
testID="labelsBtn"
|
||||
style={styles.button}
|
||||
accessibilityLabel="Content warnings"
|
||||
accessibilityHint=""
|
||||
onPress={() =>
|
||||
store.shell.openModal({name: 'self-label', labels, onChange})
|
||||
}>
|
||||
<ShieldExclamation style={pal.link} size={26} />
|
||||
{labels.length > 0 ? (
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
size={16}
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
/>
|
||||
) : null}
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 14,
|
||||
marginRight: 4,
|
||||
},
|
||||
label: {
|
||||
maxWidth: 100,
|
||||
},
|
||||
})
|
|
@ -15,6 +15,7 @@ import * as ProfilePreviewModal from './ProfilePreview'
|
|||
import * as ServerInputModal from './ServerInput'
|
||||
import * as ReportPostModal from './report/ReportPost'
|
||||
import * as RepostModal from './Repost'
|
||||
import * as SelfLabelModal from './SelfLabel'
|
||||
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
||||
import * as ListAddRemoveUserModal from './ListAddRemoveUser'
|
||||
import * as AltImageModal from './AltImage'
|
||||
|
@ -104,6 +105,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
|||
} else if (activeModal?.name === 'repost') {
|
||||
snapPoints = RepostModal.snapPoints
|
||||
element = <RepostModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'self-label') {
|
||||
snapPoints = SelfLabelModal.snapPoints
|
||||
element = <SelfLabelModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'alt-text-image') {
|
||||
snapPoints = AltImageModal.snapPoints
|
||||
element = <AltImageModal.Component {...activeModal} />
|
||||
|
|
|
@ -16,6 +16,7 @@ import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
|
|||
import * as ListAddRemoveUserModal from './ListAddRemoveUser'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as RepostModal from './Repost'
|
||||
import * as SelfLabelModal from './SelfLabel'
|
||||
import * as CropImageModal from './crop-image/CropImage.web'
|
||||
import * as AltTextImageModal from './AltImage'
|
||||
import * as EditImageModal from './EditImage'
|
||||
|
@ -89,6 +90,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
element = <DeleteAccountModal.Component />
|
||||
} else if (modal.name === 'repost') {
|
||||
element = <RepostModal.Component {...modal} />
|
||||
} else if (modal.name === 'self-label') {
|
||||
element = <SelfLabelModal.Component {...modal} />
|
||||
} else if (modal.name === 'change-handle') {
|
||||
element = <ChangeHandleModal.Component {...modal} />
|
||||
} else if (modal.name === 'waitlist') {
|
||||
|
|
|
@ -35,10 +35,7 @@ export function Component({
|
|||
name = 'Account Blocks You'
|
||||
description = 'This user has blocked you. You cannot view their content.'
|
||||
} else if (moderation.cause.type === 'muted') {
|
||||
if (moderation.cause.source.type === 'user') {
|
||||
name = 'Account Muted'
|
||||
description = 'You have muted this user.'
|
||||
} else {
|
||||
if (moderation.cause.source.type === 'list') {
|
||||
const list = moderation.cause.source.list
|
||||
name = <>Account Muted by List</>
|
||||
description = (
|
||||
|
@ -53,6 +50,9 @@ export function Component({
|
|||
list which you have muted.
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
name = 'Account Muted'
|
||||
description = 'You have muted this user.'
|
||||
}
|
||||
} else {
|
||||
name = moderation.cause.labelDef.strings[context].en.name
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
import React, {useState} from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {useStores} from 'state/index'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {SelectableBtn} from '../util/forms/SelectableBtn'
|
||||
import {ScrollView} from 'view/com/modals/util'
|
||||
|
||||
const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
|
||||
|
||||
export const snapPoints = ['50%']
|
||||
|
||||
export const Component = observer(function Component({
|
||||
labels,
|
||||
onChange,
|
||||
}: {
|
||||
labels: string[]
|
||||
onChange: (labels: string[]) => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const [selected, setSelected] = useState(labels)
|
||||
|
||||
const toggleAdultContent = (label: string) => {
|
||||
const hadLabel = selected.includes(label)
|
||||
const stripped = selected.filter(l => !ADULT_CONTENT_LABELS.includes(l))
|
||||
const final = !hadLabel ? stripped.concat([label]) : stripped
|
||||
setSelected(final)
|
||||
onChange(final)
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID="selfLabelModal" style={[pal.view, styles.container]}>
|
||||
<View style={styles.titleSection}>
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
Add a content warning
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView>
|
||||
<View style={[styles.section, pal.border, {borderBottomWidth: 1}]}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingBottom: 8,
|
||||
}}>
|
||||
<Text type="title" style={pal.text}>
|
||||
Adult Content
|
||||
</Text>
|
||||
|
||||
<Text type="lg" style={pal.text}>
|
||||
{selected.includes('sexual') ? (
|
||||
<>😏</>
|
||||
) : selected.includes('nudity') ? (
|
||||
<>🫣</>
|
||||
) : selected.includes('porn') ? (
|
||||
<>🥵</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={s.flexRow}>
|
||||
<SelectableBtn
|
||||
testID="sexualLabelBtn"
|
||||
selected={selected.includes('sexual')}
|
||||
left
|
||||
label="Suggestive"
|
||||
onSelect={() => toggleAdultContent('sexual')}
|
||||
accessibilityHint=""
|
||||
style={s.flex1}
|
||||
/>
|
||||
<SelectableBtn
|
||||
testID="nudityLabelBtn"
|
||||
selected={selected.includes('nudity')}
|
||||
label="Nudity"
|
||||
onSelect={() => toggleAdultContent('nudity')}
|
||||
accessibilityHint=""
|
||||
style={s.flex1}
|
||||
/>
|
||||
<SelectableBtn
|
||||
testID="pornLabelBtn"
|
||||
selected={selected.includes('porn')}
|
||||
label="Porn"
|
||||
right
|
||||
onSelect={() => toggleAdultContent('porn')}
|
||||
accessibilityHint=""
|
||||
style={s.flex1}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={[pal.text, styles.adultExplainer]}>
|
||||
{selected.includes('sexual') ? (
|
||||
<>Pictures meant for adults.</>
|
||||
) : selected.includes('nudity') ? (
|
||||
<>Artistic or non-erotic nudity.</>
|
||||
) : selected.includes('porn') ? (
|
||||
<>Sexual activity or erotic nudity.</>
|
||||
) : (
|
||||
<>If none are selected, suitable for all ages.</>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={[styles.btnContainer, pal.borderDark]}>
|
||||
<TouchableOpacity
|
||||
testID="confirmBtn"
|
||||
onPress={() => {
|
||||
store.shell.closeModal()
|
||||
}}
|
||||
style={styles.btn}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Confirm"
|
||||
accessibilityHint="">
|
||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: isDesktopWeb ? 0 : 40,
|
||||
},
|
||||
titleSection: {
|
||||
paddingTop: isDesktopWeb ? 0 : 4,
|
||||
paddingBottom: isDesktopWeb ? 14 : 10,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: '600',
|
||||
marginBottom: 5,
|
||||
},
|
||||
description: {
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
section: {
|
||||
borderTopWidth: 1,
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: isDesktopWeb ? 0 : 20,
|
||||
},
|
||||
adultExplainer: {
|
||||
paddingLeft: 5,
|
||||
paddingTop: 10,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 32,
|
||||
padding: 14,
|
||||
backgroundColor: colors.blue3,
|
||||
},
|
||||
btnContainer: {
|
||||
paddingTop: 20,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
})
|
|
@ -271,6 +271,7 @@ export const FeedItem = observer(function ({
|
|||
</View>
|
||||
)}
|
||||
<ContentHider
|
||||
testID="contentHider-post"
|
||||
moderation={item.moderation.content}
|
||||
ignoreMute
|
||||
style={styles.contentHider}
|
||||
|
@ -291,6 +292,7 @@ export const FeedItem = observer(function ({
|
|||
) : undefined}
|
||||
{item.post.embed ? (
|
||||
<ContentHider
|
||||
testID="contentHider-embed"
|
||||
moderation={item.moderation.embed}
|
||||
style={styles.embed}>
|
||||
<PostEmbeds
|
||||
|
|
|
@ -5,6 +5,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||
import {isDesktopWeb} from 'platform/detection'
|
||||
|
||||
interface SelectableBtnProps {
|
||||
testID?: string
|
||||
selected: boolean
|
||||
label: string
|
||||
left?: boolean
|
||||
|
@ -15,6 +16,7 @@ interface SelectableBtnProps {
|
|||
}
|
||||
|
||||
export function SelectableBtn({
|
||||
testID,
|
||||
selected,
|
||||
label,
|
||||
left,
|
||||
|
@ -25,12 +27,15 @@ export function SelectableBtn({
|
|||
}: SelectableBtnProps) {
|
||||
const pal = usePalette('default')
|
||||
const palPrimary = usePalette('inverted')
|
||||
const needsWidthStyles = !style || !('width' in style || 'flex' in style)
|
||||
return (
|
||||
<Pressable
|
||||
testID={testID}
|
||||
style={[
|
||||
styles.selectableBtn,
|
||||
left && styles.selectableBtnLeft,
|
||||
right && styles.selectableBtnRight,
|
||||
styles.btn,
|
||||
needsWidthStyles && styles.btnWidth,
|
||||
left && styles.btnLeft,
|
||||
right && styles.btnRight,
|
||||
pal.border,
|
||||
selected ? palPrimary.view : pal.view,
|
||||
style,
|
||||
|
@ -45,9 +50,7 @@ export function SelectableBtn({
|
|||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
selectableBtn: {
|
||||
flex: isDesktopWeb ? undefined : 1,
|
||||
width: isDesktopWeb ? 100 : undefined,
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
|
@ -55,12 +58,16 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 10,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
selectableBtnLeft: {
|
||||
btnWidth: {
|
||||
flex: isDesktopWeb ? undefined : 1,
|
||||
width: isDesktopWeb ? 100 : undefined,
|
||||
},
|
||||
btnLeft: {
|
||||
borderTopLeftRadius: 8,
|
||||
borderBottomLeftRadius: 8,
|
||||
borderLeftWidth: 1,
|
||||
},
|
||||
selectableBtnRight: {
|
||||
btnRight: {
|
||||
borderTopRightRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {useStores} from 'state/index'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
|
@ -58,7 +58,7 @@ export function ContentHider({
|
|||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityHint="">
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
<ShieldExclamation size={18} style={pal.text} />
|
||||
</Pressable>
|
||||
<Text type="lg" style={pal.text}>
|
||||
{desc.name}
|
||||
|
|
|
@ -3,7 +3,7 @@ import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native'
|
|||
import {ModerationUI} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {useStores} from 'state/index'
|
||||
|
||||
|
@ -41,7 +41,7 @@ export function PostAlerts({
|
|||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityHint=""
|
||||
style={[styles.container, pal.viewLight, style]}>
|
||||
<InfoCircleIcon style={pal.text} size={18} />
|
||||
<ShieldExclamation style={pal.text} size={16} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
|
|
|
@ -6,7 +6,7 @@ import {Link} from '../Link'
|
|||
import {Text} from '../text/Text'
|
||||
import {addStyle} from 'lib/styles'
|
||||
import {describeModerationCause} from 'lib/moderation'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {useStores} from 'state/index'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
|
||||
|
@ -67,7 +67,7 @@ export function PostHider({
|
|||
accessibilityRole="button"
|
||||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityHint="">
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
<ShieldExclamation size={18} style={pal.text} />
|
||||
</Pressable>
|
||||
<Text type="lg" style={pal.text}>
|
||||
{desc.name}
|
||||
|
|
|
@ -3,7 +3,7 @@ import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
|||
import {ProfileModeration} from '@atproto/api'
|
||||
import {Text} from '../text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {ShieldExclamation} from 'lib/icons'
|
||||
import {
|
||||
describeModerationCause,
|
||||
getProfileModerationCauses,
|
||||
|
@ -44,7 +44,7 @@ export function ProfileHeaderAlerts({
|
|||
accessibilityLabel="Learn more about this warning"
|
||||
accessibilityHint=""
|
||||
style={[styles.container, pal.viewLight, style]}>
|
||||
<InfoCircleIcon style={pal.text} size={24} />
|
||||
<ShieldExclamation style={pal.text} size={24} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
{desc.name}
|
||||
</Text>
|
||||
|
|
|
@ -72,6 +72,8 @@ import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSq
|
|||
import {faShield} from '@fortawesome/free-solid-svg-icons/faShield'
|
||||
import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal'
|
||||
import {faSliders} from '@fortawesome/free-solid-svg-icons/faSliders'
|
||||
import {faSquare} from '@fortawesome/free-regular-svg-icons/faSquare'
|
||||
import {faSquareCheck} from '@fortawesome/free-regular-svg-icons/faSquareCheck'
|
||||
import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus'
|
||||
import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
|
||||
import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
|
||||
|
@ -162,6 +164,8 @@ export function setup() {
|
|||
faShield,
|
||||
faSignal,
|
||||
faSliders,
|
||||
faSquare,
|
||||
faSquareCheck,
|
||||
faSquarePlus,
|
||||
faUser,
|
||||
faUsers,
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -40,10 +40,10 @@
|
|||
tlds "^1.234.0"
|
||||
typed-emitter "^2.1.0"
|
||||
|
||||
"@atproto/api@^0.5.4":
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.5.4.tgz#81a0dc36d3fcae085092434218740b68ee28f816"
|
||||
integrity sha512-e2M5d+w6PMEzunVWbeX4yD9pMXaP6FakQYOfj4gUz9tL05k94Qv1dcX8IFUHdWEunHijLGU2fU2SNC4jgiuLBA==
|
||||
"@atproto/api@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.0.tgz#c4eea08ee4d1be522928cd016d7de8061d86e573"
|
||||
integrity sha512-GkWHoGZfNneHarAYkIPJD1GGgKiI7OwnCtKS+J4AmlVKYijGEzOYgg1fY6rluT6XPT5TlQZiHUWpMlpqAkQIkQ==
|
||||
dependencies:
|
||||
"@atproto/common-web" "*"
|
||||
"@atproto/uri" "*"
|
||||
|
@ -157,10 +157,10 @@
|
|||
one-webcrypto "^1.0.3"
|
||||
uint8arrays "3.0.0"
|
||||
|
||||
"@atproto/dev-env@^0.2.2":
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.2.2.tgz#930cb63dc751b08fa0a1c69820e27587d7b38252"
|
||||
integrity sha512-94KruFi2C52YHiSmvJKjPp3414KV9ev06Sla4BebJxm1z3/DKGhmPGDEHVke4+KPZOTR3vQ2x1WKuX87mtCp7w==
|
||||
"@atproto/dev-env@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.2.3.tgz#17f7574a85f560dbd128dc3dd7620de14ff63483"
|
||||
integrity sha512-7Glr0NVWftXF8kvmVouNWhXX9DzTWpKhytQZkfPaIWf6jVRATgRSfts1QOet1lTx33DczqKBJb9xc0qDt4MqWw==
|
||||
dependencies:
|
||||
"@atproto/api" "*"
|
||||
"@atproto/bsky" "*"
|
||||
|
|
Loading…
Reference in New Issue