Hide posts tool (#2299)
* Set up hidden posts persisted state * Wrap moderatePost * Integrate hidden posts into moderation * Complete hide-post behaviors --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>zio/stable
parent
28e0df595f
commit
b199405134
|
@ -0,0 +1,29 @@
|
||||||
|
import {moderatePost} from '@atproto/api'
|
||||||
|
|
||||||
|
type ModeratePost = typeof moderatePost
|
||||||
|
type Options = Parameters<ModeratePost>[1] & {
|
||||||
|
hiddenPosts?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moderatePost_wrapped(
|
||||||
|
subject: Parameters<ModeratePost>[0],
|
||||||
|
opts: Options,
|
||||||
|
) {
|
||||||
|
const {hiddenPosts = [], ...options} = opts
|
||||||
|
const moderations = moderatePost(subject, options)
|
||||||
|
|
||||||
|
if (hiddenPosts.includes(subject.uri)) {
|
||||||
|
moderations.content.filter = true
|
||||||
|
moderations.content.blur = true
|
||||||
|
if (!moderations.content.cause) {
|
||||||
|
moderations.content.cause = {
|
||||||
|
// @ts-ignore Temporary extension to the moderation system -prf
|
||||||
|
type: 'post-hidden',
|
||||||
|
source: {type: 'user'},
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moderations
|
||||||
|
}
|
|
@ -60,6 +60,13 @@ export function describeModerationCause(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// @ts-ignore Temporary extension to the moderation system -prf
|
||||||
|
if (cause.type === 'post-hidden') {
|
||||||
|
return {
|
||||||
|
name: 'Post Hidden by You',
|
||||||
|
description: 'You have hidden this post',
|
||||||
|
}
|
||||||
|
}
|
||||||
return cause.labelDef.strings[context].en
|
return cause.labelDef.strings[context].en
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
|
||||||
onboarding: {
|
onboarding: {
|
||||||
step: legacy.onboarding?.step || defaults.onboarding.step,
|
step: legacy.onboarding?.step || defaults.onboarding.step,
|
||||||
},
|
},
|
||||||
|
hiddenPosts: defaults.hiddenPosts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ export const schema = z.object({
|
||||||
onboarding: z.object({
|
onboarding: z.object({
|
||||||
step: z.string(),
|
step: z.string(),
|
||||||
}),
|
}),
|
||||||
|
hiddenPosts: z.array(z.string()).optional(), // should move to server
|
||||||
})
|
})
|
||||||
export type Schema = z.infer<typeof schema>
|
export type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
|
@ -66,4 +67,5 @@ export const defaults: Schema = {
|
||||||
onboarding: {
|
onboarding: {
|
||||||
step: 'Home',
|
step: 'Home',
|
||||||
},
|
},
|
||||||
|
hiddenPosts: [],
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
|
||||||
|
type SetStateCb = (
|
||||||
|
s: persisted.Schema['hiddenPosts'],
|
||||||
|
) => persisted.Schema['hiddenPosts']
|
||||||
|
type StateContext = persisted.Schema['hiddenPosts']
|
||||||
|
type ApiContext = {
|
||||||
|
hidePost: ({uri}: {uri: string}) => void
|
||||||
|
unhidePost: ({uri}: {uri: string}) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateContext = React.createContext<StateContext>(
|
||||||
|
persisted.defaults.hiddenPosts,
|
||||||
|
)
|
||||||
|
const apiContext = React.createContext<ApiContext>({
|
||||||
|
hidePost: () => {},
|
||||||
|
unhidePost: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const [state, setState] = React.useState(persisted.get('hiddenPosts'))
|
||||||
|
|
||||||
|
const setStateWrapped = React.useCallback(
|
||||||
|
(fn: SetStateCb) => {
|
||||||
|
const s = fn(persisted.get('hiddenPosts'))
|
||||||
|
setState(s)
|
||||||
|
persisted.write('hiddenPosts', s)
|
||||||
|
},
|
||||||
|
[setState],
|
||||||
|
)
|
||||||
|
|
||||||
|
const api = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
hidePost: ({uri}: {uri: string}) => {
|
||||||
|
setStateWrapped(s => [...(s || []), uri])
|
||||||
|
},
|
||||||
|
unhidePost: ({uri}: {uri: string}) => {
|
||||||
|
setStateWrapped(s => (s || []).filter(u => u !== uri))
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[setStateWrapped],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return persisted.onUpdate(() => {
|
||||||
|
setState(persisted.get('hiddenPosts'))
|
||||||
|
})
|
||||||
|
}, [setStateWrapped])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<stateContext.Provider value={state}>
|
||||||
|
<apiContext.Provider value={api}>{children}</apiContext.Provider>
|
||||||
|
</stateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHiddenPosts() {
|
||||||
|
return React.useContext(stateContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHiddenPostsApi() {
|
||||||
|
return React.useContext(apiContext)
|
||||||
|
}
|
|
@ -1,17 +1,21 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Provider as LanguagesProvider} from './languages'
|
import {Provider as LanguagesProvider} from './languages'
|
||||||
import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required'
|
import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required'
|
||||||
|
import {Provider as HiddenPostsProvider} from '../preferences/hidden-posts'
|
||||||
|
|
||||||
export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
|
export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
|
||||||
export {
|
export {
|
||||||
useRequireAltTextEnabled,
|
useRequireAltTextEnabled,
|
||||||
useSetRequireAltTextEnabled,
|
useSetRequireAltTextEnabled,
|
||||||
} from './alt-text-required'
|
} from './alt-text-required'
|
||||||
|
export * from './hidden-posts'
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
return (
|
return (
|
||||||
<LanguagesProvider>
|
<LanguagesProvider>
|
||||||
<AltTextRequiredProvider>{children}</AltTextRequiredProvider>
|
<AltTextRequiredProvider>
|
||||||
|
<HiddenPostsProvider>{children}</HiddenPostsProvider>
|
||||||
|
</AltTextRequiredProvider>
|
||||||
</LanguagesProvider>
|
</LanguagesProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ import {
|
||||||
AppBskyNotificationListNotifications,
|
AppBskyNotificationListNotifications,
|
||||||
ModerationOpts,
|
ModerationOpts,
|
||||||
moderateProfile,
|
moderateProfile,
|
||||||
moderatePost,
|
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyFeedRepost,
|
AppBskyFeedRepost,
|
||||||
AppBskyFeedLike,
|
AppBskyFeedLike,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import chunk from 'lodash.chunk'
|
import chunk from 'lodash.chunk'
|
||||||
import {QueryClient} from '@tanstack/react-query'
|
import {QueryClient} from '@tanstack/react-query'
|
||||||
import {getAgent} from '../../session'
|
import {getAgent} from '../../session'
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import React, {useCallback, useEffect, useRef} from 'react'
|
import React, {useCallback, useEffect, useRef} from 'react'
|
||||||
import {
|
import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api'
|
||||||
AppBskyFeedDefs,
|
|
||||||
AppBskyFeedPost,
|
|
||||||
moderatePost,
|
|
||||||
PostModeration,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {
|
import {
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
InfiniteData,
|
InfiniteData,
|
||||||
|
@ -12,6 +7,7 @@ import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import {useFeedTuners} from '../preferences/feed-tuners'
|
import {useFeedTuners} from '../preferences/feed-tuners'
|
||||||
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
|
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
|
||||||
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
|
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from '#/state/queries/preferences/const'
|
} from '#/state/queries/preferences/const'
|
||||||
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
|
import {useHiddenPosts} from '#/state/preferences/hidden-posts'
|
||||||
|
|
||||||
export * from '#/state/queries/preferences/types'
|
export * from '#/state/queries/preferences/types'
|
||||||
export * from '#/state/queries/preferences/moderation'
|
export * from '#/state/queries/preferences/moderation'
|
||||||
|
@ -94,15 +95,21 @@ export function usePreferencesQuery() {
|
||||||
export function useModerationOpts() {
|
export function useModerationOpts() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const prefs = usePreferencesQuery()
|
const prefs = usePreferencesQuery()
|
||||||
|
const hiddenPosts = useHiddenPosts()
|
||||||
const opts = useMemo(() => {
|
const opts = useMemo(() => {
|
||||||
if (!prefs.data) {
|
if (!prefs.data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return getModerationOpts({
|
const moderationOpts = getModerationOpts({
|
||||||
userDid: currentAccount?.did || '',
|
userDid: currentAccount?.did || '',
|
||||||
preferences: prefs.data,
|
preferences: prefs.data,
|
||||||
})
|
})
|
||||||
}, [currentAccount?.did, prefs.data])
|
|
||||||
|
return {
|
||||||
|
...moderationOpts,
|
||||||
|
hiddenPosts,
|
||||||
|
}
|
||||||
|
}, [currentAccount?.did, prefs.data, hiddenPosts])
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
moderatePost,
|
|
||||||
PostModeration,
|
PostModeration,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {Link, TextLink} from '../util/Link'
|
import {Link, TextLink} from '../util/Link'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
|
|
|
@ -4,10 +4,10 @@ import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AtUri,
|
AtUri,
|
||||||
moderatePost,
|
|
||||||
PostModeration,
|
PostModeration,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {Link, TextLink} from '../util/Link'
|
import {Link, TextLink} from '../util/Link'
|
||||||
import {UserInfoText} from '../util/UserInfoText'
|
import {UserInfoText} from '../util/UserInfoText'
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {getTranslatorLink} from '#/locale/helpers'
|
||||||
import {usePostDeleteMutation} from '#/state/queries/post'
|
import {usePostDeleteMutation} from '#/state/queries/post'
|
||||||
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
|
import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
@ -50,9 +51,12 @@ let PostDropdownBtn = ({
|
||||||
const mutedThreads = useMutedThreads()
|
const mutedThreads = useMutedThreads()
|
||||||
const toggleThreadMute = useToggleThreadMute()
|
const toggleThreadMute = useToggleThreadMute()
|
||||||
const postDeleteMutation = usePostDeleteMutation()
|
const postDeleteMutation = usePostDeleteMutation()
|
||||||
|
const hiddenPosts = useHiddenPosts()
|
||||||
|
const {hidePost} = useHiddenPostsApi()
|
||||||
|
|
||||||
const rootUri = record.reply?.root?.uri || postUri
|
const rootUri = record.reply?.root?.uri || postUri
|
||||||
const isThreadMuted = mutedThreads.includes(rootUri)
|
const isThreadMuted = mutedThreads.includes(rootUri)
|
||||||
|
const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
|
||||||
const isAuthor = postAuthor.did === currentAccount?.did
|
const isAuthor = postAuthor.did === currentAccount?.did
|
||||||
const href = React.useMemo(() => {
|
const href = React.useMemo(() => {
|
||||||
const urip = new AtUri(postUri)
|
const urip = new AtUri(postUri)
|
||||||
|
@ -98,6 +102,10 @@ let PostDropdownBtn = ({
|
||||||
Linking.openURL(translatorUrl)
|
Linking.openURL(translatorUrl)
|
||||||
}, [translatorUrl])
|
}, [translatorUrl])
|
||||||
|
|
||||||
|
const onHidePost = React.useCallback(() => {
|
||||||
|
hidePost({uri: postUri})
|
||||||
|
}, [postUri, hidePost])
|
||||||
|
|
||||||
const dropdownItems: NativeDropdownItem[] = [
|
const dropdownItems: NativeDropdownItem[] = [
|
||||||
{
|
{
|
||||||
label: _(msg`Translate`),
|
label: _(msg`Translate`),
|
||||||
|
@ -159,6 +167,27 @@ let PostDropdownBtn = ({
|
||||||
web: 'comment-slash',
|
web: 'comment-slash',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hasSession &&
|
||||||
|
!isAuthor &&
|
||||||
|
!isPostHidden && {
|
||||||
|
label: _(msg`Hide post`),
|
||||||
|
onPress() {
|
||||||
|
openModal({
|
||||||
|
name: 'confirm',
|
||||||
|
title: _(msg`Hide this post?`),
|
||||||
|
message: _(msg`This will hide this post from your feeds.`),
|
||||||
|
onPressConfirm: onHidePost,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
testID: 'postDropdownHideBtn',
|
||||||
|
icon: {
|
||||||
|
ios: {
|
||||||
|
name: 'eye.slash',
|
||||||
|
},
|
||||||
|
android: 'ic_menu_delete',
|
||||||
|
web: ['far', 'eye-slash'],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'separator',
|
label: 'separator',
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue