154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
import {
|
|
type Comment,
|
|
type CommentEventType,
|
|
type CreateCommentInput,
|
|
itemCreate,
|
|
itemDelete,
|
|
itemsList,
|
|
itemUpdate,
|
|
type UpdateCommentInput,
|
|
} from '@/api/comment.api.ts'
|
|
|
|
const PAGE_SIZE = 20
|
|
|
|
/**
|
|
* Comments of the currently opened issue, oldest first. Loaded chat-style:
|
|
* the latest page first, older pages are prepended on demand.
|
|
*/
|
|
export const useCommentStore = defineStore('comment', () => {
|
|
const issueId = ref<number | null>(null)
|
|
const comments = ref<Comment[]>([])
|
|
/** id of the oldest loaded comment while older ones exist on the server */
|
|
const olderCursor = ref<string | null>(null)
|
|
const loading = ref(false)
|
|
const loadingOlder = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
/** Inserts or replaces a comment by id. New comments are always the newest — append. */
|
|
function upsert(comment: Comment) {
|
|
const index = comments.value.findIndex((c) => c.id === comment.id)
|
|
if (index !== -1) comments.value[index] = comment
|
|
else comments.value.push(comment)
|
|
}
|
|
|
|
async function load(id: number) {
|
|
issueId.value = id
|
|
comments.value = []
|
|
olderCursor.value = null
|
|
loadingOlder.value = false
|
|
loading.value = true
|
|
try {
|
|
const res = await itemsList(id, { limit: PAGE_SIZE, direction: 'backward' })
|
|
// The issue may have changed while the request was in flight
|
|
if (issueId.value !== id) return
|
|
comments.value = res.list
|
|
olderCursor.value = res.nextCursor
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'error get list of comments'
|
|
} finally {
|
|
if (issueId.value === id) loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadOlder() {
|
|
const id = issueId.value
|
|
if (id === null || !olderCursor.value || loading.value || loadingOlder.value) return
|
|
loadingOlder.value = true
|
|
try {
|
|
const res = await itemsList(id, { limit: PAGE_SIZE, cursor: olderCursor.value, direction: 'backward' })
|
|
if (issueId.value !== id) return
|
|
const known = new Set(comments.value.map((c) => c.id))
|
|
comments.value.unshift(...res.list.filter((c) => !known.has(c.id)))
|
|
olderCursor.value = res.nextCursor
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'error get list of comments'
|
|
} finally {
|
|
if (issueId.value === id) loadingOlder.value = false
|
|
}
|
|
}
|
|
|
|
/** Fetches comments newer than the last loaded one — e.g. after an SSE reconnect. */
|
|
async function loadNewer() {
|
|
const id = issueId.value
|
|
if (id === null || loading.value) return
|
|
let cursor = comments.value.at(-1)?.id
|
|
if (!cursor) return load(id)
|
|
try {
|
|
do {
|
|
const res = await itemsList(id, { limit: PAGE_SIZE, cursor, direction: 'forward' })
|
|
if (issueId.value !== id) return
|
|
res.list.forEach(upsert)
|
|
cursor = res.nextCursor ?? undefined
|
|
} while (cursor)
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'error get list of comments'
|
|
}
|
|
}
|
|
|
|
async function create(input: CreateCommentInput) {
|
|
if (issueId.value === null) return
|
|
try {
|
|
const comment = await itemCreate(issueId.value, input)
|
|
// The SSE event for the same comment may come before or after — upsert dedupes
|
|
if (comment.issueId === issueId.value) upsert(comment)
|
|
return comment
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'error creating comment'
|
|
}
|
|
}
|
|
|
|
async function update(id: string, input: UpdateCommentInput) {
|
|
try {
|
|
const comment = await itemUpdate(id, input)
|
|
if (comment.issueId === issueId.value) upsert(comment)
|
|
return comment
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'error updating comment'
|
|
}
|
|
}
|
|
|
|
async function remove(id: string) {
|
|
try {
|
|
return await itemDelete(id)
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'error deleting comment'
|
|
return false
|
|
}
|
|
}
|
|
|
|
/** Applies a server-sent event. Deleted comments stay in the list as placeholders. */
|
|
function applyEvent(type: CommentEventType, comment: Comment) {
|
|
if (comment.issueId !== issueId.value) return
|
|
// Updates of comments older than the loaded window are irrelevant
|
|
if (type !== 'comment.created' && !comments.value.some((c) => c.id === comment.id)) return
|
|
upsert(comment)
|
|
}
|
|
|
|
function reset() {
|
|
issueId.value = null
|
|
comments.value = []
|
|
olderCursor.value = null
|
|
loading.value = false
|
|
loadingOlder.value = false
|
|
}
|
|
|
|
return {
|
|
issueId,
|
|
comments,
|
|
olderCursor,
|
|
loading,
|
|
loadingOlder,
|
|
error,
|
|
load,
|
|
loadOlder,
|
|
loadNewer,
|
|
create,
|
|
update,
|
|
remove,
|
|
applyEvent,
|
|
reset,
|
|
}
|
|
})
|