mirror of
https://github.com/vale981/tdesktop
synced 2025-03-06 10:11:41 -05:00
Read and autocomplete templates (support).
This commit is contained in:
parent
36f72191ad
commit
ccaec28d0b
12 changed files with 849 additions and 4 deletions
|
@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "boxes/send_files_box.h"
|
#include "boxes/send_files_box.h"
|
||||||
#include "ui/widgets/input_fields.h"
|
#include "ui/widgets/input_fields.h"
|
||||||
#include "support/support_common.h"
|
#include "support/support_common.h"
|
||||||
|
#include "support/support_templates.h"
|
||||||
#include "observer_peer.h"
|
#include "observer_peer.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
@ -322,7 +323,11 @@ AuthSession::AuthSession(const MTPUser &user)
|
||||||
, _storage(std::make_unique<Storage::Facade>())
|
, _storage(std::make_unique<Storage::Facade>())
|
||||||
, _notifications(std::make_unique<Window::Notifications::System>(this))
|
, _notifications(std::make_unique<Window::Notifications::System>(this))
|
||||||
, _data(std::make_unique<Data::Session>(this))
|
, _data(std::make_unique<Data::Session>(this))
|
||||||
, _changelogs(Core::Changelogs::Create(this)) {
|
, _changelogs(Core::Changelogs::Create(this))
|
||||||
|
, _supportTemplates(
|
||||||
|
(Support::ValidateAccount(user)
|
||||||
|
? std::make_unique<Support::Templates>(this)
|
||||||
|
: nullptr)) {
|
||||||
App::feedUser(user);
|
App::feedUser(user);
|
||||||
|
|
||||||
_saveDataTimer.setCallback([=] {
|
_saveDataTimer.setCallback([=] {
|
||||||
|
@ -422,8 +427,13 @@ void AuthSession::checkAutoLockIn(TimeMs time) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AuthSession::supportMode() const {
|
bool AuthSession::supportMode() const {
|
||||||
return true; AssertIsDebug();
|
return (_supportTemplates != nullptr);
|
||||||
return _user->phone().startsWith(qstr("424"));
|
}
|
||||||
|
|
||||||
|
not_null<Support::Templates*> AuthSession::supportTemplates() const {
|
||||||
|
Expects(supportMode());
|
||||||
|
|
||||||
|
return _supportTemplates.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthSession::~AuthSession() = default;
|
AuthSession::~AuthSession() = default;
|
||||||
|
|
|
@ -21,6 +21,7 @@ enum class InputSubmitSettings;
|
||||||
|
|
||||||
namespace Support {
|
namespace Support {
|
||||||
enum class SwitchSettings;
|
enum class SwitchSettings;
|
||||||
|
class Templates;
|
||||||
} // namespace Support
|
} // namespace Support
|
||||||
|
|
||||||
namespace Data {
|
namespace Data {
|
||||||
|
@ -295,6 +296,7 @@ public:
|
||||||
base::Observable<std::pair<not_null<HistoryItem*>, MsgId>> messageIdChanging;
|
base::Observable<std::pair<not_null<HistoryItem*>, MsgId>> messageIdChanging;
|
||||||
|
|
||||||
bool supportMode() const;
|
bool supportMode() const;
|
||||||
|
not_null<Support::Templates*> supportTemplates() const;
|
||||||
|
|
||||||
~AuthSession();
|
~AuthSession();
|
||||||
|
|
||||||
|
@ -321,6 +323,8 @@ private:
|
||||||
// _changelogs depends on _data, subscribes on chats loading event.
|
// _changelogs depends on _data, subscribes on chats loading event.
|
||||||
const std::unique_ptr<Core::Changelogs> _changelogs;
|
const std::unique_ptr<Core::Changelogs> _changelogs;
|
||||||
|
|
||||||
|
const std::unique_ptr<Support::Templates> _supportTemplates;
|
||||||
|
|
||||||
rpl::lifetime _lifetime;
|
rpl::lifetime _lifetime;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -256,3 +256,9 @@ mentionFg: windowSubTextFg;
|
||||||
mentionFgOver: windowSubTextFgOver;
|
mentionFgOver: windowSubTextFgOver;
|
||||||
mentionFgActive: windowActiveTextFg;
|
mentionFgActive: windowActiveTextFg;
|
||||||
mentionFgOverActive: windowActiveTextFg;
|
mentionFgOverActive: windowActiveTextFg;
|
||||||
|
|
||||||
|
autocompleteSearchPadding: margins(16px, 5px, 16px, 5px);
|
||||||
|
autocompleteRowPadding: margins(16px, 5px, 16px, 5px);
|
||||||
|
autocompleteRowTitle: semiboldTextStyle;
|
||||||
|
autocompleteRowKeys: defaultTextStyle;
|
||||||
|
autocompleteRowAnswer: defaultTextStyle;
|
||||||
|
|
|
@ -70,6 +70,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||||
#include "core/crash_reports.h"
|
#include "core/crash_reports.h"
|
||||||
#include "support/support_common.h"
|
#include "support/support_common.h"
|
||||||
|
#include "support/support_autocomplete.h"
|
||||||
#include "dialogs/dialogs_key.h"
|
#include "dialogs/dialogs_key.h"
|
||||||
#include "styles/style_history.h"
|
#include "styles/style_history.h"
|
||||||
#include "styles/style_dialogs.h"
|
#include "styles/style_dialogs.h"
|
||||||
|
@ -421,6 +422,9 @@ HistoryWidget::HistoryWidget(
|
||||||
, _historyDown(_scroll, st::historyToDown)
|
, _historyDown(_scroll, st::historyToDown)
|
||||||
, _unreadMentions(_scroll, st::historyUnreadMentions)
|
, _unreadMentions(_scroll, st::historyUnreadMentions)
|
||||||
, _fieldAutocomplete(this)
|
, _fieldAutocomplete(this)
|
||||||
|
, _supportAutocomplete(Auth().supportMode()
|
||||||
|
? object_ptr<Support::Autocomplete>(this, &Auth())
|
||||||
|
: nullptr)
|
||||||
, _send(this)
|
, _send(this)
|
||||||
, _unblock(this, lang(lng_unblock_button).toUpper(), st::historyUnblock)
|
, _unblock(this, lang(lng_unblock_button).toUpper(), st::historyUnblock)
|
||||||
, _botStart(this, lang(lng_bot_start).toUpper(), st::historyComposeButton)
|
, _botStart(this, lang(lng_bot_start).toUpper(), st::historyComposeButton)
|
||||||
|
@ -523,6 +527,14 @@ HistoryWidget::HistoryWidget(
|
||||||
connect(_fieldAutocomplete, SIGNAL(botCommandChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod)));
|
connect(_fieldAutocomplete, SIGNAL(botCommandChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod)));
|
||||||
connect(_fieldAutocomplete, SIGNAL(stickerChosen(not_null<DocumentData*>,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerOrGifSend(not_null<DocumentData*>)));
|
connect(_fieldAutocomplete, SIGNAL(stickerChosen(not_null<DocumentData*>,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerOrGifSend(not_null<DocumentData*>)));
|
||||||
connect(_fieldAutocomplete, SIGNAL(moderateKeyActivate(int,bool*)), this, SLOT(onModerateKeyActivate(int,bool*)));
|
connect(_fieldAutocomplete, SIGNAL(moderateKeyActivate(int,bool*)), this, SLOT(onModerateKeyActivate(int,bool*)));
|
||||||
|
if (_supportAutocomplete) {
|
||||||
|
_supportAutocomplete->hide();
|
||||||
|
_supportAutocomplete->insertRequests(
|
||||||
|
) | rpl::start_with_next([=](const QString &text) {
|
||||||
|
_field->setFocus();
|
||||||
|
_field->textCursor().insertText(text);
|
||||||
|
}, lifetime());
|
||||||
|
}
|
||||||
_fieldLinksParser = std::make_unique<MessageLinksParser>(_field);
|
_fieldLinksParser = std::make_unique<MessageLinksParser>(_field);
|
||||||
_fieldLinksParser->list().changes(
|
_fieldLinksParser->list().changes(
|
||||||
) | rpl::start_with_next([=](QStringList &&parsed) {
|
) | rpl::start_with_next([=](QStringList &&parsed) {
|
||||||
|
@ -2178,6 +2190,9 @@ void HistoryWidget::updateControlsVisibility() {
|
||||||
}
|
}
|
||||||
_kbShown = false;
|
_kbShown = false;
|
||||||
_fieldAutocomplete->hide();
|
_fieldAutocomplete->hide();
|
||||||
|
if (_supportAutocomplete) {
|
||||||
|
_supportAutocomplete->hide();
|
||||||
|
}
|
||||||
_send->hide();
|
_send->hide();
|
||||||
if (_silent) {
|
if (_silent) {
|
||||||
_silent->hide();
|
_silent->hide();
|
||||||
|
@ -2270,6 +2285,9 @@ void HistoryWidget::updateControlsVisibility() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_fieldAutocomplete->hide();
|
_fieldAutocomplete->hide();
|
||||||
|
if (_supportAutocomplete) {
|
||||||
|
_supportAutocomplete->hide();
|
||||||
|
}
|
||||||
_send->hide();
|
_send->hide();
|
||||||
_unblock->hide();
|
_unblock->hide();
|
||||||
_botStart->hide();
|
_botStart->hide();
|
||||||
|
@ -3006,6 +3024,9 @@ bool HistoryWidget::saveEditMsgFail(History *history, const RPCError &error, mtp
|
||||||
|
|
||||||
void HistoryWidget::hideSelectorControlsAnimated() {
|
void HistoryWidget::hideSelectorControlsAnimated() {
|
||||||
_fieldAutocomplete->hideAnimated();
|
_fieldAutocomplete->hideAnimated();
|
||||||
|
if (_supportAutocomplete) {
|
||||||
|
_supportAutocomplete->hide();
|
||||||
|
}
|
||||||
if (_tabbedPanel) {
|
if (_tabbedPanel) {
|
||||||
_tabbedPanel->hideAnimated();
|
_tabbedPanel->hideAnimated();
|
||||||
}
|
}
|
||||||
|
@ -4818,6 +4839,9 @@ void HistoryWidget::updateControlsGeometry() {
|
||||||
if (_scroll->y() != scrollAreaTop) {
|
if (_scroll->y() != scrollAreaTop) {
|
||||||
_scroll->moveToLeft(0, scrollAreaTop);
|
_scroll->moveToLeft(0, scrollAreaTop);
|
||||||
_fieldAutocomplete->setBoundings(_scroll->geometry());
|
_fieldAutocomplete->setBoundings(_scroll->geometry());
|
||||||
|
if (_supportAutocomplete) {
|
||||||
|
_supportAutocomplete->setBoundings(_scroll->geometry());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (_reportSpamPanel) {
|
if (_reportSpamPanel) {
|
||||||
_reportSpamPanel->setGeometryToLeft(0, _scroll->y(), width(), _reportSpamPanel->height());
|
_reportSpamPanel->setGeometryToLeft(0, _scroll->y(), width(), _reportSpamPanel->height());
|
||||||
|
@ -4998,6 +5022,9 @@ void HistoryWidget::updateHistoryGeometry(bool initial, bool loadedDown, const S
|
||||||
}
|
}
|
||||||
|
|
||||||
_fieldAutocomplete->setBoundings(_scroll->geometry());
|
_fieldAutocomplete->setBoundings(_scroll->geometry());
|
||||||
|
if (_supportAutocomplete) {
|
||||||
|
_supportAutocomplete->setBoundings(_scroll->geometry());
|
||||||
|
}
|
||||||
if (!_historyDownShown.animating()) {
|
if (!_historyDownShown.animating()) {
|
||||||
// _historyDown is a child widget of _scroll, not me.
|
// _historyDown is a child widget of _scroll, not me.
|
||||||
_historyDown->moveToRight(st::historyToDownPosition.x(), _scroll->height() - _historyDown->height() - st::historyToDownPosition.y());
|
_historyDown->moveToRight(st::historyToDownPosition.x(), _scroll->height() - _historyDown->height() - st::historyToDownPosition.y());
|
||||||
|
@ -5412,7 +5439,9 @@ void HistoryWidget::replyToNextMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryWidget::onFieldTabbed() {
|
void HistoryWidget::onFieldTabbed() {
|
||||||
if (!_fieldAutocomplete->isHidden()) {
|
if (_supportAutocomplete) {
|
||||||
|
_supportAutocomplete->activate();
|
||||||
|
} else if (!_fieldAutocomplete->isHidden()) {
|
||||||
_fieldAutocomplete->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab);
|
_fieldAutocomplete->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,10 @@ namespace Data {
|
||||||
struct Draft;
|
struct Draft;
|
||||||
} // namespace Data
|
} // namespace Data
|
||||||
|
|
||||||
|
namespace Support {
|
||||||
|
class Autocomplete;
|
||||||
|
} // namespace Support
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class AbstractButton;
|
class AbstractButton;
|
||||||
class InnerDropdown;
|
class InnerDropdown;
|
||||||
|
@ -781,6 +785,7 @@ private:
|
||||||
object_ptr<Ui::HistoryDownButton> _unreadMentions;
|
object_ptr<Ui::HistoryDownButton> _unreadMentions;
|
||||||
|
|
||||||
object_ptr<FieldAutocomplete> _fieldAutocomplete;
|
object_ptr<FieldAutocomplete> _fieldAutocomplete;
|
||||||
|
object_ptr<Support::Autocomplete> _supportAutocomplete;
|
||||||
std::unique_ptr<MessageLinksParser> _fieldLinksParser;
|
std::unique_ptr<MessageLinksParser> _fieldLinksParser;
|
||||||
|
|
||||||
UserData *_inlineBot = nullptr;
|
UserData *_inlineBot = nullptr;
|
||||||
|
|
361
Telegram/SourceFiles/support/support_autocomplete.cpp
Normal file
361
Telegram/SourceFiles/support/support_autocomplete.cpp
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
/*
|
||||||
|
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/widgets/scroll_area.h"
|
||||||
|
#include "ui/widgets/input_fields.h"
|
||||||
|
#include "ui/wrap/padding_wrap.h"
|
||||||
|
#include "support/support_templates.h"
|
||||||
|
#include "auth_session.h"
|
||||||
|
#include "styles/style_chat_helpers.h"
|
||||||
|
#include "styles/style_window.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;
|
||||||
|
Text question = { st::windowMinWidth / 2 };
|
||||||
|
Text keys = { st::windowMinWidth / 2 };
|
||||||
|
Text 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;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
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());
|
||||||
|
update();
|
||||||
|
_selected = _pressed = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.keys.join(qstr(", ")));
|
||||||
|
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()
|
||||||
|
+ row.question.countHeight(available)
|
||||||
|
+ row.keys.countHeight(available)
|
||||||
|
+ row.answer.countHeight(available)
|
||||||
|
+ 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 Text &text) {
|
||||||
|
text.drawLeft(
|
||||||
|
p,
|
||||||
|
padding.left(),
|
||||||
|
top,
|
||||||
|
available,
|
||||||
|
width());
|
||||||
|
top += text.countHeight(available);
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
p.setPen(over ? st::mentionFgOver : st::mentionFg);
|
||||||
|
drawText(i->keys);
|
||||||
|
p.setPen(st::windowFg);
|
||||||
|
drawText(i->answer);
|
||||||
|
|
||||||
|
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({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Autocomplete::Autocomplete(QWidget *parent, not_null<AuthSession*> session)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _session(session) {
|
||||||
|
setupContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Autocomplete::activate() {
|
||||||
|
_activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
return _insertRequests.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::gifsSearchField,
|
||||||
|
[] { return "Search for templates"; }),
|
||||||
|
st::autocompleteSearchPadding);
|
||||||
|
const auto input = inputWrap->entity();
|
||||||
|
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(
|
||||||
|
this,
|
||||||
|
st::mentionScroll);
|
||||||
|
|
||||||
|
const auto inner = scroll->setOwnedWidget(object_ptr<Inner>(scroll));
|
||||||
|
|
||||||
|
const auto submit = [=] {
|
||||||
|
if (const auto question = inner->selected()) {
|
||||||
|
_insertRequests.fire_copy(question->value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto refresh = [=] {
|
||||||
|
inner->showRows(
|
||||||
|
_session->supportTemplates()->query(input->getLastText()));
|
||||||
|
scroll->scrollToY(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
inner->activated() | rpl::start_with_next(submit, lifetime());
|
||||||
|
connect(input, &Ui::InputField::blurred, [=] {
|
||||||
|
App::CallDelayed(10, this, [=] {
|
||||||
|
if (!input->hasFocus()) {
|
||||||
|
deactivate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
connect(input, &Ui::InputField::cancelled, [=] { deactivate(); });
|
||||||
|
connect(input, &Ui::InputField::changed, refresh);
|
||||||
|
connect(input, &Ui::InputField::submitted, submit);
|
||||||
|
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::start_with_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::start_with_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());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Support
|
45
Telegram/SourceFiles/support/support_autocomplete.h
Normal file
45
Telegram/SourceFiles/support/support_autocomplete.h
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
class AuthSession;
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class ScrollArea;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
namespace Support {
|
||||||
|
|
||||||
|
class Autocomplete : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
Autocomplete(QWidget *parent, not_null<AuthSession*> session);
|
||||||
|
|
||||||
|
void activate();
|
||||||
|
void deactivate();
|
||||||
|
void setBoundings(QRect rect);
|
||||||
|
|
||||||
|
rpl::producer<QString> insertRequests();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void keyPressEvent(QKeyEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupContent();
|
||||||
|
|
||||||
|
not_null<AuthSession*> _session;
|
||||||
|
Fn<void()> _activate;
|
||||||
|
Fn<void()> _deactivate;
|
||||||
|
Fn<void(int delta)> _moveSelection;
|
||||||
|
|
||||||
|
rpl::event_stream<QString> _insertRequests;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} //namespace Support
|
|
@ -11,6 +11,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
|
||||||
namespace Support {
|
namespace Support {
|
||||||
|
|
||||||
|
bool ValidateAccount(const MTPUser &self) {
|
||||||
|
return true; AssertIsDebug();
|
||||||
|
return self.match([](const MTPDuser &data) {
|
||||||
|
return data.has_phone() && qs(data.vphone).startsWith(qstr("424"));
|
||||||
|
}, [](const MTPDuserEmpty &data) {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void PerformSwitch(SwitchSettings value) {
|
void PerformSwitch(SwitchSettings value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case SwitchSettings::Next:
|
case SwitchSettings::Next:
|
||||||
|
|
|
@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
|
||||||
namespace Support {
|
namespace Support {
|
||||||
|
|
||||||
|
bool ValidateAccount(const MTPUser &self);
|
||||||
|
|
||||||
enum class SwitchSettings {
|
enum class SwitchSettings {
|
||||||
None,
|
None,
|
||||||
Next,
|
Next,
|
||||||
|
|
302
Telegram/SourceFiles/support/support_templates.cpp
Normal file
302
Telegram/SourceFiles/support/support_templates.cpp
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
namespace Support {
|
||||||
|
namespace details {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kQueryLimit = 10;
|
||||||
|
constexpr auto kWeightStep = 1000;
|
||||||
|
|
||||||
|
bool IsTemplatesFile(const QString &file) {
|
||||||
|
return file.startsWith(qstr("tl_"), Qt::CaseInsensitive)
|
||||||
|
&& file.endsWith(qstr(".txt"), 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FileResult {
|
||||||
|
TemplatesFile result;
|
||||||
|
QStringList errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
FileResult ReadFromBlob(const QByteArray &blob) {
|
||||||
|
auto result = FileResult();
|
||||||
|
const auto lines = blob.split('\n');
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
None,
|
||||||
|
Question,
|
||||||
|
Keys,
|
||||||
|
Value,
|
||||||
|
MoreValue,
|
||||||
|
Url,
|
||||||
|
};
|
||||||
|
auto state = State::None;
|
||||||
|
QStringList keys;
|
||||||
|
QString question, value;
|
||||||
|
const auto pushQuestion = [&] {
|
||||||
|
const auto normalized = NormalizeQuestion(question);
|
||||||
|
if (!normalized.isEmpty()) {
|
||||||
|
result.result.questions.emplace(
|
||||||
|
normalized,
|
||||||
|
TemplatesQuestion{ question, keys, value });
|
||||||
|
}
|
||||||
|
question = value = QString();
|
||||||
|
keys = QStringList();
|
||||||
|
};
|
||||||
|
for (const auto &utf : lines) {
|
||||||
|
const auto line = QString::fromUtf8(utf).trimmed();
|
||||||
|
const auto match = QRegularExpression(
|
||||||
|
qsl("^\\{([A-Z_]+)\\}$")
|
||||||
|
).match(line);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
const auto token = match.captured(1);
|
||||||
|
if (state == State::Value || state == State::MoreValue) {
|
||||||
|
pushQuestion();
|
||||||
|
}
|
||||||
|
if (token == qstr("VALUE")) {
|
||||||
|
state = value.isEmpty() ? State::Value : State::None;
|
||||||
|
} else if (token == qstr("KEYS")) {
|
||||||
|
state = keys.isEmpty() ? State::Keys : State::None;
|
||||||
|
} else if (token == qstr("QUESTION")) {
|
||||||
|
state = State::Question;
|
||||||
|
} else if (token == qstr("URL")) {
|
||||||
|
state = State::Url;
|
||||||
|
} else {
|
||||||
|
state = State::None;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case State::Keys:
|
||||||
|
if (!line.isEmpty()) {
|
||||||
|
keys.push_back(line);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State::MoreValue:
|
||||||
|
value += '\n';
|
||||||
|
[[fallthrough]];
|
||||||
|
case State::Value:
|
||||||
|
value += line;
|
||||||
|
state = State::MoreValue;
|
||||||
|
break;
|
||||||
|
case State::Question:
|
||||||
|
if (question.isEmpty()) question = line;
|
||||||
|
break;
|
||||||
|
case State::Url:
|
||||||
|
if (result.result.url.isEmpty()) {
|
||||||
|
result.result.url = line;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushQuestion();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileResult ReadFile(const QString &path) {
|
||||||
|
QFile f(path);
|
||||||
|
if (!f.open(QIODevice::ReadOnly)) {
|
||||||
|
auto result = FileResult();
|
||||||
|
result.errors.push_back(
|
||||||
|
qsl("Couldn't open '%1' for reading!").arg(path));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto blob = f.readAll();
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
return ReadFromBlob(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.keys) {
|
||||||
|
pushString(id, key, kWeightStep * kWeightStep);
|
||||||
|
}
|
||||||
|
pushString(id, question.question, kWeightStep);
|
||||||
|
pushString(id, question.value, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto to_vector = [](auto &&range) {
|
||||||
|
return range | ranges::to_vector;
|
||||||
|
};
|
||||||
|
auto result = TemplatesIndex();
|
||||||
|
for (const auto &[ch, unique] : uniqueFirst) {
|
||||||
|
result.first.emplace(ch, to_vector(unique));
|
||||||
|
}
|
||||||
|
for (const auto &[id, unique] : uniqueFull) {
|
||||||
|
result.full.emplace(id, to_vector(unique));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace details
|
||||||
|
|
||||||
|
Templates::Templates(not_null<AuthSession*> session) : _session(session) {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Templates::reload() {
|
||||||
|
if (_reloadAfterRead) {
|
||||||
|
return;
|
||||||
|
} else if (_reading.alive()) {
|
||||||
|
_reloadAfterRead = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [left, right] = base::make_binary_guard();
|
||||||
|
_reading = std::move(left);
|
||||||
|
crl::async([=, guard = std::move(right)]() mutable {
|
||||||
|
auto result = details::ReadFiles(cWorkingDir() + "TEMPLATES");
|
||||||
|
result.index = details::ComputeIndex(result.result);
|
||||||
|
crl::on_main([
|
||||||
|
=,
|
||||||
|
result = std::move(result),
|
||||||
|
guard = std::move(guard)
|
||||||
|
]() mutable {
|
||||||
|
if (!guard.alive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_data = 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = details::TemplatesIndex::Id;
|
||||||
|
using Term = details::TemplatesIndex::Term;
|
||||||
|
const auto questionById = [&](const Id &id) {
|
||||||
|
return _data.files.at(id.first).questions.at(id.second);
|
||||||
|
};
|
||||||
|
|
||||||
|
using Pair = std::pair<Question, int>;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
const auto pairById = [&](const Id &id) {
|
||||||
|
return std::make_pair(questionById(id), computeWeight(id));
|
||||||
|
};
|
||||||
|
const auto good = narrowed->second | ranges::view::transform(
|
||||||
|
pairById
|
||||||
|
) | ranges::view::filter([](const Pair &pair) {
|
||||||
|
return pair.second > 0;
|
||||||
|
}) | ranges::to_vector | ranges::action::sort(
|
||||||
|
std::greater<>(),
|
||||||
|
[](const Pair &pair) { return pair.second; }
|
||||||
|
);
|
||||||
|
return good | ranges::view::transform([](const Pair &pair) {
|
||||||
|
return pair.first;
|
||||||
|
}) | ranges::view::take(details::kQueryLimit) | ranges::to_vector;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Templates::update() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Support
|
68
Telegram/SourceFiles/support/support_templates.h
Normal file
68
Telegram/SourceFiles/support/support_templates.h
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
class AuthSession;
|
||||||
|
|
||||||
|
namespace Support {
|
||||||
|
namespace details {
|
||||||
|
|
||||||
|
struct TemplatesQuestion {
|
||||||
|
QString question;
|
||||||
|
QStringList keys;
|
||||||
|
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<AuthSession*> session);
|
||||||
|
|
||||||
|
void reload();
|
||||||
|
|
||||||
|
using Question = details::TemplatesQuestion;
|
||||||
|
std::vector<Question> query(const QString &text) const;
|
||||||
|
|
||||||
|
auto errors() const {
|
||||||
|
return _errors.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void update();
|
||||||
|
|
||||||
|
not_null<AuthSession*> _session;
|
||||||
|
|
||||||
|
details::TemplatesData _data;
|
||||||
|
details::TemplatesIndex _index;
|
||||||
|
rpl::event_stream<QStringList> _errors;
|
||||||
|
base::binary_guard _reading;
|
||||||
|
bool _reloadAfterRead = false;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Support
|
|
@ -576,8 +576,12 @@
|
||||||
<(src_loc)/storage/storage_sparse_ids_list.h
|
<(src_loc)/storage/storage_sparse_ids_list.h
|
||||||
<(src_loc)/storage/storage_user_photos.cpp
|
<(src_loc)/storage/storage_user_photos.cpp
|
||||||
<(src_loc)/storage/storage_user_photos.h
|
<(src_loc)/storage/storage_user_photos.h
|
||||||
|
<(src_loc)/support/support_autocomplete.cpp
|
||||||
|
<(src_loc)/support/support_autocomplete.h
|
||||||
<(src_loc)/support/support_common.cpp
|
<(src_loc)/support/support_common.cpp
|
||||||
<(src_loc)/support/support_common.h
|
<(src_loc)/support/support_common.h
|
||||||
|
<(src_loc)/support/support_templates.cpp
|
||||||
|
<(src_loc)/support/support_templates.h
|
||||||
<(src_loc)/ui/effects/cross_animation.cpp
|
<(src_loc)/ui/effects/cross_animation.cpp
|
||||||
<(src_loc)/ui/effects/cross_animation.h
|
<(src_loc)/ui/effects/cross_animation.h
|
||||||
<(src_loc)/ui/effects/fade_animation.cpp
|
<(src_loc)/ui/effects/fade_animation.cpp
|
||||||
|
|
Loading…
Add table
Reference in a new issue