From 0991207802ff0a87f1d5cd47ba4e86be5607c43a Mon Sep 17 00:00:00 2001 From: Astra Date: Wed, 9 Sep 2026 13:53:20 +0100 Subject: [PATCH] forum: project the forum's own channel with member state in getForumTopics messages.getForumTopics returned every chat via tgChannels -> tgChannelChatMin, so the forum's own channel came back as a min object with left unset. A client with no other object for that peer (a fresh account browsing a public forum by username) then rendered the forum as already joined: topic list visible, no Join button, but no messages. Render the primary channel with tgChannelChatForView so a non-member preview carries left=true; keep the other referenced channels as min. --- internal/rpc/forum_topics_preview_rpc_test.go | 15 +++++++++++ internal/rpc/messages_forum.go | 27 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/internal/rpc/forum_topics_preview_rpc_test.go b/internal/rpc/forum_topics_preview_rpc_test.go index d40f3d47..8bceefba 100644 --- a/internal/rpc/forum_topics_preview_rpc_test.go +++ b/internal/rpc/forum_topics_preview_rpc_test.go @@ -81,6 +81,21 @@ func TestGetForumTopicsVisibleToPublicNonMember(t *testing.T) { t.Fatalf("non-member did not see the Test topic: %+v", res.Topics) } + // The forum's own channel must come back with left=true so the client still + // offers a Join button instead of treating the forum as already joined. + var forumChat *tg.Channel + for _, c := range res.Chats { + if ch, ok := c.(*tg.Channel); ok && ch.ID == channel.ID { + forumChat = ch + } + } + if forumChat == nil { + t.Fatalf("forum channel missing from getForumTopics chats: %+v", res.Chats) + } + if !forumChat.Left { + t.Fatalf("non-member forum chat = %#v, want left=true", forumChat) + } + // A private forum still refuses a non-member. priv, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{Title: "Private Forum", Megagroup: true}) if err != nil { diff --git a/internal/rpc/messages_forum.go b/internal/rpc/messages_forum.go index 4a0e939a..03702495 100644 --- a/internal/rpc/messages_forum.go +++ b/internal/rpc/messages_forum.go @@ -474,12 +474,37 @@ func (r *Router) forumTopicsResponse(ctx context.Context, userID int64, view dom Count: count, Topics: topics, Messages: messages, - Chats: tgChannels(userID, channels), + Chats: r.forumTopicsChats(userID, view, channels), Users: r.tgUsersForIDs(ctx, userID, userIDs), Pts: view.Channel.Pts, }) } +// forumTopicsChats projects the forum's own channel with the viewer's member +// state (so a non-member preview carries left=true and the client still shows a +// Join button) and every other referenced channel as a min chat. Rendering the +// primary as a bare min chat lets a client that has no other object for the +// channel treat the forum as already joined. +func (r *Router) forumTopicsChats(userID int64, view domain.ChannelView, channels []domain.Channel) []tg.ChatClass { + if view.Channel.ID == 0 { + return tgChannels(userID, channels) + } + chats := make([]tg.ChatClass, 0, len(channels)) + chats = append(chats, tgChannelChatForView(userID, view)) + seen := map[int64]struct{}{view.Channel.ID: {}} + for _, extra := range channels { + if extra.ID == 0 { + continue + } + if _, dup := seen[extra.ID]; dup { + continue + } + seen[extra.ID] = struct{}{} + chats = append(chats, tgChannelChatMin(userID, extra)) + } + return chats +} + func tgForumGeneralTopic(viewerUserID int64, view domain.ChannelView, topic domain.ChannelForumTopic) *tg.ForumTopic { return &tg.ForumTopic{ My: view.Channel.CreatorUserID == viewerUserID && viewerUserID != 0,