mirror of
https://github.com/vale981/tdesktop
synced 2025-03-05 09:41:41 -05:00
Allow instant template selection (support).
This commit is contained in:
parent
ec49ff31ef
commit
e896971fa4
9 changed files with 198 additions and 62 deletions
|
@ -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<qint32>(_variables.sendSubmitWay);
|
||||
qint32 supportSwitch = static_cast<qint32>(_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) {
|
||||
|
|
|
@ -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<bool> _thirdSectionInfoEnabledValue;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -946,6 +946,23 @@ void SetupSupport(not_null<Ui::VerticalLayout*> container) {
|
|||
});
|
||||
|
||||
AddSkip(inner, st::settingsCheckboxesSkip);
|
||||
|
||||
base::ObservableViewer(
|
||||
inner->add(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
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<UserData*> self)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ private:
|
|||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
bool _selectByKeys = false;
|
||||
rpl::event_stream<Qt::KeyboardModifiers> _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<AuthSession*> session)
|
|||
setupContent();
|
||||
}
|
||||
|
||||
void Autocomplete::activate() {
|
||||
_activate();
|
||||
void Autocomplete::activate(not_null<Ui::InputField*> 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<Inner>(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);
|
||||
|
|
|
@ -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<AuthSession*> session);
|
||||
|
||||
void activate();
|
||||
void activate(not_null<Ui::InputField*> 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<AuthSession*> _session;
|
||||
Fn<void()> _activate;
|
||||
|
|
|
@ -60,9 +60,9 @@ enum class ReadState {
|
|||
|
||||
template <typename StateChange, typename LineCallback>
|
||||
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<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 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<QString, QNetworkReply*> 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<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.keys) {
|
||||
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.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<Question> {
|
||||
|
@ -584,8 +655,8 @@ auto Templates::query(const QString &text) const -> std::vector<Question> {
|
|||
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<Question> {
|
|||
);
|
||||
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
|
||||
|
|
|
@ -52,6 +52,16 @@ public:
|
|||
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:
|
||||
|
@ -61,6 +71,7 @@ private:
|
|||
void ensureUpdatesCreated();
|
||||
void updateRequestFinished(QNetworkReply *reply);
|
||||
void checkUpdateFinished();
|
||||
void setData(details::TemplatesData &&data);
|
||||
|
||||
not_null<AuthSession*> _session;
|
||||
|
||||
|
@ -70,6 +81,8 @@ private:
|
|||
base::binary_guard _reading;
|
||||
bool _reloadAfterRead = false;
|
||||
|
||||
int _maxKeyLength = 0;
|
||||
|
||||
std::unique_ptr<Updates> _updates;
|
||||
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue