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 types
zio/stable
Paul Frazee 2023-08-09 17:34:16 -07:00 committed by GitHub
parent 48813a96d6
commit 03d152675e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 443 additions and 124 deletions

View File

@ -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()
})
})

View File

@ -24,7 +24,7 @@
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.5.4", "@atproto/api": "^0.6.0",
"@bam.tech/react-native-image-resizer": "^3.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@expo/html-elements": "^0.4.2", "@expo/html-elements": "^0.4.2",
@ -146,7 +146,7 @@
"zod": "^3.20.2" "zod": "^3.20.2"
}, },
"devDependencies": { "devDependencies": {
"@atproto/dev-env": "^0.2.2", "@atproto/dev-env": "^0.2.3",
"@atproto/pds": "^0.2.0-beta.2", "@atproto/pds": "^0.2.0-beta.2",
"@babel/core": "^7.20.0", "@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0", "@babel/preset-env": "^7.20.0",

View File

@ -4,6 +4,7 @@ import {
AppBskyEmbedRecord, AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia, AppBskyEmbedRecordWithMedia,
AppBskyRichtextFacet, AppBskyRichtextFacet,
ComAtprotoLabelDefs,
ComAtprotoRepoUploadBlob, ComAtprotoRepoUploadBlob,
RichText, RichText,
} from '@atproto/api' } from '@atproto/api'
@ -77,6 +78,7 @@ interface PostOpts {
} }
extLink?: ExternalEmbedDraft extLink?: ExternalEmbedDraft
images?: ImageModel[] images?: ImageModel[]
labels?: string[]
knownHandles?: Set<string> knownHandles?: Set<string>
onStateChange?: (state: string) => void onStateChange?: (state: string) => void
langs?: string[] 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 // add top 3 languages from user preferences if langs is provided
let langs = opts.langs let langs = opts.langs
if (opts.langs) { if (opts.langs) {
@ -248,6 +259,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
reply, reply,
embed, embed,
langs, langs,
labels,
}) })
} catch (e: any) { } catch (e: any) {
console.error(`Failed to create post: ${e.toString()}`) console.error(`Failed to create post: ${e.toString()}`)

View File

@ -957,3 +957,41 @@ export function SatelliteDishIcon({
</Svg> </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>
)
}

View File

@ -29,12 +29,7 @@ export function describeModerationCause(
} }
} }
if (cause.type === 'muted') { if (cause.type === 'muted') {
if (cause.source.type === 'user') { if (cause.source.type === 'list') {
return {
name: context === 'account' ? 'Muted User' : 'Post by muted user',
description: 'You have muted this user',
}
} else {
return { return {
name: name:
context === 'account' context === 'account'
@ -42,6 +37,11 @@ export function describeModerationCause(
: `Post by muted user ("${cause.source.list.name}")`, : `Post by muted user ("${cause.source.list.name}")`,
description: 'You have muted this user', 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 return cause.labelDef.strings[context].en

View File

@ -418,13 +418,7 @@ export class PreferencesModel {
return { return {
userDid: this.rootStore.session.currentSession?.did || '', userDid: this.rootStore.session.currentSession?.did || '',
adultContentEnabled: this.adultContentEnabled, adultContentEnabled: this.adultContentEnabled,
labelerSettings: [ labels: {
{
labeler: {
did: '',
displayName: 'Bluesky Social',
},
settings: {
// TEMP translate old settings until this UI can be migrated -prf // TEMP translate old settings until this UI can be migrated -prf
porn: tempfixLabelPref(this.contentLabels.nsfw), porn: tempfixLabelPref(this.contentLabels.nsfw),
sexual: tempfixLabelPref(this.contentLabels.suggestive), sexual: tempfixLabelPref(this.contentLabels.suggestive),
@ -446,6 +440,13 @@ export class PreferencesModel {
impersonation: tempfixLabelPref(this.contentLabels.impersonation), impersonation: tempfixLabelPref(this.contentLabels.impersonation),
scam: 'warn', scam: 'warn',
}, },
labelers: [
{
labeler: {
did: '',
displayName: 'Bluesky Social',
},
labels: {},
}, },
], ],
} }

View File

@ -100,6 +100,12 @@ export interface RepostModal {
isReposted: boolean isReposted: boolean
} }
export interface SelfLabelModal {
name: 'self-label'
labels: string[]
onChange: (labels: string[]) => void
}
export interface ChangeHandleModal { export interface ChangeHandleModal {
name: 'change-handle' name: 'change-handle'
onChanged: () => void onChanged: () => void
@ -164,6 +170,7 @@ export type Modal =
| EditImageModal | EditImageModal
| ServerInputModal | ServerInputModal
| RepostModal | RepostModal
| SelfLabelModal
// Bluesky access // Bluesky access
| WaitlistModal | WaitlistModal

View File

@ -41,6 +41,7 @@ import {isDesktopWeb, isAndroid, isIOS} from 'platform/detection'
import {GalleryModel} from 'state/models/media/gallery' import {GalleryModel} from 'state/models/media/gallery'
import {Gallery} from './photos/Gallery' import {Gallery} from './photos/Gallery'
import {MAX_GRAPHEME_LENGTH} from 'lib/constants' import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
import {LabelsBtn} from './labels/LabelsBtn'
import {SelectLangBtn} from './select-language/SelectLangBtn' import {SelectLangBtn} from './select-language/SelectLangBtn'
type Props = ComposerOpts & { type Props = ComposerOpts & {
@ -67,6 +68,7 @@ export const ComposePost = observer(function ComposePost({
initQuote, initQuote,
) )
const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
const [labels, setLabels] = useState<string[]>([])
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set()) const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
const gallery = useMemo(() => new GalleryModel(store), [store]) const gallery = useMemo(() => new GalleryModel(store), [store])
@ -145,8 +147,7 @@ export const ComposePost = observer(function ComposePost({
[gallery, track], [gallery, track],
) )
const onPressPublish = useCallback( const onPressPublish = async (rt: RichText) => {
async (rt: RichText) => {
if (isProcessing || rt.graphemeLength > MAX_GRAPHEME_LENGTH) { if (isProcessing || rt.graphemeLength > MAX_GRAPHEME_LENGTH) {
return return
} }
@ -168,8 +169,9 @@ export const ComposePost = observer(function ComposePost({
rawText: rt.text, rawText: rt.text,
replyTo: replyTo?.uri, replyTo: replyTo?.uri,
images: gallery.images, images: gallery.images,
quote: quote, quote,
extLink: extLink, extLink,
labels,
onStateChange: setProcessingState, onStateChange: setProcessingState,
knownHandles: autocompleteView.knownHandles, knownHandles: autocompleteView.knownHandles,
langs: store.preferences.postLanguages, langs: store.preferences.postLanguages,
@ -197,23 +199,7 @@ export const ComposePost = observer(function ComposePost({
onPost?.() onPost?.()
onClose() onClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
}, }
[
isProcessing,
setError,
setIsProcessing,
replyTo,
autocompleteView.knownHandles,
extLink,
onClose,
onPost,
quote,
setExtLink,
store,
track,
gallery,
],
)
const canPost = useMemo( const canPost = useMemo(
() => () =>
@ -246,6 +232,7 @@ export const ComposePost = observer(function ComposePost({
<Text style={[pal.link, s.f18]}>Cancel</Text> <Text style={[pal.link, s.f18]}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={s.flex1} /> <View style={s.flex1} />
<LabelsBtn labels={labels} onChange={setLabels} />
{isProcessing ? ( {isProcessing ? (
<View style={styles.postBtn}> <View style={styles.postBtn}>
<ActivityIndicator /> <ActivityIndicator />
@ -407,7 +394,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingTop: isDesktopWeb ? 10 : undefined, paddingTop: isDesktopWeb ? 10 : undefined,
paddingBottom: 10, paddingBottom: isDesktopWeb ? 10 : 4,
paddingHorizontal: 20, paddingHorizontal: 20,
height: 55, height: 55,
}, },

View File

@ -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,
},
})

View File

@ -15,6 +15,7 @@ import * as ProfilePreviewModal from './ProfilePreview'
import * as ServerInputModal from './ServerInput' import * as ServerInputModal from './ServerInput'
import * as ReportPostModal from './report/ReportPost' import * as ReportPostModal from './report/ReportPost'
import * as RepostModal from './Repost' import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel'
import * as CreateOrEditMuteListModal from './CreateOrEditMuteList' import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
import * as ListAddRemoveUserModal from './ListAddRemoveUser' import * as ListAddRemoveUserModal from './ListAddRemoveUser'
import * as AltImageModal from './AltImage' import * as AltImageModal from './AltImage'
@ -104,6 +105,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'repost') { } else if (activeModal?.name === 'repost') {
snapPoints = RepostModal.snapPoints snapPoints = RepostModal.snapPoints
element = <RepostModal.Component {...activeModal} /> element = <RepostModal.Component {...activeModal} />
} else if (activeModal?.name === 'self-label') {
snapPoints = SelfLabelModal.snapPoints
element = <SelfLabelModal.Component {...activeModal} />
} else if (activeModal?.name === 'alt-text-image') { } else if (activeModal?.name === 'alt-text-image') {
snapPoints = AltImageModal.snapPoints snapPoints = AltImageModal.snapPoints
element = <AltImageModal.Component {...activeModal} /> element = <AltImageModal.Component {...activeModal} />

View File

@ -16,6 +16,7 @@ import * as CreateOrEditMuteListModal from './CreateOrEditMuteList'
import * as ListAddRemoveUserModal from './ListAddRemoveUser' import * as ListAddRemoveUserModal from './ListAddRemoveUser'
import * as DeleteAccountModal from './DeleteAccount' import * as DeleteAccountModal from './DeleteAccount'
import * as RepostModal from './Repost' import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel'
import * as CropImageModal from './crop-image/CropImage.web' import * as CropImageModal from './crop-image/CropImage.web'
import * as AltTextImageModal from './AltImage' import * as AltTextImageModal from './AltImage'
import * as EditImageModal from './EditImage' import * as EditImageModal from './EditImage'
@ -89,6 +90,8 @@ function Modal({modal}: {modal: ModalIface}) {
element = <DeleteAccountModal.Component /> element = <DeleteAccountModal.Component />
} else if (modal.name === 'repost') { } else if (modal.name === 'repost') {
element = <RepostModal.Component {...modal} /> element = <RepostModal.Component {...modal} />
} else if (modal.name === 'self-label') {
element = <SelfLabelModal.Component {...modal} />
} else if (modal.name === 'change-handle') { } else if (modal.name === 'change-handle') {
element = <ChangeHandleModal.Component {...modal} /> element = <ChangeHandleModal.Component {...modal} />
} else if (modal.name === 'waitlist') { } else if (modal.name === 'waitlist') {

View File

@ -35,10 +35,7 @@ export function Component({
name = 'Account Blocks You' name = 'Account Blocks You'
description = 'This user has blocked you. You cannot view their content.' description = 'This user has blocked you. You cannot view their content.'
} else if (moderation.cause.type === 'muted') { } else if (moderation.cause.type === 'muted') {
if (moderation.cause.source.type === 'user') { if (moderation.cause.source.type === 'list') {
name = 'Account Muted'
description = 'You have muted this user.'
} else {
const list = moderation.cause.source.list const list = moderation.cause.source.list
name = <>Account Muted by List</> name = <>Account Muted by List</>
description = ( description = (
@ -53,6 +50,9 @@ export function Component({
list which you have muted. list which you have muted.
</> </>
) )
} else {
name = 'Account Muted'
description = 'You have muted this user.'
} }
} else { } else {
name = moderation.cause.labelDef.strings[context].en.name name = moderation.cause.labelDef.strings[context].en.name

View File

@ -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,
},
})

View File

@ -271,6 +271,7 @@ export const FeedItem = observer(function ({
</View> </View>
)} )}
<ContentHider <ContentHider
testID="contentHider-post"
moderation={item.moderation.content} moderation={item.moderation.content}
ignoreMute ignoreMute
style={styles.contentHider} style={styles.contentHider}
@ -291,6 +292,7 @@ export const FeedItem = observer(function ({
) : undefined} ) : undefined}
{item.post.embed ? ( {item.post.embed ? (
<ContentHider <ContentHider
testID="contentHider-embed"
moderation={item.moderation.embed} moderation={item.moderation.embed}
style={styles.embed}> style={styles.embed}>
<PostEmbeds <PostEmbeds

View File

@ -5,6 +5,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb} from 'platform/detection' import {isDesktopWeb} from 'platform/detection'
interface SelectableBtnProps { interface SelectableBtnProps {
testID?: string
selected: boolean selected: boolean
label: string label: string
left?: boolean left?: boolean
@ -15,6 +16,7 @@ interface SelectableBtnProps {
} }
export function SelectableBtn({ export function SelectableBtn({
testID,
selected, selected,
label, label,
left, left,
@ -25,12 +27,15 @@ export function SelectableBtn({
}: SelectableBtnProps) { }: SelectableBtnProps) {
const pal = usePalette('default') const pal = usePalette('default')
const palPrimary = usePalette('inverted') const palPrimary = usePalette('inverted')
const needsWidthStyles = !style || !('width' in style || 'flex' in style)
return ( return (
<Pressable <Pressable
testID={testID}
style={[ style={[
styles.selectableBtn, styles.btn,
left && styles.selectableBtnLeft, needsWidthStyles && styles.btnWidth,
right && styles.selectableBtnRight, left && styles.btnLeft,
right && styles.btnRight,
pal.border, pal.border,
selected ? palPrimary.view : pal.view, selected ? palPrimary.view : pal.view,
style, style,
@ -45,9 +50,7 @@ export function SelectableBtn({
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
selectableBtn: { btn: {
flex: isDesktopWeb ? undefined : 1,
width: isDesktopWeb ? 100 : undefined,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
borderWidth: 1, borderWidth: 1,
@ -55,12 +58,16 @@ const styles = StyleSheet.create({
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 10, paddingVertical: 10,
}, },
selectableBtnLeft: { btnWidth: {
flex: isDesktopWeb ? undefined : 1,
width: isDesktopWeb ? 100 : undefined,
},
btnLeft: {
borderTopLeftRadius: 8, borderTopLeftRadius: 8,
borderBottomLeftRadius: 8, borderBottomLeftRadius: 8,
borderLeftWidth: 1, borderLeftWidth: 1,
}, },
selectableBtnRight: { btnRight: {
borderTopRightRadius: 8, borderTopRightRadius: 8,
borderBottomRightRadius: 8, borderBottomRightRadius: 8,
}, },

View File

@ -3,7 +3,7 @@ import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {ModerationUI} from '@atproto/api' import {ModerationUI} from '@atproto/api'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {InfoCircleIcon} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import {describeModerationCause} from 'lib/moderation' import {describeModerationCause} from 'lib/moderation'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {isDesktopWeb} from 'platform/detection' import {isDesktopWeb} from 'platform/detection'
@ -58,7 +58,7 @@ export function ContentHider({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="Learn more about this warning" accessibilityLabel="Learn more about this warning"
accessibilityHint=""> accessibilityHint="">
<InfoCircleIcon size={18} style={pal.text} /> <ShieldExclamation size={18} style={pal.text} />
</Pressable> </Pressable>
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
{desc.name} {desc.name}

View File

@ -3,7 +3,7 @@ import {Pressable, StyleProp, StyleSheet, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api' import {ModerationUI} from '@atproto/api'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {InfoCircleIcon} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import {describeModerationCause} from 'lib/moderation' import {describeModerationCause} from 'lib/moderation'
import {useStores} from 'state/index' import {useStores} from 'state/index'
@ -41,7 +41,7 @@ export function PostAlerts({
accessibilityLabel="Learn more about this warning" accessibilityLabel="Learn more about this warning"
accessibilityHint="" accessibilityHint=""
style={[styles.container, pal.viewLight, style]}> style={[styles.container, pal.viewLight, style]}>
<InfoCircleIcon style={pal.text} size={18} /> <ShieldExclamation style={pal.text} size={16} />
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
{desc.name} {desc.name}
</Text> </Text>

View File

@ -6,7 +6,7 @@ import {Link} from '../Link'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {addStyle} from 'lib/styles' import {addStyle} from 'lib/styles'
import {describeModerationCause} from 'lib/moderation' import {describeModerationCause} from 'lib/moderation'
import {InfoCircleIcon} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {isDesktopWeb} from 'platform/detection' import {isDesktopWeb} from 'platform/detection'
@ -67,7 +67,7 @@ export function PostHider({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="Learn more about this warning" accessibilityLabel="Learn more about this warning"
accessibilityHint=""> accessibilityHint="">
<InfoCircleIcon size={18} style={pal.text} /> <ShieldExclamation size={18} style={pal.text} />
</Pressable> </Pressable>
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
{desc.name} {desc.name}

View File

@ -3,7 +3,7 @@ import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ProfileModeration} from '@atproto/api' import {ProfileModeration} from '@atproto/api'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {InfoCircleIcon} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import { import {
describeModerationCause, describeModerationCause,
getProfileModerationCauses, getProfileModerationCauses,
@ -44,7 +44,7 @@ export function ProfileHeaderAlerts({
accessibilityLabel="Learn more about this warning" accessibilityLabel="Learn more about this warning"
accessibilityHint="" accessibilityHint=""
style={[styles.container, pal.viewLight, style]}> style={[styles.container, pal.viewLight, style]}>
<InfoCircleIcon style={pal.text} size={24} /> <ShieldExclamation style={pal.text} size={24} />
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
{desc.name} {desc.name}
</Text> </Text>

View File

@ -72,6 +72,8 @@ import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSq
import {faShield} from '@fortawesome/free-solid-svg-icons/faShield' import {faShield} from '@fortawesome/free-solid-svg-icons/faShield'
import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal' import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal'
import {faSliders} from '@fortawesome/free-solid-svg-icons/faSliders' 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 {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus'
import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
@ -162,6 +164,8 @@ export function setup() {
faShield, faShield,
faSignal, faSignal,
faSliders, faSliders,
faSquare,
faSquareCheck,
faSquarePlus, faSquarePlus,
faUser, faUser,
faUsers, faUsers,

View File

@ -40,10 +40,10 @@
tlds "^1.234.0" tlds "^1.234.0"
typed-emitter "^2.1.0" typed-emitter "^2.1.0"
"@atproto/api@^0.5.4": "@atproto/api@^0.6.0":
version "0.5.4" version "0.6.0"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.5.4.tgz#81a0dc36d3fcae085092434218740b68ee28f816" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.0.tgz#c4eea08ee4d1be522928cd016d7de8061d86e573"
integrity sha512-e2M5d+w6PMEzunVWbeX4yD9pMXaP6FakQYOfj4gUz9tL05k94Qv1dcX8IFUHdWEunHijLGU2fU2SNC4jgiuLBA== integrity sha512-GkWHoGZfNneHarAYkIPJD1GGgKiI7OwnCtKS+J4AmlVKYijGEzOYgg1fY6rluT6XPT5TlQZiHUWpMlpqAkQIkQ==
dependencies: dependencies:
"@atproto/common-web" "*" "@atproto/common-web" "*"
"@atproto/uri" "*" "@atproto/uri" "*"
@ -157,10 +157,10 @@
one-webcrypto "^1.0.3" one-webcrypto "^1.0.3"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/dev-env@^0.2.2": "@atproto/dev-env@^0.2.3":
version "0.2.2" version "0.2.3"
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.2.2.tgz#930cb63dc751b08fa0a1c69820e27587d7b38252" resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.2.3.tgz#17f7574a85f560dbd128dc3dd7620de14ff63483"
integrity sha512-94KruFi2C52YHiSmvJKjPp3414KV9ev06Sla4BebJxm1z3/DKGhmPGDEHVke4+KPZOTR3vQ2x1WKuX87mtCp7w== integrity sha512-7Glr0NVWftXF8kvmVouNWhXX9DzTWpKhytQZkfPaIWf6jVRATgRSfts1QOet1lTx33DczqKBJb9xc0qDt4MqWw==
dependencies: dependencies:
"@atproto/api" "*" "@atproto/api" "*"
"@atproto/bsky" "*" "@atproto/bsky" "*"