mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-18 07:07:08 +02:00
Support selecting quote in reply info edit.
This commit is contained in:
parent
aad157cf56
commit
d62fb5786d
21 changed files with 704 additions and 132 deletions
Telegram
CMakeLists.txt
SourceFiles/history
history_inner_widget.cpphistory_widget.cpp
view
controls
history_view_compose_controls.cpphistory_view_forward_panel.cpphistory_view_forward_panel.hhistory_view_reply_options.cpphistory_view_reply_options.h
history_view_element.hhistory_view_message.cpphistory_view_message.hhistory_view_replies_section.cpphistory_view_service_message.cpphistory_view_service_message.hmedia
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 "e) {
|
||||
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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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 "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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1226,6 +1226,24 @@ TextWithEntities Document::selectedQuote(TextSelection selection) const {
|
|||
return {};
|
||||
}
|
||||
|
||||
TextSelection Document::selectionFromQuote(
|
||||
const TextWithEntities "e) 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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue