Add start of post-hiding by labels
This commit is contained in:
@@ -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<string, LabelValGroup> = {
|
||||||
|
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: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
AppBskyFeedRepost,
|
AppBskyFeedRepost,
|
||||||
AppBskyFeedLike,
|
AppBskyFeedLike,
|
||||||
AppBskyGraphFollow,
|
AppBskyGraphFollow,
|
||||||
|
ComAtprotoLabelDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import AwaitLock from 'await-lock'
|
import AwaitLock from 'await-lock'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
@@ -49,6 +50,7 @@ export class NotificationsFeedItemModel {
|
|||||||
record?: SupportedRecord
|
record?: SupportedRecord
|
||||||
isRead: boolean = false
|
isRead: boolean = false
|
||||||
indexedAt: string = ''
|
indexedAt: string = ''
|
||||||
|
labels?: ComAtprotoLabelDefs.Label[]
|
||||||
additional?: NotificationsFeedItemModel[]
|
additional?: NotificationsFeedItemModel[]
|
||||||
|
|
||||||
// additional data
|
// additional data
|
||||||
@@ -73,6 +75,7 @@ export class NotificationsFeedItemModel {
|
|||||||
this.record = this.toSupportedRecord(v.record)
|
this.record = this.toSupportedRecord(v.record)
|
||||||
this.isRead = v.isRead
|
this.isRead = v.isRead
|
||||||
this.indexedAt = v.indexedAt
|
this.indexedAt = v.indexedAt
|
||||||
|
this.labels = v.labels
|
||||||
if (v.additional?.length) {
|
if (v.additional?.length) {
|
||||||
this.additional = []
|
this.additional = []
|
||||||
for (const add of v.additional) {
|
for (const add of v.additional) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {useStores} from 'state/index'
|
|||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {PostEmbeds} from '../util/post-embeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostCtrls} from '../util/PostCtrls'
|
import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {PostMutedWrapper} from '../util/PostMuted'
|
import {PostHider} from '../util/PostHider'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
@@ -270,7 +270,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<PostMutedWrapper isMuted={item.post.author.viewer?.muted === true}>
|
<PostHider
|
||||||
|
isMuted={item.post.author.viewer?.muted === true}
|
||||||
|
labels={item.post.labels}>
|
||||||
<Link
|
<Link
|
||||||
testID={`postThreadItem-by-${item.post.author.handle}`}
|
testID={`postThreadItem-by-${item.post.author.handle}`}
|
||||||
style={[styles.outer, {borderTopColor: pal.colors.border}, pal.view]}
|
style={[styles.outer, {borderTopColor: pal.colors.border}, pal.view]}
|
||||||
@@ -364,7 +366,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</PostMutedWrapper>
|
</PostHider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {UserInfoText} from '../util/UserInfoText'
|
|||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {PostEmbeds} from '../util/post-embeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostCtrls} from '../util/PostCtrls'
|
import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {PostMutedWrapper} from '../util/PostMuted'
|
import {PostHider} from '../util/PostHider'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
@@ -150,7 +150,9 @@ export const Post = observer(function Post({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostMutedWrapper isMuted={item.post.author.viewer?.muted === true}>
|
<PostHider
|
||||||
|
isMuted={item.post.author.viewer?.muted === true}
|
||||||
|
labels={item.post.labels}>
|
||||||
<Link
|
<Link
|
||||||
style={[styles.outer, pal.view, pal.border, style]}
|
style={[styles.outer, pal.view, pal.border, style]}
|
||||||
href={itemHref}
|
href={itemHref}
|
||||||
@@ -227,7 +229,7 @@ export const Post = observer(function Post({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
</PostMutedWrapper>
|
</PostHider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {UserInfoText} from '../util/UserInfoText'
|
|||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {PostCtrls} from '../util/PostCtrls'
|
import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {PostEmbeds} from '../util/post-embeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostMutedWrapper} from '../util/PostMuted'
|
import {PostHider} from '../util/PostHider'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
@@ -59,7 +59,7 @@ export const FeedItem = observer(function ({
|
|||||||
return urip.hostname
|
return urip.hostname
|
||||||
}, [record?.reply])
|
}, [record?.reply])
|
||||||
|
|
||||||
const onPressReply = () => {
|
const onPressReply = React.useCallback(() => {
|
||||||
track('FeedItem:PostReply')
|
track('FeedItem:PostReply')
|
||||||
store.shell.openComposer({
|
store.shell.openComposer({
|
||||||
replyTo: {
|
replyTo: {
|
||||||
@@ -73,29 +73,34 @@ export const FeedItem = observer(function ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}, [item, track, record, store])
|
||||||
const onPressToggleRepost = () => {
|
|
||||||
|
const onPressToggleRepost = React.useCallback(() => {
|
||||||
track('FeedItem:PostRepost')
|
track('FeedItem:PostRepost')
|
||||||
return item
|
return item
|
||||||
.toggleRepost()
|
.toggleRepost()
|
||||||
.catch(e => store.log.error('Failed to toggle repost', e))
|
.catch(e => store.log.error('Failed to toggle repost', e))
|
||||||
}
|
}, [track, item, store])
|
||||||
const onPressToggleLike = () => {
|
|
||||||
|
const onPressToggleLike = React.useCallback(() => {
|
||||||
track('FeedItem:PostLike')
|
track('FeedItem:PostLike')
|
||||||
return item
|
return item
|
||||||
.toggleLike()
|
.toggleLike()
|
||||||
.catch(e => store.log.error('Failed to toggle like', e))
|
.catch(e => store.log.error('Failed to toggle like', e))
|
||||||
}
|
}, [track, item, store])
|
||||||
const onCopyPostText = () => {
|
|
||||||
|
const onCopyPostText = React.useCallback(() => {
|
||||||
Clipboard.setString(record?.text || '')
|
Clipboard.setString(record?.text || '')
|
||||||
Toast.show('Copied to clipboard')
|
Toast.show('Copied to clipboard')
|
||||||
}
|
}, [record])
|
||||||
|
|
||||||
const onOpenTranslate = React.useCallback(() => {
|
const onOpenTranslate = React.useCallback(() => {
|
||||||
Linking.openURL(
|
Linking.openURL(
|
||||||
encodeURI(`https://translate.google.com/#auto|en|${record?.text || ''}`),
|
encodeURI(`https://translate.google.com/#auto|en|${record?.text || ''}`),
|
||||||
)
|
)
|
||||||
}, [record])
|
}, [record])
|
||||||
const onDeletePost = () => {
|
|
||||||
|
const onDeletePost = React.useCallback(() => {
|
||||||
track('FeedItem:PostDelete')
|
track('FeedItem:PostDelete')
|
||||||
item.delete().then(
|
item.delete().then(
|
||||||
() => {
|
() => {
|
||||||
@@ -107,7 +112,7 @@ export const FeedItem = observer(function ({
|
|||||||
Toast.show('Failed to delete post, please try again')
|
Toast.show('Failed to delete post, please try again')
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}, [track, item, setDeleted, store])
|
||||||
|
|
||||||
if (!record || deleted) {
|
if (!record || deleted) {
|
||||||
return <View />
|
return <View />
|
||||||
@@ -127,7 +132,7 @@ export const FeedItem = observer(function ({
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostMutedWrapper isMuted={isMuted}>
|
<PostHider isMuted={isMuted} labels={item.post.labels}>
|
||||||
<Link
|
<Link
|
||||||
testID={`feedItem-by-${item.post.author.handle}`}
|
testID={`feedItem-by-${item.post.author.handle}`}
|
||||||
style={outerStyles}
|
style={outerStyles}
|
||||||
@@ -257,7 +262,7 @@ export const FeedItem = observer(function ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
</PostMutedWrapper>
|
</PostHider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<View style={[styles.container, pal.view]}>
|
||||||
|
<View style={[styles.description, pal.view, pal.border]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={['far', 'eye-slash']}
|
||||||
|
style={[styles.icon, pal.text]}
|
||||||
|
/>
|
||||||
|
<Text type="md" style={pal.textLight}>
|
||||||
|
{isMuted ? (
|
||||||
|
<>Post from an account you muted.</>
|
||||||
|
) : label ? (
|
||||||
|
<>Warning: {labelGroup.title}</>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.showBtn}
|
||||||
|
onPress={() => setOverride(v => !v)}>
|
||||||
|
<Text type="md" style={pal.link}>
|
||||||
|
{override ? 'Hide' : 'Show'} post
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
{override && (
|
||||||
|
<View
|
||||||
|
style={[styles.childrenContainer, pal.border, palError.viewLight]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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 (
|
|
||||||
<View style={[styles.container, pal.view, pal.border]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={['far', 'eye-slash']}
|
|
||||||
style={[styles.icon, pal.text]}
|
|
||||||
/>
|
|
||||||
<Text type="md" style={pal.textLight}>
|
|
||||||
Post from an account you muted.
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.showBtn}
|
|
||||||
onPress={() => setOverride(true)}>
|
|
||||||
<Text type="md" style={pal.link}>
|
|
||||||
Show post
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
|
||||||
showBtn: {
|
|
||||||
marginLeft: 'auto',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user