An ATproto social media client -- with an independent Appview.
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Samuel Newman and committed by
GitHub
5f5d8450 35e54e24

+223 -220
+1 -1
package.json
··· 49 49 "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" 50 50 }, 51 51 "dependencies": { 52 - "@atproto/api": "^0.12.18", 52 + "@atproto/api": "^0.12.19", 53 53 "@bam.tech/react-native-image-resizer": "^3.0.4", 54 54 "@braintree/sanitize-url": "^6.0.2", 55 55 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+38 -38
src/App.native.tsx
··· 14 14 import {msg} from '@lingui/macro' 15 15 import {useLingui} from '@lingui/react' 16 16 17 + import {useIntentHandler} from '#/lib/hooks/useIntentHandler' 18 + import {QueryProvider} from '#/lib/react-query' 17 19 import { 18 20 initialize, 19 21 Provider as StatsigProvider, 20 22 tryFetchGates, 21 23 } from '#/lib/statsig/statsig' 24 + import {s} from '#/lib/styles' 25 + import {ThemeProvider} from '#/lib/ThemeContext' 22 26 import {logger} from '#/logger' 27 + import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' 28 + import {Provider as DialogStateProvider} from '#/state/dialogs' 29 + import {Provider as InvitesStateProvider} from '#/state/invites' 30 + import {Provider as LightboxStateProvider} from '#/state/lightbox' 23 31 import {MessagesProvider} from '#/state/messages' 32 + import {Provider as ModalStateProvider} from '#/state/modals' 24 33 import {init as initPersistedState} from '#/state/persisted' 34 + import {Provider as PrefsStateProvider} from '#/state/preferences' 25 35 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 26 36 import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' 27 - import {readLastActiveAccount} from '#/state/session/util' 28 - import {useIntentHandler} from 'lib/hooks/useIntentHandler' 29 - import {QueryProvider} from 'lib/react-query' 30 - import {s} from 'lib/styles' 31 - import {ThemeProvider} from 'lib/ThemeContext' 32 - import {Provider as DialogStateProvider} from 'state/dialogs' 33 - import {Provider as InvitesStateProvider} from 'state/invites' 34 - import {Provider as LightboxStateProvider} from 'state/lightbox' 35 - import {Provider as ModalStateProvider} from 'state/modals' 36 - import {Provider as MutedThreadsProvider} from 'state/muted-threads' 37 - import {Provider as PrefsStateProvider} from 'state/preferences' 38 - import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' 37 + import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' 39 38 import { 40 39 Provider as SessionProvider, 41 40 SessionAccount, 42 41 useSession, 43 42 useSessionApi, 44 - } from 'state/session' 45 - import {Provider as ShellStateProvider} from 'state/shell' 46 - import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' 47 - import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed' 48 - import {TestCtrls} from 'view/com/testing/TestCtrls' 49 - import * as Toast from 'view/com/util/Toast' 50 - import {Shell} from 'view/shell' 43 + } from '#/state/session' 44 + import {readLastActiveAccount} from '#/state/session/util' 45 + import {Provider as ShellStateProvider} from '#/state/shell' 46 + import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' 47 + import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' 48 + import {TestCtrls} from '#/view/com/testing/TestCtrls' 49 + import * as Toast from '#/view/com/util/Toast' 50 + import {Shell} from '#/view/shell' 51 51 import {ThemeProvider as Alf} from '#/alf' 52 52 import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 53 53 import {Provider as PortalProvider} from '#/components/Portal' ··· 112 112 <SelectedFeedProvider> 113 113 <UnreadNotifsProvider> 114 114 <BackgroundNotificationPreferencesProvider> 115 - <GestureHandlerRootView style={s.h100pct}> 116 - <TestCtrls /> 117 - <Shell /> 118 - </GestureHandlerRootView> 115 + <MutedThreadsProvider> 116 + <GestureHandlerRootView style={s.h100pct}> 117 + <TestCtrls /> 118 + <Shell /> 119 + </GestureHandlerRootView> 120 + </MutedThreadsProvider> 119 121 </BackgroundNotificationPreferencesProvider> 120 122 </UnreadNotifsProvider> 121 123 </SelectedFeedProvider> ··· 154 156 <SessionProvider> 155 157 <ShellStateProvider> 156 158 <PrefsStateProvider> 157 - <MutedThreadsProvider> 158 - <InvitesStateProvider> 159 - <ModalStateProvider> 160 - <DialogStateProvider> 161 - <LightboxStateProvider> 162 - <I18nProvider> 163 - <PortalProvider> 164 - <InnerApp /> 165 - </PortalProvider> 166 - </I18nProvider> 167 - </LightboxStateProvider> 168 - </DialogStateProvider> 169 - </ModalStateProvider> 170 - </InvitesStateProvider> 171 - </MutedThreadsProvider> 159 + <InvitesStateProvider> 160 + <ModalStateProvider> 161 + <DialogStateProvider> 162 + <LightboxStateProvider> 163 + <I18nProvider> 164 + <PortalProvider> 165 + <InnerApp /> 166 + </PortalProvider> 167 + </I18nProvider> 168 + </LightboxStateProvider> 169 + </DialogStateProvider> 170 + </ModalStateProvider> 171 + </InvitesStateProvider> 172 172 </PrefsStateProvider> 173 173 </ShellStateProvider> 174 174 </SessionProvider>
+36 -36
src/App.web.tsx
··· 8 8 import {msg} from '@lingui/macro' 9 9 import {useLingui} from '@lingui/react' 10 10 11 + import {useIntentHandler} from '#/lib/hooks/useIntentHandler' 12 + import {QueryProvider} from '#/lib/react-query' 11 13 import {Provider as StatsigProvider} from '#/lib/statsig/statsig' 14 + import {ThemeProvider} from '#/lib/ThemeContext' 12 15 import {logger} from '#/logger' 16 + import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' 17 + import {Provider as DialogStateProvider} from '#/state/dialogs' 18 + import {Provider as InvitesStateProvider} from '#/state/invites' 19 + import {Provider as LightboxStateProvider} from '#/state/lightbox' 13 20 import {MessagesProvider} from '#/state/messages' 21 + import {Provider as ModalStateProvider} from '#/state/modals' 14 22 import {init as initPersistedState} from '#/state/persisted' 23 + import {Provider as PrefsStateProvider} from '#/state/preferences' 15 24 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 16 25 import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' 17 - import {readLastActiveAccount} from '#/state/session/util' 18 - import {useIntentHandler} from 'lib/hooks/useIntentHandler' 19 - import {QueryProvider} from 'lib/react-query' 20 - import {ThemeProvider} from 'lib/ThemeContext' 21 - import {Provider as DialogStateProvider} from 'state/dialogs' 22 - import {Provider as InvitesStateProvider} from 'state/invites' 23 - import {Provider as LightboxStateProvider} from 'state/lightbox' 24 - import {Provider as ModalStateProvider} from 'state/modals' 25 - import {Provider as MutedThreadsProvider} from 'state/muted-threads' 26 - import {Provider as PrefsStateProvider} from 'state/preferences' 27 - import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' 26 + import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' 28 27 import { 29 28 Provider as SessionProvider, 30 29 SessionAccount, 31 30 useSession, 32 31 useSessionApi, 33 - } from 'state/session' 34 - import {Provider as ShellStateProvider} from 'state/shell' 35 - import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out' 36 - import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed' 37 - import * as Toast from 'view/com/util/Toast' 38 - import {ToastContainer} from 'view/com/util/Toast.web' 39 - import {Shell} from 'view/shell/index' 32 + } from '#/state/session' 33 + import {readLastActiveAccount} from '#/state/session/util' 34 + import {Provider as ShellStateProvider} from '#/state/shell' 35 + import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' 36 + import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' 37 + import * as Toast from '#/view/com/util/Toast' 38 + import {ToastContainer} from '#/view/com/util/Toast.web' 39 + import {Shell} from '#/view/shell/index' 40 40 import {ThemeProvider as Alf} from '#/alf' 41 41 import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 42 42 import {Provider as PortalProvider} from '#/components/Portal' ··· 96 96 <SelectedFeedProvider> 97 97 <UnreadNotifsProvider> 98 98 <BackgroundNotificationPreferencesProvider> 99 - <SafeAreaProvider> 100 - <Shell /> 101 - </SafeAreaProvider> 99 + <MutedThreadsProvider> 100 + <SafeAreaProvider> 101 + <Shell /> 102 + </SafeAreaProvider> 103 + </MutedThreadsProvider> 102 104 </BackgroundNotificationPreferencesProvider> 103 105 </UnreadNotifsProvider> 104 106 </SelectedFeedProvider> ··· 136 138 <SessionProvider> 137 139 <ShellStateProvider> 138 140 <PrefsStateProvider> 139 - <MutedThreadsProvider> 140 - <InvitesStateProvider> 141 - <ModalStateProvider> 142 - <DialogStateProvider> 143 - <LightboxStateProvider> 144 - <I18nProvider> 145 - <PortalProvider> 146 - <InnerApp /> 147 - </PortalProvider> 148 - </I18nProvider> 149 - </LightboxStateProvider> 150 - </DialogStateProvider> 151 - </ModalStateProvider> 152 - </InvitesStateProvider> 153 - </MutedThreadsProvider> 141 + <InvitesStateProvider> 142 + <ModalStateProvider> 143 + <DialogStateProvider> 144 + <LightboxStateProvider> 145 + <I18nProvider> 146 + <PortalProvider> 147 + <InnerApp /> 148 + </PortalProvider> 149 + </I18nProvider> 150 + </LightboxStateProvider> 151 + </DialogStateProvider> 152 + </ModalStateProvider> 153 + </InvitesStateProvider> 154 154 </PrefsStateProvider> 155 155 </ShellStateProvider> 156 156 </SessionProvider>
+2
src/lib/statsig/events.ts
··· 103 103 'post:unrepost': { 104 104 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 105 105 } 106 + 'post:mute': {} 107 + 'post:unmute': {} 106 108 'profile:follow': { 107 109 didBecomeMutual: boolean | undefined 108 110 followeeClout: number | undefined
+44
src/state/cache/thread-mutes.tsx
··· 1 + import React from 'react' 2 + 3 + type StateContext = Map<string, boolean> 4 + type SetStateContext = (uri: string, value: boolean) => void 5 + 6 + const stateContext = React.createContext<StateContext>(new Map()) 7 + const setStateContext = React.createContext<SetStateContext>( 8 + (_: string) => false, 9 + ) 10 + 11 + export function Provider({children}: React.PropsWithChildren<{}>) { 12 + const [state, setState] = React.useState<StateContext>(() => new Map()) 13 + 14 + const setThreadMute = React.useCallback( 15 + (uri: string, value: boolean) => { 16 + setState(prev => { 17 + const next = new Map(prev) 18 + next.set(uri, value) 19 + return next 20 + }) 21 + }, 22 + [setState], 23 + ) 24 + return ( 25 + <stateContext.Provider value={state}> 26 + <setStateContext.Provider value={setThreadMute}> 27 + {children} 28 + </setStateContext.Provider> 29 + </stateContext.Provider> 30 + ) 31 + } 32 + 33 + export function useMutedThreads() { 34 + return React.useContext(stateContext) 35 + } 36 + 37 + export function useIsThreadMuted(uri: string, defaultValue = false) { 38 + const state = React.useContext(stateContext) 39 + return state.get(uri) ?? defaultValue 40 + } 41 + 42 + export function useSetThreadMute() { 43 + return React.useContext(setStateContext) 44 + }
-62
src/state/muted-threads.tsx
··· 1 - import React from 'react' 2 - import * as persisted from '#/state/persisted' 3 - import {track} from '#/lib/analytics/analytics' 4 - 5 - type StateContext = persisted.Schema['mutedThreads'] 6 - type ToggleContext = (uri: string) => boolean 7 - 8 - const stateContext = React.createContext<StateContext>( 9 - persisted.defaults.mutedThreads, 10 - ) 11 - const toggleContext = React.createContext<ToggleContext>((_: string) => false) 12 - 13 - export function Provider({children}: React.PropsWithChildren<{}>) { 14 - const [state, setState] = React.useState(persisted.get('mutedThreads')) 15 - 16 - const toggleThreadMute = React.useCallback( 17 - (uri: string) => { 18 - let muted = false 19 - setState((arr: string[]) => { 20 - if (arr.includes(uri)) { 21 - arr = arr.filter(v => v !== uri) 22 - muted = false 23 - track('Post:ThreadUnmute') 24 - } else { 25 - arr = arr.concat([uri]) 26 - muted = true 27 - track('Post:ThreadMute') 28 - } 29 - persisted.write('mutedThreads', arr) 30 - return arr 31 - }) 32 - return muted 33 - }, 34 - [setState], 35 - ) 36 - 37 - React.useEffect(() => { 38 - return persisted.onUpdate(() => { 39 - setState(persisted.get('mutedThreads')) 40 - }) 41 - }, [setState]) 42 - 43 - return ( 44 - <stateContext.Provider value={state}> 45 - <toggleContext.Provider value={toggleThreadMute}> 46 - {children} 47 - </toggleContext.Provider> 48 - </stateContext.Provider> 49 - ) 50 - } 51 - 52 - export function useMutedThreads() { 53 - return React.useContext(stateContext) 54 - } 55 - 56 - export function useToggleThreadMute() { 57 - return React.useContext(toggleContext) 58 - } 59 - 60 - export function isThreadMuted(uri: string) { 61 - return persisted.get('mutedThreads').includes(uri) 62 - }
-3
src/state/queries/notifications/feed.ts
··· 26 26 useQueryClient, 27 27 } from '@tanstack/react-query' 28 28 29 - import {useMutedThreads} from '#/state/muted-threads' 30 29 import {useAgent} from '#/state/session' 31 30 import {useModerationOpts} from '../../preferences/moderation-opts' 32 31 import {STALE} from '..' ··· 54 53 const agent = useAgent() 55 54 const queryClient = useQueryClient() 56 55 const moderationOpts = useModerationOpts() 57 - const threadMutes = useMutedThreads() 58 56 const unreads = useUnreadNotificationsApi() 59 57 const enabled = opts?.enabled !== false 60 58 const lastPageCountRef = useRef(0) ··· 82 80 cursor: pageParam, 83 81 queryClient, 84 82 moderationOpts, 85 - threadMutes, 86 83 fetchAdditionalData: true, 87 84 }) 88 85 ).page
+1 -4
src/state/queries/notifications/unread.tsx
··· 9 9 10 10 import BroadcastChannel from '#/lib/broadcast' 11 11 import {logger} from '#/logger' 12 - import {useMutedThreads} from '#/state/muted-threads' 13 12 import {useAgent, useSession} from '#/state/session' 14 13 import {resetBadgeCount} from 'lib/notifications/notifications' 15 14 import {useModerationOpts} from '../../preferences/moderation-opts' ··· 48 47 const agent = useAgent() 49 48 const queryClient = useQueryClient() 50 49 const moderationOpts = useModerationOpts() 51 - const threadMutes = useMutedThreads() 52 50 53 51 const [numUnread, setNumUnread] = React.useState('') 54 52 ··· 147 145 limit: 40, 148 146 queryClient, 149 147 moderationOpts, 150 - threadMutes, 151 148 152 149 // only fetch subjects when the page is going to be used 153 150 // in the notifications query, otherwise skip it ··· 192 189 } 193 190 }, 194 191 } 195 - }, [setNumUnread, queryClient, moderationOpts, threadMutes, agent]) 192 + }, [setNumUnread, queryClient, moderationOpts, agent]) 196 193 checkUnreadRef.current = api.checkUnread 197 194 198 195 return (
-50
src/state/queries/notifications/util.ts
··· 1 1 import { 2 - AppBskyEmbedRecord, 3 2 AppBskyFeedDefs, 4 3 AppBskyFeedLike, 5 4 AppBskyFeedPost, ··· 28 27 limit, 29 28 queryClient, 30 29 moderationOpts, 31 - threadMutes, 32 30 fetchAdditionalData, 33 31 }: { 34 32 agent: BskyAgent ··· 36 34 limit: number 37 35 queryClient: QueryClient 38 36 moderationOpts: ModerationOpts | undefined 39 - threadMutes: string[] 40 37 fetchAdditionalData: boolean 41 38 }): Promise<{page: FeedPage; indexedAt: string | undefined}> { 42 39 const res = await agent.listNotifications({ ··· 66 63 } 67 64 } 68 65 } 69 - 70 - // apply thread muting 71 - notifsGrouped = notifsGrouped.filter( 72 - notif => !isThreadMuted(notif, threadMutes), 73 - ) 74 66 75 67 let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date() 76 68 if (Number.isNaN(seenAt.getTime())) { ··· 207 199 return notif.reasonSubject 208 200 } 209 201 } 210 - 211 - export function isThreadMuted(notif: FeedNotification, threadMutes: string[]) { 212 - // If there's a subject we want to use that. This will always work on the notifications tab 213 - if (notif.subject) { 214 - const record = notif.subject.record as AppBskyFeedPost.Record 215 - // Check for a quote record 216 - if ( 217 - (record.reply && threadMutes.includes(record.reply.root.uri)) || 218 - (notif.subject.uri && threadMutes.includes(notif.subject.uri)) 219 - ) { 220 - return true 221 - } else if ( 222 - AppBskyEmbedRecord.isMain(record.embed) && 223 - threadMutes.includes(record.embed.record.uri) 224 - ) { 225 - return true 226 - } 227 - } else { 228 - // Otherwise we just do the best that we can 229 - const record = notif.notification.record 230 - if (AppBskyFeedPost.isRecord(record)) { 231 - if (record.reply && threadMutes.includes(record.reply.root.uri)) { 232 - // We can always filter replies 233 - return true 234 - } else if ( 235 - AppBskyEmbedRecord.isMain(record.embed) && 236 - threadMutes.includes(record.embed.record.uri) 237 - ) { 238 - // We can also filter quotes if the quoted post is the root 239 - return true 240 - } 241 - } else if ( 242 - AppBskyFeedRepost.isRecord(record) && 243 - threadMutes.includes(record.subject.uri) 244 - ) { 245 - // Finally we can filter reposts, again if the post is the root 246 - return true 247 - } 248 - } 249 - 250 - return false 251 - }
+70
src/state/queries/post.ts
··· 8 8 import {updatePostShadow} from '#/state/cache/post-shadow' 9 9 import {Shadow} from '#/state/cache/types' 10 10 import {useAgent, useSession} from '#/state/session' 11 + import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes' 11 12 import {findProfileQueryData} from './profile' 12 13 13 14 const RQKEY_ROOT = 'post' ··· 291 292 }, 292 293 }) 293 294 } 295 + 296 + export function useThreadMuteMutationQueue( 297 + post: Shadow<AppBskyFeedDefs.PostView>, 298 + rootUri: string, 299 + ) { 300 + const threadMuteMutation = useThreadMuteMutation() 301 + const threadUnmuteMutation = useThreadUnmuteMutation() 302 + const isThreadMuted = useIsThreadMuted(rootUri, post.viewer?.threadMuted) 303 + const setThreadMute = useSetThreadMute() 304 + 305 + const queueToggle = useToggleMutationQueue<boolean>({ 306 + initialState: isThreadMuted, 307 + runMutation: async (_prev, shouldLike) => { 308 + if (shouldLike) { 309 + await threadMuteMutation.mutateAsync({ 310 + uri: rootUri, 311 + }) 312 + return true 313 + } else { 314 + await threadUnmuteMutation.mutateAsync({ 315 + uri: rootUri, 316 + }) 317 + return false 318 + } 319 + }, 320 + onSuccess(finalIsMuted) { 321 + // finalize 322 + setThreadMute(rootUri, finalIsMuted) 323 + }, 324 + }) 325 + 326 + const queueMuteThread = useCallback(() => { 327 + // optimistically update 328 + setThreadMute(rootUri, true) 329 + return queueToggle(true) 330 + }, [setThreadMute, rootUri, queueToggle]) 331 + 332 + const queueUnmuteThread = useCallback(() => { 333 + // optimistically update 334 + setThreadMute(rootUri, false) 335 + return queueToggle(false) 336 + }, [rootUri, setThreadMute, queueToggle]) 337 + 338 + return [isThreadMuted, queueMuteThread, queueUnmuteThread] as const 339 + } 340 + 341 + function useThreadMuteMutation() { 342 + const agent = useAgent() 343 + return useMutation< 344 + {}, 345 + Error, 346 + {uri: string} // the root post's uri 347 + >({ 348 + mutationFn: ({uri}) => { 349 + logEvent('post:mute', {}) 350 + return agent.api.app.bsky.graph.muteThread({root: uri}) 351 + }, 352 + }) 353 + } 354 + 355 + function useThreadUnmuteMutation() { 356 + const agent = useAgent() 357 + return useMutation<{}, Error, {uri: string}>({ 358 + mutationFn: ({uri}) => { 359 + logEvent('post:unmute', {}) 360 + return agent.api.app.bsky.graph.unmuteThread({root: uri}) 361 + }, 362 + }) 363 + }
+26 -19
src/view/com/util/forms/PostDropdownBtn.tsx
··· 7 7 } from 'react-native' 8 8 import * as Clipboard from 'expo-clipboard' 9 9 import { 10 - AppBskyActorDefs, 10 + AppBskyFeedDefs, 11 11 AppBskyFeedPost, 12 12 AtUri, 13 13 RichText as RichTextAPI, ··· 22 22 import {getTranslatorLink} from '#/locale/helpers' 23 23 import {logger} from '#/logger' 24 24 import {isWeb} from '#/platform/detection' 25 + import {Shadow} from '#/state/cache/post-shadow' 25 26 import {useFeedFeedbackContext} from '#/state/feed-feedback' 26 - import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' 27 27 import {useLanguagePrefs} from '#/state/preferences' 28 28 import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences' 29 29 import {useOpenLink} from '#/state/preferences/in-app-browser' 30 - import {usePostDeleteMutation} from '#/state/queries/post' 30 + import { 31 + usePostDeleteMutation, 32 + useThreadMuteMutationQueue, 33 + } from '#/state/queries/post' 31 34 import {useSession} from '#/state/session' 32 35 import {getCurrentRoute} from 'lib/routes/helpers' 33 36 import {shareUrl} from 'lib/sharing' ··· 62 65 63 66 let PostDropdownBtn = ({ 64 67 testID, 65 - postAuthor, 66 - postCid, 67 - postUri, 68 + post, 68 69 postFeedContext, 69 70 record, 70 71 richText, ··· 74 75 timestamp, 75 76 }: { 76 77 testID: string 77 - postAuthor: AppBskyActorDefs.ProfileViewBasic 78 - postCid: string 79 - postUri: string 78 + post: Shadow<AppBskyFeedDefs.PostView> 80 79 postFeedContext: string | undefined 81 80 record: AppBskyFeedPost.Record 82 81 richText: RichTextAPI ··· 92 91 const {_} = useLingui() 93 92 const defaultCtrlColor = theme.palette.default.postCtrl 94 93 const langPrefs = useLanguagePrefs() 95 - const mutedThreads = useMutedThreads() 96 - const toggleThreadMute = useToggleThreadMute() 97 94 const postDeleteMutation = usePostDeleteMutation() 98 95 const hiddenPosts = useHiddenPosts() 99 96 const {hidePost} = useHiddenPostsApi() ··· 107 104 const loggedOutWarningPromptControl = useDialogControl() 108 105 const embedPostControl = useDialogControl() 109 106 const sendViaChatControl = useDialogControl() 107 + const postUri = post.uri 108 + const postCid = post.cid 109 + const postAuthor = post.author 110 110 111 111 const rootUri = record.reply?.root?.uri || postUri 112 - const isThreadMuted = mutedThreads.includes(rootUri) 112 + const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue( 113 + post, 114 + rootUri, 115 + ) 113 116 const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) 114 117 const isAuthor = postAuthor.did === currentAccount?.did 115 118 ··· 162 165 163 166 const onToggleThreadMute = React.useCallback(() => { 164 167 try { 165 - const muted = toggleThreadMute(rootUri) 166 - if (muted) { 168 + if (isThreadMuted) { 169 + unmuteThread() 170 + Toast.show(_(msg`You will now receive notifications for this thread`)) 171 + } else { 172 + muteThread() 167 173 Toast.show( 168 174 _(msg`You will no longer receive notifications for this thread`), 169 175 ) 170 - } else { 171 - Toast.show(_(msg`You will now receive notifications for this thread`)) 172 176 } 173 - } catch (e) { 174 - logger.error('Failed to toggle thread mute', {message: e}) 177 + } catch (e: any) { 178 + if (e?.name !== 'AbortError') { 179 + logger.error('Failed to toggle thread mute', {message: e}) 180 + Toast.show(_(msg`Failed to toggle thread mute, please try again`)) 181 + } 175 182 } 176 - }, [rootUri, toggleThreadMute, _]) 183 + }, [isThreadMuted, unmuteThread, _, muteThread]) 177 184 178 185 const onCopyPostText = React.useCallback(() => { 179 186 const str = richTextToString(richText, true)
+1 -3
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 319 319 <View style={big ? a.align_center : [a.flex_1, a.align_start]}> 320 320 <PostDropdownBtn 321 321 testID="postDropdownBtn" 322 - postAuthor={post.author} 323 - postCid={post.cid} 324 - postUri={post.uri} 322 + post={post} 325 323 postFeedContext={feedContext} 326 324 record={record} 327 325 richText={richText}
+4 -4
yarn.lock
··· 34 34 jsonpointer "^5.0.0" 35 35 leven "^3.1.0" 36 36 37 - "@atproto/api@^0.12.18": 38 - version "0.12.18" 39 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.18.tgz#490a6f22966a3b605c22154fe7befc78bf640821" 40 - integrity sha512-Ii3J/uzmyw1qgnfhnvAsmuXa8ObRSCHelsF8TmQrgMWeXCbfypeS/VESm++1Z9+xHK7bHPOwSek3RmWB0cqEbQ== 37 + "@atproto/api@^0.12.19": 38 + version "0.12.19" 39 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.19.tgz#6d842269b6b9cd3fc5864e12824d4fb04cc033cf" 40 + integrity sha512-dsiTpjqBhjGwNW/qG/tLSgUQnmOSvd8hsQr5d8GCUDGK2AEHWl0KNgLPbwxIBEIo8Jg9NHsvqV7BMoix8YreIg== 41 41 dependencies: 42 42 "@atproto/common-web" "^0.3.0" 43 43 "@atproto/lexicon" "^0.4.0"