/* 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 "boxes/reactions_settings_box.h" // AddReactionAnimatedIcon #include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_selector.h" #include "data/data_message_reactions.h" #include "data/data_peer.h" #include "data/data_chat.h" #include "data/data_channel.h" #include "data/data_session.h" #include "main/main_session.h" #include "apiwrap.h" #include "lang/lang_keys.h" #include "ui/widgets/fields/input_field.h" #include "ui/controls/emoji_button.h" #include "ui/layers/generic_box.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/slide_wrap.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" #include "styles/style_chat_helpers.h" #include "styles/style_info.h" #include "styles/style_settings.h" #include #include #include namespace { [[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 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."); } struct UniqueCustomEmojiContext { base::flat_set ids; }; [[nodiscard]] bool RemoveNonCustomEmojiFragment( not_null document, UniqueCustomEmojiContext &context) { context.ids.clear(); 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); if (!context.ids.emplace(id.toULongLong()).second) { removeTill += fragment.length(); break; } } 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 document, UniqueCustomEmojiContext &context) { if (!RemoveNonCustomEmojiFragment(document, context)) { return false; } while (RemoveNonCustomEmojiFragment(document, context)) { } return true; } void SetupOnlyCustomEmojiField(not_null field) { field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor); field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook); struct State { bool processing = false; bool pending = false; }; const auto state = field->lifetime().make_state(); field->changes( ) | rpl::start_with_next([=] { state->pending = true; if (state->processing) { return; } auto context = UniqueCustomEmojiContext(); 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); } } if (changed) { field->forceProcessContentsChanges(); } }, field->lifetime()); } struct ReactionsSelectorArgs { not_null outer; not_null controller; rpl::producer title; std::vector list; std::vector selected; Fn)> callback; rpl::producer<> focusRequests; }; object_ptr AddReactionsSelector( not_null parent, ReactionsSelectorArgs &&args) { using namespace ChatHelpers; auto result = object_ptr( parent, st::manageGroupReactionsField, Ui::InputField::Mode::MultiLine, std::move(args.title)); const auto raw = result.data(); const auto customEmojiPaused = [controller = args.controller] { return controller->isGifPausedAtLeastFor(PauseReason::Layer); }; raw->setCustomEmojiFactory( args.controller->session().data().customEmojiManager().factory(), std::move(customEmojiPaused)); SetupOnlyCustomEmojiField(raw); std::move(args.focusRequests) | rpl::start_with_next([=] { raw->setFocusFast(); }, raw->lifetime()); const auto toggle = Ui::CreateChild( parent.get(), st::boxAttachEmoji); const auto panel = Ui::CreateChild( args.outer.get(), args.controller, object_ptr( nullptr, args.controller->uiShow(), Window::GifPauseReason::Layer, TabbedSelector::Mode::EmojiOnly)); 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 event) { const auto type = event->type(); if (type == QEvent::Move || type == QEvent::Resize) { scheduleUpdateEmojiPanelGeometry(); } return base::EventFilterResult::Continue; }; base::install_event_filter(args.outer, filterCallback); 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; } } // namespace void EditAllowedReactionsBox( not_null box, not_null navigation, bool isGroup, const std::vector &list, const Data::AllowedReactions &allowed, Fn callback) { using namespace Data; using namespace rpl::mappers; const auto iconHeight = st::editPeerReactionsPreview; box->setTitle(tr::lng_manage_peer_reactions()); enum class Option { All, Some, None, }; struct State { base::flat_map> toggles; rpl::variable