ZaStoGram_desktop/tasks/2026/08/19/align-replies-thread-shared-media-attribution.md
John Preston 68e8e52dcc Derive the pinned bar's topic root from the thread
Task: 2026/08/19/align-replies-thread-shared-media-attribution
2026-09-24 22:22:14 +04:00

3.3 KiB

The pinned bar's topic root comes from the active thread

HistoryView::Controls::TopControls built the Storage::SharedMediaType::Pinned key triple from its own _repliesRootId field. For a channel-comments thread — ChatWidget in Mode::Replies with no Data::ForumTopic — that names (discussionGroup, repliesRootId, 0), a key no Storage::SharedMedia list can ever hold, while the messages of that thread are indexed under the whole-peer (discussionGroup, 0, 0) list. This takes the root from activeThread() instead, the same expression HistoryView::PinnedTracker — constructed two lines above — and HistoryView::PinnedWidget already use.

The change is behaviour-neutral for every mode that can reach it today. Data::Thread::topicRootId() returns _topic->rootId() for a forum topic (equal to _repliesRootId, which ChatWidget::lookupTopic() resolves the topic from), and zero for a plain History or a Data::SavedSublist (every ChatViewId that sets .sublist leaves repliesRootId at zero). The monoforum half of the key keeps using _monoforumPeerId: that field is (_sublist && _sublist->parentChat()) ? _sublist->sublistPeer()->id : PeerId(), which deliberately differs from Data::Thread::monoforumPeerId() for a Saved Messages sublist, and shared-media storage supports only the monoforum form — ApiWrap::sharedMediaDone discards any slice whose monoforumPeerId does not resolve through PeerData::monoforumSublistFor, and the self peer is not a channel.

Two things were established while making this change and are worth recording, because both contradict how the defect was originally described.

The replies-root list is not stale, it is absent. Storage::SharedMedia creates a per-thread list only from SharedMediaAddExisting and SharedMediaAddSlice; SharedMediaAddNew and SharedMediaRemoveOne only find one. Every writer that can pass a non-zero root takes it from HistoryItem::topic() or ForumTopic::rootId(), and ApiWrap::sharedMediaDone returns before storing when peer->forumTopicFor(topicRootId) is null. A non-forum discussion group has no forum, so nothing ever creates that list and no removal can decrement it.

A channel-comments thread has no pinned bar at all. setupPinnedTracker() runs only from subscribeToPinnedMessages(), which ChatWidget calls only for Mode::History, for a topic, and for a sublist — never for Mode::Replies without a topic. _pinnedTracker therefore stays null there for the widget's whole life, and the three other functions that build this key all require it. That matches the pre-migration code, where ChatWidget::setupPinnedTracker() opened with Expects(_topic || _sublist).

The alternative repair — making HistoryItem::topicRootId() report the replies root for a reply in a discussion thread — was rejected. It would not have fixed anything, since nothing creates the list either way, and topicRootId() also feeds FullReplyTo, the serialized Data::DraftKey, and the outgoing top_msg_id and f_forum_topic fields, so it cannot move without changing persisted and protocol behaviour. The repository already carries HistoryItem::replyToTop() for the replies root and discriminates between the two in ApiWrap::exportDirectMessageLink.

Rationale, measurements and follow-ups live in the AI task.