Allow instant template selection (support).

This commit is contained in:
John Preston 2018-10-11 14:17:51 +03:00
parent ec49ff31ef
commit e896971fa4
9 changed files with 198 additions and 62 deletions

View File

@ -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) {

View File

@ -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;

View File

@ -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);
}

View File

@ -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)

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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;
};