about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-11-08 09:08:42 -0800
committerGitHub <noreply@github.com>2023-11-08 09:08:42 -0800
commit74f8390f1d879350ebb6516fade2b1d83d1601e7 (patch)
treebf627b889faf1c4078562d5644d17422fee2df55 /src
parent4afed4be281b6319c328938e4ed757624a78b13c (diff)
downloadvoidsky-74f8390f1d879350ebb6516fade2b1d83d1601e7.tar.zst
Move muted threads to new persistence + context (#1838)
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx5
-rw-r--r--src/App.web.tsx5
-rw-r--r--src/state/models/content/post-thread-item.ts8
-rw-r--r--src/state/models/content/post-thread.ts12
-rw-r--r--src/state/models/feeds/notifications.ts4
-rw-r--r--src/state/models/feeds/post.ts18
-rw-r--r--src/state/models/muted-threads.ts29
-rw-r--r--src/state/models/root-store.ts6
-rw-r--r--src/state/muted-threads.tsx59
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx17
-rw-r--r--src/view/com/post/Post.tsx13
-rw-r--r--src/view/com/posts/FeedItem.tsx13
12 files changed, 95 insertions, 94 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index f5d35cf74..4500b5d07 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -22,6 +22,7 @@ import * as Toast from 'view/com/util/Toast'
 import {queryClient} from 'lib/react-query'
 import {TestCtrls} from 'view/com/testing/TestCtrls'
 import {Provider as ShellStateProvider} from 'state/shell'
+import {Provider as MutedThreadsProvider} from 'state/muted-threads'
 
 SplashScreen.preventAutoHideAsync()
 
@@ -78,7 +79,9 @@ function App() {
 
   return (
     <ShellStateProvider>
-      <InnerApp />
+      <MutedThreadsProvider>
+        <InnerApp />
+      </MutedThreadsProvider>
     </ShellStateProvider>
   )
 }
diff --git a/src/App.web.tsx b/src/App.web.tsx
index adad9ddb6..9792274b0 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -17,6 +17,7 @@ import {ToastContainer} from 'view/com/util/Toast.web'
 import {ThemeProvider} from 'lib/ThemeContext'
 import {queryClient} from 'lib/react-query'
 import {Provider as ShellStateProvider} from 'state/shell'
+import {Provider as MutedThreadsProvider} from 'state/muted-threads'
 
 const InnerApp = observer(function AppImpl() {
   const colorMode = useColorMode()
@@ -68,7 +69,9 @@ function App() {
 
   return (
     <ShellStateProvider>
-      <InnerApp />
+      <MutedThreadsProvider>
+        <InnerApp />
+      </MutedThreadsProvider>
     </ShellStateProvider>
   )
 }
diff --git a/src/state/models/content/post-thread-item.ts b/src/state/models/content/post-thread-item.ts
index 942f3acc8..855b038c4 100644
--- a/src/state/models/content/post-thread-item.ts
+++ b/src/state/models/content/post-thread-item.ts
@@ -63,10 +63,6 @@ export class PostThreadItemModel {
     return this.post.uri
   }
 
-  get isThreadMuted() {
-    return this.data.isThreadMuted
-  }
-
   get moderation(): PostModeration {
     return this.data.moderation
   }
@@ -129,10 +125,6 @@ export class PostThreadItemModel {
     this.data.toggleRepost()
   }
 
-  async toggleThreadMute() {
-    this.data.toggleThreadMute()
-  }
-
   async delete() {
     this.data.delete()
   }
diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts
index fd194056a..65e74f7ce 100644
--- a/src/state/models/content/post-thread.ts
+++ b/src/state/models/content/post-thread.ts
@@ -74,10 +74,6 @@ export class PostThreadModel {
     return this.resolvedUri
   }
 
-  get isThreadMuted() {
-    return this.rootStore.mutedThreads.uris.has(this.rootUri)
-  }
-
   get isCachedPostAReply() {
     if (AppBskyFeedPost.isRecord(this.thread?.post.record)) {
       return !!this.thread?.post.record.reply
@@ -140,14 +136,6 @@ export class PostThreadModel {
     this.refresh()
   }
 
-  async toggleThreadMute() {
-    if (this.isThreadMuted) {
-      this.rootStore.mutedThreads.uris.delete(this.rootUri)
-    } else {
-      this.rootStore.mutedThreads.uris.add(this.rootUri)
-    }
-  }
-
   // state transitions
   // =
 
diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts
index 607e3038b..272d52881 100644
--- a/src/state/models/feeds/notifications.ts
+++ b/src/state/models/feeds/notifications.ts
@@ -18,6 +18,7 @@ import {RootStoreModel} from '../root-store'
 import {PostThreadModel} from '../content/post-thread'
 import {cleanError} from 'lib/strings/errors'
 import {logger} from '#/logger'
+import {isThreadMuted} from '#/state/muted-threads'
 
 const GROUPABLE_REASONS = ['like', 'repost', 'follow']
 const PAGE_SIZE = 30
@@ -550,8 +551,7 @@ export class NotificationsFeedModel {
       .filter(item => {
         const hideByLabel = item.shouldFilter
         let mutedThread = !!(
-          item.reasonSubjectRootUri &&
-          this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri)
+          item.reasonSubjectRootUri && isThreadMuted(item.reasonSubjectRootUri)
         )
         return !hideByLabel && !mutedThread
       })
diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts
index d064edc21..4fa1213b5 100644
--- a/src/state/models/feeds/post.ts
+++ b/src/state/models/feeds/post.ts
@@ -75,10 +75,6 @@ export class PostsFeedItemModel {
     return this.post.uri
   }
 
-  get isThreadMuted() {
-    return this.rootStore.mutedThreads.uris.has(this.rootUri)
-  }
-
   get moderation(): PostModeration {
     return moderatePost(this.post, this.rootStore.preferences.moderationOpts)
   }
@@ -172,20 +168,6 @@ export class PostsFeedItemModel {
     }
   }
 
-  async toggleThreadMute() {
-    try {
-      if (this.isThreadMuted) {
-        this.rootStore.mutedThreads.uris.delete(this.rootUri)
-        track('Post:ThreadUnmute')
-      } else {
-        this.rootStore.mutedThreads.uris.add(this.rootUri)
-        track('Post:ThreadMute')
-      }
-    } catch (error) {
-      logger.error('Failed to toggle thread mute', {error})
-    }
-  }
-
   async delete() {
     try {
       await this.rootStore.agent.deletePost(this.post.uri)
diff --git a/src/state/models/muted-threads.ts b/src/state/models/muted-threads.ts
deleted file mode 100644
index e6f202745..000000000
--- a/src/state/models/muted-threads.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * This is a temporary client-side system for storing muted threads
- * When the system lands on prod we should switch to that
- */
-
-import {makeAutoObservable} from 'mobx'
-import {isObj, hasProp, isStrArray} from 'lib/type-guards'
-
-export class MutedThreads {
-  uris: Set<string> = new Set()
-
-  constructor() {
-    makeAutoObservable(
-      this,
-      {serialize: false, hydrate: false},
-      {autoBind: true},
-    )
-  }
-
-  serialize() {
-    return {uris: Array.from(this.uris)}
-  }
-
-  hydrate(v: unknown) {
-    if (isObj(v) && hasProp(v, 'uris') && isStrArray(v.uris)) {
-      this.uris = new Set(v.uris)
-    }
-  }
-}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index f04a9922d..fadd279fc 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -19,7 +19,6 @@ import {InvitedUsers} from './invited-users'
 import {PreferencesModel} from './ui/preferences'
 import {resetToTab} from '../../Navigation'
 import {ImageSizesCache} from './cache/image-sizes'
-import {MutedThreads} from './muted-threads'
 import {reset as resetNavigation} from '../../Navigation'
 import {logger} from '#/logger'
 
@@ -49,7 +48,6 @@ export class RootStoreModel {
   posts = new PostsCache(this)
   linkMetas = new LinkMetasCache(this)
   imageSizes = new ImageSizesCache()
-  mutedThreads = new MutedThreads()
 
   constructor(agent: BskyAgent) {
     this.agent = agent
@@ -71,7 +69,6 @@ export class RootStoreModel {
       me: this.me.serialize(),
       preferences: this.preferences.serialize(),
       invitedUsers: this.invitedUsers.serialize(),
-      mutedThreads: this.mutedThreads.serialize(),
     }
   }
 
@@ -95,9 +92,6 @@ export class RootStoreModel {
       if (hasProp(v, 'invitedUsers')) {
         this.invitedUsers.hydrate(v.invitedUsers)
       }
-      if (hasProp(v, 'mutedThreads')) {
-        this.mutedThreads.hydrate(v.mutedThreads)
-      }
     }
   }
 
diff --git a/src/state/muted-threads.tsx b/src/state/muted-threads.tsx
new file mode 100644
index 000000000..2b3a7de6a
--- /dev/null
+++ b/src/state/muted-threads.tsx
@@ -0,0 +1,59 @@
+import React from 'react'
+import * as persisted from '#/state/persisted'
+
+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
+        } else {
+          arr = arr.concat([uri])
+          muted = true
+        }
+        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/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 351a46706..9aec638ec 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -37,6 +37,7 @@ import {makeProfileLink} from 'lib/routes/links'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {MAX_POST_LINES} from 'lib/constants'
 import {logger} from '#/logger'
+import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
 
 export const PostThreadItem = observer(function PostThreadItem({
   item,
@@ -51,6 +52,8 @@ export const PostThreadItem = observer(function PostThreadItem({
 }) {
   const pal = usePalette('default')
   const store = useStores()
+  const mutedThreads = useMutedThreads()
+  const toggleThreadMute = useToggleThreadMute()
   const [deleted, setDeleted] = React.useState(false)
   const [limitLines, setLimitLines] = React.useState(
     countLines(item.richText?.text) >= MAX_POST_LINES,
@@ -130,10 +133,10 @@ export const PostThreadItem = observer(function PostThreadItem({
     Linking.openURL(translatorUrl)
   }, [translatorUrl])
 
-  const onToggleThreadMute = React.useCallback(async () => {
+  const onToggleThreadMute = React.useCallback(() => {
     try {
-      await item.toggleThreadMute()
-      if (item.isThreadMuted) {
+      const muted = toggleThreadMute(item.data.rootUri)
+      if (muted) {
         Toast.show('You will no longer receive notifications for this thread')
       } else {
         Toast.show('You will now receive notifications for this thread')
@@ -141,7 +144,7 @@ export const PostThreadItem = observer(function PostThreadItem({
     } catch (e) {
       logger.error('Failed to toggle thread mute', {error: e})
     }
-  }, [item])
+  }, [item, toggleThreadMute])
 
   const onDeletePost = React.useCallback(() => {
     item.delete().then(
@@ -284,7 +287,7 @@ export const PostThreadItem = observer(function PostThreadItem({
               itemHref={itemHref}
               itemTitle={itemTitle}
               isAuthor={item.post.author.did === store.me.did}
-              isThreadMuted={item.isThreadMuted}
+              isThreadMuted={mutedThreads.includes(item.data.rootUri)}
               onCopyPostText={onCopyPostText}
               onOpenTranslate={onOpenTranslate}
               onToggleThreadMute={onToggleThreadMute}
@@ -391,7 +394,7 @@ export const PostThreadItem = observer(function PostThreadItem({
                 isAuthor={item.post.author.did === store.me.did}
                 isReposted={!!item.post.viewer?.repost}
                 isLiked={!!item.post.viewer?.like}
-                isThreadMuted={item.isThreadMuted}
+                isThreadMuted={mutedThreads.includes(item.data.rootUri)}
                 onPressReply={onPressReply}
                 onPressToggleRepost={onPressToggleRepost}
                 onPressToggleLike={onPressToggleLike}
@@ -534,7 +537,7 @@ export const PostThreadItem = observer(function PostThreadItem({
                 likeCount={item.post.likeCount}
                 isReposted={!!item.post.viewer?.repost}
                 isLiked={!!item.post.viewer?.like}
-                isThreadMuted={item.isThreadMuted}
+                isThreadMuted={mutedThreads.includes(item.data.rootUri)}
                 onPressReply={onPressReply}
                 onPressToggleRepost={onPressToggleRepost}
                 onPressToggleLike={onPressToggleLike}
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 4ec9db77f..db490333d 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -33,6 +33,7 @@ import {makeProfileLink} from 'lib/routes/links'
 import {MAX_POST_LINES} from 'lib/constants'
 import {countLines} from 'lib/strings/helpers'
 import {logger} from '#/logger'
+import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
 
 export const Post = observer(function PostImpl({
   view,
@@ -106,6 +107,8 @@ const PostLoaded = observer(function PostLoadedImpl({
 }) {
   const pal = usePalette('default')
   const store = useStores()
+  const mutedThreads = useMutedThreads()
+  const toggleThreadMute = useToggleThreadMute()
   const [limitLines, setLimitLines] = React.useState(
     countLines(item.richText?.text) >= MAX_POST_LINES,
   )
@@ -161,10 +164,10 @@ const PostLoaded = observer(function PostLoadedImpl({
     Linking.openURL(translatorUrl)
   }, [translatorUrl])
 
-  const onToggleThreadMute = React.useCallback(async () => {
+  const onToggleThreadMute = React.useCallback(() => {
     try {
-      await item.toggleThreadMute()
-      if (item.isThreadMuted) {
+      const muted = toggleThreadMute(item.data.rootUri)
+      if (muted) {
         Toast.show('You will no longer receive notifications for this thread')
       } else {
         Toast.show('You will now receive notifications for this thread')
@@ -172,7 +175,7 @@ const PostLoaded = observer(function PostLoadedImpl({
     } catch (e) {
       logger.error('Failed to toggle thread mute', {error: e})
     }
-  }, [item])
+  }, [item, toggleThreadMute])
 
   const onDeletePost = React.useCallback(() => {
     item.delete().then(
@@ -286,7 +289,7 @@ const PostLoaded = observer(function PostLoadedImpl({
             likeCount={item.post.likeCount}
             isReposted={!!item.post.viewer?.repost}
             isLiked={!!item.post.viewer?.like}
-            isThreadMuted={item.isThreadMuted}
+            isThreadMuted={mutedThreads.includes(item.data.rootUri)}
             onPressReply={onPressReply}
             onPressToggleRepost={onPressToggleRepost}
             onPressToggleLike={onPressToggleLike}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index aeee3e20a..772bb2561 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -33,6 +33,7 @@ import {isEmbedByEmbedder} from 'lib/embeds'
 import {MAX_POST_LINES} from 'lib/constants'
 import {countLines} from 'lib/strings/helpers'
 import {logger} from '#/logger'
+import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
 
 export const FeedItem = observer(function FeedItemImpl({
   item,
@@ -50,6 +51,8 @@ export const FeedItem = observer(function FeedItemImpl({
 }) {
   const store = useStores()
   const pal = usePalette('default')
+  const mutedThreads = useMutedThreads()
+  const toggleThreadMute = useToggleThreadMute()
   const {track} = useAnalytics()
   const [deleted, setDeleted] = useState(false)
   const [limitLines, setLimitLines] = useState(
@@ -114,11 +117,11 @@ export const FeedItem = observer(function FeedItemImpl({
     Linking.openURL(translatorUrl)
   }, [translatorUrl])
 
-  const onToggleThreadMute = React.useCallback(async () => {
+  const onToggleThreadMute = React.useCallback(() => {
     track('FeedItem:ThreadMute')
     try {
-      await item.toggleThreadMute()
-      if (item.isThreadMuted) {
+      const muted = toggleThreadMute(item.rootUri)
+      if (muted) {
         Toast.show('You will no longer receive notifications for this thread')
       } else {
         Toast.show('You will now receive notifications for this thread')
@@ -126,7 +129,7 @@ export const FeedItem = observer(function FeedItemImpl({
     } catch (e) {
       logger.error('Failed to toggle thread mute', {error: e})
     }
-  }, [track, item])
+  }, [track, toggleThreadMute, item])
 
   const onDeletePost = React.useCallback(() => {
     track('FeedItem:PostDelete')
@@ -360,7 +363,7 @@ export const FeedItem = observer(function FeedItemImpl({
             likeCount={item.post.likeCount}
             isReposted={!!item.post.viewer?.repost}
             isLiked={!!item.post.viewer?.like}
-            isThreadMuted={item.isThreadMuted}
+            isThreadMuted={mutedThreads.includes(item.rootUri)}
             onPressReply={onPressReply}
             onPressToggleRepost={onPressToggleRepost}
             onPressToggleLike={onPressToggleLike}