1011 lines
		
	
	
	
		
			26 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1011 lines
		
	
	
	
		
			26 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 "ui/controls/who_reacted_context_action.h"
 | |
| 
 | |
| #include "base/call_delayed.h"
 | |
| #include "ui/widgets/menu/menu_action.h"
 | |
| #include "ui/widgets/popup_menu.h"
 | |
| #include "ui/effects/ripple_animation.h"
 | |
| #include "ui/chat/group_call_userpics.h"
 | |
| #include "ui/text/text_custom_emoji.h"
 | |
| #include "ui/painter.h"
 | |
| #include "lang/lang_keys.h"
 | |
| #include "styles/style_chat.h"
 | |
| #include "styles/style_chat_helpers.h"
 | |
| #include "styles/style_menu_icons.h"
 | |
| 
 | |
| namespace Lang {
 | |
| namespace {
 | |
| 
 | |
| struct StringWithReacted {
 | |
| 	QString text;
 | |
| 	int seen = 0;
 | |
| };
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| template <typename ResultString>
 | |
| struct StartReplacements;
 | |
| 
 | |
| template <>
 | |
| struct StartReplacements<StringWithReacted> {
 | |
| 	static inline StringWithReacted Call(QString &&langString) {
 | |
| 		return { std::move(langString) };
 | |
| 	}
 | |
| };
 | |
| 
 | |
| template <typename ResultString>
 | |
| struct ReplaceTag;
 | |
| 
 | |
| template <>
 | |
| struct ReplaceTag<StringWithReacted> {
 | |
| 	static StringWithReacted Call(
 | |
| 		StringWithReacted &&original,
 | |
| 		ushort tag,
 | |
| 		const StringWithReacted &replacement);
 | |
| };
 | |
| 
 | |
| StringWithReacted ReplaceTag<StringWithReacted>::Call(
 | |
| 		StringWithReacted &&original,
 | |
| 		ushort tag,
 | |
| 		const StringWithReacted &replacement) {
 | |
| 	const auto offset = FindTagReplacementPosition(original.text, tag);
 | |
| 	if (offset < 0) {
 | |
| 		return std::move(original);
 | |
| 	}
 | |
| 	original.text = ReplaceTag<QString>::Call(
 | |
| 		std::move(original.text),
 | |
| 		tag,
 | |
| 		replacement.text + '/' + QString::number(original.seen));
 | |
| 	return std::move(original);
 | |
| }
 | |
| 
 | |
| } // namespace Lang
 | |
| 
 | |
| namespace Ui {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kPreloaderAlpha = 0.2;
 | |
| 
 | |
| using Text::CustomEmojiFactory;
 | |
| 
 | |
| class Action final : public Menu::ItemBase {
 | |
| public:
 | |
| 	Action(
 | |
| 		not_null<PopupMenu*> parentMenu,
 | |
| 		rpl::producer<WhoReadContent> content,
 | |
| 		CustomEmojiFactory factory,
 | |
| 		Fn<void(WhoReadParticipant)> participantChosen,
 | |
| 		Fn<void()> showAllChosen);
 | |
| 
 | |
| 	bool isEnabled() const override;
 | |
| 	not_null<QAction*> action() const override;
 | |
| 
 | |
| 	void handleKeyPress(not_null<QKeyEvent*> e) override;
 | |
| 
 | |
| protected:
 | |
| 	QPoint prepareRippleStartPosition() const override;
 | |
| 	QImage prepareRippleMask() const override;
 | |
| 
 | |
| 	int contentHeight() const override;
 | |
| 
 | |
| private:
 | |
| 	void paint(Painter &p);
 | |
| 
 | |
| 	void updateUserpicsFromContent();
 | |
| 	void resolveMinWidth();
 | |
| 	void refreshText();
 | |
| 	void refreshDimensions();
 | |
| 	void populateSubmenu();
 | |
| 
 | |
| 	const not_null<PopupMenu*> _parentMenu;
 | |
| 	const not_null<QAction*> _dummyAction;
 | |
| 	const Fn<void(WhoReadParticipant)> _participantChosen;
 | |
| 	const Fn<void()> _showAllChosen;
 | |
| 	const std::unique_ptr<GroupCallUserpics> _userpics;
 | |
| 	const style::Menu &_st;
 | |
| 	const CustomEmojiFactory _customEmojiFactory;
 | |
| 
 | |
| 	WhoReactedListMenu _submenu;
 | |
| 
 | |
| 	Text::String _text;
 | |
| 	std::unique_ptr<Ui::Text::CustomEmoji> _custom;
 | |
| 	int _textWidth = 0;
 | |
| 	const int _height = 0;
 | |
| 	int _userpicsWidth = 0;
 | |
| 	bool _appeared = false;
 | |
| 
 | |
| 	WhoReadContent _content;
 | |
| 
 | |
| };
 | |
| 
 | |
| class WhenAction final : public Menu::ItemBase {
 | |
| public:
 | |
| 	WhenAction(
 | |
| 		not_null<PopupMenu*> parentMenu,
 | |
| 		rpl::producer<WhoReadContent> content,
 | |
| 		Fn<void()> showOrPremium);
 | |
| 
 | |
| 	bool isEnabled() const override;
 | |
| 	not_null<QAction*> action() const override;
 | |
| 
 | |
| protected:
 | |
| 	QPoint prepareRippleStartPosition() const override;
 | |
| 	QImage prepareRippleMask() const override;
 | |
| 
 | |
| 	int contentHeight() const override;
 | |
| 
 | |
| private:
 | |
| 	void paint(Painter &p);
 | |
| 	void resizeEvent(QResizeEvent *e) override;
 | |
| 
 | |
| 	void resolveMinWidth();
 | |
| 	void refreshText();
 | |
| 	void refreshDimensions();
 | |
| 
 | |
| 	const not_null<PopupMenu*> _parentMenu;
 | |
| 	const not_null<QAction*> _dummyAction;
 | |
| 	const Fn<void()> _showOrPremium;
 | |
| 	const style::Menu &_st;
 | |
| 
 | |
| 	Text::String _text;
 | |
| 	Text::String _show;
 | |
| 	QRect _showRect;
 | |
| 	int _textWidth = 0;
 | |
| 	const int _height = 0;
 | |
| 
 | |
| 	WhoReadContent _content;
 | |
| 
 | |
| };
 | |
| 
 | |
| TextParseOptions MenuTextOptions = {
 | |
| 	TextParseLinks, // flags
 | |
| 	0, // maxw
 | |
| 	0, // maxh
 | |
| 	Qt::LayoutDirectionAuto, // dir
 | |
| };
 | |
| 
 | |
| [[nodiscard]] QString FormatReactedString(int reacted, int seen) {
 | |
| 	const auto projection = [&](const QString &text) {
 | |
| 		return Lang::StringWithReacted{ text, seen };
 | |
| 	};
 | |
| 	return tr::lng_context_seen_reacted(
 | |
| 		tr::now,
 | |
| 		lt_count_short,
 | |
| 		reacted,
 | |
| 		projection
 | |
| 	).text;
 | |
| }
 | |
| 
 | |
| Action::Action(
 | |
| 	not_null<PopupMenu*> parentMenu,
 | |
| 	rpl::producer<WhoReadContent> content,
 | |
| 	Text::CustomEmojiFactory factory,
 | |
| 	Fn<void(WhoReadParticipant)> participantChosen,
 | |
| 	Fn<void()> showAllChosen)
 | |
| : ItemBase(parentMenu->menu(), parentMenu->menu()->st())
 | |
| , _parentMenu(parentMenu)
 | |
| , _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))
 | |
| , _participantChosen(std::move(participantChosen))
 | |
| , _showAllChosen(std::move(showAllChosen))
 | |
| , _userpics(std::make_unique<GroupCallUserpics>(
 | |
| 	st::defaultWhoRead.userpics,
 | |
| 	rpl::never<bool>(),
 | |
| 	[=] { update(); }))
 | |
| , _st(parentMenu->menu()->st())
 | |
| , _customEmojiFactory(std::move(factory))
 | |
| , _submenu(_customEmojiFactory, _participantChosen, _showAllChosen)
 | |
| , _height(st::defaultWhoRead.itemPadding.top()
 | |
| 		+ _st.itemStyle.font->height
 | |
| 		+ st::defaultWhoRead.itemPadding.bottom()) {
 | |
| 	const auto parent = parentMenu->menu();
 | |
| 	const auto delay = anim::Disabled() ? 0 : parentMenu->st().duration;
 | |
| 	const auto checkAppeared = [=, now = crl::now()](bool force = false) {
 | |
| 		_appeared = force || ((crl::now() - now) >= delay);
 | |
| 	};
 | |
| 
 | |
| 	setAcceptBoth(true);
 | |
| 	initResizeHook(parent->sizeValue());
 | |
| 
 | |
| 	std::move(
 | |
| 		content
 | |
| 	) | rpl::start_with_next([=](WhoReadContent &&content) {
 | |
| 		checkAppeared();
 | |
| 		const auto changed = (_content.participants != content.participants)
 | |
| 			|| (_content.state != content.state);
 | |
| 		_content = content;
 | |
| 		if (changed) {
 | |
| 			PostponeCall(this, [=] { populateSubmenu(); });
 | |
| 		}
 | |
| 		updateUserpicsFromContent();
 | |
| 		refreshText();
 | |
| 		refreshDimensions();
 | |
| 		setPointerCursor(isEnabled());
 | |
| 		_dummyAction->setEnabled(isEnabled());
 | |
| 		if (!isEnabled()) {
 | |
| 			setSelected(false);
 | |
| 		}
 | |
| 		update();
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	resolveMinWidth();
 | |
| 
 | |
| 	_userpics->widthValue(
 | |
| 	) | rpl::start_with_next([=](int width) {
 | |
| 		_userpicsWidth = width;
 | |
| 		refreshDimensions();
 | |
| 		update();
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	paintRequest(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		Painter p(this);
 | |
| 		paint(p);
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	clicks(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		if (_content.participants.size() == 1) {
 | |
| 			if (const auto onstack = _participantChosen) {
 | |
| 				onstack(_content.participants.front());
 | |
| 			}
 | |
| 		} else if (_content.fullReactionsCount > 0) {
 | |
| 			if (const auto onstack = _showAllChosen) {
 | |
| 				onstack();
 | |
| 			}
 | |
| 		}
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	enableMouseSelecting();
 | |
| 
 | |
| 	base::call_delayed(parentMenu->st().duration, this, [=] {
 | |
| 		if (!_appeared) {
 | |
| 			checkAppeared(true);
 | |
| 			updateUserpicsFromContent();
 | |
| 		}
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void Action::resolveMinWidth() {
 | |
| 	const auto maxIconWidth = 0;
 | |
| 	const auto width = [&](const QString &text) {
 | |
| 		return _st.itemStyle.font->width(text);
 | |
| 	};
 | |
| 	const auto maxText = (_content.type == WhoReadType::Listened)
 | |
| 		? tr::lng_context_seen_listened(tr::now, lt_count, 999)
 | |
| 		: (_content.type == WhoReadType::Watched)
 | |
| 		? tr::lng_context_seen_watched(tr::now, lt_count, 999)
 | |
| 		: (_content.type == WhoReadType::Seen)
 | |
| 		? tr::lng_context_seen_text(tr::now, lt_count, 999)
 | |
| 		: QString();
 | |
| 	const auto maxReacted = (_content.fullReactionsCount > 0)
 | |
| 		? (!maxText.isEmpty()
 | |
| 			? FormatReactedString(_content.fullReactionsCount, 999)
 | |
| 			: tr::lng_context_seen_reacted(
 | |
| 				tr::now,
 | |
| 				lt_count_short,
 | |
| 				_content.fullReactionsCount))
 | |
| 		: QString();
 | |
| 	const auto maxTextWidth = std::max(width(maxText), width(maxReacted));
 | |
| 	const auto maxWidth = st::defaultWhoRead.itemPadding.left()
 | |
| 		+ maxIconWidth
 | |
| 		+ maxTextWidth
 | |
| 		+ _userpics->maxWidth()
 | |
| 		+ st::defaultWhoRead.itemPadding.right();
 | |
| 	setMinWidth(maxWidth);
 | |
| }
 | |
| 
 | |
| void Action::updateUserpicsFromContent() {
 | |
| 	if (!_appeared) {
 | |
| 		return;
 | |
| 	}
 | |
| 	auto users = std::vector<GroupCallUser>();
 | |
| 	if (!_content.participants.empty()) {
 | |
| 		const auto count = std::min(
 | |
| 			int(_content.participants.size()),
 | |
| 			WhoReadParticipant::kMaxSmallUserpics);
 | |
| 		const auto factor = style::DevicePixelRatio();
 | |
| 		users.reserve(count);
 | |
| 		for (auto i = 0; i != count; ++i) {
 | |
| 			auto &participant = _content.participants[i];
 | |
| 			participant.userpicSmall.setDevicePixelRatio(factor);
 | |
| 			users.push_back({
 | |
| 				.userpic = participant.userpicSmall,
 | |
| 				.userpicKey = participant.userpicKey,
 | |
| 				.id = participant.id,
 | |
| 			});
 | |
| 		}
 | |
| 	}
 | |
| 	_userpics->update(users, true);
 | |
| }
 | |
| 
 | |
| void Action::populateSubmenu() {
 | |
| 	if (_content.participants.size() < 1) {
 | |
| 		_submenu.clear();
 | |
| 		_parentMenu->removeSubmenu(action());
 | |
| 		if (!isEnabled()) {
 | |
| 			setSelected(false);
 | |
| 		}
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto submenu = _parentMenu->ensureSubmenu(
 | |
| 		action(),
 | |
| 		st::whoReadMenu);
 | |
| 	_submenu.populate(submenu, _content);
 | |
| 	_parentMenu->checkSubmenuShow();
 | |
| }
 | |
| 
 | |
| void Action::paint(Painter &p) {
 | |
| 	const auto enabled = isEnabled();
 | |
| 	const auto selected = isSelected();
 | |
| 	if (selected && _st.itemBgOver->c.alpha() < 255) {
 | |
| 		p.fillRect(0, 0, width(), _height, _st.itemBg);
 | |
| 	}
 | |
| 	const auto &bg = selected ? _st.itemBgOver : _st.itemBg;
 | |
| 	p.fillRect(0, 0, width(), _height, bg);
 | |
| 	if (enabled) {
 | |
| 		paintRipple(p, 0, 0);
 | |
| 	}
 | |
| 	if (!_custom && !_content.singleCustomEntityData.isEmpty()) {
 | |
| 		_custom = _customEmojiFactory(
 | |
| 			_content.singleCustomEntityData,
 | |
| 			[=] { update(); });
 | |
| 	}
 | |
| 	if (_custom) {
 | |
| 		const auto ratio = style::DevicePixelRatio();
 | |
| 		const auto size = Emoji::GetSizeNormal() / ratio;
 | |
| 		const auto adjusted = Text::AdjustCustomEmojiSize(size);
 | |
| 		const auto x = st::defaultWhoRead.iconPosition.x()
 | |
| 			+ (st::whoReadChecks.width() - adjusted) / 2;
 | |
| 		const auto y = (_height - adjusted) / 2;
 | |
| 		_custom->paint(p, {
 | |
| 			.textColor = (selected ? _st.itemFgOver : _st.itemFg)->c,
 | |
| 			.now = crl::now(),
 | |
| 			.position = { x, y },
 | |
| 		});
 | |
| 	} else {
 | |
| 		const auto &icon = (_content.fullReactionsCount)
 | |
| 			? (!enabled
 | |
| 				? st::whoReadReactionsDisabled
 | |
| 				: selected
 | |
| 				? st::whoReadReactionsOver
 | |
| 				: st::whoReadReactions)
 | |
| 			: (_content.type == WhoReadType::Seen)
 | |
| 			? (!enabled
 | |
| 				? st::whoReadChecksDisabled
 | |
| 				: selected
 | |
| 				? st::whoReadChecksOver
 | |
| 				: st::whoReadChecks)
 | |
| 			: (!enabled
 | |
| 				? st::whoReadPlayedDisabled
 | |
| 				: selected
 | |
| 				? st::whoReadPlayedOver
 | |
| 				: st::whoReadPlayed);
 | |
| 		icon.paint(p, st::defaultWhoRead.iconPosition, width());
 | |
| 	}
 | |
| 	p.setPen(!enabled
 | |
| 		? _st.itemFgDisabled
 | |
| 		: selected
 | |
| 		? _st.itemFgOver
 | |
| 		: _st.itemFg);
 | |
| 	_text.drawLeftElided(
 | |
| 		p,
 | |
| 		st::defaultWhoRead.itemPadding.left(),
 | |
| 		st::defaultWhoRead.itemPadding.top(),
 | |
| 		_textWidth,
 | |
| 		width());
 | |
| 	if (_appeared) {
 | |
| 		_userpics->paint(
 | |
| 			p,
 | |
| 			width() - st::defaultWhoRead.itemPadding.right(),
 | |
| 			(height() - st::defaultWhoRead.userpics.size) / 2,
 | |
| 			st::defaultWhoRead.userpics.size);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Action::refreshText() {
 | |
| 	const auto usersCount = int(_content.participants.size());
 | |
| 	const auto onlySeenCount = ranges::count(
 | |
| 		_content.participants,
 | |
| 		QString(),
 | |
| 		&WhoReadParticipant::customEntityData);
 | |
| 	const auto count = std::max(_content.fullReactionsCount, usersCount);
 | |
| 	_text.setMarkedText(
 | |
| 		_st.itemStyle,
 | |
| 		{ ((_content.state == WhoReadState::Unknown)
 | |
| 			? tr::lng_context_seen_loading(tr::now)
 | |
| 			: (usersCount == 1)
 | |
| 			? _content.participants.front().name
 | |
| 			: (_content.fullReactionsCount > 0
 | |
| 				&& _content.fullReactionsCount <= _content.fullReadCount)
 | |
| 			? FormatReactedString(
 | |
| 				_content.fullReactionsCount,
 | |
| 				_content.fullReadCount)
 | |
| 			: (_content.type == WhoReadType::Reacted
 | |
| 				|| (count > 0 && _content.fullReactionsCount > usersCount)
 | |
| 				|| (count > 0 && onlySeenCount == 0))
 | |
| 			? (count
 | |
| 				? tr::lng_context_seen_reacted(
 | |
| 					tr::now,
 | |
| 					lt_count_short,
 | |
| 					count)
 | |
| 				: tr::lng_context_seen_reacted_none(tr::now))
 | |
| 			: (_content.type == WhoReadType::Watched)
 | |
| 			? (count
 | |
| 				? tr::lng_context_seen_watched(tr::now, lt_count, count)
 | |
| 				: tr::lng_context_seen_watched_none(tr::now))
 | |
| 			: (_content.type == WhoReadType::Listened)
 | |
| 			? (count
 | |
| 				? tr::lng_context_seen_listened(tr::now, lt_count, count)
 | |
| 				: tr::lng_context_seen_listened_none(tr::now))
 | |
| 			: (count
 | |
| 				? tr::lng_context_seen_text(tr::now, lt_count, count)
 | |
| 				: tr::lng_context_seen_text_none(tr::now))) },
 | |
| 		MenuTextOptions);
 | |
| }
 | |
| 
 | |
| void Action::refreshDimensions() {
 | |
| 	if (!minWidth()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto textWidth = _text.maxWidth();
 | |
| 	const auto &padding = st::defaultWhoRead.itemPadding;
 | |
| 
 | |
| 	const auto goodWidth = padding.left()
 | |
| 		+ textWidth
 | |
| 		+ (_userpicsWidth ? (_st.itemStyle.font->spacew + _userpicsWidth) : 0)
 | |
| 		+ padding.right();
 | |
| 
 | |
| 	const auto w = std::clamp(
 | |
| 		goodWidth,
 | |
| 		_st.widthMin,
 | |
| 		std::max(minWidth(), _st.widthMin));
 | |
| 	_textWidth = w - (goodWidth - textWidth);
 | |
| }
 | |
| 
 | |
| bool Action::isEnabled() const {
 | |
| 	return !_content.participants.empty()
 | |
| 		|| (_content.state == WhoReadState::MyHidden);
 | |
| }
 | |
| 
 | |
| not_null<QAction*> Action::action() const {
 | |
| 	return _dummyAction;
 | |
| }
 | |
| 
 | |
| QPoint Action::prepareRippleStartPosition() const {
 | |
| 	return mapFromGlobal(QCursor::pos());
 | |
| }
 | |
| 
 | |
| QImage Action::prepareRippleMask() const {
 | |
| 	return Ui::RippleAnimation::RectMask(size());
 | |
| }
 | |
| 
 | |
| int Action::contentHeight() const {
 | |
| 	return _height;
 | |
| }
 | |
| 
 | |
| void Action::handleKeyPress(not_null<QKeyEvent*> e) {
 | |
| 	if (!isSelected()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto key = e->key();
 | |
| 	if (key == Qt::Key_Enter || key == Qt::Key_Return) {
 | |
| 		setClicked(Menu::TriggeredSource::Keyboard);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| WhenAction::WhenAction(
 | |
| 	not_null<PopupMenu*> parentMenu,
 | |
| 	rpl::producer<WhoReadContent> content,
 | |
| 	Fn<void()> showOrPremium)
 | |
| : ItemBase(parentMenu->menu(), parentMenu->menu()->st())
 | |
| , _parentMenu(parentMenu)
 | |
| , _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))
 | |
| , _showOrPremium(std::move(showOrPremium))
 | |
| , _st(parentMenu->menu()->st())
 | |
| , _height(st::whenReadPadding.top()
 | |
| 		+ st::whenReadStyle.font->height
 | |
| 		+ st::whenReadPadding.bottom()) {
 | |
| 	const auto parent = parentMenu->menu();
 | |
| 
 | |
| 	setAcceptBoth(true);
 | |
| 	initResizeHook(parent->sizeValue());
 | |
| 
 | |
| 	std::move(
 | |
| 		content
 | |
| 	) | rpl::start_with_next([=](WhoReadContent &&content) {
 | |
| 		_content = content;
 | |
| 		refreshText();
 | |
| 		refreshDimensions();
 | |
| 		setPointerCursor(isEnabled());
 | |
| 		_dummyAction->setEnabled(isEnabled());
 | |
| 		if (!isEnabled()) {
 | |
| 			setSelected(false);
 | |
| 		}
 | |
| 		update();
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	resolveMinWidth();
 | |
| 	refreshDimensions();
 | |
| 
 | |
| 	paintRequest(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		Painter p(this);
 | |
| 		paint(p);
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	clicks(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		if (_content.state == WhoReadState::MyHidden) {
 | |
| 			if (const auto onstack = _showOrPremium) {
 | |
| 				onstack();
 | |
| 			}
 | |
| 		}
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	enableMouseSelecting();
 | |
| }
 | |
| 
 | |
| void WhenAction::resolveMinWidth() {
 | |
| 	const auto width = [&](const QString &text) {
 | |
| 		return st::whenReadStyle.font->width(text);
 | |
| 	};
 | |
| 	const auto added = st::whenReadShowPadding.left()
 | |
| 		+ st::whenReadShowPadding.right();
 | |
| 
 | |
| 	const auto sampleDate = QDate::currentDate();
 | |
| 	const auto sampleTime = QLocale().toString(
 | |
| 		QTime::currentTime(),
 | |
| 		QLocale::ShortFormat);
 | |
| 	const auto maxTextWidth = added + std::max({
 | |
| 		width(tr::lng_contacts_loading(tr::now)),
 | |
| 		(width(tr::lng_context_read_hidden(tr::now))
 | |
| 			+ st::whenReadSkip
 | |
| 			+ width(tr::lng_context_read_show(tr::now))),
 | |
| 		width(tr::lng_mediaview_today(tr::now, lt_time, sampleTime)),
 | |
| 		width(tr::lng_mediaview_yesterday(tr::now, lt_time, sampleTime)),
 | |
| 		width(tr::lng_mediaview_date_time(
 | |
| 			tr::now,
 | |
| 			lt_date,
 | |
| 			tr::lng_month_day(
 | |
| 				tr::now,
 | |
| 				lt_month,
 | |
| 				Lang::MonthDay(sampleDate.month())(tr::now),
 | |
| 				lt_day,
 | |
| 				QString::number(sampleDate.day())),
 | |
| 			lt_time,
 | |
| 			sampleTime)),
 | |
| 	});
 | |
| 
 | |
| 	const auto maxWidth = st::whenReadPadding.left()
 | |
| 		+ maxTextWidth
 | |
| 		+ st::whenReadPadding.right();
 | |
| 	setMinWidth(maxWidth);
 | |
| }
 | |
| 
 | |
| void WhenAction::paint(Painter &p) {
 | |
| 	const auto loading = !isEnabled() && _content.participants.empty();
 | |
| 	const auto selected = isSelected();
 | |
| 	if (selected && _st.itemBgOver->c.alpha() < 255) {
 | |
| 		p.fillRect(0, 0, width(), _height, _st.itemBg);
 | |
| 	}
 | |
| 	p.fillRect(0, 0, width(), _height, _st.itemBg);
 | |
| 	const auto &icon = loading
 | |
| 		? st::whoReadChecksDisabled
 | |
| 		: selected
 | |
| 		? st::whoReadChecksOver
 | |
| 		: st::whoReadChecks;
 | |
| 	icon.paint(p, st::whenReadIconPosition, width());
 | |
| 	p.setPen(loading ? _st.itemFgDisabled : _st.itemFg);
 | |
| 	_text.drawLeftElided(
 | |
| 		p,
 | |
| 		st::whenReadPadding.left(),
 | |
| 		st::whenReadPadding.top(),
 | |
| 		_textWidth,
 | |
| 		width());
 | |
| 	if (!_show.isEmpty()) {
 | |
| 		auto hq = PainterHighQualityEnabler(p);
 | |
| 		p.setPen(Qt::NoPen);
 | |
| 		p.setBrush(_st.itemBgOver);
 | |
| 		const auto radius = _showRect.height() / 2.;
 | |
| 		p.drawRoundedRect(_showRect, radius, radius);
 | |
| 		paintRipple(p, 0, 0);
 | |
| 		const auto inner = _showRect.marginsRemoved(st::whenReadShowPadding);
 | |
| 		p.setPen(_st.itemFgOver);
 | |
| 		_show.drawLeftElided(
 | |
| 			p,
 | |
| 			inner.x(),
 | |
| 			inner.y(),
 | |
| 			inner.width(),
 | |
| 			width());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void WhenAction::refreshText() {
 | |
| 	_text.setMarkedText(
 | |
| 		st::whenReadStyle,
 | |
| 		{ ((_content.state == WhoReadState::Unknown)
 | |
| 			? tr::lng_context_seen_loading(tr::now)
 | |
| 			: _content.participants.empty()
 | |
| 			? tr::lng_context_read_hidden(tr::now)
 | |
| 			: _content.participants.front().date) },
 | |
| 		MenuTextOptions);
 | |
| 	if (_content.state == WhoReadState::MyHidden) {
 | |
| 		_show.setMarkedText(
 | |
| 			st::whenReadStyle,
 | |
| 			{ tr::lng_context_read_show(tr::now) },
 | |
| 			MenuTextOptions);
 | |
| 	} else {
 | |
| 		_show = Text::String();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void WhenAction::resizeEvent(QResizeEvent *e) {
 | |
| 	ItemBase::resizeEvent(e);
 | |
| 	refreshDimensions();
 | |
| }
 | |
| 
 | |
| void WhenAction::refreshDimensions() {
 | |
| 	if (!minWidth()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto textWidth = _text.maxWidth();
 | |
| 	const auto showWidth = _show.isEmpty() ? 0 : _show.maxWidth();
 | |
| 	const auto &padding = st::whenReadPadding;
 | |
| 
 | |
| 	const auto goodWidth = padding.left()
 | |
| 		+ textWidth
 | |
| 		+ (showWidth
 | |
| 			? (st::whenReadSkip
 | |
| 				+ st::whenReadShowPadding.left()
 | |
| 				+ showWidth
 | |
| 				+ st::whenReadShowPadding.right())
 | |
| 			: 0)
 | |
| 		+ padding.right();
 | |
| 
 | |
| 	const auto w = std::clamp(
 | |
| 		goodWidth,
 | |
| 		_st.widthMin,
 | |
| 		std::max(width(), _st.widthMin));
 | |
| 	_textWidth = std::min(w - (goodWidth - textWidth), textWidth);
 | |
| 	if (showWidth) {
 | |
| 		_showRect = QRect(
 | |
| 			padding.left() + _textWidth + st::whenReadSkip,
 | |
| 			padding.top() - st::whenReadShowPadding.top(),
 | |
| 			(st::whenReadShowPadding.left()
 | |
| 				+ showWidth
 | |
| 				+ st::whenReadShowPadding.right()),
 | |
| 			(st::whenReadShowPadding.top()
 | |
| 				+ st::whenReadStyle.font->height
 | |
| 				+ st::whenReadShowPadding.bottom()));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool WhenAction::isEnabled() const {
 | |
| 	return (_content.state == WhoReadState::MyHidden);
 | |
| }
 | |
| 
 | |
| not_null<QAction*> WhenAction::action() const {
 | |
| 	return _dummyAction;
 | |
| }
 | |
| 
 | |
| QPoint WhenAction::prepareRippleStartPosition() const {
 | |
| 	const auto result = mapFromGlobal(QCursor::pos());
 | |
| 	return _showRect.contains(result)
 | |
| 		? result
 | |
| 		: Ui::RippleButton::DisabledRippleStartPosition();
 | |
| }
 | |
| 
 | |
| QImage WhenAction::prepareRippleMask() const {
 | |
| 	return Ui::RippleAnimation::MaskByDrawer(size(), false, [&](QPainter &p) {
 | |
| 		const auto radius = _showRect.height() / 2.;
 | |
| 		p.drawRoundedRect(_showRect, radius, radius);
 | |
| 	});
 | |
| }
 | |
| 
 | |
| int WhenAction::contentHeight() const {
 | |
| 	return _height;
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| WhoReactedEntryAction::WhoReactedEntryAction(
 | |
| 	not_null<RpWidget*> parent,
 | |
| 	CustomEmojiFactory customEmojiFactory,
 | |
| 	const style::Menu &st,
 | |
| 	Data &&data)
 | |
| : ItemBase(parent, st)
 | |
| , _dummyAction(CreateChild<QAction>(parent.get()))
 | |
| , _customEmojiFactory(std::move(customEmojiFactory))
 | |
| , _st(st)
 | |
| , _height(st::defaultWhoRead.photoSkip * 2 + st::defaultWhoRead.photoSize) {
 | |
| 	setAcceptBoth(true);
 | |
| 
 | |
| 	initResizeHook(parent->sizeValue());
 | |
| 	setData(std::move(data));
 | |
| 
 | |
| 	paintRequest(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		paint(Painter(this));
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	enableMouseSelecting();
 | |
| }
 | |
| 
 | |
| not_null<QAction*> WhoReactedEntryAction::action() const {
 | |
| 	return _dummyAction.get();
 | |
| }
 | |
| 
 | |
| bool WhoReactedEntryAction::isEnabled() const {
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| int WhoReactedEntryAction::contentHeight() const {
 | |
| 	return _height;
 | |
| }
 | |
| 
 | |
| void WhoReactedEntryAction::setData(Data &&data) {
 | |
| 	setClickedCallback(std::move(data.callback));
 | |
| 	_userpic = std::move(data.userpic);
 | |
| 	_text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions);
 | |
| 	if (data.date.isEmpty()) {
 | |
| 		_date = Text::String();
 | |
| 	} else {
 | |
| 		_date.setMarkedText(
 | |
| 			st::whoReadDateStyle,
 | |
| 			{ data.date },
 | |
| 			MenuTextOptions);
 | |
| 	}
 | |
| 	_type = data.type;
 | |
| 	_custom = _customEmojiFactory
 | |
| 		? _customEmojiFactory(data.customEntityData, [=] { update(); })
 | |
| 		: nullptr;
 | |
| 	const auto ratio = style::DevicePixelRatio();
 | |
| 	const auto size = Emoji::GetSizeNormal() / ratio;
 | |
| 	_customSize = Text::AdjustCustomEmojiSize(size);
 | |
| 	const auto textWidth = std::max(
 | |
| 		_text.maxWidth(),
 | |
| 		st::whoReadDateSkip + _date.maxWidth());
 | |
| 	const auto &padding = _st.itemPadding;
 | |
| 	const auto rightSkip = padding.right()
 | |
| 		+ (_custom ? (size + padding.right()) : 0);
 | |
| 	const auto goodWidth = st::defaultWhoRead.nameLeft
 | |
| 		+ textWidth
 | |
| 		+ rightSkip;
 | |
| 	const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax);
 | |
| 	_textWidth = w - (goodWidth - textWidth);
 | |
| 	setMinWidth(w);
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void WhoReactedEntryAction::paint(Painter &&p) {
 | |
| 	const auto enabled = isEnabled();
 | |
| 	const auto selected = isSelected();
 | |
| 	if (selected && _st.itemBgOver->c.alpha() < 255) {
 | |
| 		p.fillRect(0, 0, width(), _height, _st.itemBg);
 | |
| 	}
 | |
| 	p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
 | |
| 	if (enabled) {
 | |
| 		paintRipple(p, 0, 0);
 | |
| 	}
 | |
| 	const auto photoSize = st::defaultWhoRead.photoSize;
 | |
| 	const auto photoLeft = st::defaultWhoRead.photoLeft;
 | |
| 	const auto photoTop = (height() - photoSize) / 2;
 | |
| 	const auto preloader = (_type == WhoReactedType::Preloader);
 | |
| 	const auto preloaderBrush = preloader
 | |
| 		? [&] {
 | |
| 			auto color = _st.itemFg->c;
 | |
| 			color.setAlphaF(color.alphaF() * kPreloaderAlpha);
 | |
| 			return QBrush(color);
 | |
| 		}() : QBrush();
 | |
| 	if (preloader) {
 | |
| 		auto hq = PainterHighQualityEnabler(p);
 | |
| 		p.setPen(Qt::NoPen);
 | |
| 		p.setBrush(preloaderBrush);
 | |
| 		p.drawEllipse(photoLeft, photoTop, photoSize, photoSize);
 | |
| 	} else if (!_userpic.isNull()) {
 | |
| 		p.drawImage(photoLeft, photoTop, _userpic);
 | |
| 	} else if (!_custom) {
 | |
| 		st::menuIconReactions.paintInCenter(
 | |
| 			p,
 | |
| 			QRect(photoLeft, photoTop, photoSize, photoSize));
 | |
| 	}
 | |
| 
 | |
| 	const auto withDate = !_date.isEmpty();
 | |
| 	const auto textTop = withDate
 | |
| 		? st::whoReadNameWithDateTop
 | |
| 		: (height() - _st.itemStyle.font->height) / 2;
 | |
| 	if (_type == WhoReactedType::Preloader) {
 | |
| 		auto hq = PainterHighQualityEnabler(p);
 | |
| 		p.setPen(Qt::NoPen);
 | |
| 		p.setBrush(preloaderBrush);
 | |
| 		const auto height = _st.itemStyle.font->height / 2;
 | |
| 		p.drawRoundedRect(
 | |
| 			st::defaultWhoRead.nameLeft,
 | |
| 			textTop + (_st.itemStyle.font->height - height) / 2,
 | |
| 			_textWidth,
 | |
| 			height,
 | |
| 			height / 2.,
 | |
| 			height / 2.);
 | |
| 	} else {
 | |
| 		p.setPen(selected
 | |
| 			? _st.itemFgOver
 | |
| 			: enabled
 | |
| 			? _st.itemFg
 | |
| 			: _st.itemFgDisabled);
 | |
| 		_text.drawLeftElided(
 | |
| 			p,
 | |
| 			st::defaultWhoRead.nameLeft,
 | |
| 			textTop,
 | |
| 			_textWidth,
 | |
| 			width());
 | |
| 	}
 | |
| 	if (withDate) {
 | |
| 		const auto iconPosition = QPoint(
 | |
| 			st::defaultWhoRead.nameLeft,
 | |
| 			st::whoReadDateTop) + st::whoReadDateChecksPosition;
 | |
| 		const auto icon = [&] {
 | |
| 			switch (_type) {
 | |
| 			case WhoReactedType::Viewed:
 | |
| 				return &(selected
 | |
| 					? st::whoReadDateChecksOver
 | |
| 					: st::whoReadDateChecks);
 | |
| 			case WhoReactedType::Reacted:
 | |
| 				return &(selected
 | |
| 					? st::whoLikedDateHeartOver
 | |
| 					: st::whoLikedDateHeart);
 | |
| 			case WhoReactedType::Reposted:
 | |
| 				return &(selected
 | |
| 					? st::whoRepostedDateHeartOver
 | |
| 					: st::whoRepostedDateHeart);
 | |
| 			case WhoReactedType::Forwarded:
 | |
| 				return &(selected
 | |
| 					? st::whoForwardedDateHeartOver
 | |
| 					: st::whoForwardedDateHeart);
 | |
| 			}
 | |
| 			Unexpected("Type in WhoReactedEntryAction::paint.");
 | |
| 		}();
 | |
| 		icon->paint(p, iconPosition, width());
 | |
| 		p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
 | |
| 		_date.drawLeftElided(
 | |
| 			p,
 | |
| 			st::defaultWhoRead.nameLeft + st::whoReadDateSkip,
 | |
| 			st::whoReadDateTop,
 | |
| 			_textWidth - st::whoReadDateSkip,
 | |
| 			width());
 | |
| 	}
 | |
| 	if (_custom) {
 | |
| 		const auto ratio = style::DevicePixelRatio();
 | |
| 		const auto size = Emoji::GetSizeNormal() / ratio;
 | |
| 		const auto skip = (size - _customSize) / 2;
 | |
| 		_custom->paint(p, {
 | |
| 			.textColor = (selected ? _st.itemFgOver : _st.itemFg)->c,
 | |
| 			.now = crl::now(),
 | |
| 			.position = QPoint(
 | |
| 				width() - _st.itemPadding.right() - size + skip,
 | |
| 				(height() - _customSize) / 2),
 | |
| 		});
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) {
 | |
| 	return (a.id == b.id)
 | |
| 		&& (a.name == b.name)
 | |
| 		&& (a.date == b.date)
 | |
| 		&& (a.userpicKey == b.userpicKey);
 | |
| }
 | |
| 
 | |
| bool operator!=(const WhoReadParticipant &a, const WhoReadParticipant &b) {
 | |
| 	return !(a == b);
 | |
| }
 | |
| 
 | |
| base::unique_qptr<Menu::ItemBase> WhoReactedContextAction(
 | |
| 		not_null<PopupMenu*> menu,
 | |
| 		rpl::producer<WhoReadContent> content,
 | |
| 		CustomEmojiFactory factory,
 | |
| 		Fn<void(WhoReadParticipant)> participantChosen,
 | |
| 		Fn<void()> showAllChosen) {
 | |
| 	return base::make_unique_q<Action>(
 | |
| 		menu,
 | |
| 		std::move(content),
 | |
| 		std::move(factory),
 | |
| 		std::move(participantChosen),
 | |
| 		std::move(showAllChosen));
 | |
| }
 | |
| 
 | |
| base::unique_qptr<Menu::ItemBase> WhenReadContextAction(
 | |
| 		not_null<PopupMenu*> menu,
 | |
| 		rpl::producer<WhoReadContent> content,
 | |
| 		Fn<void()> showOrPremium) {
 | |
| 	return base::make_unique_q<WhenAction>(
 | |
| 		menu,
 | |
| 		std::move(content),
 | |
| 		std::move(showOrPremium));
 | |
| }
 | |
| 
 | |
| WhoReactedListMenu::WhoReactedListMenu(
 | |
| 	CustomEmojiFactory factory,
 | |
| 	Fn<void(WhoReadParticipant)> participantChosen,
 | |
| 	Fn<void()> showAllChosen)
 | |
| : _customEmojiFactory(std::move(factory))
 | |
| , _participantChosen(std::move(participantChosen))
 | |
| , _showAllChosen(std::move(showAllChosen)) {
 | |
| }
 | |
| 
 | |
| void WhoReactedListMenu::clear() {
 | |
| 	_actions.clear();
 | |
| }
 | |
| 
 | |
| void WhoReactedListMenu::populate(
 | |
| 		not_null<PopupMenu*> menu,
 | |
| 		const WhoReadContent &content,
 | |
| 		Fn<void()> refillTopActions,
 | |
| 		int addedToBottom,
 | |
| 		Fn<void()> appendBottomActions) {
 | |
| 	const auto reactions = ranges::count_if(
 | |
| 		content.participants,
 | |
| 		[](const auto &p) { return !p.customEntityData.isEmpty(); });
 | |
| 	const auto addShowAll = (content.fullReactionsCount > reactions);
 | |
| 	const auto actionsCount = int(content.participants.size())
 | |
| 		+ (addShowAll ? 1 : 0);
 | |
| 	if (_actions.size() > actionsCount) {
 | |
| 		_actions.clear();
 | |
| 		menu->clearActions();
 | |
| 		if (refillTopActions) {
 | |
| 			refillTopActions();
 | |
| 		}
 | |
| 		addedToBottom = 0;
 | |
| 	}
 | |
| 	auto index = 0;
 | |
| 	const auto append = [&](WhoReactedEntryData &&data) {
 | |
| 		if (index < _actions.size()) {
 | |
| 			_actions[index]->setData(std::move(data));
 | |
| 		} else {
 | |
| 			auto item = base::make_unique_q<WhoReactedEntryAction>(
 | |
| 				menu->menu(),
 | |
| 				_customEmojiFactory,
 | |
| 				menu->menu()->st(),
 | |
| 				std::move(data));
 | |
| 			_actions.push_back(item.get());
 | |
| 			const auto count = int(menu->actions().size());
 | |
| 			if (addedToBottom > 0 && addedToBottom <= count) {
 | |
| 				menu->insertAction(count - addedToBottom, std::move(item));
 | |
| 			} else {
 | |
| 				menu->addAction(std::move(item));
 | |
| 			}
 | |
| 		}
 | |
| 		++index;
 | |
| 	};
 | |
| 	for (const auto &participant : content.participants) {
 | |
| 		const auto chosen = [call = _participantChosen, participant] {
 | |
| 			call(participant);
 | |
| 		};
 | |
| 		append({
 | |
| 			.text = participant.name,
 | |
| 			.date = participant.date,
 | |
| 			.type = (participant.dateReacted
 | |
| 				? WhoReactedType::Reacted
 | |
| 				: WhoReactedType::Viewed),
 | |
| 			.customEntityData = participant.customEntityData,
 | |
| 			.userpic = participant.userpicLarge,
 | |
| 			.callback = chosen,
 | |
| 		});
 | |
| 	}
 | |
| 	if (addShowAll) {
 | |
| 		append({
 | |
| 			.text = tr::lng_context_seen_reacted_all(tr::now),
 | |
| 			.callback = _showAllChosen,
 | |
| 		});
 | |
| 	}
 | |
| 	if (!addedToBottom && appendBottomActions) {
 | |
| 		appendBottomActions();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| } // namespace Ui
 | 
