about summary refs log tree commit diff
path: root/src/state/queries/post-thread.ts
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-11-09 15:35:25 -0800
committerGitHub <noreply@github.com>2023-11-09 15:35:25 -0800
commitfb4f5709c43c070653c917e3196b9b1c120418a6 (patch)
tree74e6ff954441b6da3044853e16ebf5dd12213c87 /src/state/queries/post-thread.ts
parent625cbc435f15bc0d611661b44dbf8add990dff7d (diff)
downloadvoidsky-fb4f5709c43c070653c917e3196b9b1c120418a6.tar.zst
Refactor post threads to use react query (#1851)
* Add post and post-thread queries

* Update PostThread components to use new queries

* Move from normalized cache to shadow cache model

* Merge post shadow into the post automatically

* Remove dead code

* Remove old temporary session

* Fix: set agent on session creation

* Temporarily double-login

* Handle post-thread uri resolution errors
Diffstat (limited to 'src/state/queries/post-thread.ts')
-rw-r--r--src/state/queries/post-thread.ts177
1 files changed, 177 insertions, 0 deletions
diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts
new file mode 100644
index 000000000..4dea8aaf1
--- /dev/null
+++ b/src/state/queries/post-thread.ts
@@ -0,0 +1,177 @@
+import {
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  AppBskyFeedGetPostThread,
+} from '@atproto/api'
+import {useQuery} from '@tanstack/react-query'
+import {useSession} from '../session'
+import {ThreadViewPreference} from '../models/ui/preferences'
+
+export const RQKEY = (uri: string) => ['post-thread', uri]
+type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread']
+
+export interface ThreadCtx {
+  depth: number
+  isHighlightedPost?: boolean
+  hasMore?: boolean
+  showChildReplyLine?: boolean
+  showParentReplyLine?: boolean
+}
+
+export type ThreadPost = {
+  type: 'post'
+  _reactKey: string
+  uri: string
+  post: AppBskyFeedDefs.PostView
+  record: AppBskyFeedPost.Record
+  parent?: ThreadNode
+  replies?: ThreadNode[]
+  viewer?: AppBskyFeedDefs.ViewerThreadState
+  ctx: ThreadCtx
+}
+
+export type ThreadNotFound = {
+  type: 'not-found'
+  _reactKey: string
+  uri: string
+  ctx: ThreadCtx
+}
+
+export type ThreadBlocked = {
+  type: 'blocked'
+  _reactKey: string
+  uri: string
+  ctx: ThreadCtx
+}
+
+export type ThreadUnknown = {
+  type: 'unknown'
+  uri: string
+}
+
+export type ThreadNode =
+  | ThreadPost
+  | ThreadNotFound
+  | ThreadBlocked
+  | ThreadUnknown
+
+export function usePostThreadQuery(uri: string | undefined) {
+  const {agent} = useSession()
+  return useQuery<ThreadNode, Error>(
+    RQKEY(uri || ''),
+    async () => {
+      const res = await agent.getPostThread({uri: uri!})
+      if (res.success) {
+        return responseToThreadNodes(res.data.thread)
+      }
+      return {type: 'unknown', uri: uri!}
+    },
+    {enabled: !!uri},
+  )
+}
+
+export function sortThread(
+  node: ThreadNode,
+  opts: ThreadViewPreference,
+): ThreadNode {
+  if (node.type !== 'post') {
+    return node
+  }
+  if (node.replies) {
+    node.replies.sort((a: ThreadNode, b: ThreadNode) => {
+      if (a.type !== 'post') {
+        return 1
+      }
+      if (b.type !== 'post') {
+        return -1
+      }
+
+      const aIsByOp = a.post.author.did === node.post?.author.did
+      const bIsByOp = b.post.author.did === node.post?.author.did
+      if (aIsByOp && bIsByOp) {
+        return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest
+      } else if (aIsByOp) {
+        return -1 // op's own reply
+      } else if (bIsByOp) {
+        return 1 // op's own reply
+      }
+      if (opts.prioritizeFollowedUsers) {
+        const af = a.post.author.viewer?.following
+        const bf = b.post.author.viewer?.following
+        if (af && !bf) {
+          return -1
+        } else if (!af && bf) {
+          return 1
+        }
+      }
+      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)
+      } else if (opts.sort === 'most-likes') {
+        if (a.post.likeCount === b.post.likeCount) {
+          return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest
+        } else {
+          return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes
+        }
+      } else if (opts.sort === 'random') {
+        return 0.5 - Math.random() // this is vaguely criminal but we can get away with it
+      }
+      return b.post.indexedAt.localeCompare(a.post.indexedAt)
+    })
+    node.replies.forEach(reply => sortThread(reply, opts))
+  }
+  return node
+}
+
+// internal methods
+// =
+
+function responseToThreadNodes(
+  node: ThreadViewNode,
+  depth = 0,
+  direction: 'up' | 'down' | 'start' = 'start',
+): ThreadNode {
+  if (
+    AppBskyFeedDefs.isThreadViewPost(node) &&
+    AppBskyFeedPost.isRecord(node.post.record) &&
+    AppBskyFeedPost.validateRecord(node.post.record).success
+  ) {
+    return {
+      type: 'post',
+      _reactKey: node.post.uri,
+      uri: node.post.uri,
+      post: node.post,
+      record: node.post.record,
+      parent:
+        node.parent && direction !== 'down'
+          ? responseToThreadNodes(node.parent, depth - 1, 'up')
+          : undefined,
+      replies:
+        node.replies?.length && direction !== 'up'
+          ? node.replies.map(reply =>
+              responseToThreadNodes(reply, depth + 1, 'down'),
+            )
+          : undefined,
+      viewer: node.viewer,
+      ctx: {
+        depth,
+        isHighlightedPost: depth === 0,
+        hasMore:
+          direction === 'down' && !node.replies?.length && !!node.replyCount,
+        showChildReplyLine:
+          direction === 'up' ||
+          (direction === 'down' && !!node.replies?.length),
+        showParentReplyLine:
+          (direction === 'up' && !!node.parent) ||
+          (direction === 'down' && depth !== 1),
+      },
+    }
+  } else if (AppBskyFeedDefs.isBlockedPost(node)) {
+    return {type: 'blocked', _reactKey: node.uri, uri: node.uri, ctx: {depth}}
+  } else if (AppBskyFeedDefs.isNotFoundPost(node)) {
+    return {type: 'not-found', _reactKey: node.uri, uri: node.uri, ctx: {depth}}
+  } else {
+    return {type: 'unknown', uri: ''}
+  }
+}