diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp index 91773b9fd..1ab14337f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp @@ -7,7 +7,10 @@ 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" @@ -16,14 +19,252 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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_settings.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, @@ -48,6 +289,7 @@ void EditAllowedReactionsBox( rpl::variable