feat: SSR 评论回复与 @ 提及通知
发评/审通接入 NotifyService,系统通知会话支持 kind 筛选。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -105,8 +105,8 @@
|
|||||||
- [x] 编辑评论(时限);删除评论 — SSR 帖详情入口 + `/comments/:cid/edit|delete`;作者软删进回收站
|
- [x] 编辑评论(时限);删除评论 — SSR 帖详情入口 + `/comments/:cid/edit|delete`;作者软删进回收站
|
||||||
- [x] 评论点赞 — `POST /post/:id/comments/:cid/like`
|
- [x] 评论点赞 — `POST /post/:id/comments/:cid/like`
|
||||||
- [x] 评论举报 — SSR
|
- [x] 评论举报 — SSR
|
||||||
- [ ] @ 提及 → 通知
|
- [x] @ 提及 → 通知 — SSR 发评/审通调用 `NotifyCommentMentions`
|
||||||
- [ ] 回复提醒(站内信 + 可选邮件)
|
- [x] 回复提醒(站内信 + 可选邮件)— SSR 发评/审通调用 `NotifyCommentPublished`
|
||||||
- [ ] 审核中 / 被拒评论可见性规则
|
- [ ] 审核中 / 被拒评论可见性规则
|
||||||
- [x] 管理员:通过 / 拒绝待审评论 — SSR `/admin/moderation`(回收站/修订未迁)
|
- [x] 管理员:通过 / 拒绝待审评论 — SSR `/admin/moderation`(回收站/修订未迁)
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
- [x] 发送私信 — 表单 PRG;用户主页「发私信」入口
|
- [x] 发送私信 — 表单 PRG;用户主页「发私信」入口
|
||||||
- [x] 未读数(可分私信 / 通知)— 列表分项 + 导航角标
|
- [x] 未读数(可分私信 / 通知)— 列表分项 + 导航角标
|
||||||
- [x] 标记会话已读 / 通知已读 / 全部已读 — 打开会话自动已读;全部标已读
|
- [x] 标记会话已读 / 通知已读 / 全部已读 — 打开会话自动已读;全部标已读
|
||||||
- [ ] 系统通知种类:`system` / `reject` / `report_result` / `reply` / `mention` / `moderation` 等(写入已有;筛选 UI 未迁)
|
- [x] 系统通知种类:`system` / `reject` / `report_result` / `reply` / `mention` / `moderation` 等(写入已有;`/messages/with/0?kind=` 筛选)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
| 投票卡 | 选选项提交;显示百分比;作者可结束 |
|
| 投票卡 | 选选项提交;显示百分比;作者可结束 |
|
||||||
| 悬赏条 | 显示积分与状态;采纳按钮在他人评论上;退款按钮按规则禁用并提示 |
|
| 悬赏条 | 显示积分与状态;采纳按钮在他人评论上;退款按钮按规则禁用并提示 |
|
||||||
| 抽奖卡 | 显示参与人数;开奖;中奖名单 |
|
| 抽奖卡 | 显示参与人数;开奖;中奖名单 |
|
||||||
| 评论 | 楼层列表、`reply_to` 引用、私密开关、点赞、举报、编辑(时限)、删除 — SSR;@ 未迁 |
|
| 评论 | 楼层列表、`reply_to` 引用、私密开关、点赞、举报、编辑(时限)、删除、回复/@ 通知 — SSR;嵌套树 UI 未做 |
|
||||||
| 修订 | 面板列出历史,可选对比 |
|
| 修订 | 面板列出历史,可选对比 |
|
||||||
| 图片 | Lightbox 查看 |
|
| 图片 | Lightbox 查看 |
|
||||||
|
|
||||||
@@ -155,10 +155,10 @@
|
|||||||
|
|
||||||
## 5. 消息页
|
## 5. 消息页
|
||||||
|
|
||||||
- 左:会话列表(系统会话单独)
|
- 左:会话列表(系统会话单独)— SSR `/messages`;快捷链到回复/提及筛选
|
||||||
- 右:消息时间线;发送框
|
- 右:消息时间线;发送框 — SSR `/messages/with/:peerId`
|
||||||
- 顶:未读角标(全局导航也可显示)
|
- 顶:未读角标(全局导航也可显示)
|
||||||
- 通知筛选(kind)
|
- 通知筛选(kind)— 系统会话 `?kind=reply|mention|…`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -698,6 +698,18 @@ body.j13-body {
|
|||||||
.j13-msg-bubble__subj { font-weight: 600; margin-bottom: 0.25rem; }
|
.j13-msg-bubble__subj { font-weight: 600; margin-bottom: 0.25rem; }
|
||||||
.j13-msg-bubble__body { white-space: pre-wrap; overflow-wrap: anywhere; }
|
.j13-msg-bubble__body { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
.j13-msg-compose { margin-top: 1.25rem; }
|
.j13-msg-compose { margin-top: 1.25rem; }
|
||||||
|
.j13-msg-kinds {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem 0.85rem;
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.j13-msg-kinds a { color: var(--j13-muted); }
|
||||||
|
.j13-msg-kinds a.is-active {
|
||||||
|
color: var(--j13-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* 内容门控锁定壳 */
|
/* 内容门控锁定壳 */
|
||||||
members-only,
|
members-only,
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
|||||||
SitePage: sitePageSvc,
|
SitePage: sitePageSvc,
|
||||||
Report: reportSvc,
|
Report: reportSvc,
|
||||||
Captcha: captchaSvc,
|
Captcha: captchaSvc,
|
||||||
|
Notify: notifySvc,
|
||||||
}, authMW)
|
}, authMW)
|
||||||
|
|
||||||
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
||||||
|
|||||||
@@ -357,6 +357,13 @@ func (d Deps) AdminCommentApprove(c *gin.Context) {
|
|||||||
if err := d.Comment.SetStatus(uint(id), models.ContentStatusPublished); err != nil {
|
if err := d.Comment.SetStatus(uint(id), models.ContentStatusPublished); err != nil {
|
||||||
ctx.SetFlash(err.Error())
|
ctx.SetFlash(err.Error())
|
||||||
} else {
|
} else {
|
||||||
|
if d.Notify != nil {
|
||||||
|
if comment, err := d.Comment.GetByID(uint(id)); err == nil {
|
||||||
|
comment.Status = models.ContentStatusPublished
|
||||||
|
d.Notify.AsyncNotifyCommentPublished(comment)
|
||||||
|
d.Notify.AsyncNotifyCommentMentions(comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.SetFlash("评论已通过")
|
ctx.SetFlash("评论已通过")
|
||||||
}
|
}
|
||||||
ctx.Redirect("/admin/moderation")
|
ctx.Redirect("/admin/moderation")
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Deps struct {
|
|||||||
SitePage *services.SitePageService
|
SitePage *services.SitePageService
|
||||||
Report *services.ReportService
|
Report *services.ReportService
|
||||||
Captcha *services.CaptchaService
|
Captcha *services.CaptchaService
|
||||||
|
Notify *services.NotifyService
|
||||||
}
|
}
|
||||||
|
|
||||||
// SitePageLink 导航/页脚站点单页链接
|
// SitePageLink 导航/页脚站点单页链接
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.iioio.com/freefire/jiang13-forum/models"
|
||||||
"git.iioio.com/freefire/jiang13-forum/services"
|
"git.iioio.com/freefire/jiang13-forum/services"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -52,6 +53,15 @@ type messagesThreadData struct {
|
|||||||
OlderBefore uint
|
OlderBefore uint
|
||||||
HasOlder bool
|
HasOlder bool
|
||||||
Draft string
|
Draft string
|
||||||
|
KindFilter string
|
||||||
|
KindLinks []msgKindLink
|
||||||
|
}
|
||||||
|
|
||||||
|
type msgKindLink struct {
|
||||||
|
Kind string
|
||||||
|
Label string
|
||||||
|
URL string
|
||||||
|
Active bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessagesList GET /messages
|
// MessagesList GET /messages
|
||||||
@@ -121,9 +131,22 @@ func (d Deps) renderMessagesThread(c *gin.Context, peerID uint, errMsg, beforeRa
|
|||||||
uid := ctx.UserID()
|
uid := ctx.UserID()
|
||||||
before, _ := strconv.ParseUint(beforeRaw, 10, 64)
|
before, _ := strconv.ParseUint(beforeRaw, 10, 64)
|
||||||
size := 40
|
size := 40
|
||||||
list, total, err := d.Message.ListConversationMessages(services.ConversationMessagesQuery{
|
kindFilter := ""
|
||||||
UserID: uid, PeerID: peerID, Page: 1, Size: size, Before: uint(before),
|
if peerID == 0 {
|
||||||
})
|
kindFilter = normalizeNotifyKind(c.Query("kind"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var list []models.PrivateMessage
|
||||||
|
var total int64
|
||||||
|
var err error
|
||||||
|
if peerID == 0 && kindFilter != "" && kindFilter != "all" {
|
||||||
|
list, total, err = d.Message.ListNotifications(uid, 1, size, kindFilter)
|
||||||
|
_ = before // kind 筛选暂不分页 before
|
||||||
|
} else {
|
||||||
|
list, total, err = d.Message.ListConversationMessages(services.ConversationMessagesQuery{
|
||||||
|
UserID: uid, PeerID: peerID, Page: 1, Size: size, Before: uint(before),
|
||||||
|
})
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.SetFlash(err.Error())
|
ctx.SetFlash(err.Error())
|
||||||
ctx.Redirect("/messages")
|
ctx.Redirect("/messages")
|
||||||
@@ -156,21 +179,69 @@ func (d Deps) renderMessagesThread(c *gin.Context, peerID uint, errMsg, beforeRa
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
hasOlder := false
|
hasOlder := false
|
||||||
if oldest > 0 {
|
if peerID != 0 || kindFilter == "" || kindFilter == "all" {
|
||||||
if before == 0 {
|
if oldest > 0 {
|
||||||
hasOlder = total > int64(len(list))
|
if before == 0 {
|
||||||
} else {
|
hasOlder = total > int64(len(list))
|
||||||
hasOlder = len(list) >= size
|
} else {
|
||||||
|
hasOlder = len(list) >= size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
hasOlder = false
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome := d.chrome(ctx, peerName+" · 私信 · "+d.Settings.SiteBranding().Name, "", "")
|
chrome := d.chrome(ctx, peerName+" · 私信 · "+d.Settings.SiteBranding().Name, "", "")
|
||||||
chrome.Error = errMsg
|
chrome.Error = errMsg
|
||||||
ctx.HTML(http.StatusOK, "messages/thread", messagesThreadData{
|
data := messagesThreadData{
|
||||||
PageChrome: chrome, PeerID: peerID, PeerName: peerName,
|
PageChrome: chrome, PeerID: peerID, PeerName: peerName,
|
||||||
IsSystem: peerID == 0, CanReply: canReply, Messages: bubbles,
|
IsSystem: peerID == 0, CanReply: canReply, Messages: bubbles,
|
||||||
OlderBefore: oldest, HasOlder: hasOlder, Draft: draft,
|
OlderBefore: oldest, HasOlder: hasOlder, Draft: draft,
|
||||||
})
|
}
|
||||||
|
if peerID == 0 {
|
||||||
|
data.KindFilter = kindFilter
|
||||||
|
data.KindLinks = notifyKindLinks(kindFilter)
|
||||||
|
}
|
||||||
|
ctx.HTML(http.StatusOK, "messages/thread", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeNotifyKind(raw string) string {
|
||||||
|
k := strings.TrimSpace(strings.ToLower(raw))
|
||||||
|
switch k {
|
||||||
|
case "", "all":
|
||||||
|
return "all"
|
||||||
|
case models.MessageKindReply, models.MessageKindMention, models.MessageKindSystem,
|
||||||
|
models.MessageKindReject, models.MessageKindReportResult, models.MessageKindModeration:
|
||||||
|
return k
|
||||||
|
default:
|
||||||
|
return "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func notifyKindLinks(active string) []msgKindLink {
|
||||||
|
if active == "" {
|
||||||
|
active = "all"
|
||||||
|
}
|
||||||
|
items := []struct{ kind, label string }{
|
||||||
|
{"all", "全部"},
|
||||||
|
{models.MessageKindReply, "回复"},
|
||||||
|
{models.MessageKindMention, "提及"},
|
||||||
|
{models.MessageKindSystem, "系统"},
|
||||||
|
{models.MessageKindReject, "拒绝"},
|
||||||
|
{models.MessageKindReportResult, "举报结果"},
|
||||||
|
{models.MessageKindModeration, "审核"},
|
||||||
|
}
|
||||||
|
out := make([]msgKindLink, 0, len(items))
|
||||||
|
for _, it := range items {
|
||||||
|
url := "/messages/with/0"
|
||||||
|
if it.kind != "all" {
|
||||||
|
url += "?kind=" + it.kind
|
||||||
|
}
|
||||||
|
out = append(out, msgKindLink{
|
||||||
|
Kind: it.kind, Label: it.label, URL: url, Active: active == it.kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessagesSend POST /messages/with/:peerId
|
// MessagesSend POST /messages/with/:peerId
|
||||||
|
|||||||
@@ -201,6 +201,16 @@ func (d Deps) PostComment(c *gin.Context) {
|
|||||||
ctx.Redirect(fmt.Sprintf("/post/%d#comments", id))
|
ctx.Redirect(fmt.Sprintf("/post/%d#comments", id))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if d.Notify != nil && cm != nil {
|
||||||
|
switch cm.Status {
|
||||||
|
case models.ContentStatusPublished:
|
||||||
|
d.Notify.AsyncNotifyCommentPublished(cm)
|
||||||
|
d.Notify.AsyncNotifyCommentMentions(cm)
|
||||||
|
case models.ContentStatusPending:
|
||||||
|
ctx.SetFlash("评论已提交,审核通过后公开显示")
|
||||||
|
d.Notify.AsyncNotifyPendingComment(cm)
|
||||||
|
}
|
||||||
|
}
|
||||||
anchor := "#comments"
|
anchor := "#comments"
|
||||||
if cm != nil && cm.Floor > 0 {
|
if cm != nil && cm.Floor > 0 {
|
||||||
anchor = fmt.Sprintf("#floor-%d", cm.Floor)
|
anchor = fmt.Sprintf("#floor-%d", cm.Floor)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
|
<p class="j13-muted"><a href="/messages/with/0">系统通知</a> · <a href="/messages/with/0?kind=reply">回复</a> · <a href="/messages/with/0?kind=mention">提及</a></p>
|
||||||
|
|
||||||
{{if .Conversations}}
|
{{if .Conversations}}
|
||||||
<ul class="j13-msg-list">
|
<ul class="j13-msg-list">
|
||||||
|
|||||||
@@ -4,10 +4,19 @@
|
|||||||
<main class="j13-main j13-messages">
|
<main class="j13-main j13-messages">
|
||||||
<p><a href="/messages">← 返回会话列表</a></p>
|
<p><a href="/messages">← 返回会话列表</a></p>
|
||||||
<h1>{{.PeerName}}</h1>
|
<h1>{{.PeerName}}</h1>
|
||||||
{{if .IsSystem}}<p class="j13-muted">系统通知(只读)</p>{{else}}<p class="j13-muted"><a href="/user/{{.PeerID}}">查看主页</a></p>{{end}}
|
{{if .IsSystem}}
|
||||||
|
<p class="j13-muted">系统通知(只读)</p>
|
||||||
|
{{if .KindLinks}}
|
||||||
|
<nav class="j13-msg-kinds" aria-label="通知类型">
|
||||||
|
{{range .KindLinks}}
|
||||||
|
<a href="{{.URL}}"{{if .Active}} class="is-active" aria-current="page"{{end}}>{{.Label}}</a>
|
||||||
|
{{end}}
|
||||||
|
</nav>
|
||||||
|
{{end}}
|
||||||
|
{{else}}<p class="j13-muted"><a href="/user/{{.PeerID}}">查看主页</a></p>{{end}}
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
|
|
||||||
{{if .HasOlder}}
|
{{if and .HasOlder (or (not .IsSystem) (eq .KindFilter "all") (eq .KindFilter ""))}}
|
||||||
<p><a href="/messages/with/{{.PeerID}}?before={{.OlderBefore}}">加载更早消息</a></p>
|
<p><a href="/messages/with/{{.PeerID}}?before={{.OlderBefore}}">加载更早消息</a></p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
@@ -698,6 +698,18 @@ body.j13-body {
|
|||||||
.j13-msg-bubble__subj { font-weight: 600; margin-bottom: 0.25rem; }
|
.j13-msg-bubble__subj { font-weight: 600; margin-bottom: 0.25rem; }
|
||||||
.j13-msg-bubble__body { white-space: pre-wrap; overflow-wrap: anywhere; }
|
.j13-msg-bubble__body { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
.j13-msg-compose { margin-top: 1.25rem; }
|
.j13-msg-compose { margin-top: 1.25rem; }
|
||||||
|
.j13-msg-kinds {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem 0.85rem;
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.j13-msg-kinds a { color: var(--j13-muted); }
|
||||||
|
.j13-msg-kinds a.is-active {
|
||||||
|
color: var(--j13-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* 内容门控锁定壳 */
|
/* 内容门控锁定壳 */
|
||||||
members-only,
|
members-only,
|
||||||
|
|||||||
Reference in New Issue
Block a user