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

@ -342,11 +342,24 @@ func (s *Service) appendMissingChannelPeerPreviews(ctx context.Context, userID i
if !ok || view.Forbidden {
continue
}
if view.Self.Status == domain.ChannelMemberLeft && !view.Self.Guest {
// A visible public preview still needs one transient dialog so a
// client can finish bootstrapping the requested peer. Keep the top
// message and read state at zero: the response is an admission
// token, not a persisted/chat-list dialog snapshot. In particular,
// clients that persist non-zero top dialogs will instead continue
// with messages.getHistory, which is the authoritative preview
// history path.
out.Dialogs = append(out.Dialogs, publicChannelPreviewBootstrapDialog(view))
out.Channels = append(out.Channels, view.Channel)
out.Count++
present[channelID] = struct{}{}
continue
}
// Linked discussion guests need a transient peer-dialog snapshot so
// TDesktop can finish materializing the comments History after
// requestSelf. ChannelLeft keeps the snapshot out of the main chat list,
// and Guest guarantees this path never turns an ordinary public preview
// into a dialog.
// while Guest authorizes the target's real top-message snapshot.
if view.Self.Status != domain.ChannelMemberActive && !view.Self.Guest {
continue
}
@ -378,6 +391,14 @@ func (s *Service) appendMissingChannelPeerPreviews(ctx context.Context, userID i
return out, nil
}
func publicChannelPreviewBootstrapDialog(view domain.ChannelView) domain.Dialog {
return domain.Dialog{
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: view.Channel.ID},
ChannelLeft: true,
Pts: view.Channel.Pts,
}
}
func isChannelPreviewAccessError(err error) bool {
return errors.Is(err, domain.ErrChannelPrivate) ||
errors.Is(err, domain.ErrChannelUserBanned) ||

View file

@ -60,6 +60,7 @@ type countingDialogChannelStore struct {
getChannelCalls int
getChannelsCalls int
getChannelDialogsCalls int
listHistoryCalls int
}
func (s *countingDialogChannelStore) GetChannel(ctx context.Context, viewerUserID, channelID int64) (domain.ChannelView, error) {
@ -77,6 +78,11 @@ func (s *countingDialogChannelStore) GetChannelDialogs(ctx context.Context, view
return s.ChannelStore.GetChannelDialogs(ctx, viewerUserID, channelIDs)
}
func (s *countingDialogChannelStore) ListChannelHistory(ctx context.Context, viewerUserID int64, filter domain.ChannelHistoryFilter) (domain.ChannelHistory, error) {
s.listHistoryCalls++
return s.ChannelStore.ListChannelHistory(ctx, viewerUserID, filter)
}
func TestGetDialogsHashUsesWarmStableHashCacheAndInvalidatesOnWrite(t *testing.T) {
ctx := context.Background()
const ownerID int64 = 1001
@ -695,7 +701,7 @@ func TestGetPeerDialogsRejectsHugeVector(t *testing.T) {
}
}
func TestGetPeerDialogsSkipsPublicChannelPreviewForNonMember(t *testing.T) {
func TestGetPeerDialogsReturnsZeroTopBootstrapForPublicChannelPreview(t *testing.T) {
ctx := context.Background()
channelStore := memory.NewChannelStore()
channels := appchannels.NewService(channelStore)
@ -716,12 +722,13 @@ func TestGetPeerDialogsSkipsPublicChannelPreviewForNonMember(t *testing.T) {
}); err != nil {
t.Fatalf("UpdateUsername public: %v", err)
}
if _, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{
sent, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{
ChannelID: public.Channel.ID,
RandomID: 99,
Message: "public peer dialog top",
Date: 1700002010,
}); err != nil {
})
if err != nil {
t.Fatalf("SendMessage public: %v", err)
}
private, err := channels.CreateChannel(ctx, 1001, domain.CreateChannelRequest{
@ -740,8 +747,19 @@ func TestGetPeerDialogsSkipsPublicChannelPreviewForNonMember(t *testing.T) {
if err != nil {
t.Fatalf("GetPeerDialogs public preview: %v", err)
}
if len(list.Dialogs) != 0 || len(list.ChannelMessages) != 0 || len(list.Channels) != 0 || list.Count != 0 {
t.Fatalf("peer dialogs = %+v, want no materialized public preview dialog", list)
if len(list.Dialogs) != 1 || len(list.ChannelMessages) != 0 || len(list.Channels) != 1 || list.Count != 1 {
t.Fatalf("peer dialogs = %+v, want one zero-top public preview bootstrap", list)
}
dialog := list.Dialogs[0]
if dialog.Peer.Type != domain.PeerTypeChannel || dialog.Peer.ID != public.Channel.ID ||
!dialog.ChannelLeft || dialog.TopMessage != 0 || dialog.TopMessageDate != 0 ||
dialog.ReadInboxMaxID != 0 || dialog.ReadOutboxMaxID != 0 ||
dialog.UnreadCount != 0 || dialog.UnreadMentions != 0 ||
dialog.UnreadReactions != 0 || dialog.Pts != sent.Event.Pts {
t.Fatalf("public preview bootstrap dialog = %+v", dialog)
}
if list.Channels[0].ID != public.Channel.ID {
t.Fatalf("public preview channels = %+v, want channel %d", list.Channels, public.Channel.ID)
}
}
@ -810,6 +828,7 @@ func TestGetPeerDialogsBatchesMissingChannelVisibilityChecks(t *testing.T) {
channelStore.getChannelCalls = 0
channelStore.getChannelsCalls = 0
channelStore.listHistoryCalls = 0
list, err := dialogs.GetPeerDialogs(ctx, 1002, []domain.Peer{
{Type: domain.PeerTypeChannel, ID: first.Channel.ID},
{Type: domain.PeerTypeChannel, ID: private.Channel.ID},
@ -822,8 +841,16 @@ func TestGetPeerDialogsBatchesMissingChannelVisibilityChecks(t *testing.T) {
if channelStore.getChannelsCalls != 1 || channelStore.getChannelCalls != 0 {
t.Fatalf("visibility channel calls: GetChannels=%d GetChannel=%d, want one batch call only", channelStore.getChannelsCalls, channelStore.getChannelCalls)
}
if len(list.Dialogs) != 0 || len(list.ChannelMessages) != 0 || len(list.Channels) != 0 || list.Count != 0 {
t.Fatalf("peer dialogs = %+v, want no public preview dialogs", list)
if channelStore.listHistoryCalls != 0 {
t.Fatalf("public preview history calls = %d, want zero", channelStore.listHistoryCalls)
}
if len(list.Dialogs) != 2 || len(list.ChannelMessages) != 0 || len(list.Channels) != 2 || list.Count != 2 {
t.Fatalf("peer dialogs = %+v, want two deduplicated zero-top public previews", list)
}
for _, dialog := range list.Dialogs {
if !dialog.ChannelLeft || dialog.TopMessage != 0 || dialog.ReadInboxMaxID != 0 || dialog.ReadOutboxMaxID != 0 {
t.Fatalf("public preview bootstrap dialog = %+v", dialog)
}
}
}