/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/data_folder.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_histories.h" #include "data/data_changes.h" #include "dialogs/dialogs_key.h" #include "dialogs/ui/dialogs_layout.h" #include "history/history.h" #include "history/history_item.h" #include "ui/painter.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "lang/lang_keys.h" #include "storage/storage_facade.h" #include "core/application.h" #include "core/core_settings.h" #include "main/main_account.h" #include "main/main_session.h" #include "mtproto/mtproto_config.h" #include "apiwrap.h" #include "mainwidget.h" #include "styles/style_dialogs.h" namespace Data { namespace { constexpr auto kLoadedChatsMinCount = 20; constexpr auto kShowChatNamesCount = 8; [[nodiscard]] TextWithEntities ComposeFolderListEntryText( not_null folder) { const auto &list = folder->lastHistories(); if (list.empty()) { if (const auto storiesUnread = folder->storiesUnreadCount()) { return { tr::lng_contacts_stories_status_new( tr::now, lt_count, storiesUnread), }; } else if (const auto storiesCount = folder->storiesCount()) { return { tr::lng_contacts_stories_status( tr::now, lt_count, storiesCount), }; } return {}; } const auto count = std::max( int(list.size()), folder->chatsList()->fullSize().current()); const auto throwAwayLastName = (list.size() > 1) && (count == list.size() + 1); auto &&peers = ranges::views::all( list ) | ranges::views::take( list.size() - (throwAwayLastName ? 1 : 0) ); const auto wrapName = [](not_null history) { const auto name = history->peer->name(); return TextWithEntities{ .text = name, .entities = (history->chatListBadgesState().unread ? EntitiesInText{ { EntityType::Semibold, 0, int(name.size()), QString() }, { EntityType::PlainLink, 0, int(name.size()), QString() }, } : EntitiesInText{}), }; }; const auto shown = int(peers.size()); const auto accumulated = [&] { Expects(shown > 0); auto i = peers.begin(); auto result = wrapName(*i); for (++i; i != peers.end(); ++i) { result = tr::lng_archived_last_list( tr::now, lt_accumulated, result, lt_chat, wrapName(*i), Ui::Text::WithEntities); } return result; }(); return (shown < count) ? tr::lng_archived_last( tr::now, lt_count, (count - shown), lt_chats, accumulated, Ui::Text::WithEntities) : accumulated; } } // namespace Folder::Folder(not_null owner, FolderId id) : Entry(owner, Type::Folder) , _id(id) , _chatsList( &owner->session(), FilterId(), owner->maxPinnedChatsLimitValue(this)) , _name(tr::lng_archived_name(tr::now)) { indexNameParts(); session().changes().peerUpdates( PeerUpdate::Flag::Name ) | rpl::filter([=](const PeerUpdate &update) { return ranges::contains(_lastHistories, update.peer, &History::peer); }) | rpl::start_with_next([=] { ++_chatListViewVersion; updateChatListEntryPostponed(); }, _lifetime); _chatsList.setAllAreMuted(true); _chatsList.unreadStateChanges( ) | rpl::filter([=] { return inChatList(); }) | rpl::start_with_next([=](const Dialogs::UnreadState &old) { ++_chatListViewVersion; notifyUnreadStateChange(old); }, _lifetime); _chatsList.fullSize().changes( ) | rpl::start_with_next([=] { updateChatListEntryPostponed(); }, _lifetime); } FolderId Folder::id() const { return _id; } void Folder::indexNameParts() { // We don't want archive to be filtered in the chats list. } void Folder::registerOne(not_null history) { if (_chatsList.indexed()->size() == 1) { updateChatListSortPosition(); if (!_chatsList.cloudUnreadKnown()) { owner().histories().requestDialogEntry(this); } } else { updateChatListEntry(); } reorderLastHistories(); } void Folder::unregisterOne(not_null history) { if (_chatsList.empty()) { updateChatListExistence(); } reorderLastHistories(); } int Folder::chatListNameVersion() const { return 1; } void Folder::oneListMessageChanged(HistoryItem *from, HistoryItem *to) { if (from || to) { reorderLastHistories(); } } void Folder::reorderLastHistories() { // We want first kShowChatNamesCount histories, by last message date. const auto pred = [](not_null a, not_null b) { const auto aItem = a->chatListMessage(); const auto bItem = b->chatListMessage(); const auto aDate = aItem ? aItem->date() : TimeId(0); const auto bDate = bItem ? bItem->date() : TimeId(0); return aDate > bDate; }; _lastHistories.clear(); _lastHistories.reserve(kShowChatNamesCount + 1); auto &&histories = ranges::views::all( *_chatsList.indexed() ) | ranges::views::transform([](not_null row) { return row->history(); }) | ranges::views::filter([](History *history) { return (history != nullptr); }); auto nonPinnedChecked = 0; for (const auto history : histories) { const auto i = ranges::upper_bound( _lastHistories, not_null(history), pred); if (size(_lastHistories) < kShowChatNamesCount || i != end(_lastHistories)) { _lastHistories.insert(i, history); } if (size(_lastHistories) > kShowChatNamesCount) { _lastHistories.pop_back(); } if (!history->isPinnedDialog(FilterId()) && ++nonPinnedChecked >= kShowChatNamesCount) { break; } } ++_chatListViewVersion; updateChatListEntry(); } not_null Folder::chatsList() { return &_chatsList; } void Folder::clearChatsList() { _chatsList.clear(); } void Folder::chatListPreloadData() { } void Folder::paintUserpic( Painter &p, Ui::PeerUserpicView &view, const Dialogs::Ui::PaintContext &context) const { paintUserpic( p, context.st->padding.left(), context.st->padding.top(), context.st->photoSize); } void Folder::paintUserpic(Painter &p, int x, int y, int size) const { paintUserpic(p, x, y, size, nullptr, nullptr); } void Folder::paintUserpic( Painter &p, int x, int y, int size, const style::color &bg, const style::color &fg) const { paintUserpic(p, x, y, size, &bg, &fg); } void Folder::paintUserpic( Painter &p, int x, int y, int size, const style::color *overrideBg, const style::color *overrideFg) const { p.setPen(Qt::NoPen); p.setBrush(overrideBg ? *overrideBg : st::historyPeerArchiveUserpicBg); { PainterHighQualityEnabler hq(p); p.drawEllipse(x, y, size, size); } if (size == st::defaultDialogRow.photoSize) { const auto rect = QRect{ x, y, size, size }; if (overrideFg) { st::dialogsArchiveUserpic.paintInCenter( p, rect, (*overrideFg)->c); } else { st::dialogsArchiveUserpic.paintInCenter(p, rect); } } else { p.save(); const auto ratio = size / float64(st::defaultDialogRow.photoSize); p.translate(x + size / 2., y + size / 2.); p.scale(ratio, ratio); const auto skip = st::defaultDialogRow.photoSize; const auto rect = QRect{ -skip, -skip, 2 * skip, 2 * skip }; if (overrideFg) { st::dialogsArchiveUserpic.paintInCenter( p, rect, (*overrideFg)->c); } else { st::dialogsArchiveUserpic.paintInCenter(p, rect); } p.restore(); } } const std::vector> &Folder::lastHistories() const { return _lastHistories; } void Folder::validateListEntryCache() { if (_listEntryCacheVersion == _chatListViewVersion) { return; } _listEntryCacheVersion = _chatListViewVersion; _listEntryCache.setMarkedText( st::dialogsTextStyle, ComposeFolderListEntryText(this), // Use rich options as long as the entry text does not have user text. Ui::ItemTextDefaultOptions()); } void Folder::updateStoriesCount(int count, int unread) { if (_storiesCount == count && _storiesUnreadCount == unread) { return; } const auto limit = (1 << 16) - 1; const auto was = (_storiesCount > 0); _storiesCount = std::min(count, limit); _storiesUnreadCount = std::min(unread, limit); const auto now = (_storiesCount > 0); if (was == now) { updateChatListEntryPostponed(); } else if (now) { updateChatListSortPosition(); } else { updateChatListExistence(); } ++_chatListViewVersion; } int Folder::storiesCount() const { return _storiesCount; } int Folder::storiesUnreadCount() const { return _storiesUnreadCount; } void Folder::requestChatListMessage() { if (!chatListMessageKnown()) { owner().histories().requestDialogEntry(this); } } TimeId Folder::adjustedChatListTimeId() const { return chatListTimeId(); } void Folder::applyDialog(const MTPDdialogFolder &data) { _chatsList.updateCloudUnread(data); if (const auto peerId = peerFromMTP(data.vpeer())) { const auto history = owner().history(peerId); const auto fullId = FullMsgId(peerId, data.vtop_message().v); history->setFolder(this, owner().message(fullId)); } else { _chatsList.clear(); updateChatListExistence(); } if (_chatsList.indexed()->size() < kLoadedChatsMinCount) { session().api().requestDialogs(this); } } void Folder::applyPinnedUpdate(const MTPDupdateDialogPinned &data) { const auto folderId = data.vfolder_id().value_or_empty(); if (folderId != 0) { LOG(("API Error: Nested folders detected.")); } owner().setChatPinned(this, FilterId(), data.is_pinned()); } int Folder::fixedOnTopIndex() const { return kArchiveFixOnTopIndex; } bool Folder::shouldBeInChatList() const { return !_chatsList.empty() || (_storiesCount > 0); } Dialogs::UnreadState Folder::chatListUnreadState() const { return _chatsList.unreadState(); } Dialogs::BadgesState Folder::chatListBadgesState() const { auto result = Dialogs::BadgesForUnread( chatListUnreadState(), Dialogs::CountInBadge::Chats, Dialogs::IncludeInBadge::All); result.unreadMuted = result.mentionMuted = result.reactionMuted = true; if (result.unread && !result.unreadCounter) { result.unreadCounter = 1; } return result; } HistoryItem *Folder::chatListMessage() const { return nullptr; } bool Folder::chatListMessageKnown() const { return true; } const QString &Folder::chatListName() const { return _name; } const base::flat_set &Folder::chatListNameWords() const { return _nameWords; } const base::flat_set &Folder::chatListFirstLetters() const { return _nameFirstLetters; } const QString &Folder::chatListNameSortKey() const { static const auto empty = QString(); return empty; } } // namespace Data