382 lines
		
	
	
	
		
			9.3 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
	
		
			9.3 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| 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*> folder) {
 | |
| 	const auto &list = folder->lastHistories();
 | |
| 	if (list.empty()) {
 | |
| 		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*> 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<Session*> 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*> history) {
 | |
| 	if (_chatsList.indexed()->size() == 1) {
 | |
| 		updateChatListSortPosition();
 | |
| 		if (!_chatsList.cloudUnreadKnown()) {
 | |
| 			owner().histories().requestDialogEntry(this);
 | |
| 		}
 | |
| 	} else {
 | |
| 		updateChatListEntry();
 | |
| 	}
 | |
| 	reorderLastHistories();
 | |
| }
 | |
| 
 | |
| void Folder::unregisterOne(not_null<History*> 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<History*> a, not_null<History*> 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<Dialogs::Row*> 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<Dialogs::MainList*> Folder::chatsList() {
 | |
| 	return &_chatsList;
 | |
| }
 | |
| 
 | |
| void Folder::loadUserpic() {
 | |
| }
 | |
| 
 | |
| void Folder::paintUserpic(
 | |
| 		Painter &p,
 | |
| 		std::shared_ptr<CloudImageView> &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<not_null<History*>> &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::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();
 | |
| }
 | |
| 
 | |
| 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<QString> &Folder::chatListNameWords() const {
 | |
| 	return _nameWords;
 | |
| }
 | |
| 
 | |
| const base::flat_set<QChar> &Folder::chatListFirstLetters() const {
 | |
| 	return _nameFirstLetters;
 | |
| }
 | |
| 
 | |
| const QString &Folder::chatListNameSortKey() const {
 | |
| 	static const auto empty = QString();
 | |
| 	return empty;
 | |
| }
 | |
| 
 | |
| } // namespace Data
 | 
