Allow editing text and also hide edits from the timeline

This commit is contained in:
Carl Schwan
2020-12-28 09:37:17 +00:00
parent 9d82ebb0ed
commit c69d3587ba
8 changed files with 93 additions and 78 deletions

View File

@@ -22,8 +22,10 @@ ToolBar {
property alias isReply: replyItem.visible property alias isReply: replyItem.visible
property bool isReaction: false property bool isReaction: false
property var replyUser property var replyUser
property string replyEventID property string replyEventID: ""
property string replyContent property string replyContent: ""
property string editEventId
property alias isAutoCompleting: autoCompleteListView.visible property alias isAutoCompleting: autoCompleteListView.visible
property var autoCompleteModel property var autoCompleteModel
@@ -262,6 +264,19 @@ ToolBar {
} }
} }
RowLayout {
visible: editEventId.length > 0
ToolButton {
icon.name: "dialog-cancel"
onClicked: clearEditReply();
}
Label {
Layout.alignment: Qt.AlignVCenter
text: i18n("Edit Message")
}
}
Kirigami.Separator { Kirigami.Separator {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 1 Layout.preferredHeight: 1
@@ -280,7 +295,7 @@ ToolBar {
icon.name: "dialog-cancel" icon.name: "dialog-cancel"
onClicked: clearReply() onClicked: clearEditReply()
} }
@@ -359,13 +374,13 @@ ToolBar {
} else { } else {
postMessage() postMessage()
text = "" text = ""
clearReply() clearEditReply()
closeAll() closeAll()
} }
} }
Keys.onEscapePressed: { Keys.onEscapePressed: {
clearReply(); clearEditReply();
closeAll(); closeAll();
} }
@@ -450,7 +465,7 @@ ToolBar {
// lines. // lines.
const updatedText = inputField.text.trim() const updatedText = inputField.text.trim()
.replace(/@([^: ]*):([^ ]*\.[^ ]*)/, "[@$1:$2](https://matrix.to/#/@$1:$2)"); .replace(/@([^: ]*):([^ ]*\.[^ ]*)/, "[@$1:$2](https://matrix.to/#/@$1:$2)");
documentHandler.postMessage(updatedText, attachmentPath, replyEventID); documentHandler.postMessage(updatedText, attachmentPath, replyEventID, editEventId);
clearAttachment(); clearAttachment();
currentRoom.markAllMessagesAsRead(); currentRoom.markAllMessagesAsRead();
clear(); clear();
@@ -521,7 +536,7 @@ ToolBar {
onClicked: { onClicked: {
inputField.postMessage() inputField.postMessage()
inputField.text = "" inputField.text = ""
root.clearReply() root.clearEditReply()
root.closeAll() root.closeAll()
} }
@@ -553,17 +568,23 @@ ToolBar {
inputField.clear() inputField.clear()
} }
function clearReply() { function clearEditReply() {
isReply = false isReply = false;
replyUser = null; replyUser = null;
replyContent = ""; root.replyContent = "";
replyEventID = "" root.replyEventID = "";
root.editEventId = "";
} }
function focus() { function focus() {
inputField.forceActiveFocus() inputField.forceActiveFocus()
} }
function edit(editContent, editEventId) {
inputField.text = editContent;
root.editEventId = editEventId
}
function closeAll() { function closeAll() {
replyItem.visible = false replyItem.visible = false
autoCompleteListView.visible = false autoCompleteListView.visible = false

View File

@@ -134,6 +134,13 @@ RowLayout {
onReact: currentRoom.toggleReaction(eventId, emoji) onReact: currentRoom.toggleReaction(eventId, emoji)
} }
} }
QQC2.Button {
QQC2.ToolTip.text: i18n("Edit")
QQC2.ToolTip.visible: hovered
visible: controlContainer.hovered && author.id === Controller.activeConnection.localUserId && (model.eventType === "emote" || model.eventType === "message")
icon.name: "document-edit"
onClicked: chatTextInput.edit(message, eventId)
}
QQC2.Button { QQC2.Button {
QQC2.ToolTip.text: i18n("Reply") QQC2.ToolTip.text: i18n("Reply")
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered

View File

@@ -198,12 +198,14 @@ Kirigami.ScrollablePage {
filterRowCallback: Config.showLeaveJoinEvent ? dontFilterLeaveJoin : filterLeaveJoin filterRowCallback: Config.showLeaveJoinEvent ? dontFilterLeaveJoin : filterLeaveJoin
function dontFilterLeaveJoin(row, parent) { function dontFilterLeaveJoin(row, parent) {
return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10 return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other"; && messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other";
} }
function filterLeaveJoin(row, parent) { function filterLeaveJoin(row, parent) {
return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10 return messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.SpecialMarksRole) !== EventStatus.Hidden
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.MessageRole) !== 0x10
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other" && messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "other"
&& messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "state"; && messageEventModel.data(messageEventModel.index(row, 0), MessageEventModel.EventTypeRole) !== "state";
} }

View File

@@ -167,7 +167,7 @@ QVariantMap ChatDocumentHandler::getAutocompletionInfo()
}; };
} }
void ChatDocumentHandler::postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId) const void ChatDocumentHandler::postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QString &editEventId) const
{ {
if (!m_room || !m_document) { if (!m_room || !m_document) {
return; return;
@@ -200,7 +200,7 @@ void ChatDocumentHandler::postMessage(const QString &text, const QString &attach
for (int i = 0; i < cleanedText.length(); i++) { for (int i = 0; i < cleanedText.length(); i++) {
rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>"; rainbowText = rainbowText % QStringLiteral("<font color='") % rainbowColors.at(i % rainbowColors.length()) % "'>" % cleanedText.at(i) % "</font>";
} }
m_room->postHtmlMessage(cleanedText, rainbowText, messageEventType, replyEventId); m_room->postHtmlMessage(cleanedText, rainbowText, messageEventType, replyEventId, editEventId);
return; return;
} }
@@ -211,7 +211,7 @@ void ChatDocumentHandler::postMessage(const QString &text, const QString &attach
cleanedText = cleanedText.remove(0, noticePrefix.length()); cleanedText = cleanedText.remove(0, noticePrefix.length());
messageEventType = RoomMessageEvent::MsgType::Notice; messageEventType = RoomMessageEvent::MsgType::Notice;
} }
m_room->postArbitaryMessage(cleanedText, messageEventType, replyEventId); m_room->postMessage(cleanedText, messageEventType, replyEventId, editEventId);
} }
void ChatDocumentHandler::replaceAutoComplete(const QString &word) void ChatDocumentHandler::replaceAutoComplete(const QString &word)

View File

@@ -51,7 +51,7 @@ public:
[[nodiscard]] NeoChatRoom *room() const; [[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room); void setRoom(NeoChatRoom *room);
Q_INVOKABLE void postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId) const; Q_INVOKABLE void postMessage(const QString &text, const QString &attachementPath, const QString &replyEventId, const QString &editEventId) const;
/// This function will look at the current QTextCursor and determine if there /// This function will look at the current QTextCursor and determine if there
/// is the posibility to autocomplete it. /// is the posibility to autocomplete it.

View File

@@ -415,6 +415,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
} }
} }
// isReplacement?
if (auto e = eventCast<const RoomMessageEvent>(&evt))
if (!e->replacedEvent().isEmpty())
return EventStatus::Hidden;
if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt)) { if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt)) {
return EventStatus::Hidden; return EventStatus::Hidden;
} }

View File

@@ -479,18 +479,6 @@ QString NeoChatRoom::markdownToHTML(const QString &markdown)
return result; return result;
} }
void NeoChatRoom::postArbitaryMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type, const QString &replyEventId)
{
const auto parsedHTML = markdownToHTML(text);
const bool isRichText = Qt::mightBeRichText(parsedHTML);
if (isRichText) { // Markdown
postHtmlMessage(text, parsedHTML, type, replyEventId);
} else { // Plain text
postPlainMessage(text, type, replyEventId);
}
}
QString msgTypeToString(MessageEventType msgType) QString msgTypeToString(MessageEventType msgType)
{ {
switch (msgType) { switch (msgType) {
@@ -515,59 +503,48 @@ QString msgTypeToString(MessageEventType msgType)
} }
} }
void NeoChatRoom::postPlainMessage(const QString &text, MessageEventType type, const QString &replyEventId) void NeoChatRoom::postMessage(const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
{ {
bool isReply = !replyEventId.isEmpty(); const auto html = markdownToHTML(text);
const auto replyIt = findInTimeline(replyEventId); postHtmlMessage(text, html, type, replyEventId, relateToEventId);
if (replyIt == timelineEdge()) {
isReply = false;
}
if (isReply) {
const auto &replyEvt = **replyIt;
// clang-format off
QJsonObject json{
{"msgtype", msgTypeToString(type)},
{"body", "> <" + replyEvt.senderId() + "> " + eventToString(replyEvt) + "\n\n" + text},
{"format", "org.matrix.custom.html"},
{"m.relates_to",
QJsonObject {
{"m.in_reply_to",
QJsonObject {
{"event_id", replyEventId}
}
}
}
},
{"formatted_body",
"<mx-reply><blockquote><a href=\"https://matrix.to/#/" +
id() + "/" +
replyEventId +
"\">In reply to</a> <a href=\"https://matrix.to/#/" +
replyEvt.senderId() + "\">" + replyEvt.senderId() +
"</a><br>" + eventToString(replyEvt, Qt::RichText) +
"</blockquote></mx-reply>" + text
}
};
// clang-format on
postJson("m.room.message", json);
return;
}
Room::postMessage(text, type);
} }
void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId) void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId, const QString &relateToEventId)
{ {
QString htmlWithLinks = html;
htmlWithLinks = htmlWithLinks.replace(QRegularExpression("@([^: ]*):([^ ]*\\.[^ ]*)"), "<a href=\"https://matrix.to/#/@$1:$2\">@$1:$2</a>");
bool isRichText = Qt::mightBeRichText(htmlWithLinks);
bool isReply = !replyEventId.isEmpty(); bool isReply = !replyEventId.isEmpty();
bool isEdit = !relateToEventId.isEmpty();
const auto replyIt = findInTimeline(replyEventId); const auto replyIt = findInTimeline(replyEventId);
if (replyIt == timelineEdge()) { if (replyIt == timelineEdge()) {
isReply = false; isReply = false;
} }
if (isEdit) {
QJsonObject json {
{"type", "m.room.message"},
{"msgtype", msgTypeToString(type)},
{"body", "* " + (isRichText ? text : htmlWithLinks)},
{"m.new_content",
QJsonObject {
{"body", (isRichText ? text : htmlWithLinks)},
{"msgtype", msgTypeToString(type)}
}
},
{"m.relates_to",
QJsonObject {
{"rel_type", "m.replace"},
{"event_id", relateToEventId}
}
}
};
postJson("m.room.message", json);
return;
}
if (isReply) { if (isReply) {
const auto &replyEvt = **replyIt; const auto &replyEvt = **replyIt;
@@ -592,7 +569,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
"\">In reply to</a> <a href=\"https://matrix.to/#/" + "\">In reply to</a> <a href=\"https://matrix.to/#/" +
replyEvt.senderId() + "\">" + replyEvt.senderId() + replyEvt.senderId() + "\">" + replyEvt.senderId() +
"</a><br>" + eventToString(replyEvt, Qt::RichText) + "</a><br>" + eventToString(replyEvt, Qt::RichText) +
"</blockquote></mx-reply>" + html "</blockquote></mx-reply>" + (isRichText ? htmlWithLinks : text)
} }
}; };
// clang-format on // clang-format on
@@ -602,7 +579,11 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
return; return;
} }
Room::postHtmlMessage(text, html, type); if (isRichText) {
Room::postHtmlMessage(text, html, type);
} else {
Room::postMessage(text, type);
}
} }
void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction) void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction)

View File

@@ -130,13 +130,12 @@ Q_SIGNALS:
void lastActiveTimeChanged(); void lastActiveTimeChanged();
public Q_SLOTS: public Q_SLOTS:
void uploadFile(const QUrl &url, const QString &body = ""); void uploadFile(const QUrl &url, const QString &body = QString());
void acceptInvitation(); void acceptInvitation();
void forget(); void forget();
void sendTypingNotification(bool isTyping); void sendTypingNotification(bool isTyping);
void postArbitaryMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type, const QString &replyEventId); void postMessage(const QString &text, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), const QString &relateToEventId = QString());
void postPlainMessage(const QString &text, Quotient::RoomMessageEvent::MsgType type = Quotient::MessageEventType::Text, const QString &replyEventId = ""); void postHtmlMessage(const QString &text, const QString &html, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), const QString &relateToEventId = QString());
void postHtmlMessage(const QString &text, const QString &html, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = "");
void changeAvatar(const QUrl &localFile); void changeAvatar(const QUrl &localFile);
void addLocalAlias(const QString &alias); void addLocalAlias(const QString &alias);
void removeLocalAlias(const QString &alias); void removeLocalAlias(const QString &alias);