init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,575 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "support/support_autocomplete.h"
#include "ui/chat/chat_theme.h"
#include "ui/chat/chat_style.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/painter.h"
#include "support/support_templates.h"
#include "support/support_common.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_service_message.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "base/call_delayed.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "apiwrap.h"
#include "window/window_session_controller.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_window.h"
#include "styles/style_layers.h"
namespace Support {
namespace {
class Inner : public Ui::RpWidget {
public:
Inner(QWidget *parent);
using Question = details::TemplatesQuestion;
void showRows(std::vector<Question> &&rows);
std::pair<int, int> moveSelection(int delta);
std::optional<Question> selected() const;
auto activated() const {
return _activated.events();
}
protected:
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void leaveEventHook(QEvent *e) override;
int resizeGetHeight(int newWidth) override;
private:
struct Row {
Question data;
Ui::Text::String question = { st::windowMinWidth / 2 };
Ui::Text::String keys = { st::windowMinWidth / 2 };
Ui::Text::String answer = { st::windowMinWidth / 2 };
int top = 0;
int height = 0;
};
void prepareRow(Row &row);
int resizeRowGetHeight(Row &row, int newWidth);
void setSelected(int selected);
std::vector<Row> _rows;
int _selected = -1;
int _pressed = -1;
bool _selectByKeys = false;
rpl::event_stream<> _activated;
};
int TextHeight(const Ui::Text::String &text, int available, int lines) {
Expects(text.style() != nullptr);
const auto st = text.style();
const auto line = st->lineHeight ? st->lineHeight : st->font->height;
return std::min(text.countHeight(available), lines * line);
};
Inner::Inner(QWidget *parent) : RpWidget(parent) {
setMouseTracking(true);
}
void Inner::showRows(std::vector<Question> &&rows) {
_rows.resize(0);
_rows.reserve(rows.size());
for (auto &row : rows) {
_rows.push_back({ std::move(row) });
auto &added = _rows.back();
prepareRow(added);
}
resizeToWidth(width());
_selected = _pressed = -1;
moveSelection(1);
update();
}
std::pair<int, int> Inner::moveSelection(int delta) {
const auto selected = _selected + delta;
if (selected >= 0 && selected < _rows.size()) {
_selectByKeys = true;
setSelected(selected);
const auto top = _rows[_selected].top;
return { top, top + _rows[_selected].height };
}
return { -1, -1 };
}
auto Inner::selected() const -> std::optional<Question> {
if (_rows.empty()) {
return std::nullopt;
} else if (_selected < 0) {
return _rows[0].data;
}
return _rows[_selected].data;
}
void Inner::prepareRow(Row &row) {
row.question.setText(st::autocompleteRowTitle, row.data.question);
row.keys.setText(
st::autocompleteRowKeys,
row.data.originalKeys.join(u", "_q));
row.answer.setText(st::autocompleteRowAnswer, row.data.value);
}
int Inner::resizeRowGetHeight(Row &row, int newWidth) {
const auto available = newWidth
- st::autocompleteRowPadding.left()
- st::autocompleteRowPadding.right();
return row.height = st::autocompleteRowPadding.top()
+ TextHeight(row.question, available, 1)
+ TextHeight(row.keys, available, 1)
+ TextHeight(row.answer, available, 2)
+ st::autocompleteRowPadding.bottom()
+ st::lineWidth;
}
int Inner::resizeGetHeight(int newWidth) {
auto top = 0;
for (auto &row : _rows) {
row.top = top;
top += resizeRowGetHeight(row, newWidth);
}
return top ? (top - st::lineWidth) : (3 * st::mentionHeight);
}
void Inner::paintEvent(QPaintEvent *e) {
Painter p(this);
if (_rows.empty()) {
p.setFont(st::boxTextFont);
p.setPen(st::windowSubTextFg);
p.drawText(
rect(),
"Search by question, keys or value",
style::al_center);
return;
}
const auto clip = e->rect();
const auto from = ranges::upper_bound(
_rows,
clip.y(),
std::less<>(),
[](const Row &row) { return row.top + row.height; });
const auto till = ranges::lower_bound(
_rows,
clip.y() + clip.height(),
std::less<>(),
[](const Row &row) { return row.top; });
if (from == end(_rows)) {
return;
}
p.translate(0, from->top);
const auto padding = st::autocompleteRowPadding;
const auto available = width() - padding.left() - padding.right();
auto top = padding.top();
const auto drawText = [&](const Ui::Text::String &text, int lines) {
text.drawLeftElided(
p,
padding.left(),
top,
available,
width(),
lines);
top += TextHeight(text, available, lines);
};
for (auto i = from; i != till; ++i) {
const auto over = (i - begin(_rows) == _selected);
if (over) {
p.fillRect(0, 0, width(), i->height, st::windowBgOver);
}
p.setPen(st::mentionNameFg);
drawText(i->question, 1);
p.setPen(over ? st::mentionFgOver : st::mentionFg);
drawText(i->keys, 1);
p.setPen(st::windowFg);
drawText(i->answer, 2);
p.translate(0, i->height);
top = padding.top();
if (i - begin(_rows) + 1 == _selected) {
p.fillRect(
0,
-st::lineWidth,
width(),
st::lineWidth,
st::windowBgOver);
} else if (!over) {
p.fillRect(
padding.left(),
-st::lineWidth,
available,
st::lineWidth,
st::shadowFg);
}
}
}
void Inner::mouseMoveEvent(QMouseEvent *e) {
static auto lastGlobalPos = QPoint();
const auto moved = (e->globalPos() != lastGlobalPos);
if (!moved && _selectByKeys) {
return;
}
_selectByKeys = false;
lastGlobalPos = e->globalPos();
const auto i = ranges::upper_bound(
_rows,
e->pos().y(),
std::less<>(),
[](const Row &row) { return row.top + row.height; });
setSelected((i == end(_rows)) ? -1 : (i - begin(_rows)));
}
void Inner::leaveEventHook(QEvent *e) {
setSelected(-1);
}
void Inner::setSelected(int selected) {
if (_selected != selected) {
_selected = selected;
update();
}
}
void Inner::mousePressEvent(QMouseEvent *e) {
_pressed = _selected;
}
void Inner::mouseReleaseEvent(QMouseEvent *e) {
const auto pressed = base::take(_pressed);
if (pressed == _selected && pressed >= 0) {
_activated.fire({});
}
}
AdminLog::OwnedItem GenerateCommentItem(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
const Contact &data) {
if (data.comment.isEmpty()) {
return nullptr;
}
const auto item = history->makeMessage({
.id = history->nextNonHistoryEntryId(),
.flags = (MessageFlag::HasFromId
| MessageFlag::Outgoing
| MessageFlag::FakeHistoryItem),
.from = history->session().userPeerId(),
.date = base::unixtime::now(),
}, TextWithEntities{ data.comment }, MTP_messageMediaEmpty());
return AdminLog::OwnedItem(delegate, item);
}
AdminLog::OwnedItem GenerateContactItem(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
const Contact &data) {
const auto item = history->makeMessage({
.id = history->nextNonHistoryEntryId(),
.flags = (MessageFlag::HasFromId
| MessageFlag::Outgoing
| MessageFlag::FakeHistoryItem),
.from = history->session().userPeerId(),
.date = base::unixtime::now(),
}, TextWithEntities(), MTP_messageMediaContact(
MTP_string(data.phone),
MTP_string(data.firstName),
MTP_string(data.lastName),
MTP_string(), // vcard
MTP_long(0))); // user_id
return AdminLog::OwnedItem(delegate, item);
}
} // namespace
Autocomplete::Autocomplete(QWidget *parent, not_null<Main::Session*> session)
: RpWidget(parent)
, _session(session) {
setupContent();
}
void Autocomplete::activate(not_null<Ui::InputField*> field) {
if (_session->settings().supportTemplatesAutocomplete()) {
_activate();
} else {
const auto &templates = _session->supportTemplates();
const auto max = templates.maxKeyLength();
auto cursor = field->textCursor();
const auto position = cursor.position();
const auto anchor = cursor.anchor();
const auto text = (position != anchor)
? field->getTextWithTagsPart(
std::min(position, anchor),
std::max(position, anchor))
: field->getTextWithTagsPart(
std::max(position - max, 0),
position);
const auto result = (position != anchor)
? templates.matchExact(text.text)
: templates.matchFromEnd(text.text);
if (result) {
const auto till = std::max(position, anchor);
const auto from = till - result->key.size();
cursor.setPosition(from);
cursor.setPosition(till, QTextCursor::KeepAnchor);
field->setTextCursor(cursor);
submitValue(result->question.value);
}
}
}
void Autocomplete::deactivate() {
_deactivate();
}
void Autocomplete::setBoundings(QRect rect) {
const auto maxHeight = int(4.5 * st::mentionHeight);
const auto height = std::min(rect.height(), maxHeight);
setGeometry(
rect.x(),
rect.y() + rect.height() - height,
rect.width(),
height);
}
rpl::producer<QString> Autocomplete::insertRequests() const {
return _insertRequests.events();
}
rpl::producer<Contact> Autocomplete::shareContactRequests() const {
return _shareContactRequests.events();
}
void Autocomplete::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Up) {
_moveSelection(-1);
} else if (e->key() == Qt::Key_Down) {
_moveSelection(1);
}
}
void Autocomplete::setupContent() {
const auto inputWrap = Ui::CreateChild<Ui::PaddingWrap<Ui::InputField>>(
this,
object_ptr<Ui::InputField>(
this,
st::defaultMultiSelectSearchField,
rpl::single(u"Search for templates"_q)), // #TODO hard_lang
st::autocompleteSearchPadding);
const auto input = inputWrap->entity();
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(this);
const auto inner = scroll->setOwnedWidget(object_ptr<Inner>(scroll));
const auto submit = [=] {
if (const auto question = inner->selected()) {
submitValue(question->value);
}
};
const auto refresh = [=] {
inner->showRows(
_session->supportTemplates().query(input->getLastText()));
scroll->scrollToY(0);
};
inner->activated() | rpl::on_next(submit, lifetime());
input->focusedChanges(
) | rpl::filter(!rpl::mappers::_1) | rpl::on_next([=] {
base::call_delayed(10, this, [=] {
if (!input->hasFocus()) {
deactivate();
}
});
}, input->lifetime());
input->cancelled(
) | rpl::on_next([=] {
deactivate();
}, input->lifetime());
input->changes() | rpl::on_next(refresh, input->lifetime());
input->submits() | rpl::on_next(submit, input->lifetime());
input->customUpDown(true);
_activate = [=] {
input->setText(QString());
show();
input->setFocus();
};
_deactivate = [=] {
hide();
};
_moveSelection = [=](int delta) {
const auto range = inner->moveSelection(delta);
if (range.second > range.first) {
scroll->scrollToY(range.first, range.second);
}
};
paintRequest(
) | rpl::on_next([=](QRect clip) {
QPainter p(this);
p.fillRect(
clip.intersected(QRect(0, st::lineWidth, width(), height())),
st::mentionBg);
p.fillRect(
clip.intersected(QRect(0, 0, width(), st::lineWidth)),
st::shadowFg);
}, lifetime());
sizeValue(
) | rpl::on_next([=](QSize size) {
inputWrap->resizeToWidth(size.width());
inputWrap->moveToLeft(0, st::lineWidth, size.width());
scroll->setGeometry(
0,
inputWrap->height(),
size.width(),
size.height() - inputWrap->height() - st::lineWidth);
inner->resizeToWidth(size.width());
}, lifetime());
}
void Autocomplete::submitValue(const QString &value) {
const auto prefix = u"contact:"_q;
if (value.startsWith(prefix)) {
const auto line = value.indexOf('\n');
const auto text = (line > 0) ? value.mid(line + 1) : QString();
const auto contact = value.mid(
prefix.size(),
(line > 0) ? (line - prefix.size()) : -1);
const auto parts = contact.split(' ', Qt::SkipEmptyParts);
if (parts.size() > 1) {
const auto phone = parts[0];
const auto firstName = parts[1];
const auto lastName = (parts.size() > 2)
? QStringList(parts.mid(2)).join(' ')
: QString();
_shareContactRequests.fire(Contact{
text,
phone,
firstName,
lastName });
}
} else {
_insertRequests.fire_copy(value);
}
}
ConfirmContactBox::ConfirmContactBox(
QWidget*,
not_null<Window::SessionController*> controller,
not_null<History*> history,
const Contact &data,
Fn<void(Qt::KeyboardModifiers)> submit)
: SimpleElementDelegate(controller, [=] { update(); })
, _chatStyle(std::make_unique<Ui::ChatStyle>(
history->session().colorIndicesValue()))
, _comment(GenerateCommentItem(this, history, data))
, _contact(GenerateContactItem(this, history, data))
, _submit(submit) {
_chatStyle->apply(controller->defaultChatTheme().get());
}
void ConfirmContactBox::prepare() {
setTitle(rpl::single(u"Confirmation"_q)); // #TODO hard_lang
auto maxWidth = 0;
if (_comment) {
_comment->setAttachToNext(true, _contact.get());
_contact->setAttachToPrevious(true, _comment.get());
_comment->initDimensions();
accumulate_max(maxWidth, _comment->maxWidth());
}
_contact->initDimensions();
accumulate_max(maxWidth, _contact->maxWidth());
maxWidth += st::boxPadding.left() + st::boxPadding.right();
const auto width = std::clamp(maxWidth, st::boxWidth, st::boxWideWidth);
const auto available = width
- st::boxPadding.left()
- st::boxPadding.right();
auto height = 0;
if (_comment) {
height += _comment->resizeGetHeight(available);
}
height += _contact->resizeGetHeight(available);
setDimensions(width, height);
_contact->initDimensions();
_submit = [=, original = std::move(_submit)](Qt::KeyboardModifiers m) {
const auto weak = base::make_weak(this);
original(m);
if (weak) {
closeBox();
}
};
const auto button = addButton(tr::lng_send_button(), [] {});
button->clicks(
) | rpl::on_next([=](Qt::MouseButton which) {
_submit((which == Qt::RightButton)
? SkipSwitchModifiers()
: button->clickModifiers());
}, button->lifetime());
button->setAcceptBoth(true);
addButton(tr::lng_cancel(), [=] { closeBox(); });
}
void ConfirmContactBox::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
_submit(e->modifiers());
} else {
BoxContent::keyPressEvent(e);
}
}
void ConfirmContactBox::paintEvent(QPaintEvent *e) {
Painter p(this);
p.fillRect(e->rect(), st::boxBg);
const auto theme = controller()->defaultChatTheme().get();
auto context = theme->preparePaintContext(
_chatStyle.get(),
rect(),
rect(),
controller()->isGifPausedAtLeastFor(Window::GifPauseReason::Layer));
p.translate(st::boxPadding.left(), 0);
if (_comment) {
context.outbg = _comment->hasOutLayout();
_comment->draw(p, context);
p.translate(0, _comment->height());
}
context.outbg = _contact->hasOutLayout();
_contact->draw(p, context);
}
HistoryView::Context ConfirmContactBox::elementContext() {
return HistoryView::Context::ContactPreview;
}
} // namespace Support

View File

@@ -0,0 +1,94 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/layers/box_content.h"
#include "history/admin_log/history_admin_log_item.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
namespace Main {
class Session;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class ScrollArea;
class InputField;
class ChatStyle;
} // namespace Ui
namespace Support {
struct Contact {
QString comment;
QString phone;
QString firstName;
QString lastName;
};
class Autocomplete final : public Ui::RpWidget {
public:
Autocomplete(QWidget *parent, not_null<Main::Session*> session);
void activate(not_null<Ui::InputField*> field);
void deactivate();
void setBoundings(QRect rect);
rpl::producer<QString> insertRequests() const;
rpl::producer<Contact> shareContactRequests() const;
protected:
void keyPressEvent(QKeyEvent *e) override;
private:
void setupContent();
void submitValue(const QString &value);
not_null<Main::Session*> _session;
Fn<void()> _activate;
Fn<void()> _deactivate;
Fn<void(int delta)> _moveSelection;
rpl::event_stream<QString> _insertRequests;
rpl::event_stream<Contact> _shareContactRequests;
};
class ConfirmContactBox
: public Ui::BoxContent
, public HistoryView::SimpleElementDelegate {
public:
ConfirmContactBox(
QWidget*,
not_null<Window::SessionController*> controller,
not_null<History*> history,
const Contact &data,
Fn<void(Qt::KeyboardModifiers)> submit);
using Element = HistoryView::Element;
HistoryView::Context elementContext() override;
protected:
void prepare() override;
void paintEvent(QPaintEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
private:
std::unique_ptr<Ui::ChatStyle> _chatStyle;
AdminLog::OwnedItem _comment;
AdminLog::OwnedItem _contact;
Fn<void(Qt::KeyboardModifiers)> _submit;
};
} //namespace Support

View File

@@ -0,0 +1,39 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "support/support_common.h"
#include "core/shortcuts.h"
namespace Support {
bool HandleSwitch(Qt::KeyboardModifiers modifiers) {
return !(modifiers & Qt::ShiftModifier)
|| (!(modifiers & Qt::ControlModifier)
&& !(modifiers & Qt::MetaModifier));
}
Qt::KeyboardModifiers SkipSwitchModifiers() {
return Qt::ControlModifier | Qt::ShiftModifier;
}
std::optional<Shortcuts::Command> GetSwitchCommand(SwitchSettings value) {
switch (value) {
case SwitchSettings::Next:
return Shortcuts::Command::ChatNext;
case SwitchSettings::Previous:
return Shortcuts::Command::ChatPrevious;
}
return std::nullopt;
}
FnMut<bool()> GetSwitchMethod(SwitchSettings value) {
const auto command = GetSwitchCommand(value);
return command ? Shortcuts::RequestHandler(*command) : nullptr;
}
} // namespace Support

View File

@@ -0,0 +1,28 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Shortcuts {
enum class Command;
} // namespace Shortcuts
namespace Support {
enum class SwitchSettings {
None,
Next,
Previous,
};
[[nodiscard]] Qt::KeyboardModifiers SkipSwitchModifiers();
[[nodiscard]] bool HandleSwitch(Qt::KeyboardModifiers modifiers);
[[nodiscard]] std::optional<Shortcuts::Command> GetSwitchCommand(
SwitchSettings value);
[[nodiscard]] FnMut<bool()> GetSwitchMethod(SwitchSettings value);
} // namespace Support

View File

@@ -0,0 +1,749 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "support/support_helper.h"
#include "dialogs/dialogs_key.h"
#include "data/data_drafts.h"
#include "data/data_forum.h"
#include "data/data_forum_topic.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "api/api_text_entities.h"
#include "history/history.h"
#include "boxes/abstract_box.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/text/format_values.h"
#include "ui/text/text_entity.h"
#include "ui/text/text_options.h"
#include "ui/toast/toast.h"
#include "ui/widgets/fields/input_field.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "base/unixtime.h"
#include "lang/lang_keys.h"
#include "window/window_session_controller.h"
#include "storage/storage_account.h"
#include "storage/storage_media_prepare.h"
#include "storage/localimageloader.h"
#include "core/launcher.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonArray>
namespace Main {
class Session;
} // namespace Main
namespace Support {
namespace {
constexpr auto kOccupyFor = TimeId(60);
constexpr auto kReoccupyEach = 30 * crl::time(1000);
constexpr auto kMaxSupportInfoLength = MaxMessageSize * 4;
constexpr auto kTopicRootId = MsgId(0);
constexpr auto kMonoforumPeerId = PeerId(0);
class EditInfoBox : public Ui::BoxContent {
public:
EditInfoBox(
QWidget*,
not_null<Window::SessionController*> controller,
const TextWithTags &text,
Fn<void(TextWithTags, Fn<void(bool success)>)> submit);
protected:
void prepare() override;
void setInnerFocus() override;
private:
const not_null<Window::SessionController*> _controller;
object_ptr<Ui::InputField> _field = { nullptr };
Fn<void(TextWithTags, Fn<void(bool success)>)> _submit;
};
EditInfoBox::EditInfoBox(
QWidget*,
not_null<Window::SessionController*> controller,
const TextWithTags &text,
Fn<void(TextWithTags, Fn<void(bool success)>)> submit)
: _controller(controller)
, _field(
this,
st::supportInfoField,
Ui::InputField::Mode::MultiLine,
rpl::single(u"Support information"_q), // #TODO hard_lang
text)
, _submit(std::move(submit)) {
_field->setMaxLength(kMaxSupportInfoLength);
_field->setSubmitSettings(
Core::App().settings().sendSubmitWay());
_field->setInstantReplaces(Ui::InstantReplaces::Default());
_field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue());
_field->setMarkdownReplacesEnabled(true);
_field->setEditLinkCallback(
DefaultEditLinkCallback(controller->uiShow(), _field));
}
void EditInfoBox::prepare() {
setTitle(rpl::single(u"Edit support information"_q)); // #TODO hard_lang
const auto save = [=] {
const auto done = crl::guard(this, [=](bool success) {
if (success) {
closeBox();
} else {
_field->showError();
}
});
_submit(_field->getTextWithAppliedMarkdown(), done);
};
addButton(tr::lng_settings_save(), save);
addButton(tr::lng_cancel(), [=] { closeBox(); });
_field->submits() | rpl::on_next(save, _field->lifetime());
_field->cancelled(
) | rpl::on_next([=] {
closeBox();
}, _field->lifetime());
Ui::Emoji::SuggestionsController::Init(
getDelegate()->outerContainer(),
_field,
&_controller->session());
auto cursor = _field->textCursor();
cursor.movePosition(QTextCursor::End);
_field->setTextCursor(cursor);
widthValue(
) | rpl::on_next([=](int width) {
_field->resizeToWidth(
width - st::boxPadding.left() - st::boxPadding.right());
_field->moveToLeft(st::boxPadding.left(), st::boxPadding.bottom());
}, _field->lifetime());
_field->heightValue(
) | rpl::on_next([=](int height) {
setDimensions(
st::boxWideWidth,
st::boxPadding.bottom() + height + st::boxPadding.bottom());
}, _field->lifetime());
}
void EditInfoBox::setInnerFocus() {
_field->setFocusFast();
}
uint32 OccupationTag() {
return uint32(Core::Launcher::Instance().installationTag() & 0xFFFFFFFF);
}
QString NormalizeName(QString name) {
return name.replace(':', '_').replace(';', '_');
}
Data::Draft OccupiedDraft(const QString &normalizedName) {
const auto now = base::unixtime::now(), till = now + kOccupyFor;
return {
TextWithTags{ "t:"
+ QString::number(till)
+ ";u:"
+ QString::number(OccupationTag())
+ ";n:"
+ normalizedName },
FullReplyTo(),
SuggestOptions(),
MessageCursor(),
Data::WebPageDraft()
};
}
[[nodiscard]] bool TrackHistoryOccupation(History *history) {
if (!history) {
return false;
} else if (const auto user = history->peer->asUser()) {
return !user->isBot();
}
return false;
}
uint32 ParseOccupationTag(History *history) {
if (!TrackHistoryOccupation(history)) {
return 0;
}
const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId);
if (!draft) {
return 0;
}
const auto &text = draft->textWithTags.text;
const auto parts = QStringView(text).split(';');
auto valid = false;
auto result = uint32();
for (const auto &part : parts) {
if (part.startsWith(u"t:"_q)) {
if (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {
valid = true;
} else {
return 0;
}
} else if (part.startsWith(u"u:"_q)) {
result = base::StringViewMid(part, 2).toUInt();
}
}
return valid ? result : 0;
}
QString ParseOccupationName(History *history) {
if (!TrackHistoryOccupation(history)) {
return QString();
}
const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId);
if (!draft) {
return QString();
}
const auto &text = draft->textWithTags.text;
const auto parts = QStringView(text).split(';');
auto valid = false;
auto result = QString();
for (const auto &part : parts) {
if (part.startsWith(u"t:"_q)) {
if (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {
valid = true;
} else {
return 0;
}
} else if (part.startsWith(u"n:"_q)) {
result = base::StringViewMid(part, 2).toString();
}
}
return valid ? result : QString();
}
TimeId OccupiedBySomeoneTill(History *history) {
if (!TrackHistoryOccupation(history)) {
return 0;
}
const auto draft = history->cloudDraft(kTopicRootId, kMonoforumPeerId);
if (!draft) {
return 0;
}
const auto &text = draft->textWithTags.text;
const auto parts = QStringView(text).split(';');
auto valid = false;
auto result = TimeId();
for (const auto &part : parts) {
if (part.startsWith(u"t:"_q)) {
if (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {
result = base::StringViewMid(part, 2).toInt();
} else {
return 0;
}
} else if (part.startsWith(u"u:"_q)) {
if (base::StringViewMid(part, 2).toUInt() != OccupationTag()) {
valid = true;
} else {
return 0;
}
}
}
return valid ? result : 0;
}
QString FastButtonModeIdsPath(not_null<Main::Session*> session) {
const auto base = session->account().local().supportModePath();
QDir().mkpath(base);
return base + u"/fast_button_mode_ids.json"_q;
}
} // namespace
Helper::Helper(not_null<Main::Session*> session)
: _session(session)
, _api(&_session->mtp())
, _templates(_session)
, _reoccupyTimer([=] { reoccupy(); })
, _checkOccupiedTimer([=] { checkOccupiedChats(); }) {
_api.request(MTPhelp_GetSupportName(
)).done([=](const MTPhelp_SupportName &result) {
result.match([&](const MTPDhelp_supportName &data) {
setSupportName(qs(data.vname()));
});
}).fail([=] {
setSupportName(
u"[rand^"_q
+ QString::number(Core::Launcher::Instance().installationTag())
+ ']');
}).send();
}
std::unique_ptr<Helper> Helper::Create(not_null<Main::Session*> session) {
//return std::make_unique<Helper>(session); AssertIsDebug();
return ShouldUse(session) ? std::make_unique<Helper>(session) : nullptr;
}
void Helper::CheckIfLost(not_null<Window::SessionController*> controller) {
static auto Checked = false;
if (Checked) {
return;
}
Checked = true;
const auto session = &controller->session();
if (!ShouldUse(session) || session->supportMode()) {
return;
}
session->local().writeSelf();
controller->show(Ui::MakeConfirmBox({
.text = u"This account should have support mode, "
"but it seems it was lost. Restart?"_q,
.confirmed = [=] { Core::Restart(); },
.confirmText = u"Restart"_q,
.title = u"Support Mode Lost"_q,
}));
}
bool Helper::ShouldUse(not_null<Main::Session*> session) {
return session->user()->phone().startsWith(u"424"_q);
}
void Helper::registerWindow(not_null<Window::SessionController*> controller) {
controller->activeChatValue(
) | rpl::map([](Dialogs::Key key) {
const auto history = key.history();
return TrackHistoryOccupation(history) ? history : nullptr;
}) | rpl::distinct_until_changed(
) | rpl::on_next([=](History *history) {
updateOccupiedHistory(controller, history);
}, controller->lifetime());
}
void Helper::cloudDraftChanged(not_null<History*> history) {
chatOccupiedUpdated(history);
if (history != _occupiedHistory) {
return;
}
occupyIfNotYet();
}
void Helper::chatOccupiedUpdated(not_null<History*> history) {
if (const auto till = OccupiedBySomeoneTill(history)) {
_occupiedChats[history] = till + 2;
history->session().changes().historyUpdated(
history,
Data::HistoryUpdate::Flag::ChatOccupied);
checkOccupiedChats();
} else if (_occupiedChats.take(history)) {
history->session().changes().historyUpdated(
history,
Data::HistoryUpdate::Flag::ChatOccupied);
}
}
void Helper::checkOccupiedChats() {
const auto now = base::unixtime::now();
while (!_occupiedChats.empty()) {
const auto nearest = ranges::min_element(
_occupiedChats,
std::less<>(),
[](const auto &pair) { return pair.second; });
if (nearest->second <= now) {
const auto history = nearest->first;
_occupiedChats.erase(nearest);
history->session().changes().historyUpdated(
history,
Data::HistoryUpdate::Flag::ChatOccupied);
} else {
_checkOccupiedTimer.callOnce(
(nearest->second - now) * crl::time(1000));
return;
}
}
_checkOccupiedTimer.cancel();
}
void Helper::updateOccupiedHistory(
not_null<Window::SessionController*> controller,
History *history) {
if (isOccupiedByMe(_occupiedHistory)) {
_occupiedHistory->clearCloudDraft(kTopicRootId, kMonoforumPeerId);
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
}
_occupiedHistory = history;
occupyInDraft();
}
void Helper::setSupportName(const QString &name) {
_supportName = name;
_supportNameNormalized = NormalizeName(name);
occupyIfNotYet();
}
void Helper::occupyIfNotYet() {
if (!isOccupiedByMe(_occupiedHistory)) {
occupyInDraft();
}
}
void Helper::occupyInDraft() {
if (_occupiedHistory
&& !isOccupiedBySomeone(_occupiedHistory)
&& !_supportName.isEmpty()) {
const auto draft = OccupiedDraft(_supportNameNormalized);
_occupiedHistory->createCloudDraft(
kTopicRootId,
kMonoforumPeerId,
&draft);
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
_reoccupyTimer.callEach(kReoccupyEach);
}
}
void Helper::reoccupy() {
if (isOccupiedByMe(_occupiedHistory)) {
const auto draft = OccupiedDraft(_supportNameNormalized);
_occupiedHistory->createCloudDraft(
kTopicRootId,
kMonoforumPeerId,
&draft);
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
}
}
bool Helper::isOccupiedByMe(History *history) const {
if (const auto tag = ParseOccupationTag(history)) {
return (tag == OccupationTag());
}
return false;
}
bool Helper::isOccupiedBySomeone(History *history) const {
if (const auto tag = ParseOccupationTag(history)) {
return (tag != OccupationTag());
}
return false;
}
void Helper::refreshInfo(not_null<UserData*> user) {
_api.request(MTPhelp_GetUserInfo(
user->inputUser()
)).done([=](const MTPhelp_UserInfo &result) {
applyInfo(user, result);
if (const auto controller = _userInfoEditPending.take(user)) {
if (const auto strong = controller->get()) {
showEditInfoBox(strong, user);
}
}
}).send();
}
void Helper::applyInfo(
not_null<UserData*> user,
const MTPhelp_UserInfo &result) {
const auto notify = [&] {
user->session().changes().peerUpdated(
user,
Data::PeerUpdate::Flag::SupportInfo);
};
const auto remove = [&] {
if (_userInformation.take(user)) {
notify();
}
};
result.match([&](const MTPDhelp_userInfo &data) {
auto info = UserInfo();
info.author = qs(data.vauthor());
info.date = data.vdate().v;
info.text = TextWithEntities{
qs(data.vmessage()),
Api::EntitiesFromMTP(&user->session(), data.ventities().v) };
if (info.text.empty()) {
remove();
} else if (_userInformation[user] != info) {
_userInformation[user] = info;
notify();
}
}, [&](const MTPDhelp_userInfoEmpty &) {
remove();
});
}
rpl::producer<UserInfo> Helper::infoValue(not_null<UserData*> user) const {
return user->session().changes().peerFlagsValue(
user,
Data::PeerUpdate::Flag::SupportInfo
) | rpl::map([=] {
return infoCurrent(user);
});
}
rpl::producer<QString> Helper::infoLabelValue(
not_null<UserData*> user) const {
return infoValue(
user
) | rpl::map([](const Support::UserInfo &info) {
const auto time = Ui::FormatDateTime(
base::unixtime::parse(info.date));
return info.author + ", " + time;
});
}
rpl::producer<TextWithEntities> Helper::infoTextValue(
not_null<UserData*> user) const {
return infoValue(
user
) | rpl::map([](const Support::UserInfo &info) {
return info.text;
});
}
UserInfo Helper::infoCurrent(not_null<UserData*> user) const {
const auto i = _userInformation.find(user);
return (i != end(_userInformation)) ? i->second : UserInfo();
}
void Helper::editInfo(
not_null<Window::SessionController*> controller,
not_null<UserData*> user) {
if (!_userInfoEditPending.contains(user)) {
_userInfoEditPending.emplace(user, controller.get());
refreshInfo(user);
}
}
void Helper::showEditInfoBox(
not_null<Window::SessionController*> controller,
not_null<UserData*> user) {
const auto info = infoCurrent(user);
const auto editData = TextWithTags{
info.text.text,
TextUtilities::ConvertEntitiesToTextTags(info.text.entities)
};
const auto save = [=](TextWithTags result, Fn<void(bool)> done) {
saveInfo(user, TextWithEntities{
result.text,
TextUtilities::ConvertTextTagsToEntities(result.tags)
}, done);
};
controller->show(Box<EditInfoBox>(controller, editData, save));
}
void Helper::saveInfo(
not_null<UserData*> user,
TextWithEntities text,
Fn<void(bool success)> done) {
const auto i = _userInfoSaving.find(user);
if (i != end(_userInfoSaving)) {
if (i->second.data == text) {
return;
} else {
i->second.data = text;
_api.request(base::take(i->second.requestId)).cancel();
}
} else {
_userInfoSaving.emplace(user, SavingInfo{ text });
}
TextUtilities::PrepareForSending(
text,
Ui::ItemTextDefaultOptions().flags);
TextUtilities::Trim(text);
const auto entities = Api::EntitiesToMTP(
&user->session(),
text.entities,
Api::ConvertOption::SkipLocal);
_userInfoSaving[user].requestId = _api.request(MTPhelp_EditUserInfo(
user->inputUser(),
MTP_string(text.text),
entities
)).done([=](const MTPhelp_UserInfo &result) {
applyInfo(user, result);
done(true);
}).fail([=] {
done(false);
}).send();
}
Templates &Helper::templates() {
return _templates;
}
FastButtonsBots::FastButtonsBots(not_null<Main::Session*> session)
: _session(session) {
}
bool FastButtonsBots::enabled(not_null<PeerData*> peer) const {
if (!_read) {
const_cast<FastButtonsBots*>(this)->read();
}
return _bots.contains(peer->id);
}
rpl::producer<bool> FastButtonsBots::enabledValue(
not_null<PeerData*> peer) const {
return rpl::single(
enabled(peer)
) | rpl::then(_changes.events(
) | rpl::filter([=](PeerId id) {
return (peer->id == id);
}) | rpl::map([=] {
return enabled(peer);
}));
}
void FastButtonsBots::setEnabled(not_null<PeerData*> peer, bool value) {
if (value == enabled(peer)) {
return;
} else if (value) {
_bots.emplace(peer->id);
} else {
_bots.remove(peer->id);
}
if (_bots.empty()) {
QFile(FastButtonModeIdsPath(_session)).remove();
} else {
write();
}
_changes.fire_copy(peer->id);
if (const auto history = peer->owner().history(peer)) {
if (const auto item = history->lastMessage()) {
history->owner().requestItemRepaint(item);
}
}
}
void FastButtonsBots::write() {
auto array = QJsonArray();
for (const auto &id : _bots) {
array.append(QString::number(id.value));
}
auto object = QJsonObject();
object[u"ids"_q] = array;
auto f = QFile(FastButtonModeIdsPath(_session));
if (f.open(QIODevice::WriteOnly)) {
f.write(QJsonDocument(object).toJson(QJsonDocument::Indented));
}
}
void FastButtonsBots::read() {
_read = true;
auto f = QFile(FastButtonModeIdsPath(_session));
if (!f.open(QIODevice::ReadOnly)) {
return;
}
const auto data = f.readAll();
const auto json = QJsonDocument::fromJson(data);
if (!json.isObject()) {
return;
}
const auto object = json.object();
const auto array = object.value(u"ids"_q).toArray();
for (const auto &value : array) {
const auto bareId = value.toString().toULongLong();
_bots.emplace(PeerId(bareId));
}
}
QString ChatOccupiedString(not_null<History*> history) {
const auto hand = QString::fromUtf8("\xe2\x9c\x8b\xef\xb8\x8f");
const auto name = ParseOccupationName(history);
return (name.isEmpty() || name.startsWith(u"[rand^"_q))
? hand + " chat taken"
: hand + ' ' + name + " is here";
}
QString InterpretSendPath(
not_null<Window::SessionController*> window,
const QString &path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
return "App Error: Could not open interpret file: " + path;
}
const auto content = QString::fromUtf8(f.readAll());
f.close();
const auto lines = content.split('\n');
auto toId = PeerId(0);
auto topicRootId = MsgId(0);
auto filePath = QString();
auto caption = QString();
for (const auto &line : lines) {
if (line.startsWith(u"from: "_q)) {
if (window->session().userId().bare
!= base::StringViewMid(
line,
u"from: "_q.size()).toULongLong()) {
return "App Error: Wrong current user.";
}
} else if (line.startsWith(u"channel: "_q)) {
const auto channelId = base::StringViewMid(
line,
u"channel: "_q.size()).toULongLong();
toId = peerFromChannel(channelId);
} else if (line.startsWith(u"topic: "_q)) {
const auto topicId = base::StringViewMid(
line,
u"topic: "_q.size()).toULongLong();
topicRootId = MsgId(topicId);
} else if (line.startsWith(u"file: "_q)) {
const auto path = line.mid(u"file: "_q.size());
if (!QFile(path).exists()) {
return "App Error: Could not find file with path: " + path;
}
filePath = path;
} else if (line.startsWith(u"caption: "_q)) {
caption = line.mid(u"caption: "_q.size());
} else if (!caption.isEmpty()) {
caption += '\n' + line;
} else {
return "App Error: Invalid command: " + line;
}
}
const auto history = window->session().data().historyLoaded(toId);
const auto sendTo = [=](not_null<Data::Thread*> thread) {
window->showThread(thread);
const auto premium = thread->session().user()->isPremium();
thread->session().api().sendFiles(
Storage::PrepareMediaList(
QStringList(filePath),
st::sendMediaPreviewSize,
premium),
SendMediaType::File,
{ caption },
nullptr,
Api::SendAction(thread));
};
if (!history) {
return "App Error: Could not find channel with id: "
+ QString::number(peerToChannel(toId).bare);
} else if (const auto forum = history->asForum()) {
forum->requestTopic(topicRootId, [=] {
if (const auto forum = history->asForum()) {
if (const auto topic = forum->topicFor(topicRootId)) {
sendTo(topic);
}
}
});
} else if (!topicRootId) {
sendTo(history);
}
return QString();
}
} // namespace Support

View File

@@ -0,0 +1,146 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "support/support_templates.h"
#include "mtproto/sender.h"
class History;
namespace Main {
class Session;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Support {
struct UserInfo {
QString author;
TimeId date = 0;
TextWithEntities text;
};
inline bool operator==(const UserInfo &a, const UserInfo &b) {
return (a.author == b.author)
&& (a.date == b.date)
&& (a.text == b.text);
}
inline bool operator!=(const UserInfo &a, const UserInfo &b) {
return !(a == b);
}
class Helper final {
public:
explicit Helper(not_null<Main::Session*> session);
static std::unique_ptr<Helper> Create(not_null<Main::Session*> session);
static void CheckIfLost(not_null<Window::SessionController*> controller);
void registerWindow(not_null<Window::SessionController*> controller);
void cloudDraftChanged(not_null<History*> history);
void chatOccupiedUpdated(not_null<History*> history);
[[nodiscard]] bool isOccupiedByMe(History *history) const;
[[nodiscard]] bool isOccupiedBySomeone(History *history) const;
void refreshInfo(not_null<UserData*> user);
[[nodiscard]] rpl::producer<UserInfo> infoValue(
not_null<UserData*> user) const;
[[nodiscard]] rpl::producer<QString> infoLabelValue(
not_null<UserData*> user) const;
[[nodiscard]] rpl::producer<TextWithEntities> infoTextValue(
not_null<UserData*> user) const;
[[nodiscard]] UserInfo infoCurrent(not_null<UserData*> user) const;
void editInfo(
not_null<Window::SessionController*> controller,
not_null<UserData*> user);
Templates &templates();
private:
struct SavingInfo {
TextWithEntities data;
mtpRequestId requestId = 0;
};
void checkOccupiedChats();
void updateOccupiedHistory(
not_null<Window::SessionController*> controller,
History *history);
void setSupportName(const QString &name);
void occupyIfNotYet();
void occupyInDraft();
void reoccupy();
void applyInfo(
not_null<UserData*> user,
const MTPhelp_UserInfo &result);
void showEditInfoBox(
not_null<Window::SessionController*> controller,
not_null<UserData*> user);
void saveInfo(
not_null<UserData*> user,
TextWithEntities text,
Fn<void(bool success)> done);
static bool ShouldUse(not_null<Main::Session*> session);
const not_null<Main::Session*> _session;
MTP::Sender _api;
Templates _templates;
QString _supportName;
QString _supportNameNormalized;
History *_occupiedHistory = nullptr;
base::Timer _reoccupyTimer;
base::Timer _checkOccupiedTimer;
base::flat_map<not_null<History*>, TimeId> _occupiedChats;
base::flat_map<not_null<UserData*>, UserInfo> _userInformation;
base::flat_map<
not_null<UserData*>,
base::weak_ptr<Window::SessionController>> _userInfoEditPending;
base::flat_map<not_null<UserData*>, SavingInfo> _userInfoSaving;
rpl::lifetime _lifetime;
};
class FastButtonsBots final {
public:
explicit FastButtonsBots(not_null<Main::Session*> session);
[[nodiscard]] bool enabled(not_null<PeerData*> peer) const;
[[nodiscard]] rpl::producer<bool> enabledValue(
not_null<PeerData*> peer) const;
void setEnabled(not_null<PeerData*> peer, bool value);
private:
void write();
void read();
const not_null<Main::Session*> _session;
base::flat_set<PeerId> _bots;
rpl::event_stream<PeerId> _changes;
bool _read = false;
};
QString ChatOccupiedString(not_null<History*> history);
QString InterpretSendPath(
not_null<Window::SessionController*> window,
const QString &path);
} // namespace Support

View File

@@ -0,0 +1,77 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "support/support_preload.h"
#include "history/history.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_histories.h"
#include "main/main_session.h"
#include "apiwrap.h"
namespace Support {
namespace {
constexpr auto kPreloadMessagesCount = 50;
} // namespace
int SendPreloadRequest(not_null<History*> history, Fn<void()> retry) {
auto offsetId = MsgId();
auto offset = 0;
auto loadCount = kPreloadMessagesCount;
if (const auto around = history->loadAroundId()) {
history->getReadyFor(ShowAtUnreadMsgId);
offset = -loadCount / 2;
offsetId = around;
}
const auto offsetDate = 0;
const auto maxId = 0;
const auto minId = 0;
const auto historyHash = uint64(0);
const auto type = Data::Histories::RequestType::History;
auto &histories = history->owner().histories();
return histories.sendRequest(history, type, [=](Fn<void()> finish) {
return history->session().api().request(MTPmessages_GetHistory(
history->peer->input(),
MTP_int(offsetId),
MTP_int(offsetDate),
MTP_int(offset),
MTP_int(loadCount),
MTP_int(maxId),
MTP_int(minId),
MTP_long(historyHash)
)).done([=](const MTPmessages_Messages &result) {
if (const auto around = history->loadAroundId()) {
if (around != offsetId) {
retry();
return;
}
history->clear(History::ClearType::Unload);
history->getReadyFor(ShowAtUnreadMsgId);
} else if (offsetId) {
retry();
return;
} else {
history->clear(History::ClearType::Unload);
history->getReadyFor(ShowAtTheEndMsgId);
}
result.match([](const MTPDmessages_messagesNotModified&) {
}, [&](const auto &data) {
history->owner().processUsers(data.vusers());
history->owner().processChats(data.vchats());
history->addOlderSlice(data.vmessages().v);
});
finish();
}).fail([=](const MTP::Error &error) {
finish();
}).send();
});
}
} // namespace Support

View File

@@ -0,0 +1,19 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class History;
namespace Support {
// Returns histories().request, not api().request.
[[nodiscard]] int SendPreloadRequest(
not_null<History*> history,
Fn<void()> retry);
} // namespace Support

View File

@@ -0,0 +1,739 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "support/support_templates.h"
#include "ui/toast/toast.h"
#include "data/data_session.h"
#include "core/shortcuts.h"
#include "main/main_session.h"
#include <QtNetwork/QNetworkAccessManager>
namespace Support {
namespace details {
namespace {
constexpr auto kQueryLimit = 10;
constexpr auto kWeightStep = 1000;
struct Delta {
std::vector<const TemplatesQuestion*> added;
std::vector<const TemplatesQuestion*> changed;
std::vector<const TemplatesQuestion*> removed;
std::map<QString, QStringList> keys;
explicit operator bool() const {
return !added.empty() || !changed.empty() || !removed.empty();
}
};
bool IsTemplatesFile(const QString &file) {
return file.startsWith(u"tl_"_q, Qt::CaseInsensitive)
&& file.endsWith(u".txt"_q, Qt::CaseInsensitive);
}
QString NormalizeQuestion(const QString &question) {
auto result = QString();
result.reserve(question.size());
for (const auto &ch : question) {
if (ch.isLetterOrNumber()) {
result.append(ch.toLower());
}
}
return result;
}
QString NormalizeKey(const QString &query) {
return TextUtilities::RemoveAccents(query.trimmed().toLower());
}
struct FileResult {
TemplatesFile result;
QStringList errors;
};
enum class ReadState {
None,
Question,
Keys,
Value,
Url,
};
template <typename StateChange, typename LineCallback>
void ReadByLine(
const QByteArray &blob,
StateChange &&stateChange,
LineCallback &&lineCallback) {
using State = ReadState;
auto state = State::None;
auto hadKeys = false;
auto hadValue = false;
for (const auto &utf : blob.split('\n')) {
const auto line = QString::fromUtf8(utf).trimmed();
const auto match = QRegularExpression(
u"^\\{([A-Z_]+)\\}$"_q
).match(line);
if (match.hasMatch()) {
const auto token = match.captured(1);
if (state == State::Value) {
hadKeys = hadValue = false;
}
const auto newState = [&] {
if (token == u"VALUE"_q) {
return hadValue ? State::None : State::Value;
} else if (token == u"KEYS"_q) {
return hadKeys ? State::None : State::Keys;
} else if (token == u"QUESTION"_q) {
return State::Question;
} else if (token == u"URL"_q) {
return State::Url;
} else {
return State::None;
}
}();
stateChange(state, newState);
state = newState;
lineCallback(state, line, true);
} else {
if (!line.isEmpty()) {
if (state == State::Value) {
hadValue = true;
} else if (state == State::Keys) {
hadKeys = true;
}
}
lineCallback(state, line, false);
}
}
}
template <typename Callback>
QString ReadByLineGetUrl(const QByteArray &blob, Callback &&callback) {
using State = ReadState;
auto url = QString();
auto question = TemplatesQuestion();
const auto call = [&] {
while (question.value.endsWith('\n')) {
question.value.chop(1);
}
return callback(base::take(question));
};
ReadByLine(blob, [&](State was, State now) {
if (was == State::Value) {
call();
}
}, [&](State state, const QString &line, bool stateChangeLine) {
if (stateChangeLine) {
return;
}
switch (state) {
case State::Keys:
if (!line.isEmpty()) {
question.originalKeys.push_back(line);
if (const auto norm = NormalizeKey(line); !norm.isEmpty()) {
question.normalizedKeys.push_back(norm);
}
}
break;
case State::Value:
if (!question.value.isEmpty()) {
question.value += '\n';
}
question.value += line;
break;
case State::Question:
if (question.question.isEmpty()) {
question.question = line;
}
break;
case State::Url:
if (url.isEmpty()) {
url = line;
}
break;
}
});
call();
return url;
}
FileResult ReadFromBlob(const QByteArray &blob) {
auto result = FileResult();
result.result.url = ReadByLineGetUrl(blob, [&](TemplatesQuestion &&q) {
const auto normalized = NormalizeQuestion(q.question);
if (!normalized.isEmpty()) {
result.result.questions.emplace(normalized, std::move(q));
}
});
return result;
}
FileResult ReadFile(const QString &path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
auto result = FileResult();
result.errors.push_back(
u"Couldn't open '%1' for reading!"_q.arg(path));
return result;
}
const auto blob = f.readAll();
f.close();
return ReadFromBlob(blob);
}
void WriteWithOwnUrlAndKeys(
QIODevice &device,
const QByteArray &blob,
const QString &url,
const Delta &delta) {
device.write("{URL}\n");
device.write(url.toUtf8());
device.write("\n\n");
using State = ReadState;
auto question = QString();
auto normalized = QString();
auto ownKeysWritten = false;
ReadByLine(blob, [&](State was, State now) {
if (was == State::Value) {
question = normalized = QString();
}
}, [&](State state, const QString &line, bool stateChangeLine) {
const auto writeLine = [&] {
device.write(line.toUtf8());
device.write("\n", 1);
};
switch (state) {
case State::Keys:
if (stateChangeLine) {
writeLine();
ownKeysWritten = [&] {
if (normalized.isEmpty()) {
return false;
}
const auto i = delta.keys.find(normalized);
if (i == end(delta.keys)) {
return false;
}
device.write(i->second.join('\n').toUtf8());
device.write("\n", 1);
return true;
}();
} else if (!ownKeysWritten) {
writeLine();
}
break;
case State::Value:
writeLine();
break;
case State::Question:
writeLine();
if (!stateChangeLine && question.isEmpty()) {
question = line;
normalized = NormalizeQuestion(line);
}
break;
case State::Url:
break;
}
});
}
struct FilesResult {
TemplatesData result;
TemplatesIndex index;
QStringList errors;
};
FilesResult ReadFiles(const QString &folder) {
auto result = FilesResult();
const auto files = QDir(folder).entryList(QDir::Files);
for (const auto &path : files) {
if (!IsTemplatesFile(path)) {
continue;
}
auto file = ReadFile(folder + '/' + path);
if (!file.result.url.isEmpty() || !file.result.questions.empty()) {
result.result.files[path] = std::move(file.result);
}
result.errors.append(std::move(file.errors));
}
return result;
}
TemplatesIndex ComputeIndex(const TemplatesData &data) {
using Id = TemplatesIndex::Id;
using Term = TemplatesIndex::Term;
auto uniqueFirst = std::map<QChar, base::flat_set<Id>>();
auto uniqueFull = std::map<Id, base::flat_set<Term>>();
const auto pushString = [&](
const Id &id,
const QString &string,
int weight) {
const auto list = TextUtilities::PrepareSearchWords(string);
for (const auto &word : list) {
uniqueFirst[word[0]].emplace(id);
uniqueFull[id].emplace(std::make_pair(word, weight));
}
};
for (const auto &[path, file] : data.files) {
for (const auto &[normalized, question] : file.questions) {
const auto id = std::make_pair(path, normalized);
for (const auto &key : question.normalizedKeys) {
pushString(id, key, kWeightStep * kWeightStep);
}
pushString(id, question.question, kWeightStep);
pushString(id, question.value, 1);
}
}
auto result = TemplatesIndex();
for (const auto &[ch, unique] : uniqueFirst) {
result.first.emplace(ch, unique | ranges::to_vector);
}
for (const auto &[id, unique] : uniqueFull) {
result.full.emplace(id, unique | ranges::to_vector);
}
return result;
}
void ReplaceFileIndex(
TemplatesIndex &result,
TemplatesIndex &&source,
const QString &path) {
for (auto i = begin(result.full); i != end(result.full);) {
if (i->first.first == path) {
i = result.full.erase(i);
} else {
++i;
}
}
for (auto &[id, list] : source.full) {
result.full.emplace(id, std::move(list));
}
using Id = TemplatesIndex::Id;
for (auto &[ch, list] : result.first) {
auto i = ranges::lower_bound(
list,
std::make_pair(path, QString()));
auto j = std::find_if(i, end(list), [&](const Id &id) {
return id.first != path;
});
list.erase(i, j);
}
for (auto &[ch, list] : source.first) {
auto &to = result.first[ch];
to.insert(
end(to),
std::make_move_iterator(begin(list)),
std::make_move_iterator(end(list)));
ranges::sort(to);
}
}
void MoveKeys(TemplatesFile &to, const TemplatesFile &from) {
const auto &existing = from.questions;
for (auto &[normalized, question] : to.questions) {
if (const auto i = existing.find(normalized); i != end(existing)) {
question.originalKeys = i->second.originalKeys;
question.normalizedKeys = i->second.normalizedKeys;
}
}
}
Delta ComputeDelta(const TemplatesFile &was, const TemplatesFile &now) {
auto result = Delta();
for (const auto &[normalized, question] : now.questions) {
const auto i = was.questions.find(normalized);
if (i == end(was.questions)) {
result.added.push_back(&question);
} else {
result.keys.emplace(normalized, i->second.originalKeys);
if (i->second.value != question.value) {
result.changed.push_back(&question);
}
}
}
for (const auto &[normalized, question] : was.questions) {
if (result.keys.find(normalized) == end(result.keys)) {
result.removed.push_back(&question);
}
}
return result;
}
QString FormatUpdateNotification(const QString &path, const Delta &delta) {
auto result = u"Template file '%1' updated!\n\n"_q.arg(path);
if (!delta.added.empty()) {
result += u"-------- Added --------\n\n"_q;
for (const auto question : delta.added) {
result += u"Q: %1\nK: %2\nA: %3\n\n"_q.arg(
question->question,
question->originalKeys.join(u", "_q),
question->value.trimmed());
}
}
if (!delta.changed.empty()) {
result += u"-------- Modified --------\n\n"_q;
for (const auto question : delta.changed) {
result += u"Q: %1\nA: %2\n\n"_q.arg(
question->question,
question->value.trimmed());
}
}
if (!delta.removed.empty()) {
result += u"-------- Removed --------\n\n"_q;
for (const auto question : delta.removed) {
result += u"Q: %1\n\n"_q.arg(question->question);
}
}
return result;
}
QString UpdateFile(
const QString &path,
const QByteArray &content,
const QString &url,
const Delta &delta) {
auto result = QString();
const auto full = cWorkingDir() + "TEMPLATES/" + path;
const auto old = full + u".old"_q;
QFile(old).remove();
if (QFile(full).copy(old)) {
result += u"(old file saved at '%1')"_q.arg(path + u".old"_q);
QFile f(full);
if (f.open(QIODevice::WriteOnly)) {
WriteWithOwnUrlAndKeys(f, content, url, delta);
} else {
result += u"\n\nError: could not open new file '%1'!"_q.arg(full);
}
} else {
result += u"Error: could not save old file '%1'!"_q.arg(old);
}
return result;
}
int CountMaxKeyLength(const TemplatesData &data) {
auto result = 0;
for (const auto &[path, file] : data.files) {
for (const auto &[normalized, question] : file.questions) {
for (const auto &key : question.normalizedKeys) {
accumulate_max(result, int(key.size()));
}
}
}
return result;
}
} // namespace
} // namespace details
using namespace details;
struct Templates::Updates {
QNetworkAccessManager manager;
std::map<QString, QNetworkReply*> requests;
};
Templates::Templates(not_null<Main::Session*> session) : _session(session) {
load();
Shortcuts::Requests(
) | rpl::on_next([=](not_null<Shortcuts::Request*> request) {
using Command = Shortcuts::Command;
request->check(
Command::SupportReloadTemplates
) && request->handle([=] {
reload();
return true;
});
}, _lifetime);
}
void Templates::reload() {
_reloadToastSubscription = errors(
) | rpl::on_next([=](QStringList errors) {
Ui::Toast::Show(errors.isEmpty()
? "Templates reloaded!"
: ("Errors:\n\n" + errors.join("\n\n")));
});
load();
}
void Templates::load() {
if (_reloadAfterRead) {
return;
} else if (_reading || _updates) {
_reloadAfterRead = true;
return;
}
crl::async([=, guard = _reading.make_guard()]() mutable {
auto result = ReadFiles(cWorkingDir() + "TEMPLATES");
result.index = ComputeIndex(result.result);
crl::on_main(std::move(guard), [
=,
result = std::move(result)
]() mutable {
setData(std::move(result.result));
_index = std::move(result.index);
_errors.fire(std::move(result.errors));
crl::on_main(this, [=] {
if (base::take(_reloadAfterRead)) {
reload();
} else {
update();
}
});
});
});
}
void Templates::setData(TemplatesData &&data) {
_data = std::move(data);
_maxKeyLength = CountMaxKeyLength(_data);
}
void Templates::ensureUpdatesCreated() {
if (_updates) {
return;
}
_updates = std::make_unique<Updates>();
QObject::connect(
&_updates->manager,
&QNetworkAccessManager::finished,
[=](QNetworkReply *reply) { updateRequestFinished(reply); });
}
void Templates::update() {
const auto sendRequest = [&](const QString &path, const QString &url) {
ensureUpdatesCreated();
if (_updates->requests.find(path) != end(_updates->requests)) {
return;
}
_updates->requests.emplace(
path,
_updates->manager.get(QNetworkRequest(url)));
};
for (const auto &[path, file] : _data.files) {
if (!file.url.isEmpty()) {
sendRequest(path, file.url);
}
}
}
void Templates::updateRequestFinished(QNetworkReply *reply) {
reply->deleteLater();
const auto path = [&] {
for (const auto &[file, sent] : _updates->requests) {
if (sent == reply) {
return file;
}
}
return QString();
}();
if (path.isEmpty()) {
return;
}
_updates->requests[path] = nullptr;
if (reply->error() != QNetworkReply::NoError) {
const auto message = (
u"Error: template update failed, url '%1', error %2, %3"_q
).arg(reply->url().toDisplayString()
).arg(reply->error()
).arg(reply->errorString());
_session->data().serviceNotification({ message });
return;
}
LOG(("Got template from url '%1'"
).arg(reply->url().toDisplayString()));
const auto content = reply->readAll();
crl::async([=, weak = base::make_weak(this)]{
auto result = ReadFromBlob(content);
auto one = TemplatesData();
one.files.emplace(path, std::move(result.result));
auto index = ComputeIndex(one);
crl::on_main(weak,[
=,
one = std::move(one),
errors = std::move(result.errors),
index = std::move(index)
]() mutable {
auto &existing = _data.files.at(path);
auto &parsed = one.files.at(path);
MoveKeys(parsed, existing);
ReplaceFileIndex(_index, ComputeIndex(one), path);
if (!errors.isEmpty()) {
_errors.fire(std::move(errors));
}
if (const auto delta = ComputeDelta(existing, parsed)) {
const auto text = FormatUpdateNotification(
path,
delta);
const auto copy = UpdateFile(
path,
content,
existing.url,
delta);
const auto full = text + copy;
_session->data().serviceNotification({ full });
}
_data.files.at(path) = std::move(one.files.at(path));
_updates->requests.erase(path);
checkUpdateFinished();
});
});
}
void Templates::checkUpdateFinished() {
if (!_updates || !_updates->requests.empty()) {
return;
}
_updates = nullptr;
if (base::take(_reloadAfterRead)) {
reload();
}
}
auto Templates::matchExact(QString query) const
-> std::optional<QuestionByKey> {
if (query.isEmpty() || query.size() > _maxKeyLength) {
return {};
}
query = NormalizeKey(query);
for (const auto &[path, file] : _data.files) {
for (const auto &[normalized, question] : file.questions) {
for (const auto &key : question.normalizedKeys) {
if (key == query) {
return QuestionByKey{ question, key };
}
}
}
}
return {};
}
auto Templates::matchFromEnd(QString query) const
-> std::optional<QuestionByKey> {
if (query.size() > _maxKeyLength) {
query = query.mid(query.size() - _maxKeyLength);
}
const auto size = query.size();
auto queries = std::vector<QString>();
queries.reserve(size);
for (auto i = 0; i != size; ++i) {
queries.push_back(NormalizeKey(query.mid(size - i - 1)));
}
auto result = std::optional<QuestionByKey>();
for (const auto &[path, file] : _data.files) {
for (const auto &[normalized, question] : file.questions) {
for (const auto &key : question.normalizedKeys) {
if (key.size() <= queries.size()
&& queries[key.size() - 1] == key
&& (!result || result->key.size() <= key.size())) {
result = QuestionByKey{ question, key };
}
}
}
}
return result;
}
Templates::~Templates() = default;
auto Templates::query(const QString &text) const -> std::vector<Question> {
const auto words = TextUtilities::PrepareSearchWords(text);
const auto questions = [&](const QString &word) {
const auto i = _index.first.find(word[0]);
return (i == end(_index.first)) ? 0 : i->second.size();
};
const auto best = ranges::min_element(words, std::less<>(), questions);
if (best == std::end(words)) {
return {};
}
const auto narrowed = _index.first.find((*best)[0]);
if (narrowed == end(_index.first)) {
return {};
}
using Id = TemplatesIndex::Id;
using Term = TemplatesIndex::Term;
const auto questionById = [&](const Id &id) {
return _data.files.at(id.first).questions.at(id.second);
};
const auto computeWeight = [&](const Id &id) {
auto result = 0;
const auto full = _index.full.find(id);
for (const auto &word : words) {
const auto from = ranges::lower_bound(
full->second,
word,
std::less<>(),
[](const Term &term) { return term.first; });
const auto till = std::find_if(
from,
end(full->second),
[&](const Term &term) {
return !term.first.startsWith(word);
});
const auto weight = std::max_element(
from,
till,
[](const Term &a, const Term &b) {
return a.second < b.second;
});
if (weight == till) {
return 0;
}
result += weight->second * (weight->first == word ? 2 : 1);
}
return result;
};
using Pair = std::pair<Id, int>;
const auto pairById = [&](const Id &id) {
return std::make_pair(id, computeWeight(id));
};
const auto sorter = [](const Pair &a, const Pair &b) {
// weight DESC filename DESC question ASC
if (a.second > b.second) {
return true;
} else if (a.second < b.second) {
return false;
} else if (a.first.first > b.first.first) {
return true;
} else if (a.first.first < b.first.first) {
return false;
} else {
return (a.first.second < b.first.second);
}
};
const auto good = narrowed->second | ranges::views::transform(
pairById
) | ranges::views::filter([](const Pair &pair) {
return pair.second > 0;
}) | ranges::to_vector | ranges::actions::stable_sort(sorter);
return good | ranges::views::transform([&](const Pair &pair) {
return questionById(pair.first);
}) | ranges::views::take(kQueryLimit) | ranges::to_vector;
}
} // namespace Support

View File

@@ -0,0 +1,99 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/binary_guard.h"
#include <QtNetwork/QNetworkReply>
namespace Main {
class Session;
} // namespace Main
namespace Support {
namespace details {
struct TemplatesQuestion {
QString question;
QStringList originalKeys;
QStringList normalizedKeys;
QString value;
};
struct TemplatesFile {
QString url;
std::map<QString, TemplatesQuestion> questions;
};
struct TemplatesData {
std::map<QString, TemplatesFile> files;
};
struct TemplatesIndex {
using Id = std::pair<QString, QString>; // filename, normalized question
using Term = std::pair<QString, int>; // search term, weight
std::map<QChar, std::vector<Id>> first;
std::map<Id, std::vector<Term>> full;
};
} // namespace details
class Templates : public base::has_weak_ptr {
public:
explicit Templates(not_null<Main::Session*> session);
void reload();
using Question = details::TemplatesQuestion;
std::vector<Question> query(const QString &text) const;
auto errors() const {
return _errors.events();
}
struct QuestionByKey {
Question question;
QString key;
};
std::optional<QuestionByKey> matchExact(QString text) const;
std::optional<QuestionByKey> matchFromEnd(QString text) const;
int maxKeyLength() const {
return _maxKeyLength;
}
~Templates();
private:
struct Updates;
void load();
void update();
void ensureUpdatesCreated();
void updateRequestFinished(QNetworkReply *reply);
void checkUpdateFinished();
void setData(details::TemplatesData &&data);
not_null<Main::Session*> _session;
details::TemplatesData _data;
details::TemplatesIndex _index;
rpl::event_stream<QStringList> _errors;
base::binary_guard _reading;
bool _reloadAfterRead = false;
rpl::lifetime _reloadToastSubscription;
int _maxKeyLength = 0;
std::unique_ptr<Updates> _updates;
rpl::lifetime _lifetime;
};
} // namespace Support