feat: @提及发送通知 + 通知点击定位高亮
- 新增 NotifyCommentMention 通知类型,归属 mention 分类 TAB - parseMentions 中向被@用户发送通知(排除自己和重复通知) - 消息中心 mention 通知可点击跳转至文章页面 - 通过 mention 通知进入时,来源评论置顶并持久半透明高亮 - 普通评论链接保持原有短暂高亮行为不变
This commit is contained in:
@ -13,6 +13,7 @@ const (
|
|||||||
NotifyCommentReply = "comment_reply"
|
NotifyCommentReply = "comment_reply"
|
||||||
NotifyLikeAggregated = "like_aggregated" // 每日聚合点赞通知
|
NotifyLikeAggregated = "like_aggregated" // 每日聚合点赞通知
|
||||||
NotifyFollow = "follow" // 关注通知
|
NotifyFollow = "follow" // 关注通知
|
||||||
|
NotifyCommentMention = "comment_mention" // 评论@提及通知
|
||||||
)
|
)
|
||||||
|
|
||||||
// Notification 消息/通知模型
|
// Notification 消息/通知模型
|
||||||
@ -39,6 +40,7 @@ var NotifyTypeNames = map[string]string{
|
|||||||
NotifyLevelUp: "等级提升",
|
NotifyLevelUp: "等级提升",
|
||||||
NotifyComment: "评论",
|
NotifyComment: "评论",
|
||||||
NotifyCommentReply: "回复",
|
NotifyCommentReply: "回复",
|
||||||
|
NotifyCommentMention: "@ 提及",
|
||||||
NotifyLikeAggregated: "点赞",
|
NotifyLikeAggregated: "点赞",
|
||||||
NotifyFollow: "关注",
|
NotifyFollow: "关注",
|
||||||
}
|
}
|
||||||
@ -54,6 +56,7 @@ var NotifyCategory = map[string]string{
|
|||||||
NotifyLevelUp: "system",
|
NotifyLevelUp: "system",
|
||||||
NotifyComment: "mention",
|
NotifyComment: "mention",
|
||||||
NotifyCommentReply: "mention",
|
NotifyCommentReply: "mention",
|
||||||
|
NotifyCommentMention: "mention",
|
||||||
NotifyLikeAggregated: "like",
|
NotifyLikeAggregated: "like",
|
||||||
NotifyFollow: "follow",
|
NotifyFollow: "follow",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,15 +55,15 @@ func (s *CommentService) CreateRoot(userID uint, postID uint, body string) (*mod
|
|||||||
// 文章评论数+1
|
// 文章评论数+1
|
||||||
_ = s.repo.IncrCommentsCount(postID)
|
_ = s.repo.IncrCommentsCount(postID)
|
||||||
|
|
||||||
// 解析 @ 提及
|
// 解析 @ 提及 + 发送通知
|
||||||
s.parseMentions(comment.ID, body)
|
|
||||||
|
|
||||||
// 通知文章作者(评论者 ≠ 作者),RelatedID 存 postID 以便消息页生成跳转链接
|
|
||||||
if s.notifier != nil && userID != postAuthorID {
|
|
||||||
commenterName, _ := s.repo.GetUsernameByID(userID)
|
commenterName, _ := s.repo.GetUsernameByID(userID)
|
||||||
if commenterName == "" {
|
if commenterName == "" {
|
||||||
commenterName = "用户"
|
commenterName = "用户"
|
||||||
}
|
}
|
||||||
|
s.parseMentions(comment.ID, postID, userID, commenterName, body)
|
||||||
|
|
||||||
|
// 通知文章作者(评论者 ≠ 作者),RelatedID 存 postID 以便消息页生成跳转链接
|
||||||
|
if s.notifier != nil && userID != postAuthorID {
|
||||||
s.notifier.Create(postAuthorID, model.NotifyComment,
|
s.notifier.Create(postAuthorID, model.NotifyComment,
|
||||||
"新评论",
|
"新评论",
|
||||||
fmt.Sprintf("%s 评论了你的文章", commenterName),
|
fmt.Sprintf("%s 评论了你的文章", commenterName),
|
||||||
@ -103,15 +103,15 @@ func (s *CommentService) CreateReply(userID uint, parentID uint, body string) (*
|
|||||||
// 文章评论数+1
|
// 文章评论数+1
|
||||||
_ = s.repo.IncrCommentsCount(parent.PostID)
|
_ = s.repo.IncrCommentsCount(parent.PostID)
|
||||||
|
|
||||||
// 解析 @ 提及
|
// 解析 @ 提及 + 发送通知
|
||||||
s.parseMentions(comment.ID, body)
|
|
||||||
|
|
||||||
// 通知被回复者(不自己回复自己),RelatedID 存 postID 以便消息页生成跳转链接
|
|
||||||
if s.notifier != nil && parent.UserID != userID {
|
|
||||||
commenterName, _ := s.repo.GetUsernameByID(userID)
|
commenterName, _ := s.repo.GetUsernameByID(userID)
|
||||||
if commenterName == "" {
|
if commenterName == "" {
|
||||||
commenterName = "用户"
|
commenterName = "用户"
|
||||||
}
|
}
|
||||||
|
s.parseMentions(comment.ID, parent.PostID, userID, commenterName, body)
|
||||||
|
|
||||||
|
// 通知被回复者(不自己回复自己),RelatedID 存 postID 以便消息页生成跳转链接
|
||||||
|
if s.notifier != nil && parent.UserID != userID {
|
||||||
s.notifier.Create(parent.UserID, model.NotifyCommentReply,
|
s.notifier.Create(parent.UserID, model.NotifyCommentReply,
|
||||||
"新回复",
|
"新回复",
|
||||||
fmt.Sprintf("%s 回复了你的评论", commenterName),
|
fmt.Sprintf("%s 回复了你的评论", commenterName),
|
||||||
@ -209,10 +209,12 @@ func (s *CommentService) SearchUsers(keyword string, followerUID uint) ([]model.
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseMentions 解析@用户并存储到 comment_mentions 表
|
// parseMentions 解析@用户,存储到 comment_mentions 表,并向被@用户发送通知
|
||||||
func (s *CommentService) parseMentions(commentID uint, body string) {
|
func (s *CommentService) parseMentions(commentID uint, postID uint, commenterID uint, commenterName string, body string) {
|
||||||
matches := mentionPattern.FindAllStringSubmatch(body, -1)
|
matches := mentionPattern.FindAllStringSubmatch(body, -1)
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
|
notified := make(map[uint]bool) // 防止重复通知同一用户
|
||||||
|
|
||||||
for _, m := range matches {
|
for _, m := range matches {
|
||||||
username := m[1]
|
username := m[1]
|
||||||
if seen[username] {
|
if seen[username] {
|
||||||
@ -220,12 +222,23 @@ func (s *CommentService) parseMentions(commentID uint, body string) {
|
|||||||
}
|
}
|
||||||
seen[username] = true
|
seen[username] = true
|
||||||
rows, _ := s.repo.SearchUsersByLevel(username, 2, 1, 0)
|
rows, _ := s.repo.SearchUsersByLevel(username, 2, 1, 0)
|
||||||
if len(rows) > 0 {
|
if len(rows) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mentionedUID := rows[0].UID
|
||||||
_ = s.repo.CreateMention(&model.CommentMention{
|
_ = s.repo.CreateMention(&model.CommentMention{
|
||||||
CommentID: commentID,
|
CommentID: commentID,
|
||||||
UID: rows[0].UID,
|
UID: mentionedUID,
|
||||||
OriginalUsername: username,
|
OriginalUsername: username,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 向被@用户发送通知(排除自己@自己、重复通知)
|
||||||
|
if s.notifier != nil && mentionedUID != commenterID && !notified[mentionedUID] {
|
||||||
|
notified[mentionedUID] = true
|
||||||
|
s.notifier.Create(mentionedUID, model.NotifyCommentMention,
|
||||||
|
"新提及",
|
||||||
|
fmt.Sprintf("%s 在评论中 @ 了你", commenterName),
|
||||||
|
&postID, &commentID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ func categoryTypes(category string) []string {
|
|||||||
case "system":
|
case "system":
|
||||||
return []string{model.NotifyAuditApproved, model.NotifyAuditRejected, model.NotifyPostApproved, model.NotifyPostRejected, model.NotifyPostLocked, model.NotifyPostUnlocked, model.NotifyLevelUp}
|
return []string{model.NotifyAuditApproved, model.NotifyAuditRejected, model.NotifyPostApproved, model.NotifyPostRejected, model.NotifyPostLocked, model.NotifyPostUnlocked, model.NotifyLevelUp}
|
||||||
case "mention":
|
case "mention":
|
||||||
return []string{model.NotifyComment, model.NotifyCommentReply}
|
return []string{model.NotifyComment, model.NotifyCommentReply, model.NotifyCommentMention}
|
||||||
case "like":
|
case "like":
|
||||||
return []string{model.NotifyLikeAggregated}
|
return []string{model.NotifyLikeAggregated}
|
||||||
case "follow":
|
case "follow":
|
||||||
|
|||||||
@ -97,6 +97,12 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
{{$link = printf "/posts/%d#comments" (derefUint $m.RelatedID)}}
|
{{$link = printf "/posts/%d#comments" (derefUint $m.RelatedID)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{else if eq $m.NotifyType "comment_mention"}}
|
||||||
|
{{if $m.CommentID}}
|
||||||
|
{{$link = printf "/posts/%d#mention-%d" (derefUint $m.RelatedID) (derefUint $m.CommentID)}}
|
||||||
|
{{else}}
|
||||||
|
{{$link = printf "/posts/%d#comments" (derefUint $m.RelatedID)}}
|
||||||
|
{{end}}
|
||||||
{{else if or (eq $m.NotifyType "audit_approved") (eq $m.NotifyType "audit_rejected")}}
|
{{else if or (eq $m.NotifyType "audit_approved") (eq $m.NotifyType "audit_rejected")}}
|
||||||
{{$link = "/settings"}}
|
{{$link = "/settings"}}
|
||||||
{{else if eq $m.NotifyType "follow"}}
|
{{else if eq $m.NotifyType "follow"}}
|
||||||
|
|||||||
@ -1096,6 +1096,12 @@
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* @提及通知进入:持久半透明高亮 */
|
||||||
|
.comment-card.highlight-mention,
|
||||||
|
.comment-reply-card.highlight-mention {
|
||||||
|
background: rgba(255, 248, 225, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.comment-header {
|
.comment-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@ -498,21 +498,45 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ====== 评论高亮 ======
|
// ====== 评论高亮 ======
|
||||||
function highlightComment(commentId) {
|
function highlightComment(commentId, persistent) {
|
||||||
var card = commentsList.querySelector('.comment-card[data-id="' + commentId + '"]') ||
|
var card = commentsList.querySelector('.comment-card[data-id="' + commentId + '"]') ||
|
||||||
commentsList.querySelector('.comment-reply-card[data-id="' + commentId + '"]');
|
commentsList.querySelector('.comment-reply-card[data-id="' + commentId + '"]');
|
||||||
if (card) {
|
if (card) {
|
||||||
commentsList.insertBefore(card, commentsList.firstChild);
|
commentsList.insertBefore(card, commentsList.firstChild);
|
||||||
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
if (persistent) {
|
||||||
|
card.classList.add('highlight-mention');
|
||||||
|
} else {
|
||||||
card.style.background = '#fff8e1';
|
card.style.background = '#fff8e1';
|
||||||
card.style.transition = 'background 1.5s';
|
card.style.transition = 'background 1.5s';
|
||||||
setTimeout(function() { card.style.background = ''; }, 2000);
|
setTimeout(function() { card.style.background = ''; }, 2000);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hash = window.location.hash;
|
var hash = window.location.hash;
|
||||||
|
|
||||||
|
// @提及通知进入:置顶 + 持久高亮
|
||||||
|
var mentionMatch = hash.match(/^#mention-(\d+)$/);
|
||||||
|
if (mentionMatch) {
|
||||||
|
var mentionCommentId = mentionMatch[1];
|
||||||
|
var mentionInterval = setInterval(function() {
|
||||||
|
if (highlightComment(mentionCommentId, true)) {
|
||||||
|
clearInterval(mentionInterval);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
setTimeout(function() {
|
||||||
|
clearInterval(mentionInterval);
|
||||||
|
if (!commentsList.querySelector('.comment-card[data-id="' + mentionCommentId + '"]') &&
|
||||||
|
!commentsList.querySelector('.comment-reply-card[data-id="' + mentionCommentId + '"]')) {
|
||||||
|
showToast('该评论已不存在');
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通评论/回复链接:短暂高亮
|
||||||
var commentMatch = hash.match(/^#comment-(\d+)$/);
|
var commentMatch = hash.match(/^#comment-(\d+)$/);
|
||||||
if (commentMatch) {
|
if (commentMatch) {
|
||||||
var targetCommentId = commentMatch[1];
|
var targetCommentId = commentMatch[1];
|
||||||
|
|||||||
Reference in New Issue
Block a user