fix(messages): sync exact channel pinned counts

This commit is contained in:
iamxvbaba 2026-07-27 00:17:21 +08:00
parent b742450938
commit e75fd04a28
9 changed files with 232 additions and 62 deletions

View file

@ -65,8 +65,8 @@ func TestChannelMultiPinAndroidOpenAndJump(t *testing.T) {
}
ids = append(ids, sent.Message.ID)
}
// 三条置顶:早期、中间、最新(Android 置顶栏循环跳转需要全部三条都可跳)。
pins := []int{ids[4], ids[14], ids[27]}
// 五条置顶:覆盖用户反馈中的真实规模;Android 置顶栏循环跳转需要全部可跳。
pins := []int{ids[4], ids[9], ids[14], ids[20], ids[27]}
for _, id := range pins {
if _, err := channelSvc.UpdatePinnedMessage(ctx, owner.ID, domain.UpdateChannelPinnedMessageRequest{
ChannelID: channelID,
@ -113,7 +113,10 @@ func TestChannelMultiPinAndroidOpenAndJump(t *testing.T) {
if channelMessages.Count != len(pins) {
t.Fatalf("pinned search count = %d, want %d", channelMessages.Count, len(pins))
}
wantDesc := []int{pins[2], pins[1], pins[0]}
wantDesc := make([]int, len(pins))
for i := range pins {
wantDesc[i] = pins[len(pins)-1-i]
}
for i, raw := range channelMessages.Messages {
msg, ok := raw.(*tg.Message)
if !ok {
@ -128,7 +131,49 @@ func TestChannelMultiPinAndroidOpenAndJump(t *testing.T) {
}
}
// ② 点置顶栏跳最旧 pin:scrollToMessageId → getHistory AROUND(手机 count=20)。
// ② tweb 冷加载:先用 limit=1 取最新 pin,并把 messages.channelMessages.count
// 当作完整置顶数;旧实现把 count 错算成 len(page)+hasMore,即五条只报两条。
twebSearchEnc := dispatch(&tg.MessagesSearchRequest{
Peer: peer,
Q: "",
Filter: &tg.InputMessagesFilterPinned{},
Limit: 1,
})
twebSearch, ok := twebSearchEnc.(*tg.MessagesChannelMessages)
if !ok {
t.Fatalf("tweb pinned search response = %T, want messages.channelMessages", twebSearchEnc)
}
if len(twebSearch.Messages) != 1 || twebSearch.Count != len(pins) {
t.Fatalf("tweb pinned search messages/count = %d/%d, want 1/%d", len(twebSearch.Messages), twebSearch.Count, len(pins))
}
// limit=0 是官方 count-only 入口:不得为了计数反序列化/返回消息页。
countOnlyEnc := dispatch(&tg.MessagesSearchRequest{
Peer: peer,
Q: "",
Filter: &tg.InputMessagesFilterPinned{},
Limit: 0,
})
countOnly, ok := countOnlyEnc.(*tg.MessagesChannelMessages)
if !ok {
t.Fatalf("count-only pinned search response = %T, want messages.channelMessages", countOnlyEnc)
}
if len(countOnly.Messages) != 0 || countOnly.Count != len(pins) {
t.Fatalf("count-only pinned search messages/count = %d/%d, want 0/%d", len(countOnly.Messages), countOnly.Count, len(pins))
}
counters, err := r.onMessagesGetSearchCounters(WithUserID(androidClientContext(), member.ID), &tg.MessagesGetSearchCountersRequest{
Peer: peer,
Filters: []tg.MessagesFilterClass{&tg.InputMessagesFilterPinned{}},
})
if err != nil {
t.Fatalf("messages.getSearchCounters(filterPinned): %v", err)
}
if len(counters) != 1 || counters[0].Count != len(pins) {
t.Fatalf("pinned search counters = %+v, want count %d", counters, len(pins))
}
// ③ 点置顶栏跳最旧 pin:scrollToMessageId → getHistory AROUND(手机 count=20)。
const aroundCount = 20
histEnc := dispatch(&tg.MessagesGetHistoryRequest{
Peer: peer,
@ -160,7 +205,7 @@ func TestChannelMultiPinAndroidOpenAndJump(t *testing.T) {
t.Fatalf("around history lacks anchor %d: jump shows MessageNotFound on Android", pins[0])
}
// ③ 本地缺对象补拉:MessagesStorage.loadChatInfo → channels.getMessages。
// ④ 本地缺对象补拉:MessagesStorage.loadChatInfo → channels.getMessages。
// DrKLO 发的是 pre-InputMessage 构造器 #93d7b347(id:Vector<int>),
// 该请求 500 会让客户端把这批 pin 按「已取消置顶」从本地缓存删除。
var legacy bin.Buffer
@ -208,7 +253,7 @@ func TestChannelMultiPinAndroidOpenAndJump(t *testing.T) {
}
}
// ④ chatFull 降级缓存:pinned_msg_id 必须是最新置顶(Android 以它判断是否重拉列表)。
// ⑤ chatFull 降级缓存:pinned_msg_id 必须是最新置顶(Android 以它判断是否重拉列表)。
fullEnc := dispatch(&tg.ChannelsGetFullChannelRequest{
Channel: &tg.InputChannel{ChannelID: channelID, AccessHash: memberView.Channel.AccessHash},
})
@ -220,7 +265,7 @@ func TestChannelMultiPinAndroidOpenAndJump(t *testing.T) {
if !ok {
t.Fatalf("full chat = %T, want channelFull", full.FullChat)
}
if pinnedID, _ := channelFull.GetPinnedMsgID(); pinnedID != pins[2] {
t.Fatalf("channelFull pinned_msg_id = %d, want latest pin %d", pinnedID, pins[2])
if pinnedID, _ := channelFull.GetPinnedMsgID(); pinnedID != pins[len(pins)-1] {
t.Fatalf("channelFull pinned_msg_id = %d, want latest pin %d", pinnedID, pins[len(pins)-1])
}
}

View file

@ -197,35 +197,50 @@ func (r *Router) onMessagesGetSearchCounters(ctx context.Context, req *tg.Messag
if err != nil {
return nil, err
}
needsPinned := false
needsMedia := false
for _, filter := range req.Filters {
if filter == nil {
continue
}
if _, ok := filter.(*tg.InputMessagesFilterPinned); ok {
needsPinned = true
continue
}
if len(mediaCategoriesForFilter(filter)) > 0 {
needsMedia = true
}
}
pinnedCount := 0
mediaCounts := domain.MediaCategoryCounts{}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
// 只读 PinnedMessageID(Channel 字段):走轻量 ResolveChannel,省 dialog/读态/boost 查询。
view, err := r.deps.Channels.ResolveChannel(ctx, userID, peer.ID)
if err != nil {
return nil, err
if needsPinned {
switch {
case peer.Type == domain.PeerTypeChannel && r.deps.Channels != nil:
history, err := r.deps.Channels.GetHistory(ctx, userID, domain.ChannelHistoryFilter{
ChannelID: peer.ID,
PinnedOnly: true,
NeedTotalCount: true,
CountOnly: true,
})
if err != nil {
return nil, err
}
pinnedCount = history.Count
case peer.Type == domain.PeerTypeUser && r.deps.Messages != nil:
list, err := r.deps.Messages.Search(ctx, userID, domain.MessageFilter{
HasPeer: true,
Peer: peer,
PinnedOnly: true,
Limit: 1,
NeedTotalCount: true,
})
if err != nil {
return nil, err
}
pinnedCount = list.Count
}
if view.Channel.PinnedMessageID > 0 {
pinnedCount = 1
}
counts, err := r.mediaCountsForPeer(ctx, userID, peer)
if err != nil {
return nil, err
}
mediaCounts = counts
}
if peer.Type == domain.PeerTypeUser && r.deps.Messages != nil {
list, err := r.deps.Messages.Search(ctx, userID, domain.MessageFilter{
HasPeer: true,
Peer: peer,
PinnedOnly: true,
Limit: 1,
NeedTotalCount: true,
})
if err != nil {
return nil, err
}
pinnedCount = list.Count
if needsMedia {
counts, err := r.mediaCountsForPeer(ctx, userID, peer)
if err != nil {
return nil, err
@ -828,6 +843,7 @@ func (r *Router) messageFilterFromSearchRequest(ctx context.Context, userID int6
func (r *Router) channelHistoryFilterFromSearchRequest(userID int64, req *tg.MessagesSearchRequest, channelID int64) (domain.ChannelHistoryFilter, bool) {
limit := req.Limit
countOnly := limit == 0
if limit <= 0 || limit > 100 {
limit = 100
}
@ -836,14 +852,17 @@ func (r *Router) channelHistoryFilterFromSearchRequest(userID int64, req *tg.Mes
Query: req.Q,
PinnedOnly: messagesSearchFilterPinned(req.Filter),
MusicOnly: messagesSearchFilterMusic(req.Filter),
OffsetID: req.OffsetID,
AddOffset: domain.ClampMessageHistoryAddOffset(req.AddOffset),
Limit: limit,
MinDate: req.MinDate,
MaxDate: req.MaxDate,
MaxID: req.MaxID,
MinID: req.MinID,
Hash: req.Hash,
NeedTotalCount: countOnly ||
(req.OffsetID == 0 && req.MinDate == 0 && req.MaxDate == 0 && req.AddOffset >= 0 && req.Hash == 0),
CountOnly: countOnly,
OffsetID: req.OffsetID,
AddOffset: domain.ClampMessageHistoryAddOffset(req.AddOffset),
Limit: limit,
MinDate: req.MinDate,
MaxDate: req.MaxDate,
MaxID: req.MaxID,
MinID: req.MinID,
Hash: req.Hash,
}
if req.FromID != nil {
from, ok := r.domainPeerFromInputPeer(userID, req.FromID)