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 bfe6731
Show file tree
Hide file tree
Showing 29 changed files with 197 additions and 127 deletions.
6 changes: 3 additions & 3 deletions src/headless/plugins/chat/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ export default {
*
* @method api.chats.get
* @param {String|string[]} jids - e.g. '[email protected]' or ['[email protected]', '[email protected]']
* @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param { Boolean } [create=false] - Whether the chat should be created if it's not found.
* @returns { Promise<ChatBox[]> }
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [create=false] - Whether the chat should be created if it's not found.
* @returns {Promise<ChatBox[]>}
*
* @example
* // To return a single chat, provide the JID of the contact you're chatting with in that chat:
Expand Down
25 changes: 7 additions & 18 deletions src/headless/plugins/roster/contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,15 @@ 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 () {
// XXX: Doubtful whether it's necessary to pass in the contact
// attributes hers. If so, we should perhaps look them up inside the
// `open` API method.
api.chats.open(this.get('jid'), this.attributes, true);
}

getDisplayName () {
Expand Down Expand Up @@ -154,7 +144,6 @@ class RosterContact extends ColorAwareModel(Model) {
return this;
}


/**
* Remove this contact from the roster
* @param {boolean} unauthorize - Whether to also unauthorize the
Expand Down
6 changes: 4 additions & 2 deletions src/headless/plugins/roster/contacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,14 +280,16 @@ class RosterContacts extends Collection {
/**
* Fetch the roster from the XMPP server
* @emits _converse#roster
* @param {boolean} [full=false] - Whether to fetch the full roster or just the changes.
* @returns {promise}
*/
async fetchFromServer () {
async fetchFromServer (full=false) {
const stanza = $iq({
'type': 'get',
'id': u.getUniqueId('roster'),
}).c('query', { xmlns: Strophe.NS.ROSTER });
if (this.rosterVersioningSupported()) {

if (this.rosterVersioningSupported() && !full) {
stanza.attrs({ 'ver': this.data.get('version') });
}

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
6 changes: 3 additions & 3 deletions src/headless/types/plugins/chat/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ declare namespace _default {
*
* @method api.chats.get
* @param {String|string[]} jids - e.g. '[email protected]' or ['[email protected]', '[email protected]']
* @param { Object } [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param { Boolean } [create=false] - Whether the chat should be created if it's not found.
* @returns { Promise<ChatBox[]> }
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [create=false] - Whether the chat should be created if it's not found.
* @returns {Promise<ChatBox[]>}
*
* @example
* // To return a single chat, provide the JID of the contact you're chatting with in that chat:
Expand Down
10 changes: 1 addition & 9 deletions src/headless/types/plugins/roster/contact.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,8 @@ 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
* 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(): string;
getDisplayName(): any;
getFullname(): any;
/**
Expand Down
3 changes: 2 additions & 1 deletion src/headless/types/plugins/roster/contacts.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ declare class RosterContacts extends Collection {
/**
* Fetch the roster from the XMPP server
* @emits _converse#roster
* @param {boolean} [full=false] - Whether to fetch the full roster or just the changes.
* @returns {promise}
*/
fetchFromServer(): Promise<any>;
fetchFromServer(full?: boolean): Promise<any>;
/**
* Update or create RosterContact models based on the given `item` XML
* node received in the resulting IQ stanza from the server.
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
7 changes: 7 additions & 0 deletions src/headless/types/utils/array.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @template {any} T
* @param {Array<T>} arr
* @returns {Array<T>} A new array containing only unique elements from the input array.
*/
export function unique<T extends unknown>(arr: Array<T>): Array<T>;
//# sourceMappingURL=array.d.ts.map
1 change: 1 addition & 0 deletions src/headless/types/utils/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ declare const _default: {
arrayBufferToBase64(ab: any): string;
base64ToArrayBuffer(b64: any): ArrayBufferLike;
hexToArrayBuffer(hex: any): ArrayBufferLike;
unique<T extends unknown>(arr: Array<T>): Array<T>;
} & CommonUtils & PluginUtils;
export default _default;
export type CommonUtils = Record<string, Function>;
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
2 changes: 1 addition & 1 deletion src/plugins/chatview/tests/chatbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe("Chatboxes", function () {


it("is created when you click on a roster item", mock.initConverse(
['chatBoxesFetched'], {}, async function (_converse) {
['chatBoxesFetched'], { show_self_in_roster: false }, async function (_converse) {

await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/controlbox/tests/controlbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("The Controlbox", function () {
describe("The \"Contacts\" section", function () {

it("can be used to add contact and it checks for case-sensivity",
mock.initConverse([], {}, async function (_converse) {
mock.initConverse([], { show_self_in_roster: false }, async function (_converse) {

spyOn(_converse.api, "trigger").and.callThrough();
await mock.waitForRoster(_converse, 'all', 0);
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/minimize/tests/minchats.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ describe("A Chatbox", function () {
it("can be trimmed to conserve space",
mock.initConverse(
[],
{ no_trimming: false },
{ no_trimming: false, show_self_in_roster: false },
async function (_converse) {

await mock.waitForRoster(_converse, 'current');
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/rosterview/contactview.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default class RosterContact extends CustomElement {
*/
openChat (ev) {
ev?.preventDefault?.();
this.model.openChat();
api.chats.open(this.model.get('jid'), this.model.attributes, true);
}

/**
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
25 changes: 15 additions & 10 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 { _converse, 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,11 @@ export const tplRemoveLink = (el) => {
}

/**
* @param {RosterContact} el
* @param {import('../contactview').default} el
*/
export default (el) => {
const show = el.model.presence.get('show') || 'offline';
const bare_jid = _converse.session.get('bare_jid');
const show = el.model.getStatus() || 'offline';
let classes, color;
if (show === 'online') {
[classes, color] = ['fa fa-circle', 'chat-status-online'];
Expand All @@ -35,11 +33,16 @@ export default (el) => {
} else {
[classes, color] = ['fa fa-circle', 'comment'];
}

const is_self = bare_jid === el.model.get('jid');
const desc_status = STATUSES[show];
const num_unread = getUnreadMsgsDisplay(el.model);
const display_name = el.model.getDisplayName();
const jid = el.model.get('jid');
const i18n_chat = __('Click to chat with %1$s (XMPP address: %2$s)', display_name, jid);
const i18n_chat = is_self ?
__('Click to chat with yourself') :
__('Click to chat with %1$s (XMPP address: %2$s)', display_name, jid);

return html`
<a class="list-item-link cbox-list-item open-chat ${ num_unread ? 'unread-msgs' : '' }"
title="${i18n_chat}"
Expand All @@ -60,7 +63,9 @@ export default (el) => {
class="${classes} chat-status chat-status--avatar"></converse-icon>
</span>
${ num_unread ? html`<span class="msgs-indicator badge">${ num_unread }</span>` : '' }
<span class="contact-name contact-name--${show} ${ num_unread ? 'unread-msgs' : ''}">${display_name}</span>
<span class="contact-name contact-name--${show} ${ num_unread ? 'unread-msgs' : ''}">${display_name + (is_self ? ` ${__('(me)')}` : '')}</span>
</a>
${ api.settings.get('allow_contact_removal') ? tplRemoveLink(el) : '' }`;
<span class="contact-actions">
${ api.settings.get('allow_contact_removal') && !is_self ? tplRemoveLink(el) : '' }
</span>`;
}
4 changes: 2 additions & 2 deletions src/plugins/rosterview/tests/protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

const { u, $iq, $pres, sizzle, Strophe, stx } = converse.env;

describe("The Protocol", function () {
describe("Presence subscriptions", function () {

beforeEach(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));

Expand Down Expand Up @@ -38,7 +38,7 @@ describe("The Protocol", function () {
* stanza of type "result".
*/
it("Subscribe to contact, contact accepts and subscribes back",
mock.initConverse([], { roster_groups: false }, async function (_converse) {
mock.initConverse([], { show_self_in_roster: false, roster_groups: false }, async function (_converse) {

let stanza;
await mock.waitForRoster(_converse, 'current', 0);
Expand Down
Loading

0 comments on commit bfe6731

Please sign in to comment.