Skip to content

Commit

Permalink
Add an occupants filter to the MUC sidebar
Browse files Browse the repository at this point in the history
Adds a new component `converse-contacts-filter` which is used to filter
both roster contacts as well as MUC occupants.
  • Loading branch information
jcbrand committed Oct 24, 2023
1 parent 1907f32 commit 1f58894
Show file tree
Hide file tree
Showing 17 changed files with 380 additions and 163 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 11.0.0 (Unreleased)

- #2716: Fix issue with chat display when opening via URL
- Add an occupants filter to the MUC sidebar

### Breaking changes:

Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ module.exports = function(config) {
{ pattern: "src/plugins/muc-views/tests/muc-registration.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/muc.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/nickname.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/occupants-filter.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/occupants.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/rai.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/retractions.js", type: 'module' },
Expand Down
1 change: 1 addition & 0 deletions src/headless/shared/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const api = {
...promise_api,

disco: null,
elements: null,
};

export default api;
Expand Down
14 changes: 10 additions & 4 deletions src/plugins/muc-views/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'shared/autocomplete/index.js';
import tplMUCSidebar from "./templates/muc-sidebar.js";
import { CustomElement } from 'shared/components/element.js';
import { _converse, api, converse } from "@converse/headless";
import { RosterFilter } from 'headless/plugins/roster/filter.js';
import { initStorage } from "headless/utils/storage";

import 'shared/styles/status.scss';
import './styles/muc-occupants.scss';
Expand All @@ -16,8 +18,13 @@ export default class MUCSidebar extends CustomElement {
}
}

connectedCallback () {
super.connectedCallback();
initialize() {
const filter_id = `_converse.occupants-filter-${this.jid}`;
this.filter = new RosterFilter();
this.filter.id = filter_id;
initStorage(this.filter, filter_id);
this.filter.fetch();

this.model = _converse.chatboxes.get(this.jid);
this.listenTo(this.model.occupants, 'add', () => this.requestUpdate());
this.listenTo(this.model.occupants, 'remove', () => this.requestUpdate());
Expand All @@ -28,10 +35,9 @@ export default class MUCSidebar extends CustomElement {
}

render () {
const tpl = tplMUCSidebar(Object.assign(
const tpl = tplMUCSidebar(this, Object.assign(
this.model.toJSON(), {
'occupants': [...this.model.occupants.models],
'closeSidebar': ev => this.closeSidebar(ev),
'onOccupantClicked': ev => this.onOccupantClicked(ev),
}
));
Expand Down
36 changes: 33 additions & 3 deletions src/plugins/muc-views/templates/muc-sidebar.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
import 'shared/components/contacts-filter.js';
import tplOccupant from "./occupant.js";
import tplOccupantsFilter from './occupants-filter.js';
import { __ } from 'i18n';
import { html } from "lit";
import { repeat } from 'lit/directives/repeat.js';

function isOccupantFiltered (el, occ) {
const type = el.filter.get('filter_type');
const q = (type === 'state') ?
el.filter.get('chat_state').toLowerCase() :
el.filter.get('filter_text').toLowerCase();

export default (o) => {
if (!q) return false;

if (type === 'state') {
const show = occ.get('show');
return q === 'online' ? ["offline", "unavailable"].includes(show) : !show.includes(q);
} else if (type === 'contacts') {
return !occ.getDisplayName().toLowerCase().includes(q);
}
}

function shouldShowOccupant (el, occ, o) {
return isOccupantFiltered(el, occ) ? '' : tplOccupant(occ, o);
}

export default (el, o) => {
const i18n_participants = o.occupants.length === 1 ? __('Participant') : __('Participants');
return html`
<div class="occupants-header">
<div class="occupants-header--title">
<span class="occupants-heading">${o.occupants.length} ${i18n_participants}</span>
<i class="hide-occupants" @click=${o.closeSidebar}>
<i class="hide-occupants" @click=${ev => el.closeSidebar(ev)}>
<converse-icon class="fa fa-times" size="1em"></converse-icon>
</i>
</div>
</div>
<div class="dragresize dragresize-occupants-left"></div>
<ul class="occupant-list">${ repeat(o.occupants, (occ) => occ.get('jid'), (occ) => tplOccupant(occ, o)) }</ul>
<ul class="occupant-list">
<converse-contacts-filter
@update=${() => el.requestUpdate()}
.promise=${el.model.initialized}
.contacts=${el.model.occupants}
.template=${tplOccupantsFilter}
.filter=${el.filter}></converse-contacts-filter>
${ repeat(o.occupants, (occ) => occ.get('jid'), (occ) => shouldShowOccupant(el, occ, o)) }
</ul>
`;
}
63 changes: 63 additions & 0 deletions src/plugins/muc-views/templates/occupants-filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { html } from "lit";
import { __ } from 'i18n';

/**
* @param {import('shared/components/contacts-filter').ContactsFilter} el
*/
export default (el) => {
const i18n_placeholder = __('Filter');
const title_contact_filter = __('Filter by name');
const title_status_filter = __('Filter by status');
const label_any = __('Any');
const label_online = __('Online');
const label_chatty = __('Chatty');
const label_busy = __('Busy');
const label_away = __('Away');
const label_xa = __('Extended Away');
const label_offline = __('Offline');

const chat_state = el.filter.get('chat_state');
const filter_text = el.filter.get('filter_text');
const filter_type = el.filter.get('filter_type');

return html`
<form class="contacts-filter-form input-button-group ${ (!el.shouldBeVisible()) ? 'hidden' : 'fade-in' }"
@submit=${ev => el.submitFilter(ev)}>
<div class="form-inline flex-nowrap">
<div class="filter-by d-flex flex-nowrap">
<converse-icon
size="1em"
@click=${ev => el.changeTypeFilter(ev)}
class="fa fa-user clickable ${ (filter_type === 'contacts') ? 'selected' : '' }"
data-type="contacts"
title="${title_contact_filter}"></converse-icon>
<converse-icon
size="1em"
@click=${ev => el.changeTypeFilter(ev)}
class="fa fa-circle clickable ${ (filter_type === 'state') ? 'selected' : '' }"
data-type="state"
title="${title_status_filter}"></converse-icon>
</div>
<div class="btn-group">
<input .value="${filter_text || ''}"
@keydown=${ev => el.liveFilter(ev)}
class="contacts-filter form-control ${ (filter_type === 'state') ? 'hidden' : '' }"
placeholder="${i18n_placeholder}"/>
<converse-icon size="1em"
class="fa fa-times clear-input ${ (!filter_text || filter_type === 'state') ? 'hidden' : '' }"
@click=${ev => el.clearFilter(ev)}>
</converse-icon>
</div>
<select class="form-control state-type ${ (filter_type !== 'state') ? 'hidden' : '' }"
@change=${ev => el.changeChatStateFilter(ev)}>
<option value="">${label_any}</option>
<option ?selected=${chat_state === 'online'} value="online">${label_online}</option>
<option ?selected=${chat_state === 'chat'} value="chat">${label_chatty}</option>
<option ?selected=${chat_state === 'dnd'} value="dnd">${label_busy}</option>
<option ?selected=${chat_state === 'away'} value="away">${label_away}</option>
<option ?selected=${chat_state === 'xa'} value="xa">${label_xa}</option>
<option ?selected=${chat_state === 'offline'} value="offline">${label_offline}</option>
</select>
</div>
</form>`
};
6 changes: 3 additions & 3 deletions src/plugins/muc-views/tests/nickname.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ describe("A MUC", function () {
const view = _converse.chatboxviews.get('lounge@montague.lit');
await u.waitUntil(() => view.querySelectorAll('li .occupant-nick').length, 500);
let occupants = view.querySelector('.occupant-list');
expect(occupants.childElementCount).toBe(1);
expect(occupants.firstElementChild.querySelector('.occupant-nick').textContent.trim()).toBe("oldnick");
expect(occupants.querySelectorAll('.occupant-nick').length).toBe(1);
expect(occupants.querySelector('.occupant-nick').textContent.trim()).toBe("oldnick");

const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent);
expect(csntext.trim()).toEqual("oldnick has entered the groupchat");
Expand Down Expand Up @@ -153,7 +153,7 @@ describe("A MUC", function () {
expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED);

occupants = view.querySelector('.occupant-list');
expect(occupants.childElementCount).toBe(1);
expect(occupants.querySelectorAll('.occupant-nick').length).toBe(1);

presence = $pres().attrs({
from:'lounge@montague.lit/newnick',
Expand Down
75 changes: 75 additions & 0 deletions src/plugins/muc-views/tests/occupants-filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* global mock, converse */

const { $pres, u } = converse.env;

describe("The MUC occupants filter", function () {

fit("can be used to filter which occupants are shown",
mock.initConverse(
[], {},
async function (_converse) {

const muc_jid = 'lounge@montague.lit'
const members = [{
'nick': 'juliet',
'jid': 'juliet@capulet.lit',
'affiliation': 'member'
}, {
'nick': 'tybalt',
'jid': 'tybalt@capulet.lit',
'affiliation': 'member'
}];
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members);
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.model.occupants.length === 3);

let filter_el = view.querySelector('converse-contacts-filter');
expect(u.isVisible(filter_el.firstElementChild)).toBe(false);

for (let i=0; i<mock.chatroom_names.length; i++) {
const name = mock.chatroom_names[i];
const role = mock.chatroom_roles[name].role;
// See example 21 https://xmpp.org/extensions/xep-0045.html#enter-pres
const presence = $pres({
to:'romeo@montague.lit/pda',
from:'lounge@montague.lit/'+name
}).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
.c('item').attrs({
affiliation: mock.chatroom_roles[name].affiliation,
jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit',
role: role
});
_converse.api.connection.get()._dataRecv(mock.createRequest(presence));
}

const occupants = view.querySelector('.occupant-list');
await u.waitUntil(() => occupants.querySelectorAll('li').length > 3);
expect(occupants.querySelectorAll('li').length).toBe(3+mock.chatroom_names.length);
expect(view.model.occupants.length).toBe(3+mock.chatroom_names.length);

mock.chatroom_names.forEach(name => {
const model = view.model.occupants.findWhere({'nick': name});
const index = view.model.occupants.indexOf(model);
expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name);
});

filter_el = view.querySelector('converse-contacts-filter');
expect(u.isVisible(filter_el.firstElementChild)).toBe(true);

const filter = view.querySelector('.contacts-filter');
filter.value = "j";
u.triggerEvent(filter, "keydown", "KeyboardEvent");
await u.waitUntil(() => [...view.querySelectorAll('li')].filter(u.isVisible).length === 1);

filter_el.querySelector('.fa-times').click();
await u.waitUntil(() => [...view.querySelectorAll('li')].filter(u.isVisible).length === 3+mock.chatroom_names.length);

filter_el.querySelector('.fa-circle').click();
const state_select = view.querySelector('.state-type');
state_select.value = "dnd";
u.triggerEvent(state_select, 'change');
expect(state_select.value).toBe('dnd');
expect(state_select.options[state_select.selectedIndex].textContent).toBe('Busy');
await u.waitUntil(() => [...view.querySelectorAll('li')].filter(u.isVisible).length === 0);
}));
});
2 changes: 1 addition & 1 deletion src/plugins/muc-views/tests/occupants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ describe("The occupants sidebar", function () {
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => view.model.occupants.length === 2);

const occupants = view.querySelector('.occupant-list');
for (let i=0; i<mock.chatroom_names.length; i++) {
const name = mock.chatroom_names[i];
const role = mock.chatroom_roles[name].role;
Expand All @@ -34,6 +33,7 @@ describe("The occupants sidebar", function () {
_converse.api.connection.get()._dataRecv(mock.createRequest(presence));
}

const occupants = view.querySelector('.occupant-list');
await u.waitUntil(() => occupants.querySelectorAll('li').length > 2, 500);
expect(occupants.querySelectorAll('li').length).toBe(2+mock.chatroom_names.length);
expect(view.model.occupants.length).toBe(2+mock.chatroom_names.length);
Expand Down
92 changes: 0 additions & 92 deletions src/plugins/rosterview/filterview.js

This file was deleted.

Loading

0 comments on commit 1f58894

Please sign in to comment.