feat: 接入消息未读数、全部已读与首页红点角标同步。
固定消息中心为首页入口,并在推送到达、已读后刷新桌面角标。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -11,8 +11,14 @@ protocol MessageCenterServing {
|
||||
/// 拉取消息列表。
|
||||
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse
|
||||
|
||||
/// 标记消息已读。
|
||||
func markAsRead(messageId: Int) async throws
|
||||
/// 获取当前账号未读消息总数。
|
||||
func unreadCount() async throws -> MessageUnreadCountResponse
|
||||
|
||||
/// 标记消息已读并返回最新未读数量。
|
||||
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse
|
||||
|
||||
/// 将当前账号全部消息标记已读。
|
||||
func markAllAsRead() async throws -> MessageReadAllResponse
|
||||
|
||||
/// 删除消息。
|
||||
func delete(messageId: Int) async throws
|
||||
@ -43,9 +49,16 @@ final class MessageCenterAPI: MessageCenterServing {
|
||||
)
|
||||
}
|
||||
|
||||
/// GET /api/app/msg/unread-count
|
||||
func unreadCount() async throws -> MessageUnreadCountResponse {
|
||||
try await client.send(
|
||||
APIRequest(method: .get, path: "/api/app/msg/unread-count")
|
||||
)
|
||||
}
|
||||
|
||||
/// POST /api/app/msg/read
|
||||
func markAsRead(messageId: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/app/msg/read",
|
||||
@ -54,6 +67,13 @@ final class MessageCenterAPI: MessageCenterServing {
|
||||
)
|
||||
}
|
||||
|
||||
/// POST /api/app/msg/read-all
|
||||
func markAllAsRead() async throws -> MessageReadAllResponse {
|
||||
try await client.send(
|
||||
APIRequest(method: .post, path: "/api/app/msg/read-all")
|
||||
)
|
||||
}
|
||||
|
||||
/// POST /api/app/msg/delete
|
||||
func delete(messageId: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
|
||||
@ -32,6 +32,46 @@ enum MessageJSONValue: Decodable, Hashable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 消息未读数量响应,用于同步首页红点和桌面图标角标。
|
||||
struct MessageUnreadCountResponse: Decodable, Equatable, Sendable {
|
||||
let unreadCount: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case unreadCount = "unread_count"
|
||||
}
|
||||
|
||||
init(unreadCount: Int = 0) {
|
||||
self.unreadCount = max(unreadCount, 0)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
unreadCount = max(try container.decodeLossyInt(forKey: .unreadCount) ?? 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 全部标记已读响应,包含本次更新数量和操作后的未读数量。
|
||||
struct MessageReadAllResponse: Decodable, Equatable, Sendable {
|
||||
let updatedCount: Int
|
||||
let unreadCount: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case updatedCount = "updated_count"
|
||||
case unreadCount = "unread_count"
|
||||
}
|
||||
|
||||
init(updatedCount: Int = 0, unreadCount: Int = 0) {
|
||||
self.updatedCount = max(updatedCount, 0)
|
||||
self.unreadCount = max(unreadCount, 0)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
updatedCount = max(try container.decodeLossyInt(forKey: .updatedCount) ?? 0, 0)
|
||||
unreadCount = max(try container.decodeLossyInt(forKey: .unreadCount) ?? 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 消息中心列表响应,对齐 Android `MsgResponse`。
|
||||
struct MessageListResponse: Decodable, Equatable, Sendable {
|
||||
let hasMore: Bool
|
||||
|
||||
@ -11,11 +11,13 @@ final class MessageCenterViewModel {
|
||||
private(set) var isLoading = false
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var isLoadingMore = false
|
||||
private(set) var isMarkingAllAsRead = false
|
||||
private(set) var canLoadMore = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onOpenDetail: ((MessageItem) -> Void)?
|
||||
var onUnreadMessageCountChange: ((Int) -> Void)?
|
||||
|
||||
private let pageSize = 20
|
||||
private var lastId = 0
|
||||
@ -55,9 +57,10 @@ final class MessageCenterViewModel {
|
||||
}
|
||||
|
||||
do {
|
||||
try await api.markAsRead(messageId: id)
|
||||
let response = try await api.markAsRead(messageId: id)
|
||||
let readItem = item.markedRead()
|
||||
items = items.map { $0.id == id ? readItem : $0 }
|
||||
notifyUnreadMessageCountChanged(response.unreadCount)
|
||||
onOpenDetail?(readItem)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
@ -66,6 +69,30 @@ final class MessageCenterViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将当前账号的全部消息标记已读,成功时返回服务端最新统计。
|
||||
func markAllAsRead(api: any MessageCenterServing) async -> MessageReadAllResponse? {
|
||||
guard !isLoading, !isMarkingAllAsRead else { return nil }
|
||||
isMarkingAllAsRead = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isMarkingAllAsRead = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await api.markAllAsRead()
|
||||
items = items.map { $0.isRead ? $0 : $0.markedRead() }
|
||||
notifyUnreadMessageCountChanged(response.unreadCount)
|
||||
onShowMessage?(response.updatedCount > 0 ? "已全部标记为已读" : "暂无未读消息")
|
||||
return response
|
||||
} catch is CancellationError {
|
||||
return nil
|
||||
} catch {
|
||||
onShowMessage?("全部标记已读失败,请稍后重试")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除后从本地列表移除消息。
|
||||
func removeMessage(id: Int) {
|
||||
items.removeAll { $0.id == id }
|
||||
@ -110,6 +137,16 @@ final class MessageCenterViewModel {
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
|
||||
private func notifyUnreadMessageCountChanged(_ count: Int) {
|
||||
let normalizedCount = max(count, 0)
|
||||
onUnreadMessageCountChange?(normalizedCount)
|
||||
NotificationCenter.default.post(
|
||||
name: NotificationName.unreadMessageCountDidChange,
|
||||
object: nil,
|
||||
userInfo: [NotificationUserInfoKey.unreadCount: normalizedCount]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 消息详情页 ViewModel,负责删除当前消息。
|
||||
|
||||
Reference in New Issue
Block a user