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
zio/stable
Samuel Newman 2024-06-18 19:48:34 +01:00 committed by GitHub
parent 35e54e24a0
commit 5f5d845053
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 223 additions and 220 deletions

View File

@ -49,7 +49,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.12.18", "@atproto/api": "^0.12.19",
"@bam.tech/react-native-image-resizer": "^3.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",

View File

@ -14,40 +14,40 @@ import * as SplashScreen from 'expo-splash-screen'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
import {QueryProvider} from '#/lib/react-query'
import { import {
initialize, initialize,
Provider as StatsigProvider, Provider as StatsigProvider,
tryFetchGates, tryFetchGates,
} from '#/lib/statsig/statsig' } from '#/lib/statsig/statsig'
import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' import {MessagesProvider} from '#/state/messages'
import {Provider as ModalStateProvider} from '#/state/modals'
import {init as initPersistedState} from '#/state/persisted' import {init as initPersistedState} from '#/state/persisted'
import {Provider as PrefsStateProvider} from '#/state/preferences'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
import {readLastActiveAccount} from '#/state/session/util' import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {QueryProvider} from 'lib/react-query'
import {s} from 'lib/styles'
import {ThemeProvider} from 'lib/ThemeContext'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import { import {
Provider as SessionProvider, Provider as SessionProvider,
SessionAccount, SessionAccount,
useSession, useSession,
useSessionApi, useSessionApi,
} from 'state/session' } from '#/state/session'
import {Provider as ShellStateProvider} from 'state/shell' import {readLastActiveAccount} from '#/state/session/util'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' import {Provider as ShellStateProvider} from '#/state/shell'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {TestCtrls} from 'view/com/testing/TestCtrls' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import * as Toast from 'view/com/util/Toast' import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Shell} from 'view/shell' import * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
@ -112,10 +112,12 @@ function InnerApp() {
<SelectedFeedProvider> <SelectedFeedProvider>
<UnreadNotifsProvider> <UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider> <BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<GestureHandlerRootView style={s.h100pct}> <GestureHandlerRootView style={s.h100pct}>
<TestCtrls /> <TestCtrls />
<Shell /> <Shell />
</GestureHandlerRootView> </GestureHandlerRootView>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider> </BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider> </UnreadNotifsProvider>
</SelectedFeedProvider> </SelectedFeedProvider>
@ -154,7 +156,6 @@ function App() {
<SessionProvider> <SessionProvider>
<ShellStateProvider> <ShellStateProvider>
<PrefsStateProvider> <PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider> <InvitesStateProvider>
<ModalStateProvider> <ModalStateProvider>
<DialogStateProvider> <DialogStateProvider>
@ -168,7 +169,6 @@ function App() {
</DialogStateProvider> </DialogStateProvider>
</ModalStateProvider> </ModalStateProvider>
</InvitesStateProvider> </InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider> </PrefsStateProvider>
</ShellStateProvider> </ShellStateProvider>
</SessionProvider> </SessionProvider>

View File

@ -8,35 +8,35 @@ import {SafeAreaProvider} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
import {QueryProvider} from '#/lib/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {ThemeProvider} from '#/lib/ThemeContext'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' import {MessagesProvider} from '#/state/messages'
import {Provider as ModalStateProvider} from '#/state/modals'
import {init as initPersistedState} from '#/state/persisted' import {init as initPersistedState} from '#/state/persisted'
import {Provider as PrefsStateProvider} from '#/state/preferences'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
import {readLastActiveAccount} from '#/state/session/util' import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {QueryProvider} from 'lib/react-query'
import {ThemeProvider} from 'lib/ThemeContext'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import { import {
Provider as SessionProvider, Provider as SessionProvider,
SessionAccount, SessionAccount,
useSession, useSession,
useSessionApi, useSessionApi,
} from 'state/session' } from '#/state/session'
import {Provider as ShellStateProvider} from 'state/shell' import {readLastActiveAccount} from '#/state/session/util'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' import {Provider as ShellStateProvider} from '#/state/shell'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import * as Toast from 'view/com/util/Toast' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {ToastContainer} from 'view/com/util/Toast.web' import * as Toast from '#/view/com/util/Toast'
import {Shell} from 'view/shell/index' import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
@ -96,9 +96,11 @@ function InnerApp() {
<SelectedFeedProvider> <SelectedFeedProvider>
<UnreadNotifsProvider> <UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider> <BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider> <SafeAreaProvider>
<Shell /> <Shell />
</SafeAreaProvider> </SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider> </BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider> </UnreadNotifsProvider>
</SelectedFeedProvider> </SelectedFeedProvider>
@ -136,7 +138,6 @@ function App() {
<SessionProvider> <SessionProvider>
<ShellStateProvider> <ShellStateProvider>
<PrefsStateProvider> <PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider> <InvitesStateProvider>
<ModalStateProvider> <ModalStateProvider>
<DialogStateProvider> <DialogStateProvider>
@ -150,7 +151,6 @@ function App() {
</DialogStateProvider> </DialogStateProvider>
</ModalStateProvider> </ModalStateProvider>
</InvitesStateProvider> </InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider> </PrefsStateProvider>
</ShellStateProvider> </ShellStateProvider>
</SessionProvider> </SessionProvider>

View File

@ -103,6 +103,8 @@ export type LogEvents = {
'post:unrepost': { 'post:unrepost': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
} }
'post:mute': {}
'post:unmute': {}
'profile:follow': { 'profile:follow': {
didBecomeMutual: boolean | undefined didBecomeMutual: boolean | undefined
followeeClout: number | undefined followeeClout: number | undefined

View File

@ -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)
}

View File

@ -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)
}

View File

@ -26,7 +26,6 @@ import {
useQueryClient, useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {useMutedThreads} from '#/state/muted-threads'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {useModerationOpts} from '../../preferences/moderation-opts' import {useModerationOpts} from '../../preferences/moderation-opts'
import {STALE} from '..' import {STALE} from '..'
@ -54,7 +53,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const agent = useAgent() const agent = useAgent()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads()
const unreads = useUnreadNotificationsApi() const unreads = useUnreadNotificationsApi()
const enabled = opts?.enabled !== false const enabled = opts?.enabled !== false
const lastPageCountRef = useRef(0) const lastPageCountRef = useRef(0)
@ -82,7 +80,6 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
cursor: pageParam, cursor: pageParam,
queryClient, queryClient,
moderationOpts, moderationOpts,
threadMutes,
fetchAdditionalData: true, fetchAdditionalData: true,
}) })
).page ).page

View File

@ -9,7 +9,6 @@ import EventEmitter from 'eventemitter3'
import BroadcastChannel from '#/lib/broadcast' import BroadcastChannel from '#/lib/broadcast'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useMutedThreads} from '#/state/muted-threads'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {resetBadgeCount} from 'lib/notifications/notifications' import {resetBadgeCount} from 'lib/notifications/notifications'
import {useModerationOpts} from '../../preferences/moderation-opts' import {useModerationOpts} from '../../preferences/moderation-opts'
@ -48,7 +47,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const agent = useAgent() const agent = useAgent()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads()
const [numUnread, setNumUnread] = React.useState('') const [numUnread, setNumUnread] = React.useState('')
@ -147,7 +145,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
limit: 40, limit: 40,
queryClient, queryClient,
moderationOpts, moderationOpts,
threadMutes,
// only fetch subjects when the page is going to be used // only fetch subjects when the page is going to be used
// in the notifications query, otherwise skip it // 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 checkUnreadRef.current = api.checkUnread
return ( return (

View File

@ -1,5 +1,4 @@
import { import {
AppBskyEmbedRecord,
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedLike, AppBskyFeedLike,
AppBskyFeedPost, AppBskyFeedPost,
@ -28,7 +27,6 @@ export async function fetchPage({
limit, limit,
queryClient, queryClient,
moderationOpts, moderationOpts,
threadMutes,
fetchAdditionalData, fetchAdditionalData,
}: { }: {
agent: BskyAgent agent: BskyAgent
@ -36,7 +34,6 @@ export async function fetchPage({
limit: number limit: number
queryClient: QueryClient queryClient: QueryClient
moderationOpts: ModerationOpts | undefined moderationOpts: ModerationOpts | undefined
threadMutes: string[]
fetchAdditionalData: boolean fetchAdditionalData: boolean
}): Promise<{page: FeedPage; indexedAt: string | undefined}> { }): Promise<{page: FeedPage; indexedAt: string | undefined}> {
const res = await agent.listNotifications({ 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() let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date()
if (Number.isNaN(seenAt.getTime())) { if (Number.isNaN(seenAt.getTime())) {
seenAt = new Date() seenAt = new Date()
@ -207,45 +199,3 @@ function getSubjectUri(
return notif.reasonSubject 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
}

View File

@ -8,6 +8,7 @@ import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {updatePostShadow} from '#/state/cache/post-shadow' import {updatePostShadow} from '#/state/cache/post-shadow'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
import {findProfileQueryData} from './profile' import {findProfileQueryData} from './profile'
const RQKEY_ROOT = 'post' 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})
},
})
}

View File

@ -7,7 +7,7 @@ import {
} from 'react-native' } from 'react-native'
import * as Clipboard from 'expo-clipboard' import * as Clipboard from 'expo-clipboard'
import { import {
AppBskyActorDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
RichText as RichTextAPI, RichText as RichTextAPI,
@ -22,12 +22,15 @@ import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {getTranslatorLink} from '#/locale/helpers' import {getTranslatorLink} from '#/locale/helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {Shadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences' import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
import {useOpenLink} from '#/state/preferences/in-app-browser' import {useOpenLink} from '#/state/preferences/in-app-browser'
import {usePostDeleteMutation} from '#/state/queries/post' import {
usePostDeleteMutation,
useThreadMuteMutationQueue,
} from '#/state/queries/post'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {getCurrentRoute} from 'lib/routes/helpers' import {getCurrentRoute} from 'lib/routes/helpers'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
@ -62,9 +65,7 @@ import * as Toast from '../Toast'
let PostDropdownBtn = ({ let PostDropdownBtn = ({
testID, testID,
postAuthor, post,
postCid,
postUri,
postFeedContext, postFeedContext,
record, record,
richText, richText,
@ -74,9 +75,7 @@ let PostDropdownBtn = ({
timestamp, timestamp,
}: { }: {
testID: string testID: string
postAuthor: AppBskyActorDefs.ProfileViewBasic post: Shadow<AppBskyFeedDefs.PostView>
postCid: string
postUri: string
postFeedContext: string | undefined postFeedContext: string | undefined
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
richText: RichTextAPI richText: RichTextAPI
@ -92,8 +91,6 @@ let PostDropdownBtn = ({
const {_} = useLingui() const {_} = useLingui()
const defaultCtrlColor = theme.palette.default.postCtrl const defaultCtrlColor = theme.palette.default.postCtrl
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const mutedThreads = useMutedThreads()
const toggleThreadMute = useToggleThreadMute()
const postDeleteMutation = usePostDeleteMutation() const postDeleteMutation = usePostDeleteMutation()
const hiddenPosts = useHiddenPosts() const hiddenPosts = useHiddenPosts()
const {hidePost} = useHiddenPostsApi() const {hidePost} = useHiddenPostsApi()
@ -107,9 +104,15 @@ let PostDropdownBtn = ({
const loggedOutWarningPromptControl = useDialogControl() const loggedOutWarningPromptControl = useDialogControl()
const embedPostControl = useDialogControl() const embedPostControl = useDialogControl()
const sendViaChatControl = useDialogControl() const sendViaChatControl = useDialogControl()
const postUri = post.uri
const postCid = post.cid
const postAuthor = post.author
const rootUri = record.reply?.root?.uri || postUri const rootUri = record.reply?.root?.uri || postUri
const isThreadMuted = mutedThreads.includes(rootUri) const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue(
post,
rootUri,
)
const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
const isAuthor = postAuthor.did === currentAccount?.did const isAuthor = postAuthor.did === currentAccount?.did
@ -162,18 +165,22 @@ let PostDropdownBtn = ({
const onToggleThreadMute = React.useCallback(() => { const onToggleThreadMute = React.useCallback(() => {
try { try {
const muted = toggleThreadMute(rootUri) if (isThreadMuted) {
if (muted) { unmuteThread()
Toast.show(_(msg`You will now receive notifications for this thread`))
} else {
muteThread()
Toast.show( Toast.show(
_(msg`You will no longer receive notifications for this thread`), _(msg`You will no longer receive notifications for this thread`),
) )
} else {
Toast.show(_(msg`You will now receive notifications for this thread`))
} }
} catch (e) { } catch (e: any) {
if (e?.name !== 'AbortError') {
logger.error('Failed to toggle thread mute', {message: e}) logger.error('Failed to toggle thread mute', {message: e})
Toast.show(_(msg`Failed to toggle thread mute, please try again`))
} }
}, [rootUri, toggleThreadMute, _]) }
}, [isThreadMuted, unmuteThread, _, muteThread])
const onCopyPostText = React.useCallback(() => { const onCopyPostText = React.useCallback(() => {
const str = richTextToString(richText, true) const str = richTextToString(richText, true)

View File

@ -319,9 +319,7 @@ let PostCtrls = ({
<View style={big ? a.align_center : [a.flex_1, a.align_start]}> <View style={big ? a.align_center : [a.flex_1, a.align_start]}>
<PostDropdownBtn <PostDropdownBtn
testID="postDropdownBtn" testID="postDropdownBtn"
postAuthor={post.author} post={post}
postCid={post.cid}
postUri={post.uri}
postFeedContext={feedContext} postFeedContext={feedContext}
record={record} record={record}
richText={richText} richText={richText}

View File

@ -34,10 +34,10 @@
jsonpointer "^5.0.0" jsonpointer "^5.0.0"
leven "^3.1.0" leven "^3.1.0"
"@atproto/api@^0.12.18": "@atproto/api@^0.12.19":
version "0.12.18" version "0.12.19"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.18.tgz#490a6f22966a3b605c22154fe7befc78bf640821" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.19.tgz#6d842269b6b9cd3fc5864e12824d4fb04cc033cf"
integrity sha512-Ii3J/uzmyw1qgnfhnvAsmuXa8ObRSCHelsF8TmQrgMWeXCbfypeS/VESm++1Z9+xHK7bHPOwSek3RmWB0cqEbQ== integrity sha512-dsiTpjqBhjGwNW/qG/tLSgUQnmOSvd8hsQr5d8GCUDGK2AEHWl0KNgLPbwxIBEIo8Jg9NHsvqV7BMoix8YreIg==
dependencies: dependencies:
"@atproto/common-web" "^0.3.0" "@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0" "@atproto/lexicon" "^0.4.0"