From 531ff6c43300e84edc8ca3ed15be834c82f4062a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 5 Nov 2025 12:07:01 +0200 Subject: [PATCH 1/8] restore flatten to a place that is needed (#9334) --- src/components/Dialog/index.web.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 781adedee..cf250faf7 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -16,7 +16,7 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar' import {logger} from '#/logger' import {useA11y} from '#/state/a11y' import {useDialogStateControlContext} from '#/state/dialogs' -import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {Context} from '#/components/Dialog/context' import { @@ -180,7 +180,8 @@ export function Inner({ onClick={stopPropagation} onStartShouldSetResponder={_ => true} onTouchEnd={stopPropagation} - style={[ + // note: flatten is required for some reason -sfn + style={flatten([ a.relative, a.rounded_md, a.w_full, @@ -195,7 +196,7 @@ export function Inner({ }, !reduceMotionEnabled && a.zoom_fade_in, style, - ]}> + ])}> Date: Wed, 5 Nov 2025 12:17:23 +0100 Subject: [PATCH 2/8] this is better --- README.md | 4 ++-- deer-static-about/index.html | 4 ++-- src/lib/constants.ts | 2 +- src/view/com/auth/SplashScreen.web.tsx | 2 +- web/index.html | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8941d6e1d..c4fbf2613 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ This is a fork of the soft fork [deer.social](https://github.com/a-viv-a/deer-so Get the app itself: - **Web: [social.daniela.lol](https://social.daniela.lol)** -- **Android: [Github Releases](github.com/kittibytess/deer-social/releases/latest)** - +- **Android: [Github Releases](github.com/kittibyte/deer-social/releases/latest)** + Get it on Obtainium diff --git a/deer-static-about/index.html b/deer-static-about/index.html index d642c8d92..e2c5a9f66 100644 --- a/deer-static-about/index.html +++ b/deer-static-about/index.html @@ -8,7 +8,7 @@ content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" /> social.daniela.lol | about - + @@ -16,7 +16,7 @@

redirecting to github readme...

diff --git a/src/lib/constants.ts b/src/lib/constants.ts index bfd0ecc2b..3e3910dfb 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -11,7 +11,7 @@ export const BSKY_SERVICE = 'https://bsky.social' export const BSKY_SERVICE_DID = 'did:web:bsky.social' export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app' export const DEFAULT_SERVICE = BSKY_SERVICE -export const HELP_DESK_URL = `https://github.com/kittibytess/deer-social/issues/new/choose` +export const HELP_DESK_URL = `https://github.com/kittibyte/deer-social/issues/new/choose` export const EMBED_SERVICE = 'https://embed.bsky.app' export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download' diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx index ed58bb066..b9f87875a 100644 --- a/src/view/com/auth/SplashScreen.web.tsx +++ b/src/view/com/auth/SplashScreen.web.tsx @@ -170,7 +170,7 @@ function Footer() { ]}> + to="https://github.com/kittibyte/deer-social"> Github diff --git a/web/index.html b/web/index.html index 0aa6c8749..6369dda8e 100644 --- a/web/index.html +++ b/web/index.html @@ -105,7 +105,7 @@ ">

JavaScript Required

This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is. -

Learn more about Bluesky at bsky.social and atproto.com, or this fork at github.com/kittibytess/deer-social. +

Learn more about Bluesky at bsky.social and atproto.com, or this fork at github.com/kittibyte/deer-social. -- 2.51.2 From 215f97bad9a32dc4fffc0cea35dbcd7efd83b23a Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Thu, 6 Nov 2025 02:41:01 +0000 Subject: [PATCH 3/8] Nightly source-language update --- src/locale/locales/en/messages.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index f2654d3cf..5036f30e6 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -1942,7 +1942,7 @@ msgid "Close" msgstr "" #: src/components/Dialog/index.web.tsx:118 -#: src/components/Dialog/index.web.tsx:295 +#: src/components/Dialog/index.web.tsx:296 msgid "Close active dialog" msgstr "" -- 2.51.2 From ec2c580aae5b54b25f549779d43512445bf4229d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 6 Nov 2025 17:58:14 +0200 Subject: [PATCH 4/8] Fix Android crash by try/catch dialog open (#9335) --- src/components/Dialog/context.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 1caa4eac8..b7e3c78d5 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -13,6 +13,7 @@ import { type DialogControlRefProps, type DialogOuterProps, } from '#/components/Dialog/types' +import {IS_DEV} from '#/env' import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types' export const Context = createContext({ @@ -50,10 +51,28 @@ export function useDialogControl(): DialogOuterProps['control'] { id, ref: control, open: () => { - control.current.open() + if (control.current) { + control.current.open() + } else { + if (IS_DEV) { + console.warn( + 'Attemped to open a dialog control that was not attached to a dialog!\n' + + 'Please ensure that the Dialog is mounted when calling open/close', + ) + } + } }, close: cb => { - control.current.close(cb) + if (control.current) { + control.current.close(cb) + } else { + if (IS_DEV) { + console.warn( + 'Attemped to close a dialog control that was not attached to a dialog!\n' + + 'Please ensure that the Dialog is mounted when calling open/close', + ) + } + } }, }), [id, control], -- 2.51.2 From 7bf00424c49f22408bdc79bfa07a7abe28230b33 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 6 Nov 2025 10:42:08 -0600 Subject: [PATCH 5/8] Clean up `getPostThreadV2` settings/params in prep for future work (#9179) * Deprecate prioritizeFollowedUsers * Bump API package --- package.json | 2 +- src/screens/Settings/ThreadPreferences.tsx | 31 +------------------ src/state/queries/preferences/const.ts | 1 - src/state/queries/preferences/types.ts | 11 ++----- .../preferences/useThreadPreferences.ts | 29 +---------------- src/state/queries/usePostThread/index.ts | 5 --- src/state/queries/usePostThread/types.ts | 2 +- yarn.lock | 8 ++--- 8 files changed, 10 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index d117352ab..4ca2b3833 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.17.6", + "@atproto/api": "^0.18.0", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.5", diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx index cba896a76..c44e3e01a 100644 --- a/src/screens/Settings/ThreadPreferences.tsx +++ b/src/screens/Settings/ThreadPreferences.tsx @@ -14,7 +14,6 @@ import { import {atoms as a, useTheme} from '#/alf' import * as Toggle from '#/components/forms/Toggle' import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble' -import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' import {Tree_Stroke2_Corner0_Rounded as TreeIcon} from '#/components/icons/Tree' import * as Layout from '#/components/Layout' import {Text} from '#/components/Typography' @@ -24,14 +23,7 @@ type Props = NativeStackScreenProps export function ThreadPreferencesScreen({}: Props) { const t = useTheme() const {_} = useLingui() - const { - sort, - setSort, - view, - setView, - prioritizeFollowedUsers, - setPrioritizeFollowedUsers, - } = useThreadPreferences({save: true}) + const {sort, setSort, view, setView} = useThreadPreferences({save: true}) return ( @@ -88,27 +80,6 @@ export function ThreadPreferencesScreen({}: Props) { - - - - Prioritize your Follows - - setPrioritizeFollowedUsers(value)} - style={[a.w_full, a.gap_md]}> - - - Show replies by people you follow before all other replies - - - - - - diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 84b208a9f..54d063a50 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -16,7 +16,6 @@ export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs export const DEFAULT_THREAD_VIEW_PREFS: ThreadViewPreferences = { sort: 'hotness', - prioritizeFollowedUsers: true, lab_treeViewEnabled: false, } diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts index fbe6a7e33..d46cef89f 100644 --- a/src/state/queries/preferences/types.ts +++ b/src/state/queries/preferences/types.ts @@ -1,8 +1,4 @@ -import { - type BskyFeedViewPreference, - type BskyPreferences, - type BskyThreadViewPreference, -} from '@atproto/api' +import {type BskyFeedViewPreference, type BskyPreferences} from '@atproto/api' export type UsePreferencesQueryResponse = Omit< BskyPreferences, @@ -18,10 +14,7 @@ export type UsePreferencesQueryResponse = Omit< userAge: number | undefined } -export type ThreadViewPreferences = Pick< - BskyThreadViewPreference, - 'prioritizeFollowedUsers' -> & { +export type ThreadViewPreferences = { sort: 'hotness' | 'oldest' | 'newest' | 'most-likes' | 'random' | string lab_treeViewEnabled?: boolean } diff --git a/src/state/queries/preferences/useThreadPreferences.ts b/src/state/queries/preferences/useThreadPreferences.ts index dc3122a72..70832b94a 100644 --- a/src/state/queries/preferences/useThreadPreferences.ts +++ b/src/state/queries/preferences/useThreadPreferences.ts @@ -23,8 +23,6 @@ export type ThreadPreferences = { setSort: (sort: string) => void view: ThreadViewOption setView: (view: ThreadViewOption) => void - prioritizeFollowedUsers: boolean - setPrioritizeFollowedUsers: (prioritize: boolean) => void } export function useThreadPreferences({ @@ -43,9 +41,6 @@ export function useThreadPreferences({ treeViewEnabled: !!serverPrefs?.lab_treeViewEnabled, }), ) - const [prioritizeFollowedUsers, setPrioritizeFollowedUsers] = useState( - !!serverPrefs?.prioritizeFollowedUsers, - ) /** * If we get a server update, update local state @@ -59,7 +54,6 @@ export function useThreadPreferences({ * Update */ setSort(normalizeSort(serverPrefs.sort)) - setPrioritizeFollowedUsers(serverPrefs.prioritizeFollowedUsers) setView( normalizeView({ treeViewEnabled: !!serverPrefs.lab_treeViewEnabled, @@ -70,7 +64,6 @@ export function useThreadPreferences({ logger.metric('thread:preferences:load', { sort: serverPrefs.sort, view: serverPrefs.lab_treeViewEnabled ? 'tree' : 'linear', - prioritizeFollowedUsers: serverPrefs.prioritizeFollowedUsers, }) }) } @@ -86,7 +79,6 @@ export function useThreadPreferences({ logger.metric('thread:preferences:update', { sort: prefs.sort, view: prefs.lab_treeViewEnabled ? 'tree' : 'linear', - prioritizeFollowedUsers: prefs.prioritizeFollowedUsers, }) } catch (e) { logger.error('useThreadPreferences failed to save', { @@ -101,7 +93,6 @@ export function useThreadPreferences({ if (save && userUpdatedPrefs.current) { savePrefs({ sort, - prioritizeFollowedUsers, lab_treeViewEnabled: view === 'tree', }) userUpdatedPrefs.current = false @@ -121,13 +112,6 @@ export function useThreadPreferences({ }, [setView], ) - const setPrioritizeFollowedUsersWrapped = useCallback( - (next: boolean) => { - userUpdatedPrefs.current = true - setPrioritizeFollowedUsers(next) - }, - [setPrioritizeFollowedUsers], - ) return useMemo( () => ({ @@ -137,19 +121,8 @@ export function useThreadPreferences({ setSort: setSortWrapped, view, setView: setViewWrapped, - prioritizeFollowedUsers, - setPrioritizeFollowedUsers: setPrioritizeFollowedUsersWrapped, }), - [ - isLoaded, - isSaving, - sort, - setSortWrapped, - view, - setViewWrapped, - prioritizeFollowedUsers, - setPrioritizeFollowedUsersWrapped, - ], + [isLoaded, isSaving, sort, setSortWrapped, view, setViewWrapped], ) } diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts index e00250f74..4b318a356 100644 --- a/src/state/queries/usePostThread/index.ts +++ b/src/state/queries/usePostThread/index.ts @@ -49,7 +49,6 @@ export function usePostThread({anchor}: {anchor?: string}) { setSort: baseSetSort, view, setView: baseSetView, - prioritizeFollowedUsers, } = useThreadPreferences() const below = useMemo(() => { return view === 'linear' @@ -63,11 +62,9 @@ export function usePostThread({anchor}: {anchor?: string}) { anchor, sort, view, - prioritizeFollowedUsers, }) const postThreadOtherQueryKey = createPostThreadOtherQueryKey({ anchor, - prioritizeFollowedUsers, }) const query = useQuery({ @@ -79,7 +76,6 @@ export function usePostThread({anchor}: {anchor?: string}) { branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF, below, sort: sort, - prioritizeFollowedUsers: prioritizeFollowedUsers, }) /* @@ -167,7 +163,6 @@ export function usePostThread({anchor}: {anchor?: string}) { async queryFn() { const {data} = await agent.app.bsky.unspecced.getPostThreadOtherV2({ anchor: anchor!, - prioritizeFollowedUsers, }) return data }, diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index 3e84dbfc7..295fd8bd3 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -25,7 +25,7 @@ export const createPostThreadOtherQueryKey = ( export type PostThreadParams = Pick< AppBskyUnspeccedGetPostThreadV2.QueryParams, - 'sort' | 'prioritizeFollowedUsers' + 'sort' > & { anchor?: string view: 'tree' | 'linear' diff --git a/yarn.lock b/yarn.lock index 7f1521f9c..57b893039 100644 --- a/yarn.lock +++ b/yarn.lock @@ -84,10 +84,10 @@ tlds "^1.234.0" zod "^3.23.8" -"@atproto/api@^0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.17.6.tgz#1fccd939f5f1010397c4d57110b1a0d8673058a6" - integrity sha512-0iYCD8+LOsHjHjwJcqGPfJN/h4b+IpU3GjOV0TSLk0XdCaxpHBKNu3wgCJVst4DhVjXcgsr2qQoRZ3Jja2LupA== +"@atproto/api@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.0.tgz#d8c54ddc4521d915f0af238a4bfebd119e18197f" + integrity sha512-2GxKPhhvMocDjRU7VpNj+cvCdmCHVAmRwyfNgRLMrJtPZvrosFoi9VATX+7eKN0FZvYvy8KdLSkCcpP2owH3IA== dependencies: "@atproto/common-web" "^0.4.3" "@atproto/lexicon" "^0.5.1" -- 2.51.2 From 9f98e53541273d333cce724633e8d6c583fac54e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 6 Nov 2025 19:40:31 +0200 Subject: [PATCH 6/8] Revert "push in post-post toast (#9252)" (#9344) This reverts commit 54a388d132c699a457aa8c6780b148d6f1ec5398. --- src/view/com/composer/Composer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 6aac718d5..7746d93c3 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -580,7 +580,7 @@ export const ComposePost = ({ label={_(msg`View post`)} onPress={() => { const {host: name, rkey} = new AtUri(postUri) - navigation.push('PostThread', {name, rkey}) + navigation.navigate('PostThread', {name, rkey}) }}> View -- 2.51.2 From fd14bbfef9c66524ffb3f3af5645d5282e6b5f81 Mon Sep 17 00:00:00 2001 From: David Buchanan Date: Thu, 6 Nov 2025 18:15:49 +0000 Subject: [PATCH 7/8] move yt embed script to its own source file (#9337) --- bskyweb/cmd/bskyweb/server.go | 2 +- bskyweb/static/iframe/youtube.html | 38 +----------------------------- bskyweb/static/iframe/youtube.js | 35 +++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 38 deletions(-) create mode 100644 bskyweb/static/iframe/youtube.js diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index c7350d37d..ec5261dee 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -223,7 +223,7 @@ func serve(cctx *cli.Context) error { e.GET("/robots.txt", echo.WrapHandler(staticHandler)) } - e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler)) + e.GET("/iframe/*", echo.WrapHandler(staticHandler)) e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { c.Response().Before(func() { diff --git a/bskyweb/static/iframe/youtube.html b/bskyweb/static/iframe/youtube.html index a717a67c6..483f5f528 100644 --- a/bskyweb/static/iframe/youtube.html +++ b/bskyweb/static/iframe/youtube.html @@ -16,40 +16,4 @@ }

- + diff --git a/bskyweb/static/iframe/youtube.js b/bskyweb/static/iframe/youtube.js new file mode 100644 index 000000000..149b950cd --- /dev/null +++ b/bskyweb/static/iframe/youtube.js @@ -0,0 +1,35 @@ +const url = new URL(window.location) +const viewport = document.querySelector("meta[name=viewport]") + +const tag = document.createElement("script") +tag.src = "https://www.youtube.com/iframe_api" +const firstScriptTag = document.getElementsByTagName('script')[0]; +firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + +let player +function onYouTubeIframeAPIReady() { + let videoId = url.searchParams.get('videoId') + videoId = decodeURIComponent(videoId) + videoId = videoId.replace(/[^a-zA-Z0-9_-]/g, "") + if (videoId.length !== 11) throw new Error("Invalid video ID") + + let start = url.searchParams.get('start') + start = start.replace(/[^0-9]/g, "") + + player = new YT.Player('player', { + width: "1000", + height: "1000", + videoId, + playerVars: { + autoplay: 1, + start, + rel: 0, + loop: 0, + playsinline: 1, + origin: url.origin + }, + }); +} +function onPlayerReady(event) { + event.target.playVideo(); +} -- 2.51.2 From d6ca9e6b4131c5e1b3bc030b1edf5005cbe61c13 Mon Sep 17 00:00:00 2001 From: hailey Date: Thu, 6 Nov 2025 13:40:13 -0800 Subject: [PATCH 8/8] [Reporting] Add new reporting categories and reasons (#9079) * start adding new report reasons * cleanup * use updated types * cleanup open/closed logic * other fix * state fix * rename type * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/index.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/components/moderation/ReportDialog/utils/useReportOptions.ts Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add mapping * Replace consts and maps using exported values * Map to deprecated reason if necessary, send some reports only to Bluesky * Use only new report reason types * Cleanup * Bump package, integrate new reasons * Adjust sorting, return type, integrate feedback * Just remove sort * Finish 'other' handling * Update descriptions * Just invert filter logic, no functional change but feels more correct * Clarify handling * fix deep imports * fix type error * [Reporting] Integrate new `ReportDialog` into DMs (#9213) * Update types, fork copy for convo vs message * Integrate in remaining spots * Replace DoneStep with separate dialog that opens after reporting * Handle profile error state by just showing a success message * Delete old dms ReportDialog * Chats only go to bsky * Only open block or delete dialog if submitted successfully * make close button always large * prevent expansion of blockordelete dialog * Delete old report dialog (#9214)e * Skip labeler selection for Bluesky-only cases * Rename AfterReportDialog --------- Co-authored-by: Samuel Newman --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Eric Bailey Co-authored-by: Samuel Newman --- .../ReportDialog/SelectLabelerView.tsx | 85 ---- .../ReportDialog/SelectReportOptionView.tsx | 195 -------- src/components/ReportDialog/SubmitView.tsx | 274 ----------- src/components/ReportDialog/const.ts | 1 - src/components/ReportDialog/index.tsx | 97 ---- src/components/ReportDialog/types.ts | 16 - src/components/dms/AfterReportDialog.tsx | 219 +++++++++ src/components/dms/ConvoMenu.tsx | 34 +- src/components/dms/MessageContextMenu.tsx | 23 +- src/components/dms/ReportDialog.tsx | 444 ------------------ .../moderation/ReportDialog/action.ts | 33 +- .../moderation/ReportDialog/const.ts | 112 +++++ .../moderation/ReportDialog/copy.ts | 18 +- .../moderation/ReportDialog/index.tsx | 340 ++++++++++---- .../moderation/ReportDialog/state.ts | 57 ++- .../moderation/ReportDialog/types.ts | 20 +- .../ReportDialog/utils/parseReportSubject.ts | 2 +- .../ReportDialog/utils/useReportOptions.ts | 339 +++++++++---- .../Messages/components/RequestButtons.tsx | 34 +- .../components/PreferenceControls.tsx | 9 +- 20 files changed, 984 insertions(+), 1368 deletions(-) delete mode 100644 src/components/ReportDialog/SelectLabelerView.tsx delete mode 100644 src/components/ReportDialog/SelectReportOptionView.tsx delete mode 100644 src/components/ReportDialog/SubmitView.tsx delete mode 100644 src/components/ReportDialog/const.ts delete mode 100644 src/components/ReportDialog/index.tsx delete mode 100644 src/components/ReportDialog/types.ts create mode 100644 src/components/dms/AfterReportDialog.tsx delete mode 100644 src/components/dms/ReportDialog.tsx diff --git a/src/components/ReportDialog/SelectLabelerView.tsx b/src/components/ReportDialog/SelectLabelerView.tsx deleted file mode 100644 index 02b324442..000000000 --- a/src/components/ReportDialog/SelectLabelerView.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import {View} from 'react-native' -import {type AppBskyLabelerDefs} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {getLabelingServiceTitle} from '#/lib/moderation' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, useButtonContext} from '#/components/Button' -import {Divider} from '#/components/Divider' -import * as LabelingServiceCard from '#/components/LabelingServiceCard' -import {Text} from '#/components/Typography' -import {type ReportDialogProps} from './types' - -export function SelectLabelerView({ - ...props -}: ReportDialogProps & { - labelers: AppBskyLabelerDefs.LabelerViewDetailed[] - onSelectLabeler: (v: string) => void -}) { - const t = useTheme() - const {_} = useLingui() - const {gtMobile} = useBreakpoints() - - return ( - - - - Select moderator - - - To whom would you like to send this report? - - - - - - - {props.labelers.map(labeler => { - return ( - - ) - })} - - - ) -} - -function LabelerButton({ - labeler, -}: { - labeler: AppBskyLabelerDefs.LabelerViewDetailed -}) { - const t = useTheme() - const {hovered, pressed} = useButtonContext() - const interacted = hovered || pressed - - return ( - - - - - - @{labeler.creator.handle} - - - - ) -} diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx deleted file mode 100644 index 0fd321cdf..000000000 --- a/src/components/ReportDialog/SelectReportOptionView.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import {type AppBskyLabelerDefs} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import { - type ReportOption, - useReportOptions, -} from '#/lib/moderation/useReportOptions' -import {Link} from '#/components/Link' -import {DMCA_LINK} from '#/components/ReportDialog/const' -export {useDialogControl as useReportDialogControl} from '#/components/Dialog' - -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import { - Button, - ButtonIcon, - ButtonText, - useButtonContext, -} from '#/components/Button' -import {Divider} from '#/components/Divider' -import { - ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft, - ChevronRight_Stroke2_Corner0_Rounded as ChevronRight, -} from '#/components/icons/Chevron' -import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight' -import {Text} from '#/components/Typography' -import {type ReportDialogProps} from './types' - -export function SelectReportOptionView(props: { - params: ReportDialogProps['params'] - labelers: AppBskyLabelerDefs.LabelerViewDetailed[] - onSelectReportOption: (reportOption: ReportOption) => void - goBack: () => void -}) { - const t = useTheme() - const {_} = useLingui() - const {gtMobile} = useBreakpoints() - const allReportOptions = useReportOptions() - const reportOptions = allReportOptions[props.params.type] - - const i18n = React.useMemo(() => { - let title = _(msg`Report this content`) - let description = _(msg`Why should this content be reviewed?`) - - if (props.params.type === 'account') { - title = _(msg`Report this user`) - description = _(msg`Why should this user be reviewed?`) - } else if (props.params.type === 'post') { - title = _(msg`Report this post`) - description = _(msg`Why should this post be reviewed?`) - } else if (props.params.type === 'list') { - title = _(msg`Report this list`) - description = _(msg`Why should this list be reviewed?`) - } else if (props.params.type === 'feedgen') { - title = _(msg`Report this feed`) - description = _(msg`Why should this feed be reviewed?`) - } else if (props.params.type === 'starterpack') { - title = _(msg`Report this starter pack`) - description = _(msg`Why should this starter pack be reviewed?`) - } else if (props.params.type === 'convoMessage') { - title = _(msg`Report this message`) - description = _(msg`Why should this message be reviewed?`) - } - - return { - title, - description, - } - }, [_, props.params.type]) - - return ( - - {props.labelers?.length > 1 ? ( - - ) : null} - - - {i18n.title} - - {i18n.description} - - - - - - - {reportOptions.map(reportOption => { - return ( - - ) - })} - - {(props.params.type === 'post' || props.params.type === 'account') && ( - - - Need to report a copyright violation? - - - - View details - - - - - )} - - - ) -} - -function ReportOptionButton({ - title, - description, -}: { - title: string - description: string -}) { - const t = useTheme() - const {hovered, pressed} = useButtonContext() - const interacted = hovered || pressed - - return ( - - - - {title} - - {description} - - - - - - - ) -} diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx deleted file mode 100644 index 3dbb1389d..000000000 --- a/src/components/ReportDialog/SubmitView.tsx +++ /dev/null @@ -1,274 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import {type AppBskyLabelerDefs} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {getLabelingServiceTitle} from '#/lib/moderation' -import {type ReportOption} from '#/lib/moderation/useReportOptions' -import {isAndroid} from '#/platform/detection' -import {useAgent} from '#/state/session' -import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' -import * as Toast from '#/view/com/util/Toast' -import {atoms as a, native, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import * as Toggle from '#/components/forms/Toggle' -import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' -import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' -import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' -import {type ReportDialogProps} from './types' - -export function SubmitView({ - params, - labelers, - selectedLabeler, - selectedReportOption, - goBack, - onSubmitComplete, -}: ReportDialogProps & { - labelers: AppBskyLabelerDefs.LabelerViewDetailed[] - selectedLabeler: string - selectedReportOption: ReportOption - goBack: () => void - onSubmitComplete: () => void -}) { - const t = useTheme() - const {_} = useLingui() - const agent = useAgent() - const [details, setDetails] = React.useState('') - const [submitting, setSubmitting] = React.useState(false) - const [selectedServices, setSelectedServices] = React.useState([ - selectedLabeler, - ]) - const [error, setError] = React.useState('') - - const submit = React.useCallback(async () => { - setSubmitting(true) - setError('') - - const $type = - params.type === 'account' - ? 'com.atproto.admin.defs#repoRef' - : 'com.atproto.repo.strongRef' - const report = { - reasonType: selectedReportOption.reason, - subject: { - $type, - ...params, - }, - reason: details, - } - const results = await Promise.all( - selectedServices.map(did => { - return agent - .createModerationReport(report, { - encoding: 'application/json', - headers: { - 'atproto-proxy': `${did}#atproto_labeler`, - }, - }) - .then( - _ => true, - _ => false, - ) - }), - ) - - setSubmitting(false) - - if (results.includes(true)) { - Toast.show(_(msg`Thank you. Your report has been sent.`)) - onSubmitComplete() - } else { - setError( - _( - msg`There was an issue sending your report. Please check your internet connection.`, - ), - ) - } - }, [ - _, - params, - details, - selectedReportOption, - selectedServices, - onSubmitComplete, - setError, - agent, - ]) - - return ( - - - - - - - {selectedReportOption.title} - - - {selectedReportOption.description} - - - - - - - - - Select the moderation service(s) to report to - - - - - {labelers.map(labeler => { - const title = getLabelingServiceTitle({ - displayName: labeler.creator.displayName, - handle: labeler.creator.handle, - }) - return ( - - - - ) - })} - - - - - - Optionally provide additional information below: - - - - - - - - - - - - - {!selectedServices.length || - (error && ( - - {error ? ( - error - ) : ( - You must select at least one labeler for a report - )} - - ))} - - - - {/* Maybe fix this later -h */} - {isAndroid ? : null} - - ) -} - -function LabelerToggle({title}: {title: string}) { - const t = useTheme() - const ctx = Toggle.useItemContext() - - return ( - - - - - {title} - - - - ) -} diff --git a/src/components/ReportDialog/const.ts b/src/components/ReportDialog/const.ts deleted file mode 100644 index 30c9aff88..000000000 --- a/src/components/ReportDialog/const.ts +++ /dev/null @@ -1 +0,0 @@ -export const DMCA_LINK = 'https://bsky.social/about/support/copyright' diff --git a/src/components/ReportDialog/index.tsx b/src/components/ReportDialog/index.tsx deleted file mode 100644 index 2401ab89f..000000000 --- a/src/components/ReportDialog/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react' -import {Pressable, View} from 'react-native' -import {type ScrollView} from 'react-native-gesture-handler' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {type ReportOption} from '#/lib/moderation/useReportOptions' -import {useMyLabelersQuery} from '#/state/queries/preferences' -export {useDialogControl as useReportDialogControl} from '#/components/Dialog' - -import {type AppBskyLabelerDefs} from '@atproto/api' - -import {atoms as a} from '#/alf' -import * as Dialog from '#/components/Dialog' -import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' -import {SelectLabelerView} from './SelectLabelerView' -import {SelectReportOptionView} from './SelectReportOptionView' -import {SubmitView} from './SubmitView' -import {type ReportDialogProps} from './types' - -export function ReportDialog(props: ReportDialogProps) { - return ( - - - - - ) -} - -function ReportDialogInner(props: ReportDialogProps) { - const {_} = useLingui() - const { - isLoading: isLabelerLoading, - data: labelers, - error, - } = useMyLabelersQuery({excludeNonConfigurableLabelers: true}) - const isLoading = useDelayedLoading(500, isLabelerLoading) - - const ref = React.useRef(null) - - return ( - - {isLoading ? ( - - - {/* Here to capture focus for a hot sec to prevent flash */} - - - ) : error || !labelers ? ( - - - Something went wrong, please try again. - - - ) : ( - - )} - - ) -} - -function ReportDialogLoaded( - props: ReportDialogProps & { - labelers: AppBskyLabelerDefs.LabelerViewDetailed[] - }, -) { - const [selectedLabeler, setSelectedLabeler] = React.useState< - string | undefined - >(props.labelers.length === 1 ? props.labelers[0].creator.did : undefined) - const [selectedReportOption, setSelectedReportOption] = React.useState< - ReportOption | undefined - >() - - if (selectedReportOption && selectedLabeler) { - return ( - setSelectedReportOption(undefined)} - onSubmitComplete={() => props.control.close()} - /> - ) - } - if (selectedLabeler) { - return ( - setSelectedLabeler(undefined)} - onSelectReportOption={setSelectedReportOption} - /> - ) - } - return -} diff --git a/src/components/ReportDialog/types.ts b/src/components/ReportDialog/types.ts deleted file mode 100644 index ac9a18d6d..000000000 --- a/src/components/ReportDialog/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type * as Dialog from '#/components/Dialog' - -export type ReportDialogProps = { - control: Dialog.DialogOuterProps['control'] - params: - | { - type: 'post' | 'list' | 'feedgen' | 'starterpack' | 'other' - uri: string - cid: string - } - | { - type: 'account' - did: string - } - | {type: 'convoMessage'} -} diff --git a/src/components/dms/AfterReportDialog.tsx b/src/components/dms/AfterReportDialog.tsx new file mode 100644 index 000000000..a87e5bdc2 --- /dev/null +++ b/src/components/dms/AfterReportDialog.tsx @@ -0,0 +1,219 @@ +import {memo, useState} from 'react' +import {View} from 'react-native' +import {type AppBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {StackActions, useNavigation} from '@react-navigation/native' +import type React from 'react' + +import {type NavigationProp} from '#/lib/routes/types' +import {isNative} from '#/platform/detection' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' +import { + useProfileBlockMutationQueue, + useProfileQuery, +} from '#/state/queries/profile' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as Toggle from '#/components/forms/Toggle' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +type ReportDialogParams = { + convoId: string + message: ChatBskyConvoDefs.MessageView +} + +/** + * Dialog shown after a report is submitted, allowing the user to block the + * reporter and/or leave the conversation. + */ +export const AfterReportDialog = memo(function BlockOrDeleteDialogInner({ + control, + params, + currentScreen, +}: { + control: Dialog.DialogControlProps + params: ReportDialogParams + currentScreen: 'list' | 'conversation' +}): React.ReactNode { + const {_} = useLingui() + return ( + + + + + + + + ) +}) + +function DialogInner({ + params, + currentScreen, +}: { + params: ReportDialogParams + currentScreen: 'list' | 'conversation' +}) { + const t = useTheme() + const {_} = useLingui() + const control = Dialog.useDialogContext() + const { + data: profile, + isLoading, + isError, + } = useProfileQuery({ + did: params.message.sender.did, + }) + + return isLoading ? ( + + + + ) : isError || !profile ? ( + + + + Report submitted + + + Our moderation team has received your report. + + + + + + ) : ( + + ) +} + +function DoneStep({ + convoId, + currentScreen, + profile, +}: { + convoId: string + currentScreen: 'list' | 'conversation' + profile: AppBskyActorDefs.ProfileViewDetailed +}) { + const {_} = useLingui() + const navigation = useNavigation() + const control = Dialog.useDialogContext() + const {gtMobile} = useBreakpoints() + const t = useTheme() + const [actions, setActions] = useState(['block', 'leave']) + const shadow = useProfileShadow(profile) + const [queueBlock] = useProfileBlockMutationQueue(shadow) + + const {mutate: leaveConvo} = useLeaveConvo(convoId, { + onMutate: () => { + if (currentScreen === 'conversation') { + navigation.dispatch( + StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}), + ) + } + }, + onError: () => { + Toast.show(_(msg`Could not leave chat`), 'xmark') + }, + }) + + let btnText = _(msg`Done`) + let toastMsg: string | undefined + if (actions.includes('leave') && actions.includes('block')) { + btnText = _(msg`Block and Delete`) + toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'})) + } else if (actions.includes('leave')) { + btnText = _(msg`Delete Conversation`) + toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'})) + } else if (actions.includes('block')) { + btnText = _(msg`Block User`) + toastMsg = _(msg({message: 'User blocked', context: 'toast'})) + } + + const onPressPrimaryAction = () => { + control.close(() => { + if (actions.includes('block')) { + queueBlock() + } + if (actions.includes('leave')) { + leaveConvo() + } + if (toastMsg) { + Toast.show(toastMsg, 'check') + } + }) + } + + return ( + + + + Report submitted + + + Our moderation team has received your report. + + + + + + + + Block user + + + + + + Delete conversation + + + + + + + + + + + ) +} diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index 1b1ebbcd5..fa50ec1e9 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -17,10 +17,10 @@ import * as Toast from '#/view/com/util/Toast' import {type ViewStyleProp} from '#/alf' import {atoms as a} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' +import {AfterReportDialog} from '#/components/dms/AfterReportDialog' import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog' import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt' -import {ReportDialog} from '#/components/dms/ReportDialog' import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft' import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble' import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' @@ -33,6 +33,7 @@ import { } from '#/components/icons/Person' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' import * as Menu from '#/components/Menu' +import {ReportDialog} from '#/components/moderation/ReportDialog' import * as Prompt from '#/components/Prompt' import type * as bsky from '#/types/bsky' @@ -65,6 +66,7 @@ let ConvoMenu = ({ const leaveConvoControl = Prompt.usePromptControl() const reportControl = Prompt.usePromptControl() const blockedByListControl = Prompt.usePromptControl() + const blockOrDeleteControl = Prompt.usePromptControl() const {listBlocks} = blockInfo @@ -113,15 +115,27 @@ let ConvoMenu = ({ currentScreen={currentScreen} /> {latestReportableMessage ? ( - + <> + { + blockOrDeleteControl.open() + }} + /> + + ) : ( )} diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx index 670e677db..b00b138ad 100644 --- a/src/components/dms/MessageContextMenu.tsx +++ b/src/components/dms/MessageContextMenu.tsx @@ -15,11 +15,12 @@ import {useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import * as ContextMenu from '#/components/ContextMenu' import {type TriggerProps} from '#/components/ContextMenu/types' -import {ReportDialog} from '#/components/dms/ReportDialog' +import {AfterReportDialog} from '#/components/dms/AfterReportDialog' import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' +import {ReportDialog} from '#/components/moderation/ReportDialog' import * as Prompt from '#/components/Prompt' import {usePromptControl} from '#/components/Prompt' import {EmojiReactionPicker} from './EmojiReactionPicker' @@ -37,6 +38,7 @@ export let MessageContextMenu = ({ const convo = useConvoActive() const deleteControl = usePromptControl() const reportControl = usePromptControl() + const blockOrDeleteControl = usePromptControl() const langPrefs = useLanguagePrefs() const translate = useTranslate() @@ -171,9 +173,24 @@ export let MessageContextMenu = ({ { + blockOrDeleteControl.open() + }} + /> + { - const {_} = useLingui() - return ( - - - - - - - - ) -} -ReportDialog = memo(ReportDialog) -export {ReportDialog} - -function DialogInner({ - params, - currentScreen, -}: { - params: ReportDialogParams - currentScreen: 'list' | 'conversation' -}) { - const {data: profile, isError} = useProfileQuery({ - did: params.message.sender.did, - }) - const [reportOption, setReportOption] = useState(null) - const [done, setDone] = useState(false) - const control = Dialog.useDialogContext() - - return done ? ( - profile ? ( - - ) : ( - - - - ) - ) : reportOption ? ( - setReportOption(null)} - onComplete={() => { - if (isError) { - control.close() - } else { - setDone(true) - } - }} - /> - ) : ( - - ) -} - -function ReasonStep({ - setReportOption, -}: { - setReportOption: (reportOption: ReportOption) => void - params: ReportDialogParams -}) { - const control = Dialog.useDialogContext() - - return ( - - ) -} - -function SubmitStep({ - params, - reportOption, - goBack, - onComplete, -}: { - params: ReportDialogParams - reportOption: ReportOption - goBack: () => void - onComplete: () => void -}) { - const {_} = useLingui() - const {gtMobile} = useBreakpoints() - const t = useTheme() - const [details, setDetails] = useState('') - const agent = useAgent() - - const { - mutate: submit, - error, - isPending: submitting, - } = useMutation({ - mutationFn: async () => { - if (params.type === 'convoMessage') { - const {convoId, message} = params - const subject: $Typed = { - $type: 'chat.bsky.convo.defs#messageRef', - messageId: message.id, - convoId, - did: message.sender.did, - } - - const report = { - reasonType: reportOption.reason, - subject, - reason: details, - } satisfies ComAtprotoModerationCreateReport.InputSchema - - await agent.createModerationReport(report, { - encoding: 'application/json', - headers: BLUESKY_MOD_SERVICE_HEADERS, - }) - } - }, - onSuccess: onComplete, - }) - - const copy = useMemo(() => { - return { - convoMessage: { - title: _(msg`Report this message`), - }, - }[params.type] - }, [_, params]) - - return ( - - - - - {copy.title} - - - Your report will be sent to the Bluesky Moderation Service - - - - - {params.type === 'convoMessage' && ( - - )} - - - - Reason: - {' '} - {reportOption.title} - - - - - - - Optionally provide additional information below: - - - - - - - - - - - - {error && ( - - - There was an issue sending your report. Please check your internet - connection. - - - )} - - - - - ) -} - -function DoneStep({ - convoId, - currentScreen, - profile, -}: { - convoId: string - currentScreen: 'list' | 'conversation' - profile: AppBskyActorDefs.ProfileViewDetailed -}) { - const {_} = useLingui() - const navigation = useNavigation() - const control = Dialog.useDialogContext() - const {gtMobile} = useBreakpoints() - const t = useTheme() - const [actions, setActions] = useState(['block', 'leave']) - const shadow = useProfileShadow(profile) - const [queueBlock] = useProfileBlockMutationQueue(shadow) - - const {mutate: leaveConvo} = useLeaveConvo(convoId, { - onMutate: () => { - if (currentScreen === 'conversation') { - navigation.dispatch( - StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}), - ) - } - }, - onError: () => { - Toast.show(_(msg`Could not leave chat`), 'xmark') - }, - }) - - let btnText = _(msg`Done`) - let toastMsg: string | undefined - if (actions.includes('leave') && actions.includes('block')) { - btnText = _(msg`Block and Delete`) - toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'})) - } else if (actions.includes('leave')) { - btnText = _(msg`Delete Conversation`) - toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'})) - } else if (actions.includes('block')) { - btnText = _(msg`Block User`) - toastMsg = _(msg({message: 'User blocked', context: 'toast'})) - } - - const onPressPrimaryAction = () => { - control.close(() => { - if (actions.includes('block')) { - queueBlock() - } - if (actions.includes('leave')) { - leaveConvo() - } - if (toastMsg) { - Toast.show(toastMsg, 'check') - } - }) - } - - return ( - - - - Report submitted - - - Our moderation team has received your report. - - - - - - - - Block user - - - - - - Delete conversation - - - - - - - - - - - ) -} - -function PreviewMessage({message}: {message: ChatBskyConvoDefs.MessageView}) { - const t = useTheme() - const rt = useMemo(() => { - return new RichTextAPI({text: message.text, facets: message.facets}) - }, [message.text, message.facets]) - - return ( - - - - - - - ) -} diff --git a/src/components/moderation/ReportDialog/action.ts b/src/components/moderation/ReportDialog/action.ts index 2da6e4829..1c35bd049 100644 --- a/src/components/moderation/ReportDialog/action.ts +++ b/src/components/moderation/ReportDialog/action.ts @@ -9,6 +9,7 @@ import {useMutation} from '@tanstack/react-query' import {logger} from '#/logger' import {useAgent} from '#/state/session' +import {NEW_TO_OLD_REASONS_MAP} from './const' import {type ReportState} from './state' import {type ParsedReportSubject} from './types' @@ -31,6 +32,26 @@ export function useSubmitReportMutation() { throw new Error(_(msg`Please select a moderation service`)) } + const labeler = state.selectedLabeler + const labelerSupportedReasonTypes = labeler.reasonTypes || [] + + let reasonType = state.selectedOption.reason + const backwardsCompatibleReasonType = NEW_TO_OLD_REASONS_MAP[reasonType] + const supportsNewReasonType = + labelerSupportedReasonTypes.includes(reasonType) + const supportsOldReasonType = labelerSupportedReasonTypes.includes( + backwardsCompatibleReasonType, + ) + + /* + * Only fall back for backwards compatibility if the labeler + * does not support the new reason type. If the labeler does not declare + * supported reason types, send the new version. + */ + if (supportsOldReasonType && !supportsNewReasonType) { + reasonType = backwardsCompatibleReasonType + } + let report: | ComAtprotoModerationCreateReport.InputSchema | (Omit & { @@ -40,7 +61,7 @@ export function useSubmitReportMutation() { switch (subject.type) { case 'account': { report = { - reasonType: state.selectedOption.reason, + reasonType, reason: state.details, subject: { $type: 'com.atproto.admin.defs#repoRef', @@ -54,7 +75,7 @@ export function useSubmitReportMutation() { case 'feed': case 'starterPack': { report = { - reasonType: state.selectedOption.reason, + reasonType, reason: state.details, subject: { $type: 'com.atproto.repo.strongRef', @@ -64,9 +85,9 @@ export function useSubmitReportMutation() { } break } - case 'chatMessage': { + case 'convoMessage': { report = { - reasonType: state.selectedOption.reason, + reasonType, reason: state.details, subject: { $type: 'chat.bsky.convo.defs#messageRef', @@ -82,7 +103,7 @@ export function useSubmitReportMutation() { if (__DEV__) { logger.info('Submitting report', { labeler: { - handle: state.selectedLabeler.creator.handle, + handle: labeler.creator.handle, }, report, }) @@ -90,7 +111,7 @@ export function useSubmitReportMutation() { await agent.createModerationReport(report, { encoding: 'application/json', headers: { - 'atproto-proxy': `${state.selectedLabeler.creator.did}#atproto_labeler`, + 'atproto-proxy': `${labeler.creator.did}#atproto_labeler`, }, }) } diff --git a/src/components/moderation/ReportDialog/const.ts b/src/components/moderation/ReportDialog/const.ts index 0f3fdbfac..14ff39795 100644 --- a/src/components/moderation/ReportDialog/const.ts +++ b/src/components/moderation/ReportDialog/const.ts @@ -1,2 +1,114 @@ +import { + ComAtprotoModerationDefs as RootReportDefs, + ToolsOzoneReportDefs as OzoneReportDefs, +} from '@atproto/api' + export const DMCA_LINK = 'https://bsky.social/about/support/copyright' export const SUPPORT_PAGE = 'https://bsky.social/about/support' + +export const NEW_TO_OLD_REASON_MAPPING: Record = {} + +/** + * Mapping of new (Ozone namespace) reason types to old reason types. + * + * Matches the mapping defined in the Ozone codebase: + * @see https://github.com/bluesky-social/atproto/blob/4c15fb47cec26060bff2e710e95869a90c9d7fdd/packages/ozone/src/mod-service/profile.ts#L16-L64 + */ +export const NEW_TO_OLD_REASONS_MAP: Record< + OzoneReportDefs.ReasonType, + RootReportDefs.ReasonType +> = { + [OzoneReportDefs.REASONAPPEAL]: RootReportDefs.REASONAPPEAL, + [OzoneReportDefs.REASONOTHER]: RootReportDefs.REASONOTHER, + + [OzoneReportDefs.REASONVIOLENCEANIMAL]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONVIOLENCETHREATS]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONVIOLENCEGRAPHICCONTENT]: + RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONVIOLENCEGLORIFICATION]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT]: + RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONVIOLENCETRAFFICKING]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONVIOLENCEOTHER]: RootReportDefs.REASONVIOLATION, + + [OzoneReportDefs.REASONSEXUALABUSECONTENT]: RootReportDefs.REASONSEXUAL, + [OzoneReportDefs.REASONSEXUALNCII]: RootReportDefs.REASONSEXUAL, + [OzoneReportDefs.REASONSEXUALDEEPFAKE]: RootReportDefs.REASONSEXUAL, + [OzoneReportDefs.REASONSEXUALANIMAL]: RootReportDefs.REASONSEXUAL, + [OzoneReportDefs.REASONSEXUALUNLABELED]: RootReportDefs.REASONSEXUAL, + [OzoneReportDefs.REASONSEXUALOTHER]: RootReportDefs.REASONSEXUAL, + + [OzoneReportDefs.REASONCHILDSAFETYCSAM]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONCHILDSAFETYGROOM]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONCHILDSAFETYPRIVACY]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONCHILDSAFETYHARASSMENT]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONCHILDSAFETYOTHER]: RootReportDefs.REASONVIOLATION, + + [OzoneReportDefs.REASONHARASSMENTTROLL]: RootReportDefs.REASONRUDE, + [OzoneReportDefs.REASONHARASSMENTTARGETED]: RootReportDefs.REASONRUDE, + [OzoneReportDefs.REASONHARASSMENTHATESPEECH]: RootReportDefs.REASONRUDE, + [OzoneReportDefs.REASONHARASSMENTDOXXING]: RootReportDefs.REASONRUDE, + [OzoneReportDefs.REASONHARASSMENTOTHER]: RootReportDefs.REASONRUDE, + + [OzoneReportDefs.REASONMISLEADINGBOT]: RootReportDefs.REASONMISLEADING, + [OzoneReportDefs.REASONMISLEADINGIMPERSONATION]: + RootReportDefs.REASONMISLEADING, + [OzoneReportDefs.REASONMISLEADINGSPAM]: RootReportDefs.REASONSPAM, + [OzoneReportDefs.REASONMISLEADINGSCAM]: RootReportDefs.REASONMISLEADING, + [OzoneReportDefs.REASONMISLEADINGELECTIONS]: RootReportDefs.REASONMISLEADING, + [OzoneReportDefs.REASONMISLEADINGOTHER]: RootReportDefs.REASONMISLEADING, + + [OzoneReportDefs.REASONRULESITESECURITY]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONRULEPROHIBITEDSALES]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONRULEBANEVASION]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONRULEOTHER]: RootReportDefs.REASONVIOLATION, + + [OzoneReportDefs.REASONSELFHARMCONTENT]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONSELFHARMED]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONSELFHARMSTUNTS]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONSELFHARMSUBSTANCES]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.REASONSELFHARMOTHER]: RootReportDefs.REASONVIOLATION, +} + +/** + * Mapping of old reason types to new (Ozone namespace) reason types. + * @see https://github.com/bluesky-social/proposals/tree/main/0009-mod-report-granularity#backwards-compatibility + */ +export const OLD_TO_NEW_REASONS_MAP: Record< + Exclude, + OzoneReportDefs.ReasonType +> = { + [RootReportDefs.REASONSPAM]: [OzoneReportDefs.REASONMISLEADINGSPAM], + [RootReportDefs.REASONVIOLATION]: [OzoneReportDefs.REASONRULEOTHER], + [RootReportDefs.REASONMISLEADING]: [OzoneReportDefs.REASONMISLEADINGOTHER], + [RootReportDefs.REASONSEXUAL]: [OzoneReportDefs.REASONSEXUALUNLABELED], + [RootReportDefs.REASONRUDE]: [OzoneReportDefs.REASONHARASSMENTOTHER], + [RootReportDefs.REASONOTHER]: [OzoneReportDefs.REASONOTHER], + [RootReportDefs.REASONAPPEAL]: [OzoneReportDefs.REASONAPPEAL], +} + +/** + * Set of report reasons that should optionally include additional details from + * the reporter. + */ +export const OTHER_REPORT_REASONS: Set = new Set([ + OzoneReportDefs.REASONVIOLENCEOTHER, + OzoneReportDefs.REASONSEXUALOTHER, + OzoneReportDefs.REASONCHILDSAFETYOTHER, + OzoneReportDefs.REASONHARASSMENTOTHER, + OzoneReportDefs.REASONMISLEADINGOTHER, + OzoneReportDefs.REASONRULEOTHER, + OzoneReportDefs.REASONSELFHARMOTHER, + OzoneReportDefs.REASONOTHER, +]) + +/** + * Set of report reasons that should only be sent to Bluesky's moderation service. + */ +export const BSKY_LABELER_ONLY_REPORT_REASONS: Set = + new Set([ + OzoneReportDefs.REASONCHILDSAFETYCSAM, + OzoneReportDefs.REASONCHILDSAFETYGROOM, + OzoneReportDefs.REASONCHILDSAFETYOTHER, + OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, + ]) diff --git a/src/components/moderation/ReportDialog/copy.ts b/src/components/moderation/ReportDialog/copy.ts index 11536c00c..1027673b1 100644 --- a/src/components/moderation/ReportDialog/copy.ts +++ b/src/components/moderation/ReportDialog/copy.ts @@ -38,10 +38,20 @@ export function useCopyForSubject(subject: ParsedReportSubject) { subtitle: _(msg`Why should this starter pack be reviewed?`), } } - case 'chatMessage': { - return { - title: _(msg`Report this message`), - subtitle: _(msg`Why should this message be reviewed?`), + case 'convoMessage': { + switch (subject.view) { + case 'convo': { + return { + title: _(msg`Report this conversation`), + subtitle: _(msg`Why should this conversation be reviewed?`), + } + } + case 'message': { + return { + title: _(msg`Report this message`), + subtitle: _(msg`Why should this message be reviewed?`), + } + } } } } diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index ebfbe0dcd..a264d12bc 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -1,7 +1,7 @@ import React from 'react' import {Pressable, View} from 'react-native' import {type ScrollView} from 'react-native-gesture-handler' -import {type AppBskyLabelerDefs} from '@atproto/api' +import {type AppBskyLabelerDefs, BSKY_LABELER_DID} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -30,12 +30,20 @@ import {createStaticClick, InlineLinkText, Link} from '#/components/Link' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useSubmitReportMutation} from './action' -import {SUPPORT_PAGE} from './const' +import { + BSKY_LABELER_ONLY_REPORT_REASONS, + NEW_TO_OLD_REASONS_MAP, + SUPPORT_PAGE, +} from './const' import {useCopyForSubject} from './copy' import {initialState, reducer} from './state' import {type ReportDialogProps, type ReportSubject} from './types' import {parseReportSubject} from './utils/parseReportSubject' -import {type ReportOption, useReportOptions} from './utils/useReportOptions' +import { + type ReportCategoryConfig, + type ReportOption, + useReportOptions, +} from './utils/useReportOptions' export {useDialogControl as useReportDialogControl} from '#/components/Dialog' @@ -95,7 +103,7 @@ function Inner(props: ReportDialogProps) { } = useMyLabelersQuery({excludeNonConfigurableLabelers: true}) const isLoading = useDelayedLoading(500, isLabelerLoading) const copy = useCopyForSubject(props.subject) - const reportOptions = useReportOptions() + const {categories, getCategory} = useReportOptions() const [state, dispatch] = React.useReducer(reducer, initialState) /** @@ -105,6 +113,13 @@ function Inner(props: ReportDialogProps) { const [isPending, setPending] = React.useState(false) const [isSuccess, setSuccess] = React.useState(false) + // some reasons ONLY go to Bluesky + const isBskyOnlyReason = state?.selectedOption?.reason + ? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason) + : false + // some subjects (chats) only go to Bluesky + const isBskyOnlySubject = props.subject.type === 'convoMessage' + /** * Labelers that support this `subject` and its NSID collection */ @@ -116,7 +131,7 @@ function Inner(props: ReportDialogProps) { if (subjectTypes === undefined) return true if (props.subject.type === 'account') { return subjectTypes.includes('account') - } else if (props.subject.type === 'chatMessage') { + } else if (props.subject.type === 'convoMessage') { return subjectTypes.includes('chat') } else { return subjectTypes.includes('record') @@ -126,19 +141,44 @@ function Inner(props: ReportDialogProps) { const collections: string[] | undefined = l.subjectCollections if (collections === undefined) return true // all chat collections accepted, since only Bluesky handles chats - if (props.subject.type === 'chatMessage') return true + if (props.subject.type === 'convoMessage') return true return collections.includes(props.subject.nsid) }) .filter(l => { - if (!state.selectedOption) return true - const reasonTypes: string[] | undefined = l.reasonTypes - if (reasonTypes === undefined) return true - return reasonTypes.includes(state.selectedOption.reason) + if (!state.selectedOption) return false + if (isBskyOnlyReason || isBskyOnlySubject) { + return l.creator.did === BSKY_LABELER_DID + } + const supportedReasonTypes: string[] | undefined = l.reasonTypes + if (supportedReasonTypes === undefined) return true + return ( + // supports new reason type + supportedReasonTypes.includes(state.selectedOption.reason) || + // supports old reason type (backwards compat) + supportedReasonTypes.includes( + NEW_TO_OLD_REASONS_MAP[state.selectedOption.reason], + ) + ) }) - }, [props, allLabelers, state.selectedOption]) + }, [ + props, + allLabelers, + state.selectedOption, + isBskyOnlyReason, + isBskyOnlySubject, + ]) const hasSupportedLabelers = !!supportedLabelers.length const hasSingleSupportedLabeler = supportedLabelers.length === 1 + /** + * We skip the select labeler step if there's only one possible labeler, and + * that labeler is Bluesky (which is the case for chat reports and certain + * reason types). We'll use this below to adjust the indexing and skip the + * step in the UI. + */ + const isAlwaysBskyLabeler = + hasSingleSupportedLabeler && (isBskyOnlyReason || isBskyOnlySubject) + const onSubmit = React.useCallback(async () => { dispatch({type: 'clearError'}) @@ -166,7 +206,9 @@ function Inner(props: ReportDialogProps) { ) // give time for user feedback setTimeout(() => { - props.control.close() + props.control.close(() => { + props.onAfterSubmit?.() + }) }, 1e3) } catch (e: any) { logger.metric('reportDialog:failure', {}, {statsig: false}) @@ -237,32 +279,36 @@ function Inner(props: ReportDialogProps) { ) : ( <> - {state.selectedOption ? ( + {state.selectedCategory ? ( - + ) : ( - {reportOptions[props.subject.type].map(o => ( - ( + { - dispatch({type: 'selectOption', option: o}) + dispatch({ + type: 'selectCategory', + option: o, + otherOption: getCategory('other').options[0], + }) }} /> ))} @@ -310,87 +356,142 @@ function Inner(props: ReportDialogProps) { - {state.activeStepIndex1 >= 2 && ( - <> - {state.selectedLabeler ? ( - <> - {hasSingleSupportedLabeler ? ( - - ) : ( - - - + {state.selectedOption ? ( + + + + + + + ) : state.selectedCategory ? ( + + {getCategory(state.selectedCategory.key).options.map(o => ( + { + dispatch({type: 'selectOption', option: o}) + }} + /> + ))} + + ) : null} + + + {isAlwaysBskyLabeler ? ( + !state.selectedLabeler} + callback={() => { + dispatch({ + type: 'selectLabeler', + labeler: supportedLabelers[0], + }) + }} + /> + ) : ( + + + {state.activeStepIndex1 >= 3 && ( + <> + {state.selectedLabeler ? ( + <> + {hasSingleSupportedLabeler ? ( + + ) : ( + + + + + - - - )} - - ) : ( - <> - {hasSupportedLabelers ? ( - - {hasSingleSupportedLabeler ? ( - <> - - !state.selectedLabeler} - callback={() => { - dispatch({ - type: 'selectLabeler', - labeler: supportedLabelers[0], - }) - }} - /> - - ) : ( - <> - {supportedLabelers.map(l => ( - { - dispatch({type: 'selectLabeler', labeler: l}) + )} + + ) : ( + <> + {hasSupportedLabelers ? ( + + {hasSingleSupportedLabeler ? ( + <> + + !state.selectedLabeler} + callback={() => { + dispatch({ + type: 'selectLabeler', + labeler: supportedLabelers[0], + }) }} /> - ))} - - )} - - ) : ( - // should never happen in our app - - - Unfortunately, none of your subscribed labelers supports - this report type. - - - )} - - )} - - )} - + + ) : ( + <> + {supportedLabelers.map(l => ( + { + dispatch({type: 'selectLabeler', labeler: l}) + }} + /> + ))} + + )} + + ) : ( + // should never happen in our app + + + Unfortunately, none of your subscribed labelers + supports this report type. + + + )} + + )} + + )} + + )} - {state.activeStepIndex1 === 3 && ( + {state.activeStepIndex1 === 4 && ( <> @@ -569,12 +670,12 @@ function StepTitle({ ) } -function OptionCard({ +function CategoryCard({ option, onSelect, }: { - option: ReportOption - onSelect?: (option: ReportOption) => void + option: ReportCategoryConfig + onSelect?: (option: ReportCategoryConfig) => void }) { const t = useTheme() const {_} = useLingui() @@ -584,7 +685,7 @@ function OptionCard({ }, [onSelect, option]) return ( + ) +} + function OptionCardSkeleton() { const t = useTheme() return ( diff --git a/src/components/moderation/ReportDialog/state.ts b/src/components/moderation/ReportDialog/state.ts index 18ee77df8..7b15e174b 100644 --- a/src/components/moderation/ReportDialog/state.ts +++ b/src/components/moderation/ReportDialog/state.ts @@ -1,8 +1,13 @@ -import {type AppBskyLabelerDefs, ComAtprotoModerationDefs} from '@atproto/api' +import {type AppBskyLabelerDefs} from '@atproto/api' -import {type ReportOption} from './utils/useReportOptions' +import {OTHER_REPORT_REASONS} from '#/components/moderation/ReportDialog/const' +import { + type ReportCategoryConfig, + type ReportOption, +} from '#/components/moderation/ReportDialog/utils/useReportOptions' export type ReportState = { + selectedCategory?: ReportCategoryConfig selectedOption?: ReportOption selectedLabeler?: AppBskyLabelerDefs.LabelerViewDetailed details?: string @@ -12,6 +17,14 @@ export type ReportState = { } export type ReportAction = + | { + type: 'selectCategory' + option: ReportCategoryConfig + otherOption: ReportOption + } + | { + type: 'clearCategory' + } | { type: 'selectOption' option: ReportOption @@ -42,6 +55,7 @@ export type ReportAction = } export const initialState: ReportState = { + selectedCategory: undefined, selectedOption: undefined, selectedLabeler: undefined, details: undefined, @@ -51,39 +65,52 @@ export const initialState: ReportState = { export function reducer(state: ReportState, action: ReportAction): ReportState { switch (action.type) { + case 'selectCategory': + return { + ...state, + selectedCategory: action.option, + activeStepIndex1: action.option.key === 'other' ? 3 : 2, + selectedOption: + action.option.key === 'other' ? action.otherOption : undefined, + } + case 'clearCategory': + return { + ...state, + selectedCategory: undefined, + selectedOption: undefined, + selectedLabeler: undefined, + activeStepIndex1: 1, + detailsOpen: false, + } case 'selectOption': return { ...state, selectedOption: action.option, - activeStepIndex1: 2, - detailsOpen: - !!state.details || - action.option.reason === ComAtprotoModerationDefs.REASONOTHER, + activeStepIndex1: 3, + detailsOpen: OTHER_REPORT_REASONS.has(action.option.reason), } case 'clearOption': return { ...state, selectedOption: undefined, selectedLabeler: undefined, - activeStepIndex1: 1, - detailsOpen: - !!state.details || - state.selectedOption?.reason === ComAtprotoModerationDefs.REASONOTHER, + activeStepIndex1: 2, + detailsOpen: false, } case 'selectLabeler': return { ...state, selectedLabeler: action.labeler, - activeStepIndex1: 3, + activeStepIndex1: 4, + detailsOpen: state.selectedOption + ? OTHER_REPORT_REASONS.has(state.selectedOption?.reason) + : false, } case 'clearLabeler': return { ...state, selectedLabeler: undefined, - activeStepIndex1: 2, - detailsOpen: - !!state.details || - state.selectedOption?.reason === ComAtprotoModerationDefs.REASONOTHER, + activeStepIndex1: 3, } case 'setDetails': return { diff --git a/src/components/moderation/ReportDialog/types.ts b/src/components/moderation/ReportDialog/types.ts index e84369905..6df3a82ff 100644 --- a/src/components/moderation/ReportDialog/types.ts +++ b/src/components/moderation/ReportDialog/types.ts @@ -8,6 +8,12 @@ import { import type * as Dialog from '#/components/Dialog' +export type ReportSubjectConvo = { + view: 'convo' | 'message' + convoId: string + message: ChatBskyConvoDefs.MessageView +} + export type ReportSubject = | $Typed | $Typed @@ -16,7 +22,7 @@ export type ReportSubject = | $Typed | $Typed | $Typed - | {convoId: string; message: ChatBskyConvoDefs.MessageView} + | ReportSubjectConvo export type ParsedReportSubject = | { @@ -55,13 +61,15 @@ export type ParsedReportSubject = did: string nsid: string } - | { - type: 'chatMessage' - convoId: string - message: ChatBskyConvoDefs.MessageView - } + | ({ + type: 'convoMessage' + } & ReportSubjectConvo) export type ReportDialogProps = { control: Dialog.DialogOuterProps['control'] subject: ParsedReportSubject + /** + * Called if the report was successfully submitted. + */ + onAfterSubmit?: () => void } diff --git a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts index baab1934f..0abfe1d9f 100644 --- a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts +++ b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts @@ -18,7 +18,7 @@ export function parseReportSubject( if ('convoId' in subject) { return { - type: 'chatMessage', + type: 'convoMessage', ...subject, } } diff --git a/src/components/moderation/ReportDialog/utils/useReportOptions.ts b/src/components/moderation/ReportDialog/utils/useReportOptions.ts index 38888d51a..75dae73f4 100644 --- a/src/components/moderation/ReportDialog/utils/useReportOptions.ts +++ b/src/components/moderation/ReportDialog/utils/useReportOptions.ts @@ -1,121 +1,256 @@ import {useMemo} from 'react' -import {ComAtprotoModerationDefs} from '@atproto/api' +import {ToolsOzoneReportDefs as OzoneReportDefs} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -export interface ReportOption { - reason: string +export type ReportCategory = + | 'childSafety' + | 'violencePhysicalHarm' + | 'sexualAdultContent' + | 'harassmentHate' + | 'misleading' + | 'ruleBreaking' + | 'selfHarm' + | 'other' + +export type ReportCategoryConfig = { + key: ReportCategory title: string description: string + options: ReportOption[] } -interface ReportOptions { - account: ReportOption[] - post: ReportOption[] - list: ReportOption[] - starterPack: ReportOption[] - feed: ReportOption[] - chatMessage: ReportOption[] +export type ReportOption = { + title: string + reason: OzoneReportDefs.ReasonType } -export function useReportOptions(): ReportOptions { +export function useReportOptions() { const {_} = useLingui() return useMemo(() => { - const other = { - reason: ComAtprotoModerationDefs.REASONOTHER, - title: _(msg`Other`), - description: _(msg`An issue not included in these options`), - } - const common = [ - { - reason: ComAtprotoModerationDefs.REASONRUDE, - title: _(msg`Anti-Social Behavior`), - description: _(msg`Harassment, trolling, or intolerance`), + const categories: Record = { + misleading: { + key: 'misleading', + title: _(msg`Misleading`), + description: _(msg`Spam or other inauthentic behavior or deception`), + options: [ + { + title: _(msg`Spam`), + reason: OzoneReportDefs.REASONMISLEADINGSPAM, + }, + { + title: _(msg`Scam`), + reason: OzoneReportDefs.REASONMISLEADINGSCAM, + }, + { + title: _(msg`Fake account or bot`), + reason: OzoneReportDefs.REASONMISLEADINGBOT, + }, + { + title: _(msg`Impersonation`), + reason: OzoneReportDefs.REASONMISLEADINGIMPERSONATION, + }, + { + title: _(msg`False information about elections`), + reason: OzoneReportDefs.REASONMISLEADINGELECTIONS, + }, + { + title: _(msg`Other misleading content`), + reason: OzoneReportDefs.REASONMISLEADINGOTHER, + }, + ], + }, + sexualAdultContent: { + key: 'sexualAdultContent', + title: _(msg`Adult content`), + description: _( + msg`Unlabeled, abusive, or non-consensual adult content`, + ), + options: [ + { + title: _(msg`Unlabeled adult content`), + reason: OzoneReportDefs.REASONSEXUALUNLABELED, + }, + { + title: _(msg`Adult sexual abuse content`), + reason: OzoneReportDefs.REASONSEXUALABUSECONTENT, + }, + { + title: _(msg`Non-consensual intimate imagery`), + reason: OzoneReportDefs.REASONSEXUALNCII, + }, + { + title: _(msg`Deepfake adult content`), + reason: OzoneReportDefs.REASONSEXUALDEEPFAKE, + }, + { + title: _(msg`Animal sexual abuse`), + reason: OzoneReportDefs.REASONSEXUALANIMAL, + }, + { + title: _(msg`Other sexual violence content`), + reason: OzoneReportDefs.REASONSEXUALOTHER, + }, + ], + }, + harassmentHate: { + key: 'harassmentHate', + title: _(msg`Harassment or hate`), + description: _(msg`Abusive or discriminatory behavior`), + options: [ + { + title: _(msg`Trolling`), + reason: OzoneReportDefs.REASONHARASSMENTTROLL, + }, + { + title: _(msg`Targeted harassment`), + reason: OzoneReportDefs.REASONHARASSMENTTARGETED, + }, + { + title: _(msg`Hate speech`), + reason: OzoneReportDefs.REASONHARASSMENTHATESPEECH, + }, + { + title: _(msg`Doxxing`), + reason: OzoneReportDefs.REASONHARASSMENTDOXXING, + }, + { + title: _(msg`Other harassing or hateful content`), + reason: OzoneReportDefs.REASONHARASSMENTOTHER, + }, + ], + }, + violencePhysicalHarm: { + key: 'violencePhysicalHarm', + title: _(msg`Violence`), + description: _(msg`Violent or threatening content`), + options: [ + { + title: _(msg`Animal welfare`), + reason: OzoneReportDefs.REASONVIOLENCEANIMAL, + }, + { + title: _(msg`Threats or incitement`), + reason: OzoneReportDefs.REASONVIOLENCETHREATS, + }, + { + title: _(msg`Graphic violent content`), + reason: OzoneReportDefs.REASONVIOLENCEGRAPHICCONTENT, + }, + { + title: _(msg`Glorification of violence`), + reason: OzoneReportDefs.REASONVIOLENCEGLORIFICATION, + }, + { + title: _(msg`Extremist content`), + reason: OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, + }, + { + title: _(msg`Human trafficking`), + reason: OzoneReportDefs.REASONVIOLENCETRAFFICKING, + }, + { + title: _(msg`Other violent content`), + reason: OzoneReportDefs.REASONVIOLENCEOTHER, + }, + ], }, - { - reason: ComAtprotoModerationDefs.REASONVIOLATION, - title: _(msg`Illegal and Urgent`), - description: _(msg`Glaring violations of law or terms of service`), + childSafety: { + key: 'childSafety', + title: _(msg`Child safety`), + description: _(msg`Harming or endangering minors`), + options: [ + { + title: _(msg`Child Sexual Abuse Material (CSAM)`), + reason: OzoneReportDefs.REASONCHILDSAFETYCSAM, + }, + { + title: _(msg`Grooming or predatory behavior`), + reason: OzoneReportDefs.REASONCHILDSAFETYGROOM, + }, + { + title: _(msg`Privacy violation of a minor`), + reason: OzoneReportDefs.REASONCHILDSAFETYPRIVACY, + }, + { + title: _(msg`Minor harassment or bullying`), + reason: OzoneReportDefs.REASONCHILDSAFETYHARASSMENT, + }, + { + title: _(msg`Other child safety issue`), + reason: OzoneReportDefs.REASONCHILDSAFETYOTHER, + }, + ], }, - other, - ] + selfHarm: { + key: 'selfHarm', + title: _(msg`Self-harm or dangerous behaviors`), + description: _(msg`Harmful or high-risk activities`), + options: [ + { + title: _(msg`Content promoting or depicting self-harm`), + reason: OzoneReportDefs.REASONSELFHARMCONTENT, + }, + { + title: _(msg`Eating disorders`), + reason: OzoneReportDefs.REASONSELFHARMED, + }, + { + title: _(msg`Dangerous challenges or activities`), + reason: OzoneReportDefs.REASONSELFHARMSTUNTS, + }, + { + title: _(msg`Dangerous substances or drug abuse`), + reason: OzoneReportDefs.REASONSELFHARMSUBSTANCES, + }, + { + title: _(msg`Other dangerous content`), + reason: OzoneReportDefs.REASONSELFHARMOTHER, + }, + ], + }, + ruleBreaking: { + key: 'ruleBreaking', + title: _(msg`Breaking site rules`), + description: _(msg`Banned activities or security violations`), + options: [ + { + title: _(msg`Hacking or system attacks`), + reason: OzoneReportDefs.REASONRULESITESECURITY, + }, + { + title: _(msg`Promoting or selling prohibited items or services`), + reason: OzoneReportDefs.REASONRULEPROHIBITEDSALES, + }, + { + title: _(msg`Banned user returning`), + reason: OzoneReportDefs.REASONRULEBANEVASION, + }, + { + title: _(msg`Other network rule-breaking`), + reason: OzoneReportDefs.REASONRULEOTHER, + }, + ], + }, + other: { + key: 'other', + title: _(msg`Other`), + description: _(msg`An issue not included in these options`), + options: [ + { + title: _(msg`Other`), + reason: OzoneReportDefs.REASONOTHER, + }, + ], + }, + } + return { - account: [ - { - reason: ComAtprotoModerationDefs.REASONMISLEADING, - title: _(msg`Misleading Account`), - description: _( - msg`Impersonation or false claims about identity or affiliation`, - ), - }, - { - reason: ComAtprotoModerationDefs.REASONSPAM, - title: _(msg`Frequently Posts Unwanted Content`), - description: _(msg`Spam; excessive mentions or replies`), - }, - { - reason: ComAtprotoModerationDefs.REASONVIOLATION, - title: _(msg`Name or Description Violates Community Standards`), - description: _(msg`Terms used violate community standards`), - }, - other, - ], - post: [ - { - reason: ComAtprotoModerationDefs.REASONMISLEADING, - title: _(msg`Misleading Post`), - description: _(msg`Impersonation, misinformation, or false claims`), - }, - { - reason: ComAtprotoModerationDefs.REASONSPAM, - title: _(msg`Spam`), - description: _(msg`Excessive mentions or replies`), - }, - { - reason: ComAtprotoModerationDefs.REASONSEXUAL, - title: _(msg`Unwanted Sexual Content`), - description: _(msg`Nudity or adult content not labeled as such`), - }, - ...common, - ], - chatMessage: [ - { - reason: ComAtprotoModerationDefs.REASONSPAM, - title: _(msg`Spam`), - description: _(msg`Excessive or unwanted messages`), - }, - { - reason: ComAtprotoModerationDefs.REASONSEXUAL, - title: _(msg`Unwanted Sexual Content`), - description: _(msg`Inappropriate messages or explicit links`), - }, - ...common, - ], - list: [ - { - reason: ComAtprotoModerationDefs.REASONVIOLATION, - title: _(msg`Name or Description Violates Community Standards`), - description: _(msg`Terms used violate community standards`), - }, - ...common, - ], - starterPack: [ - { - reason: ComAtprotoModerationDefs.REASONVIOLATION, - title: _(msg`Name or Description Violates Community Standards`), - description: _(msg`Terms used violate community standards`), - }, - ...common, - ], - feed: [ - { - reason: ComAtprotoModerationDefs.REASONVIOLATION, - title: _(msg`Name or Description Violates Community Standards`), - description: _(msg`Terms used violate community standards`), - }, - ...common, - ], + categories: Object.values(categories) as ReportCategoryConfig[], + getCategory(reasonName: ReportCategory) { + return categories[reasonName] + }, } }, [_]) } diff --git a/src/screens/Messages/components/RequestButtons.tsx b/src/screens/Messages/components/RequestButtons.tsx index 4437a9a43..4b53b4042 100644 --- a/src/screens/Messages/components/RequestButtons.tsx +++ b/src/screens/Messages/components/RequestButtons.tsx @@ -25,12 +25,13 @@ import { EmailDialogScreenID, useEmailDialogControl, } from '#/components/dialogs/EmailDialog' -import {ReportDialog} from '#/components/dms/ReportDialog' +import {AfterReportDialog} from '#/components/dms/AfterReportDialog' import {CircleX_Stroke2_Corner0_Rounded} from '#/components/icons/CircleX' import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag' import {PersonX_Stroke2_Corner0_Rounded as PersonXIcon} from '#/components/icons/Person' import {Loader} from '#/components/Loader' import * as Menu from '#/components/Menu' +import {ReportDialog} from '#/components/moderation/ReportDialog' export function RejectMenu({ convo, @@ -100,6 +101,7 @@ export function RejectMenu({ }, [queueBlock, leaveConvo, _]) const reportControl = useDialogControl() + const blockOrDeleteControl = useDialogControl() const lastMessage = ChatBskyConvoDefs.isMessageView(convo.lastMessage) ? convo.lastMessage @@ -162,15 +164,27 @@ export function RejectMenu({ {lastMessage && ( - + <> + { + blockOrDeleteControl.open() + }} + /> + + )} ) diff --git a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx index e63f06b4a..84e553029 100644 --- a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx @@ -1,7 +1,6 @@ import {useMemo} from 'react' import {View} from 'react-native' import {type AppBskyNotificationDefs} from '@atproto/api' -import {type FilterablePreference} from '@atproto/api/dist/client/types/app/bsky/notification/defs' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -25,7 +24,9 @@ export function PreferenceControls({ * which groups starterpack joins + verified + unverified notifications into a single toggle. */ syncOthers?: Exclude[] - preference?: AppBskyNotificationDefs.Preference | FilterablePreference + preference?: + | AppBskyNotificationDefs.Preference + | AppBskyNotificationDefs.FilterablePreference allowDisableInApp?: boolean }) { if (!preference) @@ -53,7 +54,9 @@ export function Inner({ }: { name: Exclude syncOthers?: Exclude[] - preference: AppBskyNotificationDefs.Preference | FilterablePreference + preference: + | AppBskyNotificationDefs.Preference + | AppBskyNotificationDefs.FilterablePreference allowDisableInApp: boolean }) { const t = useTheme() -- 2.51.2