fix: sync public channel preview updates

This commit is contained in:
iamxvbaba 2026-07-24 14:50:17 +08:00
parent 2289a31f46
commit b4aaf57d6b
27 changed files with 877 additions and 128 deletions

View file

@ -116,7 +116,7 @@ func TestChannelStoreDifferenceStartsAtMemberAvailableMinPts(t *testing.T) {
}
}
func TestChannelStorePublicPreviewDifferenceSkipsNonMemberMessages(t *testing.T) {
func TestChannelStorePublicPreviewDifferenceReplaysVisibleMessages(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
@ -183,12 +183,57 @@ func TestChannelStorePublicPreviewDifferenceSkipsNonMemberMessages(t *testing.T)
if err != nil {
t.Fatalf("list public preview difference: %v", err)
}
if !diff.Final || diff.Pts != sent.Event.Pts || len(diff.Events) != 0 || len(diff.NewMessages) != 0 || len(diff.OtherUpdates) != 0 {
t.Fatalf("preview diff = %+v, want empty public preview difference at current pts", diff)
if !diff.Final || diff.Pts != sent.Event.Pts || len(diff.Events) != 1 || len(diff.NewMessages) != 1 || len(diff.OtherUpdates) != 0 {
t.Fatalf("preview diff = %+v, want one durable public preview message at pts %d", diff, sent.Event.Pts)
}
if diff.NewMessages[0].ID != sent.Message.ID || diff.NewMessages[0].Body != sent.Message.Body {
t.Fatalf("preview diff message = %+v, want sent message %+v", diff.NewMessages[0], sent.Message)
}
if diff.Dialog.UnreadCount != 0 || diff.Dialog.ReadInboxMaxID < sent.Message.ID {
t.Fatalf("preview diff dialog = %+v, want read-only public preview dialog", diff.Dialog)
}
edited, err := channels.EditChannelMessage(ctx, domain.EditChannelMessageRequest{
UserID: owner.ID, ChannelID: channelID, ID: sent.Message.ID,
Message: "public preview edited", EditDate: 1700000372,
})
if err != nil {
t.Fatalf("edit public preview message: %v", err)
}
pinned, err := channels.UpdatePinnedMessage(ctx, domain.UpdateChannelPinnedMessageRequest{
UserID: owner.ID, ChannelID: channelID, MessageID: sent.Message.ID,
Pinned: true, Date: 1700000373,
})
if err != nil {
t.Fatalf("pin public preview message: %v", err)
}
deleted, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{
UserID: owner.ID, ChannelID: channelID, IDs: []int{sent.Message.ID}, Date: 1700000374,
})
if err != nil {
t.Fatalf("delete public preview message: %v", err)
}
mutations, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
UserID: viewer.ID, ChannelID: channelID, Pts: sent.Event.Pts, Limit: 10,
})
if err != nil {
t.Fatalf("list public preview mutations: %v", err)
}
if !mutations.Final || mutations.Pts != deleted.Event.Pts || len(mutations.NewMessages) != 0 || len(mutations.OtherUpdates) != 3 {
t.Fatalf("public preview mutations = %+v, want edit/pin/delete through pts %d", mutations, deleted.Event.Pts)
}
wantTypes := []domain.ChannelUpdateEventType{
domain.ChannelUpdateEditMessage,
domain.ChannelUpdatePinnedMessages,
domain.ChannelUpdateDeleteMessages,
}
for i, want := range wantTypes {
if mutations.OtherUpdates[i].Type != want {
t.Fatalf("public preview mutation[%d] = %+v, want %s", i, mutations.OtherUpdates[i], want)
}
}
if edited.Event.Pts >= pinned.Event.Pts || pinned.Event.Pts >= deleted.Event.Pts {
t.Fatalf("public preview mutation pts = edit %d pin %d delete %d, want strictly increasing", edited.Event.Pts, pinned.Event.Pts, deleted.Event.Pts)
}
}
func TestChannelStoreDifferenceUsesDurableMessageSnapshots(t *testing.T) {

View file

@ -360,3 +360,52 @@ ORDER BY user_id`, channelID, candidates[start:end])
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out, nil
}
func (s *ChannelStore) FilterChannelMessageAudienceIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error) {
if channelID == 0 || len(userIDs) == 0 {
return nil, nil
}
candidates := uniqueChannelUserIDs(userIDs, 0)
if len(candidates) == 0 {
return nil, nil
}
out := make([]int64, 0, len(candidates))
for start := 0; start < len(candidates); start += channelMemberFilterBatch {
end := start + channelMemberFilterBatch
if end > len(candidates) {
end = len(candidates)
}
rows, err := s.db.Query(ctx, `
SELECT candidate.user_id
FROM channels c
CROSS JOIN unnest($2::bigint[]) AS candidate(user_id)
LEFT JOIN channel_members m
ON m.channel_id = c.id AND m.user_id = candidate.user_id
WHERE c.id = $1
AND NOT c.deleted
AND NOT COALESCE((m.banned_rights->>'ViewMessages')::boolean, false)
AND COALESCE(m.status, '') NOT IN ('kicked', 'banned')
AND (
m.status = 'active'
OR (COALESCE(c.username, '') <> '' AND COALESCE(m.status, 'left') = 'left')
)
ORDER BY candidate.user_id`, channelID, candidates[start:end])
if err != nil {
return nil, fmt.Errorf("filter channel message audience: %w", err)
}
for rows.Next() {
var userID int64
if err := rows.Scan(&userID); err != nil {
rows.Close()
return nil, err
}
out = append(out, userID)
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
rows.Close()
}
return out, nil
}

View file

@ -75,12 +75,42 @@ func TestPublicChannelAndMegagroupPreviewPostgres(t *testing.T) {
if !found || history.Self.Status != domain.ChannelMemberLeft {
t.Fatalf("preview history = %+v self=%+v", history.Messages, history.Self)
}
audience, err := channels.FilterChannelMessageAudienceIDs(ctx, public.ID, []int64{viewer.ID, owner.ID, viewer.ID})
if err != nil {
t.Fatalf("filter public message audience: %v", err)
}
if len(audience) != 2 || audience[0] != owner.ID || audience[1] != viewer.ID {
t.Fatalf("public message audience = %v, want owner/member and viewer/subscriber", audience)
}
diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
UserID: viewer.ID, ChannelID: public.ID, Pts: created.Event.Pts, Limit: 20,
})
if err != nil {
t.Fatalf("public preview difference: %v", err)
}
if !diff.Final || diff.Pts != sent.Event.Pts || len(diff.NewMessages) != 1 || diff.NewMessages[0].ID != sent.Message.ID {
t.Fatalf("public preview difference = %+v, want sent message through pts %d", diff, sent.Event.Pts)
}
if _, err := channels.GetParticipants(ctx, viewer.ID, public.ID, domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsRecent}, 0, 20); err != nil {
t.Fatalf("public preview participants: %v", err)
}
if _, err := channels.GetParticipant(ctx, viewer.ID, public.ID, viewer.ID); !errors.Is(err, domain.ErrUserNotParticipant) {
t.Fatalf("public preview self participant err = %v, want ErrUserNotParticipant", err)
}
dialogs := appdialogs.NewService(nil, channels)
peerDialogs, err := dialogs.GetPeerDialogs(ctx, viewer.ID, []domain.Peer{{Type: domain.PeerTypeChannel, ID: public.ID}})
if err != nil {
t.Fatalf("public preview peer dialogs: %v", err)
}
if len(peerDialogs.Dialogs) != 1 || len(peerDialogs.ChannelMessages) != 0 || len(peerDialogs.Channels) != 1 {
t.Fatalf("public preview peer dialogs = %+v, want one zero-top bootstrap", peerDialogs)
}
previewDialog := peerDialogs.Dialogs[0]
if previewDialog.TopMessage != 0 || !previewDialog.ChannelLeft ||
previewDialog.ReadInboxMaxID != 0 || previewDialog.ReadOutboxMaxID != 0 ||
previewDialog.Pts != sent.Event.Pts {
t.Fatalf("public preview bootstrap dialog = %+v", previewDialog)
}
var memberExists bool
if err := pool.QueryRow(ctx, `SELECT EXISTS (
SELECT 1 FROM channel_members WHERE channel_id = $1 AND user_id = $2
@ -96,6 +126,28 @@ SELECT 1 FROM channel_members WHERE channel_id = $1 AND user_id = $2
if _, err := channels.LeaveChannel(ctx, public.ID, viewer.ID, 1700009430+i); err != nil {
t.Fatalf("leave public peer: %v", err)
}
filtered, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
UserID: viewer.ID, ChannelID: public.ID, Pts: sent.Event.Pts, Limit: 20,
})
if err != nil {
t.Fatalf("public difference across participant events: %v", err)
}
if tc.broadcast {
if !filtered.Final || filtered.Pts != sent.Event.Pts || len(filtered.Events) != 0 ||
len(filtered.NewMessages) != 0 || len(filtered.OtherUpdates) != 0 {
t.Fatalf("broadcast difference after transient participant changes = %+v, want unchanged PTS", filtered)
}
} else {
if !filtered.Final || filtered.Pts <= sent.Event.Pts || len(filtered.NewMessages) != 2 ||
len(filtered.OtherUpdates) != 0 {
t.Fatalf("megagroup join/leave difference = %+v, want two real service messages", filtered)
}
for _, message := range filtered.NewMessages {
if message.Action == nil {
t.Fatalf("megagroup join/leave difference message = %+v, want service action", message)
}
}
}
if _, err := channels.GetParticipant(ctx, viewer.ID, public.ID, viewer.ID); !errors.Is(err, domain.ErrUserNotParticipant) {
t.Fatalf("left self participant err = %v, want ErrUserNotParticipant", err)
}
@ -112,6 +164,9 @@ SELECT 1 FROM channel_members WHERE channel_id = $1 AND user_id = $2
t.Fatalf("create private group: %v", err)
}
channelIDs = append(channelIDs, private.Channel.ID)
if audience, err := channels.FilterChannelMessageAudienceIDs(ctx, private.Channel.ID, []int64{viewer.ID}); err != nil || len(audience) != 0 {
t.Fatalf("private message audience = %v err %v, want empty", audience, err)
}
if _, err := channels.ListChannelHistory(ctx, viewer.ID, domain.ChannelHistoryFilter{ChannelID: private.Channel.ID, Limit: 20}); !errors.Is(err, domain.ErrChannelPrivate) {
t.Fatalf("private preview history err = %v, want ErrChannelPrivate", err)
}

View file

@ -27,16 +27,6 @@ func (s *ChannelStore) ListChannelDifference(ctx context.Context, req domain.Cha
if limit <= 0 || limit > domain.MaxChannelDifferenceLimit {
limit = domain.MaxChannelDifferenceLimit
}
if preview && member.Status != domain.ChannelMemberActive {
return domain.ChannelDifference{
Channel: channel,
Self: member,
Pts: channel.Pts,
Final: true,
Timeout: 30,
Dialog: previewChannelDialog(req.UserID, channel, member),
}, nil
}
checkpoint, err := getChannelUpdateCheckpoint(ctx, s.db, req.ChannelID)
if err != nil {
return domain.ChannelDifference{}, err