init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
575
Telegram/SourceFiles/support/support_autocomplete.cpp
Normal file
575
Telegram/SourceFiles/support/support_autocomplete.cpp
Normal file
@@ -0,0 +1,575 @@
|
||||
/*
|
||||
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 "support/support_autocomplete.h"
|
||||
|
||||
#include "ui/chat/chat_theme.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "support/support_templates.h"
|
||||
#include "support/support_common.h"
|
||||
#include "history/view/history_view_message.h"
|
||||
#include "history/view/history_view_service_message.h"
|
||||
#include "history/history_item.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_session_settings.h"
|
||||
#include "apiwrap.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_window.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Support {
|
||||
namespace {
|
||||
|
||||
class Inner : public Ui::RpWidget {
|
||||
public:
|
||||
Inner(QWidget *parent);
|
||||
|
||||
using Question = details::TemplatesQuestion;
|
||||
void showRows(std::vector<Question> &&rows);
|
||||
|
||||
std::pair<int, int> moveSelection(int delta);
|
||||
|
||||
std::optional<Question> selected() const;
|
||||
|
||||
auto activated() const {
|
||||
return _activated.events();
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
private:
|
||||
struct Row {
|
||||
Question data;
|
||||
Ui::Text::String question = { st::windowMinWidth / 2 };
|
||||
Ui::Text::String keys = { st::windowMinWidth / 2 };
|
||||
Ui::Text::String answer = { st::windowMinWidth / 2 };
|
||||
int top = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
void prepareRow(Row &row);
|
||||
int resizeRowGetHeight(Row &row, int newWidth);
|
||||
void setSelected(int selected);
|
||||
|
||||
std::vector<Row> _rows;
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
bool _selectByKeys = false;
|
||||
rpl::event_stream<> _activated;
|
||||
|
||||
};
|
||||
|
||||
int TextHeight(const Ui::Text::String &text, int available, int lines) {
|
||||
Expects(text.style() != nullptr);
|
||||
|
||||
const auto st = text.style();
|
||||
const auto line = st->lineHeight ? st->lineHeight : st->font->height;
|
||||
return std::min(text.countHeight(available), lines * line);
|
||||
};
|
||||
|
||||
Inner::Inner(QWidget *parent) : RpWidget(parent) {
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void Inner::showRows(std::vector<Question> &&rows) {
|
||||
_rows.resize(0);
|
||||
_rows.reserve(rows.size());
|
||||
for (auto &row : rows) {
|
||||
_rows.push_back({ std::move(row) });
|
||||
auto &added = _rows.back();
|
||||
prepareRow(added);
|
||||
}
|
||||
resizeToWidth(width());
|
||||
_selected = _pressed = -1;
|
||||
moveSelection(1);
|
||||
update();
|
||||
}
|
||||
|
||||
std::pair<int, int> Inner::moveSelection(int delta) {
|
||||
const auto selected = _selected + delta;
|
||||
if (selected >= 0 && selected < _rows.size()) {
|
||||
_selectByKeys = true;
|
||||
setSelected(selected);
|
||||
const auto top = _rows[_selected].top;
|
||||
return { top, top + _rows[_selected].height };
|
||||
}
|
||||
return { -1, -1 };
|
||||
}
|
||||
|
||||
auto Inner::selected() const -> std::optional<Question> {
|
||||
if (_rows.empty()) {
|
||||
return std::nullopt;
|
||||
} else if (_selected < 0) {
|
||||
return _rows[0].data;
|
||||
}
|
||||
return _rows[_selected].data;
|
||||
}
|
||||
|
||||
void Inner::prepareRow(Row &row) {
|
||||
row.question.setText(st::autocompleteRowTitle, row.data.question);
|
||||
row.keys.setText(
|
||||
st::autocompleteRowKeys,
|
||||
row.data.originalKeys.join(u", "_q));
|
||||
row.answer.setText(st::autocompleteRowAnswer, row.data.value);
|
||||
}
|
||||
|
||||
int Inner::resizeRowGetHeight(Row &row, int newWidth) {
|
||||
const auto available = newWidth
|
||||
- st::autocompleteRowPadding.left()
|
||||
- st::autocompleteRowPadding.right();
|
||||
return row.height = st::autocompleteRowPadding.top()
|
||||
+ TextHeight(row.question, available, 1)
|
||||
+ TextHeight(row.keys, available, 1)
|
||||
+ TextHeight(row.answer, available, 2)
|
||||
+ st::autocompleteRowPadding.bottom()
|
||||
+ st::lineWidth;
|
||||
}
|
||||
|
||||
int Inner::resizeGetHeight(int newWidth) {
|
||||
auto top = 0;
|
||||
for (auto &row : _rows) {
|
||||
row.top = top;
|
||||
top += resizeRowGetHeight(row, newWidth);
|
||||
}
|
||||
return top ? (top - st::lineWidth) : (3 * st::mentionHeight);
|
||||
}
|
||||
|
||||
void Inner::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
if (_rows.empty()) {
|
||||
p.setFont(st::boxTextFont);
|
||||
p.setPen(st::windowSubTextFg);
|
||||
p.drawText(
|
||||
rect(),
|
||||
"Search by question, keys or value",
|
||||
style::al_center);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto clip = e->rect();
|
||||
const auto from = ranges::upper_bound(
|
||||
_rows,
|
||||
clip.y(),
|
||||
std::less<>(),
|
||||
[](const Row &row) { return row.top + row.height; });
|
||||
const auto till = ranges::lower_bound(
|
||||
_rows,
|
||||
clip.y() + clip.height(),
|
||||
std::less<>(),
|
||||
[](const Row &row) { return row.top; });
|
||||
if (from == end(_rows)) {
|
||||
return;
|
||||
}
|
||||
p.translate(0, from->top);
|
||||
const auto padding = st::autocompleteRowPadding;
|
||||
const auto available = width() - padding.left() - padding.right();
|
||||
auto top = padding.top();
|
||||
const auto drawText = [&](const Ui::Text::String &text, int lines) {
|
||||
text.drawLeftElided(
|
||||
p,
|
||||
padding.left(),
|
||||
top,
|
||||
available,
|
||||
width(),
|
||||
lines);
|
||||
top += TextHeight(text, available, lines);
|
||||
};
|
||||
for (auto i = from; i != till; ++i) {
|
||||
const auto over = (i - begin(_rows) == _selected);
|
||||
if (over) {
|
||||
p.fillRect(0, 0, width(), i->height, st::windowBgOver);
|
||||
}
|
||||
p.setPen(st::mentionNameFg);
|
||||
drawText(i->question, 1);
|
||||
p.setPen(over ? st::mentionFgOver : st::mentionFg);
|
||||
drawText(i->keys, 1);
|
||||
p.setPen(st::windowFg);
|
||||
drawText(i->answer, 2);
|
||||
|
||||
p.translate(0, i->height);
|
||||
top = padding.top();
|
||||
|
||||
if (i - begin(_rows) + 1 == _selected) {
|
||||
p.fillRect(
|
||||
0,
|
||||
-st::lineWidth,
|
||||
width(),
|
||||
st::lineWidth,
|
||||
st::windowBgOver);
|
||||
} else if (!over) {
|
||||
p.fillRect(
|
||||
padding.left(),
|
||||
-st::lineWidth,
|
||||
available,
|
||||
st::lineWidth,
|
||||
st::shadowFg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::mouseMoveEvent(QMouseEvent *e) {
|
||||
static auto lastGlobalPos = QPoint();
|
||||
const auto moved = (e->globalPos() != lastGlobalPos);
|
||||
if (!moved && _selectByKeys) {
|
||||
return;
|
||||
}
|
||||
_selectByKeys = false;
|
||||
lastGlobalPos = e->globalPos();
|
||||
const auto i = ranges::upper_bound(
|
||||
_rows,
|
||||
e->pos().y(),
|
||||
std::less<>(),
|
||||
[](const Row &row) { return row.top + row.height; });
|
||||
setSelected((i == end(_rows)) ? -1 : (i - begin(_rows)));
|
||||
}
|
||||
|
||||
void Inner::leaveEventHook(QEvent *e) {
|
||||
setSelected(-1);
|
||||
}
|
||||
|
||||
void Inner::setSelected(int selected) {
|
||||
if (_selected != selected) {
|
||||
_selected = selected;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void Inner::mousePressEvent(QMouseEvent *e) {
|
||||
_pressed = _selected;
|
||||
}
|
||||
|
||||
void Inner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
const auto pressed = base::take(_pressed);
|
||||
if (pressed == _selected && pressed >= 0) {
|
||||
_activated.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
AdminLog::OwnedItem GenerateCommentItem(
|
||||
not_null<HistoryView::ElementDelegate*> delegate,
|
||||
not_null<History*> history,
|
||||
const Contact &data) {
|
||||
if (data.comment.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto item = history->makeMessage({
|
||||
.id = history->nextNonHistoryEntryId(),
|
||||
.flags = (MessageFlag::HasFromId
|
||||
| MessageFlag::Outgoing
|
||||
| MessageFlag::FakeHistoryItem),
|
||||
.from = history->session().userPeerId(),
|
||||
.date = base::unixtime::now(),
|
||||
}, TextWithEntities{ data.comment }, MTP_messageMediaEmpty());
|
||||
return AdminLog::OwnedItem(delegate, item);
|
||||
}
|
||||
|
||||
AdminLog::OwnedItem GenerateContactItem(
|
||||
not_null<HistoryView::ElementDelegate*> delegate,
|
||||
not_null<History*> history,
|
||||
const Contact &data) {
|
||||
const auto item = history->makeMessage({
|
||||
.id = history->nextNonHistoryEntryId(),
|
||||
.flags = (MessageFlag::HasFromId
|
||||
| MessageFlag::Outgoing
|
||||
| MessageFlag::FakeHistoryItem),
|
||||
.from = history->session().userPeerId(),
|
||||
.date = base::unixtime::now(),
|
||||
}, TextWithEntities(), MTP_messageMediaContact(
|
||||
MTP_string(data.phone),
|
||||
MTP_string(data.firstName),
|
||||
MTP_string(data.lastName),
|
||||
MTP_string(), // vcard
|
||||
MTP_long(0))); // user_id
|
||||
return AdminLog::OwnedItem(delegate, item);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Autocomplete::Autocomplete(QWidget *parent, not_null<Main::Session*> session)
|
||||
: RpWidget(parent)
|
||||
, _session(session) {
|
||||
setupContent();
|
||||
}
|
||||
|
||||
void Autocomplete::activate(not_null<Ui::InputField*> field) {
|
||||
if (_session->settings().supportTemplatesAutocomplete()) {
|
||||
_activate();
|
||||
} else {
|
||||
const auto &templates = _session->supportTemplates();
|
||||
const auto max = templates.maxKeyLength();
|
||||
auto cursor = field->textCursor();
|
||||
const auto position = cursor.position();
|
||||
const auto anchor = cursor.anchor();
|
||||
const auto text = (position != anchor)
|
||||
? field->getTextWithTagsPart(
|
||||
std::min(position, anchor),
|
||||
std::max(position, anchor))
|
||||
: field->getTextWithTagsPart(
|
||||
std::max(position - max, 0),
|
||||
position);
|
||||
const auto result = (position != anchor)
|
||||
? templates.matchExact(text.text)
|
||||
: templates.matchFromEnd(text.text);
|
||||
if (result) {
|
||||
const auto till = std::max(position, anchor);
|
||||
const auto from = till - result->key.size();
|
||||
cursor.setPosition(from);
|
||||
cursor.setPosition(till, QTextCursor::KeepAnchor);
|
||||
field->setTextCursor(cursor);
|
||||
submitValue(result->question.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Autocomplete::deactivate() {
|
||||
_deactivate();
|
||||
}
|
||||
|
||||
void Autocomplete::setBoundings(QRect rect) {
|
||||
const auto maxHeight = int(4.5 * st::mentionHeight);
|
||||
const auto height = std::min(rect.height(), maxHeight);
|
||||
setGeometry(
|
||||
rect.x(),
|
||||
rect.y() + rect.height() - height,
|
||||
rect.width(),
|
||||
height);
|
||||
}
|
||||
|
||||
rpl::producer<QString> Autocomplete::insertRequests() const {
|
||||
return _insertRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<Contact> Autocomplete::shareContactRequests() const {
|
||||
return _shareContactRequests.events();
|
||||
}
|
||||
|
||||
void Autocomplete::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Up) {
|
||||
_moveSelection(-1);
|
||||
} else if (e->key() == Qt::Key_Down) {
|
||||
_moveSelection(1);
|
||||
}
|
||||
}
|
||||
|
||||
void Autocomplete::setupContent() {
|
||||
const auto inputWrap = Ui::CreateChild<Ui::PaddingWrap<Ui::InputField>>(
|
||||
this,
|
||||
object_ptr<Ui::InputField>(
|
||||
this,
|
||||
st::defaultMultiSelectSearchField,
|
||||
rpl::single(u"Search for templates"_q)), // #TODO hard_lang
|
||||
st::autocompleteSearchPadding);
|
||||
const auto input = inputWrap->entity();
|
||||
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(this);
|
||||
|
||||
const auto inner = scroll->setOwnedWidget(object_ptr<Inner>(scroll));
|
||||
|
||||
const auto submit = [=] {
|
||||
if (const auto question = inner->selected()) {
|
||||
submitValue(question->value);
|
||||
}
|
||||
};
|
||||
|
||||
const auto refresh = [=] {
|
||||
inner->showRows(
|
||||
_session->supportTemplates().query(input->getLastText()));
|
||||
scroll->scrollToY(0);
|
||||
};
|
||||
|
||||
inner->activated() | rpl::on_next(submit, lifetime());
|
||||
input->focusedChanges(
|
||||
) | rpl::filter(!rpl::mappers::_1) | rpl::on_next([=] {
|
||||
base::call_delayed(10, this, [=] {
|
||||
if (!input->hasFocus()) {
|
||||
deactivate();
|
||||
}
|
||||
});
|
||||
}, input->lifetime());
|
||||
input->cancelled(
|
||||
) | rpl::on_next([=] {
|
||||
deactivate();
|
||||
}, input->lifetime());
|
||||
input->changes() | rpl::on_next(refresh, input->lifetime());
|
||||
input->submits() | rpl::on_next(submit, input->lifetime());
|
||||
input->customUpDown(true);
|
||||
|
||||
_activate = [=] {
|
||||
input->setText(QString());
|
||||
show();
|
||||
input->setFocus();
|
||||
};
|
||||
_deactivate = [=] {
|
||||
hide();
|
||||
};
|
||||
_moveSelection = [=](int delta) {
|
||||
const auto range = inner->moveSelection(delta);
|
||||
if (range.second > range.first) {
|
||||
scroll->scrollToY(range.first, range.second);
|
||||
}
|
||||
};
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
QPainter p(this);
|
||||
p.fillRect(
|
||||
clip.intersected(QRect(0, st::lineWidth, width(), height())),
|
||||
st::mentionBg);
|
||||
p.fillRect(
|
||||
clip.intersected(QRect(0, 0, width(), st::lineWidth)),
|
||||
st::shadowFg);
|
||||
}, lifetime());
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](QSize size) {
|
||||
inputWrap->resizeToWidth(size.width());
|
||||
inputWrap->moveToLeft(0, st::lineWidth, size.width());
|
||||
scroll->setGeometry(
|
||||
0,
|
||||
inputWrap->height(),
|
||||
size.width(),
|
||||
size.height() - inputWrap->height() - st::lineWidth);
|
||||
inner->resizeToWidth(size.width());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void Autocomplete::submitValue(const QString &value) {
|
||||
const auto prefix = u"contact:"_q;
|
||||
if (value.startsWith(prefix)) {
|
||||
const auto line = value.indexOf('\n');
|
||||
const auto text = (line > 0) ? value.mid(line + 1) : QString();
|
||||
const auto contact = value.mid(
|
||||
prefix.size(),
|
||||
(line > 0) ? (line - prefix.size()) : -1);
|
||||
const auto parts = contact.split(' ', Qt::SkipEmptyParts);
|
||||
if (parts.size() > 1) {
|
||||
const auto phone = parts[0];
|
||||
const auto firstName = parts[1];
|
||||
const auto lastName = (parts.size() > 2)
|
||||
? QStringList(parts.mid(2)).join(' ')
|
||||
: QString();
|
||||
_shareContactRequests.fire(Contact{
|
||||
text,
|
||||
phone,
|
||||
firstName,
|
||||
lastName });
|
||||
}
|
||||
} else {
|
||||
_insertRequests.fire_copy(value);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmContactBox::ConfirmContactBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history,
|
||||
const Contact &data,
|
||||
Fn<void(Qt::KeyboardModifiers)> submit)
|
||||
: SimpleElementDelegate(controller, [=] { update(); })
|
||||
, _chatStyle(std::make_unique<Ui::ChatStyle>(
|
||||
history->session().colorIndicesValue()))
|
||||
, _comment(GenerateCommentItem(this, history, data))
|
||||
, _contact(GenerateContactItem(this, history, data))
|
||||
, _submit(submit) {
|
||||
_chatStyle->apply(controller->defaultChatTheme().get());
|
||||
}
|
||||
|
||||
void ConfirmContactBox::prepare() {
|
||||
setTitle(rpl::single(u"Confirmation"_q)); // #TODO hard_lang
|
||||
|
||||
auto maxWidth = 0;
|
||||
if (_comment) {
|
||||
_comment->setAttachToNext(true, _contact.get());
|
||||
_contact->setAttachToPrevious(true, _comment.get());
|
||||
_comment->initDimensions();
|
||||
accumulate_max(maxWidth, _comment->maxWidth());
|
||||
}
|
||||
_contact->initDimensions();
|
||||
accumulate_max(maxWidth, _contact->maxWidth());
|
||||
maxWidth += st::boxPadding.left() + st::boxPadding.right();
|
||||
const auto width = std::clamp(maxWidth, st::boxWidth, st::boxWideWidth);
|
||||
const auto available = width
|
||||
- st::boxPadding.left()
|
||||
- st::boxPadding.right();
|
||||
auto height = 0;
|
||||
if (_comment) {
|
||||
height += _comment->resizeGetHeight(available);
|
||||
}
|
||||
height += _contact->resizeGetHeight(available);
|
||||
setDimensions(width, height);
|
||||
_contact->initDimensions();
|
||||
|
||||
_submit = [=, original = std::move(_submit)](Qt::KeyboardModifiers m) {
|
||||
const auto weak = base::make_weak(this);
|
||||
original(m);
|
||||
if (weak) {
|
||||
closeBox();
|
||||
}
|
||||
};
|
||||
|
||||
const auto button = addButton(tr::lng_send_button(), [] {});
|
||||
button->clicks(
|
||||
) | rpl::on_next([=](Qt::MouseButton which) {
|
||||
_submit((which == Qt::RightButton)
|
||||
? SkipSwitchModifiers()
|
||||
: button->clickModifiers());
|
||||
}, button->lifetime());
|
||||
button->setAcceptBoth(true);
|
||||
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
}
|
||||
|
||||
void ConfirmContactBox::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
|
||||
_submit(e->modifiers());
|
||||
} else {
|
||||
BoxContent::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void ConfirmContactBox::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
p.fillRect(e->rect(), st::boxBg);
|
||||
|
||||
const auto theme = controller()->defaultChatTheme().get();
|
||||
auto context = theme->preparePaintContext(
|
||||
_chatStyle.get(),
|
||||
rect(),
|
||||
rect(),
|
||||
controller()->isGifPausedAtLeastFor(Window::GifPauseReason::Layer));
|
||||
p.translate(st::boxPadding.left(), 0);
|
||||
if (_comment) {
|
||||
context.outbg = _comment->hasOutLayout();
|
||||
_comment->draw(p, context);
|
||||
p.translate(0, _comment->height());
|
||||
}
|
||||
context.outbg = _contact->hasOutLayout();
|
||||
_contact->draw(p, context);
|
||||
}
|
||||
|
||||
HistoryView::Context ConfirmContactBox::elementContext() {
|
||||
return HistoryView::Context::ContactPreview;
|
||||
}
|
||||
|
||||
} // namespace Support
|
||||
94
Telegram/SourceFiles/support/support_autocomplete.h
Normal file
94
Telegram/SourceFiles/support/support_autocomplete.h
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/layers/box_content.h"
|
||||
#include "history/admin_log/history_admin_log_item.h"
|
||||
#include "history/view/history_view_element.h"
|
||||
#include "history/history.h"
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Ui {
|
||||
class ScrollArea;
|
||||
class InputField;
|
||||
class ChatStyle;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Support {
|
||||
|
||||
struct Contact {
|
||||
QString comment;
|
||||
QString phone;
|
||||
QString firstName;
|
||||
QString lastName;
|
||||
};
|
||||
|
||||
class Autocomplete final : public Ui::RpWidget {
|
||||
public:
|
||||
Autocomplete(QWidget *parent, not_null<Main::Session*> session);
|
||||
|
||||
void activate(not_null<Ui::InputField*> field);
|
||||
void deactivate();
|
||||
void setBoundings(QRect rect);
|
||||
|
||||
rpl::producer<QString> insertRequests() const;
|
||||
rpl::producer<Contact> shareContactRequests() const;
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
private:
|
||||
void setupContent();
|
||||
void submitValue(const QString &value);
|
||||
|
||||
not_null<Main::Session*> _session;
|
||||
Fn<void()> _activate;
|
||||
Fn<void()> _deactivate;
|
||||
Fn<void(int delta)> _moveSelection;
|
||||
|
||||
rpl::event_stream<QString> _insertRequests;
|
||||
rpl::event_stream<Contact> _shareContactRequests;
|
||||
|
||||
};
|
||||
|
||||
class ConfirmContactBox
|
||||
: public Ui::BoxContent
|
||||
, public HistoryView::SimpleElementDelegate {
|
||||
public:
|
||||
ConfirmContactBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<History*> history,
|
||||
const Contact &data,
|
||||
Fn<void(Qt::KeyboardModifiers)> submit);
|
||||
|
||||
using Element = HistoryView::Element;
|
||||
HistoryView::Context elementContext() override;
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::ChatStyle> _chatStyle;
|
||||
AdminLog::OwnedItem _comment;
|
||||
AdminLog::OwnedItem _contact;
|
||||
Fn<void(Qt::KeyboardModifiers)> _submit;
|
||||
|
||||
};
|
||||
|
||||
} //namespace Support
|
||||
39
Telegram/SourceFiles/support/support_common.cpp
Normal file
39
Telegram/SourceFiles/support/support_common.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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 "support/support_common.h"
|
||||
|
||||
#include "core/shortcuts.h"
|
||||
|
||||
namespace Support {
|
||||
|
||||
bool HandleSwitch(Qt::KeyboardModifiers modifiers) {
|
||||
return !(modifiers & Qt::ShiftModifier)
|
||||
|| (!(modifiers & Qt::ControlModifier)
|
||||
&& !(modifiers & Qt::MetaModifier));
|
||||
}
|
||||
|
||||
Qt::KeyboardModifiers SkipSwitchModifiers() {
|
||||
return Qt::ControlModifier | Qt::ShiftModifier;
|
||||
}
|
||||
|
||||
std::optional<Shortcuts::Command> GetSwitchCommand(SwitchSettings value) {
|
||||
switch (value) {
|
||||
case SwitchSettings::Next:
|
||||
return Shortcuts::Command::ChatNext;
|
||||
case SwitchSettings::Previous:
|
||||
return Shortcuts::Command::ChatPrevious;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
FnMut<bool()> GetSwitchMethod(SwitchSettings value) {
|
||||
const auto command = GetSwitchCommand(value);
|
||||
return command ? Shortcuts::RequestHandler(*command) : nullptr;
|
||||
}
|
||||
|
||||
} // namespace Support
|
||||
28
Telegram/SourceFiles/support/support_common.h
Normal file
28
Telegram/SourceFiles/support/support_common.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
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 Shortcuts {
|
||||
enum class Command;
|
||||
} // namespace Shortcuts
|
||||
|
||||
namespace Support {
|
||||
|
||||
enum class SwitchSettings {
|
||||
None,
|
||||
Next,
|
||||
Previous,
|
||||
};
|
||||
|
||||
[[nodiscard]] Qt::KeyboardModifiers SkipSwitchModifiers();
|
||||
[[nodiscard]] bool HandleSwitch(Qt::KeyboardModifiers modifiers);
|
||||
[[nodiscard]] std::optional<Shortcuts::Command> GetSwitchCommand(
|
||||
SwitchSettings value);
|
||||
[[nodiscard]] FnMut<bool()> GetSwitchMethod(SwitchSettings value);
|
||||
|
||||
} // namespace Support
|
||||
749
Telegram/SourceFiles/support/support_helper.cpp
Normal file
749
Telegram/SourceFiles/support/support_helper.cpp
Normal file
@@ -0,0 +1,749 @@
|
||||
/*
|
||||
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 "support/support_helper.h"
|
||||
|
||||
#include "dialogs/dialogs_key.h"
|
||||
#include "data/data_drafts.h"
|
||||
#include "data/data_forum.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "api/api_text_entities.h"
|
||||
#include "history/history.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_entity.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "storage/storage_account.h"
|
||||
#include "storage/storage_media_prepare.h"
|
||||
#include "storage/localimageloader.h"
|
||||
#include "core/launcher.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QJsonArray>
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Support {
|
||||
namespace {
|
||||
|
||||
constexpr auto kOccupyFor = TimeId(60);
|
||||
constexpr auto kReoccupyEach = 30 * crl::time(1000);
|
||||
constexpr auto kMaxSupportInfoLength = MaxMessageSize * 4;
|
||||
constexpr auto kTopicRootId = MsgId(0);
|
||||
constexpr auto kMonoforumPeerId = PeerId(0);
|
||||
|
||||
class EditInfoBox : public Ui::BoxContent {
|
||||
public:
|
||||
EditInfoBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
const TextWithTags &text,
|
||||
Fn<void(TextWithTags, Fn<void(bool success)>)> submit);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
|
||||
private:
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
object_ptr<Ui::InputField> _field = { nullptr };
|
||||
Fn<void(TextWithTags, Fn<void(bool success)>)> _submit;
|
||||
|
||||
};
|
||||
|
||||
EditInfoBox::EditInfoBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
const TextWithTags &text,
|
||||
Fn<void(TextWithTags, Fn<void(bool success)>)> submit)
|
||||
: _controller(controller)
|
||||
, _field(
|
||||
this,
|
||||
st::supportInfoField,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
rpl::single(u"Support information"_q), // #TODO hard_lang
|
||||
text)
|
||||
, _submit(std::move(submit)) {
|
||||
_field->setMaxLength(kMaxSupportInfoLength);
|
||||
_field->setSubmitSettings(
|
||||
Core::App().settings().sendSubmitWay());
|
||||
_field->setInstantReplaces(Ui::InstantReplaces::Default());
|
||||
_field->setInstantReplacesEnabled(
|
||||
Core::App().settings().replaceEmojiValue());
|
||||
_field->setMarkdownReplacesEnabled(true);
|
||||
_field->setEditLinkCallback(
|
||||
DefaultEditLinkCallback(controller->uiShow(), _field));
|
||||
}
|
||||
|
||||
void EditInfoBox::prepare() {
|
||||
setTitle(rpl::single(u"Edit support information"_q)); // #TODO hard_lang
|
||||
|
||||
const auto save = [=] {
|
||||
const auto done = crl::guard(this, [=](bool success) {
|
||||
if (success) {
|
||||
closeBox();
|
||||
} else {
|
||||
_field->showError();
|
||||
}
|
||||
});
|
||||
_submit(_field->getTextWithAppliedMarkdown(), done);
|
||||
};
|
||||
addButton(tr::lng_settings_save(), save);
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
_field->submits() | rpl::on_next(save, _field->lifetime());
|
||||
_field->cancelled(
|
||||
) | rpl::on_next([=] {
|
||||
closeBox();
|
||||
}, _field->lifetime());
|
||||
Ui::Emoji::SuggestionsController::Init(
|
||||
getDelegate()->outerContainer(),
|
||||
_field,
|
||||
&_controller->session());
|
||||
|
||||
auto cursor = _field->textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
_field->setTextCursor(cursor);
|
||||
|
||||
widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
_field->resizeToWidth(
|
||||
width - st::boxPadding.left() - st::boxPadding.right());
|
||||
_field->moveToLeft(st::boxPadding.left(), st::boxPadding.bottom());
|
||||
}, _field->lifetime());
|
||||
|
||||
_field->heightValue(
|
||||
) | rpl::on_next([=](int height) {
|
||||
setDimensions(
|
||||
st::boxWideWidth,
|
||||
st::boxPadding.bottom() + height + st::boxPadding.bottom());
|
||||
}, _field->lifetime());
|
||||
}
|
||||
|
||||
void EditInfoBox::setInnerFocus() {
|
||||
_field->setFocusFast();
|
||||
}
|
||||
|
||||
uint32 OccupationTag() {
|
||||
return uint32(Core::Launcher::Instance().installationTag() & 0xFFFFFFFF);
|
||||
}
|
||||
|
||||
QString NormalizeName(QString name) {
|
||||
return name.replace(':', '_').replace(';', '_');
|
||||
}
|
||||
|
||||
Data::Draft OccupiedDraft(const QString &normalizedName) {
|
||||
const auto now = base::unixtime::now(), till = now + kOccupyFor;
|
||||
return {
|
||||
TextWithTags{ "t:"
|
||||
+ QString::number(till)
|
||||
+ ";u:"
|
||||
+ QString::number(OccupationTag())
|
||||
+ ";n:"
|
||||
+ normalizedName },
|
||||
FullReplyTo(),
|
||||
SuggestOptions(),
|
||||
MessageCursor(),
|
||||
Data::WebPageDraft()
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool TrackHistoryOccupation(History *history) {
|
||||
if (!history) {
|
||||
return false;
|
||||
} else if (const auto user = history->peer->asUser()) {
|
||||
return !user->isBot();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 ParseOccupationTag(History *history) {
|
||||
if (!TrackHistoryOccupation(history)) {
|
||||
return 0;
|
||||
}
|
||||
const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId);
|
||||
if (!draft) {
|
||||
return 0;
|
||||
}
|
||||
const auto &text = draft->textWithTags.text;
|
||||
const auto parts = QStringView(text).split(';');
|
||||
auto valid = false;
|
||||
auto result = uint32();
|
||||
for (const auto &part : parts) {
|
||||
if (part.startsWith(u"t:"_q)) {
|
||||
if (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {
|
||||
valid = true;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else if (part.startsWith(u"u:"_q)) {
|
||||
result = base::StringViewMid(part, 2).toUInt();
|
||||
}
|
||||
}
|
||||
return valid ? result : 0;
|
||||
}
|
||||
|
||||
QString ParseOccupationName(History *history) {
|
||||
if (!TrackHistoryOccupation(history)) {
|
||||
return QString();
|
||||
}
|
||||
const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId);
|
||||
if (!draft) {
|
||||
return QString();
|
||||
}
|
||||
const auto &text = draft->textWithTags.text;
|
||||
const auto parts = QStringView(text).split(';');
|
||||
auto valid = false;
|
||||
auto result = QString();
|
||||
for (const auto &part : parts) {
|
||||
if (part.startsWith(u"t:"_q)) {
|
||||
if (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {
|
||||
valid = true;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else if (part.startsWith(u"n:"_q)) {
|
||||
result = base::StringViewMid(part, 2).toString();
|
||||
}
|
||||
}
|
||||
return valid ? result : QString();
|
||||
}
|
||||
|
||||
TimeId OccupiedBySomeoneTill(History *history) {
|
||||
if (!TrackHistoryOccupation(history)) {
|
||||
return 0;
|
||||
}
|
||||
const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId);
|
||||
if (!draft) {
|
||||
return 0;
|
||||
}
|
||||
const auto &text = draft->textWithTags.text;
|
||||
const auto parts = QStringView(text).split(';');
|
||||
auto valid = false;
|
||||
auto result = TimeId();
|
||||
for (const auto &part : parts) {
|
||||
if (part.startsWith(u"t:"_q)) {
|
||||
if (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {
|
||||
result = base::StringViewMid(part, 2).toInt();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else if (part.startsWith(u"u:"_q)) {
|
||||
if (base::StringViewMid(part, 2).toUInt() != OccupationTag()) {
|
||||
valid = true;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return valid ? result : 0;
|
||||
}
|
||||
|
||||
QString FastButtonModeIdsPath(not_null<Main::Session*> session) {
|
||||
const auto base = session->account().local().supportModePath();
|
||||
QDir().mkpath(base);
|
||||
return base + u"/fast_button_mode_ids.json"_q;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Helper::Helper(not_null<Main::Session*> session)
|
||||
: _session(session)
|
||||
, _api(&_session->mtp())
|
||||
, _templates(_session)
|
||||
, _reoccupyTimer([=] { reoccupy(); })
|
||||
, _checkOccupiedTimer([=] { checkOccupiedChats(); }) {
|
||||
_api.request(MTPhelp_GetSupportName(
|
||||
)).done([=](const MTPhelp_SupportName &result) {
|
||||
result.match([&](const MTPDhelp_supportName &data) {
|
||||
setSupportName(qs(data.vname()));
|
||||
});
|
||||
}).fail([=] {
|
||||
setSupportName(
|
||||
u"[rand^"_q
|
||||
+ QString::number(Core::Launcher::Instance().installationTag())
|
||||
+ ']');
|
||||
}).send();
|
||||
}
|
||||
|
||||
std::unique_ptr<Helper> Helper::Create(not_null<Main::Session*> session) {
|
||||
//return std::make_unique<Helper>(session); AssertIsDebug();
|
||||
return ShouldUse(session) ? std::make_unique<Helper>(session) : nullptr;
|
||||
}
|
||||
|
||||
void Helper::CheckIfLost(not_null<Window::SessionController*> controller) {
|
||||
static auto Checked = false;
|
||||
if (Checked) {
|
||||
return;
|
||||
}
|
||||
Checked = true;
|
||||
|
||||
const auto session = &controller->session();
|
||||
if (!ShouldUse(session) || session->supportMode()) {
|
||||
return;
|
||||
}
|
||||
session->local().writeSelf();
|
||||
controller->show(Ui::MakeConfirmBox({
|
||||
.text = u"This account should have support mode, "
|
||||
"but it seems it was lost. Restart?"_q,
|
||||
.confirmed = [=] { Core::Restart(); },
|
||||
.confirmText = u"Restart"_q,
|
||||
.title = u"Support Mode Lost"_q,
|
||||
}));
|
||||
}
|
||||
|
||||
bool Helper::ShouldUse(not_null<Main::Session*> session) {
|
||||
return session->user()->phone().startsWith(u"424"_q);
|
||||
}
|
||||
|
||||
void Helper::registerWindow(not_null<Window::SessionController*> controller) {
|
||||
controller->activeChatValue(
|
||||
) | rpl::map([](Dialogs::Key key) {
|
||||
const auto history = key.history();
|
||||
return TrackHistoryOccupation(history) ? history : nullptr;
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::on_next([=](History *history) {
|
||||
updateOccupiedHistory(controller, history);
|
||||
}, controller->lifetime());
|
||||
}
|
||||
|
||||
void Helper::cloudDraftChanged(not_null<History*> history) {
|
||||
chatOccupiedUpdated(history);
|
||||
if (history != _occupiedHistory) {
|
||||
return;
|
||||
}
|
||||
occupyIfNotYet();
|
||||
}
|
||||
|
||||
void Helper::chatOccupiedUpdated(not_null<History*> history) {
|
||||
if (const auto till = OccupiedBySomeoneTill(history)) {
|
||||
_occupiedChats[history] = till + 2;
|
||||
history->session().changes().historyUpdated(
|
||||
history,
|
||||
Data::HistoryUpdate::Flag::ChatOccupied);
|
||||
checkOccupiedChats();
|
||||
} else if (_occupiedChats.take(history)) {
|
||||
history->session().changes().historyUpdated(
|
||||
history,
|
||||
Data::HistoryUpdate::Flag::ChatOccupied);
|
||||
}
|
||||
}
|
||||
|
||||
void Helper::checkOccupiedChats() {
|
||||
const auto now = base::unixtime::now();
|
||||
while (!_occupiedChats.empty()) {
|
||||
const auto nearest = ranges::min_element(
|
||||
_occupiedChats,
|
||||
std::less<>(),
|
||||
[](const auto &pair) { return pair.second; });
|
||||
if (nearest->second <= now) {
|
||||
const auto history = nearest->first;
|
||||
_occupiedChats.erase(nearest);
|
||||
history->session().changes().historyUpdated(
|
||||
history,
|
||||
Data::HistoryUpdate::Flag::ChatOccupied);
|
||||
} else {
|
||||
_checkOccupiedTimer.callOnce(
|
||||
(nearest->second - now) * crl::time(1000));
|
||||
return;
|
||||
}
|
||||
}
|
||||
_checkOccupiedTimer.cancel();
|
||||
}
|
||||
|
||||
void Helper::updateOccupiedHistory(
|
||||
not_null<Window::SessionController*> controller,
|
||||
History *history) {
|
||||
if (isOccupiedByMe(_occupiedHistory)) {
|
||||
_occupiedHistory->clearCloudDraft(kTopicRootId, kMonoforumPeerId);
|
||||
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
|
||||
}
|
||||
_occupiedHistory = history;
|
||||
occupyInDraft();
|
||||
}
|
||||
|
||||
void Helper::setSupportName(const QString &name) {
|
||||
_supportName = name;
|
||||
_supportNameNormalized = NormalizeName(name);
|
||||
occupyIfNotYet();
|
||||
}
|
||||
|
||||
void Helper::occupyIfNotYet() {
|
||||
if (!isOccupiedByMe(_occupiedHistory)) {
|
||||
occupyInDraft();
|
||||
}
|
||||
}
|
||||
|
||||
void Helper::occupyInDraft() {
|
||||
if (_occupiedHistory
|
||||
&& !isOccupiedBySomeone(_occupiedHistory)
|
||||
&& !_supportName.isEmpty()) {
|
||||
const auto draft = OccupiedDraft(_supportNameNormalized);
|
||||
_occupiedHistory->createCloudDraft(
|
||||
kTopicRootId,
|
||||
kMonoforumPeerId,
|
||||
&draft);
|
||||
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
|
||||
_reoccupyTimer.callEach(kReoccupyEach);
|
||||
}
|
||||
}
|
||||
|
||||
void Helper::reoccupy() {
|
||||
if (isOccupiedByMe(_occupiedHistory)) {
|
||||
const auto draft = OccupiedDraft(_supportNameNormalized);
|
||||
_occupiedHistory->createCloudDraft(
|
||||
kTopicRootId,
|
||||
kMonoforumPeerId,
|
||||
&draft);
|
||||
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
|
||||
}
|
||||
}
|
||||
|
||||
bool Helper::isOccupiedByMe(History *history) const {
|
||||
if (const auto tag = ParseOccupationTag(history)) {
|
||||
return (tag == OccupationTag());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Helper::isOccupiedBySomeone(History *history) const {
|
||||
if (const auto tag = ParseOccupationTag(history)) {
|
||||
return (tag != OccupationTag());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Helper::refreshInfo(not_null<UserData*> user) {
|
||||
_api.request(MTPhelp_GetUserInfo(
|
||||
user->inputUser()
|
||||
)).done([=](const MTPhelp_UserInfo &result) {
|
||||
applyInfo(user, result);
|
||||
if (const auto controller = _userInfoEditPending.take(user)) {
|
||||
if (const auto strong = controller->get()) {
|
||||
showEditInfoBox(strong, user);
|
||||
}
|
||||
}
|
||||
}).send();
|
||||
}
|
||||
|
||||
void Helper::applyInfo(
|
||||
not_null<UserData*> user,
|
||||
const MTPhelp_UserInfo &result) {
|
||||
const auto notify = [&] {
|
||||
user->session().changes().peerUpdated(
|
||||
user,
|
||||
Data::PeerUpdate::Flag::SupportInfo);
|
||||
};
|
||||
const auto remove = [&] {
|
||||
if (_userInformation.take(user)) {
|
||||
notify();
|
||||
}
|
||||
};
|
||||
result.match([&](const MTPDhelp_userInfo &data) {
|
||||
auto info = UserInfo();
|
||||
info.author = qs(data.vauthor());
|
||||
info.date = data.vdate().v;
|
||||
info.text = TextWithEntities{
|
||||
qs(data.vmessage()),
|
||||
Api::EntitiesFromMTP(&user->session(), data.ventities().v) };
|
||||
if (info.text.empty()) {
|
||||
remove();
|
||||
} else if (_userInformation[user] != info) {
|
||||
_userInformation[user] = info;
|
||||
notify();
|
||||
}
|
||||
}, [&](const MTPDhelp_userInfoEmpty &) {
|
||||
remove();
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<UserInfo> Helper::infoValue(not_null<UserData*> user) const {
|
||||
return user->session().changes().peerFlagsValue(
|
||||
user,
|
||||
Data::PeerUpdate::Flag::SupportInfo
|
||||
) | rpl::map([=] {
|
||||
return infoCurrent(user);
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<QString> Helper::infoLabelValue(
|
||||
not_null<UserData*> user) const {
|
||||
return infoValue(
|
||||
user
|
||||
) | rpl::map([](const Support::UserInfo &info) {
|
||||
const auto time = Ui::FormatDateTime(
|
||||
base::unixtime::parse(info.date));
|
||||
return info.author + ", " + time;
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<TextWithEntities> Helper::infoTextValue(
|
||||
not_null<UserData*> user) const {
|
||||
return infoValue(
|
||||
user
|
||||
) | rpl::map([](const Support::UserInfo &info) {
|
||||
return info.text;
|
||||
});
|
||||
}
|
||||
|
||||
UserInfo Helper::infoCurrent(not_null<UserData*> user) const {
|
||||
const auto i = _userInformation.find(user);
|
||||
return (i != end(_userInformation)) ? i->second : UserInfo();
|
||||
}
|
||||
|
||||
void Helper::editInfo(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<UserData*> user) {
|
||||
if (!_userInfoEditPending.contains(user)) {
|
||||
_userInfoEditPending.emplace(user, controller.get());
|
||||
refreshInfo(user);
|
||||
}
|
||||
}
|
||||
|
||||
void Helper::showEditInfoBox(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<UserData*> user) {
|
||||
const auto info = infoCurrent(user);
|
||||
const auto editData = TextWithTags{
|
||||
info.text.text,
|
||||
TextUtilities::ConvertEntitiesToTextTags(info.text.entities)
|
||||
};
|
||||
|
||||
const auto save = [=](TextWithTags result, Fn<void(bool)> done) {
|
||||
saveInfo(user, TextWithEntities{
|
||||
result.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(result.tags)
|
||||
}, done);
|
||||
};
|
||||
controller->show(Box<EditInfoBox>(controller, editData, save));
|
||||
}
|
||||
|
||||
void Helper::saveInfo(
|
||||
not_null<UserData*> user,
|
||||
TextWithEntities text,
|
||||
Fn<void(bool success)> done) {
|
||||
const auto i = _userInfoSaving.find(user);
|
||||
if (i != end(_userInfoSaving)) {
|
||||
if (i->second.data == text) {
|
||||
return;
|
||||
} else {
|
||||
i->second.data = text;
|
||||
_api.request(base::take(i->second.requestId)).cancel();
|
||||
}
|
||||
} else {
|
||||
_userInfoSaving.emplace(user, SavingInfo{ text });
|
||||
}
|
||||
|
||||
TextUtilities::PrepareForSending(
|
||||
text,
|
||||
Ui::ItemTextDefaultOptions().flags);
|
||||
TextUtilities::Trim(text);
|
||||
|
||||
const auto entities = Api::EntitiesToMTP(
|
||||
&user->session(),
|
||||
text.entities,
|
||||
Api::ConvertOption::SkipLocal);
|
||||
_userInfoSaving[user].requestId = _api.request(MTPhelp_EditUserInfo(
|
||||
user->inputUser(),
|
||||
MTP_string(text.text),
|
||||
entities
|
||||
)).done([=](const MTPhelp_UserInfo &result) {
|
||||
applyInfo(user, result);
|
||||
done(true);
|
||||
}).fail([=] {
|
||||
done(false);
|
||||
}).send();
|
||||
}
|
||||
|
||||
Templates &Helper::templates() {
|
||||
return _templates;
|
||||
}
|
||||
|
||||
FastButtonsBots::FastButtonsBots(not_null<Main::Session*> session)
|
||||
: _session(session) {
|
||||
}
|
||||
|
||||
bool FastButtonsBots::enabled(not_null<PeerData*> peer) const {
|
||||
if (!_read) {
|
||||
const_cast<FastButtonsBots*>(this)->read();
|
||||
}
|
||||
return _bots.contains(peer->id);
|
||||
}
|
||||
|
||||
rpl::producer<bool> FastButtonsBots::enabledValue(
|
||||
not_null<PeerData*> peer) const {
|
||||
return rpl::single(
|
||||
enabled(peer)
|
||||
) | rpl::then(_changes.events(
|
||||
) | rpl::filter([=](PeerId id) {
|
||||
return (peer->id == id);
|
||||
}) | rpl::map([=] {
|
||||
return enabled(peer);
|
||||
}));
|
||||
}
|
||||
|
||||
void FastButtonsBots::setEnabled(not_null<PeerData*> peer, bool value) {
|
||||
if (value == enabled(peer)) {
|
||||
return;
|
||||
} else if (value) {
|
||||
_bots.emplace(peer->id);
|
||||
} else {
|
||||
_bots.remove(peer->id);
|
||||
}
|
||||
if (_bots.empty()) {
|
||||
QFile(FastButtonModeIdsPath(_session)).remove();
|
||||
} else {
|
||||
write();
|
||||
}
|
||||
_changes.fire_copy(peer->id);
|
||||
if (const auto history = peer->owner().history(peer)) {
|
||||
if (const auto item = history->lastMessage()) {
|
||||
history->owner().requestItemRepaint(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FastButtonsBots::write() {
|
||||
auto array = QJsonArray();
|
||||
for (const auto &id : _bots) {
|
||||
array.append(QString::number(id.value));
|
||||
}
|
||||
auto object = QJsonObject();
|
||||
object[u"ids"_q] = array;
|
||||
auto f = QFile(FastButtonModeIdsPath(_session));
|
||||
if (f.open(QIODevice::WriteOnly)) {
|
||||
f.write(QJsonDocument(object).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
}
|
||||
|
||||
void FastButtonsBots::read() {
|
||||
_read = true;
|
||||
|
||||
auto f = QFile(FastButtonModeIdsPath(_session));
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return;
|
||||
}
|
||||
const auto data = f.readAll();
|
||||
const auto json = QJsonDocument::fromJson(data);
|
||||
if (!json.isObject()) {
|
||||
return;
|
||||
}
|
||||
const auto object = json.object();
|
||||
const auto array = object.value(u"ids"_q).toArray();
|
||||
for (const auto &value : array) {
|
||||
const auto bareId = value.toString().toULongLong();
|
||||
_bots.emplace(PeerId(bareId));
|
||||
}
|
||||
}
|
||||
|
||||
QString ChatOccupiedString(not_null<History*> history) {
|
||||
const auto hand = QString::fromUtf8("\xe2\x9c\x8b\xef\xb8\x8f");
|
||||
const auto name = ParseOccupationName(history);
|
||||
return (name.isEmpty() || name.startsWith(u"[rand^"_q))
|
||||
? hand + " chat taken"
|
||||
: hand + ' ' + name + " is here";
|
||||
}
|
||||
|
||||
QString InterpretSendPath(
|
||||
not_null<Window::SessionController*> window,
|
||||
const QString &path) {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return "App Error: Could not open interpret file: " + path;
|
||||
}
|
||||
const auto content = QString::fromUtf8(f.readAll());
|
||||
f.close();
|
||||
const auto lines = content.split('\n');
|
||||
auto toId = PeerId(0);
|
||||
auto topicRootId = MsgId(0);
|
||||
auto filePath = QString();
|
||||
auto caption = QString();
|
||||
for (const auto &line : lines) {
|
||||
if (line.startsWith(u"from: "_q)) {
|
||||
if (window->session().userId().bare
|
||||
!= base::StringViewMid(
|
||||
line,
|
||||
u"from: "_q.size()).toULongLong()) {
|
||||
return "App Error: Wrong current user.";
|
||||
}
|
||||
} else if (line.startsWith(u"channel: "_q)) {
|
||||
const auto channelId = base::StringViewMid(
|
||||
line,
|
||||
u"channel: "_q.size()).toULongLong();
|
||||
toId = peerFromChannel(channelId);
|
||||
} else if (line.startsWith(u"topic: "_q)) {
|
||||
const auto topicId = base::StringViewMid(
|
||||
line,
|
||||
u"topic: "_q.size()).toULongLong();
|
||||
topicRootId = MsgId(topicId);
|
||||
} else if (line.startsWith(u"file: "_q)) {
|
||||
const auto path = line.mid(u"file: "_q.size());
|
||||
if (!QFile(path).exists()) {
|
||||
return "App Error: Could not find file with path: " + path;
|
||||
}
|
||||
filePath = path;
|
||||
} else if (line.startsWith(u"caption: "_q)) {
|
||||
caption = line.mid(u"caption: "_q.size());
|
||||
} else if (!caption.isEmpty()) {
|
||||
caption += '\n' + line;
|
||||
} else {
|
||||
return "App Error: Invalid command: " + line;
|
||||
}
|
||||
}
|
||||
const auto history = window->session().data().historyLoaded(toId);
|
||||
const auto sendTo = [=](not_null<Data::Thread*> thread) {
|
||||
window->showThread(thread);
|
||||
const auto premium = thread->session().user()->isPremium();
|
||||
thread->session().api().sendFiles(
|
||||
Storage::PrepareMediaList(
|
||||
QStringList(filePath),
|
||||
st::sendMediaPreviewSize,
|
||||
premium),
|
||||
SendMediaType::File,
|
||||
{ caption },
|
||||
nullptr,
|
||||
Api::SendAction(thread));
|
||||
};
|
||||
if (!history) {
|
||||
return "App Error: Could not find channel with id: "
|
||||
+ QString::number(peerToChannel(toId).bare);
|
||||
} else if (const auto forum = history->asForum()) {
|
||||
forum->requestTopic(topicRootId, [=] {
|
||||
if (const auto forum = history->asForum()) {
|
||||
if (const auto topic = forum->topicFor(topicRootId)) {
|
||||
sendTo(topic);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (!topicRootId) {
|
||||
sendTo(history);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace Support
|
||||
146
Telegram/SourceFiles/support/support_helper.h
Normal file
146
Telegram/SourceFiles/support/support_helper.h
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
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
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "support/support_templates.h"
|
||||
#include "mtproto/sender.h"
|
||||
|
||||
class History;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Support {
|
||||
|
||||
struct UserInfo {
|
||||
QString author;
|
||||
TimeId date = 0;
|
||||
TextWithEntities text;
|
||||
};
|
||||
|
||||
inline bool operator==(const UserInfo &a, const UserInfo &b) {
|
||||
return (a.author == b.author)
|
||||
&& (a.date == b.date)
|
||||
&& (a.text == b.text);
|
||||
}
|
||||
|
||||
inline bool operator!=(const UserInfo &a, const UserInfo &b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
class Helper final {
|
||||
public:
|
||||
explicit Helper(not_null<Main::Session*> session);
|
||||
|
||||
static std::unique_ptr<Helper> Create(not_null<Main::Session*> session);
|
||||
static void CheckIfLost(not_null<Window::SessionController*> controller);
|
||||
|
||||
void registerWindow(not_null<Window::SessionController*> controller);
|
||||
void cloudDraftChanged(not_null<History*> history);
|
||||
|
||||
void chatOccupiedUpdated(not_null<History*> history);
|
||||
|
||||
[[nodiscard]] bool isOccupiedByMe(History *history) const;
|
||||
[[nodiscard]] bool isOccupiedBySomeone(History *history) const;
|
||||
|
||||
void refreshInfo(not_null<UserData*> user);
|
||||
[[nodiscard]] rpl::producer<UserInfo> infoValue(
|
||||
not_null<UserData*> user) const;
|
||||
[[nodiscard]] rpl::producer<QString> infoLabelValue(
|
||||
not_null<UserData*> user) const;
|
||||
[[nodiscard]] rpl::producer<TextWithEntities> infoTextValue(
|
||||
not_null<UserData*> user) const;
|
||||
[[nodiscard]] UserInfo infoCurrent(not_null<UserData*> user) const;
|
||||
void editInfo(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<UserData*> user);
|
||||
|
||||
Templates &templates();
|
||||
|
||||
private:
|
||||
struct SavingInfo {
|
||||
TextWithEntities data;
|
||||
mtpRequestId requestId = 0;
|
||||
};
|
||||
void checkOccupiedChats();
|
||||
void updateOccupiedHistory(
|
||||
not_null<Window::SessionController*> controller,
|
||||
History *history);
|
||||
void setSupportName(const QString &name);
|
||||
void occupyIfNotYet();
|
||||
void occupyInDraft();
|
||||
void reoccupy();
|
||||
|
||||
void applyInfo(
|
||||
not_null<UserData*> user,
|
||||
const MTPhelp_UserInfo &result);
|
||||
void showEditInfoBox(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<UserData*> user);
|
||||
void saveInfo(
|
||||
not_null<UserData*> user,
|
||||
TextWithEntities text,
|
||||
Fn<void(bool success)> done);
|
||||
|
||||
static bool ShouldUse(not_null<Main::Session*> session);
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
MTP::Sender _api;
|
||||
Templates _templates;
|
||||
QString _supportName;
|
||||
QString _supportNameNormalized;
|
||||
|
||||
History *_occupiedHistory = nullptr;
|
||||
base::Timer _reoccupyTimer;
|
||||
base::Timer _checkOccupiedTimer;
|
||||
base::flat_map<not_null<History*>, TimeId> _occupiedChats;
|
||||
|
||||
base::flat_map<not_null<UserData*>, UserInfo> _userInformation;
|
||||
base::flat_map<
|
||||
not_null<UserData*>,
|
||||
base::weak_ptr<Window::SessionController>> _userInfoEditPending;
|
||||
base::flat_map<not_null<UserData*>, SavingInfo> _userInfoSaving;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
class FastButtonsBots final {
|
||||
public:
|
||||
explicit FastButtonsBots(not_null<Main::Session*> session);
|
||||
|
||||
[[nodiscard]] bool enabled(not_null<PeerData*> peer) const;
|
||||
[[nodiscard]] rpl::producer<bool> enabledValue(
|
||||
not_null<PeerData*> peer) const;
|
||||
void setEnabled(not_null<PeerData*> peer, bool value);
|
||||
|
||||
private:
|
||||
void write();
|
||||
void read();
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
|
||||
base::flat_set<PeerId> _bots;
|
||||
rpl::event_stream<PeerId> _changes;
|
||||
bool _read = false;
|
||||
|
||||
};
|
||||
|
||||
QString ChatOccupiedString(not_null<History*> history);
|
||||
|
||||
QString InterpretSendPath(
|
||||
not_null<Window::SessionController*> window,
|
||||
const QString &path);
|
||||
|
||||
} // namespace Support
|
||||
77
Telegram/SourceFiles/support/support_preload.cpp
Normal file
77
Telegram/SourceFiles/support/support_preload.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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 "support/support_preload.h"
|
||||
|
||||
#include "history/history.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_histories.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
|
||||
namespace Support {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPreloadMessagesCount = 50;
|
||||
|
||||
} // namespace
|
||||
|
||||
int SendPreloadRequest(not_null<History*> history, Fn<void()> retry) {
|
||||
auto offsetId = MsgId();
|
||||
auto offset = 0;
|
||||
auto loadCount = kPreloadMessagesCount;
|
||||
if (const auto around = history->loadAroundId()) {
|
||||
history->getReadyFor(ShowAtUnreadMsgId);
|
||||
offset = -loadCount / 2;
|
||||
offsetId = around;
|
||||
}
|
||||
const auto offsetDate = 0;
|
||||
const auto maxId = 0;
|
||||
const auto minId = 0;
|
||||
const auto historyHash = uint64(0);
|
||||
const auto type = Data::Histories::RequestType::History;
|
||||
auto &histories = history->owner().histories();
|
||||
return histories.sendRequest(history, type, [=](Fn<void()> finish) {
|
||||
return history->session().api().request(MTPmessages_GetHistory(
|
||||
history->peer->input(),
|
||||
MTP_int(offsetId),
|
||||
MTP_int(offsetDate),
|
||||
MTP_int(offset),
|
||||
MTP_int(loadCount),
|
||||
MTP_int(maxId),
|
||||
MTP_int(minId),
|
||||
MTP_long(historyHash)
|
||||
)).done([=](const MTPmessages_Messages &result) {
|
||||
if (const auto around = history->loadAroundId()) {
|
||||
if (around != offsetId) {
|
||||
retry();
|
||||
return;
|
||||
}
|
||||
history->clear(History::ClearType::Unload);
|
||||
history->getReadyFor(ShowAtUnreadMsgId);
|
||||
} else if (offsetId) {
|
||||
retry();
|
||||
return;
|
||||
} else {
|
||||
history->clear(History::ClearType::Unload);
|
||||
history->getReadyFor(ShowAtTheEndMsgId);
|
||||
}
|
||||
result.match([](const MTPDmessages_messagesNotModified&) {
|
||||
}, [&](const auto &data) {
|
||||
history->owner().processUsers(data.vusers());
|
||||
history->owner().processChats(data.vchats());
|
||||
history->addOlderSlice(data.vmessages().v);
|
||||
});
|
||||
finish();
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
finish();
|
||||
}).send();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Support
|
||||
19
Telegram/SourceFiles/support/support_preload.h
Normal file
19
Telegram/SourceFiles/support/support_preload.h
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
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
|
||||
|
||||
class History;
|
||||
|
||||
namespace Support {
|
||||
|
||||
// Returns histories().request, not api().request.
|
||||
[[nodiscard]] int SendPreloadRequest(
|
||||
not_null<History*> history,
|
||||
Fn<void()> retry);
|
||||
|
||||
} // namespace Support
|
||||
739
Telegram/SourceFiles/support/support_templates.cpp
Normal file
739
Telegram/SourceFiles/support/support_templates.cpp
Normal file
@@ -0,0 +1,739 @@
|
||||
/*
|
||||
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 "support/support_templates.h"
|
||||
|
||||
#include "ui/toast/toast.h"
|
||||
#include "data/data_session.h"
|
||||
#include "core/shortcuts.h"
|
||||
#include "main/main_session.h"
|
||||
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
|
||||
namespace Support {
|
||||
namespace details {
|
||||
namespace {
|
||||
|
||||
constexpr auto kQueryLimit = 10;
|
||||
constexpr auto kWeightStep = 1000;
|
||||
|
||||
struct Delta {
|
||||
std::vector<const TemplatesQuestion*> added;
|
||||
std::vector<const TemplatesQuestion*> changed;
|
||||
std::vector<const TemplatesQuestion*> removed;
|
||||
|
||||
std::map<QString, QStringList> keys;
|
||||
|
||||
explicit operator bool() const {
|
||||
return !added.empty() || !changed.empty() || !removed.empty();
|
||||
}
|
||||
};
|
||||
|
||||
bool IsTemplatesFile(const QString &file) {
|
||||
return file.startsWith(u"tl_"_q, Qt::CaseInsensitive)
|
||||
&& file.endsWith(u".txt"_q, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
QString NormalizeQuestion(const QString &question) {
|
||||
auto result = QString();
|
||||
result.reserve(question.size());
|
||||
for (const auto &ch : question) {
|
||||
if (ch.isLetterOrNumber()) {
|
||||
result.append(ch.toLower());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString NormalizeKey(const QString &query) {
|
||||
return TextUtilities::RemoveAccents(query.trimmed().toLower());
|
||||
}
|
||||
|
||||
struct FileResult {
|
||||
TemplatesFile result;
|
||||
QStringList errors;
|
||||
};
|
||||
|
||||
enum class ReadState {
|
||||
None,
|
||||
Question,
|
||||
Keys,
|
||||
Value,
|
||||
Url,
|
||||
};
|
||||
|
||||
template <typename StateChange, typename LineCallback>
|
||||
void ReadByLine(
|
||||
const QByteArray &blob,
|
||||
StateChange &&stateChange,
|
||||
LineCallback &&lineCallback) {
|
||||
using State = ReadState;
|
||||
auto state = State::None;
|
||||
auto hadKeys = false;
|
||||
auto hadValue = false;
|
||||
for (const auto &utf : blob.split('\n')) {
|
||||
const auto line = QString::fromUtf8(utf).trimmed();
|
||||
const auto match = QRegularExpression(
|
||||
u"^\\{([A-Z_]+)\\}$"_q
|
||||
).match(line);
|
||||
if (match.hasMatch()) {
|
||||
const auto token = match.captured(1);
|
||||
if (state == State::Value) {
|
||||
hadKeys = hadValue = false;
|
||||
}
|
||||
const auto newState = [&] {
|
||||
if (token == u"VALUE"_q) {
|
||||
return hadValue ? State::None : State::Value;
|
||||
} else if (token == u"KEYS"_q) {
|
||||
return hadKeys ? State::None : State::Keys;
|
||||
} else if (token == u"QUESTION"_q) {
|
||||
return State::Question;
|
||||
} else if (token == u"URL"_q) {
|
||||
return State::Url;
|
||||
} else {
|
||||
return State::None;
|
||||
}
|
||||
}();
|
||||
stateChange(state, newState);
|
||||
state = newState;
|
||||
lineCallback(state, line, true);
|
||||
} else {
|
||||
if (!line.isEmpty()) {
|
||||
if (state == State::Value) {
|
||||
hadValue = true;
|
||||
} else if (state == State::Keys) {
|
||||
hadKeys = true;
|
||||
}
|
||||
}
|
||||
lineCallback(state, line, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
QString ReadByLineGetUrl(const QByteArray &blob, Callback &&callback) {
|
||||
using State = ReadState;
|
||||
auto url = QString();
|
||||
auto question = TemplatesQuestion();
|
||||
const auto call = [&] {
|
||||
while (question.value.endsWith('\n')) {
|
||||
question.value.chop(1);
|
||||
}
|
||||
return callback(base::take(question));
|
||||
};
|
||||
ReadByLine(blob, [&](State was, State now) {
|
||||
if (was == State::Value) {
|
||||
call();
|
||||
}
|
||||
}, [&](State state, const QString &line, bool stateChangeLine) {
|
||||
if (stateChangeLine) {
|
||||
return;
|
||||
}
|
||||
switch (state) {
|
||||
case State::Keys:
|
||||
if (!line.isEmpty()) {
|
||||
question.originalKeys.push_back(line);
|
||||
if (const auto norm = NormalizeKey(line); !norm.isEmpty()) {
|
||||
question.normalizedKeys.push_back(norm);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case State::Value:
|
||||
if (!question.value.isEmpty()) {
|
||||
question.value += '\n';
|
||||
}
|
||||
question.value += line;
|
||||
break;
|
||||
case State::Question:
|
||||
if (question.question.isEmpty()) {
|
||||
question.question = line;
|
||||
}
|
||||
break;
|
||||
case State::Url:
|
||||
if (url.isEmpty()) {
|
||||
url = line;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
call();
|
||||
return url;
|
||||
}
|
||||
|
||||
FileResult ReadFromBlob(const QByteArray &blob) {
|
||||
auto result = FileResult();
|
||||
result.result.url = ReadByLineGetUrl(blob, [&](TemplatesQuestion &&q) {
|
||||
const auto normalized = NormalizeQuestion(q.question);
|
||||
if (!normalized.isEmpty()) {
|
||||
result.result.questions.emplace(normalized, std::move(q));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
FileResult ReadFile(const QString &path) {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
auto result = FileResult();
|
||||
result.errors.push_back(
|
||||
u"Couldn't open '%1' for reading!"_q.arg(path));
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto blob = f.readAll();
|
||||
f.close();
|
||||
|
||||
return ReadFromBlob(blob);
|
||||
}
|
||||
|
||||
void WriteWithOwnUrlAndKeys(
|
||||
QIODevice &device,
|
||||
const QByteArray &blob,
|
||||
const QString &url,
|
||||
const Delta &delta) {
|
||||
device.write("{URL}\n");
|
||||
device.write(url.toUtf8());
|
||||
device.write("\n\n");
|
||||
|
||||
using State = ReadState;
|
||||
auto question = QString();
|
||||
auto normalized = QString();
|
||||
auto ownKeysWritten = false;
|
||||
ReadByLine(blob, [&](State was, State now) {
|
||||
if (was == State::Value) {
|
||||
question = normalized = QString();
|
||||
}
|
||||
}, [&](State state, const QString &line, bool stateChangeLine) {
|
||||
const auto writeLine = [&] {
|
||||
device.write(line.toUtf8());
|
||||
device.write("\n", 1);
|
||||
};
|
||||
switch (state) {
|
||||
case State::Keys:
|
||||
if (stateChangeLine) {
|
||||
writeLine();
|
||||
ownKeysWritten = [&] {
|
||||
if (normalized.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const auto i = delta.keys.find(normalized);
|
||||
if (i == end(delta.keys)) {
|
||||
return false;
|
||||
}
|
||||
device.write(i->second.join('\n').toUtf8());
|
||||
device.write("\n", 1);
|
||||
return true;
|
||||
}();
|
||||
} else if (!ownKeysWritten) {
|
||||
writeLine();
|
||||
}
|
||||
break;
|
||||
case State::Value:
|
||||
writeLine();
|
||||
break;
|
||||
case State::Question:
|
||||
writeLine();
|
||||
if (!stateChangeLine && question.isEmpty()) {
|
||||
question = line;
|
||||
normalized = NormalizeQuestion(line);
|
||||
}
|
||||
break;
|
||||
case State::Url:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
struct FilesResult {
|
||||
TemplatesData result;
|
||||
TemplatesIndex index;
|
||||
QStringList errors;
|
||||
};
|
||||
|
||||
FilesResult ReadFiles(const QString &folder) {
|
||||
auto result = FilesResult();
|
||||
const auto files = QDir(folder).entryList(QDir::Files);
|
||||
for (const auto &path : files) {
|
||||
if (!IsTemplatesFile(path)) {
|
||||
continue;
|
||||
}
|
||||
auto file = ReadFile(folder + '/' + path);
|
||||
if (!file.result.url.isEmpty() || !file.result.questions.empty()) {
|
||||
result.result.files[path] = std::move(file.result);
|
||||
}
|
||||
result.errors.append(std::move(file.errors));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TemplatesIndex ComputeIndex(const TemplatesData &data) {
|
||||
using Id = TemplatesIndex::Id;
|
||||
using Term = TemplatesIndex::Term;
|
||||
|
||||
auto uniqueFirst = std::map<QChar, base::flat_set<Id>>();
|
||||
auto uniqueFull = std::map<Id, base::flat_set<Term>>();
|
||||
const auto pushString = [&](
|
||||
const Id &id,
|
||||
const QString &string,
|
||||
int weight) {
|
||||
const auto list = TextUtilities::PrepareSearchWords(string);
|
||||
for (const auto &word : list) {
|
||||
uniqueFirst[word[0]].emplace(id);
|
||||
uniqueFull[id].emplace(std::make_pair(word, weight));
|
||||
}
|
||||
};
|
||||
for (const auto &[path, file] : data.files) {
|
||||
for (const auto &[normalized, question] : file.questions) {
|
||||
const auto id = std::make_pair(path, normalized);
|
||||
for (const auto &key : question.normalizedKeys) {
|
||||
pushString(id, key, kWeightStep * kWeightStep);
|
||||
}
|
||||
pushString(id, question.question, kWeightStep);
|
||||
pushString(id, question.value, 1);
|
||||
}
|
||||
}
|
||||
|
||||
auto result = TemplatesIndex();
|
||||
for (const auto &[ch, unique] : uniqueFirst) {
|
||||
result.first.emplace(ch, unique | ranges::to_vector);
|
||||
}
|
||||
for (const auto &[id, unique] : uniqueFull) {
|
||||
result.full.emplace(id, unique | ranges::to_vector);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void ReplaceFileIndex(
|
||||
TemplatesIndex &result,
|
||||
TemplatesIndex &&source,
|
||||
const QString &path) {
|
||||
for (auto i = begin(result.full); i != end(result.full);) {
|
||||
if (i->first.first == path) {
|
||||
i = result.full.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
for (auto &[id, list] : source.full) {
|
||||
result.full.emplace(id, std::move(list));
|
||||
}
|
||||
|
||||
using Id = TemplatesIndex::Id;
|
||||
for (auto &[ch, list] : result.first) {
|
||||
auto i = ranges::lower_bound(
|
||||
list,
|
||||
std::make_pair(path, QString()));
|
||||
auto j = std::find_if(i, end(list), [&](const Id &id) {
|
||||
return id.first != path;
|
||||
});
|
||||
list.erase(i, j);
|
||||
}
|
||||
for (auto &[ch, list] : source.first) {
|
||||
auto &to = result.first[ch];
|
||||
to.insert(
|
||||
end(to),
|
||||
std::make_move_iterator(begin(list)),
|
||||
std::make_move_iterator(end(list)));
|
||||
ranges::sort(to);
|
||||
}
|
||||
}
|
||||
|
||||
void MoveKeys(TemplatesFile &to, const TemplatesFile &from) {
|
||||
const auto &existing = from.questions;
|
||||
for (auto &[normalized, question] : to.questions) {
|
||||
if (const auto i = existing.find(normalized); i != end(existing)) {
|
||||
question.originalKeys = i->second.originalKeys;
|
||||
question.normalizedKeys = i->second.normalizedKeys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Delta ComputeDelta(const TemplatesFile &was, const TemplatesFile &now) {
|
||||
auto result = Delta();
|
||||
for (const auto &[normalized, question] : now.questions) {
|
||||
const auto i = was.questions.find(normalized);
|
||||
if (i == end(was.questions)) {
|
||||
result.added.push_back(&question);
|
||||
} else {
|
||||
result.keys.emplace(normalized, i->second.originalKeys);
|
||||
if (i->second.value != question.value) {
|
||||
result.changed.push_back(&question);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto &[normalized, question] : was.questions) {
|
||||
if (result.keys.find(normalized) == end(result.keys)) {
|
||||
result.removed.push_back(&question);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString FormatUpdateNotification(const QString &path, const Delta &delta) {
|
||||
auto result = u"Template file '%1' updated!\n\n"_q.arg(path);
|
||||
if (!delta.added.empty()) {
|
||||
result += u"-------- Added --------\n\n"_q;
|
||||
for (const auto question : delta.added) {
|
||||
result += u"Q: %1\nK: %2\nA: %3\n\n"_q.arg(
|
||||
question->question,
|
||||
question->originalKeys.join(u", "_q),
|
||||
question->value.trimmed());
|
||||
}
|
||||
}
|
||||
if (!delta.changed.empty()) {
|
||||
result += u"-------- Modified --------\n\n"_q;
|
||||
for (const auto question : delta.changed) {
|
||||
result += u"Q: %1\nA: %2\n\n"_q.arg(
|
||||
question->question,
|
||||
question->value.trimmed());
|
||||
}
|
||||
}
|
||||
if (!delta.removed.empty()) {
|
||||
result += u"-------- Removed --------\n\n"_q;
|
||||
for (const auto question : delta.removed) {
|
||||
result += u"Q: %1\n\n"_q.arg(question->question);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString UpdateFile(
|
||||
const QString &path,
|
||||
const QByteArray &content,
|
||||
const QString &url,
|
||||
const Delta &delta) {
|
||||
auto result = QString();
|
||||
const auto full = cWorkingDir() + "TEMPLATES/" + path;
|
||||
const auto old = full + u".old"_q;
|
||||
QFile(old).remove();
|
||||
if (QFile(full).copy(old)) {
|
||||
result += u"(old file saved at '%1')"_q.arg(path + u".old"_q);
|
||||
|
||||
QFile f(full);
|
||||
if (f.open(QIODevice::WriteOnly)) {
|
||||
WriteWithOwnUrlAndKeys(f, content, url, delta);
|
||||
} else {
|
||||
result += u"\n\nError: could not open new file '%1'!"_q.arg(full);
|
||||
}
|
||||
} else {
|
||||
result += u"Error: could not save old file '%1'!"_q.arg(old);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int CountMaxKeyLength(const TemplatesData &data) {
|
||||
auto result = 0;
|
||||
for (const auto &[path, file] : data.files) {
|
||||
for (const auto &[normalized, question] : file.questions) {
|
||||
for (const auto &key : question.normalizedKeys) {
|
||||
accumulate_max(result, int(key.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace details
|
||||
|
||||
using namespace details;
|
||||
|
||||
struct Templates::Updates {
|
||||
QNetworkAccessManager manager;
|
||||
std::map<QString, QNetworkReply*> requests;
|
||||
};
|
||||
|
||||
Templates::Templates(not_null<Main::Session*> session) : _session(session) {
|
||||
load();
|
||||
Shortcuts::Requests(
|
||||
) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {
|
||||
using Command = Shortcuts::Command;
|
||||
request->check(
|
||||
Command::SupportReloadTemplates
|
||||
) && request->handle([=] {
|
||||
reload();
|
||||
return true;
|
||||
});
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Templates::reload() {
|
||||
_reloadToastSubscription = errors(
|
||||
) | rpl::on_next([=](QStringList errors) {
|
||||
Ui::Toast::Show(errors.isEmpty()
|
||||
? "Templates reloaded!"
|
||||
: ("Errors:\n\n" + errors.join("\n\n")));
|
||||
});
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
void Templates::load() {
|
||||
if (_reloadAfterRead) {
|
||||
return;
|
||||
} else if (_reading || _updates) {
|
||||
_reloadAfterRead = true;
|
||||
return;
|
||||
}
|
||||
|
||||
crl::async([=, guard = _reading.make_guard()]() mutable {
|
||||
auto result = ReadFiles(cWorkingDir() + "TEMPLATES");
|
||||
result.index = ComputeIndex(result.result);
|
||||
crl::on_main(std::move(guard), [
|
||||
=,
|
||||
result = std::move(result)
|
||||
]() mutable {
|
||||
setData(std::move(result.result));
|
||||
_index = std::move(result.index);
|
||||
_errors.fire(std::move(result.errors));
|
||||
crl::on_main(this, [=] {
|
||||
if (base::take(_reloadAfterRead)) {
|
||||
reload();
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Templates::setData(TemplatesData &&data) {
|
||||
_data = std::move(data);
|
||||
_maxKeyLength = CountMaxKeyLength(_data);
|
||||
}
|
||||
|
||||
void Templates::ensureUpdatesCreated() {
|
||||
if (_updates) {
|
||||
return;
|
||||
}
|
||||
_updates = std::make_unique<Updates>();
|
||||
QObject::connect(
|
||||
&_updates->manager,
|
||||
&QNetworkAccessManager::finished,
|
||||
[=](QNetworkReply *reply) { updateRequestFinished(reply); });
|
||||
}
|
||||
|
||||
void Templates::update() {
|
||||
const auto sendRequest = [&](const QString &path, const QString &url) {
|
||||
ensureUpdatesCreated();
|
||||
if (_updates->requests.find(path) != end(_updates->requests)) {
|
||||
return;
|
||||
}
|
||||
_updates->requests.emplace(
|
||||
path,
|
||||
_updates->manager.get(QNetworkRequest(url)));
|
||||
};
|
||||
|
||||
for (const auto &[path, file] : _data.files) {
|
||||
if (!file.url.isEmpty()) {
|
||||
sendRequest(path, file.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Templates::updateRequestFinished(QNetworkReply *reply) {
|
||||
reply->deleteLater();
|
||||
|
||||
const auto path = [&] {
|
||||
for (const auto &[file, sent] : _updates->requests) {
|
||||
if (sent == reply) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}();
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
_updates->requests[path] = nullptr;
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
const auto message = (
|
||||
u"Error: template update failed, url '%1', error %2, %3"_q
|
||||
).arg(reply->url().toDisplayString()
|
||||
).arg(reply->error()
|
||||
).arg(reply->errorString());
|
||||
_session->data().serviceNotification({ message });
|
||||
return;
|
||||
}
|
||||
LOG(("Got template from url '%1'"
|
||||
).arg(reply->url().toDisplayString()));
|
||||
const auto content = reply->readAll();
|
||||
crl::async([=, weak = base::make_weak(this)]{
|
||||
auto result = ReadFromBlob(content);
|
||||
auto one = TemplatesData();
|
||||
one.files.emplace(path, std::move(result.result));
|
||||
auto index = ComputeIndex(one);
|
||||
crl::on_main(weak,[
|
||||
=,
|
||||
one = std::move(one),
|
||||
errors = std::move(result.errors),
|
||||
index = std::move(index)
|
||||
]() mutable {
|
||||
auto &existing = _data.files.at(path);
|
||||
auto &parsed = one.files.at(path);
|
||||
MoveKeys(parsed, existing);
|
||||
ReplaceFileIndex(_index, ComputeIndex(one), path);
|
||||
if (!errors.isEmpty()) {
|
||||
_errors.fire(std::move(errors));
|
||||
}
|
||||
if (const auto delta = ComputeDelta(existing, parsed)) {
|
||||
const auto text = FormatUpdateNotification(
|
||||
path,
|
||||
delta);
|
||||
const auto copy = UpdateFile(
|
||||
path,
|
||||
content,
|
||||
existing.url,
|
||||
delta);
|
||||
const auto full = text + copy;
|
||||
_session->data().serviceNotification({ full });
|
||||
}
|
||||
_data.files.at(path) = std::move(one.files.at(path));
|
||||
|
||||
_updates->requests.erase(path);
|
||||
checkUpdateFinished();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Templates::checkUpdateFinished() {
|
||||
if (!_updates || !_updates->requests.empty()) {
|
||||
return;
|
||||
}
|
||||
_updates = nullptr;
|
||||
if (base::take(_reloadAfterRead)) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
auto Templates::matchExact(QString query) const
|
||||
-> std::optional<QuestionByKey> {
|
||||
if (query.isEmpty() || query.size() > _maxKeyLength) {
|
||||
return {};
|
||||
}
|
||||
|
||||
query = NormalizeKey(query);
|
||||
|
||||
for (const auto &[path, file] : _data.files) {
|
||||
for (const auto &[normalized, question] : file.questions) {
|
||||
for (const auto &key : question.normalizedKeys) {
|
||||
if (key == query) {
|
||||
return QuestionByKey{ question, key };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto Templates::matchFromEnd(QString query) const
|
||||
-> std::optional<QuestionByKey> {
|
||||
if (query.size() > _maxKeyLength) {
|
||||
query = query.mid(query.size() - _maxKeyLength);
|
||||
}
|
||||
|
||||
const auto size = query.size();
|
||||
auto queries = std::vector<QString>();
|
||||
queries.reserve(size);
|
||||
for (auto i = 0; i != size; ++i) {
|
||||
queries.push_back(NormalizeKey(query.mid(size - i - 1)));
|
||||
}
|
||||
|
||||
auto result = std::optional<QuestionByKey>();
|
||||
for (const auto &[path, file] : _data.files) {
|
||||
for (const auto &[normalized, question] : file.questions) {
|
||||
for (const auto &key : question.normalizedKeys) {
|
||||
if (key.size() <= queries.size()
|
||||
&& queries[key.size() - 1] == key
|
||||
&& (!result || result->key.size() <= key.size())) {
|
||||
result = QuestionByKey{ question, key };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Templates::~Templates() = default;
|
||||
|
||||
auto Templates::query(const QString &text) const -> std::vector<Question> {
|
||||
const auto words = TextUtilities::PrepareSearchWords(text);
|
||||
const auto questions = [&](const QString &word) {
|
||||
const auto i = _index.first.find(word[0]);
|
||||
return (i == end(_index.first)) ? 0 : i->second.size();
|
||||
};
|
||||
const auto best = ranges::min_element(words, std::less<>(), questions);
|
||||
if (best == std::end(words)) {
|
||||
return {};
|
||||
}
|
||||
const auto narrowed = _index.first.find((*best)[0]);
|
||||
if (narrowed == end(_index.first)) {
|
||||
return {};
|
||||
}
|
||||
using Id = TemplatesIndex::Id;
|
||||
using Term = TemplatesIndex::Term;
|
||||
const auto questionById = [&](const Id &id) {
|
||||
return _data.files.at(id.first).questions.at(id.second);
|
||||
};
|
||||
|
||||
const auto computeWeight = [&](const Id &id) {
|
||||
auto result = 0;
|
||||
const auto full = _index.full.find(id);
|
||||
for (const auto &word : words) {
|
||||
const auto from = ranges::lower_bound(
|
||||
full->second,
|
||||
word,
|
||||
std::less<>(),
|
||||
[](const Term &term) { return term.first; });
|
||||
const auto till = std::find_if(
|
||||
from,
|
||||
end(full->second),
|
||||
[&](const Term &term) {
|
||||
return !term.first.startsWith(word);
|
||||
});
|
||||
const auto weight = std::max_element(
|
||||
from,
|
||||
till,
|
||||
[](const Term &a, const Term &b) {
|
||||
return a.second < b.second;
|
||||
});
|
||||
if (weight == till) {
|
||||
return 0;
|
||||
}
|
||||
result += weight->second * (weight->first == word ? 2 : 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
using Pair = std::pair<Id, int>;
|
||||
const auto pairById = [&](const Id &id) {
|
||||
return std::make_pair(id, computeWeight(id));
|
||||
};
|
||||
const auto sorter = [](const Pair &a, const Pair &b) {
|
||||
// weight DESC filename DESC question ASC
|
||||
if (a.second > b.second) {
|
||||
return true;
|
||||
} else if (a.second < b.second) {
|
||||
return false;
|
||||
} else if (a.first.first > b.first.first) {
|
||||
return true;
|
||||
} else if (a.first.first < b.first.first) {
|
||||
return false;
|
||||
} else {
|
||||
return (a.first.second < b.first.second);
|
||||
}
|
||||
};
|
||||
const auto good = narrowed->second | ranges::views::transform(
|
||||
pairById
|
||||
) | ranges::views::filter([](const Pair &pair) {
|
||||
return pair.second > 0;
|
||||
}) | ranges::to_vector | ranges::actions::stable_sort(sorter);
|
||||
return good | ranges::views::transform([&](const Pair &pair) {
|
||||
return questionById(pair.first);
|
||||
}) | ranges::views::take(kQueryLimit) | ranges::to_vector;
|
||||
}
|
||||
|
||||
} // namespace Support
|
||||
99
Telegram/SourceFiles/support/support_templates.h
Normal file
99
Telegram/SourceFiles/support/support_templates.h
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
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
|
||||
|
||||
#include "base/binary_guard.h"
|
||||
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Support {
|
||||
namespace details {
|
||||
|
||||
struct TemplatesQuestion {
|
||||
QString question;
|
||||
QStringList originalKeys;
|
||||
QStringList normalizedKeys;
|
||||
QString value;
|
||||
};
|
||||
|
||||
struct TemplatesFile {
|
||||
QString url;
|
||||
std::map<QString, TemplatesQuestion> questions;
|
||||
};
|
||||
|
||||
struct TemplatesData {
|
||||
std::map<QString, TemplatesFile> files;
|
||||
};
|
||||
|
||||
struct TemplatesIndex {
|
||||
using Id = std::pair<QString, QString>; // filename, normalized question
|
||||
using Term = std::pair<QString, int>; // search term, weight
|
||||
|
||||
std::map<QChar, std::vector<Id>> first;
|
||||
std::map<Id, std::vector<Term>> full;
|
||||
};
|
||||
|
||||
} // namespace details
|
||||
|
||||
class Templates : public base::has_weak_ptr {
|
||||
public:
|
||||
explicit Templates(not_null<Main::Session*> session);
|
||||
|
||||
void reload();
|
||||
|
||||
using Question = details::TemplatesQuestion;
|
||||
std::vector<Question> query(const QString &text) const;
|
||||
|
||||
auto errors() const {
|
||||
return _errors.events();
|
||||
}
|
||||
|
||||
struct QuestionByKey {
|
||||
Question question;
|
||||
QString key;
|
||||
};
|
||||
std::optional<QuestionByKey> matchExact(QString text) const;
|
||||
std::optional<QuestionByKey> matchFromEnd(QString text) const;
|
||||
int maxKeyLength() const {
|
||||
return _maxKeyLength;
|
||||
}
|
||||
|
||||
~Templates();
|
||||
|
||||
private:
|
||||
struct Updates;
|
||||
|
||||
void load();
|
||||
void update();
|
||||
void ensureUpdatesCreated();
|
||||
void updateRequestFinished(QNetworkReply *reply);
|
||||
void checkUpdateFinished();
|
||||
void setData(details::TemplatesData &&data);
|
||||
|
||||
not_null<Main::Session*> _session;
|
||||
|
||||
details::TemplatesData _data;
|
||||
details::TemplatesIndex _index;
|
||||
rpl::event_stream<QStringList> _errors;
|
||||
base::binary_guard _reading;
|
||||
bool _reloadAfterRead = false;
|
||||
rpl::lifetime _reloadToastSubscription;
|
||||
|
||||
int _maxKeyLength = 0;
|
||||
|
||||
std::unique_ptr<Updates> _updates;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Support
|
||||
Reference in New Issue
Block a user