Skip to content

Commit

Permalink
Add support for showing self in the roster
Browse files Browse the repository at this point in the history
  • Loading branch information
jcbrand committed Jan 10, 2025
1 parent 95c60e4 commit a5bfb47
Show file tree
Hide file tree
Showing 16 changed files with 117 additions and 69 deletions.
21 changes: 4 additions & 17 deletions src/headless/plugins/roster/contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,12 @@ class RosterContact extends ColorAwareModel(Model) {
this.presence = presences.findWhere(jid) || presences.create({ jid });
}

openChat () {
api.chats.open(this.get('jid'), this.attributes, true);
getStatus () {
return this.presence.get('show') || 'offline';
}

/**
* Return a string of tab-separated values that are to be used when
* matching against filter text.
*
* The goal is to be able to filter against the VCard fullname,
* roster nickname and JID.
* @returns {string} Lower-cased, tab-separated values
*/
getFilterCriteria () {
const nick = this.get('nickname');
const jid = this.get('jid');
let criteria = this.getDisplayName();
criteria = !criteria.includes(jid) ? criteria.concat(` ${jid}`) : criteria;
criteria = !criteria.includes(nick) ? criteria.concat(` ${nick}`) : criteria;
return criteria.toLowerCase();
openChat () {
api.chats.open(this.get('jid'), this.attributes, true);
}

getDisplayName () {
Expand Down
7 changes: 4 additions & 3 deletions src/headless/plugins/roster/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ converse.plugins.add('converse-roster', {

initialize () {
api.settings.extend({
'allow_contact_requests': true,
'auto_subscribe': false,
'synchronize_availability': true
show_self_in_roster: true,
allow_contact_requests: true,
auto_subscribe: false,
synchronize_availability: true
});

api.promises.add(['cachedRoster', 'roster', 'rosterContactsFetched', 'rosterInitialized']);
Expand Down
4 changes: 4 additions & 0 deletions src/headless/plugins/status/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export default class XMPPStatus extends ColorAwareModel(Model) {
return { "status": api.settings.get("default_state") }
}

getStatus () {
return this.get('status');
}

/**
* @param {string} attr
*/
Expand Down
1 change: 1 addition & 0 deletions src/headless/types/plugins/roster/contact.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ declare class RosterContact extends RosterContact_base {
initialized: any;
setPresence(): void;
presence: any;
getStatus(): any;
openChat(): void;
/**
* Return a string of tab-separated values that are to be used when
Expand Down
1 change: 1 addition & 0 deletions src/headless/types/plugins/status/status.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default class XMPPStatus extends XMPPStatus_base {
defaults(): {
status: any;
};
getStatus(): any;
/**
* @param {string|Object} key
* @param {string|Object} [val]
Expand Down
8 changes: 8 additions & 0 deletions src/headless/utils/array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @template {any} T
* @param {Array<T>} arr
* @returns {Array<T>} A new array containing only unique elements from the input array.
*/
export function unique (arr) {
return [...new Set(arr)];
}
2 changes: 2 additions & 0 deletions src/headless/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
import { Model } from '@converse/skeletor';
import log, { LEVELS } from '../log.js';
import * as array from './array.js';
import * as arraybuffer from './arraybuffer.js';
import * as color from './color.js';
import * as form from './form.js';
Expand Down Expand Up @@ -148,6 +149,7 @@ export function getUniqueId (suffix) {
}

export default Object.assign({
...array,
...arraybuffer,
...color,
...form,
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/rosterview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ converse.plugins.add('converse-rosterview', {

/* -------- Event Handlers ----------- */
api.listen.on('chatBoxesInitialized', () => {
_converse.state.chatboxes.on('destroy', c => highlightRosterItem(c));
_converse.state.chatboxes.on('change:hidden', c => highlightRosterItem(c));
_converse.state.chatboxes.on('destroy', c => highlightRosterItem(c.get('jid')));
_converse.state.chatboxes.on('change:hidden', c => highlightRosterItem(c.get('jid')));
});
}
});
6 changes: 3 additions & 3 deletions src/plugins/rosterview/templates/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@ function renderContact (contact) {
} else if (subscription === 'both' || subscription === 'to' || u.isSameBareJID(jid, api.connection.get().jid)) {
extra_classes.push('current-xmpp-contact');
extra_classes.push(subscription);
extra_classes.push(contact.presence.get('show'));
extra_classes.push(contact.getStatus());
}
return html`
<li class="list-item d-flex controlbox-padded ${extra_classes.join(' ')}" data-status="${contact.presence.get('show')}">
<li class="list-item d-flex controlbox-padded ${extra_classes.join(' ')}" data-status="${contact.getStatus()}">
<converse-roster-contact .model=${contact}></converse-roster-contact>
</li>`;
}


export default (o) => {
export default (o) => {
const i18n_title = __('Click to hide these contacts');
const collapsed = _converse.state.roster.state.get('collapsed_groups');
return html`
Expand Down
9 changes: 7 additions & 2 deletions src/plugins/rosterview/templates/roster.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ export default (el) => {
const i18n_toggle_contacts = __('Click to toggle contacts');
const i18n_title_add_contact = __('Add a contact');
const i18n_title_new_chat = __('Start a new chat');
const roster = _converse.state.roster || [];
const { state } = _converse;
const roster = [
...(state.roster || []),
...api.settings.get('show_self_in_roster') ? [state.xmppstatus] : []
];

const contacts_map = roster.reduce((acc, contact) => populateContactsMap(acc, contact), {});
const groupnames = Object.keys(contacts_map).filter((contact) => shouldShowGroup(contact, el.model));
const is_closed = el.model.get('toggle_state') === CLOSED;
Expand Down Expand Up @@ -96,7 +101,7 @@ export default (el) => {
<span class="w-100 controlbox-heading controlbox-heading--contacts">
<a class="list-toggle open-contacts-toggle" title="${i18n_toggle_contacts}"
role="heading" aria-level="3"
@click=${el.toggleRoster}>
@click="${el.toggleRoster}">
${i18n_heading_contacts}
${ roster.length ? html`<converse-icon
Expand Down
9 changes: 3 additions & 6 deletions src/plugins/rosterview/templates/roster_item.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
/**
* @typedef {import('../contactview').default} RosterContact
*/
import { __ } from 'i18n';
import { api } from "@converse/headless";
import { html } from "lit";
import { getUnreadMsgsDisplay } from 'shared/chat/utils.js';
import { STATUSES } from '../constants.js';

/**
* @param {RosterContact} el
* @param {import('../contactview').default} el
*/
export const tplRemoveLink = (el) => {
const display_name = el.model.getDisplayName();
Expand All @@ -21,10 +18,10 @@ export const tplRemoveLink = (el) => {
}

/**
* @param {RosterContact} el
* @param {import('../contactview').default} el
*/
export default (el) => {
const show = el.model.presence.get('show') || 'offline';
const show = el.model.getStatus() || 'offline';
let classes, color;
if (show === 'online') {
[classes, color] = ['fa fa-circle', 'chat-status-online'];
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/rosterview/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import RosterContact from "@converse/headless/types/plugins/roster/contact"
import XMPPStatus from "@converse/headless/types/plugins/status/status"

export type ContactsMap = {
[Key: string]: RosterContact[]
[Key: string]: (XMPPStatus|RosterContact)[]
}
73 changes: 54 additions & 19 deletions src/plugins/rosterview/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @typedef {import('@converse/headless').RosterContacts} RosterContacts
*/
import { __ } from 'i18n';
import { _converse, api, converse, log, constants } from "@converse/headless";
import { _converse, api, converse, log, constants, u, XMPPStatus } from "@converse/headless";

const { Strophe } = converse.env;
const { STATUS_WEIGHTS } = constants;
Expand All @@ -26,10 +26,17 @@ export async function removeContact (contact) {
}
}

export function highlightRosterItem (chatbox) {
_converse.state.roster?.get(chatbox.get('jid'))?.trigger('highlight');
/**
* @param {string} jid
*/
export function highlightRosterItem (jid) {
_converse.state.roster?.get(jid)?.trigger('highlight');
}

/**
* @param {Event} ev
* @param {string} name
*/
export function toggleGroup (ev, name) {
ev?.preventDefault?.();
const { roster } = _converse.state;
Expand All @@ -42,7 +49,25 @@ export function toggleGroup (ev, name) {
}

/**
* @param {RosterContact} contact
* Return a string of tab-separated values that are to be used when
* matching against filter text.
*
* The goal is to be able to filter against the VCard fullname,
* roster nickname and JID.
* @param {RosterContact|XMPPStatus} contact
* @returns {string} Lower-cased, tab-separated values
*/
function getFilterCriteria(contact) {
const nick = contact instanceof XMPPStatus ? contact.getNickname() : contact.get('nickname');
const jid = contact.get('jid');
let criteria = contact.getDisplayName();
criteria = !criteria.includes(jid) ? criteria.concat(` ${jid}`) : criteria;
criteria = !criteria.includes(nick) ? criteria.concat(` ${nick}`) : criteria;
return criteria.toLowerCase();
}

/**
* @param {RosterContact|XMPPStatus} contact
* @param {string} groupname
* @returns {boolean}
*/
Expand All @@ -65,12 +90,12 @@ export function isContactFiltered (contact, groupname) {
} else if (q === 'unread_messages') {
return contact.get('num_unread') === 0;
} else if (q === 'online') {
return ["offline", "unavailable", "dnd", "away", "xa"].includes(contact.presence.get('show'));
return ["offline", "unavailable", "dnd", "away", "xa"].includes(contact.getStatus());
} else {
return !contact.presence.get('show').includes(q);
return !contact.getStatus().includes(q);
}
} else if (type === 'items') {
return !contact.getFilterCriteria().includes(q);
return !getFilterCriteria(contact).includes(q);
}
}

Expand All @@ -83,7 +108,7 @@ export function isContactFiltered (contact, groupname) {
export function shouldShowContact (contact, groupname, model) {
if (!model.get('filter_visible')) return true;

const chat_status = contact.presence.get('show');
const chat_status = contact.getStatus();
if (api.settings.get('hide_offline_users') && chat_status === 'offline') {
// If pending or requesting, show
if ((contact.get('ask') === 'subscribe') ||
Expand All @@ -96,6 +121,10 @@ export function shouldShowContact (contact, groupname, model) {
return !isContactFiltered(contact, groupname);
}

/**
* @param {string} group
* @param {Model} model
*/
export function shouldShowGroup (group, model) {
if (!model.get('filter_visible')) return true;

Expand All @@ -114,24 +143,26 @@ export function shouldShowGroup (group, model) {
}

/**
* Populates a contacts map with the given contact, categorizing it into appropriate groups.
* @param {import('./types').ContactsMap} contacts_map
* @param {RosterContact} contact
* @returns {import('./types').ContactsMap}
*/
export function populateContactsMap (contacts_map, contact) {
const { labels } = _converse;
let contact_groups;

const contact_groups = u.unique(contact.get('groups') ?? []);

if (contact.get('requesting')) {
contact_groups = [labels.HEADER_REQUESTING_CONTACTS];
contact_groups.push(labels.HEADER_REQUESTING_CONTACTS);
} else if (contact.get('ask') === 'subscribe') {
contact_groups = [labels.HEADER_PENDING_CONTACTS];
contact_groups.push(labels.HEADER_PENDING_CONTACTS);
} else if (contact.get('subscription') === 'none') {
contact_groups = [labels.HEADER_UNSAVED_CONTACTS];
contact_groups.push(labels.HEADER_UNSAVED_CONTACTS);
} else if (!api.settings.get('roster_groups')) {
contact_groups = [labels.HEADER_CURRENT_CONTACTS];
contact_groups.push(labels.HEADER_CURRENT_CONTACTS);
} else {
contact_groups = contact.get('groups');
contact_groups = (contact_groups.length === 0) ? [labels.HEADER_UNGROUPED] : contact_groups;
contact_groups.push(labels.HEADER_UNGROUPED);
}

for (const name of contact_groups) {
Expand All @@ -146,13 +177,13 @@ export function populateContactsMap (contacts_map, contact) {
}

/**
* @param {RosterContact} contact1
* @param {RosterContact} contact2
* @param {RosterContact|XMPPStatus} contact1
* @param {RosterContact|XMPPStatus} contact2
* @returns {(-1|0|1)}
*/
export function contactsComparator (contact1, contact2) {
const status1 = contact1.presence.get('show') || 'offline';
const status2 = contact2.presence.get('show') || 'offline';
const status1 = contact1.getStatus();
const status2 = contact2.getStatus();
if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) {
const name1 = (contact1.getDisplayName()).toLowerCase();
const name2 = (contact2.getDisplayName()).toLowerCase();
Expand All @@ -162,6 +193,10 @@ export function contactsComparator (contact1, contact2) {
}
}

/**
* @param {string} a
* @param {string} b
*/
export function groupsComparator (a, b) {
const HEADER_WEIGHTS = {};
const {
Expand Down
10 changes: 1 addition & 9 deletions src/shared/chat/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,18 +181,10 @@ export function getHats (message) {
return [];
}

/**
* @template {any} T
* @param {Array<T>} arr
* @returns {Array<T>} A new array containing only unique elements from the input array.
*/
function unique (arr) {
return [...new Set(arr)];
}

export function getTonedEmojis () {
if (!converse.emojis.toned) {
converse.emojis.toned = unique(
converse.emojis.toned = u.unique(
Object.values(converse.emojis.json.people)
.filter(person => person.sn.includes('_tone'))
.map(person => person.sn.replace(/_tone[1-5]/, ''))
Expand Down
3 changes: 2 additions & 1 deletion src/types/plugins/rosterview/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import RosterContact from "@converse/headless/types/plugins/roster/contact";
import XMPPStatus from "@converse/headless/types/plugins/status/status";
export type ContactsMap = {
[Key: string]: RosterContact[];
[Key: string]: (XMPPStatus | RosterContact)[];
};
//# sourceMappingURL=types.d.ts.map
Loading

0 comments on commit a5bfb47

Please sign in to comment.