mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {
2 type AppBskyFeedDefs,
3 AppBskyFeedPost,
4 AppBskyFeedThreadgate,
5 AppBskyUnspeccedDefs,
6 type AppBskyUnspeccedGetPostThreadV2,
7 AtUri,
8} from '@atproto/api'
9
10import {
11 type ApiThreadItem,
12 type ThreadItem,
13 type TraversalMetadata,
14} from '#/state/queries/usePostThread/types'
15import {isDevMode} from '#/storage/hooks/dev-mode'
16import * as bsky from '#/types/bsky'
17
18export function getThreadgateRecord(
19 view: AppBskyUnspeccedGetPostThreadV2.OutputSchema['threadgate'],
20) {
21 return bsky.dangerousIsType<AppBskyFeedThreadgate.Record>(
22 view?.record,
23 AppBskyFeedThreadgate.isRecord,
24 )
25 ? view?.record
26 : undefined
27}
28
29export function getRootPostAtUri(post: AppBskyFeedDefs.PostView) {
30 if (
31 bsky.dangerousIsType<AppBskyFeedPost.Record>(
32 post.record,
33 AppBskyFeedPost.isRecord,
34 )
35 ) {
36 /**
37 * If the record has no `reply` field, it is a root post.
38 */
39 if (!post.record.reply) {
40 return new AtUri(post.uri)
41 }
42 if (post.record.reply?.root?.uri) {
43 return new AtUri(post.record.reply.root.uri)
44 }
45 }
46}
47
48export function getPostRecord(post: AppBskyFeedDefs.PostView) {
49 return post.record as AppBskyFeedPost.Record
50}
51
52export function getTraversalMetadata({
53 item,
54 prevItem,
55 nextItem,
56 parentMetadata,
57}: {
58 item: ApiThreadItem
59 prevItem?: ApiThreadItem
60 nextItem?: ApiThreadItem
61 parentMetadata?: TraversalMetadata
62}): TraversalMetadata {
63 if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
64 throw new Error(`Expected thread item to be a post`)
65 }
66 const repliesCount = item.value.post.replyCount || 0
67 const repliesUnhydrated = item.value.moreReplies || 0
68 const metadata = {
69 depth: item.depth,
70 /*
71 * Unknown until after traversal
72 */
73 isLastChild: false,
74 /*
75 * Unknown until after traversal
76 */
77 isLastSibling: false,
78 /*
79 * If it's a top level reply, bc we render each top-level branch as a
80 * separate tree, it's implicitly part of the last branch. For subsequent
81 * replies, we'll override this after traversal.
82 */
83 isPartOfLastBranchFromDepth: item.depth === 1 ? 1 : undefined,
84 nextItemDepth: nextItem?.depth,
85 parentMetadata,
86 prevItemDepth: prevItem?.depth,
87 /*
88 * Unknown until after traversal
89 */
90 precedesChildReadMore: false,
91 /*
92 * Unknown until after traversal
93 */
94 followsReadMoreUp: false,
95 postData: {
96 uri: item.uri,
97 authorHandle: item.value.post.author.handle,
98 },
99 repliesCount,
100 repliesUnhydrated,
101 repliesSeenCounter: 0,
102 repliesIndexCounter: 0,
103 replyIndex: 0,
104 skippedIndentIndices: new Set<number>(),
105 }
106
107 if (isDevMode()) {
108 // @ts-ignore dev only for debugging
109 metadata.postData.text = getPostRecord(item.value.post).text
110 }
111
112 return metadata
113}
114
115export function storeTraversalMetadata(
116 metadatas: Map<string, TraversalMetadata>,
117 metadata: TraversalMetadata,
118) {
119 metadatas.set(metadata.postData.uri, metadata)
120
121 if (isDevMode()) {
122 // @ts-ignore dev only for debugging
123 metadatas.set(metadata.postData.text, metadata)
124 // @ts-ignore
125 window.__thread = metadatas
126 }
127}
128
129export function getThreadPostUI({
130 depth,
131 repliesCount,
132 prevItemDepth,
133 isLastChild,
134 skippedIndentIndices,
135 repliesSeenCounter,
136 repliesUnhydrated,
137 precedesChildReadMore,
138 followsReadMoreUp,
139}: TraversalMetadata): Extract<ThreadItem, {type: 'threadPost'}>['ui'] {
140 const isReplyAndHasReplies =
141 depth > 0 &&
142 repliesCount > 0 &&
143 (repliesCount - repliesUnhydrated === repliesSeenCounter ||
144 repliesSeenCounter > 0)
145 return {
146 isAnchor: depth === 0,
147 showParentReplyLine:
148 followsReadMoreUp ||
149 (!!prevItemDepth && prevItemDepth !== 0 && prevItemDepth < depth),
150 showChildReplyLine: depth < 0 || isReplyAndHasReplies,
151 indent: depth,
152 /*
153 * If there are no slices below this one, or the next slice has a depth <=
154 * than the depth of this post, it's the last child of the reply tree. It
155 * is not necessarily the last leaf in the parent branch, since it could
156 * have another sibling.
157 */
158 isLastChild,
159 skippedIndentIndices,
160 precedesChildReadMore: precedesChildReadMore ?? false,
161 }
162}
163
164export function getThreadPostNoUnauthenticatedUI({
165 depth,
166 prevItemDepth,
167}: {
168 depth: number
169 prevItemDepth?: number
170 nextItemDepth?: number
171}): Extract<ThreadItem, {type: 'threadPostNoUnauthenticated'}>['ui'] {
172 return {
173 showChildReplyLine: depth < 0,
174 showParentReplyLine: Boolean(prevItemDepth && prevItemDepth < depth),
175 }
176}