Skip to content

Commit

Permalink
Muc domain grouping (#3283)
Browse files Browse the repository at this point in the history
* Add option to group MUCs by their domain using collapsible lists

* Document muc_grouped_by_domain

* Add this change to CHANGES.md

* Move muc domain group functions to their own file

* Don’t use muc-domain-group class for distinct things

This template was done in a similar way to rosterview’s group.js template,
which uses the "roster-group" class for the group, and "roster-group-contacts"
for the list.
This commit changes MUC domain groups to use the "muc-domain-group" class
for the group, and "muc-domain-group-rooms" for the list.

* Attempt to add tests for MUC domain groups

* Don’t focus MUC domain group tests

* Fix focused test from master

This allows CI tests to run properly on this merge request.
It can be skipped if it’s fixed somewhere else on master first.

* Add MUC domain group scss, attempting consistency with roster groups

* Don’t use !important for MUC group headers

Unlike(?) group-toggle (roster-groups) and open-rooms-toggle,
it doesn’t seem necessary for the color to show.
  • Loading branch information
BetaRays authored Nov 3, 2023
1 parent eec5956 commit 5975323
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 5 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
- #3033: Add the `muc_grouped_by_domain` option to display MUCs on the same domain in collapsible groups
- Add an occupants filter to the MUC sidebar

### Breaking changes:
Expand Down
8 changes: 8 additions & 0 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,14 @@ By fetching member lists, Converse.js will always show these users as
participants of the MUC, giving them a permanent "presence" in the MUC.


muc_grouped_by_domain
---------------------

* Default: ``false``

If ``true``, displays MUCS of a same domain together, in collapsible groups.


muc_history_max_stanzas
-----------------------

Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ module.exports = function(config) {
{ pattern: "src/plugins/push/tests/push.js", type: 'module' },
{ pattern: "src/plugins/register/tests/register.js", type: 'module' },
{ pattern: "src/plugins/roomslist/tests/roomslist.js", type: 'module' },
{ pattern: "src/plugins/roomslist/tests/grouplists.js", type: 'module' },
{ pattern: "src/plugins/rootview/tests/root.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/add-contact-modal.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/presence.js", type: 'module' },
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/muc-views/tests/occupants-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { $pres, u } = converse.env;

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

fit("can be used to filter which occupants are shown",
it("can be used to filter which occupants are shown",
mock.initConverse(
[], {},
async function (_converse) {
Expand Down
8 changes: 6 additions & 2 deletions src/plugins/roomslist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import "@converse/headless/plugins/muc/index.js";
import './view.js';
import { converse } from "@converse/headless";
import { api, converse } from "@converse/headless";


converse.plugins.add('converse-roomslist', {
Expand All @@ -19,5 +19,9 @@ converse.plugins.add('converse-roomslist', {
"converse-bookmarks"
],

initialize () { }
initialize () {
api.settings.extend({
'muc_grouped_by_domain': false,
});
}
});
1 change: 1 addition & 0 deletions src/plugins/roomslist/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class RoomsListModel extends Model {
'muc_domain': api.settings.get('muc_domain'),
'nick': _converse.getDefaultMUCNickname(),
'toggle_state': _converse.OPENED,
'collapsed_domains': [],
};
}

Expand Down
14 changes: 14 additions & 0 deletions src/plugins/roomslist/styles/roomsgroups.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.conversejs {
#chatrooms {
.muc-domain-group-toggle {
margin: 0.75em 0 0.25em 0;
}

.muc-domain-group-toggle, .muc-domain-group-toggle .fa {
color: var(--groupchats-header-color);
&:hover {
color: var(--chatroom-head-bg-color-dark);
}
}
}
}
41 changes: 41 additions & 0 deletions src/plugins/roomslist/templates/groups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { __ } from 'i18n';
import { html } from "lit";
import { tplRoomItem } from 'plugins/roomslist/templates/roomslist.js'

import '../styles/roomsgroups.scss';

function tplRoomDomainGroup (el, domain, rooms) {
const i18n_title = __('Click to hide these rooms');
const collapsed = el.model.get('collapsed_domains');
const is_collapsed = collapsed.includes(domain);
return html`
<div class="muc-domain-group" data-domain="${domain}">
<a href="#" class="list-toggle muc-domain-group-toggle controlbox-padded" title="${i18n_title}" @click=${ev => el.toggleDomainList(ev, domain)}>
<converse-icon
class="fa ${ is_collapsed ? 'fa-caret-right' : 'fa-caret-down' }"
size="1em"
color="var(--groupchats-header-color)"></converse-icon>
${domain}
</a>
<ul class="items-list muc-domain-group-rooms ${ is_collapsed ? 'collapsed' : '' }" data-domain="${domain}">
${ rooms.map(room => tplRoomItem(el, room)) }
</ul>
</div>`;
}

export function tplRoomDomainGroupList (el, rooms) {
// The rooms should stay sorted as they are iterated and added in order
const grouped_rooms = new Map();
for (const room of rooms) {
const roomdomain = room.get('jid').split('@').at(-1).toLowerCase();
if (grouped_rooms.has(roomdomain)) {
grouped_rooms.get(roomdomain).push(room);
} else {
grouped_rooms.set(roomdomain, [room]);
}
}
const sorted_domains = Array.from(grouped_rooms.keys());
sorted_domains.sort();

return sorted_domains.map(domain => tplRoomDomainGroup(el, domain, grouped_rooms.get(domain)))
}
9 changes: 7 additions & 2 deletions src/plugins/roomslist/templates/roomslist.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { _converse, api } from "@converse/headless";
import { html } from "lit";
import { isUniView } from '@converse/headless/utils/session.js';
import { addBookmarkViaEvent } from 'plugins/bookmark-views/utils.js';
import { tplRoomDomainGroupList } from 'plugins/roomslist/templates/groups.js';


function isCurrentlyOpen (room) {
Expand Down Expand Up @@ -33,7 +34,7 @@ const tplUnreadIndicator = (room) => html`<span class="list-item-badge badge bad
const tplActivityIndicator = () => html`<span class="list-item-badge badge badge--muc msgs-indicator"></span>`;


function tplRoomItem (el, room) {
export function tplRoomItem (el, room) {
const i18n_leave_room = __('Leave this groupchat');
const has_unread_msgs = room.get('num_unread_general') || room.get('has_activity');
return html`
Expand Down Expand Up @@ -69,6 +70,7 @@ function tplRoomItem (el, room) {

export default (el) => {
const { chatboxes, CHATROOMS_TYPE, CLOSED } = _converse;
const group_by_domain = api.settings.get('muc_grouped_by_domain');
const rooms = chatboxes.filter(m => m.get('type') === CHATROOMS_TYPE);
rooms.sort((a, b) => (a.getDisplayName().toLowerCase() <= b.getDisplayName().toLowerCase() ? -1 : 1));

Expand Down Expand Up @@ -111,7 +113,10 @@ export default (el) => {
<div class="list-container list-container--openrooms ${ rooms.length ? '' : 'hidden' }">
<div class="items-list rooms-list open-rooms-list ${ is_closed ? 'collapsed' : '' }">
${ rooms.map(room => tplRoomItem(el, room)) }
${ group_by_domain ?
tplRoomDomainGroupList(el, rooms) :
rooms.map(room => tplRoomItem(el, room))
}
</div>
</div>`;
}
115 changes: 115 additions & 0 deletions src/plugins/roomslist/tests/grouplists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* global mock, converse */

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


describe("The list of MUC domains", function () {
it("is shown in controlbox", mock.initConverse(
['chatBoxesFetched'],
{ muc_grouped_by_domain: true,
allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
}, async function (_converse) {

await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse);
const controlbox = _converse.chatboxviews.get('controlbox');
let list = controlbox.querySelector('.list-container--openrooms');
expect(u.hasClass('hidden', list)).toBeTruthy();
await mock.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC');

const lview = controlbox.querySelector('converse-rooms-list');
// Check that the group is shown
await u.waitUntil(() => lview.querySelectorAll(".muc-domain-group").length);
let group_els = lview.querySelectorAll(".muc-domain-group");
expect(group_els.length).toBe(1);
// .children[0] should give the a tag with the domain in it
// there might be a more robust way to do this
// (select for ".muc-domain-group-toggle"?)
// .trim() because there is a space for the arrow/triangle icon first
expect(group_els[0].children[0].innerText.trim()).toBe('conference.shakespeare.lit');
// Check that the room is shown
await u.waitUntil(() => lview.querySelectorAll(".open-room").length);
let room_els = lview.querySelectorAll(".open-room");
expect(room_els.length).toBe(1);
expect(room_els[0].innerText).toBe('room@conference.shakespeare.lit');

// Check that a second room in the same domain is shown in the same
// domain group.
await mock.openChatRoom(_converse, 'secondroom', 'conference.shakespeare.lit', 'JC');
await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 1);
group_els = lview.querySelectorAll(".muc-domain-group");
expect(group_els.length).toBe(1); // still only one group
expect(group_els[0].children[0].innerText.trim()).toBe('conference.shakespeare.lit');
room_els = lview.querySelectorAll(".open-room");
expect(room_els.length).toBe(2); // but two rooms inside it


await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo');
await u.waitUntil(() => lview.querySelectorAll(".open-room").length > 2);
room_els = lview.querySelectorAll(".open-room");
expect(room_els.length).toBe(3);
group_els = lview.querySelectorAll(".muc-domain-group");
expect(group_els.length).toBe(2);

let view = _converse.chatboxviews.get('room@conference.shakespeare.lit');
await view.close();
room_els = lview.querySelectorAll(".open-room");
expect(room_els.length).toBe(2);
group_els = lview.querySelectorAll(".muc-domain-group");
expect(group_els.length).toBe(2);
view = _converse.chatboxviews.get('secondroom@conference.shakespeare.lit');
await view.close();
room_els = lview.querySelectorAll(".open-room");
expect(room_els.length).toBe(1);
group_els = lview.querySelectorAll(".muc-domain-group");
expect(group_els.length).toBe(1);
expect(room_els[0].innerText).toBe('lounge@montague.lit');
expect(group_els[0].children[0].innerText.trim()).toBe('montague.lit');
list = controlbox.querySelector('.list-container--openrooms');
u.waitUntil(() => Array.from(list.classList).includes('hidden'));

view = _converse.chatboxviews.get('lounge@montague.lit');
await view.close();
room_els = lview.querySelectorAll(".open-room");
expect(room_els.length).toBe(0);
group_els = lview.querySelectorAll(".muc-domain-group");
expect(group_els.length).toBe(0);

list = controlbox.querySelector('.list-container--openrooms');
expect(Array.from(list.classList).includes('hidden')).toBeTruthy();
}));
});

describe("A MUC domain group", function () {
it("is collapsible", mock.initConverse(
['chatBoxesFetched'],
{ muc_grouped_by_domain: true,
allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
}, async function (_converse) {

await mock.waitForRoster(_converse, 'current', 0);
await mock.openControlBox(_converse);
const controlbox = _converse.chatboxviews.get('controlbox');
let list = controlbox.querySelector('.list-container--openrooms');
await mock.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC');

const lview = controlbox.querySelector('converse-rooms-list');
await u.waitUntil(() => lview.querySelectorAll(".muc-domain-group").length);
expect(u.hasClass('hidden', list)).toBeFalsy();
let group_els = lview.querySelectorAll(".muc-domain-group");
expect(group_els.length).toBe(1);
expect(group_els[0].children[0].innerText.trim()).toBe('conference.shakespeare.lit');

// I would have liked to use u.isVisible on the room (.open-room) here,
// but it didn’t seem to work.
expect(u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms"))).toBe(false);
lview.querySelector('.muc-domain-group-toggle').click();
await u.waitUntil(() => u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms")) === true);
expect(u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms"))).toBe(true);
lview.querySelector('.muc-domain-group-toggle').click();
await u.waitUntil(() => u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms")) === false);
expect(u.hasClass('collapsed', lview.querySelector(".muc-domain-group-rooms"))).toBe(false);
}));
});
10 changes: 10 additions & 0 deletions src/plugins/roomslist/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ export class RoomsList extends CustomElement {
u.slideIn(list_el).then(() => this.model.save({'toggle_state': _converse.CLOSED}));
}
}

toggleDomainList (ev, domain) {
ev?.preventDefault?.();
const collapsed = this.model.get('collapsed_domains');
if (collapsed.includes(domain)) {
this.model.save({'collapsed_domains': collapsed.filter(d => d !== domain)});
} else {
this.model.save({'collapsed_domains': [...collapsed, domain]});
}
}
}

api.elements.define('converse-rooms-list', RoomsList);

0 comments on commit 5975323

Please sign in to comment.