Support selecting quote in reply info edit.

This commit is contained in:
John Preston 2023-10-24 13:46:43 +04:00
parent aad157cf56
commit d62fb5786d
21 changed files with 704 additions and 132 deletions

View File

@ -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

View File

@ -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"

View File

@ -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) {

View File

@ -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 {

View File

@ -428,125 +428,6 @@ void ClearDraftReplyTo(
}
}
void ShowReplyToChatBox(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void()> clearOldDraft) {
class Controller final : public ChooseRecipientBoxController {
public:
using Chosen = not_null<Data::Thread*>;
Controller(not_null<Main::Session*> session)
: ChooseRecipientBoxController(
session,
[=](Chosen thread) mutable { _singleChosen.fire_copy(thread); },
nullptr) {
}
void rowClicked(not_null<PeerListRow*> row) override final {
ChooseRecipientBoxController::rowClicked(row);
}
[[nodiscard]] rpl::producer<Chosen> 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<Chosen> _singleChosen;
};
struct State {
not_null<PeerListBox*> box;
not_null<Controller*> controller;
base::unique_qptr<Ui::PopupMenu> menu;
};
const auto session = &show->session();
const auto state = [&] {
auto controller = std::make_unique<Controller>(session);
const auto controllerRaw = controller.get();
auto box = Box<PeerListBox>(std::move(controller), nullptr);
const auto boxRaw = box.data();
show->show(std::move(box));
auto state = State{ boxRaw, controllerRaw };
return boxRaw->lifetime().make_state<State>(std::move(state));
}();
auto chosen = [=](not_null<Data::Thread*> 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<Data::Draft>(
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<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void()> highlight,
Fn<void()> clearOldDraft) {
show->show(Box([=](not_null<Ui::GenericBox*> 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<ChatHelpers::Show> show,
not_null<WebPageData*> webpage,

View File

@ -78,17 +78,6 @@ void ClearDraftReplyTo(
MsgId topicRootId,
FullMsgId equalTo);
void ShowReplyToChatBox(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void()> clearOldDraft = nullptr);
void EditReplyOptions(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void()> highlight,
Fn<void()> clearOldDraft = nullptr);
void EditWebPageOptions(
std::shared_ptr<ChatHelpers::Show> show,
not_null<WebPageData*> webpage,

View File

@ -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 <QtWidgets/QApplication>
#include <QtWidgets/QWidget>
namespace HistoryView::Controls {
namespace {
class PreviewDelegate final : public DefaultElementDelegate {
public:
PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update);
bool elementAnimationsPaused() override;
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
Context elementContext() override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
};
[[nodiscard]] std::unique_ptr<Ui::ChatTheme> DefaultThemeOn(
rpl::lifetime &lifetime) {
auto result = std::make_unique<Ui::ChatTheme>();
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<TextWithEntities> AddQuoteTracker(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
not_null<HistoryItem*> item,
const TextWithEntities &quote) {
struct State {
std::unique_ptr<Ui::ChatTheme> theme;
std::unique_ptr<Ui::ChatStyle> style;
std::unique_ptr<PreviewDelegate> delegate;
std::unique_ptr<Element> element;
rpl::variable<TextSelection> 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<Ui::RpWidget>(box),
QMargins(0, 0, 0, st::settingsThemesTopSkip));
const auto state = preview->lifetime().make_state<State>();
state->theme = DefaultThemeOn(preview->lifetime());
state->style = std::make_unique<Ui::ChatStyle>();
state->style->apply(state->theme.get());
state->delegate = std::make_unique<PreviewDelegate>(
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<const Element*> 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<QEvent*> e) {
const auto type = e->type();
const auto mouse = static_cast<QMouseEvent*>(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<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update)
: _parent(parent)
, _pathGradient(MakePathShiftGradient(st, update)) {
}
bool PreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
auto PreviewDelegate::elementPathShiftGradient()
-> not_null<Ui::PathShiftGradient*> {
return _pathGradient.get();
}
Context PreviewDelegate::elementContext() {
return Context::History;
}
} // namespace
void ShowReplyToChatBox(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void()> clearOldDraft) {
class Controller final : public ChooseRecipientBoxController {
public:
using Chosen = not_null<Data::Thread*>;
Controller(not_null<Main::Session*> session)
: ChooseRecipientBoxController(
session,
[=](Chosen thread) mutable { _singleChosen.fire_copy(thread); },
nullptr) {
}
void rowClicked(not_null<PeerListRow*> row) override final {
ChooseRecipientBoxController::rowClicked(row);
}
[[nodiscard]] rpl::producer<Chosen> 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<Chosen> _singleChosen;
};
struct State {
not_null<PeerListBox*> box;
not_null<Controller*> controller;
base::unique_qptr<Ui::PopupMenu> menu;
};
const auto session = &show->session();
const auto state = [&] {
auto controller = std::make_unique<Controller>(session);
const auto controllerRaw = controller.get();
auto box = Box<PeerListBox>(std::move(controller), nullptr);
const auto boxRaw = box.data();
show->show(std::move(box));
auto state = State{ boxRaw, controllerRaw };
return boxRaw->lifetime().make_state<State>(std::move(state));
}();
auto chosen = [=](not_null<Data::Thread*> 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<Data::Draft>(
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<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void(FullReplyTo)> done,
Fn<void()> highlight,
Fn<void()> clearOldDraft) {
const auto session = &show->session();
const auto item = session->data().message(reply.messageId);
if (!item) {
return;
}
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setWidth(st::boxWideWidth);
struct State {
rpl::variable<TextWithEntities> quote;
};
const auto state = box->lifetime().make_state<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<const HistoryItem*> removed) {
return removed == item;
}) | rpl::start_with_next([=] {
finish({});
}, box->lifetime());
}));
}
} // namespace HistoryView::Controls

View File

@ -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<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void(FullReplyTo)> done,
Fn<void()> highlight,
Fn<void()> clearOldDraft);
void ShowReplyToChatBox(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void()> clearOldDraft = nullptr);
} // namespace HistoryView::Controls

View File

@ -395,6 +395,11 @@ public:
virtual TextWithEntities selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const = 0;
virtual TextSelection selectionFromQuote(
const TextWithEntities &quote) const = 0;
virtual TextSelection selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const = 0;
[[nodiscard]] virtual TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const;

View File

@ -56,7 +56,7 @@ namespace {
constexpr auto kPlayStatusLimit = 2;
const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_";
std::optional<Window::SessionController*> ExtractController(
[[nodiscard]] std::optional<Window::SessionController*> ExtractController(
const ClickContext &context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
@ -65,6 +65,42 @@ std::optional<Window::SessionController*> ExtractController(
return std::nullopt;
}
[[nodiscard]] bool CheckQuoteEntities(
const EntitiesInText &quoteEntities,
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 &quote) 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 &quote) 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 {

View File

@ -99,6 +99,11 @@ public:
TextWithEntities selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const override;
TextSelection selectionFromQuote(
const TextWithEntities &quote) const override;
TextSelection selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const override;
TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;

View File

@ -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"

View File

@ -679,6 +679,17 @@ TextWithEntities Service::selectedQuote(
return {};
}
TextSelection Service::selectionFromQuote(
const TextWithEntities &quote) const {
return {};
}
TextSelection Service::selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const {
return {};
}
TextSelection Service::adjustSelection(
TextSelection selection,
TextSelectType type) const {

View File

@ -47,6 +47,11 @@ public:
TextWithEntities selectedQuote(
const Ui::Text::String &text,
TextSelection selection) const override;
TextSelection selectionFromQuote(
const TextWithEntities &quote) const override;
TextSelection selectionFromQuote(
const Ui::Text::String &text,
const TextWithEntities &quote) const override;
TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;

View File

@ -1226,6 +1226,24 @@ TextWithEntities Document::selectedQuote(TextSelection selection) const {
return {};
}
TextSelection Document::selectionFromQuote(
const TextWithEntities &quote) const {
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
const auto result = parent()->selectionFromQuote(
captioned->caption,
quote);
if (result.empty()) {
return {};
} else if (const auto voice = Get<HistoryDocumentVoice>()) {
return HistoryView::ShiftItemSelection(
result,
voice->transcribeText);
}
return result;
}
return {};
}
bool Document::uploading() const {
return _data->uploading();
}

View File

@ -47,6 +47,8 @@ public:
TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote(
const TextWithEntities &quote) const override;
bool uploading() const override;

View File

@ -1207,6 +1207,11 @@ TextWithEntities Gif::selectedQuote(TextSelection selection) const {
return parent()->selectedQuote(_caption, selection);
}
TextSelection Gif::selectionFromQuote(
const TextWithEntities &quote) const {
return parent()->selectionFromQuote(_caption, quote);
}
bool Gif::fullFeaturedGrouped(RectParts sides) const {
return (sides & RectPart::Left) && (sides & RectPart::Right);
}

View File

@ -69,6 +69,8 @@ public:
TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote(
const TextWithEntities &quote) const override;
bool uploading() const override;

View File

@ -92,6 +92,10 @@ public:
TextSelection selection) const {
return {};
}
[[nodiscard]] virtual TextSelection selectionFromQuote(
const TextWithEntities &quote) const {
return {};
}
[[nodiscard]] virtual bool isDisplayed() const;
virtual void updateNeedBubbleState() {

View File

@ -1053,6 +1053,11 @@ TextWithEntities Photo::selectedQuote(TextSelection selection) const {
return parent()->selectedQuote(_caption, selection);
}
TextSelection Photo::selectionFromQuote(
const TextWithEntities &quote) const {
return parent()->selectionFromQuote(_caption, quote);
}
void Photo::hideSpoilers() {
_caption.setSpoilerRevealed(false, anim::type::instant);
if (_spoiler) {

View File

@ -58,6 +58,8 @@ public:
TextForMimeData selectedText(TextSelection selection) const override;
TextWithEntities selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote(
const TextWithEntities &quote) const override;
PhotoData *getPhoto() const override {
return _data;