Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F115907
D22.1732835083.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
D22.1732835083.diff
View Options
diff --git a/src/contents/ui/Bubble.qml b/src/contents/ui/Bubble.qml
--- a/src/contents/ui/Bubble.qml
+++ b/src/contents/ui/Bubble.qml
@@ -11,6 +11,7 @@
import org.kde.kirigami 2.13 as Kirigami
import '.' as Kazv
+import 'matrix-helpers.js' as Helpers
ItemDelegate {
id: upper
@@ -40,15 +41,30 @@
onAccepted: eventView.reactWith(text)
}
+ function getIsEditable(event) {
+ return event.sender === matrixSdk.userId
+ && event.type === 'm.room.message'
+ && event.content.msgtype === 'm.text';
+ }
+
property var menu: Menu {
objectName: 'bubbleContextMenu'
property list<QtObject> staticItems: [
Kirigami.Action {
objectName: 'replyToMenuItem'
text: l10n.get('event-reply-action')
- onTriggered: setDraftReplyTo(currentEvent.eventId)
+ onTriggered: setDraftRelation('m.in_reply_to', currentEvent.eventId)
enabled: event && !event.redacted
},
+ Kirigami.Action {
+ objectName: 'editMenuItem'
+ text: l10n.get('event-edit-action')
+ onTriggered: {
+ setDraftRelation('m.replace', currentEvent.eventId);
+ replaceDraftRequested(Helpers.getEventBodyForEditing(event));
+ }
+ enabled: event && !event.redacted && !event.isLocalEcho && getIsEditable(event)
+ },
Kirigami.Action {
objectName: 'deleteMenuItem'
text: l10n.get('event-delete')
diff --git a/src/contents/ui/RoomPage.qml b/src/contents/ui/RoomPage.qml
--- a/src/contents/ui/RoomPage.qml
+++ b/src/contents/ui/RoomPage.qml
@@ -23,6 +23,7 @@
property var lastReceiptableEventId: getLastReceiptableEventId(roomTimeline, roomTimeline.count)
signal mentionUserRequested(string userId)
+ signal replaceDraftRequested(string newDraft)
title: room.name || roomId
@@ -135,8 +136,9 @@
}
}
- function setDraftReplyTo(eventId) {
- sendMessageBox.draftReplyTo = eventId;
+ function setDraftRelation(relType, eventId) {
+ sendMessageBox.draftRelType = relType;
+ sendMessageBox.draftRelatedTo = eventId;
}
property var joinRoomHandler: Kazv.AsyncHandler {
@@ -168,6 +170,10 @@
sendMessageBox.focusInput();
}
+ onReplaceDraftRequested: (newDraft) => {
+ sendMessageBox.replaceDraft(newDraft);
+ }
+
function getLastReceiptableEventId(timeline, timelineCount) {
for (let i = 0; i < timelineCount; ++i) {
const event = timeline.at(i);
diff --git a/src/contents/ui/SendMessageBox.qml b/src/contents/ui/SendMessageBox.qml
--- a/src/contents/ui/SendMessageBox.qml
+++ b/src/contents/ui/SendMessageBox.qml
@@ -17,24 +17,45 @@
ColumnLayout {
id: sendMessageBox
property var room
- property var draftReplyTo: ''
+ property var draftRelType: ''
+ property var draftRelatedTo: ''
property var timeline: room.timeline()
+ function getRelationPrompt(draftRelType) {
+ if (draftRelType === 'm.in_reply_to') {
+ return l10n.get('send-message-box-reply-to');
+ } else if (draftRelType === 'm.replace') {
+ return l10n.get('send-message-box-edit');
+ }
+ }
+
+ function getCancelRelationPrompt(draftRelType) {
+ if (draftRelType === 'm.in_reply_to') {
+ return l10n.get('send-message-box-remove-reply-to-action');
+ } else if (draftRelType === 'm.replace') {
+ return l10n.get('send-message-box-remove-replace-action');
+ }
+ }
+
+ function replaceDraft(newDraft) {
+ textArea.changeText(newDraft, /* inhibitTyping = */ true);
+ }
+
onRoomChanged: {
textArea.changeText(room.localDraft, true);
}
ColumnLayout {
- visible: !!sendMessageBox.draftReplyTo
+ visible: !!sendMessageBox.draftRelatedTo
RowLayout {
Label {
Layout.fillWidth: true
- text: l10n.get('send-message-box-reply-to')
+ text: getRelationPrompt(sendMessageBox.draftRelType)
}
ToolButton {
- action: removeReplyToAction
+ action: removeRelatedToAction
display: AbstractButton.IconOnly
}
}
@@ -52,7 +73,7 @@
left: parent.left
right: parent.right
}
- event: room.messageById(sendMessageBox.draftReplyTo)
+ event: room.messageById(sendMessageBox.draftRelatedTo)
compactMode: true
}
}
@@ -143,11 +164,12 @@
}
Kirigami.Action {
- id: removeReplyToAction
+ id: removeRelatedToAction
iconName: 'window-close-symbolic'
- text: l10n.get('send-message-box-remove-reply-to-action')
+ text: getCancelRelationPrompt(sendMessageBox.draftRelType)
onTriggered: {
- sendMessageBox.draftReplyTo = '';
+ sendMessageBox.draftRelType = '';
+ sendMessageBox.draftRelatedTo = '';
}
}
@@ -157,9 +179,10 @@
text: l10n.get("send-message-box-send")
onTriggered: {
room.setTyping(false);
- room.sendTextMessage(textArea.text, draftReplyTo);
+ room.sendTextMessage(textArea.text, draftRelType, draftRelatedTo);
textArea.changeText("", true);
- draftReplyTo = '';
+ sendMessageBox.draftRelType = '';
+ sendMessageBox.draftRelatedTo = '';
}
}
Kirigami.Action {
@@ -199,8 +222,9 @@
stickerPackList: matrixSdk.stickerPackList()
onSendMessageRequested: eventJson => {
console.log(JSON.stringify(eventJson));
- room.sendMessage(eventJson, draftReplyTo);
- draftReplyTo = '';
+ room.sendMessage(eventJson, draftRelType, draftRelatedTo);
+ draftRelType = '';
+ draftRelatedTo = '';
stickerPopup.close();
}
}
diff --git a/src/js/matrix-helpers.js b/src/js/matrix-helpers.js
--- a/src/js/matrix-helpers.js
+++ b/src/js/matrix-helpers.js
@@ -59,3 +59,41 @@
}
return false;
}
+
+/**
+ * Get the event body for editing.
+ *
+ * This takes into account the replied-to content.
+ * As Matrix spec has nothing for the source content of a message,
+ * we can only use some heuristics to determine what really is
+ * what the user has typed.
+ *
+ * @param event The event to get the body of.
+ * @return The body for editing.
+ */
+function getEventBodyForEditing(event) {
+ if (
+ event.replyingToEventId
+ // Element will put the quotation inside the body and it follows the
+ // following format:
+ // ```
+ // > <@userid:domain.ltd> whatever quoted content
+ // > other lines
+ // > other lines
+ //
+ // actual content
+ // ```
+ && event.content.body.startsWith('> <@')
+ ) {
+ const body = event.content.body;
+ const lines = body.split('\n');
+ const lastLineOfQuote = lines.findIndex((line, index) => {
+ // the current line is a quote but the next line is not
+ return line.startsWith('>') && !(lines[index + 1] || '').startsWith('>');
+ });
+ // Considering the empty line may not exist, strip the empty line from the beginning
+ const firstLineOfContent = lines[lastLineOfQuote + 1] ? lastLineOfQuote + 1 : lastLineOfQuote + 2
+ return lines.slice(firstLineOfContent).join('\n');
+ }
+ return event.content.body;
+}
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -96,7 +96,9 @@
send-message-box-send = 发送
send-message-box-send-file = 发送文件
send-message-box-reply-to = 回复给
+send-message-box-edit = 编辑
send-message-box-remove-reply-to-action = 移除回复关系
+send-message-box-remove-replace-action = 取消编辑
send-message-box-stickers = 发送贴纸...
send-message-box-stickers-popup-title = 发送贴纸
@@ -158,6 +160,7 @@
event-react-accept-action = 回应
event-react-cancel-action = 取消
event-react-with-prompt = 回应以:
+event-edit-action = 编辑
event-encrypted = 这条消息已加密
event-msgtype-fallback-text = 未知的消息类型:{ $msgtype }
event-fallback-text = 未知事件:{ $type }
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -96,7 +96,9 @@
send-message-box-send = Send
send-message-box-send-file = Send file
send-message-box-reply-to = Replying to
+send-message-box-edit = Editing
send-message-box-remove-reply-to-action = Remove reply-to relationship
+send-message-box-remove-replace-action = Cancel editing
send-message-box-stickers = Send a sticker...
send-message-box-stickers-popup-title = Send a sticker
@@ -170,6 +172,7 @@
event-react-accept-action = React
event-react-cancel-action = Cancel
event-react-with-prompt = React with:
+event-edit-action = Edit
event-encrypted = This message is encrypteed
event-msgtype-fallback-text = Unknown message type: { $msgtype }
event-fallback-text = Unknown event: { $type }
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -24,7 +24,7 @@
class MatrixRoomMemberListModel;
class MatrixEvent;
-nlohmann::json makeTextMessageJson(const QString &text, const QString &replyTo, Kazv::Event replyToEvent);
+nlohmann::json makeTextMessageJson(const QString &text, const QString &relType, const QString &relatedTo, Kazv::Event replyToEvent);
class MatrixRoom : public QObject
{
@@ -68,9 +68,9 @@
Q_INVOKABLE MatrixEvent *messageById(QString eventId) const;
- Q_INVOKABLE void sendMessage(const QJsonObject &eventJson, const QString &replyTo) const;
+ Q_INVOKABLE void sendMessage(const QJsonObject &eventJson, const QString &relType, const QString &relatedTo) const;
- Q_INVOKABLE void sendTextMessage(QString text, QString replyTo) const;
+ Q_INVOKABLE void sendTextMessage(QString text, const QString &relType, QString relatedTo) const;
Q_INVOKABLE void sendMediaFileMessage(QString fileName, QString mimeType,
qint64 fileSize, QString mxcUri) const;
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -92,20 +92,29 @@
return new MatrixRoomTimeline(m_room);
}
-static void maybeAddRelations(nlohmann::json &msg, const QString &replyTo)
+static void maybeAddRelations(nlohmann::json &msg, const QString &relType, const QString &relatedTo)
{
- if (!replyTo.isEmpty()) {
+ if (relatedTo.isEmpty()) {
+ return;
+ }
+
+ if (relType == "m.in_reply_to") {
msg["content"]["m.relates_to"] = nlohmann::json{
{"m.in_reply_to", {
- {"event_id", replyTo},
+ {"event_id", relatedTo},
}},
};
+ } else {
+ msg["content"]["m.relates_to"] = nlohmann::json{
+ {"rel_type", relType},
+ {"event_id", relatedTo},
+ };
}
}
static const std::string HTML_FORMAT = "org.matrix.custom.html";
-nlohmann::json makeTextMessageJson(const QString &text, const QString &replyTo, Event replyToEvent)
+nlohmann::json makeTextMessageJson(const QString &text, const QString &relType, const QString &relatedTo, Event replyToEvent)
{
auto msg = nlohmann::json{
{"type", "m.room.message"},
@@ -116,7 +125,7 @@
};
std::string replyToBody;
- if (!replyTo.isEmpty()) {
+ if (relType == "m.in_reply_to" && !relatedTo.isEmpty()) {
auto replyToContent = replyToEvent.content().get();
if (replyToContent.contains("format")
&& replyToContent["format"] == HTML_FORMAT
@@ -150,23 +159,27 @@
};
}
- maybeAddRelations(msg, replyTo);
+ if (relType == "m.replace" && !relatedTo.isEmpty()) {
+ msg["content"]["m.new_content"] = msg["content"];
+ }
+
+ maybeAddRelations(msg, relType, relatedTo);
return msg;
}
-void MatrixRoom::sendMessage(const QJsonObject &eventJson, const QString &replyTo) const
+void MatrixRoom::sendMessage(const QJsonObject &eventJson, const QString &relType, const QString &relatedTo) const
{
auto msg = nlohmann::json(eventJson);
- maybeAddRelations(msg, replyTo);
+ maybeAddRelations(msg, relType, relatedTo);
m_room.sendMessage(Event(msg));
}
-void MatrixRoom::sendTextMessage(QString text, QString replyTo) const
+void MatrixRoom::sendTextMessage(QString text, const QString &relType, QString relatedTo) const
{
- Event replyToEvent = replyTo.isEmpty()
- ? Event()
- : m_room.message(lager::make_constant(replyTo.toStdString())).make().get();
- auto j = makeTextMessageJson(text, replyTo, replyToEvent);
+ Event replyToEvent = (relType == "m.in_reply_to" && !relatedTo.isEmpty())
+ ? m_room.message(lager::make_constant(relatedTo.toStdString())).make().get()
+ : Event();
+ auto j = makeTextMessageJson(text, relType, relatedTo, replyToEvent);
m_room.sendMessage(Event(j));
}
diff --git a/src/tests/kazv-markdown-test.cpp b/src/tests/kazv-markdown-test.cpp
--- a/src/tests/kazv-markdown-test.cpp
+++ b/src/tests/kazv-markdown-test.cpp
@@ -114,14 +114,14 @@
void KazvMarkdownTest::testMakeMentionsEvent()
{
auto replyToEvent = makeEvent(withEventKV(json::json_pointer("/sender"), "@foo:example.com"));
- auto e1 = makeTextMessageJson("@foo:example.com mew", "", Kazv::Event());
+ auto e1 = makeTextMessageJson("@foo:example.com mew", "", "", Kazv::Event());
QVERIFY(e1["content"]["m.mentions"]["user_ids"] == json{"@foo:example.com"});
- auto e2 = makeTextMessageJson("@foo:example.com mew", QString::fromStdString(replyToEvent.id()), replyToEvent);
+ auto e2 = makeTextMessageJson("@foo:example.com mew", "", QString::fromStdString(replyToEvent.id()), replyToEvent);
QVERIFY(e2["content"]["m.mentions"]["user_ids"] == json{"@foo:example.com"});
- auto e3 = makeTextMessageJson("@bar:example.com mew", QString::fromStdString(replyToEvent.id()), replyToEvent);
+ auto e3 = makeTextMessageJson("@bar:example.com mew", "", QString::fromStdString(replyToEvent.id()), replyToEvent);
QVERIFY((e3["content"]["m.mentions"]["user_ids"] == json{"@bar:example.com", "@foo:example.com"}));
}
diff --git a/src/tests/quick-tests/tst_EventView.qml b/src/tests/quick-tests/tst_EventView.qml
--- a/src/tests/quick-tests/tst_EventView.qml
+++ b/src/tests/quick-tests/tst_EventView.qml
@@ -151,6 +151,18 @@
formattedTime: '4:06 P.M.',
})
+ property var eventTextBySomeoneElse: ({
+ eventId: '',
+ sender: '@bar:tusooa.xyz',
+ type: 'm.room.message',
+ stateKey: '',
+ content: {
+ msgtype: 'm.text',
+ body: 'some body',
+ },
+ formattedTime: '4:06 P.M.',
+ })
+
property var ignoredEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
@@ -204,6 +216,13 @@
avatarMxcUri: '',
})
+ property var senderOther: ({
+ membership: 'join',
+ userId: '@bar:tusooa.xyz',
+ name: 'bar',
+ avatarMxcUri: '',
+ })
+
ColumnLayout {
anchors.fill: parent
Kazv.EventView {
@@ -307,6 +326,12 @@
sender: item.sender
prevEvent: item.ignoredEvent
}
+
+ Kazv.EventView {
+ id: eventViewTextBySomeoneElse
+ event: item.eventTextBySomeoneElse
+ sender: item.senderOther
+ }
}
TestCase {
@@ -382,6 +407,7 @@
const menu = findChild(eventViewText, 'bubbleContextMenu');
verify(menu);
tryVerify(() => findChild(menu, 'replyToMenuItem').enabled);
+ tryVerify(() => findChild(menu, 'editMenuItem').enabled);
tryVerify(() => findChild(menu, 'deleteMenuItem').enabled);
}
@@ -422,5 +448,11 @@
verify(findChild(eventViewDontCollapseIgnoredEvent, 'senderAvatar').visible);
verify(!findChild(eventViewDontCollapseIgnoredEvent, 'senderCollapsedPlaceholder').visible);
}
+
+ function test_eventByOther() {
+ const menu = findChild(eventViewTextBySomeoneElse, 'bubbleContextMenu');
+ verify(menu);
+ tryVerify(() => !findChild(menu, 'editMenuItem').enabled);
+ }
}
}
diff --git a/src/tests/quick-tests/tst_MatrixHelpers.qml b/src/tests/quick-tests/tst_MatrixHelpers.qml
new file mode 100644
--- /dev/null
+++ b/src/tests/quick-tests/tst_MatrixHelpers.qml
@@ -0,0 +1,67 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import QtQuick 2.15
+import QtTest 1.0
+
+import '../../js/matrix-helpers.js' as Helpers
+
+TestCase {
+ id: matrixHelpersTest
+ name: 'MatrixHelpersTest'
+ when: true
+
+ property var eventTestData: [
+ [{
+ replyingToEventId: '',
+ content: {
+ body: '> <@tusooa:tusooa.xyz> some text\n> some text\n\nmew\n\nmew',
+ },
+ }, '> <@tusooa:tusooa.xyz> some text\n> some text\n\nmew\n\nmew'],
+ [{
+ replyingToEventId: '$1',
+ content: {
+ body: '> <@tusooa:tusooa.xyz> some text\n> some text\n\nmewmew',
+ },
+ }, 'mewmew'],
+ [{
+ replyingToEventId: '$1',
+ content: {
+ body: '> <@tusooa:tusooa.xyz> some text\n\nmewmew',
+ },
+ }, 'mewmew'],
+ [{
+ replyingToEventId: '$1',
+ content: {
+ body: '> <@tusooa:tusooa.xyz> some text\nmewmew',
+ },
+ }, 'mewmew'],
+ [{
+ replyingToEventId: '$1',
+ content: {
+ body: 'mewmew',
+ },
+ }, 'mewmew'],
+ [{
+ replyingToEventId: '$1',
+ content: {
+ body: '> <@tusooa:tusooa.xyz> some text\n> some text\n\n> some\nmewmew',
+ },
+ }, '> some\nmewmew'],
+ [{
+ replyingToEventId: '$1',
+ content: {
+ body: '> some\n\nmewmew',
+ },
+ }, '> some\n\nmewmew'],
+ ]
+
+ function test_getEventBodyForEditing() {
+ for (const [event, expected] of eventTestData) {
+ compare(Helpers.getEventBodyForEditing(event), expected);
+ }
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Nov 28, 3:04 PM (17 h, 20 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41237
Default Alt Text
D22.1732835083.diff (17 KB)
Attached To
Mode
D22: Implement event editing
Attached
Detach File
Event Timeline
Log In to Comment