Show avatars in MUC occupants sidebar

Fixes #1322

(Also clean up some loose threads)
This commit is contained in:
JC Brand 2021-11-19 13:43:14 +01:00
parent bdac6f1b47
commit 35947e3d62
20 changed files with 167 additions and 128 deletions

View File

@ -8,6 +8,7 @@
- Fix trimming of chats in overlayed view mode
- OMEMO bugfix: Always create device session based on real JID.
- If `auto_register_muc_nickname` is set, make sure to register when the user changes current nick.
- #1322: Display occupants avatars in the occupants list
- #1419: Clicking on avatar should show bigger version
- #2647: Singleton mode doesn't work
- #2704: Send button doesn't work in a multi-user chat

View File

@ -82,8 +82,10 @@ function getVCardForChatroomOccupant (message) {
export async function setVCardOnOccupant (occupant) {
await api.waitUntil('VCardsInitialized');
occupant.vcard = getVCardForChatroomOccupant(occupant);
occupant.vcard.on('change', () => occupant.trigger('vcard:change'));
occupant.trigger('vcard:add');
if (occupant.vcard) {
occupant.vcard.on('change', () => occupant.trigger('vcard:change'));
occupant.trigger('vcard:add');
}
}
export async function setVCardOnMUCMessage (message) {
@ -92,8 +94,10 @@ export async function setVCardOnMUCMessage (message) {
} else {
await api.waitUntil('VCardsInitialized');
message.vcard = getVCardForChatroomOccupant(message);
message.vcard.on('change', () => message.trigger('vcard:change'));
message.trigger('vcard:add');
if (message.vcard) {
message.vcard.on('change', () => message.trigger('vcard:change'));
message.trigger('vcard:add');
}
}
}

View File

@ -16,7 +16,7 @@ export default (o) => {
<div class="row">
<div class="col-auto">
<converse-avatar
class="avatar chat-msg__avatar"
class="avatar modal-avatar"
.data=${o.vcard?.attributes}
nonce=${o.vcard?.get('vcard_updated')}
height="120" width="120"></converse-avatar>

View File

@ -75,6 +75,11 @@
order: -1;
color: var(--controlbox-text-color);
.chat-status--avatar {
border: 1px solid var(--controlbox-pane-background-color);
background: var(--controlbox-pane-background-color);
}
converse-brand-logo {
width: 100%;
display: block;

View File

@ -0,0 +1,9 @@
export const PRETTY_CHAT_STATUS = {
'offline': 'Offline',
'unavailable': 'Unavailable',
'xa': 'Extended Away',
'away': 'Away',
'dnd': 'Do not disturb',
'chat': 'Chattty',
'online': 'Online'
};

View File

@ -3,6 +3,7 @@ import tpl_muc_sidebar from "./templates/muc-sidebar.js";
import { CustomElement } from 'shared/components/element.js';
import { _converse, api, converse } from "@converse/headless/core";
import 'shared/styles/status.scss';
import './styles/muc-occupants.scss';
const { u } = converse.env;
@ -21,6 +22,8 @@ export default class MUCSidebar extends CustomElement {
this.listenTo(this.model.occupants, 'add', this.requestUpdate);
this.listenTo(this.model.occupants, 'remove', this.requestUpdate);
this.listenTo(this.model.occupants, 'change', this.requestUpdate);
this.listenTo(this.model.occupants, 'vcard:change', this.requestUpdate);
this.listenTo(this.model.occupants, 'vcard:add', this.requestUpdate);
this.model.initialized.then(() => this.requestUpdate());
}

View File

@ -67,9 +67,6 @@ converse-muc-destroyed {
display: none;
}
}
.occupant-status {
margin-top: 6px;
}
}
}
}

View File

@ -1,5 +1,15 @@
.conversejs {
converse-muc.chatroom {
.chat-status--avatar {
background: var(--occupants-background-color);
border: 1px solid var(--occupants-background-color);
}
.badge-groupchat {
background-color: var(--groupchats-header-color);
}
.box-flyout {
.occupants {
display: flex;
@ -90,6 +100,7 @@
flex-direction: row;
span {
height: 1.6em;
margin-right: 0.25rem;
}
}
@ -102,30 +113,6 @@
.badge {
margin-bottom: 0.125rem;
}
.occupant-status {
display: inline-block;
margin: 0 0.5em 0.125em 0;
width: 0.5em;
height: 0.5em;
&.occupant-online,
&.occupant-chat {
background-color: #1A9707;
}
&.occupant-dnd {
background-color: red;
}
&.occupant-away {
background-color: darkorange;
}
&.occupant-xa {
background-color: orange;
}
&.occupant-offline {
background-color: darkgrey;
}
}
}
}
}

View File

@ -3,29 +3,8 @@ import { __ } from 'i18n';
import tpl_occupant from "./occupant.js";
const PRETTY_CHAT_STATUS = {
'offline': 'Offline',
'unavailable': 'Unavailable',
'xa': 'Extended Away',
'away': 'Away',
'dnd': 'Do not disturb',
'chat': 'Chattty',
'online': 'Online'
};
export default (o) => {
const i18n_occupant_hint = (occupant) => __('Click to mention %1$s in your message.', occupant.get('nick'))
const i18n_participants = __('Participants');
const occupant_tpls = o.occupants.map(occupant => {
return tpl_occupant(Object.assign({
'jid': '',
'hint_show': PRETTY_CHAT_STATUS[occupant.get('show')],
'hint_occupant': i18n_occupant_hint(occupant),
'onOccupantClicked': o.onOccupantClicked
}, occupant.toJSON()));
});
return html`
<div class="occupants-header">
<i class="hide-occupants" @click=${o.closeSidebar}>
@ -36,6 +15,6 @@ export default (o) => {
</div>
</div>
<div class="dragresize dragresize-occupants-left"></div>
<ul class="occupant-list">${occupant_tpls}</ul>
<ul class="occupant-list">${o.occupants.map(occ => tpl_occupant(occ, o))}</ul>
`;
}

View File

@ -1,44 +1,77 @@
import { html } from "lit";
import { PRETTY_CHAT_STATUS } from '../constants.js';
import { __ } from 'i18n';
import { html } from "lit";
import { showOccupantModal } from '../utils.js';
const i18n_occupant_hint = (o) => __('Click to mention %1$s in your message.', o.get('nick'))
const occupant_title = (o) => {
const role = o.get('role');
const hint_occupant = i18n_occupant_hint(o);
const i18n_moderator_hint = __('This user is a moderator.');
const i18n_participant_hint = __('This user can send messages in this groupchat.');
const i18n_visitor_hint = __('This user can NOT send messages in this groupchat.')
const spaced_jid = `${o.jid} ` || '';
if (o.role === "moderator") {
return `${spaced_jid}${i18n_moderator_hint} ${o.hint_occupant}`;
} else if (o.role === "participant") {
return `${spaced_jid}${i18n_participant_hint} ${o.hint_occupant}`;
} else if (o.role === "visitor") {
return `${spaced_jid}${i18n_visitor_hint} ${o.hint_occupant}`;
} else if (!["visitor", "participant", "moderator"].includes(o.role)) {
return `${spaced_jid}${o.hint_occupant}`;
const spaced_jid = o.get('jid') ? `${o.get('jid')} ` : '';
if (role === "moderator") {
return `${spaced_jid}${i18n_moderator_hint} ${hint_occupant}`;
} else if (role === "participant") {
return `${spaced_jid}${i18n_participant_hint} ${hint_occupant}`;
} else if (role === "visitor") {
return `${spaced_jid}${i18n_visitor_hint} ${hint_occupant}`;
} else if (!["visitor", "participant", "moderator"].includes(role)) {
return `${spaced_jid}${hint_occupant}`;
}
}
export default (o) => {
const i18n_owner = __('Owner');
export default (o, chat) => {
const affiliation = o.get('affiliation');
const hint_show = PRETTY_CHAT_STATUS[o.get('show')];
const i18n_admin = __('Admin');
const i18n_member = __('Member');
const i18n_moderator = __('Moderator');
const i18n_owner = __('Owner');
const i18n_visitor = __('Visitor');
const role = o.get('role');
const show = o.get('show');
let classes, color;
if (show === 'online') {
[classes, color] = ['fa fa-circle', 'chat-status-online'];
} else if (show === 'dnd') {
[classes, color] = ['fa fa-minus-circle', 'chat-status-busy'];
} else if (show === 'away') {
[classes, color] = ['fa fa-circle', 'chat-status-away'];
} else {
[classes, color] = ['fa fa-circle', 'subdued-color'];
}
return html`
<li class="occupant" id="${o.id}" title="${occupant_title(o)}">
<div class="row no-gutters">
<div class="col-auto">
<div class="occupant-status occupant-${o.show} circle" title="${o.hint_show}"></div>
<a class="show-msg-author-modal" @click=${(ev) => showOccupantModal(ev, o)}>
<converse-avatar
class="avatar chat-msg__avatar"
.data=${o.vcard?.attributes}
nonce=${o.vcard?.get('vcard_updated')}
height="30" width="30"></converse-avatar>
<converse-icon
title="${hint_show}"
color="var(--${color})"
style="margin-top: -0.1em"
size="0.82em"
class="${classes} chat-status chat-status--avatar"></converse-icon>
</a>
</div>
<div class="col occupant-nick-badge">
<span class="occupant-nick" @click=${o.onOccupantClicked}>${o.nick || o.jid}</span>
<span class="occupant-nick" @click=${chat.onOccupantClicked}>${o.getDisplayName()}</span>
<span class="occupant-badges">
${ (o.affiliation === "owner") ? html`<span class="badge badge-groupchat">${i18n_owner}</span>` : '' }
${ (o.affiliation === "admin") ? html`<span class="badge badge-info">${i18n_admin}</span>` : '' }
${ (o.affiliation === "member") ? html`<span class="badge badge-info">${i18n_member}</span>` : '' }
${ (o.role === "moderator") ? html`<span class="badge badge-info">${i18n_moderator}</span>` : '' }
${ (o.role === "visitor") ? html`<span class="badge badge-secondary">${i18n_visitor}</span>` : '' }
${ (affiliation === "owner") ? html`<span class="badge badge-groupchat">${i18n_owner}</span>` : '' }
${ (affiliation === "admin") ? html`<span class="badge badge-info">${i18n_admin}</span>` : '' }
${ (affiliation === "member") ? html`<span class="badge badge-info">${i18n_member}</span>` : '' }
${ (role === "moderator") ? html`<span class="badge badge-info">${i18n_moderator}</span>` : '' }
${ (role === "visitor") ? html`<span class="badge badge-secondary">${i18n_visitor}</span>` : '' }
</span>
</div>
</div>

View File

@ -1,4 +1,5 @@
import ModeratorToolsModal from './modals/moderator-tools.js';
import OccupantModal from 'modals/occupant.js';
import log from "@converse/headless/log";
import tpl_spinner from 'templates/spinner.js';
import { __ } from 'i18n';
@ -292,6 +293,11 @@ export function showModeratorToolsModal (muc, affiliation) {
}
export function showOccupantModal (ev, occupant) {
api.modal.show(OccupantModal, { 'model': occupant }, ev);
}
export function parseMessageForMUCCommands (muc, text) {
if (
api.settings.get('muc_disable_slash_commands') &&

View File

@ -4,7 +4,7 @@ import { CustomElement } from 'shared/components/element.js';
import { __ } from 'i18n';
import { _converse, api } from '@converse/headless/core';
class ProfileView extends CustomElement {
class Profile extends CustomElement {
initialize () {
this.model = _converse.xmppstatus;
@ -41,4 +41,4 @@ class ProfileView extends CustomElement {
}
}
api.elements.define('converse-user-profile', ProfileView);
api.elements.define('converse-user-profile', Profile);

View File

@ -51,7 +51,7 @@ export default (el) => {
<div class="d-flex xmpp-status">
<a class="change-status" title="${i18n_change_status}" data-toggle="modal" data-target="#changeStatusModal" @click=${el.showStatusChangeModal}>
<span class="${chat_status} w-100 align-self-center" data-value="${chat_status}">
<converse-icon color="var(--${color})" size="1em" class="${classes}"></converse-icon> ${status_message}</span>
<converse-icon color="var(--${color})" style="margin-top: -0.1em" size="0.82em" class="${classes}"></converse-icon> ${status_message}</span>
</a>
</div>
</div>`

View File

@ -12,6 +12,7 @@ import { RosterFilter, RosterFilterView } from './filterview.js';
import { _converse, api, converse } from "@converse/headless/core";
import { highlightRosterItem } from './utils.js';
import 'shared/styles/status.scss';
import './styles/roster.scss';

View File

@ -81,35 +81,6 @@
.current-xmpp-contact {
margin: 0.25em 0;
.chat-status {
vertical-align: middle;
font-size: 0.6em;
margin-right: 0;
margin-left: -0.7em;
margin-bottom: -1.5em;
border-radius: 50%;
border: 2px solid var(--occupants-background-color);
}
.chat-status--offline {
margin-right: 0.8em;
}
.chat-status--online {
color: var(--chat-status-online);
}
.chat-status--busy {
color: var(--chat-status-busy);
}
.chat-status--away {
color: var(--chat-status-away);
}
.chat-status--offline {
display: none;
}
.far.fa-circle,
.fa-times-circle {
color: var(--subdued-color);
}
}
li {

View File

@ -6,31 +6,35 @@ import { STATUSES } from '../constants.js';
export default (el, 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';
}
let classes, color;
if (show === 'online') {
[classes, color] = ['fa fa-circle', 'chat-status-online'];
} else if (show === 'dnd') {
[classes, color] = ['fa fa-minus-circle', 'chat-status-busy'];
} else if (show === 'away') {
[classes, color] = ['fa fa-circle', 'chat-status-away'];
} else {
[classes, color] = ['fa fa-circle', 'subdued-color'];
}
const display_name = item.getDisplayName();
const desc_status = STATUSES[show];
const num_unread = item.get('num_unread') || 0;
const i18n_chat = __('Click to chat with %1$s (XMPP address: %2$s)', display_name, el.jid);
const i18n_chat = __('Click to chat with %1$s (XMPP address: %2$s)', display_name, el.model.get('jid'));
const i18n_remove = __('Click to remove %1$s as a contact', display_name);
return html`
<a class="list-item-link cbox-list-item open-chat ${ num_unread ? 'unread-msgs' : '' }" title="${i18n_chat}" href="#" @click=${el.openChat}>
<converse-avatar
class="avatar"
.data=${el.model.vcard?.attributes}
nonce=${el.model.vcard?.get('vcard_updated')}
height="30" width="30"></converse-avatar>
<span class="${status_icon}" title="${desc_status}"></span>
<span>
<converse-avatar
class="avatar"
.data=${el.model.vcard?.attributes}
nonce=${el.model.vcard?.get('vcard_updated')}
height="30" width="30"></converse-avatar>
<converse-icon
title="${desc_status}"
color="var(--${color})"
size="1em"
class="${classes} chat-status chat-status--avatar"></converse-icon>
</span>
${ num_unread ? html`<span class="msgs-indicator">${ num_unread }</span>` : '' }
<span class="contact-name contact-name--${el.show} ${ num_unread ? 'unread-msgs' : ''}">${display_name}</span>
</a>

View File

@ -1,6 +1,12 @@
converse-avatar {
border: 0;
background: transparent;
&.modal-avatar {
display: block;
margin-bottom: 1em;
}
.avatar {
border-radius: var(--avatar-border-radius);
}

View File

@ -7,9 +7,9 @@ const getImgHref = (image, image_type) => {
export default (o) => {
if (o.image) {
return html`
<svg xmlns="http://www.w3.org/2000/svg" class="avatar ${o.classes}" width="${o.width}" height="${o.height}">
<image width="${o.width}" height="${o.height}" preserveAspectRatio="xMidYMid meet" href="${getImgHref(o.image, o.image_type)}"/>
</svg>`;
<svg xmlns="http://www.w3.org/2000/svg" class="avatar ${o.classes}" width="${o.width}" height="${o.height}">
<image width="${o.width}" height="${o.height}" preserveAspectRatio="xMidYMid meet" href="${getImgHref(o.image, o.image_type)}"/>
</svg>`;
} else {
return '';
}

View File

@ -252,7 +252,6 @@
.chat-msg__heading {
width: 100%;
margin-top: 0.5em;
padding-right: 0.25rem;
padding-bottom: 0.25rem;

View File

@ -0,0 +1,34 @@
.conversejs {
.chat-status {
vertical-align: middle;
margin-right: 0;
border-radius: 50%;
font-size: 1em;
&.chat-status--avatar {
font-size: 0.6rem;
margin-left: -0.7em;
margin-bottom: -1.9em;
border-radius: 50%;
}
}
.chat-status--offline {
margin-right: 0.8em;
}
.chat-status--online {
color: var(--chat-status-online);
}
.chat-status--busy {
color: var(--chat-status-busy);
}
.chat-status--away {
color: var(--chat-status-away);
}
.chat-status--offline {
display: none;
}
.far.fa-circle,
.fa-times-circle {
color: var(--subdued-color);
}
}