Read and autocomplete templates (support).

This commit is contained in:
John Preston 2018-10-02 23:39:54 +03:00
parent 36f72191ad
commit ccaec28d0b
12 changed files with 849 additions and 4 deletions

View File

@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/send_files_box.h"
#include "ui/widgets/input_fields.h"
#include "support/support_common.h"
#include "support/support_templates.h"
#include "observer_peer.h"
namespace {
@ -322,7 +323,11 @@ AuthSession::AuthSession(const MTPUser &user)
, _storage(std::make_unique<Storage::Facade>())
, _notifications(std::make_unique<Window::Notifications::System>(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);
_saveDataTimer.setCallback([=] {
@ -422,8 +427,13 @@ void AuthSession::checkAutoLockIn(TimeMs time) {
}
bool AuthSession::supportMode() const {
return true; AssertIsDebug();
return _user->phone().startsWith(qstr("424"));
return (_supportTemplates != nullptr);
}
not_null<Support::Templates*> AuthSession::supportTemplates() const {
Expects(supportMode());
return _supportTemplates.get();
}
AuthSession::~AuthSession() = default;

View File

@ -21,6 +21,7 @@ enum class InputSubmitSettings;
namespace Support {
enum class SwitchSettings;
class Templates;
} // namespace Support
namespace Data {
@ -295,6 +296,7 @@ public:
base::Observable<std::pair<not_null<HistoryItem*>, MsgId>> messageIdChanging;
bool supportMode() const;
not_null<Support::Templates*> supportTemplates() const;
~AuthSession();
@ -321,6 +323,8 @@ private:
// _changelogs depends on _data, subscribes on chats loading event.
const std::unique_ptr<Core::Changelogs> _changelogs;
const std::unique_ptr<Support::Templates> _supportTemplates;
rpl::lifetime _lifetime;
};

View File

@ -256,3 +256,9 @@ mentionFg: windowSubTextFg;
mentionFgOver: windowSubTextFgOver;
mentionFgActive: windowActiveTextFg;
mentionFgOverActive: windowActiveTextFg;
autocompleteSearchPadding: margins(16px, 5px, 16px, 5px);
autocompleteRowPadding: margins(16px, 5px, 16px, 5px);
autocompleteRowTitle: semiboldTextStyle;
autocompleteRowKeys: defaultTextStyle;
autocompleteRowAnswer: defaultTextStyle;

View File

@ -70,6 +70,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/emoji_suggestions_widget.h"
#include "core/crash_reports.h"
#include "support/support_common.h"
#include "support/support_autocomplete.h"
#include "dialogs/dialogs_key.h"
#include "styles/style_history.h"
#include "styles/style_dialogs.h"
@ -421,6 +422,9 @@ HistoryWidget::HistoryWidget(
, _historyDown(_scroll, st::historyToDown)
, _unreadMentions(_scroll, st::historyUnreadMentions)
, _fieldAutocomplete(this)
, _supportAutocomplete(Auth().supportMode()
? object_ptr<Support::Autocomplete>(this, &Auth())
: nullptr)
, _send(this)
, _unblock(this, lang(lng_unblock_button).toUpper(), st::historyUnblock)
, _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(stickerChosen(not_null<DocumentData*>,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerOrGifSend(not_null<DocumentData*>)));
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->list().changes(
) | rpl::start_with_next([=](QStringList &&parsed) {
@ -2178,6 +2190,9 @@ void HistoryWidget::updateControlsVisibility() {
}
_kbShown = false;
_fieldAutocomplete->hide();
if (_supportAutocomplete) {
_supportAutocomplete->hide();
}
_send->hide();
if (_silent) {
_silent->hide();
@ -2270,6 +2285,9 @@ void HistoryWidget::updateControlsVisibility() {
}
} else {
_fieldAutocomplete->hide();
if (_supportAutocomplete) {
_supportAutocomplete->hide();
}
_send->hide();
_unblock->hide();
_botStart->hide();
@ -3006,6 +3024,9 @@ bool HistoryWidget::saveEditMsgFail(History *history, const RPCError &error, mtp
void HistoryWidget::hideSelectorControlsAnimated() {
_fieldAutocomplete->hideAnimated();
if (_supportAutocomplete) {
_supportAutocomplete->hide();
}
if (_tabbedPanel) {
_tabbedPanel->hideAnimated();
}
@ -4818,6 +4839,9 @@ void HistoryWidget::updateControlsGeometry() {
if (_scroll->y() != scrollAreaTop) {
_scroll->moveToLeft(0, scrollAreaTop);
_fieldAutocomplete->setBoundings(_scroll->geometry());
if (_supportAutocomplete) {
_supportAutocomplete->setBoundings(_scroll->geometry());
}
}
if (_reportSpamPanel) {
_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());
if (_supportAutocomplete) {
_supportAutocomplete->setBoundings(_scroll->geometry());
}
if (!_historyDownShown.animating()) {
// _historyDown is a child widget of _scroll, not me.
_historyDown->moveToRight(st::historyToDownPosition.x(), _scroll->height() - _historyDown->height() - st::historyToDownPosition.y());
@ -5412,7 +5439,9 @@ void HistoryWidget::replyToNextMessage() {
}
void HistoryWidget::onFieldTabbed() {
if (!_fieldAutocomplete->isHidden()) {
if (_supportAutocomplete) {
_supportAutocomplete->activate();
} else if (!_fieldAutocomplete->isHidden()) {
_fieldAutocomplete->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab);
}
}

View File

@ -36,6 +36,10 @@ namespace Data {
struct Draft;
} // namespace Data
namespace Support {
class Autocomplete;
} // namespace Support
namespace Ui {
class AbstractButton;
class InnerDropdown;
@ -781,6 +785,7 @@ private:
object_ptr<Ui::HistoryDownButton> _unreadMentions;
object_ptr<FieldAutocomplete> _fieldAutocomplete;
object_ptr<Support::Autocomplete> _supportAutocomplete;
std::unique_ptr<MessageLinksParser> _fieldLinksParser;
UserData *_inlineBot = nullptr;

View 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

View 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

View File

@ -11,6 +11,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
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) {
switch (value) {
case SwitchSettings::Next:

View File

@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Support {
bool ValidateAccount(const MTPUser &self);
enum class SwitchSettings {
None,
Next,

View 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

View 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

View File

@ -576,8 +576,12 @@
<(src_loc)/storage/storage_sparse_ids_list.h
<(src_loc)/storage/storage_user_photos.cpp
<(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.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.h
<(src_loc)/ui/effects/fade_animation.cpp