import ViewWithAvatar from 'shared/avatar.js'; import log from "@converse/headless/log"; import tpl_pending_contact from "./templates/pending_contact.js"; import tpl_requesting_contact from "./templates/requesting_contact.js"; import tpl_roster_item from "./templates/roster_item.js"; import { __ } from 'i18n'; import { _converse, api, converse } from "@converse/headless/core"; import { debounce, without } from "lodash-es"; import { render } from 'lit-html'; const u = converse.env.utils; const STATUSES = { 'dnd': __('This contact is busy'), 'online': __('This contact is online'), 'offline': __('This contact is offline'), 'unavailable': __('This contact is unavailable'), 'xa': __('This contact is away for an extended period'), 'away': __('This contact is away') }; const RosterContactView = ViewWithAvatar.extend({ tagName: 'li', className: 'list-item d-flex hidden controlbox-padded', events: { "click .accept-xmpp-request": "acceptRequest", "click .decline-xmpp-request": "declineRequest", "click .open-chat": "openChat", "click .remove-xmpp-contact": "removeContact" }, async initialize () { await this.model.initialized; this.debouncedRender = debounce(this.render, 50); this.listenTo(this.model, "change", this.debouncedRender); this.listenTo(this.model, "destroy", this.remove); this.listenTo(this.model, "highlight", this.highlight); this.listenTo(this.model, "remove", this.remove); this.listenTo(this.model, 'vcard:change', this.debouncedRender); this.listenTo(this.model.presence, "change:show", this.debouncedRender); this.render(); }, render () { if (!this.mayBeShown()) { u.hideElement(this.el); return this; } const ask = this.model.get('ask'), show = this.model.presence.get('show'), requesting = this.model.get('requesting'), subscription = this.model.get('subscription'), jid = this.model.get('jid'); const classes_to_remove = [ 'current-xmpp-contact', 'pending-xmpp-contact', 'requesting-xmpp-contact' ].concat(Object.keys(STATUSES)); classes_to_remove.forEach(c => u.removeClass(c, this.el)); this.el.classList.add(show); this.el.setAttribute('data-status', show); this.highlight(); if (_converse.isUniView()) { const chatbox = _converse.chatboxes.get(this.model.get('jid')); if (chatbox) { if (chatbox.get('hidden')) { this.el.classList.remove('open'); } else { this.el.classList.add('open'); } } } if ((ask === 'subscribe') || (subscription === 'from')) { /* ask === 'subscribe' * Means we have asked to subscribe to them. * * subscription === 'from' * They are subscribed to use, but not vice versa. * We assume that there is a pending subscription * from us to them (otherwise we're in a state not * supported by converse.js). * * So in both cases the user is a "pending" contact. */ const display_name = this.model.getDisplayName(); this.el.classList.add('pending-xmpp-contact'); render(tpl_pending_contact(Object.assign(this.model.toJSON(), { display_name })), this.el); } else if (requesting === true) { const display_name = this.model.getDisplayName(); this.el.classList.add('requesting-xmpp-contact'); render(tpl_requesting_contact( Object.assign(this.model.toJSON(), { display_name, 'desc_accept': __("Click to accept the contact request from %1$s", display_name), 'desc_decline': __("Click to decline the contact request from %1$s", display_name), 'allow_chat_pending_contacts': api.settings.get('allow_chat_pending_contacts') }) ), this.el); } else if (subscription === 'both' || subscription === 'to' || _converse.rosterview.isSelf(jid)) { this.el.classList.add('current-xmpp-contact'); this.el.classList.remove(without(['both', 'to'], subscription)[0]); this.el.classList.add(subscription); this.renderRosterItem(this.model); } return this; }, /** * If appropriate, highlight the contact (by adding the 'open' class). * @private * @method _converse.RosterContactView#highlight */ highlight () { if (_converse.isUniView()) { const chatbox = _converse.chatboxes.get(this.model.get('jid')); if ((chatbox && chatbox.get('hidden')) || !chatbox) { this.el.classList.remove('open'); } else { this.el.classList.add('open'); } } }, renderRosterItem (item) { const show = item.presence.get('show') || 'offline'; let status_icon; if (show === 'online') { status_icon = 'fa fa-circle chat-status chat-status--online'; } else if (show === 'away') { status_icon = 'fa fa-circle chat-status chat-status--away'; } else if (show === 'xa') { status_icon = 'far fa-circle chat-status chat-status-xa'; } else if (show === 'dnd') { status_icon = 'fa fa-minus-circle chat-status chat-status--busy'; } else { status_icon = 'fa fa-times-circle chat-status chat-status--offline'; } const display_name = item.getDisplayName(); render(tpl_roster_item( Object.assign(item.toJSON(), { show, display_name, status_icon, 'desc_status': STATUSES[show], 'num_unread': item.get('num_unread') || 0, classes: '' }) ), this.el); this.renderAvatar(); return this; }, /** * Returns a boolean indicating whether this contact should * generally be visible in the roster. * It doesn't check for the more specific case of whether * the group it's in is collapsed. * @private * @method _converse.RosterContactView#mayBeShown */ mayBeShown () { const chatStatus = this.model.presence.get('show'); if (api.settings.get('hide_offline_users') && chatStatus === 'offline') { // If pending or requesting, show if ((this.model.get('ask') === 'subscribe') || (this.model.get('subscription') === 'from') || (this.model.get('requesting') === true)) { return true; } return false; } return true; }, openChat (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.model.openChat(); }, async removeContact (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } if (!api.settings.get('allow_contact_removal')) { return; } if (!confirm(__("Are you sure you want to remove this contact?"))) { return; } try { await this.model.removeFromRoster(); this.remove(); if (this.model.collection) { // The model might have already been removed as // result of a roster push. this.model.destroy(); } } catch (e) { log.error(e); api.alert('error', __('Error'), [__('Sorry, there was an error while trying to remove %1$s as a contact.', this.model.getDisplayName())] ); } }, async acceptRequest (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } await _converse.roster.sendContactAddIQ( this.model.get('jid'), this.model.getFullname(), [] ); this.model.authorize().subscribe(); }, declineRequest (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } const result = confirm(__("Are you sure you want to decline this contact request?")); if (result === true) { this.model.unauthorize().destroy(); } return this; } }); export default RosterContactView;