From 2acfe890c8b1bd96eb6854d846e159cd29237226 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Fri, 28 Jul 2023 00:02:35 -0400 Subject: [PATCH] Allow editing messages via regex This reuses the Neochat code to edit your most recent message by regex, but if you reply to one of your messages, it will try to edit that message instead. --- src/UserSettingsPage.cpp | 33 ++++++++++++++++++++ src/UserSettingsPage.h | 6 ++++ src/timeline/InputBar.cpp | 64 +++++++++++++++++++++++++++++++++++---- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 7c30f8779..4098d80d0 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -72,6 +72,7 @@ UserSettings::load(std::optional profile) settings.value(QStringLiteral("user/timeline/enlarge_emoji_only_msg"), false).toBool(); markdown_ = settings.value(QStringLiteral("user/markdown_enabled"), true).toBool(); invertEnterKey_ = settings.value(QStringLiteral("user/invert_enter_key"), false).toBool(); + regexEditing_ = settings.value(QStringLiteral("user/regex_editing"), true).toBool(); bubbles_ = settings.value(QStringLiteral("user/bubbles_enabled"), false).toBool(); smallAvatars_ = settings.value(QStringLiteral("user/small_avatars_enabled"), false).toBool(); animateImagesOnHover_ = @@ -342,6 +343,17 @@ UserSettings::setInvertEnterKey(bool state) save(); } +void +UserSettings::setRegexEditing(bool state) +{ + if (state == regexEditing_) + return; + + regexEditing_ = state; + emit regexEditingChanged(state); + save(); +} + void UserSettings::setBubbles(bool state) { @@ -910,6 +922,7 @@ UserSettings::save() settings.setValue(QStringLiteral("scrollbars_in_roomlist"), scrollbarsInRoomlist_); settings.setValue(QStringLiteral("markdown_enabled"), markdown_); settings.setValue(QStringLiteral("invert_enter_key"), invertEnterKey_); + settings.setValue(QStringLiteral("regex_editing"), regexEditing_); settings.setValue(QStringLiteral("bubbles_enabled"), bubbles_); settings.setValue(QStringLiteral("small_avatars_enabled"), smallAvatars_); settings.setValue(QStringLiteral("animate_images_on_hover"), animateImagesOnHover_); @@ -1023,6 +1036,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return tr("Send messages as Markdown"); case InvertEnterKey: return tr("Use shift+enter to send and enter to start a new line"); + case RegexEditing: + return tr("Allow editing your last message by regex"); case Bubbles: return tr("Enable message bubbles"); case SmallAvatars: @@ -1173,6 +1188,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return i->markdown(); case InvertEnterKey: return i->invertEnterKey(); + case RegexEditing: + return i->regexEditing(); case Bubbles: return i->bubbles(); case SmallAvatars: @@ -1329,6 +1346,11 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return tr( "Invert the behavior of the enter key in the text input, making it send the message " "when shift+enter is pressed and starting a new line when enter is pressed."); + case RegexEditing: + return tr("If you send a message that is a valid replacement regex (e.g. s/foo/bar), " + "try to apply it to your last sent message as an edit instead of sending it " + "as a regular message. If the regex cannot be applied to your last message, " + "it will be sent as a normal message."); case Bubbles: return tr( "Messages get a bubble background. This also triggers some layout changes (WIP)."); @@ -1497,6 +1519,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const case ScrollbarsInRoomlist: case Markdown: case InvertEnterKey: + case RegexEditing: case Bubbles: case SmallAvatars: case AnimateImagesOnHover: @@ -1738,6 +1761,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int } else return false; } + case RegexEditing: { + if (value.userType() == QMetaType::Bool) { + i->setRegexEditing(value.toBool()); + return true; + } else + return false; + } case Bubbles: { if (value.userType() == QMetaType::Bool) { i->setBubbles(value.toBool()); @@ -2192,6 +2222,9 @@ UserSettingsModel::UserSettingsModel(QObject *p) connect(s.get(), &UserSettings::invertEnterKeyChanged, this, [this]() { emit dataChanged(index(InvertEnterKey), index(InvertEnterKey), {Value}); }); + connect(s.get(), &UserSettings::regexEditingChanged, this, [this]() { + emit dataChanged(index(RegexEditing), index(RegexEditing), {Value}); + }); connect(s.get(), &UserSettings::bubblesChanged, this, [this]() { emit dataChanged(index(Bubbles), index(Bubbles), {Value}); }); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 4e2691e50..900bfcddf 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -40,6 +40,7 @@ class UserSettings final : public QObject Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged) Q_PROPERTY( bool invertEnterKey READ invertEnterKey WRITE setInvertEnterKey NOTIFY invertEnterKeyChanged) + Q_PROPERTY(bool regexEditing READ regexEditing WRITE setRegexEditing NOTIFY regexEditingChanged) Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged) Q_PROPERTY(bool smallAvatars READ smallAvatars WRITE setSmallAvatars NOTIFY smallAvatarsChanged) Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover @@ -181,6 +182,7 @@ class UserSettings final : public QObject void setScrollbarsInRoomlist(bool state); void setMarkdown(bool state); void setInvertEnterKey(bool state); + void setRegexEditing(bool state); void setBubbles(bool state); void setSmallAvatars(bool state); void setAnimateImagesOnHover(bool state); @@ -253,6 +255,7 @@ class UserSettings final : public QObject int privacyScreenTimeout() const { return privacyScreenTimeout_; } bool markdown() const { return markdown_; } bool invertEnterKey() const { return invertEnterKey_; } + bool regexEditing() const { return regexEditing_; } bool bubbles() const { return bubbles_; } bool smallAvatars() const { return smallAvatars_; } bool animateImagesOnHover() const { return animateImagesOnHover_; } @@ -324,6 +327,7 @@ class UserSettings final : public QObject void startInTrayChanged(bool state); void markdownChanged(bool state); void invertEnterKeyChanged(bool state); + void regexEditingChanged(bool state); void bubblesChanged(bool state); void smallAvatarsChanged(bool state); void animateImagesOnHoverChanged(bool state); @@ -393,6 +397,7 @@ class UserSettings final : public QObject bool scrollbarsInRoomlist_; bool markdown_; bool invertEnterKey_; + bool regexEditing_; bool bubbles_; bool smallAvatars_; bool animateImagesOnHover_; @@ -501,6 +506,7 @@ class UserSettingsModel : public QAbstractListModel ReadReceipts, Markdown, InvertEnterKey, + RegexEditing, Bubbles, SmallAvatars, diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index a371e2b44..918a198ff 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -450,15 +450,67 @@ InputBar::generateRelations() const void InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbowify) { + auto trimmed = msg.trimmed(); mtx::events::msg::Text text = {}; - text.body = msg.trimmed().toStdString(); + text.body = trimmed.toStdString(); + + // we don't want to have unexpected behavior if somebody was genuinely trying to edit a message + // to be a regex + if (UserSettings::instance()->regexEditing() && room->edit().isEmpty()) [[unlikely]] { + // The bulk of this logic was shamelessly stolen from Neochat. Thanks to Carl Schwan for + // letting us have this :) + static const QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$"); + auto match = sed.match(trimmed); + + if (match.hasMatch()) { + const QString regex = match.captured(1); + const QString replacement = match.captured(2).toHtmlEscaped(); + const QString flags = match.captured(3); + + std::optional event; + if (!room->reply().isEmpty()) { + auto ev = room->eventById(room->reply()); + if (ev && room->data(*ev, TimelineModel::IsEditable).toBool()) + event = ev; + } else { + for (int i = 0; i < room->rowCount(); ++i) { + const auto idx = room->index(i); + if (room->data(idx, TimelineModel::IsEditable).toBool()) { + event = room->eventById(room->data(idx, TimelineModel::EventId).toString()); + break; + } + } + } + + if (event) { + auto other = room->data(*event, TimelineModel::FormattedBody).toString(); + const auto original{other}; + if (flags == "/g") + other.replace(regex, replacement); + else + other.replace(other.indexOf(regex), regex.size(), replacement); + + // don't bother sending a pointless edit + if (original != other) { + text.formatted_body = other.toStdString(); + text.format = "org.matrix.custom.html"; + text.relations.relations.push_back( + {.rel_type = mtx::common::RelationType::Replace, + .event_id = + room->data(*event, TimelineModel::EventId).toString().toStdString()}); + room->sendMessageEvent(text, mtx::events::EventType::RoomMessage); + return; + } + } + } + } if ((ChatPage::instance()->userSettings()->markdown() && useMarkdown == MarkdownOverride::NOT_SPECIFIED) || useMarkdown == MarkdownOverride::ON) { text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString(); // Remove markdown links by completer - text.body = replaceMatrixToMarkdownLink(msg.trimmed()).toStdString(); + text.body = replaceMatrixToMarkdownLink(trimmed).toStdString(); // Don't send formatted_body, when we don't need to // Specifically, if it includes no html tag and no newlines (which behave differently in @@ -473,7 +525,7 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow // disable all markdown extensions text.formatted_body = utils::markdownToHtml(msg, rainbowify, true).toStdString(); // keep everything as it was - text.body = msg.trimmed().toStdString(); + text.body = trimmed.toStdString(); // always send formatted text.format = "org.matrix.custom.html"; @@ -484,9 +536,9 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow auto related = room->relatedInfo(room->reply()); // Skip reply fallbacks to users who would cause a room ping with the fallback. - // This should be fine, since in some cases the reply fallback can be omitted now and the - // alternative is worse! On Element Android this applies to any substring, but that is their - // bug to fix. + // This should be fine, since in some cases the reply fallback can be omitted now and + // the alternative is worse! On Element Android this applies to any substring, but that + // is their bug to fix. if (!related.quoted_user.startsWith("@room:")) { QString body; bool firstLine = true;