Bluesky app fork with some witchin' additions ๐Ÿ’ซ
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[๐Ÿด] Report message dialog (#3941)

* message report dialog

* report chat prompt

* typo

* 100% height sheet on android

* messages-specific report options

* restore unwanted sexual content

* chat -> conversation

authored by

Samuel Newman and committed by
GitHub
ab21aafc 7370bebf

+309 -15
+7 -1
src/components/ReportDialog/SelectReportOptionView.tsx
··· 25 25 import {Text} from '#/components/Typography' 26 26 import {ReportDialogProps} from './types' 27 27 28 + type ParamsWithMessages = ReportDialogProps['params'] | {type: 'message'} 29 + 28 30 export function SelectReportOptionView({ 29 31 ...props 30 - }: ReportDialogProps & { 32 + }: { 33 + params: ParamsWithMessages 31 34 labelers: AppBskyLabelerDefs.LabelerViewDetailed[] 32 35 onSelectReportOption: (reportOption: ReportOption) => void 33 36 goBack: () => void ··· 54 57 } else if (props.params.type === 'feedgen') { 55 58 title = _(msg`Report this feed`) 56 59 description = _(msg`Why should this feed be reviewed?`) 60 + } else if (props.params.type === 'message') { 61 + title = _(msg`Report this message`) 62 + description = _(msg`Why should this message be reviewed?`) 57 63 } 58 64 59 65 return {
+17 -5
src/components/dms/ConvoMenu.tsx
··· 50 50 const {_} = useLingui() 51 51 const t = useTheme() 52 52 const leaveConvoControl = Prompt.usePromptControl() 53 + const reportControl = Prompt.usePromptControl() 53 54 const {mutate: markAsRead} = useMarkAsReadMutation() 54 55 55 56 const {data: convo} = useConvoQuery(initialConvo) ··· 147 148 </Menu.Item> 148 149 </Menu.Group> 149 150 <Menu.Divider /> 150 - {/* TODO(samuel): implement these */} 151 + {/* TODO(samuel): implement this */} 151 152 <Menu.Group> 152 153 <Menu.Item 153 154 label={_(msg`Block account`)} ··· 161 162 /> 162 163 </Menu.Item> 163 164 <Menu.Item 164 - label={_(msg`Report account`)} 165 - onPress={() => {}} 166 - disabled> 165 + label={_(msg`Report conversation`)} 166 + onPress={reportControl.open}> 167 167 <Menu.ItemText> 168 - <Trans>Report account</Trans> 168 + <Trans>Report conversation</Trans> 169 169 </Menu.ItemText> 170 170 <Menu.ItemIcon icon={Flag} /> 171 171 </Menu.Item> ··· 194 194 confirmButtonColor="negative" 195 195 onConfirm={() => leaveConvo()} 196 196 /> 197 + 198 + <Prompt.Basic 199 + control={reportControl} 200 + title={_(msg`Report conversation`)} 201 + description={_( 202 + msg`To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue.`, 203 + )} 204 + confirmButtonCta={_(msg`I understand`)} 205 + onConfirm={noop} 206 + /> 197 207 </> 198 208 ) 199 209 } 200 210 ConvoMenu = React.memo(ConvoMenu) 201 211 202 212 export {ConvoMenu} 213 + 214 + function noop() {}
+1
src/components/dms/MessageItem.tsx
··· 193 193 } 194 194 195 195 MessageItemMetadata = React.memo(MessageItemMetadata) 196 + export {MessageItemMetadata} 196 197 197 198 function localDateString(date: Date) { 198 199 // can't use toISOString because it should be in local time
+16 -9
src/components/dms/MessageMenu.tsx
··· 1 1 import React from 'react' 2 2 import {LayoutAnimation, Pressable, View} from 'react-native' 3 3 import * as Clipboard from 'expo-clipboard' 4 + import {RichText} from '@atproto/api' 4 5 import {ChatBskyConvoDefs} from '@atproto-labs/api' 5 6 import {msg} from '@lingui/macro' 6 7 import {useLingui} from '@lingui/react' 7 8 9 + import {richTextToString} from '#/lib/strings/rich-text-helpers' 8 10 import {isWeb} from 'platform/detection' 9 11 import {useConvo} from 'state/messages/convo' 10 12 import {ConvoStatus} from 'state/messages/convo/types' ··· 18 20 import * as Prompt from '#/components/Prompt' 19 21 import {usePromptControl} from '#/components/Prompt' 20 22 import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '../icons/Clipboard' 23 + import {MessageReportDialog} from './MessageReportDialog' 21 24 22 25 export let MessageMenu = ({ 23 26 message, ··· 35 38 const convo = useConvo() 36 39 const deleteControl = usePromptControl() 37 40 const retryDeleteControl = usePromptControl() 41 + const reportControl = usePromptControl() 38 42 39 43 const isFromSelf = message.sender?.did === currentAccount?.did 40 44 41 45 const onCopyPostText = React.useCallback(() => { 42 - // use when we have rich text 43 - // const str = richTextToString(richText, true) 46 + const str = richTextToString( 47 + new RichText({ 48 + text: message.text, 49 + facets: message.facets, 50 + }), 51 + true, 52 + ) 44 53 45 - Clipboard.setStringAsync(message.text) 54 + Clipboard.setStringAsync(str) 46 55 Toast.show(_(msg`Copied to clipboard`)) 47 - }, [_, message.text]) 56 + }, [_, message.text, message.facets]) 48 57 49 58 const onDelete = React.useCallback(() => { 50 59 if (convo.status !== ConvoStatus.Ready) return ··· 55 64 .then(() => Toast.show(_(msg`Message deleted`))) 56 65 .catch(() => retryDeleteControl.open()) 57 66 }, [_, convo, message.id, retryDeleteControl]) 58 - 59 - const onReport = React.useCallback(() => { 60 - // TODO report the message 61 - }, []) 62 67 63 68 return ( 64 69 <> ··· 104 109 <Menu.Item 105 110 testID="messageDropdownReportBtn" 106 111 label={_(msg`Report message`)} 107 - onPress={onReport}> 112 + onPress={reportControl.open}> 108 113 <Menu.ItemText>{_(msg`Report`)}</Menu.ItemText> 109 114 <Menu.ItemIcon icon={Warning} position="right" /> 110 115 </Menu.Item> ··· 112 117 </Menu.Group> 113 118 </Menu.Outer> 114 119 </Menu.Root> 120 + 121 + <MessageReportDialog message={message} control={reportControl} /> 115 122 116 123 <Prompt.Basic 117 124 control={deleteControl}
+254
src/components/dms/MessageReportDialog.tsx
··· 1 + import React, {memo, useMemo, useState} from 'react' 2 + import {View} from 'react-native' 3 + import {RichText as RichTextAPI} from '@atproto/api' 4 + import { 5 + ChatBskyConvoDefs, 6 + ComAtprotoModerationCreateReport, 7 + } from '@atproto-labs/api' 8 + import {msg, Trans} from '@lingui/macro' 9 + import {useLingui} from '@lingui/react' 10 + import {useMutation} from '@tanstack/react-query' 11 + 12 + import {ReportOption} from '#/lib/moderation/useReportOptions' 13 + import {isAndroid} from '#/platform/detection' 14 + import {useAgent} from '#/state/session' 15 + import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 16 + import * as Toast from '#/view/com/util/Toast' 17 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 18 + import * as Dialog from '#/components/Dialog' 19 + import {Button, ButtonIcon, ButtonText} from '../Button' 20 + import {Divider} from '../Divider' 21 + import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron' 22 + import {Loader} from '../Loader' 23 + import {SelectReportOptionView} from '../ReportDialog/SelectReportOptionView' 24 + import {RichText} from '../RichText' 25 + import {Text} from '../Typography' 26 + import {MessageItemMetadata} from './MessageItem' 27 + 28 + let MessageReportDialog = ({ 29 + control, 30 + message, 31 + }: { 32 + control: Dialog.DialogControlProps 33 + message: ChatBskyConvoDefs.MessageView 34 + }): React.ReactNode => { 35 + const {_} = useLingui() 36 + return ( 37 + <Dialog.Outer 38 + control={control} 39 + nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}> 40 + <Dialog.Handle /> 41 + <Dialog.ScrollableInner label={_(msg`Report this message`)}> 42 + <DialogInner message={message} /> 43 + <Dialog.Close /> 44 + </Dialog.ScrollableInner> 45 + </Dialog.Outer> 46 + ) 47 + } 48 + MessageReportDialog = memo(MessageReportDialog) 49 + export {MessageReportDialog} 50 + 51 + function DialogInner({message}: {message: ChatBskyConvoDefs.MessageView}) { 52 + const [reportOption, setReportOption] = useState<ReportOption | null>(null) 53 + 54 + return reportOption ? ( 55 + <SubmitStep 56 + message={message} 57 + reportOption={reportOption} 58 + goBack={() => setReportOption(null)} 59 + /> 60 + ) : ( 61 + <ReasonStep setReportOption={setReportOption} /> 62 + ) 63 + } 64 + 65 + function ReasonStep({ 66 + setReportOption, 67 + }: { 68 + setReportOption: (reportOption: ReportOption) => void 69 + }) { 70 + const control = Dialog.useDialogContext() 71 + 72 + return ( 73 + <SelectReportOptionView 74 + labelers={[]} 75 + goBack={control.close} 76 + params={{type: 'message'}} 77 + onSelectReportOption={setReportOption} 78 + /> 79 + ) 80 + } 81 + 82 + function SubmitStep({ 83 + message, 84 + reportOption, 85 + goBack, 86 + }: { 87 + message: ChatBskyConvoDefs.MessageView 88 + reportOption: ReportOption 89 + goBack: () => void 90 + }) { 91 + const {_} = useLingui() 92 + const {gtMobile} = useBreakpoints() 93 + const t = useTheme() 94 + const [details, setDetails] = useState('') 95 + const control = Dialog.useDialogContext() 96 + const {getAgent} = useAgent() 97 + 98 + const { 99 + mutate: submit, 100 + error, 101 + isPending: submitting, 102 + } = useMutation({ 103 + mutationFn: async () => { 104 + const report = { 105 + reasonType: reportOption.reason, 106 + subject: { 107 + $type: 'chat.bsky.convo.defs#messageRef', 108 + messageId: message.id, 109 + did: message.sender!.did, 110 + } satisfies ChatBskyConvoDefs.MessageRef, 111 + reason: details, 112 + } satisfies ComAtprotoModerationCreateReport.InputSchema 113 + 114 + await getAgent().createModerationReport(report) 115 + }, 116 + onSuccess: () => { 117 + control.close(() => { 118 + Toast.show(_(msg`Thank you. Your report has been sent.`)) 119 + }) 120 + }, 121 + }) 122 + 123 + return ( 124 + <View style={a.gap_lg}> 125 + <Button 126 + size="small" 127 + variant="solid" 128 + color="secondary" 129 + shape="round" 130 + label={_(msg`Go back to previous step`)} 131 + onPress={goBack}> 132 + <ButtonIcon icon={Chevron} /> 133 + </Button> 134 + 135 + <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}> 136 + <Text style={[a.text_2xl, a.font_bold]}> 137 + <Trans>Report this message</Trans> 138 + </Text> 139 + <Text style={[a.text_md, t.atoms.text_contrast_medium]}> 140 + <Trans> 141 + Your report will be sent to the Bluesky Moderation Service 142 + </Trans> 143 + </Text> 144 + </View> 145 + 146 + <PreviewMessage message={message} /> 147 + 148 + <Text style={[a.text_md, t.atoms.text_contrast_medium]}> 149 + <Trans>Reason: {reportOption.title}</Trans> 150 + </Text> 151 + 152 + <Divider /> 153 + 154 + <View style={[a.gap_md]}> 155 + <Text style={[t.atoms.text_contrast_medium]}> 156 + <Trans>Optionally provide additional information below:</Trans> 157 + </Text> 158 + 159 + <View style={[a.relative, a.w_full]}> 160 + <Dialog.Input 161 + multiline 162 + value={details} 163 + onChangeText={setDetails} 164 + label="Text field" 165 + style={{paddingRight: 60}} 166 + numberOfLines={6} 167 + /> 168 + 169 + <View 170 + style={[ 171 + a.absolute, 172 + a.flex_row, 173 + a.align_center, 174 + a.pr_md, 175 + a.pb_sm, 176 + { 177 + bottom: 0, 178 + right: 0, 179 + }, 180 + ]}> 181 + <CharProgress count={details?.length || 0} /> 182 + </View> 183 + </View> 184 + </View> 185 + 186 + <View style={[a.flex_row, a.align_center, a.justify_end, a.gap_lg]}> 187 + {error && ( 188 + <Text 189 + style={[ 190 + a.flex_1, 191 + a.italic, 192 + a.leading_snug, 193 + t.atoms.text_contrast_medium, 194 + ]}> 195 + <Trans> 196 + There was an issue sending your report. Please check your internet 197 + connection. 198 + </Trans> 199 + </Text> 200 + )} 201 + 202 + <Button 203 + testID="sendReportBtn" 204 + size="large" 205 + variant="solid" 206 + color="negative" 207 + label={_(msg`Send report`)} 208 + onPress={() => submit()}> 209 + <ButtonText> 210 + <Trans>Send report</Trans> 211 + </ButtonText> 212 + {submitting && <ButtonIcon icon={Loader} />} 213 + </Button> 214 + </View> 215 + </View> 216 + ) 217 + } 218 + 219 + function PreviewMessage({message}: {message: ChatBskyConvoDefs.MessageView}) { 220 + const t = useTheme() 221 + const rt = useMemo(() => { 222 + return new RichTextAPI({text: message.text, facets: message.facets}) 223 + }, [message.text, message.facets]) 224 + 225 + return ( 226 + <View style={a.align_start}> 227 + <View 228 + style={[ 229 + a.py_sm, 230 + a.my_2xs, 231 + a.rounded_md, 232 + { 233 + paddingLeft: 14, 234 + paddingRight: 14, 235 + backgroundColor: t.palette.contrast_50, 236 + borderRadius: 17, 237 + }, 238 + {borderBottomLeftRadius: 2}, 239 + ]}> 240 + <RichText 241 + value={rt} 242 + style={[a.text_md, a.leading_snug]} 243 + interactiveStyle={a.underline} 244 + enableTags 245 + /> 246 + </View> 247 + <MessageItemMetadata 248 + message={message} 249 + isLastInGroup 250 + style={[a.text_left, a.mb_0]} 251 + /> 252 + </View> 253 + ) 254 + }
+14
src/lib/moderation/useReportOptions.ts
··· 15 15 list: ReportOption[] 16 16 feedgen: ReportOption[] 17 17 other: ReportOption[] 18 + message: ReportOption[] 18 19 } 19 20 20 21 export function useReportOptions(): ReportOptions { ··· 69 70 reason: ComAtprotoModerationDefs.REASONSEXUAL, 70 71 title: _(msg`Unwanted Sexual Content`), 71 72 description: _(msg`Nudity or adult content not labeled as such`), 73 + }, 74 + ...common, 75 + ], 76 + message: [ 77 + { 78 + reason: ComAtprotoModerationDefs.REASONSPAM, 79 + title: _(msg`Spam`), 80 + description: _(msg`Excessive or unwanted messages`), 81 + }, 82 + { 83 + reason: ComAtprotoModerationDefs.REASONSEXUAL, 84 + title: _(msg`Unwanted Sexual Content`), 85 + description: _(msg`Unwanted sexual content`), 72 86 }, 73 87 ...common, 74 88 ],