Commit 794a7096 authored by JC Brand's avatar JC Brand

Move converse-rosterview plugin into folder

parent da131715
...@@ -29,7 +29,7 @@ import "./plugins/profile.js"; ...@@ -29,7 +29,7 @@ import "./plugins/profile.js";
import "./plugins/push.js"; // XEP-0357 Push Notifications import "./plugins/push.js"; // XEP-0357 Push Notifications
import "./plugins/register.js"; // XEP-0077 In-band registration import "./plugins/register.js"; // XEP-0077 In-band registration
import "./plugins/roomslist/index.js"; // Show currently open chat rooms import "./plugins/roomslist/index.js"; // Show currently open chat rooms
import "./plugins/rosterview.js"; import "./plugins/rosterview/index.js";
import "./plugins/singleton.js"; import "./plugins/singleton.js";
/* END: Removable components */ /* END: Removable components */
......
...@@ -5,9 +5,8 @@ ...@@ -5,9 +5,8 @@
*/ */
import '@converse/headless/plugins/chatboxes'; import '@converse/headless/plugins/chatboxes';
import 'components/converse.js'; import 'components/converse.js';
import AvatarMixin from 'shared/avatar.js'; import ViewWithAvatar from 'shared/avatar.js';
import ChatBoxViews from './view.js'; import ChatBoxViews from './view.js';
import { View } from '@converse/skeletor/src/view';
import { _converse, api, converse } from '@converse/headless/core'; import { _converse, api, converse } from '@converse/headless/core';
function onChatBoxViewsInitialized () { function onChatBoxViewsInitialized () {
...@@ -47,7 +46,7 @@ converse.plugins.add('converse-chatboxviews', { ...@@ -47,7 +46,7 @@ converse.plugins.add('converse-chatboxviews', {
'theme': 'default' 'theme': 'default'
}); });
_converse.ViewWithAvatar = View.extend(AvatarMixin); _converse.ViewWithAvatar = ViewWithAvatar;
_converse.ChatBoxViews = ChatBoxViews; _converse.ChatBoxViews = ChatBoxViews;
/************************ BEGIN Event Handlers ************************/ /************************ BEGIN Event Handlers ************************/
......
This diff is collapsed.
import ViewWithAvatar from 'shared/avatar.js';
import log from "@converse/headless/log";
import tpl_pending_contact from "./templates/pending_contact.html";
import tpl_requesting_contact from "./templates/requesting_contact.html";
import tpl_roster_item from "./templates/roster_item.html";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { debounce, without } from "lodash-es";
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');
this.el.innerHTML = tpl_pending_contact(
Object.assign(this.model.toJSON(), {
display_name,
'desc_remove': __('Click to remove %1$s as a contact', display_name),
'allow_chat_pending_contacts': api.settings.get('allow_chat_pending_contacts')
})
);
} else if (requesting === true) {
const display_name = this.model.getDisplayName();
this.el.classList.add('requesting-xmpp-contact');
this.el.innerHTML = 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')
})
);
} 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();
this.el.innerHTML = tpl_roster_item(
Object.assign(item.toJSON(), {
show,
display_name,
status_icon,
'desc_status': STATUSES[show],
'desc_chat': __('Click to chat with %1$s (XMPP address: %2$s)', display_name, item.get('jid')),
'desc_remove': __('Click to remove %1$s as a contact', display_name),
'allow_contact_removal': api.settings.get('allow_contact_removal'),
'num_unread': item.get('num_unread') || 0,
classes: ''
})
);
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;
import tpl_roster_filter from "./templates/roster_filter.js";
import { Model } from '@converse/skeletor/src/model.js';
import { View } from '@converse/skeletor/src/view.js';
import { __ } from 'i18n';
import { _converse } from "@converse/headless/core";
import { debounce } from "lodash-es";
export const RosterFilter = Model.extend({
initialize () {
this.set({
'filter_text': '',
'filter_type': 'contacts',
'chat_state': 'online'
});
},
});
export const RosterFilterView = View.extend({
tagName: 'span',
initialize () {
this.listenTo(this.model, 'change:filter_type', this.render);
this.listenTo(this.model, 'change:filter_text', this.render);
},
toHTML () {
return tpl_roster_filter(
Object.assign(this.model.toJSON(), {
visible: this.shouldBeVisible(),
placeholder: __('Filter'),
title_contact_filter: __('Filter by contact name'),
title_group_filter: __('Filter by group name'),
title_status_filter: __('Filter by status'),
label_any: __('Any'),
label_unread_messages: __('Unread'),
label_online: __('Online'),
label_chatty: __('Chatty'),
label_busy: __('Busy'),
label_away: __('Away'),
label_xa: __('Extended Away'),
label_offline: __('Offline'),
changeChatStateFilter: ev => this.changeChatStateFilter(ev),
changeTypeFilter: ev => this.changeTypeFilter(ev),
clearFilter: ev => this.clearFilter(ev),
liveFilter: ev => this.liveFilter(ev),
submitFilter: ev => this.submitFilter(ev),
}));
},
changeChatStateFilter (ev) {
ev && ev.preventDefault();
this.model.save({'chat_state': this.el.querySelector('.state-type').value});
},
changeTypeFilter (ev) {
ev && ev.preventDefault();
const type = ev.target.dataset.type;
if (type === 'state') {
this.model.save({
'filter_type': type,
'chat_state': this.el.querySelector('.state-type').value
});
} else {
this.model.save({
'filter_type': type,
'filter_text': this.el.querySelector('.roster-filter').value
});
}
},
liveFilter: debounce(function () {
this.model.save({'filter_text': this.el.querySelector('.roster-filter').value});
}, 250),
submitFilter (ev) {
ev && ev.preventDefault();
this.liveFilter();
},
/**
* Returns true if the filter is enabled (i.e. if the user
* has added values to the filter).
* @private
* @method _converse.RosterFilterView#isActive
*/
isActive () {
return (this.model.get('filter_type') === 'state' || this.model.get('filter_text'));
},
shouldBeVisible () {
return _converse.roster && _converse.roster.length >= 5 || this.isActive();
},
clearFilter (ev) {
ev && ev.preventDefault();
this.model.save({'filter_text': ''});
}
});
import RosterContactView from './contactview.js';
import tpl_group_header from "./templates/group_header.html";
import { OrderedListView } from "@converse/skeletor/src/overview";
import { _converse, converse } from "@converse/headless/core";
const u = converse.env.utils;
/**
* @class
* @namespace _converse.RosterGroupView
* @memberOf _converse
*/
const RosterGroupView = OrderedListView.extend({
tagName: 'div',
className: 'roster-group hidden',
events: {
"click a.group-toggle": "toggle"
},
sortImmediatelyOnAdd: true,
ItemView: RosterContactView,
listItems: 'model.contacts',
listSelector: '.roster-group-contacts',
sortEvent: 'presenceChanged',
initialize () {
OrderedListView.prototype.initialize.apply(this, arguments);
if (this.model.get('name') === _converse.HEADER_UNREAD) {
this.listenTo(this.model.contacts, "change:num_unread",
c => !this.model.get('unread_messages') && this.removeContact(c)
);
}
if (this.model.get('name') === _converse.HEADER_REQUESTING_CONTACTS) {
this.listenTo(this.model.contacts, "change:requesting",
c => !c.get('requesting') && this.removeContact(c)
);
}
if (this.model.get('name') === _converse.HEADER_PENDING_CONTACTS) {
this.listenTo(this.model.contacts, "change:subscription",
c => (c.get('subscription') !== 'from') && this.removeContact(c)
);
}
this.listenTo(this.model.contacts, "remove", this.onRemove);
this.listenTo(_converse.roster, 'change:groups', this.onContactGroupChange);
// This event gets triggered once *all* contacts (i.e. not
// just this group's) have been fetched from browser
// storage or the XMPP server and once they've been
// assigned to their various groups.
_converse.rosterview.on(
'rosterContactsFetchedAndProcessed',
() => this.sortAndPositionAllItems()
);
},
render () {
this.el.setAttribute('data-group', this.model.get('name'));
this.el.innerHTML = tpl_group_header({
'label_group': this.model.get('name'),
'desc_group_toggle': this.model.get('description'),
'toggle_state': this.model.get('state'),
'_converse': _converse
});
this.contacts_el = this.el.querySelector('.roster-group-contacts');
return this;
},
show () {
u.showElement(this.el);
if (this.model.get('state') === _converse.OPENED) {
Object.values(this.getAll())
.filter(v => v.mayBeShown())
.forEach(v => u.showElement(v.el));
}
return this;
},
collapse () {
return u.slideIn(this.contacts_el);
},
/* Given a list of contacts, make sure they're filtered out
* (aka hidden) and that all other contacts are visible.
* If all contacts are hidden, then also hide the group title.
* @private
* @method _converse.RosterGroupView#filterOutContacts
* @param { Array } contacts
*/
filterOutContacts (contacts=[]) {
let shown = 0;
this.model.contacts.forEach(contact => {
const contact_view = this.get(contact.get('id'));
if (contacts.includes(contact)) {
u.hideElement(contact_view.el);
} else if (contact_view.mayBeShown()) {
u.showElement(contact_view.el);
shown += 1;
}
});
if (shown) {
u.showElement(this.el);
} else {
u.hideElement(this.el);
}
},
/**
* Given the filter query "q" and the filter type "type",
* return a list of contacts that need to be filtered out.
* @private
* @method _converse.RosterGroupView#getFilterMatches
* @param { String } q - The filter query
* @param { String } type - The filter type
*/
getFilterMatches (q, type) {
if (q.length === 0) {
return [];
}
q = q.toLowerCase();
const contacts = this.model.contacts;
if (type === 'state') {
const sticky_groups = [_converse.HEADER_REQUESTING_CONTACTS, _converse.HEADER_UNREAD];
if (sticky_groups.includes(this.model.get('name'))) {
// When filtering by chat state, we still want to
// show sticky groups, even though they don't
// match the state in question.
return [];
} else if (q === 'unread_messages') {
return contacts.filter({'num_unread': 0});
} else if (q === 'online') {
return contacts.filter(c => ["offline", "unavailable"].includes(c.presence.get('show')));
} else {
return contacts.filter(c => !c.presence.get('show').includes(q));
}
} else {
return contacts.filter(c => !c.getFilterCriteria().includes(q));
}
},
/**
* Filter the group's contacts based on the query "q".
*
* If all contacts are filtered out (i.e. hidden), then the
* group must be filtered out as well.
* @private
* @method _converse.RosterGroupView#filter
* @param { string } q - The query to filter against
* @param { string } type
*/
filter (q, type) {
if (q === null || q === undefined) {
type = type || _converse.rosterview.filter_view.model.get('filter_type');
if (type === 'state') {
q = _converse.rosterview.filter_view.model.get('chat_state');
} else {
q = _converse.rosterview.filter_view.model.get('filter_text');
}
}
this.filterOutContacts(this.getFilterMatches(q, type));
},
async toggle (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
const icon_el = ev.target.matches('.fa') ? ev.target : ev.target.querySelector('.fa');
if (u.hasClass("fa-caret-down", icon_el)) {
this.model.save({state: _converse.CLOSED});
await this.collapse();
icon_el.classList.remove("fa-caret-down");
icon_el.classList.add("fa-caret-right");
} else {
icon_el.classList.remove("fa-caret-right");
icon_el.classList.add("fa-caret-down");
this.model.save({state: _converse.OPENED});
this.filter();
u.showElement(this.el);
u.slideOut(this.contacts_el);
}
},
onContactGroupChange (contact) {
const in_this_group = contact.get('groups').includes(this.model.get('name'));
const cid = contact.get('id');
const in_this_overview = !this.get(cid);
if (in_this_group && !in_this_overview) {
this.items.trigger('add', contact);
} else if (!in_this_group) {
this.removeContact(contact);
}
},
removeContact (contact) {
// We suppress events, otherwise the remove event will
// also cause the contact's view to be removed from the
// "Pending Contacts" group.
this.model.contacts.remove(contact, {'silent': true});
this.onRemove(contact);
},
onRemove (contact) {
this.remove(contact.get('jid'));
if (this.model.contacts.length === 0) {
this.remove();
}
}
});
export default RosterGroupView;
/**
* @module converse-rosterview
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import "../modal";
import "@converse/headless/plugins/chatboxes";
import "@converse/headless/plugins/roster";
import "modals/add-contact.js";
import RosterContactView from './contactview.js';
import RosterGroupView from './groupview.js';
import RosterView from './rosterview.js';
import log from "@converse/headless/log";
import { RosterFilter, RosterFilterView } from './filterview.js';
import { _converse, api, converse } from "@converse/headless/core";
converse.plugins.add('converse-rosterview', {
dependencies: ["converse-roster", "converse-modal", "converse-chatboxviews"],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
api.settings.extend({
'autocomplete_add_contact': true,
'allow_chat_pending_contacts': true,
'allow_contact_removal': true,
'hide_offline_users': false,
'roster_groups': true,
'xhr_user_search_url': null,
});
api.promises.add('rosterViewInitialized');
_converse.RosterFilter = RosterFilter;
_converse.RosterFilterView = RosterFilterView;
_converse.RosterContactView = RosterContactView;
_converse.RosterGroupView = RosterGroupView;
_converse.RosterView = RosterView;
/* -------- Event Handlers ----------- */
api.listen.on('chatBoxesInitialized', () => {
function highlightRosterItem (chatbox) {
const contact = _converse.roster && _converse.roster.findWhere({'jid': chatbox.get('jid')});
if (contact !== undefined) {
contact.trigger('highlight');
}
}
_converse.chatboxes.on('destroy', chatbox => highlightRosterItem(chatbox));
_converse.chatboxes.on('change:hidden', chatbox => highlightRosterItem(chatbox));
});
api.listen.on('controlBoxInitialized', (view) => {
function insertRoster () {
if (!view.model.get('connected') || api.settings.get("authentication") === _converse.ANONYMOUS) {
return;
}
/* Place the rosterview inside the "Contacts" panel. */
api.waitUntil('rosterViewInitialized')
.then(() => view.controlbox_pane.el.insertAdjacentElement('beforeEnd', _converse.rosterview.el))
.catch(e => log.fatal(e));
}
insertRoster();
view.model.on('change:connected', insertRoster);
});
function initRosterView () {
/* Create an instance of RosterView once the RosterGroups
* collection has been created (in @converse/headless/core.js)
*/
if (api.settings.get("authentication") === _converse.ANONYMOUS) {
return;
}
_converse.rosterview = new _converse.RosterView({
'model': _converse.rostergroups
});
_converse.rosterview.render();
/**
* Triggered once the _converse.RosterView instance has been created and initialized.
* @event _converse#rosterViewInitialized
* @example _converse.api.listen.on('rosterViewInitialized', () => { ... });
*/
api.trigger('rosterViewInitialized');
}
api.listen.on('rosterInitialized', initRosterView);
api.listen.on('rosterReadyAfterReconnection', initRosterView);
api.listen.on('afterTearDown', () => {
if (converse.rosterview) {
converse.rosterview.model.off().reset();
converse.rosterview.each(groupview => groupview.removeAll().remove());
converse.rosterview.removeAll().remove();
delete converse.rosterview;
}
});
}
});
import RosterGroupView from './groupview.js';
import log from "@converse/headless/log";
import tpl_roster from "./templates/roster.html";
import { Model } from '@converse/skeletor/src/model.js';
import { OrderedListView } from "@converse/skeletor/src/overview";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { debounce, has } from "lodash-es";
const u = converse.env.utils;
/**
* @class
* @namespace _converse.RosterView
* @memberOf _converse
*/
const RosterView = OrderedListView.extend({
tagName: 'div',
id: 'converse-roster',
className: 'controlbox-section',
ItemView: RosterGroupView,
listItems: 'model',
listSelector: '.roster-contacts',
sortEvent: null, // Groups are immutable, so they don't get re-sorted
subviewIndex: 'name',
sortImmediatelyOnAdd: true,
events: {
'click a.controlbox-heading__btn.add-contact': 'showAddContactModal',
'click a.controlbox-heading__btn.sync-contacts': 'syncContacts'
},
initialize () {
OrderedListView.prototype.initialize.apply(this, arguments);
this.listenTo(_converse.roster, "add", this.onContactAdded);
this.listenTo(_converse.roster, 'change:groups', this.onContactAdded);
this.listenTo(_converse.roster, 'change', this.onContactChange);
this.listenTo(_converse.roster, "destroy", this.update);
this.listenTo(_converse.roster, "remove", this.update);
_converse.presences.on('change:show', () => {
this.update();
this.updateFilter();
});
this.listenTo(this.model, "reset", this.reset);
// This event gets triggered once *all* contacts (i.e. not
// just this group's) have been fetched from browser
// storage or the XMPP server and once they've been
// assigned to their various groups.
api.listen.on('rosterGroupsFetched', this.sortAndPositionAllItems.bind(this));
api.listen.on('rosterContactsFetched', () => {
_converse.roster.each(contact => this.addRosterContact(contact, {'silent': true}));
this.update();
this.updateFilter();
this.trigger('rosterContactsFetchedAndProcessed');
});
this.createRosterFilter();
},
render () {
this.el.innerHTML = tpl_roster({
'allow_contact_requests': _converse.allow_contact_requests,
'heading_contacts': __('Contacts'),
'title_add_contact': __('Add a contact'),
'title_sync_contacts': __('Re-sync your contacts')
});
const form = this.el.querySelector('.roster-filter-form');
this.el.replaceChild(this.filter_view.render().el, form);
this.roster_el = this.el.querySelector('.roster-contacts');
return this;
},
showAddContactModal (ev) {
api.modal.show(_converse.AddContactModal, {'model': new Model()}, ev);
},
createRosterFilter () {
// Create a model on which we can store filter properties
const model = new _converse.RosterFilter();
model.id = `_converse.rosterfilter-${_converse.bare_jid}`;
model.browserStorage = _converse.createStore(model.id);
this.filter_view = new _converse.RosterFilterView({model});
this.listenTo(this.filter_view.model, 'change', this.updateFilter);
this.filter_view.model.fetch();
},
/**
* Called whenever the filter settings have been changed or
* when contacts have been added, removed or changed.
*
* Debounced for 100ms so that it doesn't get called for every
* contact fetched from browser storage.
*/
updateFilter: debounce(function () {
const type = this.filter_view.model.get('filter_type');
if (type === 'state') {
this.filter(this.filter_view.model.get('chat_state'), type);
} else {
this.filter(this.filter_view.model.get('filter_text'), type);
}
}, 100),
update () {
if (!u.isVisible(this.roster_el)) {
u.showElement(this.roster_el);
}
this.filter_view.render();
return this;
},
filter (query, type) {
const views = Object.values(this.getAll());
// First ensure the filter is restored to its original state
views.forEach(v => (v.model.contacts.length > 0) && v.show().filter(''));
// Now we can filter
query = query.toLowerCase();
if (type === 'groups') {
views.forEach(view => {
if (!view.model.get('name').toLowerCase().includes(query)) {
u.slideIn(view.el);
} else if (view.model.contacts.length > 0) {
u.slideOut(view.el);
}
});
} else {
views.forEach(v => v.filter(query, type));
}
},
async syncContacts (ev) {
ev.preventDefault();
u.addClass('fa-spin', ev.target);
_converse.roster.data.save('version', null);
await _converse.roster.fetchFromServer();
api.user.presence.send();
u.removeClass('fa-spin', ev.target);
},
reset () {
this.removeAll();
this.render().update();
return this;
},
onContactAdded (contact) {
this.addRosterContact(contact)
this.update();
this.updateFilter();
},
onContactChange (contact) {
this.update();
if (has(contact.changed, 'subscription')) {
if (contact.changed.subscription === 'from') {
this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
} else if (['both', 'to'].includes(contact.get('subscription'))) {
this.addExistingContact(contact);
}
}
if (has(contact.changed, 'num_unread') && contact.get('num_unread')) {
this.addContactToGroup(contact, _converse.HEADER_UNREAD);
}
if (has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') {
this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
}
if (has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS);
}
this.updateFilter();
},
/**
* Returns the group as specified by name.
* Creates the group if it doesn't exist.
* @method _converse.RosterView#getGroup
* @private
* @param {string} name
*/
getGroup (name) {
const view = this.get(name);
if (view) {
return view.model;
}
return this.model.create({name});
},
addContactToGroup (contact, name, options) {
this.getGroup(name).contacts.add(contact, options);
this.sortAndPositionAllItems();
},
addExistingContact (contact, options) {
let groups;
if (api.settings.get('roster_groups')) {
groups = contact.get('groups');
groups = (groups.length === 0) ? [_converse.HEADER_UNGROUPED] : groups;
} else {
groups = [_converse.HEADER_CURRENT_CONTACTS];
}
if (contact.get('num_unread')) {
groups.push(_converse.HEADER_UNREAD);
}
groups.forEach(g => this.addContactToGroup(contact, g, options));
},
isSelf (jid) {
return u.isSameBareJID(jid, _converse.connection.jid);
},
addRosterContact (contact, options) {
const jid = contact.get('jid');
if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to' || this.isSelf(jid)) {
this.addExistingContact(contact, options);
} else {
if (!_converse.allow_contact_requests) {
log.debug(
`Not adding requesting or pending contact ${jid} `+
`because allow_contact_requests is false`
);
return;
}
if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS, options);
} else if (contact.get('requesting') === true) {
this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS, options);
}
}
return this;
}
});
export default RosterView;
import tpl_avatar from 'templates/avatar.js'; import tpl_avatar from 'templates/avatar.js';
import { View } from '@converse/skeletor/src/view';
import { converse } from '@converse/headless/core'; import { converse } from '@converse/headless/core';
const u = converse.env.utils; const u = converse.env.utils;
const AvatarMixin = { const ViewWithAvatar = View.extend({
renderAvatar (el) { renderAvatar (el) {
el = el || this.el; el = el || this.el;
const avatar_el = el.querySelector('canvas.avatar, svg.avatar'); const avatar_el = el.querySelector('canvas.avatar, svg.avatar');
...@@ -21,6 +22,6 @@ const AvatarMixin = { ...@@ -21,6 +22,6 @@ const AvatarMixin = {
avatar_el.outerHTML = u.getElementFromTemplateResult(tpl_avatar(data)).outerHTML; avatar_el.outerHTML = u.getElementFromTemplateResult(tpl_avatar(data)).outerHTML;
} }
} }
}; });
export default AvatarMixin; export default ViewWithAvatar;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment