mirror of
https://github.com/telegramdesktop/tdesktop
synced 2024-12-26 00:12:25 +00:00
Read and autocomplete templates (support).
This commit is contained in:
parent
36f72191ad
commit
ccaec28d0b
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
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 {
|
||||
|
||||
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:
|
||||
|
@ -9,6 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace Support {
|
||||
|
||||
bool ValidateAccount(const MTPUser &self);
|
||||
|
||||
enum class SwitchSettings {
|
||||
None,
|
||||
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_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
|
||||
|
Loading…
Reference in New Issue
Block a user