Compare commits

...

2 Commits

Author SHA1 Message Date
17026a74c5 Update comments UI 2025-10-02 21:18:37 +08:00
1e01e04f7b Add replies field to CommentView and update logic to fetch comment replies 2025-10-02 20:16:29 +08:00
6 changed files with 74 additions and 25 deletions

View File

@@ -1,6 +1,5 @@
import { useTranslation } from "../utils/i18n"; import { useTranslation } from "../utils/i18n";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { MdOutlineComment } from "react-icons/md";
import { Comment } from "../network/models"; import { Comment } from "../network/models";
import { network } from "../network/network"; import { network } from "../network/network";
import Badge from "./badge"; import Badge from "./badge";
@@ -55,29 +54,66 @@ export function CommentTile({
{new Date(comment.created_at).toLocaleDateString()} {new Date(comment.created_at).toLocaleDateString()}
</Badge> </Badge>
</div> </div>
<div className={"p-2 comment_tile"}> <div className={"px-2 pt-2 comment_tile"}>
<CommentContent content={comment.content} /> <CommentContent content={comment.content} />
</div> </div>
<div className={"flex items-center"}> {comment.content_truncated ? (
{comment.content_truncated && ( <div className={"pl-2 pb-2"}>
<Badge className="badge-ghost">{t("Click to view more")}</Badge> <Badge className={"badge-soft badge-info badge-sm"}>
)} {t("Click to view more")}
<span className={"grow"}></span>
{comment.reply_count > 0 && (
<Badge className={"badge-soft badge-primary mr-2"}>
<MdOutlineComment size={16} className={"inline-block"} />
{comment.reply_count}
</Badge> </Badge>
)} </div>
</div> ) : (
<div className={"h-2"} />
)}
<CommentReplies comment={comment} />
</a> </a>
); );
} }
function CommentReplies({ comment }: { comment: Comment }) {
const { t } = useTranslation();
if (!comment.replies) {
return null;
}
return (
<div className={"bg-base-200 mx-2 p-2 rounded-lg"}>
{comment.replies.map((e) => {
return (
<p className={"text-xs mb-1"}>
<span className={"font-bold"}>{e.user.username}: </span>
{CommentToPlainText(e.content)}
</p>
);
})}
{comment.reply_count > comment.replies.length ? (
<p className={"text-xs text-primary mt-1"}>
{t("View {count} more replies").replace(
"{count}",
(comment.reply_count - comment.replies.length).toString(),
)}
</p>
) : null}
</div>
);
}
function CommentToPlainText(content: string) {
// Remove Markdown syntax to convert to plain text
return content
.replace(/!\[.*?]\(.*?\)/g, "") // Remove images
.replace(/\[([^\]]+)]\((.*?)\)/g, "$1") // Convert links to just the text
.replace(/[#>*_`~-]/g, "") // Remove other Markdown characters
.replace(/\n+/g, " ") // Replace newlines with spaces
.trim();
}
export function CommentContent({ content }: { content: string }) { export function CommentContent({ content }: { content: string }) {
const lines = content.split("\n"); const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
let line = lines[i]; const line = lines[i];
if (!line.endsWith(" ")) { if (!line.endsWith(" ")) {
// Ensure that each line ends with two spaces for Markdown to recognize it as a line break // Ensure that each line ends with two spaces for Markdown to recognize it as a line break
lines[i] = line + " "; lines[i] = line + " ";

View File

@@ -254,6 +254,7 @@ export const i18nData = {
"You do not have permission to upload files, please contact the administrator.": "You do not have permission to upload files, please contact the administrator.":
"您没有上传文件的权限,请联系管理员。", "您没有上传文件的权限,请联系管理员。",
"Private": "私有", "Private": "私有",
"View {count} more replies": "查看另外 {count} 条回复",
}, },
}, },
"zh-TW": { "zh-TW": {
@@ -511,6 +512,7 @@ export const i18nData = {
"You do not have permission to upload files, please contact the administrator.": "You do not have permission to upload files, please contact the administrator.":
"您沒有上傳檔案的權限,請聯繫管理員。", "您沒有上傳檔案的權限,請聯繫管理員。",
"Private": "私有", "Private": "私有",
"View {count} more replies": "查看另外 {count} 條回覆",
}, },
}, },
}; };

View File

@@ -126,6 +126,7 @@ export interface Comment {
images: Image[]; images: Image[];
content_truncated: boolean; content_truncated: boolean;
reply_count: number; reply_count: number;
replies: Comment[];
} }
export interface CommentWithResource { export interface CommentWithResource {

View File

@@ -1,7 +1,7 @@
import { createContext, useContext, useMemo } from "react"; import { createContext, useContext, useMemo } from "react";
function t(data: any, language: string) { function t(data: any, language: string) {
return (key: string) => { return (key: string): string => {
return data[language]?.["translation"]?.[key] || key; return data[language]?.["translation"]?.[key] || key;
}; };
} }

View File

@@ -25,13 +25,14 @@ const (
) )
type CommentView struct { type CommentView struct {
ID uint `json:"id"` ID uint `json:"id"`
Content string `json:"content"` Content string `json:"content"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
User UserView `json:"user"` User UserView `json:"user"`
Images []ImageView `json:"images"` Images []ImageView `json:"images"`
ReplyCount uint `json:"reply_count"` ReplyCount uint `json:"reply_count"`
ContentTruncated bool `json:"content_truncated"` ContentTruncated bool `json:"content_truncated"`
Replies []CommentView `json:"replies,omitempty"`
} }
func (c *Comment) ToView() *CommentView { func (c *Comment) ToView() *CommentView {

View File

@@ -122,9 +122,18 @@ func ListResourceComments(resourceID uint, page int) ([]model.CommentView, int,
res := make([]model.CommentView, 0, len(comments)) res := make([]model.CommentView, 0, len(comments))
for _, c := range comments { for _, c := range comments {
v := *c.ToView() v := *c.ToView()
var truncated bool v.Content, v.ContentTruncated = restrictCommentLength(v.Content)
v.Content, truncated = restrictCommentLength(v.Content) replies, _, err := dao.GetCommentReplies(c.ID, 1, 3)
v.ContentTruncated = truncated if err != nil {
log.Error("Error getting replies for comment:", err)
return nil, 0, model.NewInternalServerError("Error getting replies for comment")
}
v.Replies = make([]model.CommentView, 0, len(replies))
for _, r := range replies {
rv := *r.ToView()
rv.Content, rv.ContentTruncated = restrictCommentLength(rv.Content)
v.Replies = append(v.Replies, rv)
}
res = append(res, v) res = append(res, v)
} }
return res, totalPages, nil return res, totalPages, nil