branch: elpa/jabber
commit 0487687c4d60376088f0d37d2d28a36f3c0175e9
Author: Thanos Apollo <[email protected]>
Commit: Thanos Apollo <[email protected]>

    chat: Quote the original message in fallback-less replies
---
 CHANGELOG.org             |  1 +
 lisp/jabber-chat.el       | 32 ++++++++++++++++++++++++++++++--
 lisp/jabber-db.el         | 14 ++++++++++++++
 tests/jabber-test-chat.el | 34 ++++++++++++++++++++++++++++++++++
 tests/jabber-test-db.el   | 31 +++++++++++++++++++++++++++++++
 5 files changed, 110 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.org b/CHANGELOG.org
index 6d86808dd8..361907c333 100644
--- a/CHANGELOG.org
+++ b/CHANGELOG.org
@@ -3,6 +3,7 @@
 * [Unreleased]
 
 ** Added
+- Replies without an inline quote now show a snippet of the original message, 
looked up from local history
 - Reply context survives buffer reloads: reply metadata is now stored in the 
message database
 - Outgoing messages carry an XEP-0359 origin-id, so replies from other clients 
reference a stable id
 - OMEMO signed pre-keys now rotate automatically on connect 
(~jabber-omemo-signed-pre-key-rotation-period~, 7 days default)
diff --git a/lisp/jabber-chat.el b/lisp/jabber-chat.el
index e0cd72c429..ee1e454ec1 100644
--- a/lisp/jabber-chat.el
+++ b/lisp/jabber-chat.el
@@ -289,6 +289,8 @@ holding state for the next composed message stay inert."
                            &optional resource stanza-id
                            server-id occupant-id oob-entries
                            encrypted reply))
+(declare-function jabber-db-reply-target-body "jabber-db.el"
+                  (account peer reply-id muc-p))
 (declare-function jabber-db--extract-occupant-id "jabber-db.el" (xml-data))
 (declare-function jabber-message-correct--replace-id "jabber-message-correct"
                   (xml-data))
@@ -1195,10 +1197,36 @@ is in the body the reply context is already visible 
inline."
              (format "reply to %s" who)
            "reply"))))
 
+(defun jabber-chat--first-line-snippet (text limit)
+  "Return the first line of TEXT, truncated to LIMIT characters."
+  (let ((line (car (split-string text "\n"))))
+    (if (> (length line) limit)
+        (concat (substring line 0 limit) "…")
+      line)))
+
+(defun jabber-chat--reply-context-snippet (msg)
+  "Return a snippet of the original body MSG replies to, or nil.
+Looks the referenced message up in the local database."
+  (and-let* ((reply-id (plist-get msg :reply-to-id))
+             (jabber-buffer-connection)
+             (peer (or (bound-and-true-p jabber-group)
+                       jabber-chatting-with))
+             (body (jabber-db-reply-target-body
+                    (jabber-connection-bare-jid jabber-buffer-connection)
+                    (jabber-jid-user peer) reply-id
+                    (and (bound-and-true-p jabber-group) t))))
+    (jabber-chat--first-line-snippet body 80)))
+
 (defun jabber-chat--insert-reply-context (msg)
-  "Insert reply context line for MSG when it needs one."
+  "Insert reply context line for MSG when it needs one.
+When the referenced message is in the local database, quote a
+snippet of it after the label."
   (when-let* ((label (jabber-chat--reply-context-label msg)))
-    (insert (propertize (concat label "\n") 'face 'shadow))))
+    (let ((snippet (jabber-chat--reply-context-snippet msg)))
+      (insert (propertize
+               (if snippet (format "%s: %s\n" label snippet)
+                 (concat label "\n"))
+               'face 'shadow)))))
 
 (defun jabber-chat-pp--local (data)
   "Render a locally sent message from DATA."
diff --git a/lisp/jabber-db.el b/lisp/jabber-db.el
index cb8cb2c9b7..79b71436f7 100644
--- a/lisp/jabber-db.el
+++ b/lisp/jabber-db.el
@@ -805,6 +805,20 @@ FROM message WHERE stanza_id = ? LIMIT 1"
             (if resource (concat peer "/" resource) peer)
           account)))))
 
+(defun jabber-db-reply-target-body (account peer reply-id muc-p)
+  "Return the body of the message REPLY-ID references, or nil.
+In a MUC (MUC-P non-nil) REPLY-ID is the room-assigned stanza-id
+\(XEP-0461), so match on server_id; in 1:1 chat it is the sender's
+origin id, so match on stanza_id.  ACCOUNT and PEER scope the lookup."
+  (when-let* ((db (jabber-db-ensure-open)))
+    (caar (sqlite-select
+           db
+           (format "SELECT body FROM message \
+WHERE account = ? AND peer = ? AND %s = ? AND retracted_by IS NULL \
+LIMIT 1"
+                   (if muc-p "server_id" "stanza_id"))
+           (list account peer reply-id)))))
+
 ;;; Reactions
 
 (defun jabber-db--reaction-id-column (type)
diff --git a/tests/jabber-test-chat.el b/tests/jabber-test-chat.el
index a824448ee7..eb36954285 100644
--- a/tests/jabber-test-chat.el
+++ b/tests/jabber-test-chat.el
@@ -261,6 +261,40 @@
          (plist (jabber-chat--msg-plist-from-stanza stanza)))
     (should-not (plist-get plist :fallback-range))))
 
+(ert-deftest jabber-test-chat-reply-context-synthesizes-quote ()
+  "A fallback-less reply quotes the original body from the database."
+  (with-temp-buffer
+    (setq-local jabber-chatting-with "[email protected]")
+    (setq-local jabber-buffer-connection 'fake-jc)
+    (cl-letf (((symbol-function 'jabber-connection-bare-jid)
+               (lambda (_jc) "[email protected]"))
+              ((symbol-function 'jabber-muc-sender-p)
+               (lambda (_jid) nil))
+              ((symbol-function 'jabber-db-reply-target-body)
+               (lambda (_account _peer reply-id _muc-p)
+                 (and (equal reply-id "orig-1")
+                      "original text\nsecond line"))))
+      (jabber-chat--insert-reply-context
+       '(:reply-to-id "orig-1" :reply-to-jid "[email protected]"))
+      (should (string-match-p "reply to [email protected]: original text"
+                              (buffer-string)))
+      (should-not (string-match-p "second line" (buffer-string))))))
+
+(ert-deftest jabber-test-chat-reply-context-label-without-db-hit ()
+  "A fallback-less reply falls back to the bare label when unresolved."
+  (with-temp-buffer
+    (setq-local jabber-chatting-with "[email protected]")
+    (setq-local jabber-buffer-connection 'fake-jc)
+    (cl-letf (((symbol-function 'jabber-connection-bare-jid)
+               (lambda (_jc) "[email protected]"))
+              ((symbol-function 'jabber-muc-sender-p)
+               (lambda (_jid) nil))
+              ((symbol-function 'jabber-db-reply-target-body)
+               (lambda (&rest _) nil)))
+      (jabber-chat--insert-reply-context
+       '(:reply-to-id "orig-2" :reply-to-jid "[email protected]"))
+      (should (equal "reply to [email protected]\n" (buffer-string))))))
+
 (ert-deftest jabber-test-chat-outgoing-handler-stores-reply-metadata ()
   "The DB outgoing handler reads reply elements off the final stanza."
   (require 'jabber-db)
diff --git a/tests/jabber-test-db.el b/tests/jabber-test-db.el
index a4b191cefe..f5550b9208 100644
--- a/tests/jabber-test-db.el
+++ b/tests/jabber-test-db.el
@@ -1765,6 +1765,37 @@ CREATE TABLE omemo_store (
         (should (equal "orig-4" (plist-get msg :reply-to-id)))
         (should (eq 'all (plist-get msg :fallback-range)))))))
 
+(ert-deftest jabber-test-db-reply-target-body-lookup ()
+  "Reply target lookup matches server_id in MUC, stanza_id in 1:1."
+  (jabber-test-db-with-db
+    (let ((ts (floor (float-time))))
+      (jabber-db-store-message
+       "[email protected]" "[email protected]" "in" "groupchat" "the original" ts
+       "alice" "short-1" "room-uuid-1")
+      (jabber-db-store-message
+       "[email protected]" "[email protected]" "in" "chat" "direct original" ts
+       "phone" "origin-7"))
+    (should (equal "the original"
+                   (jabber-db-reply-target-body
+                    "[email protected]" "[email protected]" "room-uuid-1" t)))
+    (should-not (jabber-db-reply-target-body
+                 "[email protected]" "[email protected]" "short-1" t))
+    (should (equal "direct original"
+                   (jabber-db-reply-target-body
+                    "[email protected]" "[email protected]" "origin-7" nil)))
+    (should-not (jabber-db-reply-target-body
+                 "[email protected]" "[email protected]" "missing" nil))))
+
+(ert-deftest jabber-test-db-reply-target-body-skips-retracted ()
+  "A retracted reply target is not quoted."
+  (jabber-test-db-with-db
+    (jabber-db-store-message
+     "[email protected]" "[email protected]" "in" "chat" "soon gone"
+     (floor (float-time)) "phone" "orig-8" "srv-8")
+    (jabber-db-retract-message "srv-8" "[email protected]/phone" nil)
+    (should-not (jabber-db-reply-target-body
+                 "[email protected]" "[email protected]" "orig-8" nil))))
+
 (ert-deftest jabber-test-db-extract-reply-fields ()
   "Reply extraction matches the chat-side parser's shape."
   (let ((stanza '(message ((from . "[email protected]") (type . "chat"))

Reply via email to