feat: SSR 私信与通知(/messages)
会话列表、读写、未读角标与全部已读;系统会话只读;用户主页可发起私信。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -47,6 +47,7 @@ type PageChrome struct {
|
||||
CSRF string
|
||||
Flash string
|
||||
Error string
|
||||
UnreadCount int64 // 登录用户未读私信/通知总数;未登录为 0
|
||||
}
|
||||
|
||||
func (d Deps) ctx(c *gin.Context) *webctx.Context {
|
||||
@@ -73,6 +74,10 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
|
||||
for _, b := range boards {
|
||||
bv = append(bv, BoardView{ID: b.ID, Name: b.Name})
|
||||
}
|
||||
var unread int64
|
||||
if ctx.IsSigned() && d.Message != nil {
|
||||
unread, _ = d.Message.UnreadCount(ctx.UserID())
|
||||
}
|
||||
return PageChrome{
|
||||
Title: title,
|
||||
Description: desc,
|
||||
@@ -86,6 +91,7 @@ func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome
|
||||
Inner: inner,
|
||||
CSRF: ctx.EnsureCSRF(),
|
||||
Flash: ctx.TakeFlash(),
|
||||
UnreadCount: unread,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,10 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
||||
g.POST("/profile/password", authMW.RequireAuth(), deps.ProfilePasswordPost)
|
||||
g.POST("/profile/avatar", authMW.RequireAuth(), deps.ProfileAvatarPost)
|
||||
g.GET("/favorites", authMW.RequireAuth(), deps.FavoritesGet)
|
||||
g.GET("/messages", deps.PendingPage)
|
||||
g.GET("/messages", authMW.RequireAuth(), deps.MessagesList)
|
||||
g.GET("/messages/with/:peerId", authMW.RequireAuth(), deps.MessagesThread)
|
||||
g.POST("/messages/with/:peerId", authMW.RequireAuth(), deps.MessagesSend)
|
||||
g.POST("/messages/read-all", authMW.RequireAuth(), deps.MessagesReadAll)
|
||||
g.GET("/projects", deps.PendingPage)
|
||||
g.GET("/links", deps.PendingPage)
|
||||
g.GET("/boards", deps.PendingPage)
|
||||
|
||||
242
routers/web/messages.go
Normal file
242
routers/web/messages.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type msgConvRow struct {
|
||||
PeerID uint
|
||||
PeerName string
|
||||
IsSystem bool
|
||||
Preview string
|
||||
Unread int64
|
||||
UpdatedLabel string
|
||||
}
|
||||
|
||||
type messagesListData struct {
|
||||
PageChrome
|
||||
Conversations []msgConvRow
|
||||
UnreadTotal int64
|
||||
UnreadDM int64
|
||||
UnreadNotify int64
|
||||
Page int
|
||||
PrevPage int
|
||||
NextPage int
|
||||
HasPrev bool
|
||||
HasMore bool
|
||||
}
|
||||
|
||||
type msgBubble struct {
|
||||
ID uint
|
||||
FromSelf bool
|
||||
IsSystem bool
|
||||
Subject string
|
||||
Content string
|
||||
CreatedLabel string
|
||||
Kind string
|
||||
}
|
||||
|
||||
type messagesThreadData struct {
|
||||
PageChrome
|
||||
PeerID uint
|
||||
PeerName string
|
||||
IsSystem bool
|
||||
CanReply bool
|
||||
Messages []msgBubble
|
||||
OlderBefore uint
|
||||
HasOlder bool
|
||||
Draft string
|
||||
}
|
||||
|
||||
// MessagesList GET /messages
|
||||
func (d Deps) MessagesList(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
size := 30
|
||||
list, total, err := d.Message.ListConversations(services.ConversationListQuery{
|
||||
UserID: ctx.UserID(), Page: page, Size: size,
|
||||
})
|
||||
if err != nil {
|
||||
chrome := d.chrome(ctx, "私信 · "+d.Settings.SiteBranding().Name, "", "")
|
||||
chrome.Error = err.Error()
|
||||
ctx.HTML(http.StatusOK, "messages/list", messagesListData{PageChrome: chrome})
|
||||
return
|
||||
}
|
||||
rows := make([]msgConvRow, 0, len(list))
|
||||
for _, cv := range list {
|
||||
name := "系统通知"
|
||||
if !cv.IsSystem {
|
||||
name = "用户"
|
||||
if cv.PeerUser != nil {
|
||||
name = displayName(cv.PeerUser)
|
||||
}
|
||||
}
|
||||
preview := ""
|
||||
if cv.LastMessage != nil {
|
||||
preview = strings.TrimSpace(cv.LastMessage.Subject)
|
||||
if preview == "" {
|
||||
preview = truncateRunes(cv.LastMessage.Content, 48)
|
||||
}
|
||||
}
|
||||
rows = append(rows, msgConvRow{
|
||||
PeerID: cv.PeerUserID, PeerName: name, IsSystem: cv.IsSystem,
|
||||
Preview: preview, Unread: cv.UnreadCount,
|
||||
UpdatedLabel: formatTime(cv.UpdatedAt),
|
||||
})
|
||||
}
|
||||
totalUnread, dm, notify, _ := d.Message.UnreadCounts(ctx.UserID())
|
||||
chrome := d.chrome(ctx, "私信 · "+d.Settings.SiteBranding().Name, "", "")
|
||||
hasMore := int64(page*size) < total
|
||||
ctx.HTML(http.StatusOK, "messages/list", messagesListData{
|
||||
PageChrome: chrome, Conversations: rows,
|
||||
UnreadTotal: totalUnread, UnreadDM: dm, UnreadNotify: notify,
|
||||
Page: page, PrevPage: page - 1, NextPage: page + 1,
|
||||
HasPrev: page > 1, HasMore: hasMore,
|
||||
})
|
||||
}
|
||||
|
||||
// MessagesThread GET /messages/with/:peerId
|
||||
func (d Deps) MessagesThread(c *gin.Context) {
|
||||
peerID, err := parsePeerID(c.Param("peerId"))
|
||||
if err != nil {
|
||||
ctx := d.ctx(c)
|
||||
ctx.SetFlash("无效会话")
|
||||
ctx.Redirect("/messages")
|
||||
return
|
||||
}
|
||||
d.renderMessagesThread(c, peerID, "", c.Query("before"), "")
|
||||
}
|
||||
|
||||
func (d Deps) renderMessagesThread(c *gin.Context, peerID uint, errMsg, beforeRaw, draft string) {
|
||||
ctx := d.ctx(c)
|
||||
uid := ctx.UserID()
|
||||
before, _ := strconv.ParseUint(beforeRaw, 10, 64)
|
||||
size := 40
|
||||
list, total, err := d.Message.ListConversationMessages(services.ConversationMessagesQuery{
|
||||
UserID: uid, PeerID: peerID, Page: 1, Size: size, Before: uint(before),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.SetFlash(err.Error())
|
||||
ctx.Redirect("/messages")
|
||||
return
|
||||
}
|
||||
if before == 0 {
|
||||
_ = d.Message.MarkConversationRead(uid, peerID)
|
||||
}
|
||||
|
||||
peerName := "系统通知"
|
||||
canReply := peerID > 0
|
||||
if peerID > 0 {
|
||||
if u, e := d.User.GetByID(peerID); e == nil {
|
||||
peerName = displayName(u)
|
||||
} else {
|
||||
peerName = fmt.Sprintf("用户 #%d", peerID)
|
||||
}
|
||||
}
|
||||
|
||||
bubbles := make([]msgBubble, 0, len(list))
|
||||
var oldest uint
|
||||
for _, m := range list {
|
||||
if oldest == 0 || m.ID < oldest {
|
||||
oldest = m.ID
|
||||
}
|
||||
bubbles = append(bubbles, msgBubble{
|
||||
ID: m.ID, FromSelf: m.FromUserID == uid && m.FromUserID > 0,
|
||||
IsSystem: m.FromUserID == 0, Subject: m.Subject, Content: m.Content,
|
||||
CreatedLabel: formatTime(m.CreatedAt), Kind: m.Kind,
|
||||
})
|
||||
}
|
||||
hasOlder := false
|
||||
if oldest > 0 {
|
||||
if before == 0 {
|
||||
hasOlder = total > int64(len(list))
|
||||
} else {
|
||||
hasOlder = len(list) >= size
|
||||
}
|
||||
}
|
||||
|
||||
chrome := d.chrome(ctx, peerName+" · 私信 · "+d.Settings.SiteBranding().Name, "", "")
|
||||
chrome.Error = errMsg
|
||||
ctx.HTML(http.StatusOK, "messages/thread", messagesThreadData{
|
||||
PageChrome: chrome, PeerID: peerID, PeerName: peerName,
|
||||
IsSystem: peerID == 0, CanReply: canReply, Messages: bubbles,
|
||||
OlderBefore: oldest, HasOlder: hasOlder, Draft: draft,
|
||||
})
|
||||
}
|
||||
|
||||
// MessagesSend POST /messages/with/:peerId
|
||||
func (d Deps) MessagesSend(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
peerID, err := parsePeerID(c.Param("peerId"))
|
||||
if err != nil || peerID == 0 {
|
||||
ctx.SetFlash("无法向系统通知会话发送私信")
|
||||
ctx.Redirect("/messages")
|
||||
return
|
||||
}
|
||||
content := strings.TrimSpace(c.PostForm("content"))
|
||||
if !ctx.CheckCSRF() {
|
||||
d.renderMessagesThread(c, peerID, "无效请求,请重试", "", content)
|
||||
return
|
||||
}
|
||||
if d.Limiter != nil && !d.Limiter.Allow("message", fmt.Sprintf("%d", ctx.UserID())) {
|
||||
d.renderMessagesThread(c, peerID, "发送过于频繁,请稍后再试", "", content)
|
||||
return
|
||||
}
|
||||
if ctx.Doer != nil && ctx.Doer.Banned {
|
||||
d.renderMessagesThread(c, peerID, "账号已被禁言,无法发私信", "", content)
|
||||
return
|
||||
}
|
||||
_, err = d.Message.Send(services.MessageSendInput{
|
||||
FromUserID: ctx.UserID(),
|
||||
ToUserID: peerID,
|
||||
Content: content,
|
||||
})
|
||||
if err != nil {
|
||||
d.renderMessagesThread(c, peerID, err.Error(), "", content)
|
||||
return
|
||||
}
|
||||
ctx.SetFlash("已发送")
|
||||
ctx.Redirect(fmt.Sprintf("/messages/with/%d", peerID))
|
||||
}
|
||||
|
||||
// MessagesReadAll POST /messages/read-all
|
||||
func (d Deps) MessagesReadAll(c *gin.Context) {
|
||||
ctx := d.ctx(c)
|
||||
if !ctx.CheckCSRF() {
|
||||
ctx.SetFlash("无效请求,请重试")
|
||||
ctx.Redirect("/messages")
|
||||
return
|
||||
}
|
||||
_ = d.Message.MarkAllRead(ctx.UserID())
|
||||
ctx.SetFlash("已全部标为已读")
|
||||
ctx.Redirect("/messages")
|
||||
}
|
||||
|
||||
func parsePeerID(raw string) (uint, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || raw == "system" {
|
||||
return 0, nil
|
||||
}
|
||||
id, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
|
||||
func truncateRunes(s string, max int) string {
|
||||
r := []rune(strings.TrimSpace(s))
|
||||
if len(r) <= max {
|
||||
return string(r)
|
||||
}
|
||||
return string(r[:max]) + "…"
|
||||
}
|
||||
Reference in New Issue
Block a user