summaryrefslogtreecommitdiff
path: root/jabber-roster.el
diff options
context:
space:
mode:
Diffstat (limited to 'jabber-roster.el')
-rw-r--r--jabber-roster.el893
1 files changed, 893 insertions, 0 deletions
diff --git a/jabber-roster.el b/jabber-roster.el
new file mode 100644
index 0000000..b62b182
--- /dev/null
+++ b/jabber-roster.el
@@ -0,0 +1,893 @@
+;; jabber-roster.el - displaying the roster -*- coding: utf-8; -*-
+
+;; Copyright (C) 2009 - Kirill A. Korinskiy - catap@catap.ru
+;; Copyright (C) 2003, 2004, 2007, 2008 - Magnus Henoch - mange@freemail.hu
+;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net
+
+;; This file is a part of jabber.el.
+
+;; This program 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 2 of the License, or
+;; (at your option) any later version.
+
+;; This program 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 this program; if not, write to the Free Software
+;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+(require 'jabber-presence)
+(require 'jabber-util)
+(require 'jabber-alert)
+(require 'jabber-keymap)
+(require 'format-spec)
+(require 'cl) ;for `find'
+(require 'jabber-private)
+
+(defgroup jabber-roster nil "roster display options"
+ :group 'jabber)
+
+(defcustom jabber-roster-line-format " %a %c %-25n %u %-8s %S"
+ "The format specification of the lines in the roster display.
+
+These fields are available:
+
+%a Avatar, if any
+%c \"*\" if the contact is connected, or \" \" if not
+%u sUbscription state - see below
+%n Nickname of contact, or JID if no nickname
+%j Bare JID of contact (without resource)
+%r Highest-priority resource of contact
+%s Availability of contact as string (\"Online\", \"Away\" etc)
+%S Status string specified by contact
+
+%u is replaced by one of the strings given by
+`jabber-roster-subscription-display'."
+ :type 'string
+ :group 'jabber-roster)
+
+(defcustom jabber-roster-subscription-display '(("none" . " ")
+ ("from" . "< ")
+ ("to" . " >")
+ ("both" . "<->"))
+ "Strings used for indicating subscription status of contacts.
+\"none\" means that there is no subscription between you and the
+contact.
+\"from\" means that the contact has a subscription to you, but you
+have no subscription to the contact.
+\"to\" means that you have a subscription to the contact, but the
+contact has no subscription to you.
+\"both\" means a mutual subscription.
+
+Having a \"presence subscription\" means being able to see the
+other person's presence.
+
+Some fancy arrows you might want to use, if your system can
+display them: ← → ⇄ ↔"
+ :type '(list (cons :format "%v" (const :format "" "none") (string :tag "None"))
+ (cons :format "%v" (const :format "" "from") (string :tag "From"))
+ (cons :format "%v" (const :format "" "to") (string :tag "To"))
+ (cons :format "%v" (const :format "" "both") (string :tag "Both")))
+ :group 'jabber-roster)
+
+(defcustom jabber-resource-line-format " %r - %s (%S), priority %p"
+ "The format specification of resource lines in the roster display.
+These are displayed when `jabber-show-resources' permits it.
+
+These fields are available:
+
+%c \"*\" if the contact is connected, or \" \" if not
+%n Nickname of contact, or JID if no nickname
+%j Bare JID of contact (without resource)
+%p Priority of this resource
+%r Name of this resource
+%s Availability of resource as string (\"Online\", \"Away\" etc)
+%S Status string specified by resource"
+ :type 'string
+ :group 'jabber-roster)
+
+(defcustom jabber-roster-sort-functions
+ '(jabber-roster-sort-by-status jabber-roster-sort-by-displayname)
+ "Sort roster according to these criteria.
+
+These functions should take two roster items A and B, and return:
+<0 if A < B
+0 if A = B
+>0 if A > B"
+ :type 'hook
+ :options '(jabber-roster-sort-by-status
+ jabber-roster-sort-by-displayname
+ jabber-roster-sort-by-group)
+ :group 'jabber-roster)
+
+(defcustom jabber-sort-order '("chat" "" "away" "dnd" "xa")
+ "Sort by status in this order. Anything not in list goes last.
+Offline is represented as nil."
+ :type '(repeat (restricted-sexp :match-alternatives (stringp nil)))
+ :group 'jabber-roster)
+
+(defcustom jabber-show-resources 'sometimes
+ "Show contacts' resources in roster?
+This can be one of the following symbols:
+
+nil Never show resources
+sometimes Show resources when there are more than one
+always Always show resources"
+ :type '(radio (const :tag "Never" nil)
+ (const :tag "When more than one connected resource" sometimes)
+ (const :tag "Always" always))
+ :group 'jabber-roster)
+
+(defcustom jabber-show-offline-contacts t
+ "Show offline contacts in roster when non-nil"
+ :type 'boolean
+ :group 'jabber-roster)
+
+(defcustom jabber-remove-newlines t
+ "Remove newlines in status messages?
+Newlines in status messages mess up the roster display. However,
+they are essential to status message poets. Therefore, you get to
+choose the behaviour.
+
+Trailing newlines are always removed, regardless of this variable."
+ :type 'boolean
+ :group 'jabber-roster)
+
+(defcustom jabber-roster-show-bindings t
+ "Show keybindings in roster buffer?"
+ :type 'boolean
+ :group 'jabber-roster)
+
+(defcustom jabber-roster-show-title t
+ "Show title in roster buffer?"
+ :type 'boolean
+ :group 'jabber-roster)
+
+(defcustom jabber-roster-mode-hook nil
+ "Hook run when entering Roster mode."
+ :group 'jabber-roster
+ :type 'hook)
+
+(defcustom jabber-roster-default-group-name "other"
+ "Default group name for buddies without groups."
+ :group 'jabber-roster
+ :type 'string
+ :get '(lambda (var)
+ (let ((val (symbol-value var)))
+ (when (stringp val)
+ (set-text-properties 0 (length val) nil val))
+ val))
+ :set '(lambda (var val)
+ (when (stringp val)
+ (set-text-properties 0 (length val) nil val))
+ (custom-set-default var val))
+ )
+
+(defcustom jabber-roster-show-empty-group nil
+ "Show empty groups in roster?"
+ :group 'jabber-roster
+ :type 'boolean)
+
+(defcustom jabber-roster-roll-up-group nil
+ "Show empty groups in roster?"
+ :group 'jabber-roster
+ :type 'boolean)
+
+(defface jabber-roster-user-online
+ '((t (:foreground "blue" :weight bold :slant normal)))
+ "face for displaying online users"
+ :group 'jabber-roster)
+
+(defface jabber-roster-user-xa
+ '((((background dark)) (:foreground "magenta" :weight normal :slant italic))
+ (t (:foreground "black" :weight normal :slant italic)))
+ "face for displaying extended away users"
+ :group 'jabber-roster)
+
+(defface jabber-roster-user-dnd
+ '((t (:foreground "red" :weight normal :slant italic)))
+ "face for displaying do not disturb users"
+ :group 'jabber-roster)
+
+(defface jabber-roster-user-away
+ '((t (:foreground "dark green" :weight normal :slant italic)))
+ "face for displaying away users"
+ :group 'jabber-roster)
+
+(defface jabber-roster-user-chatty
+ '((t (:foreground "dark orange" :weight bold :slant normal)))
+ "face for displaying chatty users"
+ :group 'jabber-roster)
+
+(defface jabber-roster-user-error
+ '((t (:foreground "red" :weight light :slant italic)))
+ "face for displaying users sending presence errors"
+ :group 'jabber-roster)
+
+(defface jabber-roster-user-offline
+ '((t (:foreground "dark grey" :weight light :slant italic)))
+ "face for displaying offline users"
+ :group 'jabber-roster)
+
+(defvar jabber-roster-debug nil
+ "debug roster draw")
+
+(defvar jabber-roster-mode-map
+ (let ((map (make-sparse-keymap)))
+ (suppress-keymap map)
+ (set-keymap-parent map jabber-common-keymap)
+ (define-key map [mouse-2] 'jabber-roster-mouse-2-action-at-point)
+ (define-key map (kbd "TAB") 'jabber-go-to-next-roster-item)
+ (define-key map (kbd "S-TAB") 'jabber-go-to-previous-roster-item)
+ (define-key map (kbd "M-TAB") 'jabber-go-to-previous-roster-item)
+ (define-key map (kbd "<backtab>") 'jabber-go-to-previous-roster-item)
+ (define-key map (kbd "RET") 'jabber-roster-ret-action-at-point)
+ (define-key map (kbd "C-k") 'jabber-roster-delete-at-point)
+
+ (define-key map "e" 'jabber-roster-edit-action-at-point)
+ (define-key map "s" 'jabber-send-subscription-request)
+ (define-key map "q" 'bury-buffer)
+ (define-key map "i" 'jabber-get-disco-items)
+ (define-key map "j" 'jabber-muc-join)
+ (define-key map "I" 'jabber-get-disco-info)
+ (define-key map "b" 'jabber-get-browse)
+ (define-key map "v" 'jabber-get-version)
+ (define-key map "a" 'jabber-send-presence)
+ (define-key map "g" 'jabber-display-roster)
+ (define-key map "S" 'jabber-ft-send)
+ (define-key map "o" 'jabber-roster-toggle-offline-display)
+ (define-key map "H" 'jabber-roster-toggle-binding-display)
+ ;;(define-key map "D" 'jabber-disconnect)
+ map))
+
+(defun jabber-roster-ret-action-at-point ()
+ "Action for ret. Before try to roll up/down group. Eval
+chat-with-jid-at-point is no group at point"
+ (interactive)
+ (let ((group-at-point (get-text-property (point)
+ 'jabber-group))
+ (account-at-point (get-text-property (point)
+ 'jabber-account))
+ (jid-at-point (get-text-property (point)
+ 'jabber-jid)))
+ (if (and group-at-point account-at-point)
+ (jabber-roster-roll-group account-at-point group-at-point)
+ ;; Is this a normal contact, or a groupchat? Let's ask it.
+ (jabber-disco-get-info
+ account-at-point (jabber-jid-user jid-at-point) nil
+ #'jabber-roster-ret-action-at-point-1
+ jid-at-point))))
+
+(defun jabber-roster-ret-action-at-point-1 (jc jid result)
+ ;; If we get an error, assume it's a normal contact.
+ (if (eq (car result) 'error)
+ (jabber-chat-with jc jid)
+ ;; Otherwise, let's check whether it has a groupchat identity.
+ (let ((identities (car result)))
+ (if (find "conference" (if (sequencep identities) identities nil)
+ :key (lambda (i) (aref i 1))
+ :test #'string=)
+ ;; Yes! Let's join it.
+ (jabber-muc-join jc jid
+ (jabber-muc-read-my-nickname jc jid t)
+ t)
+ ;; No. Let's open a normal chat buffer.
+ (jabber-chat-with jc jid)))))
+
+(defun jabber-roster-mouse-2-action-at-point (e)
+ "Action for mouse-2. Before try to roll up/down group. Eval
+chat-with-jid-at-point is no group at point"
+ (interactive "e")
+ (mouse-set-point e)
+ (let ((group-at-point (get-text-property (point)
+ 'jabber-group))
+ (account-at-point (get-text-property (point)
+ 'jabber-account)))
+ (if (and group-at-point account-at-point)
+ (jabber-roster-roll-group account-at-point group-at-point)
+ (jabber-popup-combined-menu))))
+
+(defun jabber-roster-delete-at-point ()
+ "Delete at point from roster.
+Try to delete the group from all contaacs.
+Delete a jid if there is no group at point."
+ (interactive)
+ (let ((group-at-point (get-text-property (point)
+ 'jabber-group))
+ (account-at-point (get-text-property (point)
+ 'jabber-account)))
+ (if (and group-at-point account-at-point)
+ (let ((jids-with-group
+ (gethash group-at-point
+ (plist-get
+ (fsm-get-state-data account-at-point)
+ :roster-hash))))
+ (jabber-roster-delete-group-from-jids account-at-point
+ jids-with-group
+ group-at-point))
+ (jabber-roster-delete-jid-at-point))))
+
+(defun jabber-roster-edit-action-at-point ()
+ "Action for e. Before try to edit group name.
+Eval `jabber-roster-change' is no group at point"
+ (interactive)
+ (let ((group-at-point (get-text-property (point)
+ 'jabber-group))
+ (account-at-point (get-text-property (point)
+ 'jabber-account)))
+ (if (and group-at-point account-at-point)
+ (let ((jids-with-group
+ (gethash group-at-point
+ (plist-get
+ (fsm-get-state-data account-at-point)
+ :roster-hash))))
+ (jabber-roster-edit-group-from-jids account-at-point
+ jids-with-group
+ group-at-point))
+ (call-interactively 'jabber-roster-change))))
+
+(defun jabber-roster-roll-group (jc group-name &optional set)
+ "Roll up/down group in roster.
+If optional SET is t, roll up group.
+If SET is nor t or nil, roll down group."
+ (let* ((state-data (fsm-get-state-data jc))
+ (roll-groups (plist-get state-data :roster-roll-groups))
+ (new-roll-groups (if (find group-name roll-groups :test 'string=)
+ ;; group is rolled up, roll it down if needed
+ (if (or (not set) (and set (not (eq set t))))
+ (remove-if-not (lambda (group-name-in-list)
+ (not (string= group-name
+ group-name-in-list)))
+ roll-groups)
+ roll-groups)
+ ;; group is rolled down, roll it up if needed
+ (if (or (not set) (and set (eq set t)))
+ (append roll-groups (list group-name))
+ roll-groups))) )
+ (unless (equal roll-groups new-roll-groups)
+ (plist-put
+ state-data :roster-roll-groups
+ new-roll-groups)
+ (jabber-display-roster))))
+
+(defun jabber-roster-mode ()
+ "Major mode for Jabber roster display.
+Use the keybindings (mnemonic as Chat, Roster, Info, MUC, Service) to
+bring up menus of actions.
+\\{jabber-roster-mode-map}"
+ (kill-all-local-variables)
+ (setq major-mode 'jabber-roster-mode
+ mode-name "jabber-roster")
+ (use-local-map jabber-roster-mode-map)
+ (setq buffer-read-only t)
+ (if (fboundp 'run-mode-hooks)
+ (run-mode-hooks 'jabber-roster-mode-hook)
+ (run-hooks 'jabber-roster-mode-hook)))
+
+(put 'jabber-roster-mode 'mode-class 'special)
+
+;;;###autoload
+(defun jabber-switch-to-roster-buffer (&optional jc)
+ "Switch to roster buffer.
+Optional JC argument is ignored; it's there so this function can
+be used in `jabber-post-connection-hooks'."
+ (interactive)
+ (if (not (get-buffer jabber-roster-buffer))
+ (jabber-display-roster)
+ (switch-to-buffer jabber-roster-buffer)))
+
+(defun jabber-sort-roster (jc)
+ "sort roster according to online status"
+ (let ((state-data (fsm-get-state-data jc)))
+ (dolist (group (plist-get state-data :roster-groups))
+ (let ((group-name (car group)))
+ (puthash group-name
+ (sort
+ (gethash group-name
+ (plist-get state-data :roster-hash))
+ #'jabber-roster-sort-items)
+ (plist-get state-data :roster-hash))))))
+
+(defun jabber-roster-prepare-roster (jc)
+ "make a hash based roster"
+ (let* ((state-data (fsm-get-state-data jc))
+ (hash (make-hash-table :test 'equal))
+ (buddies (plist-get state-data :roster))
+ (all-groups '()))
+ (dolist (buddy buddies)
+ (let ((groups (get buddy 'groups)))
+ (if groups
+ (progn
+ (dolist (group groups)
+ (progn
+ (setq all-groups (append all-groups (list group)))
+ (puthash group
+ (append (gethash group hash)
+ (list buddy))
+ hash))))
+ (progn
+ (setq all-groups (append all-groups
+ (list jabber-roster-default-group-name)))
+ (puthash jabber-roster-default-group-name
+ (append (gethash jabber-roster-default-group-name hash)
+ (list buddy))
+ hash)))))
+
+ ;; remove duplicates name of group
+ (setq all-groups (sort
+ (remove-duplicates all-groups
+ :test 'string=)
+ 'string<))
+
+ ;; put to state-data all-groups as list of list
+ (plist-put state-data :roster-groups
+ (mapcar #'list all-groups))
+
+ ;; put to state-data hash-roster
+ (plist-put state-data :roster-hash
+ hash)))
+
+(defun jabber-roster-sort-items (a b)
+ "Sort roster items A and B according to `jabber-roster-sort-functions'.
+Return t if A is less than B."
+ (dolist (fn jabber-roster-sort-functions)
+ (let ((comparison (funcall fn a b)))
+ (cond
+ ((< comparison 0)
+ (return t))
+ ((> comparison 0)
+ (return nil))))))
+
+(defun jabber-roster-sort-by-status (a b)
+ "Sort roster items by online status.
+See `jabber-sort-order' for order used."
+ (flet ((order (item) (length (member (get item 'show) jabber-sort-order))))
+ (let ((a-order (order a))
+ (b-order (order b)))
+ ;; Note reversed test. Items with longer X-order go first.
+ (cond
+ ((< a-order b-order)
+ 1)
+ ((> a-order b-order)
+ -1)
+ (t
+ 0)))))
+
+(defun jabber-roster-sort-by-displayname (a b)
+ "Sort roster items by displayed name."
+ (let ((a-name (jabber-jid-displayname a))
+ (b-name (jabber-jid-displayname b)))
+ (cond
+ ((string-lessp a-name b-name) -1)
+ ((string= a-name b-name) 0)
+ (t 1))))
+
+(defun jabber-roster-sort-by-group (a b)
+ "Sort roster items by group membership."
+ (flet ((first-group (item) (or (car (get item 'groups)) "")))
+ (let ((a-group (first-group a))
+ (b-group (first-group b)))
+ (cond
+ ((string-lessp a-group b-group) -1)
+ ((string= a-group b-group) 0)
+ (t 1)))))
+
+(defun jabber-fix-status (status)
+ "Make status strings more readable"
+ (when status
+ (when (string-match "\n+$" status)
+ (setq status (replace-match "" t t status)))
+ (when jabber-remove-newlines
+ (while (string-match "\n" status)
+ (setq status (replace-match " " t t status))))
+ status))
+
+(defvar jabber-roster-ewoc nil
+ "Ewoc displaying the roster.
+There is only one; we don't rely on buffer-local variables or
+such.")
+
+(defun jabber-roster-filter-display (buddies)
+ "Filter BUDDIES for items to be displayed in the roster"
+ (remove-if-not (lambda (buddy) (or jabber-show-offline-contacts
+ (get buddy 'connected)))
+ buddies))
+
+(defun jabber-roster-toggle-offline-display ()
+ "Toggle display of offline contacts.
+To change this permanently, customize the `jabber-show-offline-contacts'."
+ (interactive)
+ (setq jabber-show-offline-contacts
+ (not jabber-show-offline-contacts))
+ (jabber-display-roster))
+
+(defun jabber-roster-toggle-binding-display ()
+ "Toggle display of the roster binding text."
+ (interactive)
+ (setq jabber-roster-show-bindings
+ (not jabber-roster-show-bindings))
+ (jabber-display-roster))
+
+(defun jabber-display-roster ()
+ "switch to the main jabber buffer and refresh the roster display to reflect the current information"
+ (interactive)
+ (with-current-buffer (get-buffer-create jabber-roster-buffer)
+ (if (not (eq major-mode 'jabber-roster-mode))
+ (jabber-roster-mode))
+ (setq buffer-read-only nil)
+ ;; line-number-at-pos is in Emacs >= 21.4. Only used to avoid
+ ;; excessive scrolling when updating roster, so not absolutely
+ ;; necessary.
+ (let ((current-line (and (fboundp 'line-number-at-pos) (line-number-at-pos)))
+ (current-column (current-column)))
+ (erase-buffer)
+ (setq jabber-roster-ewoc nil)
+ (when jabber-roster-show-title
+ (insert (jabber-propertize "Jabber roster" 'face 'jabber-title-large) "\n"))
+ (when jabber-roster-show-bindings
+ (insert "RET Open chat buffer C-k Delete roster item
+e Edit item s Send subscription request
+q Bury buffer i Get disco items
+I Get disco info b Browse
+j Join groupchat (MUC) v Get client version
+a Send presence o Show offline contacts on/off
+C-c C-c Chat menu C-c C-m Multi-User Chat menu
+C-c C-i Info menu C-c C-r Roster menu
+C-c C-s Service menu
+
+H Toggle displaying this text
+"))
+ (insert "__________________________________\n\n")
+ (if (null jabber-connections)
+ (insert "Not connected\n")
+ (let ((map (make-sparse-keymap)))
+ (define-key map [mouse-2] #'jabber-send-presence)
+ (insert (jabber-propertize (concat (format " - %s"
+ (cdr (assoc *jabber-current-show* jabber-presence-strings)))
+ (if (not (zerop (length *jabber-current-status*)))
+ (format " (%s)"
+ (jabber-fix-status *jabber-current-status*)))
+ " -")
+ 'face (or (cdr (assoc *jabber-current-show* jabber-presence-faces))
+ 'jabber-roster-user-online)
+ ;;'mouse-face (cons 'background-color "light grey")
+ 'keymap map)
+ "\n")))
+
+ (dolist (jc jabber-connections)
+ ;; use a hash-based roster
+ (when (not (plist-get (fsm-get-state-data jc) :roster-hash))
+ (jabber-roster-prepare-roster jc))
+ ;; We sort everything before putting it in the ewoc
+ (jabber-sort-roster jc)
+ (let ((before-ewoc (point))
+ (ewoc (ewoc-create
+ (lexical-let ((jc jc))
+ (lambda (data)
+ (let* ((group (car data))
+ (group-name (car group))
+ (buddy (car (cdr data))))
+ (jabber-display-roster-entry jc group-name buddy))))
+ (concat
+ (jabber-propertize (concat
+ (plist-get (fsm-get-state-data jc) :username)
+ "@"
+ (plist-get (fsm-get-state-data jc) :server))
+ 'face 'jabber-title-medium)
+ "\n__________________________________\n")
+ "__________________________________"))
+ (new-groups '()))
+ (plist-put(fsm-get-state-data jc) :roster-ewoc ewoc)
+ (dolist (group (plist-get (fsm-get-state-data jc) :roster-groups))
+ (let* ((group-name (car group))
+ (buddies (jabber-roster-filter-display
+ (gethash group-name
+ (plist-get (fsm-get-state-data jc) :roster-hash)))))
+ (when (or jabber-roster-show-empty-group
+ (> (length buddies) 0))
+ (let ((group-node (ewoc-enter-last ewoc (list group nil))))
+ (if (not (find
+ group-name
+ (plist-get (fsm-get-state-data jc) :roster-roll-groups)
+ :test 'string=))
+ (dolist (buddy (reverse buddies))
+ (ewoc-enter-after ewoc group-node (list group buddy))))))))
+ (goto-char (point-max))
+ (insert "\n")
+ (put-text-property before-ewoc (point)
+ 'jabber-account jc)))
+
+ (goto-char (point-min))
+ (setq buffer-read-only t)
+ (if (interactive-p)
+ (dolist (hook '(jabber-info-message-hooks jabber-alert-info-message-hooks))
+ (run-hook-with-args hook 'roster (current-buffer) (funcall jabber-alert-info-message-function 'roster (current-buffer)))))
+ (when current-line
+ ;; Go back to previous line - don't use goto-line, since it
+ ;; sets the mark.
+ (goto-char (point-min))
+ (forward-line (1- current-line))
+ ;; ...and go back to previous column
+ (move-to-column current-column)))))
+
+(defun jabber-display-roster-entry (jc group-name buddy)
+ "Format and insert a roster entry for BUDDY at point.
+BUDDY is a JID symbol."
+ (if buddy
+ (let ((buddy-str (format-spec
+ jabber-roster-line-format
+ (list
+ (cons ?a (jabber-propertize
+ " "
+ 'display (get buddy 'avatar)))
+ (cons ?c (if (get buddy 'connected) "*" " "))
+ (cons ?u (cdr (assoc
+ (or
+ (get buddy 'subscription) "none")
+ jabber-roster-subscription-display)))
+ (cons ?n (if (> (length (get buddy 'name)) 0)
+ (get buddy 'name)
+ (symbol-name buddy)))
+ (cons ?j (symbol-name buddy))
+ (cons ?r (or (get buddy 'resource) ""))
+ (cons ?s (or
+ (cdr (assoc (get buddy 'show)
+ jabber-presence-strings))
+ (get buddy 'show)))
+ (cons ?S (if (get buddy 'status)
+ (jabber-fix-status (get buddy 'status))
+ ""))
+ ))))
+ (add-text-properties 0
+ (length buddy-str)
+ (list
+ 'face
+ (or (cdr (assoc (get buddy 'show) jabber-presence-faces))
+ 'jabber-roster-user-online)
+ ;;'mouse-face
+ ;;(cons 'background-color "light grey")
+ 'help-echo
+ (symbol-name buddy)
+ 'jabber-jid
+ (symbol-name buddy)
+ 'jabber-account
+ jc)
+ buddy-str)
+ (insert buddy-str)
+
+ (when (or (eq jabber-show-resources 'always)
+ (and (eq jabber-show-resources 'sometimes)
+ (> (jabber-count-connected-resources buddy) 1)))
+ (dolist (resource (get buddy 'resources))
+ (when (plist-get (cdr resource) 'connected)
+ (let ((resource-str (format-spec jabber-resource-line-format
+ (list
+ (cons ?c "*")
+ (cons ?n (if (>
+ (length
+ (get buddy 'name)) 0)
+ (get buddy 'name)
+ (symbol-name buddy)))
+ (cons ?j (symbol-name buddy))
+ (cons ?r (if (>
+ (length
+ (car resource)) 0)
+ (car resource)
+ "empty"))
+ (cons ?s (or
+ (cdr (assoc
+ (plist-get
+ (cdr resource) 'show)
+ jabber-presence-strings))
+ (plist-get
+ (cdr resource) 'show)))
+ (cons ?S (if (plist-get
+ (cdr resource) 'status)
+ (jabber-fix-status
+ (plist-get (cdr resource)
+ 'status))
+ ""))
+ (cons ?p (number-to-string
+ (plist-get (cdr resource)
+ 'priority)))))))
+ (add-text-properties 0
+ (length resource-str)
+ (list
+ 'face
+ (or (cdr (assoc (plist-get
+ (cdr resource)
+ 'show)
+ jabber-presence-faces))
+ 'jabber-roster-user-online)
+ 'jabber-jid
+ (format "%s/%s" (symbol-name buddy) (car resource))
+ 'jabber-account
+ jc)
+ resource-str)
+ (insert "\n" resource-str))))))
+ (let ((group-name (or group-name
+ jabber-roster-default-group-name)))
+ (add-text-properties 0
+ (length group-name)
+ (list
+ 'face 'jabber-title-small
+ 'jabber-group group-name
+ 'jabber-account jc)
+ group-name)
+ (insert group-name))))
+
+;;;###autoload
+(defun jabber-roster-update (jc new-items changed-items deleted-items)
+ "Update roster, in memory and on display.
+Add NEW-ITEMS, update CHANGED-ITEMS and remove DELETED-ITEMS, all
+three being lists of JID symbols."
+ (let* ((roster (plist-get (fsm-get-state-data jc) :roster))
+ (hash (plist-get (fsm-get-state-data jc) :roster-hash))
+ (ewoc (plist-get (fsm-get-state-data jc) :roster-ewoc))
+ (all-groups (plist-get (fsm-get-state-data jc) :roster-groups))
+ (terminator
+ (lambda (deleted-items)
+ (dolist (delete-this deleted-items)
+ (let ((groups (get delete-this 'groups))
+ (terminator
+ (lambda (g)
+ (let*
+ ((group (or g jabber-roster-default-group-name))
+ (buddies (gethash group hash)))
+ (when (not buddies)
+ (setq new-groups (append new-groups (list group))))
+ (puthash group
+ (delq delete-this buddies)
+ hash)))))
+ (if groups
+ (dolist (group groups)
+ (terminator group))
+ (terminator groups)))))))
+
+ ;; fix a old-roster
+ (dolist (delete-this deleted-items)
+ (setq roster (delq delete-this roster)))
+ (setq roster (append new-items roster))
+ (plist-put (fsm-get-state-data jc) :roster roster)
+
+ ;; update a hash-roster
+ (if (not hash)
+ (jabber-roster-prepare-roster jc)
+
+ (when jabber-roster-debug
+ (message "update hash-based roster"))
+
+ ;; delete items
+ (dolist (delete-this (append deleted-items changed-items))
+ (let ((jid (symbol-name delete-this)))
+ (when jabber-roster-debug
+ (message (concat "delete jid: " jid)))
+ (dolist (group (mapcar (lambda (g) (car g)) all-groups))
+ (when jabber-roster-debug
+ (message (concat "try to delete jid: " jid " from group " group)))
+ (puthash group
+ (delq delete-this (gethash group hash))
+ hash))))
+
+ ;; insert changed-items
+ (dolist (insert-this (append changed-items new-items))
+ (let ((jid (symbol-name insert-this)))
+ (when jabber-roster-debug
+ (message (concat "insert jid: " jid)))
+ (dolist (group (or (get insert-this 'groups)
+ (list jabber-roster-default-group-name)))
+ (when jabber-roster-debug
+ (message (concat "insert jid: " jid " to group " group)))
+ (puthash group
+ (append (gethash group hash)
+ (list insert-this))
+ hash)
+ (setq all-groups (append all-groups (list (list group)))))))
+
+
+ (when jabber-roster-debug
+ (message "remove duplicates from new group"))
+ (setq all-groups (sort
+ (remove-duplicates all-groups
+ :test (lambda (g1 g2)
+ (let ((g1-name (car g1))
+ (g2-name (car g2)))
+ (string= g1-name
+ g2-name))))
+ (lambda (g1 g2)
+ (let ((g1-name (car g1))
+ (g2-name (car g2)))
+ (string< g1-name
+ g2-name)))))
+
+ (plist-put (fsm-get-state-data jc) :roster-groups all-groups))
+
+
+ (when jabber-roster-debug
+ (message "re display roster"))
+
+ ;; recreate roster buffer
+ (jabber-display-roster)))
+
+(defalias 'jabber-presence-update-roster 'ignore)
+;;jabber-presence-update-roster is not needed anymore.
+;;Its work is done in `jabber-process-presence'."
+(make-obsolete 'jabber-presence-update-roster 'ignore)
+
+(defun jabber-next-property (&optional prev)
+ "Return position of next property appearence or nil if there is none.
+If optional PREV is non-nil, return position of previous property appearence."
+ (let ((pos (point))
+ (found nil)
+ (nextprev (if prev 'previous-single-property-change
+ 'next-single-property-change)))
+ (while (not found)
+ (setq pos
+ (let ((jid (funcall nextprev pos 'jabber-jid))
+ (group (funcall nextprev pos 'jabber-group)))
+ (cond
+ ((not jid) group)
+ ((not group) jid)
+ (t (funcall (if prev 'max 'min) jid group)))))
+ (if (not pos)
+ (setq found t)
+ (setq found (or (get-text-property pos 'jabber-jid)
+ (get-text-property pos 'jabber-group)))))
+ pos))
+
+(defun jabber-go-to-next-roster-item ()
+ "Move the cursor to the next jid/group in the buffer"
+ (interactive)
+ (let* ((next (jabber-next-property))
+ (next (if (not next)
+ (progn (goto-char (point-min))
+ (jabber-next-property)) next)))
+ (if next (goto-char next)
+ (goto-char (point-min)))))
+
+(defun jabber-go-to-previous-roster-item ()
+ "Move the cursor to the previous jid/group in the buffer"
+ (interactive)
+ (let* ((previous (jabber-next-property 'prev))
+ (previous (if (not previous)
+ (progn (goto-char (point-max))
+ (jabber-next-property 'prev)) previous)))
+ (if previous (goto-char previous)
+ (goto-char (point-max)))))
+
+(defun jabber-roster-restore-groups (jc)
+ "Restore roster's groups rolling state from private storage"
+ (interactive (list (jabber-read-account)))
+ (jabber-private-get jc 'roster "emacs-jabber"
+ 'jabber-roster-restore-groups-1 'ignore))
+
+(defun jabber-roster-restore-groups-1 (jc xml-data)
+ "Parse roster groups and restore rolling state"
+ (when (string= (jabber-xml-get-xmlns xml-data) "emacs-jabber")
+ (let* ((data (car (last xml-data)))
+ (groups (if (stringp data) (split-string data "\n") nil)))
+ (dolist (group groups)
+ (jabber-roster-roll-group jc group t)))))
+
+(defun jabber-roster-save-groups ()
+ "Save roster's groups rolling state in private storage"
+ (interactive)
+ (dolist (jc jabber-connections)
+ (let* ((groups (plist-get (fsm-get-state-data jc) :roster-roll-groups))
+ (roll-groups
+ (if groups
+ (mapconcat (lambda (a) (substring-no-properties a)) groups "\n")
+ "")))
+ (jabber-private-set jc
+ `(roster ((xmlns . "emacs-jabber"))
+ ,roll-groups)
+ 'jabber-report-success "Roster groups saved"
+ 'jabber-report-success "Failed to save roster groups"))))
+
+(provide 'jabber-roster)
+
+;;; arch-tag: 096af063-0526-4dd2-90fd-bc6b5ba07d32