package memory import ( "context" "encoding/binary" "hash/fnv" "sort" "sync" "telesrv/internal/domain" ) // DialogStore 是 store.DialogStore 的内存实现。 type DialogStore struct { mu sync.RWMutex m map[int64]domain.DialogList drafts map[int64]map[dialogDraftKey]domain.DialogDraft folders map[int64]map[int]domain.DialogFolder folderOrder map[int64][]int folderTags map[int64]bool // archivePinned 记录 archive folder 行置顶状态;无记录时官方默认 true。 archivePinned map[int64]bool } type dialogDraftKey struct { peerType domain.PeerType peerID int64 topMessageID int } // NewDialogStore 创建内存 DialogStore。 func NewDialogStore() *DialogStore { return &DialogStore{ m: make(map[int64]domain.DialogList), drafts: make(map[int64]map[dialogDraftKey]domain.DialogDraft), folders: make(map[int64]map[int]domain.DialogFolder), folderOrder: make(map[int64][]int), folderTags: make(map[int64]bool), archivePinned: make(map[int64]bool), } } func (s *DialogStore) ListByUser(_ context.Context, userID int64, filter domain.DialogFilter) (domain.DialogList, error) { s.mu.RLock() list := s.m[userID] s.mu.RUnlock() list.Dialogs = cloneDialogs(list.Dialogs) list.Messages = cloneMessages(list.Messages) list.Users = append([]domain.User(nil), list.Users...) return filterDialogList(list, filter), nil } func (s *DialogStore) ListByPeers(_ context.Context, userID int64, peers []domain.Peer) (domain.DialogList, error) { s.mu.RLock() list := s.m[userID] s.mu.RUnlock() list.Dialogs = cloneDialogs(list.Dialogs) list.Messages = cloneMessages(list.Messages) list.Users = append([]domain.User(nil), list.Users...) byPeer := make(map[domain.Peer]domain.Dialog, len(list.Dialogs)) for _, dialog := range list.Dialogs { byPeer[dialog.Peer] = dialog } out := domain.DialogList{ Dialogs: make([]domain.Dialog, 0, len(peers)), Users: make([]domain.User, 0, len(peers)), } seenPeers := make(map[domain.Peer]struct{}, len(peers)) seenUsers := map[int64]struct{}{} for _, peer := range peers { if _, ok := seenPeers[peer]; ok { continue } seenPeers[peer] = struct{}{} dialog := byPeer[peer] if dialog.Peer.ID == 0 { dialog.Peer = peer } out.Dialogs = append(out.Dialogs, dialog) if peer.Type == domain.PeerTypeUser { if user, ok := findDialogUser(list.Users, peer.ID); ok { appendDialogUser(&out, seenUsers, user) } else if u, ok := domain.SystemUserByID(peer.ID); ok { appendDialogUser(&out, seenUsers, u) } } } out.Messages = keepDialogMessages(list.Messages, out.Dialogs) out.Count = len(out.Dialogs) out.Hash = dialogListHash(out.Dialogs) return out, nil } // SaveList 保存一份用户会话列表,供测试和本地替身使用。 func (s *DialogStore) SaveList(_ context.Context, userID int64, list domain.DialogList) error { list.Dialogs = cloneDialogs(list.Dialogs) list.Messages = cloneMessages(list.Messages) list.Users = append([]domain.User(nil), list.Users...) s.mu.Lock() s.m[userID] = list s.mu.Unlock() return nil } func (s *DialogStore) Upsert(_ context.Context, userID int64, dialog domain.Dialog) error { s.mu.Lock() defer s.mu.Unlock() list := s.m[userID] for i, existing := range list.Dialogs { if existing.Peer == dialog.Peer { if dialog.FolderID == domain.DialogMainFolderID && existing.FolderID != domain.DialogMainFolderID { dialog.FolderID = existing.FolderID } list.Dialogs[i] = dialog s.m[userID] = list return nil } } list.Dialogs = append(list.Dialogs, dialog) s.m[userID] = list return nil } func (s *DialogStore) UpsertInbox(_ context.Context, userID int64, dialog domain.Dialog) error { s.mu.Lock() defer s.mu.Unlock() list := s.m[userID] for i, existing := range list.Dialogs { if existing.Peer != dialog.Peer { continue } if dialog.TopMessage >= existing.TopMessage { existing.TopMessage = dialog.TopMessage existing.TopMessageDate = dialog.TopMessageDate } existing.UnreadCount = countInboxUnread(list, existing.Peer, existing.ReadInboxMaxID, existing.TopMessage) list.Dialogs[i] = existing s.m[userID] = list return nil } dialog.UnreadCount = countInboxUnread(list, dialog.Peer, dialog.ReadInboxMaxID, dialog.TopMessage) if dialog.UnreadCount == 0 { dialog.UnreadCount = 1 } list.Dialogs = append(list.Dialogs, dialog) s.m[userID] = list return nil } func countInboxUnread(list domain.DialogList, peer domain.Peer, readMax, topMessage int) int { unread := 0 for _, msg := range list.Messages { if msg.Peer == peer && !msg.Out && msg.ID > readMax && (topMessage <= 0 || msg.ID <= topMessage) { unread++ } } return unread } func (s *DialogStore) SaveDraft(_ context.Context, userID int64, draft domain.DialogDraft) error { s.mu.Lock() defer s.mu.Unlock() if s.drafts[userID] == nil { s.drafts[userID] = make(map[dialogDraftKey]domain.DialogDraft) } s.drafts[userID][draftKey(draft.Peer, draft.TopMessageID)] = cloneDialogDraft(draft) return nil } func (s *DialogStore) GetDraft(_ context.Context, userID int64, peer domain.Peer, topMessageID int) (domain.DialogDraft, bool, error) { s.mu.RLock() defer s.mu.RUnlock() draft, ok := s.drafts[userID][draftKey(peer, topMessageID)] if !ok { return domain.DialogDraft{}, false, nil } return cloneDialogDraft(draft), true, nil } func (s *DialogStore) DeleteDraft(_ context.Context, userID int64, peer domain.Peer, topMessageID int) (bool, error) { s.mu.Lock() defer s.mu.Unlock() items := s.drafts[userID] if len(items) == 0 { return false, nil } key := draftKey(peer, topMessageID) if _, ok := items[key]; !ok { return false, nil } delete(items, key) return true, nil } func (s *DialogStore) ListDrafts(_ context.Context, userID int64, limit int) ([]domain.DialogDraft, error) { s.mu.RLock() items := s.drafts[userID] out := make([]domain.DialogDraft, 0, len(items)) for _, draft := range items { out = append(out, cloneDialogDraft(draft)) } s.mu.RUnlock() sortDialogDrafts(out) if limit <= 0 || limit > domain.MaxDialogDraftsPerUser { limit = domain.MaxDialogDraftsPerUser } if len(out) > limit { out = out[:limit] } return out, nil } func (s *DialogStore) ClearDrafts(_ context.Context, userID int64, limit int) ([]domain.DialogDraft, error) { if limit <= 0 || limit > domain.MaxDialogDraftsPerUser { limit = domain.MaxDialogDraftsPerUser } s.mu.Lock() defer s.mu.Unlock() items := s.drafts[userID] if len(items) == 0 { return nil, nil } out := make([]domain.DialogDraft, 0, len(items)) for _, draft := range items { out = append(out, cloneDialogDraft(draft)) } sortDialogDrafts(out) if len(out) > limit { out = out[:limit] } for _, draft := range out { delete(items, draftKey(draft.Peer, draft.TopMessageID)) } return out, nil } func (s *DialogStore) MarkRead(_ context.Context, userID int64, peer domain.Peer, maxID int) (domain.ReadHistoryResult, error) { result := domain.ReadHistoryResult{OwnerUserID: userID, Peer: peer, MaxID: maxID} s.mu.Lock() defer s.mu.Unlock() list := s.m[userID] for i, dialog := range list.Dialogs { if dialog.Peer != peer { continue } readMax := maxID if readMax <= 0 { readMax = dialog.TopMessage } if readMax > dialog.TopMessage { readMax = dialog.TopMessage } result.MaxID = readMax result.Changed = dialog.UnreadCount > 0 || readMax > dialog.ReadInboxMaxID if readMax > dialog.ReadInboxMaxID { dialog.ReadInboxMaxID = readMax } if readMax >= dialog.TopMessage { dialog.UnreadCount = 0 dialog.UnreadMentions = 0 dialog.UnreadReactions = 0 } dialog.UnreadMark = false result.StillUnreadCount = dialog.UnreadCount list.Dialogs[i] = dialog s.m[userID] = list return result, nil } return result, nil } func (s *DialogStore) SetPinned(_ context.Context, userID int64, peer domain.Peer, pinned bool) (bool, int, error) { s.mu.Lock() defer s.mu.Unlock() list := s.m[userID] targetFolderID := domain.DialogMainFolderID for _, dialog := range list.Dialogs { if dialog.Peer == peer { targetFolderID = dialog.FolderID break } } // order 仅在目标会话所在 folder 内分配;memory 双 store 互不可见, // 与 postgres 跨表统一 order 空间相比此处只看私聊表(reorder 会统一重排)。 nextOrder := 1 for _, dialog := range list.Dialogs { if dialog.Pinned && dialog.FolderID == targetFolderID && dialog.PinnedOrder >= nextOrder { nextOrder = dialog.PinnedOrder + 1 } } for i := range list.Dialogs { if list.Dialogs[i].Peer != peer { continue } list.Dialogs[i].Pinned = pinned if pinned { if list.Dialogs[i].PinnedOrder == 0 { list.Dialogs[i].PinnedOrder = nextOrder } } else { list.Dialogs[i].PinnedOrder = 0 } s.m[userID] = list return true, targetFolderID, nil } return false, 0, nil } func (s *DialogStore) ReorderPinned(_ context.Context, userID int64, folderID int, order []domain.Peer, force bool) (bool, error) { s.mu.Lock() defer s.mu.Unlock() list := s.m[userID] changed := false positions := make(map[domain.Peer]int, len(order)) for i, peer := range order { if peer.Type == "" || peer.ID == 0 { continue } if _, ok := positions[peer]; ok { continue } positions[peer] = len(order) - i } for i := range list.Dialogs { if list.Dialogs[i].FolderID != folderID { continue } pos, ok := positions[list.Dialogs[i].Peer] if ok { if !list.Dialogs[i].Pinned || list.Dialogs[i].PinnedOrder != pos { changed = true } list.Dialogs[i].Pinned = true list.Dialogs[i].PinnedOrder = pos continue } if force && list.Dialogs[i].Pinned { changed = true list.Dialogs[i].Pinned = false list.Dialogs[i].PinnedOrder = 0 } } s.m[userID] = list return changed, nil } func (s *DialogStore) SetUnreadMark(_ context.Context, userID int64, peer domain.Peer, unread bool) (bool, error) { s.mu.Lock() defer s.mu.Unlock() list := s.m[userID] for i := range list.Dialogs { if list.Dialogs[i].Peer != peer { continue } // 值守卫对齐 postgres:重复标记同值返回 changed=false,避免幽灵事件。 if list.Dialogs[i].UnreadMark == unread { return false, nil } list.Dialogs[i].UnreadMark = unread s.m[userID] = list return true, nil } return false, nil } func (s *DialogStore) ListUnreadMarked(_ context.Context, userID int64) ([]domain.Peer, error) { s.mu.RLock() list := s.m[userID] s.mu.RUnlock() out := make([]domain.Peer, 0, len(list.Dialogs)) for _, dialog := range list.Dialogs { if dialog.UnreadMark { out = append(out, dialog.Peer) } } return out, nil } func (s *DialogStore) SetChatTheme(_ context.Context, userID int64, peer domain.Peer, emoticon string) (bool, error) { if userID == 0 || peer.Type == "" || peer.ID == 0 { return false, nil } s.mu.Lock() defer s.mu.Unlock() list := s.m[userID] for i := range list.Dialogs { if list.Dialogs[i].Peer != peer { continue } if list.Dialogs[i].ThemeEmoticon == emoticon { return false, nil } list.Dialogs[i].ThemeEmoticon = emoticon s.m[userID] = list return true, nil } if emoticon == "" { return false, nil } list.Dialogs = append(list.Dialogs, domain.Dialog{Peer: peer, ThemeEmoticon: emoticon}) s.m[userID] = list return true, nil } func (s *DialogStore) SetPeerSettingsBarHidden(_ context.Context, userID int64, peer domain.Peer) (bool, error) { s.mu.Lock() defer s.mu.Unlock() list := s.m[userID] for i := range list.Dialogs { if list.Dialogs[i].Peer != peer { continue } list.Dialogs[i].PeerSettingsBarHidden = true s.m[userID] = list return true, nil } return false, nil } func (s *DialogStore) PeerSettingsBarHidden(_ context.Context, userID int64, peer domain.Peer) (bool, error) { s.mu.RLock() list := s.m[userID] s.mu.RUnlock() for _, dialog := range list.Dialogs { if dialog.Peer == peer { return dialog.PeerSettingsBarHidden, nil } } return false, nil } func (s *DialogStore) ListFolders(_ context.Context, userID int64) (domain.DialogFolderList, error) { s.mu.RLock() defer s.mu.RUnlock() byID := s.folders[userID] order := append([]int(nil), s.folderOrder[userID]...) seen := make(map[int]struct{}, len(byID)) out := domain.DialogFolderList{ TagsEnabled: s.folderTags[userID], Folders: make([]domain.DialogFolder, 0, len(byID)), } for _, id := range order { folder, ok := byID[id] if !ok { continue } seen[id] = struct{}{} out.Folders = append(out.Folders, cloneDialogFolder(folder)) } remaining := make([]int, 0, len(byID)) for id := range byID { if _, ok := seen[id]; !ok { remaining = append(remaining, id) } } sort.Ints(remaining) for _, id := range remaining { out.Folders = append(out.Folders, cloneDialogFolder(byID[id])) } return out, nil } func (s *DialogStore) GetFolder(_ context.Context, userID int64, folderID int) (domain.DialogFolder, bool, error) { s.mu.RLock() defer s.mu.RUnlock() folder, ok := s.folders[userID][folderID] if !ok { return domain.DialogFolder{}, false, nil } return cloneDialogFolder(folder), true, nil } func (s *DialogStore) UpsertFolder(_ context.Context, userID int64, folder domain.DialogFolder) error { s.mu.Lock() defer s.mu.Unlock() if s.folders[userID] == nil { s.folders[userID] = make(map[int]domain.DialogFolder) } s.folders[userID][folder.ID] = cloneDialogFolder(folder) if !containsInt(s.folderOrder[userID], folder.ID) { s.folderOrder[userID] = append(s.folderOrder[userID], folder.ID) } return nil } func (s *DialogStore) DeleteFolder(_ context.Context, userID int64, folderID int) error { s.mu.Lock() defer s.mu.Unlock() delete(s.folders[userID], folderID) s.folderOrder[userID] = removeInt(s.folderOrder[userID], folderID) return nil } func (s *DialogStore) ReorderFolders(_ context.Context, userID int64, order []int) error { s.mu.Lock() defer s.mu.Unlock() byID := s.folders[userID] seen := make(map[int]struct{}, len(order)) next := make([]int, 0, len(byID)) for _, id := range order { if id < domain.DialogCustomFolderMinID { continue } if _, ok := byID[id]; !ok { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} next = append(next, id) } remaining := make([]int, 0, len(byID)) for id := range byID { if _, ok := seen[id]; !ok { remaining = append(remaining, id) } } sort.Ints(remaining) next = append(next, remaining...) s.folderOrder[userID] = next return nil } func (s *DialogStore) SetFolderTagsEnabled(_ context.Context, userID int64, enabled bool) error { s.mu.Lock() s.folderTags[userID] = enabled s.mu.Unlock() return nil } func (s *DialogStore) EditPeerFolders(_ context.Context, userID int64, peers []domain.FolderPeerUpdate) error { s.mu.Lock() defer s.mu.Unlock() list := s.m[userID] updates := make(map[domain.Peer]int, len(peers)) for _, item := range peers { if item.Peer.Type == "" || item.Peer.ID == 0 { continue } if item.FolderID != domain.DialogMainFolderID && item.FolderID != domain.DialogArchiveFolderID { continue } updates[item.Peer] = item.FolderID } for i := range list.Dialogs { folderID, ok := updates[list.Dialogs[i].Peer] if !ok { continue } // 换 folder 时清 pinned:TDesktop 在归档/还原时本地无条件 unpin, // 服务端保留旧 pin 会在下次 getDialogs 时把状态漂移回来。 if list.Dialogs[i].FolderID != folderID { list.Dialogs[i].Pinned = false list.Dialogs[i].PinnedOrder = 0 } list.Dialogs[i].FolderID = folderID } s.m[userID] = list return nil } func (s *DialogStore) SetArchivePinned(_ context.Context, userID int64, pinned bool) (bool, error) { s.mu.Lock() defer s.mu.Unlock() current, ok := s.archivePinned[userID] if !ok { current = true } s.archivePinned[userID] = pinned return current != pinned, nil } func (s *DialogStore) ArchivePinned(_ context.Context, userID int64) (bool, error) { s.mu.RLock() defer s.mu.RUnlock() if pinned, ok := s.archivePinned[userID]; ok { return pinned, nil } return true, nil } func (s *DialogStore) CountArchiveUnread(_ context.Context, userID int64) (int, int, error) { s.mu.RLock() list := s.m[userID] s.mu.RUnlock() peers, messages := 0, 0 for _, dialog := range list.Dialogs { if dialog.FolderID != domain.DialogArchiveFolderID { continue } if dialog.UnreadCount > 0 || dialog.UnreadMark { peers++ } messages += dialog.UnreadCount } return peers, messages, nil } func upsertMemoryDialog(list domain.DialogList, dialog domain.Dialog) domain.DialogList { for i := range list.Dialogs { if list.Dialogs[i].Peer == dialog.Peer { if dialog.ReadInboxMaxID == 0 { dialog.ReadInboxMaxID = list.Dialogs[i].ReadInboxMaxID } if dialog.ReadOutboxMaxID == 0 { dialog.ReadOutboxMaxID = list.Dialogs[i].ReadOutboxMaxID } if dialog.FolderID == domain.DialogMainFolderID && list.Dialogs[i].FolderID != domain.DialogMainFolderID { dialog.FolderID = list.Dialogs[i].FolderID } if dialog.UnreadCount == 0 { dialog.UnreadCount = list.Dialogs[i].UnreadCount } if dialog.UnreadMentions == 0 { dialog.UnreadMentions = list.Dialogs[i].UnreadMentions } if dialog.UnreadReactions == 0 { dialog.UnreadReactions = list.Dialogs[i].UnreadReactions } if !dialog.UnreadMark { dialog.UnreadMark = list.Dialogs[i].UnreadMark } // 与 postgres 的 ON CONFLICT 局部更新对齐:消息路径的 upsert // 不得抹掉既有置顶/设置类状态。 if !dialog.Pinned && list.Dialogs[i].Pinned { dialog.Pinned = list.Dialogs[i].Pinned dialog.PinnedOrder = list.Dialogs[i].PinnedOrder } if dialog.TTLPeriod == 0 { dialog.TTLPeriod = list.Dialogs[i].TTLPeriod } if dialog.ThemeEmoticon == "" { dialog.ThemeEmoticon = list.Dialogs[i].ThemeEmoticon } if !dialog.HasScheduled { dialog.HasScheduled = list.Dialogs[i].HasScheduled } if !dialog.PeerSettingsBarHidden { dialog.PeerSettingsBarHidden = list.Dialogs[i].PeerSettingsBarHidden } if !dialog.ViewForumAsMessages { dialog.ViewForumAsMessages = list.Dialogs[i].ViewForumAsMessages } if dialog.Draft == nil { dialog.Draft = list.Dialogs[i].Draft } list.Dialogs[i] = dialog return list } } list.Dialogs = append(list.Dialogs, dialog) return list } func filterDialogList(list domain.DialogList, filter domain.DialogFilter) domain.DialogList { sort.SliceStable(list.Dialogs, func(i, j int) bool { return dialogLess(list.Dialogs[i], list.Dialogs[j]) }) base := make([]domain.Dialog, 0, len(list.Dialogs)) for _, d := range list.Dialogs { if !dialogMatchesFolder(d, list.Users, filter) { continue } if filter.PinnedOnly && !d.Pinned { continue } if filter.ExcludePinned && d.Pinned { continue } base = append(base, d) } list.Count = len(base) list.Hash = dialogListHash(base) limit := filter.Limit if limit <= 0 { limit = 100 } if limit > 500 { limit = 500 } page := make([]domain.Dialog, 0, len(base)) for _, d := range base { if !afterDialogOffset(d, filter) { continue } page = append(page, d) if len(page) >= limit { break } } list.Dialogs = page list.Messages = keepDialogMessages(list.Messages, page) return list } func dialogMatchesFolder(d domain.Dialog, users []domain.User, filter domain.DialogFilter) bool { if !filter.HasFolderID { // 不带 folder_id 视为主列表(folder 0):归档对话只以 dialogFolder // 聚合条目出现,DrKLO Android 主列表请求不设 flag。 return d.FolderID == domain.DialogMainFolderID } if filter.FolderID < domain.DialogCustomFolderMinID { return d.FolderID == filter.FolderID } if filter.Folder == nil { return false } folder := filter.Folder if folder.ExcludeArchived && d.FolderID == domain.DialogArchiveFolderID { return false } if folder.ExcludeRead && d.UnreadCount == 0 && !d.UnreadMark { return false } if hasFolderPeer(folder.ExcludePeers, d.Peer) { return false } if hasFolderPeer(folder.IncludePeers, d.Peer) || hasFolderPeer(folder.PinnedPeers, d.Peer) { return true } if d.Peer.Type == domain.PeerTypeUser { user, ok := findDialogUser(users, d.Peer.ID) if ok && user.Contact && folder.Contacts { return true } if (!ok || !user.Contact) && folder.NonContacts { return true } } return false } func hasFolderPeer(peers []domain.DialogFolderPeer, peer domain.Peer) bool { for _, item := range peers { if item.Peer == peer { return true } } return false } func dialogLess(a, b domain.Dialog) bool { if a.Pinned != b.Pinned { return a.Pinned && !b.Pinned } if a.Pinned && b.Pinned && a.PinnedOrder != b.PinnedOrder { if a.PinnedOrder == 0 { return false } if b.PinnedOrder == 0 { return true } return a.PinnedOrder > b.PinnedOrder } if a.TopMessageDate != b.TopMessageDate { return a.TopMessageDate > b.TopMessageDate } if a.TopMessage != b.TopMessage { return a.TopMessage > b.TopMessage } return a.Peer.ID > b.Peer.ID } func afterDialogOffset(d domain.Dialog, filter domain.DialogFilter) bool { if filter.OffsetDate <= 0 && filter.OffsetID <= 0 { return true } if filter.OffsetDate > 0 { if d.TopMessageDate != filter.OffsetDate { return d.TopMessageDate < filter.OffsetDate } if filter.OffsetID <= 0 { return false } if d.TopMessage != filter.OffsetID { return d.TopMessage < filter.OffsetID } if filter.HasOffsetPeer { return d.Peer.ID < filter.OffsetPeer.ID } return false } return d.TopMessage < filter.OffsetID } func keepDialogMessages(messages []domain.Message, dialogs []domain.Dialog) []domain.Message { want := make(map[int]struct{}, len(dialogs)) for _, d := range dialogs { if d.TopMessage != 0 { want[d.TopMessage] = struct{}{} } } out := make([]domain.Message, 0, len(want)) for _, msg := range messages { if _, ok := want[msg.ID]; ok { out = append(out, msg) } } return out } func findDialogUser(users []domain.User, id int64) (domain.User, bool) { for _, user := range users { if user.ID == id { return user, true } } return domain.User{}, false } func appendDialogUser(list *domain.DialogList, seen map[int64]struct{}, user domain.User) { if user.ID == 0 { return } if _, ok := seen[user.ID]; ok { return } seen[user.ID] = struct{}{} list.Users = append(list.Users, user) } func cloneDialogs(dialogs []domain.Dialog) []domain.Dialog { out := append([]domain.Dialog(nil), dialogs...) for i := range out { if out[i].Draft != nil { draft := cloneDialogDraft(*out[i].Draft) out[i].Draft = &draft } } return out } func cloneDialogDraft(draft domain.DialogDraft) domain.DialogDraft { draft.Entities = append([]domain.MessageEntity(nil), draft.Entities...) draft.ReplyTo = cloneMessageReply(draft.ReplyTo) if draft.WebPage != nil { webpage := *draft.WebPage draft.WebPage = &webpage } return draft } func sortDialogDrafts(drafts []domain.DialogDraft) { sort.SliceStable(drafts, func(i, j int) bool { if drafts[i].Date != drafts[j].Date { return drafts[i].Date > drafts[j].Date } if drafts[i].Peer.Type != drafts[j].Peer.Type { return drafts[i].Peer.Type < drafts[j].Peer.Type } if drafts[i].Peer.ID != drafts[j].Peer.ID { return drafts[i].Peer.ID > drafts[j].Peer.ID } return drafts[i].TopMessageID > drafts[j].TopMessageID }) } func cloneDialogFolder(folder domain.DialogFolder) domain.DialogFolder { folder.TitleEntities = append([]domain.MessageEntity(nil), folder.TitleEntities...) folder.PinnedPeers = append([]domain.DialogFolderPeer(nil), folder.PinnedPeers...) folder.IncludePeers = append([]domain.DialogFolderPeer(nil), folder.IncludePeers...) folder.ExcludePeers = append([]domain.DialogFolderPeer(nil), folder.ExcludePeers...) return folder } func containsInt(items []int, value int) bool { for _, item := range items { if item == value { return true } } return false } func removeInt(items []int, value int) []int { out := items[:0] for _, item := range items { if item != value { out = append(out, item) } } return out } func dialogListHash(dialogs []domain.Dialog) int64 { if len(dialogs) == 0 { return 0 } h := fnv.New64a() var buf [47]byte for _, d := range dialogs { binary.LittleEndian.PutUint64(buf[:8], uint64(d.Peer.ID)) binary.LittleEndian.PutUint32(buf[8:12], uint32(d.FolderID)) binary.LittleEndian.PutUint32(buf[12:16], uint32(d.TopMessage)) binary.LittleEndian.PutUint32(buf[16:20], uint32(d.TopMessageDate)) binary.LittleEndian.PutUint32(buf[20:24], uint32(d.ReadInboxMaxID)) binary.LittleEndian.PutUint32(buf[24:28], uint32(d.ReadOutboxMaxID)) binary.LittleEndian.PutUint32(buf[28:32], uint32(d.UnreadCount)) binary.LittleEndian.PutUint32(buf[32:36], uint32(d.UnreadMentions)) binary.LittleEndian.PutUint32(buf[36:40], uint32(d.UnreadReactions)) if d.Pinned { buf[40] = 1 } else { buf[40] = 0 } binary.LittleEndian.PutUint32(buf[41:45], uint32(d.PinnedOrder)) if d.UnreadMark { buf[45] = 1 } else { buf[45] = 0 } if d.PeerSettingsBarHidden { buf[46] = 1 } else { buf[46] = 0 } _, _ = h.Write(buf[:]) _, _ = h.Write([]byte(d.ThemeEmoticon)) } return int64(h.Sum64()) }