diff options
Diffstat (limited to 'src/state/queries/post-thread.ts')
-rw-r--r-- | src/state/queries/post-thread.ts | 177 |
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: ''} + } +} |