From b730b7c81fbc9fa5a6e3ca7b31fafe02a055b60c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 11 Apr 2023 22:44:53 -0700 Subject: [PATCH] Add start of post-hiding by labels --- src/lib/labeling/const.ts | 115 ++++++++++++++++++++ src/lib/labeling/helpers.ts | 11 ++ src/state/models/feeds/notifications.ts | 3 + src/view/com/post-thread/PostThreadItem.tsx | 8 +- src/view/com/post/Post.tsx | 8 +- src/view/com/posts/FeedItem.tsx | 31 +++--- src/view/com/util/PostHider.tsx | 83 ++++++++++++++ src/view/com/util/PostMuted.tsx | 50 --------- 8 files changed, 240 insertions(+), 69 deletions(-) create mode 100644 src/lib/labeling/const.ts create mode 100644 src/lib/labeling/helpers.ts create mode 100644 src/view/com/util/PostHider.tsx delete mode 100644 src/view/com/util/PostMuted.tsx diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts new file mode 100644 index 0000000000..028f3191f5 --- /dev/null +++ b/src/lib/labeling/const.ts @@ -0,0 +1,115 @@ +export const FILTER_SETTINGS = { + base: { + sourceDids: [ + 'did:plc:ar7c4by46qjdydhdevvrndac', + 'did:plc:jonz5u6ulzdkwpnhqlwbsbrh', + ], + filterLabelVals: ['csam', 'dmca-violation', 'nudity-nonconsentual'], + }, + 'bsky-unfiltered': { + sourceDids: [ + 'did:plc:ar7c4by46qjdydhdevvrndac', + 'did:plc:jonz5u6ulzdkwpnhqlwbsbrh', + ], + filterLabelVals: [], + }, + 'bsky-default': { + sourceDids: [ + 'did:plc:ar7c4by46qjdydhdevvrndac', + 'did:plc:jonz5u6ulzdkwpnhqlwbsbrh', + ], + filterLabelVals: ['porn', 'nudity', 'gore', 'self-harm', 'torture', 'spam'], + }, + 'bsky-calm': { + sourceDids: [ + 'did:plc:ar7c4by46qjdydhdevvrndac', + 'did:plc:jonz5u6ulzdkwpnhqlwbsbrh', + ], + filterLabelVals: [ + 'porn', + 'nudity', + 'sexual', + 'gore', + 'self-harm', + 'torture', + 'icon-kkk', + 'icon-nazi', + 'icon-confederate', + 'spam', + 'impersonation', + ], + }, +} + +export const FILTERS = { + 'bsky-unfiltered': { + title: 'Unfiltered', + author: 'Bluesky Team', + description: 'Show me everything.', + filterLabelVals: FILTER_SETTINGS.base.filterLabelVals.concat( + FILTER_SETTINGS['bsky-unfiltered'].filterLabelVals, + ), + }, + 'bsky-default': { + title: 'Default', + author: 'Bluesky Team', + description: + 'Filters out NSFW and nudity, gore, self-harm, torture, and spam.', + filterLabelVals: FILTER_SETTINGS.base.filterLabelVals.concat( + FILTER_SETTINGS['bsky-default'].filterLabelVals, + ), + }, + 'bsky-calm': { + title: 'Calm', + author: 'Bluesky Team', + description: + 'Like default, but also filters out hate-group iconography and impersonations.', + filterLabelVals: FILTER_SETTINGS.base.filterLabelVals.concat( + FILTER_SETTINGS['bsky-calm'].filterLabelVals, + ), + }, +} + +export interface LabelValGroup { + id: string + title: string + values: string[] +} + +export const LABEL_VAL_GROUPS: Record = { + illegal: { + id: 'illegal', + title: 'Illegal Content', + values: ['csam', 'dmca-violation', 'nudity-nonconsentual'], + }, + nsfw: { + id: 'nsfw', + title: 'Sexual Content', + values: ['porn', 'nudity', 'sexual'], + }, + gore: { + id: 'gore', + title: 'Violent / Bloody Content', + values: ['gore', 'self-harm', 'torture'], + }, + hate: { + id: 'hate', + title: 'Political Hate-Group Content', + values: ['icon-kkk', 'icon-nazi', 'icon-confederate'], + }, + spam: { + id: 'spam', + title: 'Spam', + values: ['spam'], + }, + impersonation: { + id: 'impersonation', + title: 'Impersonated Content', + values: ['impersonation'], + }, + unknown: { + id: 'unknown', + title: 'Unknown Label', + values: [], + }, +} diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts new file mode 100644 index 0000000000..f1849a3d0f --- /dev/null +++ b/src/lib/labeling/helpers.ts @@ -0,0 +1,11 @@ +import {ComAtprotoLabelDefs} from '@atproto/api' +import {LabelValGroup, LABEL_VAL_GROUPS} from './const' + +export function getLabelValueGroup(labelVal: string): LabelValGroup { + for (const id in LABEL_VAL_GROUPS) { + if (LABEL_VAL_GROUPS[id].values.includes(labelVal)) { + return LABEL_VAL_GROUPS[id] + } + } + return LABEL_VAL_GROUPS.unknown +} diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index 96563836f6..49dfea64a8 100644 --- a/src/state/models/feeds/notifications.ts +++ b/src/state/models/feeds/notifications.ts @@ -6,6 +6,7 @@ import { AppBskyFeedRepost, AppBskyFeedLike, AppBskyGraphFollow, + ComAtprotoLabelDefs, } from '@atproto/api' import AwaitLock from 'await-lock' import {bundleAsync} from 'lib/async/bundle' @@ -49,6 +50,7 @@ export class NotificationsFeedItemModel { record?: SupportedRecord isRead: boolean = false indexedAt: string = '' + labels?: ComAtprotoLabelDefs.Label[] additional?: NotificationsFeedItemModel[] // additional data @@ -73,6 +75,7 @@ export class NotificationsFeedItemModel { this.record = this.toSupportedRecord(v.record) this.isRead = v.isRead this.indexedAt = v.indexedAt + this.labels = v.labels if (v.additional?.length) { this.additional = [] for (const add of v.additional) { diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 3d3647f60f..a0ae768088 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -22,7 +22,7 @@ import {useStores} from 'state/index' import {PostMeta} from '../util/PostMeta' import {PostEmbeds} from '../util/post-embeds' import {PostCtrls} from '../util/PostCtrls' -import {PostMutedWrapper} from '../util/PostMuted' +import {PostHider} from '../util/PostHider' import {ErrorMessage} from '../util/error/ErrorMessage' import {usePalette} from 'lib/hooks/usePalette' @@ -270,7 +270,9 @@ export const PostThreadItem = observer(function PostThreadItem({ ) } else { return ( - + ) : undefined} - + ) } }) diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 186946fb49..eb8d623960 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -17,7 +17,7 @@ import {UserInfoText} from '../util/UserInfoText' import {PostMeta} from '../util/PostMeta' import {PostEmbeds} from '../util/post-embeds' import {PostCtrls} from '../util/PostCtrls' -import {PostMutedWrapper} from '../util/PostMuted' +import {PostHider} from '../util/PostHider' import {Text} from '../util/text/Text' import {RichText} from '../util/text/RichText' import * as Toast from '../util/Toast' @@ -150,7 +150,9 @@ export const Post = observer(function Post({ } return ( - + - + ) }) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 569d11257d..2f093cdc3f 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -14,7 +14,7 @@ import {UserInfoText} from '../util/UserInfoText' import {PostMeta} from '../util/PostMeta' import {PostCtrls} from '../util/PostCtrls' import {PostEmbeds} from '../util/post-embeds' -import {PostMutedWrapper} from '../util/PostMuted' +import {PostHider} from '../util/PostHider' import {RichText} from '../util/text/RichText' import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' @@ -59,7 +59,7 @@ export const FeedItem = observer(function ({ return urip.hostname }, [record?.reply]) - const onPressReply = () => { + const onPressReply = React.useCallback(() => { track('FeedItem:PostReply') store.shell.openComposer({ replyTo: { @@ -73,29 +73,34 @@ export const FeedItem = observer(function ({ }, }, }) - } - const onPressToggleRepost = () => { + }, [item, track, record, store]) + + const onPressToggleRepost = React.useCallback(() => { track('FeedItem:PostRepost') return item .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) - } - const onPressToggleLike = () => { + }, [track, item, store]) + + const onPressToggleLike = React.useCallback(() => { track('FeedItem:PostLike') return item .toggleLike() .catch(e => store.log.error('Failed to toggle like', e)) - } - const onCopyPostText = () => { + }, [track, item, store]) + + const onCopyPostText = React.useCallback(() => { Clipboard.setString(record?.text || '') Toast.show('Copied to clipboard') - } + }, [record]) + const onOpenTranslate = React.useCallback(() => { Linking.openURL( encodeURI(`https://translate.google.com/#auto|en|${record?.text || ''}`), ) }, [record]) - const onDeletePost = () => { + + const onDeletePost = React.useCallback(() => { track('FeedItem:PostDelete') item.delete().then( () => { @@ -107,7 +112,7 @@ export const FeedItem = observer(function ({ Toast.show('Failed to delete post, please try again') }, ) - } + }, [track, item, setDeleted, store]) if (!record || deleted) { return @@ -127,7 +132,7 @@ export const FeedItem = observer(function ({ ] return ( - + - + ) }) diff --git a/src/view/com/util/PostHider.tsx b/src/view/com/util/PostHider.tsx new file mode 100644 index 0000000000..4df256de70 --- /dev/null +++ b/src/view/com/util/PostHider.tsx @@ -0,0 +1,83 @@ +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {ComAtprotoLabelDefs} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {usePalette} from 'lib/hooks/usePalette' +import {Text} from './text/Text' +import {getLabelValueGroup} from 'lib/labeling/helpers' + +export function PostHider({ + isMuted, + labels, + children, +}: React.PropsWithChildren<{ + isMuted?: boolean + labels: ComAtprotoLabelDefs.Label[] | undefined +}>) { + const pal = usePalette('default') + const palError = usePalette('error') + const [override, setOverride] = React.useState(false) + + if (!isMuted && !labels?.length) { + return <>{children} + } + + const label = labels?.[0] // TODO use config to settle on most relevant item + const labelGroup = getLabelValueGroup(label?.val || '') + if (labelGroup.id === 'illegal') { + return <> + } + + return ( + + + + + {isMuted ? ( + <>Post from an account you muted. + ) : label ? ( + <>Warning: {labelGroup.title} + ) : ( + '' + )} + + setOverride(v => !v)}> + + {override ? 'Hide' : 'Show'} post + + + + {override && ( + + {children} + + )} + + ) +} + +const styles = StyleSheet.create({ + description: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 14, + paddingHorizontal: 18, + borderTopWidth: 1, + }, + icon: { + marginRight: 10, + }, + showBtn: { + marginLeft: 'auto', + }, + childrenContainer: { + paddingHorizontal: 6, + paddingVertical: 6, + }, +}) diff --git a/src/view/com/util/PostMuted.tsx b/src/view/com/util/PostMuted.tsx deleted file mode 100644 index 539a71ecf4..0000000000 --- a/src/view/com/util/PostMuted.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {usePalette} from 'lib/hooks/usePalette' -import {Text} from './text/Text' - -export function PostMutedWrapper({ - isMuted, - children, -}: React.PropsWithChildren<{isMuted?: boolean}>) { - const pal = usePalette('default') - const [override, setOverride] = React.useState(false) - if (!isMuted || override) { - return <>{children} - } - return ( - - - - Post from an account you muted. - - setOverride(true)}> - - Show post - - - - ) -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 14, - paddingHorizontal: 18, - borderTopWidth: 1, - }, - icon: { - marginRight: 10, - }, - showBtn: { - marginLeft: 'auto', - }, -})