From 8999a22f6ddb0d22a33f18ca47ce71869297ea02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=9A=E7=80=9A=E5=90=9B?= <41739624+bohanjun@users.noreply.github.com> Date: Fri, 21 Jul 2023 00:20:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=9E=E5=A4=8D=E5=8F=96=E4=B8=8B=E4=B8=8E?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/viewer/src/app/[id]/[page]/page.tsx | 18 ++- .../src/app/[id]/context/[user]/route.ts | 36 ++++++ packages/viewer/src/app/[id]/replies/route.ts | 19 +++- packages/viewer/src/app/bootstrap.scss | 12 ++ .../viewer/src/app/r/[rid]/get-reply-raw.ts | 11 ++ packages/viewer/src/lib/luogu.ts | 12 ++ packages/viewer/src/lib/serialize-reply.ts | 107 +++++++++++++++++- 7 files changed, 207 insertions(+), 8 deletions(-) diff --git a/packages/viewer/src/app/[id]/[page]/page.tsx b/packages/viewer/src/app/[id]/[page]/page.tsx index 3b1ff69..0a411c8 100644 --- a/packages/viewer/src/app/[id]/[page]/page.tsx +++ b/packages/viewer/src/app/[id]/[page]/page.tsx @@ -29,7 +29,23 @@ export default async function Page({ take: 1, }, replies: { - select: { id: true, author: true, time: true, content: true }, + select: { + id: true, + author: true, + time: true, + content: true, + takedown: { + select: { + submitter: { + select: { + id: true, + username: true, + }, + }, + reason: true, + }, + }, + }, orderBy: { id: "asc" }, skip: (page - 1) * REPLIES_PER_PAGE, take: REPLIES_PER_PAGE, diff --git a/packages/viewer/src/app/[id]/context/[user]/route.ts b/packages/viewer/src/app/[id]/context/[user]/route.ts index d9e0b00..47c6639 100644 --- a/packages/viewer/src/app/[id]/context/[user]/route.ts +++ b/packages/viewer/src/app/[id]/context/[user]/route.ts @@ -14,11 +14,47 @@ export async function GET( /* eslint-enable @typescript-eslint/no-non-null-assertion */ const reply = await (offset <= 0 ? prisma.reply.findFirst({ + select: { + id: true, + author: true, + time: true, + content: true, + discussionId: true, + takedown: { + select: { + submitter: { + select: { + id: true, + username: true, + }, + }, + reason: true, + }, + }, + }, where: { discussionId, authorId, id: { lt: replyId } }, orderBy: { id: "desc" }, skip: -offset, }) : prisma.reply.findFirst({ + select: { + id: true, + author: true, + time: true, + content: true, + discussionId: true, + takedown: { + select: { + submitter: { + select: { + id: true, + username: true, + }, + }, + reason: true, + }, + }, + }, where: { discussionId, authorId, id: { gt: replyId } }, orderBy: { id: "asc" }, skip: offset - 1, diff --git a/packages/viewer/src/app/[id]/replies/route.ts b/packages/viewer/src/app/[id]/replies/route.ts index 8523812..f4a7cd4 100644 --- a/packages/viewer/src/app/[id]/replies/route.ts +++ b/packages/viewer/src/app/[id]/replies/route.ts @@ -10,11 +10,28 @@ export async function GET( const cursor = request.nextUrl.searchParams.get("cursor"); const limit = request.nextUrl.searchParams.get("limit"); const replies = await prisma.reply.findMany({ + select: { + id: true, + author: true, + time: true, + content: true, + discussionId: true, + takedown: { + select: { + submitter: { + select: { + id: true, + username: true, + }, + }, + reason: true, + }, + }, + }, where: { discussionId: id, id: { gt: cursor ? parseInt(cursor, 10) : undefined }, }, - select: { id: true, time: true, author: true, content: true }, take: parseInt(limit ?? "10", 10), }); return NextResponse.json({ diff --git a/packages/viewer/src/app/bootstrap.scss b/packages/viewer/src/app/bootstrap.scss index 7ce67cd..d0e20a6 100644 --- a/packages/viewer/src/app/bootstrap.scss +++ b/packages/viewer/src/app/bootstrap.scss @@ -234,3 +234,15 @@ details[open] summary .open-show-inline { bottom: -2.8em; } } + +.link-at-user { + text-decoration: none; +} + +.link-at-user:hover { + text-decoration: underline; +} + +.link-at-user-unstored { + color: #00b5ad; +} diff --git a/packages/viewer/src/app/r/[rid]/get-reply-raw.ts b/packages/viewer/src/app/r/[rid]/get-reply-raw.ts index 1ad7bfb..087b63a 100644 --- a/packages/viewer/src/app/r/[rid]/get-reply-raw.ts +++ b/packages/viewer/src/app/r/[rid]/get-reply-raw.ts @@ -18,6 +18,17 @@ export default async (id: number) => }, }, }, + takedown: { + select: { + submitter: { + select: { + id: true, + username: true, + }, + }, + reason: true, + }, + }, }, where: { id }, })) ?? notFound(); diff --git a/packages/viewer/src/lib/luogu.ts b/packages/viewer/src/lib/luogu.ts index 1ce69bd..4df4efc 100644 --- a/packages/viewer/src/lib/luogu.ts +++ b/packages/viewer/src/lib/luogu.ts @@ -42,6 +42,18 @@ export function getUserIdFromUrl(target: URL) { return Number.isNaN(uid) ? null : uid; } +export function getDiscussionIdFromUrl(target: URL) { + if (!isLuoguUrl(target)) return null; + const discussionId = parseInt( + (target.pathname.startsWith("/discuss/") && + target.pathname.split("/")[2]) || + ((target.pathname === "/discuss/show" && + target.searchParams.get("postid")) as string), + 10, + ); + return Number.isNaN(discussionId) ? null : discussionId; +} + export function getDiscussionId(s: string) { const id = parseInt(s, 10); if (!Number.isNaN(id)) return id; diff --git a/packages/viewer/src/lib/serialize-reply.ts b/packages/viewer/src/lib/serialize-reply.ts index 6b22a33..90006f4 100644 --- a/packages/viewer/src/lib/serialize-reply.ts +++ b/packages/viewer/src/lib/serialize-reply.ts @@ -1,8 +1,13 @@ import { JSDOM } from "jsdom"; import hljs from "highlight.js"; -import type { User } from "@prisma/client"; +import type { Discussion, ReplyTakedown, User } from "@prisma/client"; import prisma from "@/lib/prisma"; -import { getUserIdFromUrl, getUserUrl } from "@/lib/luogu"; +import { + getDiscussionIdFromUrl, + getUserIdFromUrl, + getUserUrl, + getUserRealUrl, +} from "@/lib/luogu"; import stringifyTime from "@/lib/time"; export type UserMetioned = User & { numReplies?: number }; @@ -16,6 +21,26 @@ function getMentionedUser(element: Element) { ); } +function getDiscussionUrl(discussionId: number) { + return `/${discussionId}`; +} + +function getHtmlTookdown(takedown: { + submitter: { id: number; username: string }; + reason: string; +}) { + return `
该回复已按 + + @${ + takedown.submitter.username + } + 要求删除。 +
++`; +} + hljs.registerAliases(["plain"], { languageName: "plaintext" }); hljs.configure({ languages: ["cpp"] }); @@ -27,11 +52,26 @@ function renderHljs(body: HTMLElement) { export default async function serializeReply( discussionId: number, - { content, time }: { content: string; time: Date }, + { + content, + time, + takedown, + }: { + content: string; + time: Date; + takedown?: { + submitter: { id: number; username: string }; + reason: string; + } | null; + }, ) { const users: number[] = []; + const userElements: { ele: Element; user: number }[] = []; + const discussions: number[] = []; + const discussionElements: { ele: Element; discussion: number }[] = []; - const { document } = new JSDOM(content).window; + const { document } = new JSDOM(takedown ? getHtmlTookdown(takedown) : content) + .window; document.body.querySelectorAll("a[href]").forEach((element) => { element.setAttribute("target", "_blank"); element.setAttribute("rel", "noopener noreferrer"); @@ -44,9 +84,26 @@ export default async function serializeReply( const uid = getMentionedUser(element); if (uid) { users.push(uid); + userElements.push({ ele: element, user: uid }); element.setAttribute("data-uid", uid.toString()); - element.classList.add("text-decoration-none", "link-teal"); + // element.classList.add("text-decoration-none", "link-teal"); element.setAttribute("href", getUserUrl(uid)); + } else { + const mentionedDiscussionId = getDiscussionIdFromUrl( + new URL(element.getAttribute("href") as string), + ); + if (mentionedDiscussionId) { + discussions.push(mentionedDiscussionId); + discussionElements.push({ + ele: element, + discussion: mentionedDiscussionId, + }); + element.setAttribute( + "data-discussion-id", + mentionedDiscussionId.toString(), + ); + element.setAttribute("href", getDiscussionUrl(mentionedDiscussionId)); + } } } catch (e) { // Invalid URL @@ -62,6 +119,38 @@ export default async function serializeReply( .then((r) => Object.fromEntries(r.map((u) => [u.authorId, u._count]))), prisma.user.findMany({ where: { id: { in: users } } }), ]); + const indUsersMetioned: { [k: number]: User } = {}; + // eslint-disable-next-line no-return-assign + usersMetioned.forEach((el) => (indUsersMetioned[el.id] = el)); + // eslint-disable-next-line no-restricted-syntax + for (const ue of userElements) { + if (ue.user in indUsersMetioned) { + ue.ele.classList.add( + `lg-fg-${indUsersMetioned[ue.user].color}`, + "link-at-user", + ); + } else { + ue.ele.classList.add("link-at-user", "link-at-user-unstored"); + } + } + const discussionsMetioned = await prisma.discussion.findMany({ + where: { id: { in: discussions } }, + }); + const indDiscussionsMetioned: { [k: number]: Discussion } = {}; + // eslint-disable-next-line no-return-assign + discussionsMetioned.forEach((el) => (indDiscussionsMetioned[el.id] = el)); + // eslint-disable-next-line no-restricted-syntax + for (const de of discussionElements) { + if (de.discussion in indDiscussionsMetioned) { + de.ele.classList.add("link-teal", "link-discussion"); + } else { + de.ele.classList.add( + "link-danger", + "link-discussion", + "link-discussion-unstored", + ); + } + } renderHljs(document.body); return { content: document.body.innerHTML, @@ -76,16 +165,22 @@ export default async function serializeReply( export async function serializeReplyNoninteractive({ content, time, + takedown, }: { content: string; time: Date; + takedown?: { + submitter: { id: number; username: string }; + reason: string; + } | null; }) { const users: number[] = []; const userElements: { ele: Element; user: number }[] = []; const links: Set${takedown.reason}
+