mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-01-12 02:21:49 +00:00
feat: forums posting, replies, likes, markdown
This commit is contained in:
parent
6c4be388a2
commit
6b32ae67e3
13 changed files with 228 additions and 51 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "6.0.12",
|
||||
"version": "6.0.13",
|
||||
"license": "BUSL-1.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.14.4",
|
||||
|
|
@ -13,7 +13,8 @@
|
|||
"lint": "eslint -c eslint.config.js",
|
||||
"lint:fix": "eslint -c eslint.config.js --fix",
|
||||
"gql:turbo": "node ./node_modules/gql.tada/bin/cli.js turbo",
|
||||
"gql:check": "node ./node_modules/gql.tada/bin/cli.js check"
|
||||
"gql:check": "node ./node_modules/gql.tada/bin/cli.js check",
|
||||
"gql:generate": "node --experimental-strip-types ./generateALIntrospection.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gql.tada/svelte-support": "^1.0.1",
|
||||
|
|
@ -64,6 +65,7 @@
|
|||
"idb-keyval": "^6.2.1",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"lucide-svelte": "^0.452.0",
|
||||
"marked": "^15.0.11",
|
||||
"p2pt": "^1.5.1",
|
||||
"semver": "^7.7.1",
|
||||
"simple-store-svelte": "^1.0.6",
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ importers:
|
|||
lucide-svelte:
|
||||
specifier: ^0.452.0
|
||||
version: 0.452.0(svelte@4.2.19)
|
||||
marked:
|
||||
specifier: ^15.0.11
|
||||
version: 15.0.11
|
||||
p2pt:
|
||||
specifier: ^1.5.1
|
||||
version: 1.5.1
|
||||
|
|
@ -1743,6 +1746,11 @@ packages:
|
|||
magic-string@0.30.12:
|
||||
resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
|
||||
|
||||
marked@15.0.11:
|
||||
resolution: {integrity: sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==}
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
mdn-data@2.0.30:
|
||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||
|
||||
|
|
@ -4228,6 +4236,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
marked@15.0.11: {}
|
||||
|
||||
mdn-data@2.0.30: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
<script lang='ts'>
|
||||
import dompurify from 'dompurify'
|
||||
import { marked } from 'marked'
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
pedantic: false
|
||||
})
|
||||
|
||||
export let html = ''
|
||||
let root: ShadowRoot | undefined
|
||||
|
||||
|
|
@ -15,10 +23,32 @@
|
|||
-webkit-user-drag: none;
|
||||
}`)
|
||||
|
||||
function sanitize (html: string) {
|
||||
return dompurify.sanitize(html, { ALLOWED_TAGS: ['a', 'b', 'blockquote', 'br', 'center', 'del', 'div', 'em', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'hr', 'i', 'img', 'li', 'ol', 'p', 'pre', 'code', 'span', 'strike', 'strong', 'ul'], ALLOWED_ATTR: ['align', 'height', 'href', 'src', 'target', 'width', 'rel'] })
|
||||
}
|
||||
|
||||
// i mean holy shit anilist, could you have made it any harder on yourself
|
||||
function shadow (node: HTMLDivElement, html: string) {
|
||||
root ??= node.attachShadow({ mode: 'closed' })
|
||||
root.adoptedStyleSheets = [style]
|
||||
root.innerHTML = dompurify.sanitize(html)
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
html = html.replace(/(http)(:([\/|.|\w|\s|-])*\.(?:jpg|.jpeg|gif|png|mp4|webm))/gi, '$1s$2')
|
||||
.replace(/img\s?(\d+%?)?\s?\((.[\S]+)\)/gi, "<img width='$1' src='$2'>")
|
||||
.replace(/(^|>| )@([A-Za-z0-9]+)/gm, "$1<a href='#'>@$2</a>")
|
||||
.replace(/youtube\s?\([^]*?([-_0-9A-Za-z]{10,15})[^]*?\)/gi, 'youtube ($1)')
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
.replace(/webm\s?\(h?([A-Za-z0-9-._~:\/?#\[\]@!$&()*+,;=%]+)\)/gi, 'webmv(`$1`)')
|
||||
.replace(/~{3}([^]*?)~{3}/gm, '+++$1+++')
|
||||
.replace(/~!([^]*?)!~/gm, '<div rel="spoiler">$1</div>')
|
||||
html = sanitize(marked.parse(html, { async: false }))
|
||||
.replace(/\+{3}([^]*?)\+{3}/gm, '<center>$1</center>')
|
||||
// t = t.replace(/<div rel="spoiler">([\s\S]*?)<\/div>/gm, "<p><span onclick='showSpoiler(this)' class='markdown-spoiler'><i class='hide-spoiler el-icon-circle-close' onclick='hideSpoiler(this)'></i><span>$1</span></span></p>")
|
||||
// t = t.replace(/youtube\s?\(([-_0-9A-Za-z]{10,15})\)/gi, "<span class='youtube' id='$1' style='width: 500px; height: 200px; max-width: 100%;'><span class='play'></span></span>")
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
.replace(/webmv\s?\(<code>([A-Za-z0-9-._~:\/?#\[\]@!$&()*+,;=%]+)<\/code>\)/gi, "<video muted loop controls><source src='h$1' type='video/webm'>Your browser does not support the video tag.</video>")
|
||||
// t = t.replace(/(?:<a href="https?:\/\/anilist.co\/(anime|manga)\/)([0-9]+).*?>(?:https?:\/\/anilist.co\/(?:anime|manga)\/[0-9]+).*?<\/a>/gm, '<span class="media-embed" data-media-type="$1" data-media-id="$2"></span>')
|
||||
|
||||
root.innerHTML = html
|
||||
}
|
||||
|
||||
let className: string | undefined | null
|
||||
|
|
|
|||
|
|
@ -1,49 +1,71 @@
|
|||
<script lang='ts'>
|
||||
import { Heart } from 'lucide-svelte'
|
||||
import { Heart, PenLine, Reply, Trash2 } from 'lucide-svelte'
|
||||
|
||||
import Shadow from '../../Shadow.svelte'
|
||||
import { Button, iconSizes } from '../button'
|
||||
|
||||
import { Write } from '.'
|
||||
|
||||
import type { ResultOf } from 'gql.tada'
|
||||
import type { CommentFrag } from '$lib/modules/anilist/queries'
|
||||
|
||||
import * as Avatar from '$lib/components/ui/avatar'
|
||||
import { since } from '$lib/utils'
|
||||
import { client } from '$lib/modules/anilist'
|
||||
|
||||
export let comment: ResultOf<typeof CommentFrag>
|
||||
export let depth = 0
|
||||
export let isLocked = false
|
||||
export let threadId: number
|
||||
export let rootCommentId = comment.id
|
||||
|
||||
let childComments: Array<ResultOf<typeof CommentFrag>>
|
||||
$: childComments = comment.childComments as Array<ResultOf<typeof CommentFrag>> | null ?? []
|
||||
|
||||
const viewer = client.viewer
|
||||
</script>
|
||||
|
||||
<div class='rounded-md {depth % 2 === 1 ? 'bg-black' : 'bg-neutral-950'} text-secondary-foreground flex w-full py-6 px-8 flex-col'>
|
||||
<div class='rounded-md {depth % 2 === 1 ? 'bg-black' : 'bg-neutral-950'} text-secondary-foreground flex w-full py-4 px-6 flex-col'>
|
||||
<div class='flex w-full justify-between text-xl'>
|
||||
<div class='font-bold mb-2 line-clamp-1 flex leading-none items-center'>
|
||||
<Avatar.Root class='inline-block size-8 mr-4'>
|
||||
<div class='font-bold mb-2 line-clamp-1 flex leading-none items-center text-[16px]'>
|
||||
<Avatar.Root class='inline-block size-5 mr-2'>
|
||||
<Avatar.Image src={comment.user?.avatar?.medium ?? ''} alt={comment.user?.name ?? 'N/A'} />
|
||||
<Avatar.Fallback>{comment.user?.name ?? 'N/A'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
{comment.user?.name ?? 'N/A'}
|
||||
</div>
|
||||
<div class='flex ml-2 text-[12.8px] leading-none mt-0.5'>
|
||||
<div class='flex ml-2 text-[12.8px] leading-none '>
|
||||
<Heart size='12' class='mr-1' />
|
||||
{comment.likeCount}
|
||||
</div>
|
||||
</div>
|
||||
<Shadow html={comment.comment ?? ''} class='my-3 text-muted-foreground leading-relaxed [&_*]:flex [&_*]:flex-col [&_br]:hidden w-full overflow-clip' />
|
||||
<Shadow html={comment.comment ?? ''} class='text-muted-foreground text-sm [&_*]:flex [&_*]:flex-col [&_br]:hidden w-full overflow-clip' />
|
||||
{#each childComments as comment (comment.id)}
|
||||
{#if comment}
|
||||
<div class='py-2'>
|
||||
<svelte:self {comment} depth={depth + 1} />
|
||||
<svelte:self {comment} depth={depth + 1} {isLocked} {threadId} {rootCommentId} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class='flex w-full justify-between mt-auto text-[9.6px]'>
|
||||
<div class='pt-2 flex items-end'>
|
||||
{since(new Date(comment.createdAt * 1000))}
|
||||
</div>
|
||||
<div class='ml-auto inline-flex flex-wrap gap-2 items-end'>
|
||||
Reply
|
||||
<div class='flex items-center leading-none'>
|
||||
<Button size='icon-sm' variant='ghost' class='mr-1' on:click={() => client.toggleLike(comment.id, 'THREAD_COMMENT', !!comment.isLiked)} disabled={isLocked || !$viewer?.viewer}>
|
||||
<Heart fill={comment.isLiked ? 'currentColor' : 'transparent'} size={iconSizes['icon-sm']} />
|
||||
</Button>
|
||||
<Write parentCommentId={comment.id} {threadId} {isLocked} {rootCommentId}>
|
||||
<Reply size={iconSizes['icon-sm']} />
|
||||
</Write>
|
||||
{#if $viewer?.viewer?.id === comment.user?.id}
|
||||
<Write id={comment.id} {threadId} {isLocked} {rootCommentId} value={comment.comment ?? ''}>
|
||||
<PenLine size={iconSizes['icon-sm']} />
|
||||
</Write>
|
||||
<Button size='icon-sm' variant='ghost' class='mr-1' on:click={() => client.deleteComment(comment.id, rootCommentId)} disabled={isLocked || !$viewer?.viewer}>
|
||||
<Trash2 size={iconSizes['icon-sm']} />
|
||||
</Button>
|
||||
{/if}
|
||||
<span class='ml-2'>
|
||||
{since(new Date(comment.createdAt * 1000))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@
|
|||
import type { client } from '$lib/modules/anilist'
|
||||
|
||||
export let comments: ReturnType<typeof client.comments>
|
||||
|
||||
export let isLocked = false
|
||||
export let threadId: number
|
||||
</script>
|
||||
|
||||
{#each $comments.data?.Page?.threadComments ?? [] as comment, i (comment?.id ?? i)}
|
||||
{#if comment}
|
||||
<Comment {comment} />
|
||||
<Comment {comment} {isLocked} {threadId} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script lang='ts'>
|
||||
import { ChevronLeft, ChevronRight, Eye, Heart, MessagesSquare } from 'lucide-svelte'
|
||||
import { ChevronLeft, ChevronRight, Eye, Heart, MessagesSquare, Lock } from 'lucide-svelte'
|
||||
|
||||
import Pagination from '../../Pagination.svelte'
|
||||
import { Button } from '../button'
|
||||
|
||||
import { client, type Media } from '$lib/modules/anilist'
|
||||
import { isMobile, since } from '$lib/utils'
|
||||
import { dragScroll } from '$lib/modules/navigate'
|
||||
import * as Tooltip from '$lib/components/ui/tooltip'
|
||||
import * as Avatar from '$lib/components/ui/avatar'
|
||||
|
||||
|
|
@ -36,8 +35,8 @@
|
|||
</script>
|
||||
|
||||
<Pagination {count} {perPage} bind:currentPage let:pages let:hasNext let:hasPrev let:range let:setPage siblingCount={1}>
|
||||
<div class='overflow-y-auto pt-3 -mx-14 px-14 pointer-events-none -mb-3 pb-3' use:dragScroll>
|
||||
<div class='grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(500px,1fr))] place-items-center gap-x-10 gap-y-7 justify-center align-middle pointer-events-auto'>
|
||||
<div class='pt-3'>
|
||||
<div class='grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(500px,1fr))] place-items-center gap-x-10 gap-y-7 justify-center align-middle'>
|
||||
{#if $currentPageStore.fetching}
|
||||
Loading threads...
|
||||
{:else if $currentPageStore.error}
|
||||
|
|
@ -70,6 +69,9 @@
|
|||
{thread.viewCount ?? 0}
|
||||
<MessagesSquare size='12' class='mr-1 ml-2' />
|
||||
{thread.replyCount ?? 0}
|
||||
{#if thread.isLocked}
|
||||
<Lock size='12' class='mr-1 ml-2 text-red-500' />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex w-full justify-between mt-auto text-[9.6px]'>
|
||||
|
|
|
|||
47
src/lib/components/ui/forums/Write.svelte
Normal file
47
src/lib/components/ui/forums/Write.svelte
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<script lang='ts'>
|
||||
import { Button } from '../button'
|
||||
import { Textarea } from '../textarea'
|
||||
|
||||
import { client } from '$lib/modules/anilist'
|
||||
import * as Drawer from '$lib/components/ui/drawer'
|
||||
|
||||
export let isLocked = false
|
||||
|
||||
const viewer = client.viewer
|
||||
|
||||
export let threadId: number | undefined = undefined
|
||||
export let parentCommentId: number | undefined = undefined
|
||||
export let rootCommentId: number | undefined = undefined
|
||||
export let id: number | undefined = undefined
|
||||
|
||||
export let value = ''
|
||||
|
||||
const placeholder = 'Write a comment on AniList \n\nDO NOT ASK FOR HELP HERE!\n\nAsking questions such as "why isnt X playing" or "why cant i find any torrents" !__WILL GET YOU BANNED__!\n\nTHIS IS A 3RD PARTY FORUM!'
|
||||
|
||||
function comment () {
|
||||
client.comment({ threadId, id, parentCommentId, comment: value, rootCommentId })
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer.Root portal='html'>
|
||||
<Drawer.Trigger asChild let:builder>
|
||||
<Button size='icon-sm' variant='ghost' class='mr-1' disabled={isLocked || !$viewer?.viewer} builders={[builder]}>
|
||||
<slot />
|
||||
</Button>
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content tabindex={null} class='px-20 py-10 gap-4'>
|
||||
<Textarea class='form-control w-full shrink-0 min-h-56 bg-dark' {placeholder} bind:value />
|
||||
<div class='flex gap-2 justify-end'>
|
||||
<Drawer.Close asChild let:builder>
|
||||
<Button variant='secondary' builders={[builder]}>
|
||||
Close
|
||||
</Button>
|
||||
</Drawer.Close>
|
||||
<Drawer.Close asChild let:builder>
|
||||
<Button builders={[builder]} on:click={comment}>
|
||||
Send
|
||||
</Button>
|
||||
</Drawer.Close>
|
||||
</div>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export { default as Comment } from './Comment.svelte'
|
||||
export { default as Threads } from './Threads.svelte'
|
||||
export { default as Comments } from './Comments.svelte'
|
||||
export { default as Write } from './Write.svelte'
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { derived } from 'svelte/store'
|
|||
import lavenshtein from 'js-levenshtein'
|
||||
|
||||
import schema from './schema.json' with { type: 'json' }
|
||||
import { CommentFrag, Comments, CustomLists, DeleteEntry, Entry, Following, FullMedia, FullMediaList, IDMedia, Schedule, Search, ThreadFrag, Threads, ToggleFavourite, UserLists, Viewer } from './queries'
|
||||
import { CommentFrag, Comments, CustomLists, DeleteEntry, DeleteThreadComment, Entry, Following, FullMedia, FullMediaList, IDMedia, SaveThreadComment, Schedule, Search, ThreadFrag, Threads, ToggleFavourite, ToggleLike, UserLists, Viewer } from './queries'
|
||||
import { currentSeason, currentYear, lastSeason, lastYear, nextSeason, nextYear } from './util'
|
||||
import gql from './gql'
|
||||
|
||||
|
|
@ -127,6 +127,24 @@ class AnilistClient {
|
|||
targetList.entries.push(oldEntry)
|
||||
return { ...data, MediaListCollection: { ...data.MediaListCollection, lists } }
|
||||
})
|
||||
},
|
||||
SaveThreadComment: (_result, args, cache, _info) => {
|
||||
if (_info.variables.rootCommentId) {
|
||||
const id = _info.variables.rootCommentId as number
|
||||
cache.invalidate({
|
||||
__typename: 'ThreadComment',
|
||||
id
|
||||
})
|
||||
} else {
|
||||
cache.invalidate('ThreadComment')
|
||||
}
|
||||
},
|
||||
DeleteThreadComment: (_result, args, cache, _info) => {
|
||||
const id = (_info.variables.rootCommentId ?? args.id) as number
|
||||
cache.invalidate({
|
||||
__typename: 'ThreadComment',
|
||||
id
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -182,11 +200,12 @@ class AnilistClient {
|
|||
// @ts-expect-error idk whats wrong here but it works correctly
|
||||
const likableUnion = cache.readFragment(likable === 'THREAD' ? ThreadFrag : CommentFrag, { id: threadOrCommentId })
|
||||
|
||||
if (!likableUnion) return {}
|
||||
if (!likableUnion) return null
|
||||
|
||||
return {
|
||||
id: threadOrCommentId,
|
||||
isLiked: !likableUnion.isLiked,
|
||||
likeCount: likableUnion.likeCount + (likableUnion.isLiked ? -1 : 1),
|
||||
__typename: likable === 'THREAD' ? 'Thread' : 'ThreadComment'
|
||||
}
|
||||
}
|
||||
|
|
@ -427,6 +446,18 @@ class AnilistClient {
|
|||
comments (threadId: number, page = 1) {
|
||||
return queryStore({ client: this.client, query: Comments, variables: { threadId, page } })
|
||||
}
|
||||
|
||||
async toggleLike (id: number, type: 'THREAD' | 'THREAD_COMMENT' | 'ACTIVITY' | 'ACTIVITY_REPLY', wasLiked: boolean) {
|
||||
return await this.client.mutation(ToggleLike, { id, type, wasLiked })
|
||||
}
|
||||
|
||||
async comment (variables: VariablesOf<typeof SaveThreadComment> & { rootCommentId?: number }) {
|
||||
return await this.client.mutation(SaveThreadComment, variables)
|
||||
}
|
||||
|
||||
async deleteComment (id: number, rootCommentId: number) {
|
||||
return await this.client.mutation(DeleteThreadComment, { id, rootCommentId })
|
||||
}
|
||||
}
|
||||
|
||||
// sveltekit/vite does the funny and evaluates at compile, this is a hack to fix development mode
|
||||
|
|
|
|||
20
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
20
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
|
|
@ -34,18 +34,22 @@ declare module 'gql.tada' {
|
|||
TadaDocumentNode<{ ToggleFavourite: { anime: { nodes: ({ id: number; } | null)[] | null; } | null; } | null; }, { id: number; }, void>;
|
||||
"\n fragment ThreadFrag on Thread @_unmask {\n id,\n title,\n body(asHtml: true),\n userId,\n replyCount,\n viewCount,\n isLocked,\n isSubscribed,\n isLiked,\n likeCount,\n repliedAt,\n createdAt,\n user {\n id,\n name,\n avatar {\n medium\n }\n },\n categories {\n id,\n name\n }\n }\n":
|
||||
TadaDocumentNode<{ id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; }, {}, { fragment: "ThreadFrag"; on: "Thread"; masked: false; }>;
|
||||
"\n query Threads($id: Int, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n hasNextPage\n },\n threads(mediaCategoryId: $id, sort: ID_DESC) {\n ...ThreadFrag\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ Page: { pageInfo: { hasNextPage: boolean | null; } | null; threads: ({ id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; } | null)[] | null; } | null; }, { perPage?: number | null | undefined; page?: number | null | undefined; id?: number | null | undefined; }, void>;
|
||||
"\n query Thread($threadId: Int) {\n Thread(id: $threadId) {\n ...ThreadFrag\n }\n }\n":
|
||||
TadaDocumentNode<{ Thread: { id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; } | null; }, { threadId?: number | null | undefined; }, void>;
|
||||
"\n fragment CommentFrag on ThreadComment @_unmask {\n id,\n comment(asHtml: true),\n isLiked,\n likeCount,\n createdAt,\n user {\n id,\n name,\n moderatorRoles,\n avatar {\n medium\n },\n },\n childComments,\n isLocked\n }\n":
|
||||
"\n query Threads($id: Int!, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n hasNextPage\n },\n threads(mediaCategoryId: $id, sort: ID_DESC) {\n ...ThreadFrag\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ Page: { pageInfo: { hasNextPage: boolean | null; } | null; threads: ({ id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; } | null)[] | null; } | null; }, { perPage?: number | null | undefined; page?: number | null | undefined; id: number; }, void>;
|
||||
"\n query Thread($threadId: Int!) {\n Thread(id: $threadId) {\n ...ThreadFrag\n }\n }\n":
|
||||
TadaDocumentNode<{ Thread: { id: number; title: string | null; body: string | null; userId: number; replyCount: number | null; viewCount: number | null; isLocked: boolean | null; isSubscribed: boolean | null; isLiked: boolean | null; likeCount: number; repliedAt: number | null; createdAt: number; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; categories: ({ id: number; name: string; } | null)[] | null; } | null; }, { threadId: number; }, void>;
|
||||
"\n fragment CommentFrag on ThreadComment @_unmask {\n id,\n comment(asHtml: true),\n isLiked,\n likeCount,\n createdAt,\n user {\n id,\n name,\n moderatorRoles,\n avatar {\n medium\n },\n },\n childComments{\n id\n },\n isLocked\n }\n":
|
||||
TadaDocumentNode<{ id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; name: string; moderatorRoles: ("ADMIN" | "LEAD_DEVELOPER" | "DEVELOPER" | "LEAD_COMMUNITY" | "COMMUNITY" | "DISCORD_COMMUNITY" | "LEAD_ANIME_DATA" | "ANIME_DATA" | "LEAD_MANGA_DATA" | "MANGA_DATA" | "LEAD_SOCIAL_MEDIA" | "SOCIAL_MEDIA" | "RETIRED" | "CHARACTER_DATA" | "STAFF_DATA" | null)[] | null; avatar: { medium: string | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; }, {}, { fragment: "CommentFrag"; on: "ThreadComment"; masked: false; }>;
|
||||
"\n query Comments($threadId: Int, $page: Int) {\n Page(page: $page, perPage: 15) {\n pageInfo {\n hasNextPage\n }\n threadComments(threadId: $threadId) {\n id,\n comment(asHtml: true),\n isLiked,\n likeCount,\n createdAt,\n user {\n id,\n name,\n moderatorRoles,\n avatar {\n medium\n },\n },\n childComments,\n isLocked\n }\n }\n }\n":
|
||||
"\n query Comments($threadId: Int, $page: Int) {\n Page(page: $page, perPage: 15) {\n pageInfo {\n hasNextPage\n }\n threadComments(threadId: $threadId) {\n id,\n comment(asHtml: true),\n isLiked,\n likeCount,\n createdAt,\n user {\n id,\n name,\n moderatorRoles,\n avatar {\n medium\n },\n },\n childComments{\n id\n },\n isLocked\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ Page: { pageInfo: { hasNextPage: boolean | null; } | null; threadComments: ({ id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; name: string; moderatorRoles: ("ADMIN" | "LEAD_DEVELOPER" | "DEVELOPER" | "LEAD_COMMUNITY" | "COMMUNITY" | "DISCORD_COMMUNITY" | "LEAD_ANIME_DATA" | "ANIME_DATA" | "LEAD_MANGA_DATA" | "MANGA_DATA" | "LEAD_SOCIAL_MEDIA" | "SOCIAL_MEDIA" | "RETIRED" | "CHARACTER_DATA" | "STAFF_DATA" | null)[] | null; avatar: { medium: string | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; } | null)[] | null; } | null; }, { page?: number | null | undefined; threadId?: number | null | undefined; }, void>;
|
||||
"\n query User ($id: Int) {\n User(id: $id) {\n id,\n name,\n avatar {\n large\n },\n bannerImage,\n about(asHtml: true),\n isFollowing,\n isFollower,\n donatorBadge,\n createdAt,\n options {\n profileColor\n },\n statistics {\n anime {\n count,\n minutesWatched,\n episodesWatched,\n genrePreview: genres(limit: 10, sort: COUNT_DESC) {\n genre,\n count\n }\n }\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ User: { id: number; name: string; avatar: { large: string | null; } | null; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; createdAt: number | null; options: { profileColor: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genrePreview: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; } | null; }, { id?: number | null | undefined; }, void>;
|
||||
"\n mutation ToggleLike ($id: Int, $type: LikeableType) {\n ToggleLikeV2(id: $id, type: $type) {\n ... on Thread {\n id\n likeCount\n isLiked\n }\n ... on ThreadComment {\n id\n likeCount\n isLiked\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ ToggleLikeV2: { __typename?: "ActivityReply" | undefined; } | { __typename?: "ListActivity" | undefined; } | { __typename?: "MessageActivity" | undefined; } | { __typename?: "TextActivity" | undefined; } | { __typename?: "Thread" | undefined; id: number; likeCount: number; isLiked: boolean | null; } | { __typename?: "ThreadComment" | undefined; id: number; likeCount: number; isLiked: boolean | null; } | null; }, { type?: "THREAD" | "THREAD_COMMENT" | "ACTIVITY" | "ACTIVITY_REPLY" | null | undefined; id?: number | null | undefined; }, void>;
|
||||
"\n mutation ToggleLike ($id: Int!, $type: LikeableType!) {\n ToggleLikeV2(id: $id, type: $type) {\n ... on Thread {\n id\n likeCount\n isLiked\n }\n ... on ThreadComment {\n id\n likeCount\n isLiked\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ ToggleLikeV2: { __typename?: "ActivityReply" | undefined; } | { __typename?: "ListActivity" | undefined; } | { __typename?: "MessageActivity" | undefined; } | { __typename?: "TextActivity" | undefined; } | { __typename?: "Thread" | undefined; id: number; likeCount: number; isLiked: boolean | null; } | { __typename?: "ThreadComment" | undefined; id: number; likeCount: number; isLiked: boolean | null; } | null; }, { type: "THREAD" | "THREAD_COMMENT" | "ACTIVITY" | "ACTIVITY_REPLY"; id: number; }, void>;
|
||||
"\n mutation SaveThreadComment ($id: Int, $threadId: Int, $parentCommentId: Int, $comment: String) {\n SaveThreadComment(id: $id, threadId: $threadId, parentCommentId: $parentCommentId, comment: $comment) {\n ...CommentFrag\n }\n }\n":
|
||||
TadaDocumentNode<{ SaveThreadComment: { id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; name: string; moderatorRoles: ("ADMIN" | "LEAD_DEVELOPER" | "DEVELOPER" | "LEAD_COMMUNITY" | "COMMUNITY" | "DISCORD_COMMUNITY" | "LEAD_ANIME_DATA" | "ANIME_DATA" | "LEAD_MANGA_DATA" | "MANGA_DATA" | "LEAD_SOCIAL_MEDIA" | "SOCIAL_MEDIA" | "RETIRED" | "CHARACTER_DATA" | "STAFF_DATA" | null)[] | null; avatar: { medium: string | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; } | null; }, { comment?: string | null | undefined; parentCommentId?: number | null | undefined; threadId?: number | null | undefined; id?: number | null | undefined; }, void>;
|
||||
"\n mutation DeleteThreadComment ($id: Int) {\n DeleteThreadComment(id: $id) {\n deleted\n }\n }\n":
|
||||
TadaDocumentNode<{ DeleteThreadComment: { deleted: boolean | null; } | null; }, { id?: number | null | undefined; }, void>;
|
||||
"fragment Med on Media {id, isFavourite}":
|
||||
TadaDocumentNode<{ id: number; isFavourite: boolean; }, {}, { fragment: "Med"; on: "Media"; masked: true; }>;
|
||||
"fragment Med on Media {id, mediaListEntry {status, progress, repeat, score, customLists }}":
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ export const ThreadFrag = gql(`
|
|||
fragment ThreadFrag on Thread @_unmask {
|
||||
id,
|
||||
title,
|
||||
body(asHtml: true),
|
||||
body,
|
||||
userId,
|
||||
replyCount,
|
||||
viewCount,
|
||||
|
|
@ -331,7 +331,7 @@ export const ThreadFrag = gql(`
|
|||
`)
|
||||
|
||||
export const Threads = gql(`
|
||||
query Threads($id: Int, $page: Int, $perPage: Int) {
|
||||
query Threads($id: Int!, $page: Int, $perPage: Int) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
|
|
@ -344,7 +344,7 @@ export const Threads = gql(`
|
|||
`, [ThreadFrag])
|
||||
|
||||
export const Thread = gql(`
|
||||
query Thread($threadId: Int) {
|
||||
query Thread($threadId: Int!) {
|
||||
Thread(id: $threadId) {
|
||||
...ThreadFrag
|
||||
}
|
||||
|
|
@ -354,7 +354,7 @@ export const Thread = gql(`
|
|||
export const CommentFrag = gql(`
|
||||
fragment CommentFrag on ThreadComment @_unmask {
|
||||
id,
|
||||
comment(asHtml: true),
|
||||
comment,
|
||||
isLiked,
|
||||
likeCount,
|
||||
createdAt,
|
||||
|
|
@ -380,7 +380,7 @@ export const Comments = gql(`
|
|||
}
|
||||
threadComments(threadId: $threadId) {
|
||||
id,
|
||||
comment(asHtml: true),
|
||||
comment,
|
||||
isLiked,
|
||||
likeCount,
|
||||
createdAt,
|
||||
|
|
@ -408,7 +408,7 @@ export const User = gql(`
|
|||
large
|
||||
},
|
||||
bannerImage,
|
||||
about(asHtml: true),
|
||||
about,
|
||||
isFollowing,
|
||||
isFollower,
|
||||
donatorBadge,
|
||||
|
|
@ -432,7 +432,7 @@ export const User = gql(`
|
|||
`)
|
||||
|
||||
export const ToggleLike = gql(`
|
||||
mutation ToggleLike ($id: Int, $type: LikeableType) {
|
||||
mutation ToggleLike ($id: Int!, $type: LikeableType!) {
|
||||
ToggleLikeV2(id: $id, type: $type) {
|
||||
... on Thread {
|
||||
id
|
||||
|
|
@ -447,3 +447,19 @@ export const ToggleLike = gql(`
|
|||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const SaveThreadComment = gql(`
|
||||
mutation SaveThreadComment ($id: Int, $threadId: Int, $parentCommentId: Int, $comment: String) {
|
||||
SaveThreadComment(id: $id, threadId: $threadId, parentCommentId: $parentCommentId, comment: $comment) {
|
||||
...CommentFrag
|
||||
}
|
||||
}
|
||||
`, [CommentFrag])
|
||||
|
||||
export const DeleteThreadComment = gql(`
|
||||
mutation DeleteThreadComment ($id: Int) {
|
||||
DeleteThreadComment(id: $id) {
|
||||
deleted
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,14 +1,12 @@
|
|||
<script lang='ts'>
|
||||
import { ChevronLeft, Eye, MessagesSquare, Heart } from 'lucide-svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { ChevronLeft, Eye, MessagesSquare, Heart, Lock, Reply } from 'lucide-svelte'
|
||||
|
||||
import type { PageData } from './$types'
|
||||
|
||||
import * as Avatar from '$lib/components/ui/avatar'
|
||||
import { since } from '$lib/utils'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { Comments } from '$lib/components/ui/forums'
|
||||
import { ToggleLike } from '$lib/modules/anilist/queries'
|
||||
import { Button, iconSizes } from '$lib/components/ui/button'
|
||||
import { Comments, Write } from '$lib/components/ui/forums'
|
||||
import { client } from '$lib/modules/anilist'
|
||||
import Shadow from '$lib/components/Shadow.svelte'
|
||||
|
||||
|
|
@ -28,15 +26,15 @@
|
|||
commentQueries = commentQueries
|
||||
}
|
||||
|
||||
onMount(loadComments)
|
||||
$: commentQueries = [client.comments(thread.id, 1)]
|
||||
|
||||
$: anime = data.anime
|
||||
$: media = $anime.Media!
|
||||
|
||||
const x = async () => await client.client.mutation(ToggleLike, { id: thread.id, type: 'THREAD' })
|
||||
|
||||
$: latestQuery = commentQueries[commentQueries.length - 1]
|
||||
$: hasMore = $latestQuery?.data?.Page?.pageInfo?.hasNextPage ?? false
|
||||
|
||||
const viewer = client.viewer
|
||||
</script>
|
||||
|
||||
<div class='flex items-center w-full'>
|
||||
|
|
@ -55,18 +53,29 @@
|
|||
{thread.user?.name ?? 'N/A'}
|
||||
</div>
|
||||
<div class='flex ml-2 text-[12.8px] leading-none mt-0.5'>
|
||||
<Heart size='12' class='mr-1' />
|
||||
<Heart size='12' class='mr-1' fill={thread.isLiked ? 'currentColor' : 'transparent'} />
|
||||
{thread.likeCount}
|
||||
<Eye size='12' class='mr-1 ml-2' />
|
||||
{thread.viewCount ?? 0}
|
||||
<MessagesSquare size='12' class='mr-1 ml-2' />
|
||||
{thread.replyCount ?? 0}
|
||||
{#if thread.isLocked}
|
||||
<Lock size='12' class='mr-1 ml-2 text-red-500' />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Shadow html={thread.body ?? ''} class='my-3 text-muted-foreground leading-relaxed [&_*]:flex [&_*]:flex-col [&_br]:hidden overflow-clip' />
|
||||
<Shadow html={thread.body ?? ''} class='my-3 text-muted-foreground [&_*]:flex [&_*]:flex-col [&_br]:hidden overflow-clip' />
|
||||
<div class='flex w-full justify-between mt-auto text-[9.6px]'>
|
||||
<div class='pt-2 flex items-end'>
|
||||
{since(new Date(thread.createdAt * 1000))}
|
||||
<div class='flex items-center leading-none'>
|
||||
<Button size='icon-sm' variant='ghost' class='mr-1' on:click={() => client.toggleLike(thread.id, 'THREAD', !!thread.isLiked)} disabled={!!thread.isLocked || !$viewer?.viewer}>
|
||||
<Heart fill={thread.isLiked ? 'currentColor' : 'transparent'} size={iconSizes['icon-sm']} />
|
||||
</Button>
|
||||
<Write threadId={thread.id} isLocked={!!thread.isLocked}>
|
||||
<Reply size={iconSizes['icon-sm']} />
|
||||
</Write>
|
||||
<span class='ml-2'>
|
||||
{since(new Date(thread.createdAt * 1000))}
|
||||
</span>
|
||||
</div>
|
||||
<div class='ml-auto inline-flex flex-wrap gap-2 items-end'>
|
||||
{#each thread.categories?.filter(category => category?.name !== 'Anime') ?? [] as category, i (category?.id ?? i)}
|
||||
|
|
@ -84,7 +93,7 @@
|
|||
{thread.replyCount} Replies
|
||||
</div>
|
||||
{#each commentQueries as comments, i (i)}
|
||||
<Comments bind:comments />
|
||||
<Comments bind:comments isLocked={!!thread.isLocked} threadId={thread.id} />
|
||||
{/each}
|
||||
{#if hasMore}
|
||||
<Button size='lg' class='w-full font-bold' on:click={loadComments}>Load more comments</Button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue