From af5dd1ceb407c445bfa6f27ec737f989329bbdc4 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Sat, 12 Oct 2024 17:44:30 -0700 Subject: [PATCH 3/3] [5.7] Use erc-match-type API for erc-desktop-notifications * etc/ERC-NEWS: New section for 5.7 and new entries for the `erc-match-type' API and `erc-notifications-focused-context' option. * lisp/erc/erc-desktop-notifications.el (erc-notifications-focused-contexts): New option. (erc-notifications-notify): Address ancient comment regarding PRIVP parameter possibly being unneeded when the current target matches the nick. (erc-notifications-PRIVMSG): Deprecate. (erc-notifications-notify-on-match): Account for new option. (erc-notifications-mode) (erc-notifications-enable, erc-notifications-disable): Instead of the "PRIVMSG" response-handler hook, use the `erc-match-type' API. (erc-desktop-notifications--setup): New function (erc-desktop-notifications-match-query-commands): New variable. (erc-desktop-notifications--match-type-query): New struct type. (erc-desktop-notifications--query-p): New function. (erc-desktop-notification--query-notify): New function. * test/lisp/erc/erc-desktop-notifications-tests.el: New file. --- etc/ERC-NEWS | 22 ++++ lisp/erc/erc-desktop-notifications.el | 69 +++++++++-- .../erc/erc-desktop-notifications-tests.el | 115 ++++++++++++++++++ 3 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 test/lisp/erc/erc-desktop-notifications-tests.el diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index 3970f67d725..4b85b652cb7 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -11,6 +11,28 @@ This file is about changes in ERC, the powerful, modular, and extensible IRC (Internet Relay Chat) client distributed with GNU Emacs since Emacs version 22.1. + +* Changes in ERC 5.7 + +** An extensibility focused 'match' API. +Users have often expressed frustration over ERC's lack of a simple API +for matching, highlighting, and filtering based on a message's content +and metadata, like the sender or associated IRC command. While it's +true that discussions have been ongoing for a more powerful message +formatting and construction API that will hopefully one day offer access +to the various parts of a message before they're assembled, users will +be needing something practical and effective in the interim. Enter the +'erc-match-type' API, which is based on a simple hook-like handler +system. You subscribe by enrolling a function that takes a special +'erc-match-type' object with useful fields to help with matching, +filtering, and applying faces. See Info node 'Match API' to find out +more. + +** Opt out of desktop notifications from the active buffer. +Option 'erc-notifications-focused-contexts' can help spare you from +seeing desktop alerts for messages you're reading or those inserted +while you're typing. + * Changes in ERC 5.6.1 diff --git a/lisp/erc/erc-desktop-notifications.el b/lisp/erc/erc-desktop-notifications.el index 9bb89fbfc81..2d605ced5f5 100644 --- a/lisp/erc/erc-desktop-notifications.el +++ b/lisp/erc/erc-desktop-notifications.el @@ -47,6 +47,11 @@ erc-notifications-icon "Icon to use for notification." :type '(choice (const :tag "No icon" nil) file)) +(defcustom erc-notifications-focused-contexts '(query mention) + "Where to notify even if a match appears in the selected window." + :package-version '(ERC . "5.7") ; FIXME sync on release + :type '(set (const query) (const mention))) + (defcustom erc-notifications-bus :session "D-Bus bus to use for notification." :version "25.1" @@ -60,12 +65,15 @@ dbus-debug (defun erc-notifications-notify (nick msg &optional privp) "Notify that NICK send some MSG, where PRIVP should be non-nil for PRIVMSGs. This will replace the last notification sent with this function." - ;; TODO: can we do this without PRIVP? (by "fixing" ERC's not - ;; setting the current buffer to the existing query buffer) (dbus-ignore-errors (setq erc-notifications-last-notification - (let* ((channel (if privp (erc-get-buffer nick) (current-buffer))) - (title (format "%s in %s" (xml-escape-string nick t) channel)) + (let* ((channel (or (and privp (not (equal nick (erc-target))) + (erc-get-buffer nick)) + (current-buffer))) + (title (if (or privp (equal nick (erc-target))) + (xml-escape-string nick t) + (format "%s in %s" + (xml-escape-string nick t) channel))) (body (xml-escape-string (erc-controls-strip msg) t))) (funcall (cond ((featurep 'android) #'android-notifications-notify) @@ -82,6 +90,7 @@ erc-notifications-notify (pop-to-buffer channel))))))) (defun erc-notifications-PRIVMSG (_proc parsed) + (declare (obsolete "switched to `erc-match-type' API" "31.1")) (let ((nick (car (erc-parse-user (erc-response.sender parsed)))) (target (car (erc-response.command-args parsed))) (msg (erc-response.contents parsed))) @@ -97,20 +106,64 @@ erc-notifications-notify-on-match (when (eq match-type 'current-nick) (let ((nick (nth 0 (erc-parse-user nickuserhost)))) (unless (or (string-match-p "^Server:" nick) - (when (boundp 'erc-track-exclude) - (member nick erc-track-exclude))) + (and (eq (current-buffer) (window-buffer)) + (frame-focus-state) ; t or unknown + (not (memq 'mention + erc-notifications-focused-contexts))) + (and (boundp 'erc-track-exclude) + (member nick erc-track-exclude))) (erc-notifications-notify nick msg))))) ;;;###autoload(autoload 'erc-notifications-mode "erc-desktop-notifications" "" t) (define-erc-module notifications nil "Send notifications on private message reception and mentions." ;; Enable - ((add-hook 'erc-server-PRIVMSG-functions #'erc-notifications-PRIVMSG) + ((unless erc--updating-modules-p + (erc-buffer-do #'erc-desktop-notifications--setup)) + (add-hook 'erc-mode-hook #'erc-desktop-notifications--setup) (add-hook 'erc-text-matched-hook #'erc-notifications-notify-on-match)) ;; Disable - ((remove-hook 'erc-server-PRIVMSG-functions #'erc-notifications-PRIVMSG) + ((erc-buffer-do #'erc-desktop-notifications--setup) (remove-hook 'erc-text-matched-hook #'erc-notifications-notify-on-match))) +(defun erc-desktop-notifications--setup () + (if erc-notifications-mode + (add-hook 'erc-match-functions + #'erc-desktop-notifications--match-type-query 0 t) + (remove-hook 'erc-match-functions + #'erc-desktop-notifications--match-type-query t))) + +(defvar erc-desktop-notifications-match-query-commands '(PRIVMSG) + "IRC commands considered in query buffers for notification. +Omits \"NOTICE\"s by default because they're typically reserved for bots +and services that you interact with directly.") + +(cl-defstruct (erc-desktop-notifications--match-type-query + (:constructor erc-desktop-notifications--match-type-query) + (:include erc-match-user + (category nil) + (data erc-desktop-notifications-match-query-commands) + (predicate #'erc-desktop-notifications--query-p) + (handler #'erc-desktop-notifications--query-notify))) + "Notification match type for queries.") + +(defun erc-desktop-notifications--query-p (match) + "Return non-nil if MATCH object describes a \"PRIVMSG\" query." + (and (erc-query-buffer-p) + (or (memq 'query erc-notifications-focused-contexts) + (null (frame-focus-state)) + (not (eq (current-buffer) (window-buffer)))) + (memq (erc-match-command match) (erc-match-user-data match)) + (always (cl-assert (erc-match-nick match))) + (not (and (boundp 'erc-track-exclude) + (member (erc-target) erc-track-exclude))))) + +(defun erc-desktop-notifications--query-notify (match) + ;; No need to pass argument PRIVP because current buffer is correct. + (erc-notifications-notify (erc-target) + (erc-match-get-message-body match))) + + (provide 'erc-desktop-notifications) ;;; erc-desktop-notifications.el ends here diff --git a/test/lisp/erc/erc-desktop-notifications-tests.el b/test/lisp/erc/erc-desktop-notifications-tests.el new file mode 100644 index 00000000000..5a9ad0ff5ba --- /dev/null +++ b/test/lisp/erc/erc-desktop-notifications-tests.el @@ -0,0 +1,115 @@ +;;; erc-desktop-notifications-tests.el --- Notifications tests -*- lexical-binding:t -*- + +;; Copyright (C) 2024 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published +;; by the Free Software Foundation, either version 3 of the License, +;; or (at your option) any later version. +;; +;; GNU Emacs is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: +;;; Code: +(require 'erc-desktop-notifications) + +(require 'ert-x) +(eval-and-compile + (let ((load-path (cons (ert-resource-directory) load-path))) + (require 'erc-tests-common))) + +(defun erc-desktop-notifications-tests--perform (test) + (erc-tests-common-make-server-buf) + (erc-notifications-mode +1) + (setq erc-server-current-nick "tester") + + (cl-letf* ((calls nil) + ((frame-parameter nil 'last-focus-update) + t) + ((symbol-function 'erc-notifications-notify) + (lambda (&rest r) (push r calls)))) + (with-current-buffer (erc--open-target "#chan") + (funcall test (lambda () (prog1 calls (setq calls nil)))))) + + (when noninteractive + (erc-notifications-mode -1) + (erc-tests-common-kill-buffers))) + +(defun erc-desktop-notifications-tests--populate-chan (test) + (erc-desktop-notifications-tests--perform + (lambda (check) + (erc-tests-common-add-cmem "bob") + (erc-tests-common-add-cmem "alice") + + (erc-tests-common-simulate-line + ":irc.foonet.org 353 tester = #chan :alice bob tester") + (erc-tests-common-simulate-line + ":irc.foonet.org 366 tester #chan :End of NAMES list") + (erc-tests-common-simulate-privmsg "bob" "hi tester") + + (should (equal (current-buffer) (get-buffer "#chan"))) + (should (not (eq (current-buffer) (window-buffer)))) ; *ert* or *scratch* + (funcall test check)))) + +(ert-deftest erc-notifications-focused-contexts/default () + (should (equal erc-notifications-focused-contexts '(query mention))) + + (erc-desktop-notifications-tests--populate-chan + (lambda (check) + + ;; A private query triggers a notification. + (erc-tests-common-simulate-line ":bob!~bob@fsf.org PRIVMSG tester yo") + (should (eq (current-buffer) (get-buffer "bob"))) + + ;; A NOTICE command doesn't trigger a notification because it's + ;; absent from `erc-desktop-notifications-match-query-commands'. + (erc-tests-common-simulate-line ":irc.foonet.org NOTICE tester nope") + + (should (equal (funcall check) + '(("bob" "yo") + ("bob" "hi tester\n")))) + + ;; Setting the window to the buffer where insertions are happening + ;; makes no difference: notifications are still sent. + (erc-tests-common-simulate-line ":bob!~bob@fsf.org PRIVMSG tester ho") + + (set-window-buffer nil (set-buffer "#chan")) + (erc-tests-common-simulate-privmsg "alice" "hi tester") + + (should (equal (funcall check) + '(("alice" "hi tester\n") + ("bob" "ho"))))))) + +(ert-deftest erc-notifications-focused-contexts/unselected () + (should (equal erc-notifications-focused-contexts '(query mention))) + + (let ((erc-notifications-focused-contexts)) + + (erc-desktop-notifications-tests--populate-chan + (lambda (check) + (should (equal (funcall check) '(("bob" "hi tester\n")))) + + ;; Buffer #chan is current and displayed in the selected window, + ;; so no notification is sent. + (set-window-buffer nil "#chan") ; #chan + (erc-tests-common-simulate-privmsg "alice" "hi tester") + + ;; A new query comes in for a buffer that doesn't exist. The + ;; option `erc-receive-query-display' tells ERC to switch to that + ;; buffer and show it before insertion. Therefore, no + ;; notification is sent. + (let ((erc-receive-query-display 'buffer)) + (erc-tests-common-simulate-line + ":bob!~bob@fsf.org PRIVMSG tester yo")) + + (should-not (funcall check)))))) + +;;; erc-desktop-notifications-tests.el ends here -- 2.46.2