diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 7b0d52e5f..d43e91d1f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -653,6 +653,8 @@ PRIVATE history/view/controls/history_view_compose_search.h history/view/controls/history_view_forward_panel.cpp history/view/controls/history_view_forward_panel.h + history/view/controls/history_view_reply_options.cpp + history/view/controls/history_view_reply_options.h history/view/controls/history_view_ttl_button.cpp history/view/controls/history_view_ttl_button.h history/view/controls/history_view_voice_record_bar.cpp diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 6f103704c..c1eb6cf98 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/view/controls/history_view_forward_panel.h" +#include "history/view/controls/history_view_reply_options.h" #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_web_page.h" diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 927055b6e..c3ac82ff9 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -82,6 +82,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_unread_things.h" #include "history/view/controls/history_view_compose_search.h" #include "history/view/controls/history_view_forward_panel.h" +#include "history/view/controls/history_view_reply_options.h" #include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_ttl_button.h" #include "history/view/reactions/history_view_reactions_button.h" @@ -6235,6 +6236,9 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { _forwardPanel->editOptions(controller()->uiShow()); } } else if (const auto reply = replyTo()) { + const auto done = [=](FullReplyTo replyTo) { + replyToMessage(replyTo); + }; const auto highlight = [=] { controller()->showPeerHistory( reply.messageId.peer, @@ -6246,6 +6250,7 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { EditReplyOptions( controller()->uiShow(), reply, + done, highlight, [=] { ClearDraftReplyTo(history, reply.messageId); }); } else if (_editMsgId) { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index b5d03eeca..098840e8a 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -46,9 +46,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/power_saving.h" #include "history/history.h" #include "history/history_item.h" +#include "history/view/controls/history_view_forward_panel.h" +#include "history/view/controls/history_view_reply_options.h" #include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_ttl_button.h" -#include "history/view/controls/history_view_forward_panel.h" #include "history/view/history_view_webpage_preview.h" #include "inline_bots/bot_attach_web_view.h" #include "inline_bots/inline_results_widget.h" @@ -624,12 +625,16 @@ void FieldHeader::init() { }; const auto history = _history; const auto topicRootId = _topicRootId; + const auto done = [=](FullReplyTo replyTo) { + replyToMessage(replyTo); + }; const auto clearOldReplyTo = [=, id = reply.messageId] { ClearDraftReplyTo(history, topicRootId, id); }; EditReplyOptions( _show, reply, + done, highlight, clearOldReplyTo); } else { diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp index eabef48c6..30869de20 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp @@ -428,125 +428,6 @@ void ClearDraftReplyTo( } } -void ShowReplyToChatBox( - std::shared_ptr show, - FullReplyTo reply, - Fn clearOldDraft) { - class Controller final : public ChooseRecipientBoxController { - public: - using Chosen = not_null; - - Controller(not_null session) - : ChooseRecipientBoxController( - session, - [=](Chosen thread) mutable { _singleChosen.fire_copy(thread); }, - nullptr) { - } - - void rowClicked(not_null row) override final { - ChooseRecipientBoxController::rowClicked(row); - } - - [[nodiscard]] rpl::producer singleChosen() const{ - return _singleChosen.events(); - } - - bool respectSavedMessagesChat() const override { - return false; - } - - private: - void prepareViewHook() override { - delegate()->peerListSetTitle(rpl::single(u"Reply in..."_q)); - } - - rpl::event_stream _singleChosen; - - }; - - struct State { - not_null box; - not_null controller; - base::unique_qptr menu; - }; - const auto session = &show->session(); - const auto state = [&] { - auto controller = std::make_unique(session); - const auto controllerRaw = controller.get(); - auto box = Box(std::move(controller), nullptr); - const auto boxRaw = box.data(); - show->show(std::move(box)); - auto state = State{ boxRaw, controllerRaw }; - return boxRaw->lifetime().make_state(std::move(state)); - }(); - - auto chosen = [=](not_null thread) mutable { - const auto history = thread->owningHistory(); - const auto topicRootId = thread->topicRootId(); - const auto draft = history->localDraft(topicRootId); - const auto textWithTags = draft - ? draft->textWithTags - : TextWithTags(); - const auto cursor = draft ? draft->cursor : MessageCursor(); - reply.topicRootId = topicRootId; - history->setLocalDraft(std::make_unique( - textWithTags, - reply, - cursor, - Data::WebPageDraft())); - history->clearLocalEditDraft(topicRootId); - history->session().changes().entryUpdated( - thread, - Data::EntryUpdate::Flag::LocalDraftSet); - - if (clearOldDraft) { - crl::on_main(&history->session(), clearOldDraft); - } - return true; - }; - auto callback = [=, chosen = std::move(chosen)]( - Controller::Chosen thread) mutable { - const auto weak = Ui::MakeWeak(state->box); - if (!chosen(thread)) { - return; - } else if (const auto strong = weak.data()) { - strong->closeBox(); - } - }; - state->controller->singleChosen( - ) | rpl::start_with_next(std::move(callback), state->box->lifetime()); -} - -void EditReplyOptions( - std::shared_ptr show, - FullReplyTo reply, - Fn highlight, - Fn clearOldDraft) { - show->show(Box([=](not_null box) { - box->setTitle(rpl::single(u"Reply to Message"_q)); - - Settings::AddButton( - box->verticalLayout(), - rpl::single(u"Reply in another chat"_q), - st::settingsButton, - { &st::menuIconReply } - )->setClickedCallback([=] { - ShowReplyToChatBox(show, reply, clearOldDraft); - }); - - Settings::AddButton( - box->verticalLayout(), - rpl::single(u"Show message"_q), - st::settingsButton, - { &st::menuIconShowInChat } - )->setClickedCallback(highlight); - - box->addButton(tr::lng_box_ok(), [=] { - box->closeBox(); - }); - })); -} - void EditWebPageOptions( std::shared_ptr show, not_null webpage, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h index c881dc06f..7e3d2fac4 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h @@ -78,17 +78,6 @@ void ClearDraftReplyTo( MsgId topicRootId, FullMsgId equalTo); -void ShowReplyToChatBox( - std::shared_ptr show, - FullReplyTo reply, - Fn clearOldDraft = nullptr); - -void EditReplyOptions( - std::shared_ptr show, - FullReplyTo reply, - Fn highlight, - Fn clearOldDraft = nullptr); - void EditWebPageOptions( std::shared_ptr show, not_null webpage, diff --git a/Telegram/SourceFiles/history/view/controls/history_view_reply_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_reply_options.cpp new file mode 100644 index 000000000..7d348beb9 --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_reply_options.cpp @@ -0,0 +1,496 @@ +/* +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 "history/view/controls/history_view_reply_options.h" + +#include "boxes/peer_list_box.h" +#include "boxes/peer_list_controllers.h" +#include "chat_helpers/compose/compose_show.h" +#include "data/data_changes.h" +#include "data/data_drafts.h" +#include "data/data_file_origin.h" +#include "data/data_session.h" +#include "data/data_thread.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "history/view/history_view_element.h" +#include "history/view/history_view_cursor_state.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/effects/path_shift_gradient.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/buttons.h" +#include "ui/painter.h" +#include "window/themes/window_theme.h" +#include "window/section_widget.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" +#include "styles/style_settings.h" + +#include +#include + +namespace HistoryView::Controls { +namespace { + +class PreviewDelegate final : public DefaultElementDelegate { +public: + PreviewDelegate( + not_null parent, + not_null st, + Fn update); + + bool elementAnimationsPaused() override; + not_null elementPathShiftGradient() override; + Context elementContext() override; + +private: + const not_null _parent; + const std::unique_ptr _pathGradient; + +}; + +[[nodiscard]] std::unique_ptr DefaultThemeOn( + rpl::lifetime &lifetime) { + auto result = std::make_unique(); + + using namespace Window::Theme; + const auto push = [=, raw = result.get()] { + const auto background = Background(); + const auto &paper = background->paper(); + raw->setBackground({ + .prepared = background->prepared(), + .preparedForTiled = background->preparedForTiled(), + .gradientForFill = background->gradientForFill(), + .colorForFill = background->colorForFill(), + .colors = paper.backgroundColors(), + .patternOpacity = paper.patternOpacity(), + .gradientRotation = paper.gradientRotation(), + .isPattern = paper.isPattern(), + .tile = background->tile(), + }); + }; + + push(); + Background()->updates( + ) | rpl::start_with_next([=](const BackgroundUpdate &update) { + if (update.type == BackgroundUpdate::Type::New + || update.type == BackgroundUpdate::Type::Changed) { + push(); + } + }, lifetime); + + return result; +} + +[[nodiscard]] rpl::producer AddQuoteTracker( + not_null box, + std::shared_ptr show, + not_null item, + const TextWithEntities "e) { + struct State { + std::unique_ptr theme; + std::unique_ptr style; + std::unique_ptr delegate; + std::unique_ptr element; + rpl::variable selection; + Ui::PeerUserpicView userpic; + QPoint position; + + base::Timer trippleClickTimer; + TextSelectType selectType = TextSelectType::Letters; + uint16 symbol = 0; + bool afterSymbol = false; + bool textCursor = false; + bool selecting = false; + bool over = false; + uint16 selectionStartSymbol = 0; + bool selectionStartAfterSymbol = false; + }; + + const auto preview = box->addRow( + object_ptr(box), + QMargins(0, 0, 0, st::settingsThemesTopSkip)); + const auto state = preview->lifetime().make_state(); + state->theme = DefaultThemeOn(preview->lifetime()); + + state->style = std::make_unique(); + state->style->apply(state->theme.get()); + + state->delegate = std::make_unique( + box, + state->style.get(), + [=] { preview->update(); }); + + state->element = item->createView(state->delegate.get()); + state->element->initDimensions(); + state->position = QPoint(0, st::msgMargin.bottom()); + + state->selection = state->element->selectionFromQuote(quote); + + const auto session = &show->session(); + session->data().viewRepaintRequest( + ) | rpl::start_with_next([=](not_null view) { + if (view == state->element.get()) { + preview->update(); + } + }, preview->lifetime()); + + state->selection.changes() | rpl::start_with_next([=] { + preview->update(); + }, preview->lifetime()); + + const auto resolveNewSelection = [=] { + const auto make = [](uint16 symbol, bool afterSymbol) { + return uint16(symbol + (afterSymbol ? 1 : 0)); + }; + const auto first = make(state->symbol, state->afterSymbol); + const auto second = make( + state->selectionStartSymbol, + state->selectionStartAfterSymbol); + const auto result = (first <= second) + ? TextSelection{ first, second } + : TextSelection{ second, first }; + return state->element->adjustSelection(result, state->selectType); + }; + const auto startSelection = [=](TextSelectType type) { + if (state->selecting && state->selectType >= type) { + return; + } + state->selecting = true; + state->selectType = type; + state->selectionStartSymbol = state->symbol; + state->selectionStartAfterSymbol = state->afterSymbol; + if (!state->textCursor) { + preview->setCursor(style::cur_text); + } + preview->update(); + }; + preview->setMouseTracking(true); + preview->events() | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + const auto mouse = static_cast(e.get()); + if (type == QEvent::MouseMove) { + auto resolved = state->element->textState( + mouse->pos() - state->position, + { .flags = Ui::Text::StateRequest::Flag::LookupSymbol }); + state->over = true; + const auto text = (resolved.cursor == CursorState::Text); + if (state->textCursor != text) { + state->textCursor = text; + preview->setCursor((text || state->selecting) + ? style::cur_text + : style::cur_default); + } + if (state->symbol != resolved.symbol + || state->afterSymbol != resolved.afterSymbol) { + state->symbol = resolved.symbol; + state->afterSymbol = resolved.afterSymbol; + if (state->selecting) { + preview->update(); + } + } + } else if (type == QEvent::Leave && state->over) { + state->over = false; + if (state->textCursor) { + state->textCursor = false; + if (!state->selecting) { + preview->setCursor(style::cur_default); + } + } + } else if (type == QEvent::MouseButtonDblClick && state->over) { + startSelection(TextSelectType::Words); + state->trippleClickTimer.callOnce( + QApplication::doubleClickInterval()); + } else if (type == QEvent::MouseButtonPress && state->over) { + startSelection(state->trippleClickTimer.isActive() + ? TextSelectType::Paragraphs + : TextSelectType::Letters); + } else if (type == QEvent::MouseButtonRelease && state->selecting) { + const auto result = resolveNewSelection(); + state->selecting = false; + state->selectType = TextSelectType::Letters; + state->selection = result; + if (!state->textCursor) { + preview->setCursor(style::cur_default); + } + } + }, preview->lifetime()); + + preview->widthValue( + ) | rpl::filter([=](int width) { + return width > st::msgMinWidth; + }) | rpl::start_with_next([=](int width) { + const auto height = state->element->resizeGetHeight(width) + + state->position.y() + + st::msgMargin.top(); + preview->resize(width, height); + }, preview->lifetime()); + + preview->paintRequest() | rpl::start_with_next([=](QRect clip) { + Window::SectionWidget::PaintBackground( + state->theme.get(), + preview, + preview->window()->height(), + 0, + clip); + + auto p = Painter(preview); + auto hq = PainterHighQualityEnabler(p); + p.translate(state->position); + auto context = state->theme->preparePaintContext( + state->style.get(), + preview->rect(), + clip, + !box->window()->isActiveWindow()); + context.outbg = state->element->hasOutLayout(); + context.selection = state->selecting + ? resolveNewSelection() + : state->selection.current(); + state->element->draw(p, context); + if (state->element->displayFromPhoto()) { + auto userpicMinBottomSkip = st::historyPaddingBottom + + st::msgMargin.bottom(); + auto userpicBottom = preview->height() + - state->element->marginBottom() + - state->element->marginTop(); + const auto userpicTop = userpicBottom - st::msgPhotoSize; + if (const auto from = item->displayFrom()) { + from->paintUserpicLeft( + p, + state->userpic, + st::historyPhotoLeft, + userpicTop, + preview->width(), + st::msgPhotoSize); + } else if (const auto info = item->hiddenSenderInfo()) { + if (info->customUserpic.empty()) { + info->emptyUserpic.paintCircle( + p, + st::historyPhotoLeft, + userpicTop, + preview->width(), + st::msgPhotoSize); + } else { + const auto valid = info->paintCustomUserpic( + p, + state->userpic, + st::historyPhotoLeft, + userpicTop, + preview->width(), + st::msgPhotoSize); + if (!valid) { + info->customUserpic.load(session, item->fullId()); + } + } + } else { + Unexpected("Corrupt forwarded information in message."); + } + } + }, preview->lifetime()); + + return state->selection.value( + ) | rpl::map([=](TextSelection selection) { + return state->element->selectedQuote(selection); + }); +} + +PreviewDelegate::PreviewDelegate( + not_null parent, + not_null st, + Fn update) +: _parent(parent) +, _pathGradient(MakePathShiftGradient(st, update)) { +} + +bool PreviewDelegate::elementAnimationsPaused() { + return _parent->window()->isActiveWindow(); +} + +auto PreviewDelegate::elementPathShiftGradient() +-> not_null { + return _pathGradient.get(); +} + +Context PreviewDelegate::elementContext() { + return Context::History; +} + +} // namespace + +void ShowReplyToChatBox( + std::shared_ptr show, + FullReplyTo reply, + Fn clearOldDraft) { + class Controller final : public ChooseRecipientBoxController { + public: + using Chosen = not_null; + + Controller(not_null session) + : ChooseRecipientBoxController( + session, + [=](Chosen thread) mutable { _singleChosen.fire_copy(thread); }, + nullptr) { + } + + void rowClicked(not_null row) override final { + ChooseRecipientBoxController::rowClicked(row); + } + + [[nodiscard]] rpl::producer singleChosen() const{ + return _singleChosen.events(); + } + + bool respectSavedMessagesChat() const override { + return false; + } + + private: + void prepareViewHook() override { + delegate()->peerListSetTitle(rpl::single(u"Reply in..."_q)); + } + + rpl::event_stream _singleChosen; + + }; + + struct State { + not_null box; + not_null controller; + base::unique_qptr menu; + }; + const auto session = &show->session(); + const auto state = [&] { + auto controller = std::make_unique(session); + const auto controllerRaw = controller.get(); + auto box = Box(std::move(controller), nullptr); + const auto boxRaw = box.data(); + show->show(std::move(box)); + auto state = State{ boxRaw, controllerRaw }; + return boxRaw->lifetime().make_state(std::move(state)); + }(); + + auto chosen = [=](not_null thread) mutable { + const auto history = thread->owningHistory(); + const auto topicRootId = thread->topicRootId(); + const auto draft = history->localDraft(topicRootId); + const auto textWithTags = draft + ? draft->textWithTags + : TextWithTags(); + const auto cursor = draft ? draft->cursor : MessageCursor(); + reply.topicRootId = topicRootId; + history->setLocalDraft(std::make_unique( + textWithTags, + reply, + cursor, + Data::WebPageDraft())); + history->clearLocalEditDraft(topicRootId); + history->session().changes().entryUpdated( + thread, + Data::EntryUpdate::Flag::LocalDraftSet); + + if (clearOldDraft) { + crl::on_main(&history->session(), clearOldDraft); + } + return true; + }; + auto callback = [=, chosen = std::move(chosen)]( + Controller::Chosen thread) mutable { + const auto weak = Ui::MakeWeak(state->box); + if (!chosen(thread)) { + return; + } else if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + state->controller->singleChosen( + ) | rpl::start_with_next(std::move(callback), state->box->lifetime()); +} + +void EditReplyOptions( + std::shared_ptr show, + FullReplyTo reply, + Fn done, + Fn highlight, + Fn clearOldDraft) { + const auto session = &show->session(); + const auto item = session->data().message(reply.messageId); + if (!item) { + return; + } + show->show(Box([=](not_null box) { + box->setWidth(st::boxWideWidth); + + struct State { + rpl::variable quote; + }; + const auto state = box->lifetime().make_state(); + state->quote = AddQuoteTracker(box, show, item, reply.quote); + + box->setTitle(reply.quote.empty() + ? rpl::single(u"Reply to Message"_q) + : rpl::single(u"Update Quote"_q)); + + Settings::AddButton( + box->verticalLayout(), + rpl::single(u"Reply in another chat"_q), + st::settingsButton, + { &st::menuIconReply } + )->setClickedCallback([=] { + ShowReplyToChatBox(show, reply, clearOldDraft); + }); + + Settings::AddButton( + box->verticalLayout(), + rpl::single(u"Show message"_q), + st::settingsButton, + { &st::menuIconShowInChat } + )->setClickedCallback(highlight); + + const auto finish = [=](FullReplyTo result) { + const auto weak = Ui::MakeWeak(box); + done(std::move(result)); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }; + + Settings::AddButton( + box->verticalLayout(), + rpl::single(u"Remove reply"_q), + st::settingsAttentionButtonWithIcon, + { &st::menuIconDeleteAttention } + )->setClickedCallback([=] { + finish({}); + }); + + box->addButton(rpl::single(u"Apply"_q), [=] { + auto result = reply; + result.quote = state->quote.current(); + finish(result); + }); + + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + + session->data().itemRemoved( + ) | rpl::filter([=](not_null removed) { + return removed == item; + }) | rpl::start_with_next([=] { + finish({}); + }, box->lifetime()); + })); +} + +} // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_reply_options.h b/Telegram/SourceFiles/history/view/controls/history_view_reply_options.h new file mode 100644 index 000000000..048fd8eba --- /dev/null +++ b/Telegram/SourceFiles/history/view/controls/history_view_reply_options.h @@ -0,0 +1,32 @@ +/* +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 +*/ +#pragma once + +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + +namespace Window { +class SessionController; +} // namespace Window + +namespace HistoryView::Controls { + +void EditReplyOptions( + std::shared_ptr show, + FullReplyTo reply, + Fn done, + Fn highlight, + Fn clearOldDraft); + +void ShowReplyToChatBox( + std::shared_ptr show, + FullReplyTo reply, + Fn clearOldDraft = nullptr); + +} // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 3cc0903ff..b51487703 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -395,6 +395,11 @@ public: virtual TextWithEntities selectedQuote( const Ui::Text::String &text, TextSelection selection) const = 0; + virtual TextSelection selectionFromQuote( + const TextWithEntities "e) const = 0; + virtual TextSelection selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const = 0; [[nodiscard]] virtual TextSelection adjustSelection( TextSelection selection, TextSelectType type) const; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index b89a6385a..142020884 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -56,7 +56,7 @@ namespace { constexpr auto kPlayStatusLimit = 2; const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_"; -std::optional ExtractController( +[[nodiscard]] std::optional ExtractController( const ClickContext &context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { @@ -65,6 +65,42 @@ std::optional ExtractController( return std::nullopt; } +[[nodiscard]] bool CheckQuoteEntities( + const EntitiesInText "eEntities, + const TextWithEntities &original, + TextSelection selection) { + auto left = quoteEntities; + const auto allowed = std::array{ + EntityType::Bold, + EntityType::Italic, + EntityType::Underline, + EntityType::StrikeOut, + EntityType::Spoiler, + EntityType::CustomEmoji, + }; + for (const auto &entity : original.entities) { + const auto from = entity.offset(); + const auto till = from + entity.length(); + if (till <= selection.from || from >= selection.to) { + continue; + } + const auto quoteFrom = std::max(from, int(selection.from)); + const auto quoteTill = std::min(till, int(selection.to)); + const auto cut = EntityInText( + entity.type(), + quoteFrom - int(selection.from), + quoteTill - quoteFrom, + entity.data()); + const auto i = ranges::find(left, cut); + if (i != left.end()) { + left.erase(i); + } else if (!ranges::contains(allowed, cut.type())) { + return false; + } + } + return left.empty(); +}; + class KeyboardStyle : public ReplyKeyboard::Style { public: KeyboardStyle(const style::BotKeyboardButton &st); @@ -2674,6 +2710,66 @@ TextWithEntities Message::selectedQuote( return result; } +TextSelection Message::selectionFromQuote( + const TextWithEntities "e) const { + const auto item = data(); + const auto &translated = item->translatedText(); + const auto &original = item->originalText(); + if (&translated != &original || quote.empty()) { + return {}; + } else if (hasVisibleText()) { + return selectionFromQuote(text(), quote); + } else if (const auto media = this->media()) { + if (media->isDisplayed() || isHiddenByGroup()) { + return media->selectionFromQuote(quote); + } + } + return {}; +} + +TextSelection Message::selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const { + if (quote.empty()) { + return {}; + } + const auto &original = data()->originalText(); + auto result = TextSelection(); + auto offset = 0; + while (true) { + const auto i = original.text.indexOf(quote.text, offset); + if (i < 0) { + return {}; + } + auto selection = TextSelection{ + uint16(i), + uint16(i + quote.text.size()), + }; + if (CheckQuoteEntities(quote.entities, original, selection)) { + result = selection; + break; + } + offset = i + 1; + } + const auto &modifications = text.modifications(); + //for (const auto &modification : text.modifications()) { + // if (modification.position >= selection.to) { + // break; + // } else if (modification.position <= selection.from) { + // modified.from += modification.skipped; + // if (modification.added + // && modification.position < selection.from) { + // --modified.from; + // } + // } + // modified.to += modification.skipped; + // if (modification.added && modified.to > modified.from) { + // --modified.to; + // } + //} + return result; +} + TextSelection Message::adjustSelection( TextSelection selection, TextSelectType type) const { diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 33c1b6607..7506bf355 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -99,6 +99,11 @@ public: TextWithEntities selectedQuote( const Ui::Text::String &text, TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; + TextSelection selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const override; TextSelection adjustSelection( TextSelection selection, TextSelectType type) const override; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 162243feb..faf393999 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/controls/history_view_compose_controls.h" #include "history/view/controls/history_view_forward_panel.h" +#include "history/view/controls/history_view_reply_options.h" #include "history/view/history_view_top_bar_widget.h" #include "history/view/history_view_list_widget.h" #include "history/view/history_view_schedule_box.h" diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.cpp b/Telegram/SourceFiles/history/view/history_view_service_message.cpp index 983d7ff11..a3d6550c5 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_service_message.cpp @@ -679,6 +679,17 @@ TextWithEntities Service::selectedQuote( return {}; } +TextSelection Service::selectionFromQuote( + const TextWithEntities "e) const { + return {}; +} + +TextSelection Service::selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const { + return {}; +} + TextSelection Service::adjustSelection( TextSelection selection, TextSelectType type) const { diff --git a/Telegram/SourceFiles/history/view/history_view_service_message.h b/Telegram/SourceFiles/history/view/history_view_service_message.h index d40a69c8b..c862ce657 100644 --- a/Telegram/SourceFiles/history/view/history_view_service_message.h +++ b/Telegram/SourceFiles/history/view/history_view_service_message.h @@ -47,6 +47,11 @@ public: TextWithEntities selectedQuote( const Ui::Text::String &text, TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; + TextSelection selectionFromQuote( + const Ui::Text::String &text, + const TextWithEntities "e) const override; TextSelection adjustSelection( TextSelection selection, TextSelectType type) const override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 071b6bb3d..5e45b8877 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -1226,6 +1226,24 @@ TextWithEntities Document::selectedQuote(TextSelection selection) const { return {}; } +TextSelection Document::selectionFromQuote( + const TextWithEntities "e) const { + if (const auto captioned = Get()) { + const auto result = parent()->selectionFromQuote( + captioned->caption, + quote); + if (result.empty()) { + return {}; + } else if (const auto voice = Get()) { + return HistoryView::ShiftItemSelection( + result, + voice->transcribeText); + } + return result; + } + return {}; +} + bool Document::uploading() const { return _data->uploading(); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.h b/Telegram/SourceFiles/history/view/media/history_view_document.h index 6084820eb..31dd9886a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_document.h @@ -47,6 +47,8 @@ public: TextForMimeData selectedText(TextSelection selection) const override; TextWithEntities selectedQuote(TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; bool uploading() const override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp index b30035efa..70a81264d 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.cpp @@ -1207,6 +1207,11 @@ TextWithEntities Gif::selectedQuote(TextSelection selection) const { return parent()->selectedQuote(_caption, selection); } +TextSelection Gif::selectionFromQuote( + const TextWithEntities "e) const { + return parent()->selectionFromQuote(_caption, quote); +} + bool Gif::fullFeaturedGrouped(RectParts sides) const { return (sides & RectPart::Left) && (sides & RectPart::Right); } diff --git a/Telegram/SourceFiles/history/view/media/history_view_gif.h b/Telegram/SourceFiles/history/view/media/history_view_gif.h index 9a81457eb..fdba9a0af 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_gif.h +++ b/Telegram/SourceFiles/history/view/media/history_view_gif.h @@ -69,6 +69,8 @@ public: TextForMimeData selectedText(TextSelection selection) const override; TextWithEntities selectedQuote(TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; bool uploading() const override; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index ea60e186f..bda57224e 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -92,6 +92,10 @@ public: TextSelection selection) const { return {}; } + [[nodiscard]] virtual TextSelection selectionFromQuote( + const TextWithEntities "e) const { + return {}; + } [[nodiscard]] virtual bool isDisplayed() const; virtual void updateNeedBubbleState() { diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp index 9d09faf17..81dee1c54 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.cpp @@ -1053,6 +1053,11 @@ TextWithEntities Photo::selectedQuote(TextSelection selection) const { return parent()->selectedQuote(_caption, selection); } +TextSelection Photo::selectionFromQuote( + const TextWithEntities "e) const { + return parent()->selectionFromQuote(_caption, quote); +} + void Photo::hideSpoilers() { _caption.setSpoilerRevealed(false, anim::type::instant); if (_spoiler) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_photo.h b/Telegram/SourceFiles/history/view/media/history_view_photo.h index 76c1ff6ad..b76bbe264 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_photo.h +++ b/Telegram/SourceFiles/history/view/media/history_view_photo.h @@ -58,6 +58,8 @@ public: TextForMimeData selectedText(TextSelection selection) const override; TextWithEntities selectedQuote(TextSelection selection) const override; + TextSelection selectionFromQuote( + const TextWithEntities "e) const override; PhotoData *getPhoto() const override { return _data;