about summary refs log tree commit diff
path: root/src/state/queries
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/queries')
-rw-r--r--src/state/queries/email-verification-required.ts25
-rw-r--r--src/state/queries/messages/conversation.ts39
-rw-r--r--src/state/queries/messages/list-converations.tsx16
-rw-r--r--src/state/queries/post-feed.ts2
-rw-r--r--src/state/queries/post-thread.ts32
-rw-r--r--src/state/queries/post.ts24
-rw-r--r--src/state/queries/preferences/const.ts2
-rw-r--r--src/state/queries/preferences/types.ts2
-rw-r--r--src/state/queries/profile.ts14
9 files changed, 123 insertions, 33 deletions
diff --git a/src/state/queries/email-verification-required.ts b/src/state/queries/email-verification-required.ts
new file mode 100644
index 000000000..94ff5cbc6
--- /dev/null
+++ b/src/state/queries/email-verification-required.ts
@@ -0,0 +1,25 @@
+import {useQuery} from '@tanstack/react-query'
+
+interface ServiceConfig {
+  checkEmailConfirmed: boolean
+}
+
+export function useServiceConfigQuery() {
+  return useQuery({
+    queryKey: ['service-config'],
+    queryFn: async () => {
+      const res = await fetch(
+        'https://api.bsky.app/xrpc/app.bsky.unspecced.getConfig',
+      )
+      if (!res.ok) {
+        return {
+          checkEmailConfirmed: false,
+        }
+      }
+
+      const json = await res.json()
+      return json as ServiceConfig
+    },
+    staleTime: 5 * 60 * 1000,
+  })
+}
diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts
index fa8a883d0..db96d21a9 100644
--- a/src/state/queries/messages/conversation.ts
+++ b/src/state/queries/messages/conversation.ts
@@ -5,7 +5,11 @@ import {STALE} from '#/state/queries'
 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useOnMarkAsRead} from '#/state/queries/messages/list-converations'
 import {useAgent} from '#/state/session'
-import {RQKEY as LIST_CONVOS_KEY} from './list-converations'
+import {
+  ConvoListQueryData,
+  getConvoFromQueryData,
+  RQKEY as LIST_CONVOS_KEY,
+} from './list-converations'
 
 const RQKEY_ROOT = 'convo'
 export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
@@ -57,8 +61,37 @@ export function useMarkAsReadMutation() {
       if (!convoId) throw new Error('No convoId provided')
       optimisticUpdate(convoId)
     },
-    onSettled() {
-      queryClient.invalidateQueries({queryKey: LIST_CONVOS_KEY})
+    onSuccess(_, {convoId}) {
+      if (!convoId) return
+
+      queryClient.setQueryData(LIST_CONVOS_KEY, (old: ConvoListQueryData) => {
+        if (!old) return old
+
+        const existingConvo = getConvoFromQueryData(convoId, old)
+
+        if (existingConvo) {
+          return {
+            ...old,
+            pages: old.pages.map(page => {
+              return {
+                ...page,
+                convos: page.convos.map(convo => {
+                  if (convo.id === convoId) {
+                    return {
+                      ...convo,
+                      unreadCount: 0,
+                    }
+                  }
+                  return convo
+                }),
+              }
+            }),
+          }
+        } else {
+          // If we somehow marked a convo as read that doesn't exist in the
+          // list, then we don't need to do anything.
+        }
+      })
     },
   })
 }
diff --git a/src/state/queries/messages/list-converations.tsx b/src/state/queries/messages/list-converations.tsx
index eeab246ab..ae379f962 100644
--- a/src/state/queries/messages/list-converations.tsx
+++ b/src/state/queries/messages/list-converations.tsx
@@ -39,7 +39,7 @@ export function useListConvosQuery({
     queryKey: RQKEY,
     queryFn: async ({pageParam}) => {
       const {data} = await agent.api.chat.bsky.convo.listConvos(
-        {cursor: pageParam},
+        {cursor: pageParam, limit: 20},
         {headers: DM_SERVICE_HEADERS},
       )
 
@@ -47,9 +47,6 @@ export function useListConvosQuery({
     },
     initialPageParam: undefined as RQPageParam,
     getNextPageParam: lastPage => lastPage.cursor,
-    // refetch every 60 seconds since we can't get *all* info from the logs
-    // i.e. reading chats on another device won't update the unread count
-    refetchInterval: 60_000,
   })
 }
 
@@ -180,6 +177,11 @@ export function ListConvosProviderInner({
                   }),
                 }
               } else {
+                /**
+                 * We received a message from an conversation old enough that
+                 * it doesn't exist in the query cache, meaning we need to
+                 * refetch and bump the old convo to the top.
+                 */
                 debouncedRefetch()
               }
             })
@@ -245,12 +247,12 @@ export function useUnreadMessageCount() {
   return useMemo(() => {
     return {
       count,
-      numUnread: count > 0 ? (count > 30 ? '30+' : String(count)) : undefined,
+      numUnread: count > 0 ? (count > 10 ? '10+' : String(count)) : undefined,
     }
   }, [count])
 }
 
-type ConvoListQueryData = {
+export type ConvoListQueryData = {
   pageParams: Array<string | undefined>
   pages: Array<ChatBskyConvoListConvos.OutputSchema>
 }
@@ -301,7 +303,7 @@ function optimisticDelete(chatId: string, old: ConvoListQueryData) {
   }
 }
 
-function getConvoFromQueryData(chatId: string, old: ConvoListQueryData) {
+export function getConvoFromQueryData(chatId: string, old: ConvoListQueryData) {
   for (const page of old.pages) {
     for (const convo of page.convos) {
       if (convo.id === chatId) {
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index b2e9dcd4c..016d8893b 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -602,7 +602,7 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
   }
 
   if (!somePostsPassModeration) {
-    throw new Error(KnownError.FeedNSFPublic)
+    throw new Error(KnownError.FeedSignedInOnly)
   }
 }
 
diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts
index 103a1d03b..4784a9d75 100644
--- a/src/state/queries/post-thread.ts
+++ b/src/state/queries/post-thread.ts
@@ -216,6 +216,17 @@ export function sortThread(
         }
       }
 
+      const aPin = Boolean(a.record.text.trim() === '📌')
+      const bPin = Boolean(b.record.text.trim() === '📌')
+      if (aPin !== bPin) {
+        if (aPin) {
+          return 1
+        }
+        if (bPin) {
+          return -1
+        }
+      }
+
       if (opts.prioritizeFollowedUsers) {
         const af = a.post.author.viewer?.following
         const bf = b.post.author.viewer?.following
@@ -226,7 +237,11 @@ export function sortThread(
         }
       }
 
-      if (opts.sort === 'oldest') {
+      if (opts.sort === 'hotness') {
+        const aHotness = getHotness(a.post)
+        const bHotness = getHotness(b.post)
+        return bHotness - aHotness
+      } else if (opts.sort === 'oldest') {
         return a.post.indexedAt.localeCompare(b.post.indexedAt)
       } else if (opts.sort === 'newest') {
         return b.post.indexedAt.localeCompare(a.post.indexedAt)
@@ -258,6 +273,21 @@ export function sortThread(
 // internal methods
 // =
 
+// Inspired by https://join-lemmy.org/docs/contributors/07-ranking-algo.html
+// We want to give recent comments a real chance (and not bury them deep below the fold)
+// while also surfacing well-liked comments from the past. In the future, we can explore
+// something more sophisticated, but we don't have much data on the client right now.
+function getHotness(post: AppBskyFeedDefs.PostView) {
+  const hoursAgo =
+    (new Date().getTime() - new Date(post.indexedAt).getTime()) /
+    (1000 * 60 * 60)
+  const likeCount = post.likeCount ?? 0
+  const likeOrder = Math.log(3 + likeCount)
+  const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount))
+  const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent)
+  return likeOrder / timePenalty
+}
+
 function responseToThreadNodes(
   node: ThreadViewNode,
   depth = 0,
diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts
index 7023580bb..7052590ca 100644
--- a/src/state/queries/post.ts
+++ b/src/state/queries/post.ts
@@ -98,8 +98,8 @@ export function useGetPosts() {
 
 export function usePostLikeMutationQueue(
   post: Shadow<AppBskyFeedDefs.PostView>,
-  logContext: LogEvents['post:like:sampled']['logContext'] &
-    LogEvents['post:unlike:sampled']['logContext'],
+  logContext: LogEvents['post:like']['logContext'] &
+    LogEvents['post:unlike']['logContext'],
 ) {
   const queryClient = useQueryClient()
   const postUri = post.uri
@@ -157,7 +157,7 @@ export function usePostLikeMutationQueue(
 }
 
 function usePostLikeMutation(
-  logContext: LogEvents['post:like:sampled']['logContext'],
+  logContext: LogEvents['post:like']['logContext'],
   post: Shadow<AppBskyFeedDefs.PostView>,
 ) {
   const {currentAccount} = useSession()
@@ -174,7 +174,7 @@ function usePostLikeMutation(
       if (currentAccount) {
         ownProfile = findProfileQueryData(queryClient, currentAccount.did)
       }
-      logEvent('post:like:sampled', {
+      logEvent('post:like', {
         logContext,
         doesPosterFollowLiker: postAuthor.viewer
           ? Boolean(postAuthor.viewer.followedBy)
@@ -196,12 +196,12 @@ function usePostLikeMutation(
 }
 
 function usePostUnlikeMutation(
-  logContext: LogEvents['post:unlike:sampled']['logContext'],
+  logContext: LogEvents['post:unlike']['logContext'],
 ) {
   const agent = useAgent()
   return useMutation<void, Error, {postUri: string; likeUri: string}>({
     mutationFn: ({likeUri}) => {
-      logEvent('post:unlike:sampled', {logContext})
+      logEvent('post:unlike', {logContext})
       return agent.deleteLike(likeUri)
     },
   })
@@ -209,8 +209,8 @@ function usePostUnlikeMutation(
 
 export function usePostRepostMutationQueue(
   post: Shadow<AppBskyFeedDefs.PostView>,
-  logContext: LogEvents['post:repost:sampled']['logContext'] &
-    LogEvents['post:unrepost:sampled']['logContext'],
+  logContext: LogEvents['post:repost']['logContext'] &
+    LogEvents['post:unrepost']['logContext'],
 ) {
   const queryClient = useQueryClient()
   const postUri = post.uri
@@ -266,7 +266,7 @@ export function usePostRepostMutationQueue(
 }
 
 function usePostRepostMutation(
-  logContext: LogEvents['post:repost:sampled']['logContext'],
+  logContext: LogEvents['post:repost']['logContext'],
 ) {
   const agent = useAgent()
   return useMutation<
@@ -275,19 +275,19 @@ function usePostRepostMutation(
     {uri: string; cid: string} // the post's uri and cid
   >({
     mutationFn: post => {
-      logEvent('post:repost:sampled', {logContext})
+      logEvent('post:repost', {logContext})
       return agent.repost(post.uri, post.cid)
     },
   })
 }
 
 function usePostUnrepostMutation(
-  logContext: LogEvents['post:unrepost:sampled']['logContext'],
+  logContext: LogEvents['post:unrepost']['logContext'],
 ) {
   const agent = useAgent()
   return useMutation<void, Error, {postUri: string; repostUri: string}>({
     mutationFn: ({repostUri}) => {
-      logEvent('post:unrepost:sampled', {logContext})
+      logEvent('post:unrepost', {logContext})
       return agent.deleteRepost(repostUri)
     },
   })
diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts
index e07f40ec5..549f7ce29 100644
--- a/src/state/queries/preferences/const.ts
+++ b/src/state/queries/preferences/const.ts
@@ -15,7 +15,7 @@ export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs
   }
 
 export const DEFAULT_THREAD_VIEW_PREFS: ThreadViewPreferences = {
-  sort: 'newest',
+  sort: 'hotness',
   prioritizeFollowedUsers: true,
   lab_treeViewEnabled: false,
 }
diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts
index 928bb90da..8f523fcf2 100644
--- a/src/state/queries/preferences/types.ts
+++ b/src/state/queries/preferences/types.ts
@@ -22,6 +22,6 @@ export type ThreadViewPreferences = Pick<
   BskyThreadViewPreference,
   'prioritizeFollowedUsers'
 > & {
-  sort: 'oldest' | 'newest' | 'most-likes' | 'random' | string
+  sort: 'hotness' | 'oldest' | 'newest' | 'most-likes' | 'random' | string
   lab_treeViewEnabled?: boolean
 }
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 3059d9efe..63c405788 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -221,8 +221,8 @@ export function useProfileUpdateMutation() {
 
 export function useProfileFollowMutationQueue(
   profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
-  logContext: LogEvents['profile:follow:sampled']['logContext'] &
-    LogEvents['profile:follow:sampled']['logContext'],
+  logContext: LogEvents['profile:follow']['logContext'] &
+    LogEvents['profile:follow']['logContext'],
 ) {
   const agent = useAgent()
   const queryClient = useQueryClient()
@@ -293,7 +293,7 @@ export function useProfileFollowMutationQueue(
 }
 
 function useProfileFollowMutation(
-  logContext: LogEvents['profile:follow:sampled']['logContext'],
+  logContext: LogEvents['profile:follow']['logContext'],
   profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
 ) {
   const {currentAccount} = useSession()
@@ -308,7 +308,7 @@ function useProfileFollowMutation(
         ownProfile = findProfileQueryData(queryClient, currentAccount.did)
       }
       captureAction(ProgressGuideAction.Follow)
-      logEvent('profile:follow:sampled', {
+      logEvent('profile:follow', {
         logContext,
         didBecomeMutual: profile.viewer
           ? Boolean(profile.viewer.followedBy)
@@ -322,12 +322,12 @@ function useProfileFollowMutation(
 }
 
 function useProfileUnfollowMutation(
-  logContext: LogEvents['profile:unfollow:sampled']['logContext'],
+  logContext: LogEvents['profile:unfollow']['logContext'],
 ) {
   const agent = useAgent()
   return useMutation<void, Error, {did: string; followUri: string}>({
     mutationFn: async ({followUri}) => {
-      logEvent('profile:unfollow:sampled', {logContext})
+      logEvent('profile:unfollow', {logContext})
       return await agent.deleteFollow(followUri)
     },
   })
@@ -409,7 +409,7 @@ function useProfileUnmuteMutation() {
 }
 
 export function useProfileBlockMutationQueue(
-  profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>,
+  profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
 ) {
   const queryClient = useQueryClient()
   const did = profile.did