From ec259f683ec0a0a4f4f52c1dfdc2f7e97478ae09 Mon Sep 17 00:00:00 2001 From: Sergei Ilinykh Date: Wed, 3 Jul 2024 23:23:15 +0300 Subject: [PATCH] Added XEP-0424: message retraction support --- iris | 2 +- psi.doap | 10 ++ src/chatdlg.cpp | 14 +- src/chatdlg.h | 1 + src/chatview_te.h | 1 + src/chatview_webkit.cpp | 19 ++- src/chatview_webkit.h | 1 + src/groupchatdlg.cpp | 20 +++ src/groupchatdlg.h | 1 + src/messageview.cpp | 7 + src/messageview.h | 9 +- src/psiaccount.cpp | 6 +- themes/chatview/psi/adapter.js | 11 +- themes/chatview/psi/bubble/index.html | 192 +++++++++++++------------- themes/chatview/psi/bubble/load.js | 2 +- themes/chatview/util.js | 1 + 16 files changed, 185 insertions(+), 112 deletions(-) diff --git a/iris b/iris index c94e7df88..e50f3d34d 160000 --- a/iris +++ b/iris @@ -1 +1 @@ -Subproject commit c94e7df8887a2836991b7132ee7ddf9132ad3acd +Subproject commit e50f3d34dfa39ed0016386bf731fe2df3ff5036f diff --git a/psi.doap b/psi.doap index 1c7dcfb5d..b00f7bd97 100644 --- a/psi.doap +++ b/psi.doap @@ -781,6 +781,16 @@ + + + + + partial + Without security checks for now. + 0.2.1 + + + diff --git a/src/chatdlg.cpp b/src/chatdlg.cpp index b7f846d23..a3add68d6 100644 --- a/src/chatdlg.cpp +++ b/src/chatdlg.cpp @@ -29,7 +29,6 @@ #include "iconaction.h" #include "iconlabel.h" #include "iconselect.h" -#include "iconwidget.h" #include "jidutil.h" #include "msgmle.h" #include "pgputil.h" @@ -149,6 +148,7 @@ void ChatDlg::init() #endif chatView()->init(); connect(chatView(), &ChatView::outgoingReactions, this, &ChatDlg::sendOutgoingReactions); + connect(chatView(), &ChatView::outgoingMessageRetraction, this, &ChatDlg::sendMessageRetraction); // seems its useless hack // connect(chatView(), SIGNAL(selectionChanged()), SLOT(logSelectionChanged())); // @@ -769,6 +769,14 @@ void ChatDlg::sendOutgoingReactions(const QString &messageId, const QSetdispatchMessage(mv); } + if (!m.retraction().isEmpty()) { + auto mv = MessageView::retractionMessage(m.retraction()); + chatView()->dispatchMessage(mv); + } } else { // Normal message // Check if user requests event messages diff --git a/src/chatdlg.h b/src/chatdlg.h index 5da70143c..ff946009d 100644 --- a/src/chatdlg.h +++ b/src/chatdlg.h @@ -147,6 +147,7 @@ private slots: void setComposing(); void getHistory(); void sendOutgoingReactions(const QString &messageId, const QSet &reactions); + void sendMessageRetraction(const QString &messageId); protected slots: void checkComposing(); diff --git a/src/chatview_te.h b/src/chatview_te.h index 694b6a9f1..440af9c76 100644 --- a/src/chatview_te.h +++ b/src/chatview_te.h @@ -102,6 +102,7 @@ private slots: void quote(const QString &text); void nickInsertClick(const QString &nick); void outgoingReactions(const QString &messageId, const QSet &reactions); + void outgoingMessageRetraction(const QString &messageId); private: bool isMuc_; diff --git a/src/chatview_webkit.cpp b/src/chatview_webkit.cpp index 5d2e16f24..18b0172f7 100644 --- a/src/chatview_webkit.cpp +++ b/src/chatview_webkit.cpp @@ -339,13 +339,22 @@ class ChatViewJSObject : public ChatViewThemeSession { _view->outgoingReaction(messageId, reaction); } - Q_INVOKABLE void deleteMessage(const QString &messageId) { qDebug() << "deleteMessage" << messageId; } + Q_INVOKABLE void deleteMessage(const QString &messageId) + { + emit _view->outgoingMessageRetraction(messageId); + if (!_view->d->isMuc_) { + // send back immediately coz it's not it's not iq and not MUC where server sends it back + QVariantMap vm; + vm["type"] = QLatin1String("msgretract"); + vm["targetid"] = messageId; + emit newMessage(vm); + } + } Q_INVOKABLE void replyMessage(const QString &messageId, const QString "edHtml) { auto plainText = TextUtil::rich2plain(quotedHtml); emit _view->quote(plainText); - qDebug() << "replyMessage" << messageId; } Q_INVOKABLE void copyMessage(const QString &messageId) { qDebug() << "copyMessage" << messageId; } @@ -676,6 +685,7 @@ void ChatView::dispatchMessage(const MessageView &mv) types.insert(MessageView::FileTransferFinished, "ftfin"); types.insert(MessageView::NickChange, "newnick"); types.insert(MessageView::Reactions, "reactions"); + types.insert(MessageView::MessageRetraction, "msgretract"); } QVariantMap m; switch (mv.type()) { @@ -741,6 +751,9 @@ void ChatView::dispatchMessage(const MessageView &mv) d->sendReactionsToUI(n, mv.reactionsId(), mv.reactions()); return; } + case MessageView::MessageRetraction: + m["targetid"] = mv.retractionId(); + break; case MessageView::FileTransferRequest: case MessageView::FileTransferFinished: break; @@ -762,7 +775,7 @@ void ChatView::dispatchMessage(const MessageView &mv) if (!replaceId.isEmpty()) { m["type"] = "replace"; m["replaceId"] = replaceId; - } else { + } else if (mv.type() != MessageView::MessageRetraction) { m["mtype"] = m["type"]; m["type"] = "message"; } diff --git a/src/chatview_webkit.h b/src/chatview_webkit.h index 13599a158..3b5cd587a 100644 --- a/src/chatview_webkit.h +++ b/src/chatview_webkit.h @@ -98,6 +98,7 @@ private slots: void nickInsertClick(const QString &nick); void quote(const QString &text); void outgoingReactions(const QString &messageId, const QSet &reactions); + void outgoingMessageRetraction(const QString &messageId); private: friend class ChatViewPrivate; diff --git a/src/groupchatdlg.cpp b/src/groupchatdlg.cpp index aac806c66..6e7a894e8 100644 --- a/src/groupchatdlg.cpp +++ b/src/groupchatdlg.cpp @@ -690,6 +690,19 @@ void GCMainDlg::outgoingReactions(const QString &messageId, const QSet emit aSend(m); } +void GCMainDlg::sendMessageRetraction(const QString &messageId) +{ + Message m(jid()); + m.setType(Message::Type::Groupchat); + m.setRetraction(messageId); + // QString id = account()->client()->genUniqueId(); + // m.setId(id); // we need id early for message manipulations in chatview + // m.setTimeStamp(QDateTime::currentDateTime()); + // d->mle()->appendMessageHistory(m.body()); + + emit aSend(m); +} + void GCMainDlg::doContactContextMenu(const QString &nick) { auto itm = d->usersModel->findEntry(nick); @@ -891,6 +904,7 @@ GCMainDlg::GCMainDlg(PsiAccount *pa, const Jid &j, TabManager *tabManager) : Tab connect(URLObject::getInstance(), SIGNAL(openURL(QString)), SLOT(openURL(QString))); connect(ui_.log, SIGNAL(nickInsertClick(QString)), SLOT(onNickInsertClick(QString))); connect(ui_.log, &ChatView::outgoingReactions, this, &GCMainDlg::outgoingReactions); + connect(ui_.log, &ChatView::outgoingMessageRetraction, this, &GCMainDlg::sendMessageRetraction); PsiToolTip::install(ui_.le_topic); @@ -2179,6 +2193,12 @@ void GCMainDlg::message(const Message &_m, const PsiEvent::Ptr &e) return; } + if (!m.retraction().isEmpty()) { + auto mv = MessageView::retractionMessage(m.retraction()); + ui_.log->dispatchMessage(mv); + return; + } + if (m.body().isEmpty()) return; diff --git a/src/groupchatdlg.h b/src/groupchatdlg.h index 2adf4574c..29a7e934a 100644 --- a/src/groupchatdlg.h +++ b/src/groupchatdlg.h @@ -146,6 +146,7 @@ private slots: void doContactContextMenu(const QString &nick); void outgoingReactions(const QString &messageId, const QSet &reactions); + void sendMessageRetraction(const QString &messageId); public: class Private; friend class Private; diff --git a/src/messageview.cpp b/src/messageview.cpp index 7df4d7ccc..c002a95d1 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -102,6 +102,13 @@ MessageView MessageView::reactionsMessage(const QString &nick, const QString &ta return mv; } +MessageView MessageView::retractionMessage(const QString &targetMessageId) +{ + MessageView mv(MessageRetraction); + mv.setRetractionId(targetMessageId); + return mv; +} + MessageView MessageView::statusMessage(const QString &nick, int status, const QString &statusText, int priority) { QString message = QObject::tr("%1 is now %2").arg(nick, status2txt(status)); diff --git a/src/messageview.h b/src/messageview.h index 289f10562..6a5fee6ac 100644 --- a/src/messageview.h +++ b/src/messageview.h @@ -40,6 +40,7 @@ class MessageView { FileTransferRequest, FileTransferFinished, Reactions, + MessageRetraction }; enum Flag { @@ -74,6 +75,7 @@ class MessageView { static MessageView nickChangeMessage(const QString &nick, const QString &newNick); static MessageView reactionsMessage(const QString &nick, const QString &targetMessageId, const QSet &reactions); + static MessageView retractionMessage(const QString &targetMessageId); inline Type type() const { return _type; } inline const QString &text() const { return _text; } @@ -122,12 +124,14 @@ class MessageView { inline const QString &replaceId() const { return _replaceId; } inline void setQuoteId(const QString &id) { _quoteId = id; } inline const QString "eId() const { return _quoteId; } - inline void setReactionsId(const QString &id) { _reactionsId = id; } - inline const QString &reactionsId() const { return _reactionsId; } + inline void setRetractionId(const QString &id) { _retractionId = id; } + inline const QString &retractionId() const { return _retractionId; } inline void setCarbonDirection(XMPP::Message::CarbonDir c) { _carbon = c; } inline XMPP::Message::CarbonDir carbonDirection() const { return _carbon; } inline void addReference(FileSharingItem *fsi) { _references.append(fsi); } inline const QList &references() const { return _references; } + inline void setReactionsId(const QString &id) { _reactionsId = id; } + inline const QString &reactionsId() const { return _reactionsId; } inline void setReactions(const QSet &r) { _reactions = r; } inline const QSet &reactions() const { return _reactions; } @@ -145,6 +149,7 @@ class MessageView { QMap _urls; QString _replaceId; QString _quoteId; + QString _retractionId; QString _reactionsId; XMPP::Message::CarbonDir _carbon; QList _references; diff --git a/src/psiaccount.cpp b/src/psiaccount.cpp index 2b3d0093d..3bf4a8eea 100644 --- a/src/psiaccount.cpp +++ b/src/psiaccount.cpp @@ -1576,6 +1576,9 @@ void PsiAccount::updateFeatures() if (themeFeatures.contains(QStringLiteral("reactions"))) { features << QLatin1String("urn:xmpp:reactions:0"); } + if (themeFeatures.contains(QStringLiteral("message-retract"))) { + features << QLatin1String("urn:xmpp:message-retract:1"); + } } #endif @@ -2836,7 +2839,8 @@ void PsiAccount::processIncomingMessage(const Message &_m) if (_m.type() != Message::Type::Error && _m.body().isEmpty() && _m.urlList().isEmpty() && _m.invite().isEmpty() && !_m.containsEvents() && _m.chatState() == StateNone && _m.subject().isNull() && _m.rosterExchangeItems().isEmpty() && _m.mucInvites().isEmpty() && _m.getForm().fields().empty() - && _m.messageReceipt() == ReceiptNone && _m.getMUCStatuses().isEmpty() && _m.reactions().targetId.isEmpty()) + && _m.messageReceipt() == ReceiptNone && _m.getMUCStatuses().isEmpty() && _m.reactions().targetId.isEmpty() + && _m.retraction().isEmpty()) return; // skip headlines? diff --git a/themes/chatview/psi/adapter.js b/themes/chatview/psi/adapter.js index 370ca18da..8dca1fb55 100644 --- a/themes/chatview/psi/adapter.js +++ b/themes/chatview/psi/adapter.js @@ -151,18 +151,17 @@ function psiThemeAdapter(chat) { shared.msgElPostProcess = config.postProcess; for (var tname in config.templates) { if (config.templates[tname]) { - t[tname] = new shared.Template(config.templates[tname]); + t[tname] = new shared.Template(config.templates[tname].trim()); } } t.message = t.message || "%message%"; t.sys = t.sys || "%message%"; t.sysMessage = t.sysMessage || t.sys; t.sysMessageUT = t.sysMessageUT || t.sysMessage; - t.statusMessageUT = t.statusMessageUT || (t.statusMessage || t.sysMessageUT); t.statusMessage = t.statusMessage || t.sysMessage; + t.statusMessageUT = t.statusMessageUT || (t.statusMessage || t.sysMessageUT); t.sentMessage = t.sentMessage || t.message; t.receivedMessage = t.receivedMessage || t.message; - t.spooledMessage = t.spooledMessage || t.message; t.receivedMessageGroupping = t.receivedMessageGroupping || t.messageGroupping; t.sentMessageGroupping = t.sentMessageGroupping || t.messageGroupping; t.lastMsgDate = t.lastMsgDate || t.sys; @@ -297,11 +296,7 @@ function psiThemeAdapter(chat) { } if (!template) { data.nextOfGroup = false; //can't group w/o template - if (data.spooled) { - template = shared.templates.spooledMessage; - } else { - template = data.local?shared.templates.sentMessage:shared.templates.receivedMessage; - } + template = data.local?shared.templates.sentMessage:shared.templates.receivedMessage; } break; case "join": diff --git a/themes/chatview/psi/bubble/index.html b/themes/chatview/psi/bubble/index.html index f0048bc8b..847a36720 100644 --- a/themes/chatview/psi/bubble/index.html +++ b/themes/chatview/psi/bubble/index.html @@ -21,22 +21,41 @@ util.getFont(function(cssFont){util.updateObject(cssBody, cssFont)}); } -const messageTemplate = `
- +const messageTemplate = ` +
+
+%template{messageGroupping}% +%next% +
+`; + +const receivedMessageTemplate = ` +
+
%sender% -%nickControl% +
Reply
+%template{messageGroupping}% +%next% +
+`; + +const messageGroupping = ` +
%time% %quoteTxt% %message% -
%next%`; +
+%next% +` shared.initTheme({ chatElement : document.body, templates : { message: messageTemplate, - messageGroupping: messageTemplate, + receivedMessage: receivedMessageTemplate, + messageGroupping: messageGroupping, sys: `
%message%
`, sysMessageUT: `
%message%
%usertext%
`, lastMsgDate: `
%time{LL}%
`, @@ -49,19 +68,11 @@ if (shared.cdata.type == "reactions") { renderReactions(shared.cdata); return false; + } else if (shared.cdata.type == "msgretract") { + retractMessage(shared.cdata.targetid); } }, varHandlers : { - msgClasses: function() { - let classes = ["msg"]; - if (shared.cdata.nextOfGroup) { - classes.push("grnext") - } - if (shared.cdata.local) { - classes.push("mymsg"); - } - return classes.join(" "); - }, msgtextClasses: function() { let classes = ["msgtext"]; if (shared.cdata.alert) { @@ -69,9 +80,6 @@ } return classes.join(" "); }, - nickControl: function() { - return shared.cdata.local? "": `
Reply
`; - }, quoteTxt: function() { if (!shared.cdata.reply) { return ""; @@ -86,6 +94,22 @@ } }, postProcess: function(el) { + if (shared.cdata.mtype != "message") { + return; + } + if (!el.classList.contains("grnext")) { + var replyBtn = el.getElementsByClassName("reply").item(0); + if (replyBtn) { + replyBtn.addEventListener("click", ()=>{onReplyClicked(el);}); + } + var nickEl = el.getElementsByClassName("nick").item(0); + if (nickEl) { + nickEl.addEventListener("click", ()=>{shared.session.nickInsertClick(nickEl.textContent);}); + } + el = el.getElementsByClassName("grnext").item(0); + } else { + fixReplyVisibility(el.parentNode); + } likeButton.setupForMessageElement(el); if (shared.cdata.reply) { const bq = el.getElementsByTagName("blockquote")[0]; @@ -93,17 +117,17 @@ bq.addEventListener("click", () => { quoteMsg.scrollIntoView({ "behavior": "smooth", "block": "center" }) }); } } - var replyBtn = el.getElementsByClassName("reply").item(0); - if (replyBtn) { - replyBtn.addEventListener("click", ()=>{onReplyClicked(el);}); - } - var nickEl = el.getElementsByClassName("nick").item(0); - if (nickEl) { - nickEl.addEventListener("click", ()=>{shared.session.nickInsertClick(nickEl.textContent);}); - } } }); +function fixReplyVisibility(msgBlock) { + const reply = msgBlock.getElementsByClassName("reply").item(0); + if (reply) { + const visible = msgBlock.getElementsByClassName("grnext").length == 1; + reply.style.display = visible? "inline-block" : "none"; + } +} + function onReplyClicked(el) { var textEl = el.getElementsByClassName("msgtext").item(0); shared.session.replyMessage(el.id, textEl.innerHTML); @@ -113,10 +137,16 @@ chatMenu.addItemProvider((event) => { const isNick = event.target.classList.contains("nick"); let msgNode; + let hasGrNext = false; if (!isNick) { msgNode = event.target; while (msgNode) { - if (msgNode.classList && msgNode.classList.contains("msg")) { + const mcl = msgNode.classList; + hasGrNext = mcl.contains("grnext"); + if (mcl.contains("avatar")) { + return []; // ignore avatar completely + } + if (msgNode.classList && (hasGrNext || mcl.contains("msg"))) { break; } msgNode = msgNode.parentNode; @@ -135,11 +165,20 @@ ]) } } else { + if (!hasGrNext) { + const nodes = msgNode.getElementsByClassName("grnext"); + if (nodes.length > 1) { + // clicked on message header but there are many messages + return []; + } + msgNode = nodes.item(0); + } items = [ - { text: "Delete", action: ()=>{ shared.session.deleteMessage(msgNode.id); } }, { text: "Reply", action: ()=>{ onReplyClicked(msgNode); } }, + { text: "Copy", action: ()=>{ shared.session.copyMessage(msgNode.id); } }, { text: "Forward", action: ()=>{ shared.session.forwardMessage(msgNode.id); } }, - { text: "Copy", action: ()=>{ shared.session.copyMessage(msgNode.id); } } + { text: "Edit", action: ()=>{ shared.session.editMessage(msgNode.id); } }, + { text: "Delete", action: ()=>{ shared.session.deleteMessage(msgNode.id); } } ] } return items; @@ -148,6 +187,16 @@ }); } +function retractMessage(targetId) { + const msg = document.getElementById(targetId); + if (msg && msg.classList.contains("grnext")) { + const parent = msg.parentNode; + parent.removeChild(msg); + if (parent.getElementsByClassName("grnext").length == 0) { + parent.parentNode.removeChild(parent); + } + } +} function renderReactions(event) { const msg = document.getElementById(event.messageid); @@ -273,10 +322,6 @@ flex-wrap: wrap; } -.msg:hover { - background-color: #ffffff; -} - .msg::before { width: 2rem; height: 2rem; @@ -293,10 +338,6 @@ clip: rect(0.6rem, 3rem, 1.5rem, 2rem); } -.msg:hover::before { - border-color: #ffffff; -} - .mymsg { margin-left: 0.5rem; margin-right: 3rem; @@ -305,10 +346,6 @@ padding-top: .5rem; } -.mymsg:hover { - background-color: #ffe; -} - .mymsg::before { left: inherit; right: 0; @@ -318,10 +355,6 @@ clip: rect(0.6rem, 2rem, 1.5rem, 0); } -.mymsg:hover::before { - border-color: #ffe; -} - .msgheader { display: block; flex-basis: 100%; @@ -355,10 +388,6 @@ text-decoration: underline; } -.mymsg .msgheader { - display: none; -} - .time { order: 4; right: .5rem; @@ -375,9 +404,9 @@ top: 0; left: 0; position: absolute; - max-width: 2.3rem; z-index: -1; - border-radius: .5rem; + height: 1.1em; + width: 2.3rem; } .mymsg .avatar { @@ -386,6 +415,14 @@ right: 0; } +.avatar > img { + max-height: 100%; + max-width: 100%; + display: block; + margin: 0 auto; + border-radius: 0.5rem; +} + .msgtext { color: #111; word-break: break-word; @@ -427,8 +464,8 @@ .like_button { position: absolute; - bottom: .3rem; - left: -2.5rem; + bottom: 0rem; + left: -3rem; width: 3rem; height: 1.5rem; text-align: center; @@ -451,53 +488,18 @@ .mymsg .like_button { left: inherit; - right: -2.5rem; + right: -3rem; } .grnext { display: flex; + width: 100%; flex-wrap: wrap; - margin-top: -0.15rem; - padding-top: 0.3rem; - border-top-right-radius: 0; - border-top-left-radius: 0; - /* We clip it like -_ _ -| |_____| | -| | -|_________| -where central part is for the text and everything else for the shadow. -this 0.08rem below together with margin-top: -0.15rem; is a workaround for -shadow clipping Google Chrome bug. -*/ - clip-path: polygon(0 0, - 0 -.5rem, - -5rem -.5rem, - -5rem calc(100% + .5rem), - calc(100% + 5rem) calc(100% + .5rem), - calc(100% + 5rem) -.5rem, - 100% -.5rem, - 100% 0.08rem, - 0 0.08rem); -} - -.grnext .msgheader { - display: none; -} - -.grnext .avatar { - display: none; -} - -.grnext:before { - display: none; + position: relative; } -.msg:has(+ .grnext) { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - margin-bottom: 0; - padding-bottom: .2rem; +.grnext:hover { + background: linear-gradient(90deg, #FFFFFF00 0%, #FFFFFFFF 10%, #FFFFFFFF 90%, #FFFFFF00 100%) } blockquote { @@ -524,7 +526,7 @@ .reactions_selector { display: none; position: absolute; - padding: .5rem; + padding: .3rem; margin-top: 1rem; border-radius: 0.2rem; background-color: #fafafa; @@ -536,7 +538,7 @@ .reactions_selector em { font-style: normal; cursor: pointer; - font-size: 1.5rem; + font-size: 1.2rem; } .context_menu { diff --git a/themes/chatview/psi/bubble/load.js b/themes/chatview/psi/bubble/load.js index 40bec2150..fc6bcb172 100644 --- a/themes/chatview/psi/bubble/load.js +++ b/themes/chatview/psi/bubble/load.js @@ -4,5 +4,5 @@ srvLoader.setMetaData({ authors: ["Sergei Ilinykh "], description: "Bubble style.", url: "https://psi-im.org", - features: ["reactions"] + features: ["reactions", "message-retract"] }); diff --git a/themes/chatview/util.js b/themes/chatview/util.js index 0405cbcf4..0c40e478a 100644 --- a/themes/chatview/util.js +++ b/themes/chatview/util.js @@ -413,6 +413,7 @@ function initPsiTheme() { this.show = function(messageId, nearEl, scrollEl) { this.currentMessage = messageId; const nbr = nearEl.getBoundingClientRect(); + rs.style.left = "0px"; rs.style.top = (nbr.top + scrollEl.scrollTop + document.documentElement.scrollTop) + "px"; rs.style.display = "flex"; const selectorRect = rs.getBoundingClientRect();