* Update to react-query v5 * Introduce post-feed react query * Add feed refresh behaviors * Only fetch feeds of visible pages * Implement polling for latest on feeds * Add moderation filtering to slices * Handle block errors * Update feed error messages * Remove old models * Replace simple-feed option with disable-tuner option * Add missing useMemo * Implement the mergefeed and fixes to polling * Correctly handle failed load more state * Improve error and empty state behaviors * Clearer naming
177 lines
4.8 KiB
TypeScript
177 lines
4.8 KiB
TypeScript
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>({
|
|
queryKey: RQKEY(uri || ''),
|
|
async queryFn() {
|
|
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: ''}
|
|
}
|
|
}
|