diff --git a/Telegram/SourceFiles/auth_session.cpp b/Telegram/SourceFiles/auth_session.cpp index 5271e3e8d..1a18fb5f6 100644 --- a/Telegram/SourceFiles/auth_session.cpp +++ b/Telegram/SourceFiles/auth_session.cpp @@ -40,7 +40,7 @@ AuthSessionSettings::Variables::Variables() , floatPlayerColumn(Window::Column::Second) , floatPlayerCorner(RectPart::TopRight) , sendSubmitWay(Ui::InputSubmitSettings::Enter) -, supportSwitch(Support::SwitchSettings::None) { +, supportSwitch(Support::SwitchSettings::Next) { } QByteArray AuthSessionSettings::serialize() const { @@ -82,6 +82,7 @@ QByteArray AuthSessionSettings::serialize() const { stream << qint32(_variables.sendSubmitWay); stream << qint32(_variables.supportSwitch); stream << qint32(_variables.supportFixChatsOrder ? 1 : 0); + stream << qint32(_variables.supportTemplatesAutocomplete ? 1 : 0); } return result; } @@ -111,6 +112,7 @@ void AuthSessionSettings::constructFromSerialized(const QByteArray &serialized) qint32 sendSubmitWay = static_cast(_variables.sendSubmitWay); qint32 supportSwitch = static_cast(_variables.supportSwitch); qint32 supportFixChatsOrder = _variables.supportFixChatsOrder ? 1 : 0; + qint32 supportTemplatesAutocomplete = _variables.supportTemplatesAutocomplete ? 1 : 0; stream >> selectorTab; stream >> lastSeenWarningSeen; @@ -171,6 +173,9 @@ void AuthSessionSettings::constructFromSerialized(const QByteArray &serialized) stream >> supportSwitch; stream >> supportFixChatsOrder; } + if (!stream.atEnd()) { + stream >> supportTemplatesAutocomplete; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for AuthSessionSettings::constructFromSerialized()")); @@ -236,7 +241,8 @@ void AuthSessionSettings::constructFromSerialized(const QByteArray &serialized) case Support::SwitchSettings::Next: case Support::SwitchSettings::Previous: _variables.supportSwitch = uncheckedSupportSwitch; break; } - _variables.supportFixChatsOrder = (supportFixChatsOrder ? 1 : 0); + _variables.supportFixChatsOrder = (supportFixChatsOrder == 1); + _variables.supportTemplatesAutocomplete = (supportTemplatesAutocomplete == 1); } void AuthSessionSettings::setTabbedSelectorSectionEnabled(bool enabled) { diff --git a/Telegram/SourceFiles/auth_session.h b/Telegram/SourceFiles/auth_session.h index 3e66fc1f7..8775453bf 100644 --- a/Telegram/SourceFiles/auth_session.h +++ b/Telegram/SourceFiles/auth_session.h @@ -96,6 +96,12 @@ public: bool supportFixChatsOrder() const { return _variables.supportFixChatsOrder; } + void setSupportTemplatesAutocomplete(bool enabled) { + _variables.supportTemplatesAutocomplete = enabled; + } + bool supportTemplatesAutocomplete() const { + return _variables.supportTemplatesAutocomplete; + } ChatHelpers::SelectorTab selectorTab() const { return _variables.selectorTab; @@ -216,6 +222,7 @@ private: Support::SwitchSettings supportSwitch; bool supportFixChatsOrder = true; + bool supportTemplatesAutocomplete = true; }; rpl::event_stream _thirdSectionInfoEnabledValue; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 3ac3d64b3..41b0485c7 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -795,10 +795,7 @@ void HistoryWidget::supportShareContact(Support::Contact contact) { if (!_history) { return; } - const auto commented = !contact.comment.isEmpty(); - if (commented) { - supportInsertText(contact.comment); - } + supportInsertText(contact.comment); contact.comment = _field->getLastText(); const auto submit = [=](Qt::KeyboardModifiers modifiers) { @@ -5522,7 +5519,7 @@ void HistoryWidget::replyToNextMessage() { void HistoryWidget::onFieldTabbed() { if (_supportAutocomplete) { - _supportAutocomplete->activate(); + _supportAutocomplete->activate(_field.data()); } else if (!_fieldAutocomplete->isHidden()) { _fieldAutocomplete->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab); } diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index ea0aae3fd..bc1b7eb67 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -946,6 +946,23 @@ void SetupSupport(not_null container) { }); AddSkip(inner, st::settingsCheckboxesSkip); + + base::ObservableViewer( + inner->add( + object_ptr( + inner, + "Enable templates autocomplete", + Auth().settings().supportTemplatesAutocomplete(), + st::settingsCheckbox), + st::settingsSendTypePadding + )->checkedChanged + ) | rpl::start_with_next([=](bool checked) { + Auth().settings().setSupportTemplatesAutocomplete(checked); + Local::writeUserSettings(); + }, inner->lifetime()); + + AddSkip(inner, st::settingsCheckboxesSkip); + AddSkip(inner); } Chat::Chat(QWidget *parent, not_null self) diff --git a/Telegram/SourceFiles/storage/storage_media_prepare.cpp b/Telegram/SourceFiles/storage/storage_media_prepare.cpp index 4568c6edb..cd19cd393 100644 --- a/Telegram/SourceFiles/storage/storage_media_prepare.cpp +++ b/Telegram/SourceFiles/storage/storage_media_prepare.cpp @@ -84,6 +84,7 @@ bool PrepareAlbumMediaIsWaiting( std::min(previewWidth, convertScale(image->data.width())) * cIntRetinaFactor(), Qt::SmoothTransformation)); + Assert(!file.preview.isNull()); file.preview.setDevicePixelRatio(cRetinaFactor()); file.type = PreparedFile::AlbumType::Photo; } @@ -95,6 +96,7 @@ bool PrepareAlbumMediaIsWaiting( file.preview = std::move(blurred).scaledToWidth( previewWidth * cIntRetinaFactor(), Qt::SmoothTransformation); + Assert(!file.preview.isNull()); file.preview.setDevicePixelRatio(cRetinaFactor()); file.type = PreparedFile::AlbumType::Video; } diff --git a/Telegram/SourceFiles/support/support_autocomplete.cpp b/Telegram/SourceFiles/support/support_autocomplete.cpp index 7eacb65f8..6cfc6d4c2 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.cpp +++ b/Telegram/SourceFiles/support/support_autocomplete.cpp @@ -68,7 +68,7 @@ private: int _selected = -1; int _pressed = -1; bool _selectByKeys = false; - rpl::event_stream _activated; + rpl::event_stream<> _activated; }; @@ -255,7 +255,7 @@ void Inner::mousePressEvent(QMouseEvent *e) { void Inner::mouseReleaseEvent(QMouseEvent *e) { const auto pressed = base::take(_pressed); if (pressed == _selected && pressed >= 0) { - _activated.fire(e->modifiers()); + _activated.fire({}); } } @@ -327,8 +327,34 @@ Autocomplete::Autocomplete(QWidget *parent, not_null session) setupContent(); } -void Autocomplete::activate() { - _activate(); +void Autocomplete::activate(not_null field) { + if (Auth().settings().supportTemplatesAutocomplete()) { + _activate(); + } else { + const auto templates = Auth().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() { @@ -376,9 +402,9 @@ void Autocomplete::setupContent() { const auto inner = scroll->setOwnedWidget(object_ptr(scroll)); - const auto submit = [=](Qt::KeyboardModifiers modifiers) { + const auto submit = [=] { if (const auto question = inner->selected()) { - submitValue(question->value, modifiers); + submitValue(question->value); } }; @@ -440,9 +466,7 @@ void Autocomplete::setupContent() { }, lifetime()); } -void Autocomplete::submitValue( - const QString &value, - Qt::KeyboardModifiers modifiers) { +void Autocomplete::submitValue(const QString &value) { const auto prefix = qstr("contact:"); if (value.startsWith(prefix)) { const auto line = value.indexOf('\n'); @@ -462,8 +486,7 @@ void Autocomplete::submitValue( text, phone, firstName, - lastName, - HandleSwitch(modifiers) }); + lastName }); } } else { _insertRequests.fire_copy(value); diff --git a/Telegram/SourceFiles/support/support_autocomplete.h b/Telegram/SourceFiles/support/support_autocomplete.h index 9243c1c82..b7cd99d45 100644 --- a/Telegram/SourceFiles/support/support_autocomplete.h +++ b/Telegram/SourceFiles/support/support_autocomplete.h @@ -17,6 +17,7 @@ class AuthSession; namespace Ui { class ScrollArea; +class InputField; } // namespace Ui namespace Support { @@ -26,14 +27,13 @@ struct Contact { QString phone; QString firstName; QString lastName; - bool handleSwitch = false; }; class Autocomplete : public Ui::RpWidget { public: Autocomplete(QWidget *parent, not_null session); - void activate(); + void activate(not_null field); void deactivate(); void setBoundings(QRect rect); @@ -45,7 +45,7 @@ protected: private: void setupContent(); - void submitValue(const QString &value, Qt::KeyboardModifiers modifiers); + void submitValue(const QString &value); not_null _session; Fn _activate; diff --git a/Telegram/SourceFiles/support/support_templates.cpp b/Telegram/SourceFiles/support/support_templates.cpp index 46c1a093a..c2f408a12 100644 --- a/Telegram/SourceFiles/support/support_templates.cpp +++ b/Telegram/SourceFiles/support/support_templates.cpp @@ -60,9 +60,9 @@ enum class ReadState { template void ReadByLine( - const QByteArray &blob, - StateChange &&stateChange, - LineCallback &&lineCallback) { + const QByteArray &blob, + StateChange &&stateChange, + LineCallback &&lineCallback) { using State = ReadState; auto state = State::None; auto hadKeys = false; @@ -266,9 +266,9 @@ TemplatesIndex ComputeIndex(const TemplatesData &data) { auto uniqueFirst = std::map>(); auto uniqueFull = std::map>(); const auto pushString = [&]( - const Id &id, - const QString &string, - int weight) { + const Id &id, + const QString &string, + int weight) { const auto list = TextUtilities::PrepareSearchWords(string); for (const auto &word : list) { uniqueFirst[word[0]].emplace(id); @@ -390,10 +390,10 @@ QString FormatUpdateNotification(const QString &path, const Delta &delta) { } QString UpdateFile( - const QString &path, - const QByteArray &content, - const QString &url, - const Delta &delta) { + 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 + qstr(".old"); @@ -416,9 +416,27 @@ QString UpdateFile( 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.keys) { + accumulate_max(result, key.size()); + } + } + } + return result; +} + +QString NormalizeKey(const QString &query) { + return TextUtilities::RemoveAccents(query.trimmed().toLower()); +} + } // namespace } // namespace details +using namespace details; + struct Templates::Updates { QNetworkAccessManager manager; std::map requests; @@ -436,33 +454,38 @@ void Templates::reload() { return; } - auto [left, right] = base::make_binary_guard(); + 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); + auto result = ReadFiles(cWorkingDir() + "TEMPLATES"); + result.index = ComputeIndex(result.result); crl::on_main([ =, - result = std::move(result), - guard = std::move(guard) + 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(); + if (!guard.alive()) { + return; } + 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; @@ -520,12 +543,12 @@ void Templates::updateRequestFinished(QNetworkReply *reply) { LOG(("Got template from url '%1'" ).arg(reply->url().toDisplayString())); const auto content = reply->readAll(); - crl::async([=, weak = base::make_weak(this)] { - auto result = details::ReadFromBlob(content); - auto one = details::TemplatesData(); + crl::async([=, weak = base::make_weak(this)]{ + auto result = ReadFromBlob(content); + auto one = TemplatesData(); one.files.emplace(path, std::move(result.result)); - auto index = details::ComputeIndex(one); - crl::on_main(weak, [ + auto index = ComputeIndex(one); + crl::on_main(weak,[ =, one = std::move(one), errors = std::move(result.errors), @@ -533,16 +556,16 @@ void Templates::updateRequestFinished(QNetworkReply *reply) { ]() mutable { auto &existing = _data.files.at(path); auto &parsed = one.files.at(path); - details::MoveKeys(parsed, existing); - details::ReplaceFileIndex(_index, details::ComputeIndex(one), path); + MoveKeys(parsed, existing); + ReplaceFileIndex(_index, ComputeIndex(one), path); if (!errors.isEmpty()) { _errors.fire(std::move(errors)); } - if (const auto delta = details::ComputeDelta(existing, parsed)) { - const auto text = details::FormatUpdateNotification( + if (const auto delta = ComputeDelta(existing, parsed)) { + const auto text = FormatUpdateNotification( path, delta); - const auto copy = details::UpdateFile( + const auto copy = UpdateFile( path, content, existing.url, @@ -555,7 +578,7 @@ void Templates::updateRequestFinished(QNetworkReply *reply) { _updates->requests.erase(path); checkUpdateFinished(); }); - }); + }); } void Templates::checkUpdateFinished() { @@ -568,6 +591,54 @@ void Templates::checkUpdateFinished() { } } +auto Templates::matchExact(QString query) const +-> std::optional { + 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.keys) { + if (key == query) { + return QuestionByKey{ question, key }; + } + } + } + } + return {}; +} + +auto Templates::matchFromEnd(QString query) const +-> std::optional { + if (query.size() > _maxKeyLength) { + query = query.mid(query.size() - _maxKeyLength); + } + + const auto size = query.size(); + auto queries = std::vector(); + queries.reserve(size); + for (auto i = 0; i != size; ++i) { + queries.push_back(NormalizeKey(query.mid(size - i - 1))); + } + + auto result = std::optional(); + for (const auto &[path, file] : _data.files) { + for (const auto &[normalized, question] : file.questions) { + for (const auto &key : question.keys) { + 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 { @@ -584,8 +655,8 @@ auto Templates::query(const QString &text) const -> std::vector { if (narrowed == end(_index.first)) { return {}; } - using Id = details::TemplatesIndex::Id; - using Term = details::TemplatesIndex::Term; + using Id = TemplatesIndex::Id; + using Term = TemplatesIndex::Term; const auto questionById = [&](const Id &id) { return _data.files.at(id.first).questions.at(id.second); }; @@ -632,7 +703,7 @@ auto Templates::query(const QString &text) const -> std::vector { ); return good | ranges::view::transform([](const Pair &pair) { return pair.first; - }) | ranges::view::take(details::kQueryLimit) | ranges::to_vector; + }) | ranges::view::take(kQueryLimit) | ranges::to_vector; } } // namespace Support diff --git a/Telegram/SourceFiles/support/support_templates.h b/Telegram/SourceFiles/support/support_templates.h index 56292d910..cdf470c98 100644 --- a/Telegram/SourceFiles/support/support_templates.h +++ b/Telegram/SourceFiles/support/support_templates.h @@ -52,6 +52,16 @@ public: return _errors.events(); } + struct QuestionByKey { + Question question; + QString key; + }; + std::optional matchExact(QString text) const; + std::optional matchFromEnd(QString text) const; + int maxKeyLength() const { + return _maxKeyLength; + } + ~Templates(); private: @@ -61,6 +71,7 @@ private: void ensureUpdatesCreated(); void updateRequestFinished(QNetworkReply *reply); void checkUpdateFinished(); + void setData(details::TemplatesData &&data); not_null _session; @@ -70,6 +81,8 @@ private: base::binary_guard _reading; bool _reloadAfterRead = false; + int _maxKeyLength = 0; + std::unique_ptr _updates; };