812 lines
		
	
	
	
		
			23 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			812 lines
		
	
	
	
		
			23 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 "boxes/peers/edit_peer_reactions.h"
 | 
						|
 | 
						|
#include "base/event_filter.h"
 | 
						|
#include "chat_helpers/tabbed_panel.h"
 | 
						|
#include "chat_helpers/tabbed_selector.h"
 | 
						|
#include "data/data_chat.h"
 | 
						|
#include "data/data_channel.h"
 | 
						|
#include "data/data_document.h"
 | 
						|
#include "data/data_session.h"
 | 
						|
#include "history/view/reactions/history_view_reactions_selector.h"
 | 
						|
#include "main/main_session.h"
 | 
						|
#include "apiwrap.h"
 | 
						|
#include "lang/lang_keys.h"
 | 
						|
#include "ui/boxes/boost_box.h"
 | 
						|
#include "ui/widgets/fields/input_field.h"
 | 
						|
#include "ui/layers/generic_box.h"
 | 
						|
#include "ui/text/text_utilities.h"
 | 
						|
#include "ui/widgets/checkbox.h"
 | 
						|
#include "ui/wrap/slide_wrap.h"
 | 
						|
#include "ui/vertical_list.h"
 | 
						|
#include "window/window_session_controller.h"
 | 
						|
#include "window/window_session_controller_link_info.h"
 | 
						|
#include "styles/style_chat_helpers.h"
 | 
						|
#include "styles/style_info.h"
 | 
						|
#include "styles/style_settings.h"
 | 
						|
#include "styles/style_layers.h"
 | 
						|
 | 
						|
#include <QtWidgets/QTextEdit>
 | 
						|
#include <QtGui/QTextBlock>
 | 
						|
#include <QtGui/QTextDocumentFragment>
 | 
						|
 | 
						|
namespace {
 | 
						|
 | 
						|
constexpr auto kDisabledEmojiOpacity = 0.4;
 | 
						|
 | 
						|
struct UniqueCustomEmojiContext {
 | 
						|
	std::vector<DocumentId> ids;
 | 
						|
	Fn<bool(DocumentId)> applyHardLimit;
 | 
						|
	int hardLimit = 0;
 | 
						|
	int hardLimitChecked = 0;
 | 
						|
	bool hardLimitHit = false;
 | 
						|
};
 | 
						|
 | 
						|
class MaybeDisabledEmoji final : public Ui::Text::CustomEmoji {
 | 
						|
public:
 | 
						|
	MaybeDisabledEmoji(
 | 
						|
		std::unique_ptr<CustomEmoji> wrapped,
 | 
						|
		Fn<bool()> enabled);
 | 
						|
 | 
						|
	int width() override;
 | 
						|
	QString entityData() override;
 | 
						|
	void paint(QPainter &p, const Context &context) override;
 | 
						|
	void unload() override;
 | 
						|
	bool ready() override;
 | 
						|
	bool readyInDefaultState() override;
 | 
						|
 | 
						|
private:
 | 
						|
	const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
 | 
						|
	const Fn<bool()> _enabled;
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
MaybeDisabledEmoji::MaybeDisabledEmoji(
 | 
						|
	std::unique_ptr<CustomEmoji> wrapped,
 | 
						|
	Fn<bool()> enabled)
 | 
						|
: _wrapped(std::move(wrapped))
 | 
						|
, _enabled(std::move(enabled)) {
 | 
						|
}
 | 
						|
 | 
						|
int MaybeDisabledEmoji::width() {
 | 
						|
	return _wrapped->width();
 | 
						|
}
 | 
						|
 | 
						|
QString MaybeDisabledEmoji::entityData() {
 | 
						|
	return _wrapped->entityData();
 | 
						|
}
 | 
						|
 | 
						|
void MaybeDisabledEmoji::paint(QPainter &p, const Context &context) {
 | 
						|
	const auto disabled = !_enabled();
 | 
						|
	const auto was = disabled ? p.opacity() : 1.;
 | 
						|
	if (disabled) {
 | 
						|
		p.setOpacity(kDisabledEmojiOpacity);
 | 
						|
	}
 | 
						|
	_wrapped->paint(p, context);
 | 
						|
	if (disabled) {
 | 
						|
		p.setOpacity(was);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void MaybeDisabledEmoji::unload() {
 | 
						|
	_wrapped->unload();
 | 
						|
}
 | 
						|
 | 
						|
bool MaybeDisabledEmoji::ready() {
 | 
						|
	return _wrapped->ready();
 | 
						|
}
 | 
						|
 | 
						|
bool MaybeDisabledEmoji::readyInDefaultState() {
 | 
						|
	return _wrapped->readyInDefaultState();
 | 
						|
}
 | 
						|
 | 
						|
[[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) {
 | 
						|
	auto all = TextUtilities::SplitTags(mimeTag);
 | 
						|
	for (auto i = all.begin(); i != all.end();) {
 | 
						|
		if (Ui::InputField::IsCustomEmojiLink(*i)) {
 | 
						|
			++i;
 | 
						|
		} else {
 | 
						|
			i = all.erase(i);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return TextUtilities::JoinTag(all);
 | 
						|
}
 | 
						|
 | 
						|
[[nodiscard]] bool AllowOnlyCustomEmojiMimeDataHook(
 | 
						|
		not_null<const QMimeData*> data,
 | 
						|
		Ui::InputField::MimeAction action) {
 | 
						|
	if (action == Ui::InputField::MimeAction::Check) {
 | 
						|
		const auto textMime = TextUtilities::TagsTextMimeType();
 | 
						|
		const auto tagsMime = TextUtilities::TagsMimeType();
 | 
						|
		if (!data->hasFormat(textMime) || !data->hasFormat(tagsMime)) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
		auto text = QString::fromUtf8(data->data(textMime));
 | 
						|
		auto tags = TextUtilities::DeserializeTags(
 | 
						|
			data->data(tagsMime),
 | 
						|
			text.size());
 | 
						|
		auto checkedTill = 0;
 | 
						|
		ranges::sort(tags, ranges::less(), &TextWithTags::Tag::offset);
 | 
						|
		for (const auto &tag : tags) {
 | 
						|
			if (tag.offset != checkedTill
 | 
						|
				|| AllowOnlyCustomEmojiProcessor(tag.id) != tag.id) {
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
			checkedTill += tag.length;
 | 
						|
		}
 | 
						|
		return true;
 | 
						|
	} else if (action == Ui::InputField::MimeAction::Insert) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	Unexpected("Action in MimeData hook.");
 | 
						|
}
 | 
						|
 | 
						|
[[nodiscard]] std::vector<Data::ReactionId> DefaultSelected() {
 | 
						|
	const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d");
 | 
						|
	const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
 | 
						|
	return { Data::ReactionId{ like }, Data::ReactionId{ dislike } };
 | 
						|
}
 | 
						|
 | 
						|
[[nodiscard]] bool RemoveNonCustomEmojiFragment(
 | 
						|
		not_null<QTextDocument*> document,
 | 
						|
		UniqueCustomEmojiContext &context) {
 | 
						|
	context.ids.clear();
 | 
						|
	context.hardLimitChecked = 0;
 | 
						|
	auto removeFrom = 0;
 | 
						|
	auto removeTill = 0;
 | 
						|
	auto block = document->begin();
 | 
						|
	for (auto j = block.begin(); !j.atEnd(); ++j) {
 | 
						|
		const auto fragment = j.fragment();
 | 
						|
		Assert(fragment.isValid());
 | 
						|
 | 
						|
		removeTill = removeFrom = fragment.position();
 | 
						|
		const auto format = fragment.charFormat();
 | 
						|
		if (format.objectType() != Ui::InputField::kCustomEmojiFormat) {
 | 
						|
			removeTill += fragment.length();
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		const auto id = format.property(Ui::InputField::kCustomEmojiId);
 | 
						|
		const auto documentId = id.toULongLong();
 | 
						|
		const auto applyHardLimit = context.applyHardLimit(documentId);
 | 
						|
		if (ranges::contains(context.ids, documentId)) {
 | 
						|
			removeTill += fragment.length();
 | 
						|
			break;
 | 
						|
		} else if (applyHardLimit
 | 
						|
			&& context.hardLimitChecked >= context.hardLimit) {
 | 
						|
			context.hardLimitHit = true;
 | 
						|
			removeTill += fragment.length();
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		context.ids.push_back(documentId);
 | 
						|
		if (applyHardLimit) {
 | 
						|
			++context.hardLimitChecked;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	while (removeTill == removeFrom) {
 | 
						|
		block = block.next();
 | 
						|
		if (block == document->end()) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
		removeTill = block.position();
 | 
						|
	}
 | 
						|
	Ui::PrepareFormattingOptimization(document);
 | 
						|
	auto cursor = QTextCursor(document);
 | 
						|
	cursor.setPosition(removeFrom);
 | 
						|
	cursor.setPosition(removeTill, QTextCursor::KeepAnchor);
 | 
						|
	cursor.removeSelectedText();
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
bool RemoveNonCustomEmoji(
 | 
						|
		not_null<QTextDocument*> document,
 | 
						|
		UniqueCustomEmojiContext &context) {
 | 
						|
	if (!RemoveNonCustomEmojiFragment(document, context)) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
	while (RemoveNonCustomEmojiFragment(document, context)) {
 | 
						|
	}
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
void SetupOnlyCustomEmojiField(
 | 
						|
		not_null<Ui::InputField*> field,
 | 
						|
		Fn<void(std::vector<DocumentId>, bool)> callback,
 | 
						|
		Fn<bool(DocumentId)> applyHardLimit,
 | 
						|
		int customHardLimit) {
 | 
						|
	field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor);
 | 
						|
	field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook);
 | 
						|
 | 
						|
	struct State {
 | 
						|
		bool processing = false;
 | 
						|
		bool pending = false;
 | 
						|
	};
 | 
						|
	const auto state = field->lifetime().make_state<State>();
 | 
						|
 | 
						|
	field->changes(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		state->pending = true;
 | 
						|
		if (state->processing) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
		auto context = UniqueCustomEmojiContext{
 | 
						|
			.applyHardLimit = applyHardLimit,
 | 
						|
			.hardLimit = customHardLimit,
 | 
						|
		};
 | 
						|
		auto changed = false;
 | 
						|
		state->processing = true;
 | 
						|
		while (state->pending) {
 | 
						|
			state->pending = false;
 | 
						|
			const auto document = field->rawTextEdit()->document();
 | 
						|
			const auto pageSize = document->pageSize();
 | 
						|
			QTextCursor(document).joinPreviousEditBlock();
 | 
						|
			if (RemoveNonCustomEmoji(document, context)) {
 | 
						|
				changed = true;
 | 
						|
			}
 | 
						|
			state->processing = false;
 | 
						|
			QTextCursor(document).endEditBlock();
 | 
						|
			if (document->pageSize() != pageSize) {
 | 
						|
				document->setPageSize(pageSize);
 | 
						|
			}
 | 
						|
		}
 | 
						|
		callback(context.ids, context.hardLimitHit);
 | 
						|
		if (changed) {
 | 
						|
			field->forceProcessContentsChanges();
 | 
						|
		}
 | 
						|
	}, field->lifetime());
 | 
						|
}
 | 
						|
 | 
						|
[[nodiscard]] TextWithTags ComposeEmojiList(
 | 
						|
		not_null<Data::Reactions*> reactions,
 | 
						|
		const std::vector<Data::ReactionId> &list) {
 | 
						|
	auto result = TextWithTags();
 | 
						|
	const auto size = [&] {
 | 
						|
		return int(result.text.size());
 | 
						|
	};
 | 
						|
	auto added = base::flat_set<Data::ReactionId>();
 | 
						|
	const auto &all = reactions->list(Data::Reactions::Type::All);
 | 
						|
	const auto add = [&](Data::ReactionId id) {
 | 
						|
		if (!added.emplace(id).second) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
		auto unifiedId = id.custom();
 | 
						|
		const auto offset = size();
 | 
						|
		if (unifiedId) {
 | 
						|
			result.text.append('@');
 | 
						|
		} else {
 | 
						|
			result.text.append(id.emoji());
 | 
						|
			const auto i = ranges::find(all, id, &Data::Reaction::id);
 | 
						|
			if (i == end(all)) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
			unifiedId = i->selectAnimation->id;
 | 
						|
		}
 | 
						|
		const auto data = Data::SerializeCustomEmojiId(unifiedId);
 | 
						|
		const auto tag = Ui::InputField::CustomEmojiLink(data);
 | 
						|
		result.tags.append({ offset, size() - offset, tag });
 | 
						|
	};
 | 
						|
	for (const auto &id : list) {
 | 
						|
		add(id);
 | 
						|
	}
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
enum class ReactionsSelectorState {
 | 
						|
	Active,
 | 
						|
	Disabled,
 | 
						|
	Hidden,
 | 
						|
};
 | 
						|
 | 
						|
struct ReactionsSelectorArgs {
 | 
						|
	not_null<QWidget*> outer;
 | 
						|
	not_null<Window::SessionController*> controller;
 | 
						|
	rpl::producer<QString> title;
 | 
						|
	std::vector<Data::Reaction> list;
 | 
						|
	std::vector<Data::ReactionId> selected;
 | 
						|
	Fn<void(std::vector<Data::ReactionId>, bool)> callback;
 | 
						|
	rpl::producer<ReactionsSelectorState> stateValue;
 | 
						|
	int customAllowed = 0;
 | 
						|
	int customHardLimit = 0;
 | 
						|
	bool all = false;
 | 
						|
};
 | 
						|
 | 
						|
object_ptr<Ui::RpWidget> AddReactionsSelector(
 | 
						|
		not_null<Ui::RpWidget*> parent,
 | 
						|
		ReactionsSelectorArgs &&args) {
 | 
						|
	using namespace ChatHelpers;
 | 
						|
	using HistoryView::Reactions::UnifiedFactoryOwner;
 | 
						|
 | 
						|
	auto result = object_ptr<Ui::InputField>(
 | 
						|
		parent,
 | 
						|
		st::manageGroupReactionsField,
 | 
						|
		Ui::InputField::Mode::MultiLine,
 | 
						|
		std::move(args.title));
 | 
						|
	const auto raw = result.data();
 | 
						|
	const auto session = &args.controller->session();
 | 
						|
	const auto owner = &session->data();
 | 
						|
	const auto reactions = &owner->reactions();
 | 
						|
	const auto customAllowed = args.customAllowed;
 | 
						|
 | 
						|
	struct State {
 | 
						|
		std::unique_ptr<Ui::RpWidget> overlay;
 | 
						|
		std::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner;
 | 
						|
		UnifiedFactoryOwner::RecentFactory factory;
 | 
						|
		base::flat_set<DocumentId> allowed;
 | 
						|
		rpl::lifetime focusLifetime;
 | 
						|
	};
 | 
						|
	const auto state = raw->lifetime().make_state<State>();
 | 
						|
	state->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
 | 
						|
		session,
 | 
						|
		reactions->list(Data::Reactions::Type::Active));
 | 
						|
	state->factory = state->unifiedFactoryOwner->factory();
 | 
						|
 | 
						|
	const auto customEmojiPaused = [controller = args.controller] {
 | 
						|
		return controller->isGifPausedAtLeastFor(PauseReason::Layer);
 | 
						|
	};
 | 
						|
	raw->setCustomEmojiFactory([=](QStringView data, Fn<void()> update)
 | 
						|
	-> std::unique_ptr<Ui::Text::CustomEmoji> {
 | 
						|
		const auto id = Data::ParseCustomEmojiData(data);
 | 
						|
		auto result = owner->customEmojiManager().create(
 | 
						|
			data,
 | 
						|
			std::move(update));
 | 
						|
		if (state->unifiedFactoryOwner->lookupReactionId(id).custom()) {
 | 
						|
			return std::make_unique<MaybeDisabledEmoji>(
 | 
						|
				std::move(result),
 | 
						|
				[=] { return state->allowed.contains(id); });
 | 
						|
		}
 | 
						|
		using namespace Ui::Text;
 | 
						|
		return std::make_unique<FirstFrameEmoji>(std::move(result));
 | 
						|
	}, std::move(customEmojiPaused));
 | 
						|
 | 
						|
	const auto callback = args.callback;
 | 
						|
	const auto isCustom = [=](DocumentId id) {
 | 
						|
		return state->unifiedFactoryOwner->lookupReactionId(id).custom();
 | 
						|
	};
 | 
						|
	SetupOnlyCustomEmojiField(raw, [=](
 | 
						|
			std::vector<DocumentId> ids,
 | 
						|
			bool hardLimitHit) {
 | 
						|
		auto allowed = base::flat_set<DocumentId>();
 | 
						|
		auto reactions = std::vector<Data::ReactionId>();
 | 
						|
		reactions.reserve(ids.size());
 | 
						|
		allowed.reserve(std::min(customAllowed, int(ids.size())));
 | 
						|
		const auto owner = state->unifiedFactoryOwner.get();
 | 
						|
		for (const auto id : ids) {
 | 
						|
			const auto reactionId = owner->lookupReactionId(id);
 | 
						|
			if (reactionId.custom() && allowed.size() < customAllowed) {
 | 
						|
				allowed.emplace(id);
 | 
						|
			}
 | 
						|
			reactions.push_back(reactionId);
 | 
						|
		}
 | 
						|
		if (state->allowed != allowed) {
 | 
						|
			state->allowed = std::move(allowed);
 | 
						|
			raw->rawTextEdit()->update();
 | 
						|
		}
 | 
						|
		callback(std::move(reactions), hardLimitHit);
 | 
						|
	}, isCustom, args.customHardLimit);
 | 
						|
	raw->setTextWithTags(ComposeEmojiList(reactions, args.selected));
 | 
						|
 | 
						|
	using SelectorState = ReactionsSelectorState;
 | 
						|
	std::move(
 | 
						|
		args.stateValue
 | 
						|
	) | rpl::start_with_next([=](SelectorState value) {
 | 
						|
		switch (value) {
 | 
						|
		case SelectorState::Active:
 | 
						|
			state->overlay = nullptr;
 | 
						|
			state->focusLifetime.destroy();
 | 
						|
			if (raw->empty()) {
 | 
						|
				raw->setTextWithTags(
 | 
						|
					ComposeEmojiList(reactions, DefaultSelected()));
 | 
						|
			}
 | 
						|
			raw->setDisabled(false);
 | 
						|
			raw->setFocusFast();
 | 
						|
			break;
 | 
						|
		case SelectorState::Disabled:
 | 
						|
			state->overlay = std::make_unique<Ui::RpWidget>(parent);
 | 
						|
			state->overlay->show();
 | 
						|
			raw->geometryValue() | rpl::start_with_next([=](QRect rect) {
 | 
						|
				state->overlay->setGeometry(rect);
 | 
						|
			}, state->overlay->lifetime());
 | 
						|
			state->overlay->paintRequest() | rpl::start_with_next([=](QRect clip) {
 | 
						|
				auto color = st::boxBg->c;
 | 
						|
				color.setAlphaF(0.5);
 | 
						|
				QPainter(state->overlay.get()).fillRect(
 | 
						|
					clip,
 | 
						|
					color);
 | 
						|
			}, state->overlay->lifetime());
 | 
						|
			[[fallthrough]];
 | 
						|
		case SelectorState::Hidden:
 | 
						|
			if (Ui::InFocusChain(raw)) {
 | 
						|
				raw->parentWidget()->setFocus();
 | 
						|
			}
 | 
						|
			raw->setDisabled(true);
 | 
						|
			raw->focusedChanges(
 | 
						|
			) | rpl::start_with_next([=](bool focused) {
 | 
						|
				if (focused) {
 | 
						|
					raw->parentWidget()->setFocus();
 | 
						|
				}
 | 
						|
			}, state->focusLifetime);
 | 
						|
			break;
 | 
						|
		}
 | 
						|
	}, raw->lifetime());
 | 
						|
 | 
						|
	const auto toggle = Ui::CreateChild<Ui::IconButton>(
 | 
						|
		parent.get(),
 | 
						|
		st::manageGroupReactions);
 | 
						|
 | 
						|
	const auto panel = Ui::CreateChild<TabbedPanel>(
 | 
						|
		args.outer.get(),
 | 
						|
		args.controller,
 | 
						|
		object_ptr<TabbedSelector>(
 | 
						|
			nullptr,
 | 
						|
			args.controller->uiShow(),
 | 
						|
			Window::GifPauseReason::Layer,
 | 
						|
			(args.all
 | 
						|
				? TabbedSelector::Mode::FullReactions
 | 
						|
				: TabbedSelector::Mode::RecentReactions)));
 | 
						|
	panel->selector()->provideRecentEmoji(
 | 
						|
		state->unifiedFactoryOwner->unifiedIdsList());
 | 
						|
	panel->setDesiredHeightValues(
 | 
						|
		1.,
 | 
						|
		st::emojiPanMinHeight / 2,
 | 
						|
		st::emojiPanMinHeight);
 | 
						|
	panel->hide();
 | 
						|
	panel->selector()->customEmojiChosen(
 | 
						|
	) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
 | 
						|
		Data::InsertCustomEmoji(raw, data.document);
 | 
						|
	}, panel->lifetime());
 | 
						|
 | 
						|
	const auto updateEmojiPanelGeometry = [=] {
 | 
						|
		const auto parent = panel->parentWidget();
 | 
						|
		const auto global = toggle->mapToGlobal({ 0, 0 });
 | 
						|
		const auto local = parent->mapFromGlobal(global);
 | 
						|
		panel->moveBottomRight(
 | 
						|
			local.y(),
 | 
						|
			local.x() + toggle->width() * 3);
 | 
						|
	};
 | 
						|
	const auto scheduleUpdateEmojiPanelGeometry = [=] {
 | 
						|
		// updateEmojiPanelGeometry uses not only container geometry, but
 | 
						|
		// also container children geometries that will be updated later.
 | 
						|
		crl::on_main(raw, updateEmojiPanelGeometry);
 | 
						|
	};
 | 
						|
	const auto filterCallback = [=](not_null<QEvent*> event) {
 | 
						|
		const auto type = event->type();
 | 
						|
		if (type == QEvent::Move || type == QEvent::Resize) {
 | 
						|
			scheduleUpdateEmojiPanelGeometry();
 | 
						|
		}
 | 
						|
		return base::EventFilterResult::Continue;
 | 
						|
	};
 | 
						|
	for (auto widget = (QWidget*)raw
 | 
						|
		; widget && widget != args.outer
 | 
						|
		; widget = widget->parentWidget()) {
 | 
						|
		base::install_event_filter(raw, widget, filterCallback);
 | 
						|
	}
 | 
						|
	base::install_event_filter(raw, args.outer, filterCallback);
 | 
						|
	scheduleUpdateEmojiPanelGeometry();
 | 
						|
 | 
						|
	toggle->installEventFilter(panel);
 | 
						|
	toggle->addClickHandler([=] {
 | 
						|
		panel->toggleAnimated();
 | 
						|
	});
 | 
						|
 | 
						|
	raw->geometryValue() | rpl::start_with_next([=](QRect geometry) {
 | 
						|
		toggle->move(
 | 
						|
			geometry.x() + geometry.width() - toggle->width(),
 | 
						|
			geometry.y() + geometry.height() - toggle->height());
 | 
						|
		updateEmojiPanelGeometry();
 | 
						|
	}, toggle->lifetime());
 | 
						|
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
void AddReactionsText(
 | 
						|
		not_null<Ui::VerticalLayout*> container,
 | 
						|
		not_null<Window::SessionNavigation*> navigation,
 | 
						|
		int allowedCustomReactions,
 | 
						|
		rpl::producer<int> customCountValue,
 | 
						|
		Fn<void(int required)> askForBoosts) {
 | 
						|
	auto ownedInner = object_ptr<Ui::VerticalLayout>(container);
 | 
						|
	const auto inner = ownedInner.data();
 | 
						|
	const auto count = inner->lifetime().make_state<rpl::variable<int>>(
 | 
						|
		std::move(customCountValue));
 | 
						|
 | 
						|
	container->add(
 | 
						|
		object_ptr<Ui::DividerLabel>(
 | 
						|
			container,
 | 
						|
			std::move(ownedInner),
 | 
						|
			st::defaultBoxDividerLabelPadding),
 | 
						|
		QMargins(0, st::manageGroupReactionsTextSkip, 0, 0));
 | 
						|
	const auto label = inner->add(
 | 
						|
		object_ptr<Ui::FlatLabel>(
 | 
						|
			inner,
 | 
						|
			tr::lng_manage_peer_reactions_own(
 | 
						|
				lt_link,
 | 
						|
				tr::lng_manage_peer_reactions_own_link(
 | 
						|
				) | Ui::Text::ToLink(),
 | 
						|
				Ui::Text::WithEntities),
 | 
						|
			st::boxDividerLabel));
 | 
						|
	const auto weak = base::make_weak(navigation);
 | 
						|
	label->setClickHandlerFilter([=](const auto &...) {
 | 
						|
		if (const auto strong = weak.get()) {
 | 
						|
			strong->showPeerByLink(Window::PeerByLinkInfo{
 | 
						|
				.usernameOrId = u"stickers"_q,
 | 
						|
				.resolveType = Window::ResolveType::Mention,
 | 
						|
			});
 | 
						|
		}
 | 
						|
		return false;
 | 
						|
	});
 | 
						|
	auto countString = count->value() | rpl::map([](int count) {
 | 
						|
		return TextWithEntities{ QString::number(count) };
 | 
						|
	});
 | 
						|
	auto needs = rpl::combine(
 | 
						|
		tr::lng_manage_peer_reactions_level(
 | 
						|
			lt_count,
 | 
						|
			count->value() | tr::to_count(),
 | 
						|
			lt_same_count,
 | 
						|
			std::move(countString),
 | 
						|
			Ui::Text::RichLangValue),
 | 
						|
		tr::lng_manage_peer_reactions_boost(
 | 
						|
			lt_link,
 | 
						|
			tr::lng_manage_peer_reactions_boost_link() | Ui::Text::ToLink(),
 | 
						|
			Ui::Text::RichLangValue)
 | 
						|
	) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {
 | 
						|
		a.append(' ').append(std::move(b));
 | 
						|
		return std::move(a);
 | 
						|
	});
 | 
						|
	const auto wrap = inner->add(
 | 
						|
		object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
 | 
						|
			inner,
 | 
						|
			object_ptr<Ui::FlatLabel>(
 | 
						|
				inner,
 | 
						|
				std::move(needs),
 | 
						|
				st::boxDividerLabel),
 | 
						|
			QMargins{ 0, st::normalFont->height, 0, 0 }));
 | 
						|
	wrap->toggleOn(count->value() | rpl::map(
 | 
						|
		rpl::mappers::_1 > allowedCustomReactions
 | 
						|
	));
 | 
						|
	wrap->finishAnimating();
 | 
						|
 | 
						|
	wrap->entity()->setClickHandlerFilter([=](const auto &...) {
 | 
						|
		askForBoosts(count->current());
 | 
						|
		return false;
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
} // namespace
 | 
						|
 | 
						|
void EditAllowedReactionsBox(
 | 
						|
		not_null<Ui::GenericBox*> box,
 | 
						|
		EditAllowedReactionsArgs &&args) {
 | 
						|
	using namespace Data;
 | 
						|
	using namespace rpl::mappers;
 | 
						|
 | 
						|
	box->setTitle(tr::lng_manage_peer_reactions());
 | 
						|
	box->setWidth(st::boxWideWidth);
 | 
						|
 | 
						|
	enum class Option {
 | 
						|
		All,
 | 
						|
		Some,
 | 
						|
		None,
 | 
						|
	};
 | 
						|
	using SelectorState = ReactionsSelectorState;
 | 
						|
	struct State {
 | 
						|
		rpl::variable<Option> option; // For groups.
 | 
						|
		rpl::variable<SelectorState> selectorState;
 | 
						|
		std::vector<Data::ReactionId> selected;
 | 
						|
		rpl::variable<int> customCount;
 | 
						|
	};
 | 
						|
	const auto allowed = args.allowed;
 | 
						|
	const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
 | 
						|
		? Option::All
 | 
						|
		: allowed.some.empty()
 | 
						|
		? Option::None
 | 
						|
		: Option::Some;
 | 
						|
	const auto state = box->lifetime().make_state<State>(State{
 | 
						|
		.option = optionInitial,
 | 
						|
	});
 | 
						|
 | 
						|
	const auto container = box->verticalLayout();
 | 
						|
	const auto isGroup = args.isGroup;
 | 
						|
	const auto enabled = isGroup
 | 
						|
		? nullptr
 | 
						|
		: container->add(object_ptr<Ui::SettingsButton>(
 | 
						|
			container.get(),
 | 
						|
			tr::lng_manage_peer_reactions_enable(),
 | 
						|
			st::manageGroupNoIconButton.button));
 | 
						|
	if (enabled) {
 | 
						|
		enabled->toggleOn(rpl::single(optionInitial != Option::None));
 | 
						|
		enabled->toggledValue(
 | 
						|
		) | rpl::start_with_next([=](bool value) {
 | 
						|
			state->selectorState = value
 | 
						|
				? SelectorState::Active
 | 
						|
				: SelectorState::Disabled;
 | 
						|
		}, enabled->lifetime());
 | 
						|
	}
 | 
						|
	const auto group = std::make_shared<Ui::RadioenumGroup<Option>>(
 | 
						|
		state->option.current());
 | 
						|
	group->setChangedCallback([=](Option value) {
 | 
						|
		state->option = value;
 | 
						|
	});
 | 
						|
	const auto addOption = [&](Option option, const QString &text) {
 | 
						|
		if (!isGroup) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
		container->add(
 | 
						|
			object_ptr<Ui::Radioenum<Option>>(
 | 
						|
				container,
 | 
						|
				group,
 | 
						|
				option,
 | 
						|
				text,
 | 
						|
				st::settingsSendType),
 | 
						|
			st::settingsSendTypePadding);
 | 
						|
	};
 | 
						|
	addOption(Option::All, tr::lng_manage_peer_reactions_all(tr::now));
 | 
						|
	addOption(Option::Some, tr::lng_manage_peer_reactions_some(tr::now));
 | 
						|
	addOption(Option::None, tr::lng_manage_peer_reactions_none(tr::now));
 | 
						|
 | 
						|
	const auto about = [](Option option) {
 | 
						|
		switch (option) {
 | 
						|
		case Option::All: return tr::lng_manage_peer_reactions_all_about();
 | 
						|
		case Option::Some: return tr::lng_manage_peer_reactions_some_about();
 | 
						|
		case Option::None: return tr::lng_manage_peer_reactions_none_about();
 | 
						|
		}
 | 
						|
		Unexpected("Option value in EditAllowedReactionsBox.");
 | 
						|
	};
 | 
						|
	Ui::AddSkip(container);
 | 
						|
	Ui::AddDividerText(
 | 
						|
		container,
 | 
						|
		(isGroup
 | 
						|
			? (state->option.value()
 | 
						|
				| rpl::map(about)
 | 
						|
				| rpl::flatten_latest())
 | 
						|
			: tr::lng_manage_peer_reactions_about_channel()));
 | 
						|
 | 
						|
	const auto wrap = enabled ? nullptr : container->add(
 | 
						|
		object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
 | 
						|
			container,
 | 
						|
			object_ptr<Ui::VerticalLayout>(container)));
 | 
						|
	if (wrap) {
 | 
						|
		wrap->toggleOn(state->option.value(
 | 
						|
		) | rpl::map(_1 == Option::Some) | rpl::before_next([=](bool some) {
 | 
						|
			if (!some) {
 | 
						|
				state->selectorState = SelectorState::Hidden;
 | 
						|
			}
 | 
						|
		}) | rpl::after_next([=](bool some) {
 | 
						|
			if (some) {
 | 
						|
				state->selectorState = SelectorState::Active;
 | 
						|
			}
 | 
						|
		}));
 | 
						|
		wrap->finishAnimating();
 | 
						|
	}
 | 
						|
	const auto reactions = wrap ? wrap->entity() : container.get();
 | 
						|
 | 
						|
	Ui::AddSkip(reactions);
 | 
						|
 | 
						|
	const auto all = args.list;
 | 
						|
	auto selected = (allowed.type != AllowedReactionsType::Some)
 | 
						|
		? (all
 | 
						|
			| ranges::views::transform(&Data::Reaction::id)
 | 
						|
			| ranges::to_vector)
 | 
						|
		: allowed.some;
 | 
						|
	const auto changed = [=](
 | 
						|
		std::vector<Data::ReactionId> chosen,
 | 
						|
		bool hardLimitHit) {
 | 
						|
		state->selected = std::move(chosen);
 | 
						|
		state->customCount = ranges::count_if(
 | 
						|
			state->selected,
 | 
						|
			&Data::ReactionId::custom);
 | 
						|
		if (hardLimitHit) {
 | 
						|
			box->uiShow()->showToast(
 | 
						|
				tr::lng_manage_peer_reactions_limit(tr::now));
 | 
						|
		}
 | 
						|
	};
 | 
						|
	changed(selected.empty() ? DefaultSelected() : std::move(selected), {});
 | 
						|
	reactions->add(AddReactionsSelector(reactions, {
 | 
						|
		.outer = box->getDelegate()->outerContainer(),
 | 
						|
		.controller = args.navigation->parentController(),
 | 
						|
		.title = (enabled
 | 
						|
			? tr::lng_manage_peer_reactions_available()
 | 
						|
			: tr::lng_manage_peer_reactions_some_title()),
 | 
						|
		.list = all,
 | 
						|
		.selected = state->selected,
 | 
						|
		.callback = changed,
 | 
						|
		.stateValue = state->selectorState.value(),
 | 
						|
		.customAllowed = args.allowedCustomReactions,
 | 
						|
		.customHardLimit = args.customReactionsHardLimit,
 | 
						|
		.all = !args.isGroup,
 | 
						|
	}), st::boxRowPadding);
 | 
						|
 | 
						|
	box->setFocusCallback([=] {
 | 
						|
		if (!wrap || state->option.current() == Option::Some) {
 | 
						|
			state->selectorState.force_assign(SelectorState::Active);
 | 
						|
		}
 | 
						|
	});
 | 
						|
 | 
						|
	if (!isGroup) {
 | 
						|
		AddReactionsText(
 | 
						|
			container,
 | 
						|
			args.navigation,
 | 
						|
			args.allowedCustomReactions,
 | 
						|
			state->customCount.value(),
 | 
						|
			args.askForBoosts);
 | 
						|
	}
 | 
						|
	const auto collect = [=] {
 | 
						|
		auto result = AllowedReactions();
 | 
						|
		if (isGroup
 | 
						|
			? (state->option.current() == Option::Some)
 | 
						|
			: (enabled->toggled())) {
 | 
						|
			result.some = state->selected;
 | 
						|
		}
 | 
						|
		auto some = result.some;
 | 
						|
		auto simple = all | ranges::views::transform(
 | 
						|
			&Data::Reaction::id
 | 
						|
		) | ranges::to_vector;
 | 
						|
		ranges::sort(some);
 | 
						|
		ranges::sort(simple);
 | 
						|
		result.type = isGroup
 | 
						|
			? (state->option.current() != Option::All
 | 
						|
				? AllowedReactionsType::Some
 | 
						|
				: AllowedReactionsType::All)
 | 
						|
			: (some == simple)
 | 
						|
			? AllowedReactionsType::Default
 | 
						|
			: AllowedReactionsType::Some;
 | 
						|
		return result;
 | 
						|
	};
 | 
						|
 | 
						|
	box->addButton(tr::lng_settings_save(), [=] {
 | 
						|
		const auto result = collect();
 | 
						|
		if (!isGroup) {
 | 
						|
			const auto custom = ranges::count_if(
 | 
						|
				result.some,
 | 
						|
				&Data::ReactionId::custom);
 | 
						|
			if (custom > args.allowedCustomReactions) {
 | 
						|
				args.askForBoosts(custom);
 | 
						|
				return;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		box->closeBox();
 | 
						|
		args.save(result);
 | 
						|
	});
 | 
						|
	box->addButton(tr::lng_cancel(), [=] {
 | 
						|
		box->closeBox();
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
void SaveAllowedReactions(
 | 
						|
		not_null<PeerData*> peer,
 | 
						|
		const Data::AllowedReactions &allowed) {
 | 
						|
	auto ids = allowed.some | ranges::views::transform(
 | 
						|
		Data::ReactionToMTP
 | 
						|
	) | ranges::to<QVector<MTPReaction>>;
 | 
						|
 | 
						|
	using Type = Data::AllowedReactionsType;
 | 
						|
	const auto updated = (allowed.type != Type::Some)
 | 
						|
		? MTP_chatReactionsAll(MTP_flags((allowed.type == Type::Default)
 | 
						|
			? MTPDchatReactionsAll::Flag(0)
 | 
						|
			: MTPDchatReactionsAll::Flag::f_allow_custom))
 | 
						|
		: allowed.some.empty()
 | 
						|
		? MTP_chatReactionsNone()
 | 
						|
		: MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));
 | 
						|
	peer->session().api().request(MTPmessages_SetChatAvailableReactions(
 | 
						|
		peer->input,
 | 
						|
		updated
 | 
						|
	)).done([=](const MTPUpdates &result) {
 | 
						|
		peer->session().api().applyUpdates(result);
 | 
						|
		if (const auto chat = peer->asChat()) {
 | 
						|
			chat->setAllowedReactions(Data::Parse(updated));
 | 
						|
		} else if (const auto channel = peer->asChannel()) {
 | 
						|
			channel->setAllowedReactions(Data::Parse(updated));
 | 
						|
		} else {
 | 
						|
			Unexpected("Invalid peer type in SaveAllowedReactions.");
 | 
						|
		}
 | 
						|
	}).fail([=](const MTP::Error &error) {
 | 
						|
		if (error.type() == u"REACTION_INVALID"_q) {
 | 
						|
			peer->updateFullForced();
 | 
						|
			peer->owner().reactions().refreshDefault();
 | 
						|
		}
 | 
						|
	}).send();
 | 
						|
}
 |