mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-21 18:50:47 +03:00
Allow editing messages via sed expressions
This reuses the Neochat code to edit your most recent message by sed expressions, but if you reply to one of your messages, it will try to edit that message instead.
This commit is contained in:
parent
057e1f5c67
commit
2a67eb7486
3 changed files with 97 additions and 6 deletions
|
@ -72,6 +72,7 @@ UserSettings::load(std::optional<QString> profile)
|
||||||
settings.value(QStringLiteral("user/timeline/enlarge_emoji_only_msg"), false).toBool();
|
settings.value(QStringLiteral("user/timeline/enlarge_emoji_only_msg"), false).toBool();
|
||||||
markdown_ = settings.value(QStringLiteral("user/markdown_enabled"), true).toBool();
|
markdown_ = settings.value(QStringLiteral("user/markdown_enabled"), true).toBool();
|
||||||
invertEnterKey_ = settings.value(QStringLiteral("user/invert_enter_key"), false).toBool();
|
invertEnterKey_ = settings.value(QStringLiteral("user/invert_enter_key"), false).toBool();
|
||||||
|
sedEditing_ = settings.value(QStringLiteral("user/sed_editing"), false).toBool();
|
||||||
bubbles_ = settings.value(QStringLiteral("user/bubbles_enabled"), false).toBool();
|
bubbles_ = settings.value(QStringLiteral("user/bubbles_enabled"), false).toBool();
|
||||||
smallAvatars_ = settings.value(QStringLiteral("user/small_avatars_enabled"), false).toBool();
|
smallAvatars_ = settings.value(QStringLiteral("user/small_avatars_enabled"), false).toBool();
|
||||||
animateImagesOnHover_ =
|
animateImagesOnHover_ =
|
||||||
|
@ -342,6 +343,17 @@ UserSettings::setInvertEnterKey(bool state)
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserSettings::setSedEditing(bool state)
|
||||||
|
{
|
||||||
|
if (state == sedEditing_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
sedEditing_ = state;
|
||||||
|
emit sedEditingChanged(state);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
UserSettings::setBubbles(bool state)
|
UserSettings::setBubbles(bool state)
|
||||||
{
|
{
|
||||||
|
@ -910,6 +922,7 @@ UserSettings::save()
|
||||||
settings.setValue(QStringLiteral("scrollbars_in_roomlist"), scrollbarsInRoomlist_);
|
settings.setValue(QStringLiteral("scrollbars_in_roomlist"), scrollbarsInRoomlist_);
|
||||||
settings.setValue(QStringLiteral("markdown_enabled"), markdown_);
|
settings.setValue(QStringLiteral("markdown_enabled"), markdown_);
|
||||||
settings.setValue(QStringLiteral("invert_enter_key"), invertEnterKey_);
|
settings.setValue(QStringLiteral("invert_enter_key"), invertEnterKey_);
|
||||||
|
settings.setValue(QStringLiteral("sed_editing"), sedEditing_);
|
||||||
settings.setValue(QStringLiteral("bubbles_enabled"), bubbles_);
|
settings.setValue(QStringLiteral("bubbles_enabled"), bubbles_);
|
||||||
settings.setValue(QStringLiteral("small_avatars_enabled"), smallAvatars_);
|
settings.setValue(QStringLiteral("small_avatars_enabled"), smallAvatars_);
|
||||||
settings.setValue(QStringLiteral("animate_images_on_hover"), animateImagesOnHover_);
|
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");
|
return tr("Send messages as Markdown");
|
||||||
case InvertEnterKey:
|
case InvertEnterKey:
|
||||||
return tr("Use shift+enter to send and enter to start a new line");
|
return tr("Use shift+enter to send and enter to start a new line");
|
||||||
|
case SedEditing:
|
||||||
|
return tr("Allow editing your last message with sed expressions");
|
||||||
case Bubbles:
|
case Bubbles:
|
||||||
return tr("Enable message bubbles");
|
return tr("Enable message bubbles");
|
||||||
case SmallAvatars:
|
case SmallAvatars:
|
||||||
|
@ -1177,6 +1192,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
|
||||||
return i->markdown();
|
return i->markdown();
|
||||||
case InvertEnterKey:
|
case InvertEnterKey:
|
||||||
return i->invertEnterKey();
|
return i->invertEnterKey();
|
||||||
|
case SedEditing:
|
||||||
|
return i->sedEditing();
|
||||||
case Bubbles:
|
case Bubbles:
|
||||||
return i->bubbles();
|
return i->bubbles();
|
||||||
case SmallAvatars:
|
case SmallAvatars:
|
||||||
|
@ -1341,6 +1358,11 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
|
||||||
return tr(
|
return tr(
|
||||||
"Invert the behavior of the enter key in the text input, making it send the message "
|
"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.");
|
"when shift+enter is pressed and starting a new line when enter is pressed.");
|
||||||
|
case SedEditing:
|
||||||
|
return tr("If you send a message that is a valid sed expression (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 sed expression cannot be applied to your last "
|
||||||
|
"message, it will be sent as a normal message.");
|
||||||
case Bubbles:
|
case Bubbles:
|
||||||
return tr(
|
return tr(
|
||||||
"Messages get a bubble background. This also triggers some layout changes (WIP).");
|
"Messages get a bubble background. This also triggers some layout changes (WIP).");
|
||||||
|
@ -1512,6 +1534,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
|
||||||
case ScrollbarsInRoomlist:
|
case ScrollbarsInRoomlist:
|
||||||
case Markdown:
|
case Markdown:
|
||||||
case InvertEnterKey:
|
case InvertEnterKey:
|
||||||
|
case SedEditing:
|
||||||
case Bubbles:
|
case Bubbles:
|
||||||
case SmallAvatars:
|
case SmallAvatars:
|
||||||
case AnimateImagesOnHover:
|
case AnimateImagesOnHover:
|
||||||
|
@ -1762,6 +1785,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
|
||||||
} else
|
} else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
case SedEditing: {
|
||||||
|
if (value.userType() == QMetaType::Bool) {
|
||||||
|
i->setSedEditing(value.toBool());
|
||||||
|
return true;
|
||||||
|
} else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
case Bubbles: {
|
case Bubbles: {
|
||||||
if (value.userType() == QMetaType::Bool) {
|
if (value.userType() == QMetaType::Bool) {
|
||||||
i->setBubbles(value.toBool());
|
i->setBubbles(value.toBool());
|
||||||
|
@ -2221,6 +2251,9 @@ UserSettingsModel::UserSettingsModel(QObject *p)
|
||||||
connect(s.get(), &UserSettings::invertEnterKeyChanged, this, [this]() {
|
connect(s.get(), &UserSettings::invertEnterKeyChanged, this, [this]() {
|
||||||
emit dataChanged(index(InvertEnterKey), index(InvertEnterKey), {Value});
|
emit dataChanged(index(InvertEnterKey), index(InvertEnterKey), {Value});
|
||||||
});
|
});
|
||||||
|
connect(s.get(), &UserSettings::sedEditingChanged, this, [this]() {
|
||||||
|
emit dataChanged(index(SedEditing), index(SedEditing), {Value});
|
||||||
|
});
|
||||||
connect(s.get(), &UserSettings::bubblesChanged, this, [this]() {
|
connect(s.get(), &UserSettings::bubblesChanged, this, [this]() {
|
||||||
emit dataChanged(index(Bubbles), index(Bubbles), {Value});
|
emit dataChanged(index(Bubbles), index(Bubbles), {Value});
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,6 +31,7 @@ class UserSettings final : public QObject
|
||||||
Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged)
|
Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged)
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
bool invertEnterKey READ invertEnterKey WRITE setInvertEnterKey NOTIFY invertEnterKeyChanged)
|
bool invertEnterKey READ invertEnterKey WRITE setInvertEnterKey NOTIFY invertEnterKeyChanged)
|
||||||
|
Q_PROPERTY(bool sedEditing READ sedEditing WRITE setSedEditing NOTIFY sedEditingChanged)
|
||||||
Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged)
|
Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged)
|
||||||
Q_PROPERTY(bool smallAvatars READ smallAvatars WRITE setSmallAvatars NOTIFY smallAvatarsChanged)
|
Q_PROPERTY(bool smallAvatars READ smallAvatars WRITE setSmallAvatars NOTIFY smallAvatarsChanged)
|
||||||
Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover
|
Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover
|
||||||
|
@ -172,6 +173,7 @@ public:
|
||||||
void setScrollbarsInRoomlist(bool state);
|
void setScrollbarsInRoomlist(bool state);
|
||||||
void setMarkdown(bool state);
|
void setMarkdown(bool state);
|
||||||
void setInvertEnterKey(bool state);
|
void setInvertEnterKey(bool state);
|
||||||
|
void setSedEditing(bool state);
|
||||||
void setBubbles(bool state);
|
void setBubbles(bool state);
|
||||||
void setSmallAvatars(bool state);
|
void setSmallAvatars(bool state);
|
||||||
void setAnimateImagesOnHover(bool state);
|
void setAnimateImagesOnHover(bool state);
|
||||||
|
@ -244,6 +246,7 @@ public:
|
||||||
int privacyScreenTimeout() const { return privacyScreenTimeout_; }
|
int privacyScreenTimeout() const { return privacyScreenTimeout_; }
|
||||||
bool markdown() const { return markdown_; }
|
bool markdown() const { return markdown_; }
|
||||||
bool invertEnterKey() const { return invertEnterKey_; }
|
bool invertEnterKey() const { return invertEnterKey_; }
|
||||||
|
bool sedEditing() const { return sedEditing_; }
|
||||||
bool bubbles() const { return bubbles_; }
|
bool bubbles() const { return bubbles_; }
|
||||||
bool smallAvatars() const { return smallAvatars_; }
|
bool smallAvatars() const { return smallAvatars_; }
|
||||||
bool animateImagesOnHover() const { return animateImagesOnHover_; }
|
bool animateImagesOnHover() const { return animateImagesOnHover_; }
|
||||||
|
@ -315,6 +318,7 @@ signals:
|
||||||
void startInTrayChanged(bool state);
|
void startInTrayChanged(bool state);
|
||||||
void markdownChanged(bool state);
|
void markdownChanged(bool state);
|
||||||
void invertEnterKeyChanged(bool state);
|
void invertEnterKeyChanged(bool state);
|
||||||
|
void sedEditingChanged(bool state);
|
||||||
void bubblesChanged(bool state);
|
void bubblesChanged(bool state);
|
||||||
void smallAvatarsChanged(bool state);
|
void smallAvatarsChanged(bool state);
|
||||||
void animateImagesOnHoverChanged(bool state);
|
void animateImagesOnHoverChanged(bool state);
|
||||||
|
@ -384,6 +388,7 @@ private:
|
||||||
bool scrollbarsInRoomlist_;
|
bool scrollbarsInRoomlist_;
|
||||||
bool markdown_;
|
bool markdown_;
|
||||||
bool invertEnterKey_;
|
bool invertEnterKey_;
|
||||||
|
bool sedEditing_;
|
||||||
bool bubbles_;
|
bool bubbles_;
|
||||||
bool smallAvatars_;
|
bool smallAvatars_;
|
||||||
bool animateImagesOnHover_;
|
bool animateImagesOnHover_;
|
||||||
|
@ -491,6 +496,7 @@ class UserSettingsModel : public QAbstractListModel
|
||||||
ReadReceipts,
|
ReadReceipts,
|
||||||
Markdown,
|
Markdown,
|
||||||
InvertEnterKey,
|
InvertEnterKey,
|
||||||
|
SedEditing,
|
||||||
Bubbles,
|
Bubbles,
|
||||||
SmallAvatars,
|
SmallAvatars,
|
||||||
|
|
||||||
|
|
|
@ -450,15 +450,67 @@ InputBar::generateRelations() const
|
||||||
void
|
void
|
||||||
InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbowify)
|
InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbowify)
|
||||||
{
|
{
|
||||||
|
auto trimmed = msg.trimmed();
|
||||||
mtx::events::msg::Text text = {};
|
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()->sedEditing() && 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<mtx::events::collections::TimelineEvents> 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() &&
|
if ((ChatPage::instance()->userSettings()->markdown() &&
|
||||||
useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
|
useMarkdown == MarkdownOverride::NOT_SPECIFIED) ||
|
||||||
useMarkdown == MarkdownOverride::ON) {
|
useMarkdown == MarkdownOverride::ON) {
|
||||||
text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString();
|
text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString();
|
||||||
// Remove markdown links by completer
|
// 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
|
// 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
|
// 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
|
// disable all markdown extensions
|
||||||
text.formatted_body = utils::markdownToHtml(msg, rainbowify, true).toStdString();
|
text.formatted_body = utils::markdownToHtml(msg, rainbowify, true).toStdString();
|
||||||
// keep everything as it was
|
// keep everything as it was
|
||||||
text.body = msg.trimmed().toStdString();
|
text.body = trimmed.toStdString();
|
||||||
|
|
||||||
// always send formatted
|
// always send formatted
|
||||||
text.format = "org.matrix.custom.html";
|
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());
|
auto related = room->relatedInfo(room->reply());
|
||||||
|
|
||||||
// Skip reply fallbacks to users who would cause a room ping with the fallback.
|
// 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
|
// This should be fine, since in some cases the reply fallback can be omitted now and
|
||||||
// alternative is worse! On Element Android this applies to any substring, but that is their
|
// the alternative is worse! On Element Android this applies to any substring, but that
|
||||||
// bug to fix.
|
// is their bug to fix.
|
||||||
if (!related.quoted_user.startsWith("@room:")) {
|
if (!related.quoted_user.startsWith("@room:")) {
|
||||||
QString body;
|
QString body;
|
||||||
bool firstLine = true;
|
bool firstLine = true;
|
||||||
|
|
Loading…
Reference in a new issue