506 lines
14 KiB
C++
506 lines
14 KiB
C++
/*
|
|
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();
|
|
};
|
|
const auto media = item->media();
|
|
const auto onlyMessageText = media
|
|
&& (media->webpage()
|
|
|| media->game()
|
|
|| (!media->photo() && !media->document()));
|
|
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 request = StateRequest{
|
|
.flags = Ui::Text::StateRequest::Flag::LookupSymbol,
|
|
.onlyMessageText = onlyMessageText,
|
|
};
|
|
auto resolved = state->element->textState(
|
|
mouse->pos() - state->position,
|
|
request);
|
|
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
|