fix(channels): sync preserve locally cleared dialogs
This commit is contained in:
parent
e22fac1c3f
commit
73cf6a8184
50 changed files with 1525 additions and 327 deletions
|
|
@ -95,8 +95,8 @@ func (s *ChannelStore) ListChannelDialogs(_ context.Context, viewerUserID int64,
|
|||
out.Dialogs = append(out.Dialogs, dialog)
|
||||
channel := s.channels[dialog.Peer.ID]
|
||||
out.Channels = append(out.Channels, channel)
|
||||
if msg, ok := s.findMessageLocked(dialog.Peer.ID, dialog.TopMessage); ok && !msg.Deleted {
|
||||
out.Messages = append(out.Messages, cloneChannelMessage(msg))
|
||||
if msg, ok := s.channelMessageForMemberLocked(viewerUserID, dialog.Peer.ID, dialog.TopMessage); ok {
|
||||
out.Messages = append(out.Messages, msg)
|
||||
}
|
||||
}
|
||||
// 与 PG 同因:getDialogs top message 按 viewer 补未读提及标志。
|
||||
|
|
@ -151,8 +151,8 @@ func (s *ChannelStore) GetChannelDialogs(_ context.Context, viewerUserID int64,
|
|||
out.Channels = append(out.Channels, cloneChannel(parent))
|
||||
}
|
||||
}
|
||||
if msg, ok := s.findMessageLocked(channelID, dialog.TopMessage); ok && !msg.Deleted {
|
||||
out.Messages = append(out.Messages, cloneChannelMessage(msg))
|
||||
if msg, ok := s.channelMessageForMemberLocked(viewerUserID, channelID, dialog.TopMessage); ok {
|
||||
out.Messages = append(out.Messages, msg)
|
||||
}
|
||||
}
|
||||
out.Count = len(out.Dialogs)
|
||||
|
|
@ -470,36 +470,47 @@ func channelDialogToDialog(dialog domain.ChannelDialog, channelPts int, memberSt
|
|||
return domain.Dialog{
|
||||
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: dialog.ChannelID},
|
||||
// 非成员预览(publicPreviewMember/被踢)须标记 ChannelLeft,客户端据此把频道渲染为只读 left 预览。
|
||||
ChannelLeft: memberStatus == domain.ChannelMemberLeft,
|
||||
FolderID: dialog.FolderID,
|
||||
TopMessage: dialog.TopMessageID,
|
||||
TopMessageDate: dialog.TopMessageDate,
|
||||
ReadInboxMaxID: dialog.ReadInboxMaxID,
|
||||
ReadOutboxMaxID: dialog.ReadOutboxMaxID,
|
||||
UnreadCount: dialog.UnreadCount,
|
||||
UnreadMentions: dialog.UnreadMentions,
|
||||
UnreadReactions: dialog.UnreadReactions,
|
||||
Pinned: dialog.Pinned,
|
||||
PinnedOrder: dialog.PinnedOrder,
|
||||
UnreadMark: dialog.UnreadMark,
|
||||
ViewForumAsMessages: dialog.ViewForumAsMessages,
|
||||
HasScheduled: dialog.HasScheduled,
|
||||
Pts: channelPts,
|
||||
ChannelLeft: memberStatus == domain.ChannelMemberLeft,
|
||||
FolderID: dialog.FolderID,
|
||||
TopMessage: dialog.TopMessageID,
|
||||
TopMessageDate: dialog.TopMessageDate,
|
||||
HistoryClearAnchorID: dialog.HistoryClearAnchorID,
|
||||
HistoryClearAnchorDate: dialog.HistoryClearAnchorDate,
|
||||
ReadInboxMaxID: dialog.ReadInboxMaxID,
|
||||
ReadOutboxMaxID: dialog.ReadOutboxMaxID,
|
||||
UnreadCount: dialog.UnreadCount,
|
||||
UnreadMentions: dialog.UnreadMentions,
|
||||
UnreadReactions: dialog.UnreadReactions,
|
||||
Pinned: dialog.Pinned,
|
||||
PinnedOrder: dialog.PinnedOrder,
|
||||
UnreadMark: dialog.UnreadMark,
|
||||
ViewForumAsMessages: dialog.ViewForumAsMessages,
|
||||
HasScheduled: dialog.HasScheduled,
|
||||
Pts: channelPts,
|
||||
}
|
||||
}
|
||||
|
||||
func previewChannelDialog(userID int64, channel domain.Channel, member domain.ChannelMember) domain.ChannelDialog {
|
||||
topMessageID := channel.TopMessageID
|
||||
if topMessageID <= member.AvailableMinID {
|
||||
topMessageID = 0
|
||||
topMessageID = member.HistoryClearAnchorID
|
||||
if topMessageID != member.AvailableMinID {
|
||||
topMessageID = 0
|
||||
}
|
||||
}
|
||||
topMessageDate := channel.Date
|
||||
if topMessageID > 0 && topMessageID == member.HistoryClearAnchorID {
|
||||
topMessageDate = member.HistoryClearAnchorDate
|
||||
}
|
||||
return domain.ChannelDialog{
|
||||
UserID: userID,
|
||||
ChannelID: channel.ID,
|
||||
TopMessageID: topMessageID,
|
||||
TopMessageDate: channel.Date,
|
||||
ReadInboxMaxID: maxInt(channel.TopMessageID, member.ReadInboxMaxID),
|
||||
ReadOutboxMaxID: maxInt(channel.TopMessageID, member.ReadOutboxMaxID),
|
||||
UserID: userID,
|
||||
ChannelID: channel.ID,
|
||||
TopMessageID: topMessageID,
|
||||
TopMessageDate: topMessageDate,
|
||||
HistoryClearAnchorID: member.HistoryClearAnchorID,
|
||||
HistoryClearAnchorDate: member.HistoryClearAnchorDate,
|
||||
ReadInboxMaxID: maxInt(channel.TopMessageID, member.ReadInboxMaxID),
|
||||
ReadOutboxMaxID: maxInt(channel.TopMessageID, member.ReadOutboxMaxID),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -337,9 +337,19 @@ func (s *ChannelStore) ListDirtyActiveChannelsForUser(_ context.Context, userID
|
|||
if !ok || member.Status != domain.ChannelMemberActive {
|
||||
continue
|
||||
}
|
||||
item := domain.DirtyChannel{ChannelID: channelID, Pts: channel.Pts}
|
||||
checkpoint := s.channelUpdateCheckpointLocked(channelID, channel)
|
||||
if checkpoint.LatestEventDate > sinceDate {
|
||||
out = append(out, domain.DirtyChannel{ChannelID: channelID, Pts: channel.Pts})
|
||||
item.ChannelUpdatesDirty = true
|
||||
}
|
||||
if clearDate := s.historyClearDates[channelID][userID]; clearDate >= sinceDate &&
|
||||
member.HistoryClearAnchorID > 0 &&
|
||||
member.HistoryClearAnchorID == member.AvailableMinID {
|
||||
item.AvailableMinID = member.AvailableMinID
|
||||
item.HistoryClearDate = clearDate
|
||||
}
|
||||
if item.ChannelUpdatesDirty || item.AvailableMinID > 0 {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
for channelID, channel := range s.channels {
|
||||
|
|
@ -356,7 +366,11 @@ func (s *ChannelStore) ListDirtyActiveChannelsForUser(_ context.Context, userID
|
|||
}
|
||||
}
|
||||
if !found {
|
||||
out = append(out, domain.DirtyChannel{ChannelID: channelID, Pts: channel.Pts})
|
||||
out = append(out, domain.DirtyChannel{
|
||||
ChannelID: channelID,
|
||||
Pts: channel.Pts,
|
||||
ChannelUpdatesDirty: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -453,12 +467,16 @@ func (s *ChannelStore) dialogForMemberLocked(userID int64, channel domain.Channe
|
|||
dialog.UserID = userID
|
||||
dialog.ChannelID = channel.ID
|
||||
dialog.TopMessageID = s.visibleTopMessageIDForMemberLocked(channel, member)
|
||||
dialog.HistoryClearAnchorID = member.HistoryClearAnchorID
|
||||
dialog.HistoryClearAnchorDate = member.HistoryClearAnchorDate
|
||||
// TopMessageDate 必须从可见 top 消息派生(不能继承空缓存的 0),否则会话排序/分页与预览
|
||||
// dialog 的日期全错。与 postgres GetChannelDialogs 用 getChannelMessage 设 date 对齐。
|
||||
dialog.TopMessageDate = 0
|
||||
if dialog.TopMessageID > 0 {
|
||||
if top, ok := s.findMessageLocked(channel.ID, dialog.TopMessageID); ok {
|
||||
if top, ok := s.channelMessageForMemberLocked(userID, channel.ID, dialog.TopMessageID); ok {
|
||||
dialog.TopMessageDate = top.Date
|
||||
} else if dialog.TopMessageID == member.HistoryClearAnchorID {
|
||||
dialog.TopMessageDate = member.HistoryClearAnchorDate
|
||||
}
|
||||
}
|
||||
if member.ReadInboxMaxID > dialog.ReadInboxMaxID {
|
||||
|
|
|
|||
|
|
@ -1107,6 +1107,8 @@ func syntheticMonoforumAdminMember(mono domain.Channel, parentMember domain.Chan
|
|||
}
|
||||
member.AvailableMinID = 0
|
||||
member.AvailableMinPts = 0
|
||||
member.HistoryClearAnchorID = 0
|
||||
member.HistoryClearAnchorDate = 0
|
||||
member.ReadInboxMaxID = mono.TopMessageID
|
||||
member.ReadOutboxMaxID = mono.TopMessageID
|
||||
member.UnreadMark = false
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ func (s *ChannelStore) DeleteChannelHistory(_ context.Context, req domain.Delete
|
|||
if err != nil {
|
||||
return domain.DeleteChannelHistoryResult{}, err
|
||||
}
|
||||
if req.Date <= 0 {
|
||||
req.Date = channel.Date
|
||||
if req.Date <= 0 {
|
||||
req.Date = 1
|
||||
}
|
||||
}
|
||||
maxID := req.MaxID
|
||||
if maxID <= 0 || maxID > channel.TopMessageID {
|
||||
maxID = channel.TopMessageID
|
||||
|
|
@ -79,6 +85,22 @@ func (s *ChannelStore) DeleteChannelHistory(_ context.Context, req domain.Delete
|
|||
member := s.members[req.ChannelID][req.UserID]
|
||||
if !req.ForEveryone {
|
||||
appliedMinID := maxInt(member.AvailableMinID, maxID)
|
||||
changed := appliedMinID > member.AvailableMinID
|
||||
if changed {
|
||||
anchorDate := req.Date
|
||||
if msg, ok := s.findMessageLocked(req.ChannelID, appliedMinID); ok && msg.Date > 0 {
|
||||
anchorDate = msg.Date
|
||||
}
|
||||
if anchorDate <= 0 {
|
||||
anchorDate = channel.Date
|
||||
}
|
||||
member.HistoryClearAnchorID = appliedMinID
|
||||
member.HistoryClearAnchorDate = anchorDate
|
||||
if s.historyClearDates[req.ChannelID] == nil {
|
||||
s.historyClearDates[req.ChannelID] = make(map[int64]int)
|
||||
}
|
||||
s.historyClearDates[req.ChannelID][req.UserID] = req.Date
|
||||
}
|
||||
member.AvailableMinID = appliedMinID
|
||||
member.ReadInboxMaxID = maxInt(member.ReadInboxMaxID, appliedMinID)
|
||||
member.UnreadMark = false
|
||||
|
|
@ -92,7 +114,11 @@ func (s *ChannelStore) DeleteChannelHistory(_ context.Context, req domain.Delete
|
|||
s.dialogs[req.UserID] = make(map[int64]domain.ChannelDialog)
|
||||
}
|
||||
s.dialogs[req.UserID][req.ChannelID] = s.dialogForUserLocked(req.UserID, channel)
|
||||
return domain.DeleteChannelHistoryResult{Channel: channel, AvailableMinID: appliedMinID}, nil
|
||||
return domain.DeleteChannelHistoryResult{
|
||||
Channel: channel,
|
||||
AvailableMinID: appliedMinID,
|
||||
AvailableMinChanged: changed,
|
||||
}, nil
|
||||
}
|
||||
if !canDeleteAnyChannelMessage(member) {
|
||||
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelAdminRequired
|
||||
|
|
|
|||
|
|
@ -25,8 +25,16 @@ func (s *ChannelStore) ListChannelHistory(_ context.Context, viewerUserID int64,
|
|||
query := strings.ToLower(strings.TrimSpace(filter.Query))
|
||||
matched := make([]domain.ChannelMessage, 0, len(items))
|
||||
monoforumUserView := channel.Monoforum && !member.CanManageDirectMessages()
|
||||
anchorID := 0
|
||||
if filter.IncludeHistoryClearAnchor &&
|
||||
member.HistoryClearAnchorID > 0 &&
|
||||
member.HistoryClearAnchorID == member.AvailableMinID {
|
||||
anchorID = member.HistoryClearAnchorID
|
||||
}
|
||||
anchorSeen := false
|
||||
for _, msg := range items {
|
||||
if msg.Deleted {
|
||||
isAnchor := anchorID > 0 && msg.ID == anchorID
|
||||
if msg.Deleted && !isAnchor {
|
||||
continue
|
||||
}
|
||||
if channel.Monoforum {
|
||||
|
|
@ -37,9 +45,18 @@ func (s *ChannelStore) ListChannelHistory(_ context.Context, viewerUserID int64,
|
|||
continue
|
||||
}
|
||||
}
|
||||
if msg.ID <= member.AvailableMinID {
|
||||
if msg.ID < member.AvailableMinID || (msg.ID == member.AvailableMinID && !isAnchor) {
|
||||
continue
|
||||
}
|
||||
if isAnchor {
|
||||
msg = domain.ProjectChannelHistoryClearMessage(
|
||||
msg,
|
||||
filter.ChannelID,
|
||||
member.HistoryClearAnchorID,
|
||||
member.HistoryClearAnchorDate,
|
||||
)
|
||||
anchorSeen = true
|
||||
}
|
||||
if filter.PinnedOnly && !msg.Pinned {
|
||||
continue
|
||||
}
|
||||
|
|
@ -66,6 +83,24 @@ func (s *ChannelStore) ListChannelHistory(_ context.Context, viewerUserID int64,
|
|||
}
|
||||
matched = append(matched, msg)
|
||||
}
|
||||
if anchorID > 0 && !anchorSeen {
|
||||
msg := domain.ProjectChannelHistoryClearMessage(
|
||||
domain.ChannelMessage{},
|
||||
filter.ChannelID,
|
||||
member.HistoryClearAnchorID,
|
||||
member.HistoryClearAnchorDate,
|
||||
)
|
||||
if (filter.MinDate <= 0 || msg.Date > filter.MinDate) &&
|
||||
(filter.MaxDate <= 0 || msg.Date < filter.MaxDate) &&
|
||||
(filter.MaxID <= 0 || msg.ID <= filter.MaxID) &&
|
||||
(filter.MinID <= 0 || msg.ID > filter.MinID) &&
|
||||
!filter.PinnedOnly &&
|
||||
!filter.MusicOnly &&
|
||||
query == "" &&
|
||||
filter.SenderUserID == 0 {
|
||||
matched = append(matched, msg)
|
||||
}
|
||||
}
|
||||
extraChannels := []domain.Channel(nil)
|
||||
if channel.Monoforum && channel.LinkedMonoforumID != 0 {
|
||||
if parent, ok := s.channels[channel.LinkedMonoforumID]; ok && !parent.Deleted {
|
||||
|
|
@ -179,6 +214,7 @@ func (s *ChannelStore) ListChannelHistory(_ context.Context, viewerUserID int64,
|
|||
}
|
||||
s.populateChannelMessageRepliesLocked(viewerUserID, filter.ChannelID, out)
|
||||
s.populateChannelMessageReactionsLocked(viewerUserID, channel, out)
|
||||
projectMemoryChannelHistoryClearMessages(channel.ID, member, out)
|
||||
return domain.ChannelHistory{
|
||||
Channel: channel,
|
||||
Self: member,
|
||||
|
|
@ -318,17 +354,59 @@ func (s *ChannelStore) GetChannelMessages(_ context.Context, viewerUserID, chann
|
|||
if _, ok := wanted[msg.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
if msg.ID == member.HistoryClearAnchorID &&
|
||||
member.HistoryClearAnchorID == member.AvailableMinID {
|
||||
messages = append(messages, domain.ProjectChannelHistoryClearMessage(
|
||||
msg,
|
||||
channelID,
|
||||
member.HistoryClearAnchorID,
|
||||
member.HistoryClearAnchorDate,
|
||||
))
|
||||
delete(wanted, msg.ID)
|
||||
continue
|
||||
}
|
||||
if msg.Deleted || msg.ID <= member.AvailableMinID {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, cloneChannelMessage(msg))
|
||||
delete(wanted, msg.ID)
|
||||
}
|
||||
if member.HistoryClearAnchorID > 0 &&
|
||||
member.HistoryClearAnchorID == member.AvailableMinID {
|
||||
if _, ok := wanted[member.HistoryClearAnchorID]; ok {
|
||||
messages = append(messages, domain.ProjectChannelHistoryClearMessage(
|
||||
domain.ChannelMessage{},
|
||||
channelID,
|
||||
member.HistoryClearAnchorID,
|
||||
member.HistoryClearAnchorDate,
|
||||
))
|
||||
}
|
||||
}
|
||||
sort.Slice(messages, func(i, j int) bool { return messages[i].ID > messages[j].ID })
|
||||
s.populateChannelMessageRepliesLocked(viewerUserID, channelID, messages)
|
||||
s.populateChannelMessageReactionsLocked(viewerUserID, channel, messages)
|
||||
projectMemoryChannelHistoryClearMessages(channelID, member, messages)
|
||||
return domain.ChannelHistory{Channel: channel, Self: member, Messages: messages, Count: len(messages)}, nil
|
||||
}
|
||||
|
||||
func projectMemoryChannelHistoryClearMessages(channelID int64, member domain.ChannelMember, messages []domain.ChannelMessage) {
|
||||
if member.HistoryClearAnchorID <= 0 ||
|
||||
member.HistoryClearAnchorID != member.AvailableMinID {
|
||||
return
|
||||
}
|
||||
for i := range messages {
|
||||
if messages[i].ID != member.HistoryClearAnchorID {
|
||||
continue
|
||||
}
|
||||
messages[i] = domain.ProjectChannelHistoryClearMessage(
|
||||
messages[i],
|
||||
channelID,
|
||||
member.HistoryClearAnchorID,
|
||||
member.HistoryClearAnchorDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ListStoryMessageForwards(_ context.Context, req domain.StoryMessageForwardListRequest) (domain.StoryMessageForwardList, error) {
|
||||
if req.ViewerUserID == 0 || req.Owner.ID == 0 || req.StoryID <= 0 || req.StoryID > domain.MaxStoryID {
|
||||
return domain.StoryMessageForwardList{}, domain.ErrStoryIDInvalid
|
||||
|
|
@ -631,9 +709,40 @@ func (s *ChannelStore) visibleTopMessageIDForMemberLocked(channel domain.Channel
|
|||
return msg.ID
|
||||
}
|
||||
}
|
||||
if member.HistoryClearAnchorID > 0 &&
|
||||
member.HistoryClearAnchorID == member.AvailableMinID {
|
||||
return member.HistoryClearAnchorID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *ChannelStore) channelMessageForMemberLocked(userID, channelID int64, messageID int) (domain.ChannelMessage, bool) {
|
||||
member, ok := s.members[channelID][userID]
|
||||
if !ok {
|
||||
msg, found := s.findMessageLocked(channelID, messageID)
|
||||
if !found || msg.Deleted {
|
||||
return domain.ChannelMessage{}, false
|
||||
}
|
||||
return cloneChannelMessage(msg), true
|
||||
}
|
||||
if member.HistoryClearAnchorID > 0 &&
|
||||
member.HistoryClearAnchorID == member.AvailableMinID &&
|
||||
messageID == member.HistoryClearAnchorID {
|
||||
source, _ := s.findMessageLocked(channelID, messageID)
|
||||
return domain.ProjectChannelHistoryClearMessage(
|
||||
source,
|
||||
channelID,
|
||||
member.HistoryClearAnchorID,
|
||||
member.HistoryClearAnchorDate,
|
||||
), true
|
||||
}
|
||||
msg, ok := s.findMessageLocked(channelID, messageID)
|
||||
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID {
|
||||
return domain.ChannelMessage{}, false
|
||||
}
|
||||
return cloneChannelMessage(msg), true
|
||||
}
|
||||
|
||||
func (s *ChannelStore) topicHasVisibleMessagesLocked(channelID int64, topicID int) bool {
|
||||
for _, msg := range s.messages[channelID] {
|
||||
if msg.Deleted {
|
||||
|
|
|
|||
|
|
@ -73,14 +73,19 @@ type ChannelStore struct {
|
|||
messages map[int64][]domain.ChannelMessage
|
||||
reactions map[int64]map[int]map[int64][]domain.ChannelMessagePeerReaction
|
||||
// paidReactions 是 per-(channel,message,user) 付费 reaction 累计星数 + 匿名标志。
|
||||
paidReactions map[int64]map[int]map[int64]memoryPaidReaction
|
||||
top map[int64]map[string]domain.TopMessageReaction
|
||||
recent map[int64]map[string]domain.RecentMessageReaction
|
||||
mentions map[int64]map[int64]map[int]memoryMention
|
||||
msgViews map[int64]map[int]int
|
||||
msgViewers map[int64]map[int]map[int64]struct{}
|
||||
events map[int64][]domain.ChannelUpdateEvent
|
||||
retention map[int64]domain.ChannelUpdateRetentionCheckpoint
|
||||
paidReactions map[int64]map[int]map[int64]memoryPaidReaction
|
||||
top map[int64]map[string]domain.TopMessageReaction
|
||||
recent map[int64]map[string]domain.RecentMessageReaction
|
||||
mentions map[int64]map[int64]map[int]memoryMention
|
||||
msgViews map[int64]map[int]int
|
||||
msgViewers map[int64]map[int]map[int64]struct{}
|
||||
events map[int64][]domain.ChannelUpdateEvent
|
||||
retention map[int64]domain.ChannelUpdateRetentionCheckpoint
|
||||
// historyClearDates is the no-PTS recovery timestamp for a future
|
||||
// owner-local clear, keyed by channel then user. The member remains the
|
||||
// absolute boundary authority; this map only makes account difference
|
||||
// discovery bounded without scanning messages.
|
||||
historyClearDates map[int64]map[int64]int
|
||||
adminLogs map[int64][]domain.ChannelAdminLogEvent
|
||||
invites map[string]domain.ChannelInvite
|
||||
importers map[int64]map[int64]domain.ChannelInviteImporter
|
||||
|
|
@ -137,6 +142,7 @@ func NewChannelStore() *ChannelStore {
|
|||
msgViewers: make(map[int64]map[int]map[int64]struct{}),
|
||||
events: make(map[int64][]domain.ChannelUpdateEvent),
|
||||
retention: make(map[int64]domain.ChannelUpdateRetentionCheckpoint),
|
||||
historyClearDates: make(map[int64]map[int64]int),
|
||||
adminLogs: make(map[int64][]domain.ChannelAdminLogEvent),
|
||||
invites: make(map[string]domain.ChannelInvite),
|
||||
importers: make(map[int64]map[int64]domain.ChannelInviteImporter),
|
||||
|
|
|
|||
|
|
@ -873,6 +873,7 @@ func TestChannelDeleteHistoryLocalClearReturnsMonotonicAvailableMinID(t *testing
|
|||
CreatorUserID: 1,
|
||||
Title: "monotonic local clear",
|
||||
Megagroup: true,
|
||||
MemberUserIDs: []int64{2},
|
||||
Date: 1_700_000_250,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -911,6 +912,20 @@ func TestChannelDeleteHistoryLocalClearReturnsMonotonicAvailableMinID(t *testing
|
|||
if high.AvailableMinID != second.Message.ID {
|
||||
t.Fatalf("high available_min_id = %d, want %d", high.AvailableMinID, second.Message.ID)
|
||||
}
|
||||
if !high.AvailableMinChanged {
|
||||
t.Fatal("high clear did not report an advanced owner-local boundary")
|
||||
}
|
||||
dirtyAfterClear, err := store.ListDirtyActiveChannelsForUser(ctx, 1, 1_700_000_253, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("list dirty channels after owner-local clear: %v", err)
|
||||
}
|
||||
if len(dirtyAfterClear) != 1 ||
|
||||
dirtyAfterClear[0].ChannelID != created.Channel.ID ||
|
||||
dirtyAfterClear[0].AvailableMinID != second.Message.ID ||
|
||||
dirtyAfterClear[0].HistoryClearDate != 1_700_000_253 ||
|
||||
dirtyAfterClear[0].ChannelUpdatesDirty {
|
||||
t.Fatalf("dirty owner-local clear = %+v, want only absolute boundary %d at date 1700000253", dirtyAfterClear, second.Message.ID)
|
||||
}
|
||||
|
||||
stale, err := store.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
|
||||
UserID: 1,
|
||||
|
|
@ -924,13 +939,38 @@ func TestChannelDeleteHistoryLocalClearReturnsMonotonicAvailableMinID(t *testing
|
|||
if stale.AvailableMinID != second.Message.ID {
|
||||
t.Fatalf("stale available_min_id = %d, want monotonic %d", stale.AvailableMinID, second.Message.ID)
|
||||
}
|
||||
if stale.AvailableMinChanged {
|
||||
t.Fatal("stale clear unexpectedly replaced the owner-local anchor")
|
||||
}
|
||||
dirtyAfterStale, err := store.ListDirtyActiveChannelsForUser(ctx, 1, 1_700_000_254, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("list dirty channels after stale owner-local clear: %v", err)
|
||||
}
|
||||
if len(dirtyAfterStale) != 0 {
|
||||
t.Fatalf("stale clear refreshed recovery timestamp: %+v", dirtyAfterStale)
|
||||
}
|
||||
|
||||
history, err := store.ListChannelHistory(ctx, 1, domain.ChannelHistoryFilter{ChannelID: created.Channel.ID, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("list history: %v", err)
|
||||
}
|
||||
if len(history.Messages) != 0 {
|
||||
t.Fatalf("history after stale clear = %+v, want no visible messages", history.Messages)
|
||||
t.Fatalf("unprojected history after stale clear = %+v, want no shared messages", history.Messages)
|
||||
}
|
||||
history, err = store.ListChannelHistory(ctx, 1, domain.ChannelHistoryFilter{
|
||||
ChannelID: created.Channel.ID,
|
||||
Limit: 10,
|
||||
IncludeHistoryClearAnchor: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list projected history: %v", err)
|
||||
}
|
||||
if len(history.Messages) != 1 ||
|
||||
history.Messages[0].ID != second.Message.ID ||
|
||||
!domain.IsChannelHistoryClearMessage(history.Messages[0]) ||
|
||||
history.Messages[0].Body != "" ||
|
||||
history.Messages[0].Media != nil {
|
||||
t.Fatalf("projected history after stale clear = %+v, want sanitized history-clear anchor %d", history.Messages, second.Message.ID)
|
||||
}
|
||||
dialogs, err := store.GetChannelDialogs(ctx, 1, []int64{created.Channel.ID})
|
||||
if err != nil {
|
||||
|
|
@ -939,8 +979,140 @@ func TestChannelDeleteHistoryLocalClearReturnsMonotonicAvailableMinID(t *testing
|
|||
if len(dialogs.Dialogs) != 1 {
|
||||
t.Fatalf("dialogs = %+v, want one dialog", dialogs.Dialogs)
|
||||
}
|
||||
if dialogs.Dialogs[0].TopMessage != 0 || dialogs.Dialogs[0].ReadInboxMaxID != second.Message.ID || dialogs.Dialogs[0].UnreadCount != 0 {
|
||||
t.Fatalf("dialog after stale clear = %+v, want top=0 read=%d unread=0", dialogs.Dialogs[0], second.Message.ID)
|
||||
if dialogs.Dialogs[0].TopMessage != second.Message.ID ||
|
||||
dialogs.Dialogs[0].ReadInboxMaxID != second.Message.ID ||
|
||||
dialogs.Dialogs[0].UnreadCount != 0 ||
|
||||
len(dialogs.Messages) != 1 ||
|
||||
!domain.IsChannelHistoryClearMessage(dialogs.Messages[0]) {
|
||||
t.Fatalf("dialog after stale clear = %+v messages=%+v, want anchored top=%d read=%d unread=0", dialogs.Dialogs[0], dialogs.Messages, second.Message.ID, second.Message.ID)
|
||||
}
|
||||
otherDialogs, err := store.GetChannelDialogs(ctx, 2, []int64{created.Channel.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("get other member dialog: %v", err)
|
||||
}
|
||||
if len(otherDialogs.Dialogs) != 1 ||
|
||||
otherDialogs.Dialogs[0].TopMessage != second.Message.ID ||
|
||||
len(otherDialogs.Messages) != 1 ||
|
||||
otherDialogs.Messages[0].Body != "second visible" ||
|
||||
otherDialogs.Messages[0].Action != nil {
|
||||
t.Fatalf("other member projection changed by owner clear: dialogs=%+v messages=%+v", otherDialogs.Dialogs, otherDialogs.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDeleteHistoryLocalClearKeepsMegagroupAndBroadcastDialogs(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
megagroup bool
|
||||
broadcast bool
|
||||
}{
|
||||
{name: "megagroup", megagroup: true},
|
||||
{name: "broadcast", broadcast: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewChannelStore()
|
||||
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: 11,
|
||||
Title: tc.name + " local clear",
|
||||
Megagroup: tc.megagroup,
|
||||
Broadcast: tc.broadcast,
|
||||
Date: 1_700_000_270,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
sent, err := store.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: 11,
|
||||
ChannelID: created.Channel.ID,
|
||||
RandomID: 31_001,
|
||||
Message: "clear this",
|
||||
Date: 1_700_000_271,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send channel message: %v", err)
|
||||
}
|
||||
channelPts := sent.Channel.Pts
|
||||
cleared, err := store.DeleteChannelHistory(ctx, domain.DeleteChannelHistoryRequest{
|
||||
UserID: 11,
|
||||
ChannelID: created.Channel.ID,
|
||||
MaxID: sent.Message.ID,
|
||||
Date: 1_700_000_272,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("clear local history: %v", err)
|
||||
}
|
||||
if cleared.Channel.Pts != channelPts || cleared.Event.Pts != 0 {
|
||||
t.Fatalf("local clear changed channel pts: before=%d result=%+v", channelPts, cleared)
|
||||
}
|
||||
dialogs, err := store.GetChannelDialogs(ctx, 11, []int64{created.Channel.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("get dialogs after clear: %v", err)
|
||||
}
|
||||
if len(dialogs.Dialogs) != 1 ||
|
||||
dialogs.Dialogs[0].TopMessage != sent.Message.ID ||
|
||||
len(dialogs.Messages) != 1 ||
|
||||
!domain.IsChannelHistoryClearMessage(dialogs.Messages[0]) {
|
||||
t.Fatalf("dialog disappeared after %s clear: dialogs=%+v messages=%+v", tc.name, dialogs.Dialogs, dialogs.Messages)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelPrehistoryBoundaryDoesNotCreateHistoryClearAnchor(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := NewChannelStore()
|
||||
created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: 21,
|
||||
Title: "hidden prehistory",
|
||||
Megagroup: true,
|
||||
Date: 1_700_000_280,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
old, err := store.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: 21,
|
||||
ChannelID: created.Channel.ID,
|
||||
RandomID: 32_001,
|
||||
Message: "prehistory",
|
||||
Date: 1_700_000_281,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send prehistory message: %v", err)
|
||||
}
|
||||
if _, err := store.SetPreHistoryHidden(ctx, 21, created.Channel.ID, true); err != nil {
|
||||
t.Fatalf("hide prehistory: %v", err)
|
||||
}
|
||||
invited, err := store.InviteToChannel(ctx, created.Channel.ID, 21, []int64{22}, 1_700_000_282)
|
||||
if err != nil {
|
||||
t.Fatalf("invite member: %v", err)
|
||||
}
|
||||
if len(invited.Members) != 1 ||
|
||||
invited.Members[0].AvailableMinID != old.Message.ID ||
|
||||
invited.Members[0].HistoryClearAnchorID != 0 ||
|
||||
invited.Members[0].HistoryClearAnchorDate != 0 {
|
||||
t.Fatalf("invited member = %+v, want prehistory boundary without local-clear anchor", invited.Members)
|
||||
}
|
||||
|
||||
history, err := store.ListChannelHistory(ctx, 22, domain.ChannelHistoryFilter{
|
||||
ChannelID: created.Channel.ID,
|
||||
Limit: 10,
|
||||
IncludeHistoryClearAnchor: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list invited member history: %v", err)
|
||||
}
|
||||
for _, message := range history.Messages {
|
||||
if message.ID == old.Message.ID || domain.IsChannelHistoryClearMessage(message) {
|
||||
t.Fatalf("prehistory boundary leaked a local-clear marker: %+v", history.Messages)
|
||||
}
|
||||
}
|
||||
byID, err := store.GetChannelMessages(ctx, 22, created.Channel.ID, []int{old.Message.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("get prehistory message by id: %v", err)
|
||||
}
|
||||
if len(byID.Messages) != 0 {
|
||||
t.Fatalf("prehistory message by id = %+v, want hidden without fabricated marker", byID.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue