Commit b5d57f0e authored by JC Brand's avatar JC Brand

Handle and render chat state notifications separately from messages

parent 283a810d
...@@ -230,10 +230,21 @@ ...@@ -230,10 +230,21 @@
width: 100% width: 100%
} }
} }
.chat-content-sendbutton { .chat-content-sendbutton {
height: calc(100% - (var(--chat-textarea-height) + var(--send-button-height) + 2 * var(--send-button-margin))); height: calc(100% - (var(--chat-textarea-height) + var(--send-button-height) + 2 * var(--send-button-margin)));
} }
.chat-state-notifications {
white-space: pre;
background-color: var(--chat-content-background-color);
color: var(--subdued-color);
font-size: 90%;
font-style: italic;
line-height: var(--line-height-small);
padding: 0 1em 0.3em;
}
.dropdown { /* status dropdown styles */ .dropdown { /* status dropdown styles */
background-color: var(--light-background-color); background-color: var(--light-background-color);
dd { dd {
......
This diff is collapsed.
...@@ -1108,21 +1108,8 @@ ...@@ -1108,21 +1108,8 @@
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
await new Promise(resolve => view.once('messageInserted', resolve)); await new Promise(resolve => view.once('messageInserted', resolve));
jasmine.clock().tick(1000);
// Insert <composing> message, to also check that
// text messages are inserted correctly with
// temporary chat events in the chat contents.
_converse.handleMessageStanza($msg({
'id': 'aeb219',
'to': _converse.bare_jid,
'xmlns': 'jabber:client',
'from': sender_jid,
'type': 'chat'})
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
.tree());
await new Promise(resolve => view.once('messageInserted', resolve));
jasmine.clock().tick(1*ONE_MINUTE_LATER); jasmine.clock().tick(1*ONE_MINUTE_LATER);
_converse.handleMessageStanza($msg({ _converse.handleMessageStanza($msg({
'from': sender_jid, 'from': sender_jid,
'to': _converse.connection.jid, 'to': _converse.connection.jid,
......
This diff is collapsed.
...@@ -197,13 +197,14 @@ converse.plugins.add('converse-chatview', { ...@@ -197,13 +197,14 @@ converse.plugins.add('converse-chatview', {
this.initDebounced(); this.initDebounced();
this.listenTo(this.model.messages, 'add', this.onMessageAdded); this.listenTo(this.model.messages, 'add', this.onMessageAdded);
this.listenTo(this.model.messages, 'change:edited', this.onMessageEdited);
this.listenTo(this.model.messages, 'rendered', this.scrollDown); this.listenTo(this.model.messages, 'rendered', this.scrollDown);
this.model.messages.on('reset', () => { this.model.messages.on('reset', () => {
this.content.innerHTML = ''; this.content.innerHTML = '';
this.removeAll(); this.removeAll();
}); });
this.listenTo(this.model.csn, 'change', this.renderChatStateNotification);
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged); this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
this.listenTo(this.model, 'destroy', this.remove); this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'show', this.show); this.listenTo(this.model, 'show', this.show);
...@@ -248,11 +249,25 @@ converse.plugins.add('converse-chatview', { ...@@ -248,11 +249,25 @@ converse.plugins.add('converse-chatview', {
); );
render(result, this.el); render(result, this.el);
this.content = this.el.querySelector('.chat-content'); this.content = this.el.querySelector('.chat-content');
this.csn = this.el.querySelector('.chat-state-notifications');
this.renderChatStateNotification();
this.renderMessageForm(); this.renderMessageForm();
this.renderHeading(); this.renderHeading();
return this; return this;
}, },
renderChatStateNotification () {
if (this.model.csn.get('chat_state') === _converse.COMPOSING) {
this.csn.innerText = __('%1$s is typing', this.model.getDisplayName());
} else if (this.model.csn.get('chat_state') === _converse.PAUSED) {
this.csn.innerText = __('%1$s has stopped typing', this.model.getDisplayName());
} else if (this.model.csn.get('chat_state') === _converse.GONE) {
this.csn.innerText = __('%1$s has gone away', this.model.getDisplayName());
} else {
this.csn.innerText = '';
}
},
renderToolbar () { renderToolbar () {
if (!_converse.show_toolbar) { if (!_converse.show_toolbar) {
return this; return this;
...@@ -729,7 +744,6 @@ converse.plugins.add('converse-chatview', { ...@@ -729,7 +744,6 @@ converse.plugins.add('converse-chatview', {
await message.initialized; await message.initialized;
const view = this.add(message.get('id'), new _converse.MessageView({'model': message})); const view = this.add(message.get('id'), new _converse.MessageView({'model': message}));
await view.render(); await view.render();
this.clearChatStateForSender(message.get('from'));
this.insertMessage(view); this.insertMessage(view);
this.insertDayIndicator(view.el); this.insertDayIndicator(view.el);
this.setScrollPosition(view.el); this.setScrollPosition(view.el);
...@@ -741,7 +755,7 @@ converse.plugins.add('converse-chatview', { ...@@ -741,7 +755,7 @@ converse.plugins.add('converse-chatview', {
// when the user writes a message as opposed to when a // when the user writes a message as opposed to when a
// message is received. // message is received.
this.model.set('scrolled', false); this.model.set('scrolled', false);
} else if (this.model.get('scrolled', true) && !u.isOnlyChatStateNotification(message)) { } else if (this.model.get('scrolled', true)) {
this.showNewMessagesIndicator(); this.showNewMessagesIndicator();
} }
} }
...@@ -784,16 +798,6 @@ converse.plugins.add('converse-chatview', { ...@@ -784,16 +798,6 @@ converse.plugins.add('converse-chatview', {
}); });
}, },
/**
* Handler that gets called when a message object has been edited via LMC.
* @private
* @method _converse.ChatBoxView#onMessageEdited
* @param { object } message - The updated message object.
*/
onMessageEdited (message) {
this.clearChatStateForSender(message.get('from'));
},
parseMessageForCommands (text) { parseMessageForCommands (text) {
const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/); const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
if (match) { if (match) {
...@@ -1086,16 +1090,6 @@ converse.plugins.add('converse-chatview', { ...@@ -1086,16 +1090,6 @@ converse.plugins.add('converse-chatview', {
return this; return this;
}, },
/**
* Remove chat state notifications for a given sender JID.
* @private
* @method _converse.ChatBoxView#clearChatStateForSender
* @param {string} sender - The sender of the chat state
*/
clearChatStateForSender (sender) {
sizzle(`.chat-state-notification[data-csn="${sender}"]`, this.content).forEach(u.removeElement);
},
/** /**
* Insert a particular string value into the textarea of this chat box. * Insert a particular string value into the textarea of this chat box.
* @private * @private
......
...@@ -12,7 +12,6 @@ import { debounce } from 'lodash' ...@@ -12,7 +12,6 @@ import { debounce } from 'lodash'
import { render } from "lit-html"; import { render } from "lit-html";
import filesize from "filesize"; import filesize from "filesize";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import tpl_csn from "templates/csn.html";
import tpl_file_progress from "templates/file_progress.html"; import tpl_file_progress from "templates/file_progress.html";
import tpl_info from "templates/info.html"; import tpl_info from "templates/info.html";
import tpl_message from "templates/message.html"; import tpl_message from "templates/message.html";
...@@ -119,9 +118,7 @@ converse.plugins.add('converse-message-view', { ...@@ -119,9 +118,7 @@ converse.plugins.add('converse-message-view', {
async render () { async render () {
const is_followup = u.hasClass('chat-msg--followup', this.el); const is_followup = u.hasClass('chat-msg--followup', this.el);
if (this.model.isOnlyChatStateNotification()) { if (this.model.get('file') && !this.model.get('oob_url')) {
this.renderChatStateNotification()
} else if (this.model.get('file') && !this.model.get('oob_url')) {
if (!this.model.file) { if (!this.model.file) {
log.error("Attempted to render a file upload message with no file data"); log.error("Attempted to render a file upload message with no file data");
return this.el; return this.el;
...@@ -327,38 +324,6 @@ converse.plugins.add('converse-message-view', { ...@@ -327,38 +324,6 @@ converse.plugins.add('converse-message-view', {
return this.replaceElement(msg); return this.replaceElement(msg);
}, },
renderChatStateNotification () {
let text;
const from = this.model.get('from');
const name = this.model.getDisplayName();
if (this.model.get('chat_state') === _converse.COMPOSING) {
if (this.model.get('sender') === 'me') {
text = __('Typing from another device');
} else {
text = __('%1$s is typing', name);
}
} else if (this.model.get('chat_state') === _converse.PAUSED) {
if (this.model.get('sender') === 'me') {
text = __('Stopped typing on the other device');
} else {
text = __('%1$s has stopped typing', name);
}
} else if (this.model.get('chat_state') === _converse.GONE) {
text = __('%1$s has gone away', name);
} else {
return;
}
const isodate = (new Date()).toISOString();
this.replaceElement(
u.stringToElement(
tpl_csn({
'message': text,
'from': from,
'isodate': isodate
})));
},
renderFileUploadProgresBar () { renderFileUploadProgresBar () {
const msg = u.stringToElement(tpl_file_progress( const msg = u.stringToElement(tpl_file_progress(
Object.assign(this.model.toJSON(), { Object.assign(this.model.toJSON(), {
......
...@@ -15,7 +15,6 @@ import { __ } from '@converse/headless/i18n'; ...@@ -15,7 +15,6 @@ import { __ } from '@converse/headless/i18n';
import converse from "@converse/headless/converse-core"; import converse from "@converse/headless/converse-core";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js"; import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js";
import tpl_chatarea from "templates/chatarea.html";
import tpl_chatroom from "templates/chatroom.js"; import tpl_chatroom from "templates/chatroom.js";
import tpl_chatroom_bottom_panel from "templates/chatroom_bottom_panel.html"; import tpl_chatroom_bottom_panel from "templates/chatroom_bottom_panel.html";
import tpl_chatroom_destroyed from "templates/chatroom_destroyed.html"; import tpl_chatroom_destroyed from "templates/chatroom_destroyed.html";
...@@ -708,8 +707,10 @@ converse.plugins.add('converse-muc-views', { ...@@ -708,8 +707,10 @@ converse.plugins.add('converse-muc-views', {
this.removeAll(); this.removeAll();
}); });
this.listenTo(this.model, 'change', this.renderHeading); this.listenTo(this.model.csn, 'change', this.renderChatStateNotifications);
this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged); this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
this.listenTo(this.model, 'change', this.renderHeading);
this.listenTo(this.model, 'change:hidden_occupants', this.updateOccupantsToggle); this.listenTo(this.model, 'change:hidden_occupants', this.updateOccupantsToggle);
this.listenTo(this.model, 'change:subject', this.setChatRoomSubject); this.listenTo(this.model, 'change:subject', this.setChatRoomSubject);
this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm); this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
...@@ -745,10 +746,14 @@ converse.plugins.add('converse-muc-views', { ...@@ -745,10 +746,14 @@ converse.plugins.add('converse-muc-views', {
render () { render () {
this.el.setAttribute('id', this.model.get('box_id')); this.el.setAttribute('id', this.model.get('box_id'));
render(tpl_chatroom(), this.el); render(tpl_chatroom({
'muc_show_logs_before_join': _converse.muc_show_logs_before_join,
'show_send_button': _converse.show_send_button
}), this.el);
this.renderHeading(); this.renderHeading();
this.renderChatArea();
this.renderBottomPanel(); this.renderBottomPanel();
this.content = this.el.querySelector('.chat-content');
this.csn = this.el.querySelector('.chat-state-notifications');
if (!_converse.muc_show_logs_before_join) { if (!_converse.muc_show_logs_before_join) {
this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED && this.showSpinner(); this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED && this.showSpinner();
} }
...@@ -758,6 +763,47 @@ converse.plugins.add('converse-muc-views', { ...@@ -758,6 +763,47 @@ converse.plugins.add('converse-muc-views', {
return this; return this;
}, },
renderChatStateNotifications () {
const actors_per_state = this.model.csn.toJSON();
const message = converse.CHAT_STATES.reduce((result, state) => {
const existing_actors = actors_per_state[state];
if (!existing_actors) {
return result;
}
const actors = existing_actors
.map(a => this.model.getOccupant(a))
.filter(a => a)
.map(a => a.getDisplayName());
if (actors.length === 1) {
if (state === 'composing') {
return `${result} ${__('%1$s is typing', actors[0])}\n`;
} else if (state === 'paused') {
return `${result} ${__('%1$s has stopped typing', actors[0])}\n`;
} else if (state === _converse.GONE) {
return `${result} ${__('%1$s has gone away', actors[0])}\n`;
}
} else if (actors.length > 1) {
let actors_str;
if (actors.length > 3) {
actors_str = `${Array.from(actors).slice(0, 2).join(', ')} and others`;
} else {
const last_actor = actors.pop();
actors_str = __('%1$s and %2$s', actors.join(', '), last_actor);
}
if (state === 'composing') {
return `${result} ${__('%1$s are typing', actors_str)}\n`;
} else if (state === 'paused') {
return `${result} ${__('%1$s have stopped typing', actors_str)}\n`;
} else if (state === _converse.GONE) {
return `${result} ${__('%1$s have gone away', actors_str)}\n`;
}
}
return result;
}, '');
this.csn.innerHTML = message;
},
/** /**
* Renders the MUC heading if any relevant attributes have changed. * Renders the MUC heading if any relevant attributes have changed.
* @private * @private
...@@ -780,23 +826,6 @@ converse.plugins.add('converse-muc-views', { ...@@ -780,23 +826,6 @@ converse.plugins.add('converse-muc-views', {
} }
}, },
renderChatArea () {
// Render the UI container in which groupchat messages will appear.
if (this.el.querySelector('.chat-area') === null) {
const container_el = this.el.querySelector('.chatroom-body');
container_el.insertAdjacentHTML(
'beforeend',
tpl_chatarea({
__,
'muc_show_logs_before_join': _converse.muc_show_logs_before_join,
'show_send_button': _converse.show_send_button
})
);
this.content = this.el.querySelector('.chat-content');
}
return this;
},
createSidebarView () { createSidebarView () {
this.model.occupants.chatroomview = this; this.model.occupants.chatroomview = this;
this.sidebar_view = new _converse.MUCSidebar({'model': this.model.occupants}); this.sidebar_view = new _converse.MUCSidebar({'model': this.model.occupants});
......
...@@ -150,12 +150,8 @@ converse.plugins.add('converse-chat', { ...@@ -150,12 +150,8 @@ converse.plugins.add('converse-chat', {
} }
}, },
isOnlyChatStateNotification () {
return u.isOnlyChatStateNotification(this);
},
isEphemeral () { isEphemeral () {
return this.get('is_ephemeral') || u.isOnlyChatStateNotification(this); return this.get('is_ephemeral');
}, },
getDisplayName () { getDisplayName () {
...@@ -325,6 +321,7 @@ converse.plugins.add('converse-chat', { ...@@ -325,6 +321,7 @@ converse.plugins.add('converse-chat', {
} }
this.set({'box_id': `box-${btoa(jid)}`}); this.set({'box_id': `box-${btoa(jid)}`});
this.initMessages(); this.initMessages();
this.initCSN();
if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) { if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid}); this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
...@@ -357,6 +354,10 @@ converse.plugins.add('converse-chat', { ...@@ -357,6 +354,10 @@ converse.plugins.add('converse-chat', {
}); });
}, },
initCSN () {
this.csn = new Model();
},
afterMessagesFetched () { afterMessagesFetched () {
/** /**
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from * Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
...@@ -409,8 +410,13 @@ converse.plugins.add('converse-chat', { ...@@ -409,8 +410,13 @@ converse.plugins.add('converse-chat', {
return; return;
} }
this.setEditable(attrs, attrs.time, stanza); this.setEditable(attrs, attrs.time, stanza);
if (attrs['chat_state'] && attrs.sender === 'them') {
this.csn.set('chat_state', attrs.chat_state);
}
if (u.shouldCreateMessage(attrs)) { if (u.shouldCreateMessage(attrs)) {
const msg = this.handleCorrection(attrs) || await this.createMessage(attrs); const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);
this.csn.set({'chat_state': null});
this.incrementUnreadMsgCounter(msg); this.incrementUnreadMsgCounter(msg);
} }
} }
...@@ -945,12 +951,18 @@ converse.plugins.add('converse-chat', { ...@@ -945,12 +951,18 @@ converse.plugins.add('converse-chat', {
} }
}, },
/**
* @async
* @private
* @method _converse.ChatBox#createMessage
*/
createMessage (attrs, options) { createMessage (attrs, options) {
return this.messages.create(attrs, Object.assign({'wait': true, 'promise':true}, options)); return this.messages.create(attrs, Object.assign({'wait': true, 'promise':true}, options));
}, },
/** /**
* Responsible for sending off a text message inside an ongoing chat conversation. * Responsible for sending off a text message inside an ongoing chat conversation.
* @private
* @method _converse.ChatBox#sendMessage * @method _converse.ChatBox#sendMessage
* @memberOf _converse.ChatBox * @memberOf _converse.ChatBox
* @param { String } text - The chat message text * @param { String } text - The chat message text
...@@ -1073,7 +1085,6 @@ converse.plugins.add('converse-chat', { ...@@ -1073,7 +1085,6 @@ converse.plugins.add('converse-chat', {
}, },
maybeShow () { maybeShow () {
// Returns the chatbox
return this.trigger("show"); return this.trigger("show");
}, },
......
...@@ -1691,6 +1691,9 @@ window.converse = window.converse || {}; ...@@ -1691,6 +1691,9 @@ window.converse = window.converse || {};
* @namespace converse * @namespace converse
*/ */
Object.assign(window.converse, { Object.assign(window.converse, {
CHAT_STATES: ['active', 'composing', 'gone', 'inactive', 'paused'],
keycodes: { keycodes: {
TAB: 9, TAB: 9,
ENTER: 13, ENTER: 13,
......
...@@ -354,6 +354,7 @@ converse.plugins.add('converse-muc', { ...@@ -354,6 +354,7 @@ converse.plugins.add('converse-muc', {
this.debouncedRejoin = debounce(this.rejoin, 250); this.debouncedRejoin = debounce(this.rejoin, 250);
this.set('box_id', `box-${btoa(this.get('jid'))}`); this.set('box_id', `box-${btoa(this.get('jid'))}`);
this.initMessages(); this.initMessages();
this.initCSN();
this.initOccupants(); this.initOccupants();
this.initDiscoModels(); // sendChatState depends on this.features this.initDiscoModels(); // sendChatState depends on this.features
this.registerHandlers(); this.registerHandlers();
...@@ -1818,6 +1819,36 @@ converse.plugins.add('converse-muc', { ...@@ -1818,6 +1819,36 @@ converse.plugins.add('converse-muc', {
} }
}, },
removeCSNFor (actor, state) {
const actors_per_state = this.csn.toJSON();
const existing_actors = Array.from(actors_per_state[state]) || [];
if (existing_actors.includes(actor)) {
const idx = existing_actors.indexOf(actor);
existing_actors.splice(idx, 1);
this.csn.set(state, Array.from(existing_actors));
}
},
updateCSN (attrs) {
const actor = attrs.nick;
const state = attrs.chat_state;
const actors_per_state = this.csn.toJSON();
const existing_actors = actors_per_state[state] || [];
if (existing_actors.includes(actor)) {
return;
}
const new_actors_per_state = converse.CHAT_STATES.reduce((out, s) => {
if (s === state) {
out[s] = [...existing_actors, actor];
} else {
out[s] = (actors_per_state[s] || []).filter(a => a !== actor);
}
return out;
}, {});
this.csn.set(new_actors_per_state);
window.setTimeout(() => this.removeCSNFor(actor, state), 10000);
},
/** /**
* Handler for all MUC messages sent to this groupchat. This method * Handler for all MUC messages sent to this groupchat. This method
* shouldn't be called directly, instead {@link _converse.ChatRoom#queueMessage} * shouldn't be called directly, instead {@link _converse.ChatRoom#queueMessage}
...@@ -1865,7 +1896,9 @@ converse.plugins.add('converse-muc', { ...@@ -1865,7 +1896,9 @@ converse.plugins.add('converse-muc', {
} }
this.setEditable(attrs, attrs.time); this.setEditable(attrs, attrs.time);
if (u.shouldCreateGroupchatMessage(attrs)) { if (attrs['chat_state']) {
this.updateCSN(attrs);
} else if (u.shouldCreateGroupchatMessage(attrs)) {
const msg = this.handleCorrection(attrs) || await this.createMessage(attrs); const msg = this.handleCorrection(attrs) || await this.createMessage(attrs);
this.incrementUnreadMsgCounter(msg); this.incrementUnreadMsgCounter(msg);
} }
......
...@@ -124,8 +124,7 @@ u.isNewMessage = function (message) { ...@@ -124,8 +124,7 @@ u.isNewMessage = function (message) {
}; };
u.shouldCreateMessage = function (attrs) { u.shouldCreateMessage = function (attrs) {
return attrs['chat_state'] || return attrs['retracted'] || // Retraction received *before* the message
attrs['retracted'] || // Retraction received *before* the message
!u.isEmptyMessage(attrs); !u.isEmptyMessage(attrs);
} }
......
<div class="chat-area col">
<div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}" aria-live="polite">
{[ if (o.muc_show_logs_before_join) { ]}
<div class="empty-history-feedback"><span>{{{o.__('No message history available.')}}}</span></div>
{[ } ]}
</div>
<div class="bottom-panel"></div>
</div>
...@@ -4,9 +4,8 @@ export default (o) => html` ...@@ -4,9 +4,8 @@ export default (o) => html`
<div class="flyout box-flyout"> <div class="flyout box-flyout">
<div class="chat-head chat-head-chatbox row no-gutters"></div> <div class="chat-head chat-head-chatbox row no-gutters"></div>
<div class="chat-body"> <div class="chat-body">
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" <div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" @scroll=${o.markScrolled} aria-live="polite"></div>
@scroll=${o.markScrolled} <div class="chat-state-notifications"></div>
aria-live="polite"></div>
<div class="bottom-panel"> <div class="bottom-panel">
<div class="emoji-picker__container dropup"></div> <div class="emoji-picker__container dropup"></div>
<div class="message-form-container"> <div class="message-form-container">
......
import { html } from "lit-html"; import { html } from "lit-html";
import { __ } from '@converse/headless/i18n';
const i18n_no_history = __('No message history available.');
export default () => html`
export default (o) => html`
<div class="flyout box-flyout"> <div class="flyout box-flyout">
<div class="chat-head chat-head-chatroom row no-gutters"></div> <div class="chat-head chat-head-chatroom row no-gutters"></div>
<div class="chat-body chatroom-body row no-gutters"> <div class="chat-body chatroom-body row no-gutters">
<div class="chat-area col">
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
${ o.muc_show_logs_before_join ? html`<div class="empty-history-feedback"><span>${ i18n_no_history }</span></div>` : '' }
</div>
<div class="chat-state-notifications"></div>
<div class="bottom-panel"></div>
</div>
<div class="disconnect-container hidden"></div> <div class="disconnect-container hidden"></div>
</div> </div>
</div> </div>
......
<div class="message chat-info chat-state-notification"
data-isodate="{{{o.isodate}}}"
data-csn="{{{o.from}}}">{{{o.message}}}</div>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment