diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-06-18 19:48:34 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-18 21:48:34 +0300 |
commit | 5f5d845053e13169f89fc70a3f858b0a9e5ed4fd (patch) | |
tree | 2902f50fbfdce9b59b38d74c0dac96b3a2abf8b9 /src/state | |
parent | 35e54e24a0b08ce0f2e3389aeb4fb0f29778170e (diff) | |
download | voidsky-5f5d845053e13169f89fc70a3f858b0a9e5ed4fd.tar.zst |
Server-side thread mutes (#4518)
* update atproto/api * move thread mutes to server side * rm log * move muted threads provider to inside did key * use map instead of object
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/cache/thread-mutes.tsx | 44 | ||||
-rw-r--r-- | src/state/muted-threads.tsx | 62 | ||||
-rw-r--r-- | src/state/queries/notifications/feed.ts | 3 | ||||
-rw-r--r-- | src/state/queries/notifications/unread.tsx | 5 | ||||
-rw-r--r-- | src/state/queries/notifications/util.ts | 50 | ||||
-rw-r--r-- | src/state/queries/post.ts | 70 |
6 files changed, 115 insertions, 119 deletions
diff --git a/src/state/cache/thread-mutes.tsx b/src/state/cache/thread-mutes.tsx new file mode 100644 index 000000000..b58bd430f --- /dev/null +++ b/src/state/cache/thread-mutes.tsx @@ -0,0 +1,44 @@ +import React from 'react' + +type StateContext = Map<string, boolean> +type SetStateContext = (uri: string, value: boolean) => void + +const stateContext = React.createContext<StateContext>(new Map()) +const setStateContext = React.createContext<SetStateContext>( + (_: string) => false, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState<StateContext>(() => new Map()) + + const setThreadMute = React.useCallback( + (uri: string, value: boolean) => { + setState(prev => { + const next = new Map(prev) + next.set(uri, value) + return next + }) + }, + [setState], + ) + return ( + <stateContext.Provider value={state}> + <setStateContext.Provider value={setThreadMute}> + {children} + </setStateContext.Provider> + </stateContext.Provider> + ) +} + +export function useMutedThreads() { + return React.useContext(stateContext) +} + +export function useIsThreadMuted(uri: string, defaultValue = false) { + const state = React.useContext(stateContext) + return state.get(uri) ?? defaultValue +} + +export function useSetThreadMute() { + return React.useContext(setStateContext) +} diff --git a/src/state/muted-threads.tsx b/src/state/muted-threads.tsx deleted file mode 100644 index 84a717eb7..000000000 --- a/src/state/muted-threads.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import * as persisted from '#/state/persisted' -import {track} from '#/lib/analytics/analytics' - -type StateContext = persisted.Schema['mutedThreads'] -type ToggleContext = (uri: string) => boolean - -const stateContext = React.createContext<StateContext>( - persisted.defaults.mutedThreads, -) -const toggleContext = React.createContext<ToggleContext>((_: string) => false) - -export function Provider({children}: React.PropsWithChildren<{}>) { - const [state, setState] = React.useState(persisted.get('mutedThreads')) - - const toggleThreadMute = React.useCallback( - (uri: string) => { - let muted = false - setState((arr: string[]) => { - if (arr.includes(uri)) { - arr = arr.filter(v => v !== uri) - muted = false - track('Post:ThreadUnmute') - } else { - arr = arr.concat([uri]) - muted = true - track('Post:ThreadMute') - } - persisted.write('mutedThreads', arr) - return arr - }) - return muted - }, - [setState], - ) - - React.useEffect(() => { - return persisted.onUpdate(() => { - setState(persisted.get('mutedThreads')) - }) - }, [setState]) - - return ( - <stateContext.Provider value={state}> - <toggleContext.Provider value={toggleThreadMute}> - {children} - </toggleContext.Provider> - </stateContext.Provider> - ) -} - -export function useMutedThreads() { - return React.useContext(stateContext) -} - -export function useToggleThreadMute() { - return React.useContext(toggleContext) -} - -export function isThreadMuted(uri: string) { - return persisted.get('mutedThreads').includes(uri) -} diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index d9f019af3..0607f07a1 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -26,7 +26,6 @@ import { useQueryClient, } from '@tanstack/react-query' -import {useMutedThreads} from '#/state/muted-threads' import {useAgent} from '#/state/session' import {useModerationOpts} from '../../preferences/moderation-opts' import {STALE} from '..' @@ -54,7 +53,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { const agent = useAgent() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() - const threadMutes = useMutedThreads() const unreads = useUnreadNotificationsApi() const enabled = opts?.enabled !== false const lastPageCountRef = useRef(0) @@ -82,7 +80,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { cursor: pageParam, queryClient, moderationOpts, - threadMutes, fetchAdditionalData: true, }) ).page diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index ffb8d03bc..7bb325ea9 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -9,7 +9,6 @@ import EventEmitter from 'eventemitter3' import BroadcastChannel from '#/lib/broadcast' import {logger} from '#/logger' -import {useMutedThreads} from '#/state/muted-threads' import {useAgent, useSession} from '#/state/session' import {resetBadgeCount} from 'lib/notifications/notifications' import {useModerationOpts} from '../../preferences/moderation-opts' @@ -48,7 +47,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const agent = useAgent() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() - const threadMutes = useMutedThreads() const [numUnread, setNumUnread] = React.useState('') @@ -147,7 +145,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { limit: 40, queryClient, moderationOpts, - threadMutes, // only fetch subjects when the page is going to be used // in the notifications query, otherwise skip it @@ -192,7 +189,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } }, } - }, [setNumUnread, queryClient, moderationOpts, threadMutes, agent]) + }, [setNumUnread, queryClient, moderationOpts, agent]) checkUnreadRef.current = api.checkUnread return ( diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index 466249353..8ed1c0390 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -1,5 +1,4 @@ import { - AppBskyEmbedRecord, AppBskyFeedDefs, AppBskyFeedLike, AppBskyFeedPost, @@ -28,7 +27,6 @@ export async function fetchPage({ limit, queryClient, moderationOpts, - threadMutes, fetchAdditionalData, }: { agent: BskyAgent @@ -36,7 +34,6 @@ export async function fetchPage({ limit: number queryClient: QueryClient moderationOpts: ModerationOpts | undefined - threadMutes: string[] fetchAdditionalData: boolean }): Promise<{page: FeedPage; indexedAt: string | undefined}> { const res = await agent.listNotifications({ @@ -67,11 +64,6 @@ export async function fetchPage({ } } - // apply thread muting - notifsGrouped = notifsGrouped.filter( - notif => !isThreadMuted(notif, threadMutes), - ) - let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date() if (Number.isNaN(seenAt.getTime())) { seenAt = new Date() @@ -207,45 +199,3 @@ function getSubjectUri( return notif.reasonSubject } } - -export function isThreadMuted(notif: FeedNotification, threadMutes: string[]) { - // If there's a subject we want to use that. This will always work on the notifications tab - if (notif.subject) { - const record = notif.subject.record as AppBskyFeedPost.Record - // Check for a quote record - if ( - (record.reply && threadMutes.includes(record.reply.root.uri)) || - (notif.subject.uri && threadMutes.includes(notif.subject.uri)) - ) { - return true - } else if ( - AppBskyEmbedRecord.isMain(record.embed) && - threadMutes.includes(record.embed.record.uri) - ) { - return true - } - } else { - // Otherwise we just do the best that we can - const record = notif.notification.record - if (AppBskyFeedPost.isRecord(record)) { - if (record.reply && threadMutes.includes(record.reply.root.uri)) { - // We can always filter replies - return true - } else if ( - AppBskyEmbedRecord.isMain(record.embed) && - threadMutes.includes(record.embed.record.uri) - ) { - // We can also filter quotes if the quoted post is the root - return true - } - } else if ( - AppBskyFeedRepost.isRecord(record) && - threadMutes.includes(record.subject.uri) - ) { - // Finally we can filter reposts, again if the post is the root - return true - } - } - - return false -} diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 794f48eb1..8e77bf6b9 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -8,6 +8,7 @@ import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig' import {updatePostShadow} from '#/state/cache/post-shadow' import {Shadow} from '#/state/cache/types' import {useAgent, useSession} from '#/state/session' +import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes' import {findProfileQueryData} from './profile' const RQKEY_ROOT = 'post' @@ -291,3 +292,72 @@ export function usePostDeleteMutation() { }, }) } + +export function useThreadMuteMutationQueue( + post: Shadow<AppBskyFeedDefs.PostView>, + rootUri: string, +) { + const threadMuteMutation = useThreadMuteMutation() + const threadUnmuteMutation = useThreadUnmuteMutation() + const isThreadMuted = useIsThreadMuted(rootUri, post.viewer?.threadMuted) + const setThreadMute = useSetThreadMute() + + const queueToggle = useToggleMutationQueue<boolean>({ + initialState: isThreadMuted, + runMutation: async (_prev, shouldLike) => { + if (shouldLike) { + await threadMuteMutation.mutateAsync({ + uri: rootUri, + }) + return true + } else { + await threadUnmuteMutation.mutateAsync({ + uri: rootUri, + }) + return false + } + }, + onSuccess(finalIsMuted) { + // finalize + setThreadMute(rootUri, finalIsMuted) + }, + }) + + const queueMuteThread = useCallback(() => { + // optimistically update + setThreadMute(rootUri, true) + return queueToggle(true) + }, [setThreadMute, rootUri, queueToggle]) + + const queueUnmuteThread = useCallback(() => { + // optimistically update + setThreadMute(rootUri, false) + return queueToggle(false) + }, [rootUri, setThreadMute, queueToggle]) + + return [isThreadMuted, queueMuteThread, queueUnmuteThread] as const +} + +function useThreadMuteMutation() { + const agent = useAgent() + return useMutation< + {}, + Error, + {uri: string} // the root post's uri + >({ + mutationFn: ({uri}) => { + logEvent('post:mute', {}) + return agent.api.app.bsky.graph.muteThread({root: uri}) + }, + }) +} + +function useThreadUnmuteMutation() { + const agent = useAgent() + return useMutation<{}, Error, {uri: string}>({ + mutationFn: ({uri}) => { + logEvent('post:unmute', {}) + return agent.api.app.bsky.graph.unmuteThread({root: uri}) + }, + }) +} |