diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-08-09 17:34:16 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-09 17:34:16 -0700 |
commit | 03d152675ee1ce208856498acf7285fbf07fd45b (patch) | |
tree | 70803ebe16276b3a6b7c350f78d069641a0a6118 | |
parent | 48813a96d686d97009e260d0a87f32d28a631052 (diff) | |
download | voidsky-03d152675ee1ce208856498acf7285fbf07fd45b.tar.zst |
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
-rw-r--r-- | __e2e__/tests/self-labeling.test.ts | 34 | ||||
-rw-r--r-- | package.json | 4 | ||||
-rw-r--r-- | src/lib/api/index.ts | 12 | ||||
-rw-r--r-- | src/lib/icons.tsx | 38 | ||||
-rw-r--r-- | src/lib/moderation.ts | 12 | ||||
-rw-r--r-- | src/state/models/ui/preferences.ts | 47 | ||||
-rw-r--r-- | src/state/models/ui/shell.ts | 7 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 117 | ||||
-rw-r--r-- | src/view/com/composer/labels/LabelsBtn.tsx | 53 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 3 | ||||
-rw-r--r-- | src/view/com/modals/ModerationDetails.tsx | 8 | ||||
-rw-r--r-- | src/view/com/modals/SelfLabel.tsx | 167 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/forms/SelectableBtn.tsx | 23 | ||||
-rw-r--r-- | src/view/com/util/moderation/ContentHider.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/moderation/PostAlerts.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/moderation/PostHider.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/moderation/ProfileHeaderAlerts.tsx | 4 | ||||
-rw-r--r-- | src/view/index.ts | 4 | ||||
-rw-r--r-- | yarn.lock | 16 |
21 files changed, 443 insertions, 124 deletions
diff --git a/__e2e__/tests/self-labeling.test.ts b/__e2e__/tests/self-labeling.test.ts new file mode 100644 index 000000000..70164cb85 --- /dev/null +++ b/__e2e__/tests/self-labeling.test.ts @@ -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() + }) +}) diff --git a/package.json b/package.json index bcf32ff52..64e7a4e66 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 381d78435..bb4ff8fcb 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -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()}`) diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx index 4ea3b4d65..233f8a473 100644 --- a/src/lib/icons.tsx +++ b/src/lib/icons.tsx @@ -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> + ) +} diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts index 1ed830fd8..70c4444fe 100644 --- a/src/lib/moderation.ts +++ b/src/lib/moderation.ts @@ -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 diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index a892d8d34..23668a3dc 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -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: {}, }, ], } diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 476277592..348fa4899 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -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 diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 9fdf0bc6f..7d3e27571 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -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') - } - if (!replyTo) { - store.me.mainFeed.onPostCreated() + 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) } - 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, }, diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx new file mode 100644 index 000000000..f41dd2600 --- /dev/null +++ b/src/view/com/composer/labels/LabelsBtn.tsx @@ -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, + }, +}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 469492971..ce5dc40e0 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -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} /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index df13dfed2..0f6247822 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -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') { diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx index abeb2fdf4..598d26924 100644 --- a/src/view/com/modals/ModerationDetails.tsx +++ b/src/view/com/modals/ModerationDetails.tsx @@ -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 diff --git a/src/view/com/modals/SelfLabel.tsx b/src/view/com/modals/SelfLabel.tsx new file mode 100644 index 000000000..cb78f3f15 --- /dev/null +++ b/src/view/com/modals/SelfLabel.tsx @@ -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, + }, +}) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index bed3d3a61..60dff9153 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -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 diff --git a/src/view/com/util/forms/SelectableBtn.tsx b/src/view/com/util/forms/SelectableBtn.tsx index 503c49b2f..4b494264e 100644 --- a/src/view/com/util/forms/SelectableBtn.tsx +++ b/src/view/com/util/forms/SelectableBtn.tsx @@ -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, }, diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx index 6be2f8be0..9286d1c8b 100644 --- a/src/view/com/util/moderation/ContentHider.tsx +++ b/src/view/com/util/moderation/ContentHider.tsx @@ -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} diff --git a/src/view/com/util/moderation/PostAlerts.tsx b/src/view/com/util/moderation/PostAlerts.tsx index 45937c2d8..8a6cbbb85 100644 --- a/src/view/com/util/moderation/PostAlerts.tsx +++ b/src/view/com/util/moderation/PostAlerts.tsx @@ -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> diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx index dc74d3e39..2a52561d4 100644 --- a/src/view/com/util/moderation/PostHider.tsx +++ b/src/view/com/util/moderation/PostHider.tsx @@ -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} diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx index 3cc3b5b9e..b7781e06d 100644 --- a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx +++ b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx @@ -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> diff --git a/src/view/index.ts b/src/view/index.ts index 4226e07e7..22cc6e837 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -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, diff --git a/yarn.lock b/yarn.lock index d4bfa8a5f..6e13bd51e 100644 --- a/yarn.lock +++ b/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" "*" |