Page MenuHomePhorge

D22.1733020547.diff
No OneTemporary

Size
17 KB
Referenced Files
None
Subscribers
None

D22.1733020547.diff

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

Mime Type
text/plain
Expires
Sat, Nov 30, 6:35 PM (15 h, 57 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41561
Default Alt Text
D22.1733020547.diff (17 KB)

Event Timeline