Skip to content

Commit

Permalink
feat: 未读消息提醒功能
Browse files Browse the repository at this point in the history
  • Loading branch information
fy0 committed Oct 29, 2024
1 parent 7584fd1 commit 2694161
Show file tree
Hide file tree
Showing 17 changed files with 352 additions and 20 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/auto-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,16 @@ jobs:
cd output && zip -r ../release/sealchat_$(date +%Y%m%d)_${SHORT_SHA}_${{ matrix.name }}.zip sealchat-server.exe
fi
- name: Set slug output
id: slug
run: |
echo "::set-output name=SHORT_SHA::$(echo ${{ github.sha }} | cut -c1-7)"
echo "::set-output name=DATE::$(date +%Y-%m-%d)"
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
tag_name: dev-release
tag_name: ${{ steps.slug.outputs.DATE }}-${{ steps.slug.outputs.SHORT_SHA }}
files: |
release/sealchat_*_*_${{ matrix.name }}.*
env:
Expand Down
2 changes: 1 addition & 1 deletion api/api_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func Init(config *utils.AppConfig, uiStatic fs.FS) {
corsConfig := cors.New(cors.Config{
AllowOrigins: "*",
AllowMethods: "GET, POST, PUT, DELETE",
AllowHeaders: "Origin, Content-Type, Accept, Authorization, ChannelId",
AllowHeaders: "Origin, Content-Type, Accept, Authorization, ObjectId",
ExposeHeaders: "Content-Length",
AllowCredentials: false,
MaxAge: 3600,
Expand Down
39 changes: 39 additions & 0 deletions api/chat_api_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ func apiMessageCreate(ctx *ChatContext, data *struct {
db := model.GetDB()
channelId := data.ChannelID

var privateOtherUser string

// 权限检查
if len(channelId) < 30 { // 注意,这不是一个好的区分方式
// 群内
Expand All @@ -76,6 +78,11 @@ func apiMessageCreate(ctx *ChatContext, data *struct {
if fr.ID == "" {
return nil, nil
}

privateOtherUser = fr.UserID1
if fr.UserID1 == ctx.User.ID {
privateOtherUser = fr.UserID2
}
}

content := data.Content
Expand All @@ -95,6 +102,28 @@ func apiMessageCreate(ctx *ChatContext, data *struct {
// ids := channel.GetPrivateUserIDs()
// model.FriendRelationSetVisible(ids[0], ids[1])
model.FriendRelationSetVisibleById(channel.ID)
_ = model.ChannelReadInit(data.ChannelID, privateOtherUser)

// 发送快速更新通知
ctx.BroadcastToUserJSON(privateOtherUser, map[string]any{
"op": 0,
"type": "message-created-notice",
"channelId": data.ChannelID,
})
} else {
// 给当前在线人都通知一遍
var uids []string
ctx.UserId2ConnInfo.Range(func(key string, value *utils.SyncMap[*WsSyncConn, *ConnInfo]) bool {
uids = append(uids, key)
return true
})
_ = model.ChannelReadInitInBatches(data.ChannelID, uids)
// 发送快速更新通知
ctx.BroadcastJSON(map[string]any{
"op": 0,
"type": "message-created-notice",
"channelId": data.ChannelID,
})
}

var quote model.MessageModel
Expand Down Expand Up @@ -227,6 +256,8 @@ func apiMessageList(ctx *ChatContext, data *struct {
i.Quote = x[0]
}, "id, content, created_at, user_id, is_revoked")

_ = model.ChannelReadSet(data.ChannelID, ctx.User.ID)

q.Count(&count)
var next string

Expand Down Expand Up @@ -323,3 +354,11 @@ func builtinSealBotSolve(ctx *ChatContext, data *struct {
})
}
}

func apiUnreadCount(ctx *ChatContext, data *struct{}) (any, error) {
lst, err := model.ChannelUnreadFetch(ctx.User.ID)
if err != nil {
return nil, err
}
return lst, err
}
18 changes: 18 additions & 0 deletions api/chat_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ type ChatContext struct {
UserId2ConnInfo *utils.SyncMap[string, *utils.SyncMap[*WsSyncConn, *ConnInfo]]
}

func (ctx *ChatContext) BroadcastToUserJSON(userId string, data any) {
value, _ := ctx.UserId2ConnInfo.Load(userId)
value.Range(func(key *WsSyncConn, value *ConnInfo) bool {
_ = value.Conn.WriteJSON(data)
return true
})
}

func (ctx *ChatContext) BroadcastJSON(data any) {
ctx.UserId2ConnInfo.Range(func(key string, value *utils.SyncMap[*WsSyncConn, *ConnInfo]) bool {
value.Range(func(key *WsSyncConn, value *ConnInfo) bool {
_ = value.Conn.WriteJSON(data)
return true
})
return true
})
}

func (ctx *ChatContext) BroadcastEvent(data *protocol.Event) {
data.Timestamp = time.Now().Unix()
ctx.UserId2ConnInfo.Range(func(key string, value *utils.SyncMap[*WsSyncConn, *ConnInfo]) bool {
Expand Down
3 changes: 3 additions & 0 deletions api/chat_websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ func websocketWorks(app *fiber.App) {
apiWrap(ctx, msg, apiMessageList)
solved = true

case "unread.count":
apiWrap(ctx, msg, apiUnreadCount)

case "guild.member.list":
apiWrap(ctx, msg, apiGuildMemberList)
solved = true
Expand Down
13 changes: 10 additions & 3 deletions deploy_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,17 +172,24 @@ dbUrl: ./data/chat.db
## 一份配置文件示例

```yaml
chatHistoryPersistentDays: 599
dbUrl: postgresql://seal:123@localhost:5432/sealchat
# 主页
domain: 127.0.0.1:3212
# 是否压缩图片
imageCompress: true
# 图片上传大小限制
imageSizeLimit: 99999999
# 注册是否开放
registerOpen: true
# 提供服务端口
serveAt: :3212
#
# 前端子路径
webUrl: /
# 启用小海豹
builtInSealBotEnable: true
# 历史保留时限,用户能看到多少天前的聊天记录,默认为-1(永久),未实装
chatHistoryPersistentDays: -1
# 数据库地址,默认为 ./data/chat.db
dbUrl: postgresql://seal:123@localhost:5432/sealchat
```

## 其他说明
Expand Down
103 changes: 103 additions & 0 deletions model/channel_latest_read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package model

import (
"time"

"gorm.io/gorm/clause"
)

/*
本来我想了一些复杂的方案,并估算了内存和硬盘的使用
但随后,我意识到并不需要考虑那么多。
*/

type ChannelLatestReadModel struct {
StringPKBaseModel

ChannelId string `gorm:"index:idx_channel_user,unique" json:"channelId"` // 目前仅用于频道ID
UserId string `gorm:"index:idx_channel_user,unique;index" json:"userId"` // 用户ID

MessageId string
MessageTime int64

Mark string `json:"mark"` // 特殊标记
}

func (*ChannelLatestReadModel) TableName() string {
return "channel_latest_read"
}

func ChannelReadListByUserId(userId string) ([]*ChannelLatestReadModel, error) {
var records []*ChannelLatestReadModel
err := db.Where("user_id = ?", userId).Find(&records).Error
return records, err
}

func ChannelUnreadFetch(userId string) (map[string]int64, error) {
items, err := ChannelReadListByUserId(userId)
if err != nil {
return nil, err
}

var chIds []string
var timeLst []time.Time
for _, i := range items {
chIds = append(chIds, i.ChannelId)
timeLst = append(timeLst, time.UnixMilli(i.MessageTime))
}

unreadMap, err := MessagesCountByChannelIDsAfterTime(chIds, timeLst, userId)
if err != nil {
return nil, err
}

return unreadMap, err
}

func ChannelReadSet(channelId, userId string) error {
var record ChannelLatestReadModel
err := db.Where("channel_id = ? AND user_id = ?", channelId, userId).Limit(1).Find(&record).Error
if err != nil {
return err
}
if record.ID == "" {
// 记录不存在,创建新记录
record = ChannelLatestReadModel{
ChannelId: channelId,
UserId: userId,
MessageTime: time.Now().UnixMilli(),
}
return db.Create(&record).Error
}

return db.Model(&ChannelLatestReadModel{}).
Where("channel_id = ? AND user_id = ?", channelId, userId).
Updates(map[string]any{
"message_time": time.Now().UnixMilli(),
}).Error
}

func ChannelReadInit(channelId, userId string) error {
return db.Clauses(clause.OnConflict{
DoNothing: true, // 对应 INSERT OR IGNORE
}).Create(&ChannelLatestReadModel{
ChannelId: channelId,
UserId: userId,
MessageTime: 0,
}).Error
}

func ChannelReadInitInBatches(channelId string, userIds []string) error {
models := make([]ChannelLatestReadModel, len(userIds))
for i, userId := range userIds {
models[i] = ChannelLatestReadModel{
ChannelId: channelId,
UserId: userId,
MessageTime: 0,
}
}

return db.Clauses(clause.OnConflict{
DoNothing: true, // 对应 INSERT OR IGNORE
}).CreateInBatches(models, 100).Error
}
3 changes: 3 additions & 0 deletions model/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"sealchat/utils"
)

// 注: 所有时间戳使用 time.Now().UnixMilli()

var db *gorm.DB

type StringPKBaseModel struct {
Expand Down Expand Up @@ -88,6 +90,7 @@ func DBInit(dsn string) {
db.AutoMigrate(&TimelineUserLastRecordModel{})
db.AutoMigrate(&UserEmojiModel{})
db.AutoMigrate(&BotTokenModel{})
db.AutoMigrate(&ChannelLatestReadModel{})

db.AutoMigrate(&SystemRoleModel{}, &ChannelRoleModel{}, &RolePermissionModel{}, &UserRoleMappingModel{})
db.AutoMigrate(&FriendModel{}, &FriendRequestModel{})
Expand Down
1 change: 0 additions & 1 deletion model/friend.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ func FriendRelationSetVisibleById(id string) {
"visible1": true,
"visible2": true,
}
fmt.Println("!!!", id)
db.Model(&FriendModel{}).Where("id = ?", id).Updates(updates)
}

Expand Down
39 changes: 39 additions & 0 deletions model/message.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package model

import (
"errors"
"time"

"sealchat/protocol"
Expand Down Expand Up @@ -38,3 +39,41 @@ func (m *MessageModel) ToProtocolType2(channelData *protocol.Channel) *protocol.
CreatedAt: time.Now().UnixMilli(), // 跟js相匹配
}
}

func MessagesCountByChannelIDsAfterTime(channelIDs []string, updateTimes []time.Time, userID string) (map[string]int64, error) {
// updateTimes []int64
if len(channelIDs) != len(updateTimes) {
return nil, errors.New("channelIDs和updateTimes长度不匹配")
}

var results []struct {
ChannelID string
Count int64
}

query := db.Model(&MessageModel{}).
Select("channel_id, count(*) as count").
Where("user_id <> ?", userID)

// 使用gorm的条件构建器
conditions := db.Where("1 = 0") // 初始为false的条件
for i, channelID := range channelIDs {
conditions = conditions.Or(db.Where("channel_id = ? AND created_at > ?", channelID, updateTimes[i]))
}

err := query.Where(conditions).
Group("channel_id").
Find(&results).Error

if err != nil {
return nil, err
}

// 转换为map
countMap := make(map[string]int64)
for _, result := range results {
countMap[result.ChannelID] = result.Count
}

return countMap, nil
}
2 changes: 0 additions & 2 deletions ui/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ declare module 'vue' {
NDropdown: typeof import('naive-ui')['NDropdown']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber']
Expand Down
4 changes: 2 additions & 2 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<meta name=viewport
content="width=device-width, initial-scale=1.0, minimum-scale=1.0 maximum-scale=1.0, user-scalable=no">
<title>海豹尬聊 SealChat</title>
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
<!-- <script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script> -->
</head>

<body>
Expand Down
4 changes: 4 additions & 0 deletions ui/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ html, body {
}

/* .sc-header {} */

.label-unread {
@apply rounded-full bg-red-500 text-white text-xs px-1 py-0;
}
Loading

0 comments on commit 2694161

Please sign in to comment.