about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/modals/AppealLabel.tsx139
-rw-r--r--src/view/com/modals/Modal.tsx4
-rw-r--r--src/view/com/modals/Modal.web.tsx3
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx10
-rw-r--r--src/view/com/profile/ProfileHeader.tsx4
-rw-r--r--src/view/com/util/moderation/LabelInfo.tsx60
6 files changed, 220 insertions, 0 deletions
diff --git a/src/view/com/modals/AppealLabel.tsx b/src/view/com/modals/AppealLabel.tsx
new file mode 100644
index 000000000..2db070bc6
--- /dev/null
+++ b/src/view/com/modals/AppealLabel.tsx
@@ -0,0 +1,139 @@
+import React, {useState} from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {ComAtprotoModerationDefs} from '@atproto/api'
+import {ScrollView, TextInput} from './util'
+import {Text} from '../util/text/Text'
+import {s, colors} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {CharProgress} from '../composer/char-progress/CharProgress'
+import {getAgent} from '#/state/session'
+import * as Toast from '../util/Toast'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+
+export const snapPoints = ['40%']
+
+type ReportComponentProps =
+  | {
+      uri: string
+      cid: string
+    }
+  | {
+      did: string
+    }
+
+export function Component(props: ReportComponentProps) {
+  const pal = usePalette('default')
+  const [details, setDetails] = useState<string>('')
+  const {_} = useLingui()
+  const {closeModal} = useModalControls()
+  const {isMobile} = useWebMediaQueries()
+  const isAccountReport = 'did' in props
+
+  const submit = async () => {
+    try {
+      const $type = !isAccountReport
+        ? 'com.atproto.repo.strongRef'
+        : 'com.atproto.admin.defs#repoRef'
+      await getAgent().createModerationReport({
+        reasonType: ComAtprotoModerationDefs.REASONOTHER,
+        subject: {
+          $type,
+          ...props,
+        },
+        reason: details,
+      })
+      Toast.show("We'll look into your appeal promptly.")
+    } finally {
+      closeModal()
+    }
+  }
+
+  return (
+    <View
+      style={[
+        pal.view,
+        s.flex1,
+        isMobile ? {paddingHorizontal: 12} : undefined,
+      ]}
+      testID="appealLabelModal">
+      <Text
+        type="2xl-bold"
+        style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
+        <Trans>Appeal Decision</Trans>
+      </Text>
+      <ScrollView>
+        <View style={[pal.btn, styles.detailsInputContainer]}>
+          <TextInput
+            accessibilityLabel={_(msg`Text input field`)}
+            accessibilityHint={_(
+              msg`Please tell us why you think this decision was incorrect.`,
+            )}
+            placeholder={_(
+              msg`Please tell us why you think this decision was incorrect.`,
+            )}
+            placeholderTextColor={pal.textLight.color}
+            value={details}
+            onChangeText={setDetails}
+            autoFocus={true}
+            numberOfLines={3}
+            multiline={true}
+            textAlignVertical="top"
+            maxLength={300}
+            style={[styles.detailsInput, pal.text]}
+          />
+          <View style={styles.detailsInputBottomBar}>
+            <View style={styles.charCounter}>
+              <CharProgress count={details?.length || 0} />
+            </View>
+          </View>
+        </View>
+        <TouchableOpacity
+          testID="confirmBtn"
+          onPress={submit}
+          style={styles.btn}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Confirm`)}
+          accessibilityHint="">
+          <Text style={[s.white, s.bold, s.f18]}>
+            <Trans>Submit</Trans>
+          </Text>
+        </TouchableOpacity>
+      </ScrollView>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  detailsInputContainer: {
+    borderRadius: 8,
+    marginBottom: 8,
+  },
+  detailsInput: {
+    paddingHorizontal: 12,
+    paddingTop: 12,
+    paddingBottom: 12,
+    borderRadius: 8,
+    minHeight: 100,
+    fontSize: 16,
+  },
+  detailsInputBottomBar: {
+    alignSelf: 'flex-end',
+  },
+  charCounter: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingRight: 10,
+    paddingBottom: 8,
+  },
+  btn: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    borderRadius: 32,
+    padding: 14,
+    backgroundColor: colors.blue3,
+  },
+})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index a3e6fb9e5..0384e301c 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -22,6 +22,7 @@ import * as ListAddUserModal from './ListAddRemoveUsers'
 import * as AltImageModal from './AltImage'
 import * as EditImageModal from './AltImage'
 import * as ReportModal from './report/Modal'
+import * as AppealLabelModal from './AppealLabel'
 import * as DeleteAccountModal from './DeleteAccount'
 import * as ChangeHandleModal from './ChangeHandle'
 import * as WaitlistModal from './Waitlist'
@@ -105,6 +106,9 @@ export function ModalsContainer() {
   } else if (activeModal?.name === 'report') {
     snapPoints = ReportModal.snapPoints
     element = <ReportModal.Component {...activeModal} />
+  } else if (activeModal?.name === 'appeal-label') {
+    snapPoints = AppealLabelModal.snapPoints
+    element = <AppealLabelModal.Component {...activeModal} />
   } else if (activeModal?.name === 'create-or-edit-list') {
     snapPoints = CreateOrEditListModal.snapPoints
     element = <CreateOrEditListModal.Component {...activeModal} />
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index c39ba1f51..ce1e67fae 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -11,6 +11,7 @@ import * as EditProfileModal from './EditProfile'
 import * as ProfilePreviewModal from './ProfilePreview'
 import * as ServerInputModal from './ServerInput'
 import * as ReportModal from './report/Modal'
+import * as AppealLabelModal from './AppealLabel'
 import * as CreateOrEditListModal from './CreateOrEditList'
 import * as UserAddRemoveLists from './UserAddRemoveLists'
 import * as ListAddUserModal from './ListAddRemoveUsers'
@@ -81,6 +82,8 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <ServerInputModal.Component {...modal} />
   } else if (modal.name === 'report') {
     element = <ReportModal.Component {...modal} />
+  } else if (modal.name === 'appeal-label') {
+    element = <AppealLabelModal.Component {...modal} />
   } else if (modal.name === 'create-or-edit-list') {
     element = <CreateOrEditListModal.Component {...modal} />
   } else if (modal.name === 'user-add-remove-lists') {
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 900839015..a2aa3716e 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -42,6 +42,8 @@ import {useComposerControls} from '#/state/shell/composer'
 import {useModerationOpts} from '#/state/queries/preferences'
 import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
 import {ThreadPost} from '#/state/queries/post-thread'
+import {LabelInfo} from '../util/moderation/LabelInfo'
+import {useSession} from '#/state/session'
 
 export function PostThreadItem({
   post,
@@ -158,6 +160,7 @@ let PostThreadItemLoaded = ({
   const pal = usePalette('default')
   const langPrefs = useLanguagePrefs()
   const {openComposer} = useComposerControls()
+  const {currentAccount} = useSession()
   const [limitLines, setLimitLines] = React.useState(
     () => countLines(richText?.text) >= MAX_POST_LINES,
   )
@@ -345,6 +348,13 @@ let PostThreadItemLoaded = ({
                 includeMute
                 style={styles.alert}
               />
+              {post.author.did === currentAccount?.did ? (
+                <LabelInfo
+                  details={{uri: post.uri, cid: post.cid}}
+                  labels={post.labels}
+                  style={{marginBottom: 8}}
+                />
+              ) : null}
               {richText?.text ? (
                 <View
                   style={[
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index e006cac7d..6975e3964 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -54,6 +54,7 @@ import {logger} from '#/logger'
 import {useSession} from '#/state/session'
 import {Shadow} from '#/state/cache/types'
 import {useRequireAuth} from '#/state/session'
+import {LabelInfo} from '../util/moderation/LabelInfo'
 
 interface Props {
   profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> | null
@@ -619,6 +620,9 @@ let ProfileHeaderLoaded = ({
           </>
         )}
         <ProfileHeaderAlerts moderation={moderation} />
+        {isMe && (
+          <LabelInfo details={{did: profile.did}} labels={profile.labels} />
+        )}
       </View>
 
       {!isProfilePreview && showSuggestedFollows && (
diff --git a/src/view/com/util/moderation/LabelInfo.tsx b/src/view/com/util/moderation/LabelInfo.tsx
new file mode 100644
index 000000000..8fe3765c2
--- /dev/null
+++ b/src/view/com/util/moderation/LabelInfo.tsx
@@ -0,0 +1,60 @@
+import React from 'react'
+import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
+import {ComAtprotoLabelDefs} from '@atproto/api'
+import {Text} from '../text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+
+export function LabelInfo({
+  details,
+  labels,
+  style,
+}: {
+  details: {did: string} | {uri: string; cid: string}
+  labels: ComAtprotoLabelDefs.Label[] | undefined
+  style?: StyleProp<ViewStyle>
+}) {
+  const pal = usePalette('default')
+  const {_} = useLingui()
+  const {openModal} = useModalControls()
+
+  if (!labels) {
+    return null
+  }
+  labels = labels.filter(l => !l.val.startsWith('!'))
+  if (!labels.length) {
+    return null
+  }
+
+  return (
+    <View
+      style={[
+        pal.viewLight,
+        {
+          flexDirection: 'row',
+          flexWrap: 'wrap',
+          paddingHorizontal: 12,
+          paddingVertical: 10,
+          borderRadius: 8,
+        },
+        style,
+      ]}>
+      <Text type="sm" style={pal.text}>
+        <Trans>
+          This {'did' in details ? 'account' : 'post'} has been labeled.
+        </Trans>{' '}
+      </Text>
+      <Pressable
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`Appeal this decision`)}
+        accessibilityHint=""
+        onPress={() => openModal({name: 'appeal-label', ...details})}>
+        <Text type="sm" style={pal.link}>
+          <Trans>Appeal this decision.</Trans>
+        </Text>
+      </Pressable>
+    </View>
+  )
+}