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 objectzio/stable
parent
35e54e24a0
commit
5f5d845053
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
<GestureHandlerRootView style={s.h100pct}>
|
<MutedThreadsProvider>
|
||||||
<TestCtrls />
|
<GestureHandlerRootView style={s.h100pct}>
|
||||||
<Shell />
|
<TestCtrls />
|
||||||
</GestureHandlerRootView>
|
<Shell />
|
||||||
|
</GestureHandlerRootView>
|
||||||
|
</MutedThreadsProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</UnreadNotifsProvider>
|
</UnreadNotifsProvider>
|
||||||
</SelectedFeedProvider>
|
</SelectedFeedProvider>
|
||||||
|
@ -154,21 +156,19 @@ function App() {
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<ShellStateProvider>
|
<ShellStateProvider>
|
||||||
<PrefsStateProvider>
|
<PrefsStateProvider>
|
||||||
<MutedThreadsProvider>
|
<InvitesStateProvider>
|
||||||
<InvitesStateProvider>
|
<ModalStateProvider>
|
||||||
<ModalStateProvider>
|
<DialogStateProvider>
|
||||||
<DialogStateProvider>
|
<LightboxStateProvider>
|
||||||
<LightboxStateProvider>
|
<I18nProvider>
|
||||||
<I18nProvider>
|
<PortalProvider>
|
||||||
<PortalProvider>
|
<InnerApp />
|
||||||
<InnerApp />
|
</PortalProvider>
|
||||||
</PortalProvider>
|
</I18nProvider>
|
||||||
</I18nProvider>
|
</LightboxStateProvider>
|
||||||
</LightboxStateProvider>
|
</DialogStateProvider>
|
||||||
</DialogStateProvider>
|
</ModalStateProvider>
|
||||||
</ModalStateProvider>
|
</InvitesStateProvider>
|
||||||
</InvitesStateProvider>
|
|
||||||
</MutedThreadsProvider>
|
|
||||||
</PrefsStateProvider>
|
</PrefsStateProvider>
|
||||||
</ShellStateProvider>
|
</ShellStateProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
|
|
|
@ -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>
|
||||||
<SafeAreaProvider>
|
<MutedThreadsProvider>
|
||||||
<Shell />
|
<SafeAreaProvider>
|
||||||
</SafeAreaProvider>
|
<Shell />
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</MutedThreadsProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</UnreadNotifsProvider>
|
</UnreadNotifsProvider>
|
||||||
</SelectedFeedProvider>
|
</SelectedFeedProvider>
|
||||||
|
@ -136,21 +138,19 @@ function App() {
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<ShellStateProvider>
|
<ShellStateProvider>
|
||||||
<PrefsStateProvider>
|
<PrefsStateProvider>
|
||||||
<MutedThreadsProvider>
|
<InvitesStateProvider>
|
||||||
<InvitesStateProvider>
|
<ModalStateProvider>
|
||||||
<ModalStateProvider>
|
<DialogStateProvider>
|
||||||
<DialogStateProvider>
|
<LightboxStateProvider>
|
||||||
<LightboxStateProvider>
|
<I18nProvider>
|
||||||
<I18nProvider>
|
<PortalProvider>
|
||||||
<PortalProvider>
|
<InnerApp />
|
||||||
<InnerApp />
|
</PortalProvider>
|
||||||
</PortalProvider>
|
</I18nProvider>
|
||||||
</I18nProvider>
|
</LightboxStateProvider>
|
||||||
</LightboxStateProvider>
|
</DialogStateProvider>
|
||||||
</DialogStateProvider>
|
</ModalStateProvider>
|
||||||
</ModalStateProvider>
|
</InvitesStateProvider>
|
||||||
</InvitesStateProvider>
|
|
||||||
</MutedThreadsProvider>
|
|
||||||
</PrefsStateProvider>
|
</PrefsStateProvider>
|
||||||
</ShellStateProvider>
|
</ShellStateProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
logger.error('Failed to toggle thread mute', {message: e})
|
if (e?.name !== 'AbortError') {
|
||||||
|
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)
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue