Add a placeholder to indicate a gap in the message history

The user can click the placeholder to fill in the gap.
This commit is contained in:
JC Brand 2021-06-15 12:17:02 +02:00
parent 14f0ed43c5
commit dc711d494f
24 changed files with 458 additions and 62 deletions

View File

@ -26,6 +26,7 @@
- Add support for rendering unfurls via [mod_ogp](https://modules.prosody.im/mod_ogp.html)
- Add a Description Of A Project (DOAP) file
- Add ability to deregister nickname when closing a MUC by setting `auto_register_muc_nickname` to `'unregister'`.
- Show a gap placeholder when there are gaps in the chat history. The user can click these to fill the gaps.
### Breaking Changes

View File

@ -234,4 +234,4 @@ doc: node_modules docsdev apidoc
PHONY: apidoc
apidoc:
$(JSDOC) --private --readme docs/source/jsdoc_intro.md -c docs/source/conf.json -d docs/html/api src/templates/**/*.js src/*.js src/**/*.js src/headless/**/*.js src/shared/**/*.js
$(JSDOC) --private --readme docs/source/jsdoc_intro.md -c docs/source/conf.json -d docs/html/api src/templates/*.js src/*.js src/**/*.js src/headless/**/*.js src/shared/**/*.js

View File

@ -55,6 +55,7 @@ module.exports = function(config) {
{ pattern: "src/plugins/controlbox/tests/login.js", type: 'module' },
{ pattern: "src/plugins/headlines-view/tests/headline.js", type: 'module' },
{ pattern: "src/plugins/mam-views/tests/mam.js", type: 'module' },
{ pattern: "src/plugins/mam-views/tests/placeholder.js", type: 'module' },
{ pattern: "src/plugins/minimize/tests/minchats.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/autocomplete.js", type: 'module' },
{ pattern: "src/plugins/muc-views/tests/component.js", type: 'module' },

1
package-lock.json generated
View File

@ -2941,6 +2941,7 @@
"dev": true,
"requires": {
"@converse/skeletor": "github:conversejs/skeletor#f354bc530493a17d031f6f9c524cc34e073908e3",
"dayjs": "1.10.4",
"filesize": "^6.1.0",
"localforage": "^1.9.0",
"localforage-driver-memory": "^1.0.5",

View File

@ -1,4 +1,5 @@
import ModelWithContact from './model-with-contact.js';
import dayjs from 'dayjs';
import log from '../../log.js';
import { _converse, api, converse } from '../../core.js';
import { getOpenPromise } from '@converse/openpromise';
@ -102,10 +103,52 @@ const MessageMixin = {
}
},
/**
* Returns a boolean indicating whether this message is ephemeral,
* meaning it will get automatically removed after ten seconds.
* @returns { boolean }
*/
isEphemeral () {
return this.get('is_ephemeral');
},
/**
* Returns a boolean indicating whether this message is a XEP-0245 /me command.
* @returns { boolean }
*/
isMeCommand () {
const text = this.getMessageText();
if (!text) {
return false;
}
return text.startsWith('/me ');
},
/**
* Returns a boolean indicating whether this message is considered a followup
* message from the previous one. Followup messages are shown grouped together
* under one author heading.
* A message is considered a followup of it's predecessor when it's a chat
* message from the same author, within 10 minutes.
* @returns { boolean }
*/
isFollowup () {
const messages = this.collection.models;
const idx = messages.indexOf(this);
const prev_model = idx ? messages[idx-1] : null;
if (prev_model === null) {
return false;
}
const date = dayjs(this.get('time'));
return this.get('from') === prev_model.get('from') &&
!this.isMeCommand() &&
!prev_model.isMeCommand() &&
this.get('type') !== 'info' &&
prev_model.get('type') !== 'info' &&
date.isBefore(dayjs(prev_model.get('time')).add(10, 'minutes')) &&
!!this.get('is_encrypted') === !!prev_model.get('is_encrypted');
},
getDisplayName () {
if (this.get('type') === 'groupchat') {
return this.get('nick');
@ -126,14 +169,6 @@ const MessageMixin = {
return this.get('message');
},
isMeCommand () {
const text = this.getMessageText();
if (!text) {
return false;
}
return text.startsWith('/me ');
},
/**
* Send out an IQ stanza to request a file upload slot.
* https://xmpp.org/extensions/xep-0363.html#request

View File

@ -3,8 +3,9 @@
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import mam_api from './api.js';
import '../disco/index.js';
import MAMPlaceholderMessage from './placeholder.js';
import mam_api from './api.js';
import {
onMAMError,
onMAMPreferences,
@ -31,7 +32,7 @@ converse.plugins.add('converse-mam', {
Object.assign(api, mam_api);
// This is mainly done to aid with tests
Object.assign(_converse, { onMAMError, onMAMPreferences, handleMAMResult });
Object.assign(_converse, { onMAMError, onMAMPreferences, handleMAMResult, MAMPlaceholderMessage });
/************************ Event Handlers ************************/
api.listen.on('addClientFeatures', () => api.disco.own.features.add(NS.MAM));

View File

@ -0,0 +1,14 @@
import { Model } from '@converse/skeletor/src/model.js';
import { converse } from '../../core.js';
const u = converse.env.utils;
export default class MAMPlaceholderMessage extends Model {
defaults () { // eslint-disable-line class-methods-use-this
return {
'msgid': u.getUniqueId(),
'is_ephemeral': false
};
}
}

View File

@ -1,8 +1,9 @@
import MAMPlaceholderMessage from './placeholder.js';
import log from '@converse/headless/log';
import sizzle from 'sizzle';
import { _converse, api, converse } from '@converse/headless/core';
import { parseMUCMessage } from '@converse/headless/plugins/muc/parsers';
import { parseMessage } from '@converse/headless/plugins/chat/parsers';
import { _converse, api, converse } from '@converse/headless/core';
const { Strophe, $iq } = converse.env;
const { NS } = Strophe;
@ -97,8 +98,8 @@ export async function handleMAMResult (model, result, query, options, should_pag
}
/**
* Fetch XEP-0313 archived messages based on the passed in criteria.
* @param { Object } options
* @typedef { Object } MAMOptions
* A map of MAM related options that may be passed to fetchArchivedMessages
* @param { integer } [options.max] - The maximum number of items to return.
* Defaults to "archived_messages_page_size"
* @param { string } [options.after] - The XEP-0359 stanza ID of a message
@ -112,10 +113,17 @@ export async function handleMAMResult (model, result, query, options, should_pag
* @param { string } [options.with] - The JID of the entity with
* which messages were exchanged.
* @param { boolean } [options.groupchat] - True if archive in groupchat.
* @param { ('forwards'|'backwards'|null)} [should_page=null] - Determines whether this function should
* recursively page through the entire result set if a limited number of results were returned.
*/
export async function fetchArchivedMessages (model, options = {}, should_page=null) {
/**
* Fetch XEP-0313 archived messages based on the passed in criteria.
* @param { _converse.ChatBox | _converse.ChatRoom } model
* @param { MAMOptions } [options]
* @param { ('forwards'|'backwards'|null)} [should_page=null] - Determines whether
* this function should recursively page through the entire result set if a limited
* number of results were returned.
*/
export async function fetchArchivedMessages (model, options = {}, should_page = null) {
if (model.disable_mam) {
return;
}
@ -146,16 +154,49 @@ export async function fetchArchivedMessages (model, options = {}, should_page=nu
}
return fetchArchivedMessages(model, options, should_page);
} else {
// TODO: Add a special kind of message which will
// render as a link to fetch further messages, either
// to fetch older messages or to fill in a gap.
createPlaceholder(model, options, result);
}
}
}
/**
* Create a placeholder message which is used to indicate gaps in the history.
* @param { _converse.ChatBox | _converse.ChatRoom } model
* @param { MAMOptions } options
* @param { object } result - The RSM result object
*/
async function createPlaceholder (model, options, result) {
if (options.before == '' && (model.messages.length === 0 || !options.start)) {
// Fetching the latest MAM messages with an empty local cache
return;
}
if (options.before && !options.start) {
// Infinite scrolling upward
return;
}
if (options.before == null) { // eslint-disable-line no-eq-null
// Adding placeholders when paging forwards is not supported yet,
// since currently with standard Converse, we only page forwards
// when fetching the entire history (i.e. no gaps should arise).
return;
}
const msgs = await Promise.all(result.messages);
const { rsm } = result;
const key = `stanza_id ${model.get('jid')}`;
const adjacent_message = msgs.find(m => m[key] === rsm.result.first);
const msg_data = {
'template_hook': 'getMessageTemplate',
'time': new Date(new Date(adjacent_message['time']) - 1).toISOString(),
'before': rsm.result.first,
'start': options.start
}
model.messages.add(new MAMPlaceholderMessage(msg_data));
}
/**
* Fetches messages that might have been archived *after*
* the last archived message in our local cache.
* @param { _converse.ChatBox | _converse.ChatRoom }
*/
export function fetchNewestMessages (model) {
if (model.disable_mam) {

View File

@ -386,7 +386,7 @@ describe("XEP-0363: HTTP File Upload", function () {
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
var file = {
const file = {
'type': 'image/jpeg',
'size': '5242881',
'lastModifiedDate': "",

View File

@ -741,7 +741,6 @@ describe("A Chat Message", function () {
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false);
expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true);
expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
"Another message 3 minutes later");
@ -1188,7 +1187,6 @@ describe("A Chat Message", function () {
}));
});
it("will cause the chat area to be scrolled down only if it was at the bottom originally",
mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {

View File

@ -3,8 +3,9 @@
* @copyright 2021, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import './placeholder.js';
import { api, converse } from '@converse/headless/core';
import { fetchMessagesOnScrollUp } from './utils.js';
import { fetchMessagesOnScrollUp, getPlaceholderTemplate } from './utils.js';
converse.plugins.add('converse-mam-views', {
@ -12,5 +13,6 @@ converse.plugins.add('converse-mam-views', {
initialize () {
api.listen.on('chatBoxScrolledUp', fetchMessagesOnScrollUp);
api.listen.on('getMessageTemplate', getPlaceholderTemplate);
}
});

View File

@ -0,0 +1,33 @@
import { CustomElement } from 'shared/components/element.js';
import tpl_placeholder from './templates/placeholder.js';
import { api } from "@converse/headless/core";
import { fetchArchivedMessages } from '@converse/headless/plugins/mam/utils.js';
import './styles/placeholder.scss';
class Placeholder extends CustomElement {
static get properties () {
return {
'model': { type: Object }
}
}
render () {
return tpl_placeholder(this);
}
async fetchMissingMessages (ev) {
ev?.preventDefault?.();
this.model.set('fetching', true);
const options = {
'before': this.model.get('before'),
'start': this.model.get('start')
}
await fetchArchivedMessages(this.model.collection.chatbox, options);
this.model.destroy();
}
}
api.elements.define('converse-mam-placeholder', Placeholder);

View File

@ -0,0 +1,31 @@
converse-mam-placeholder {
.mam-placeholder {
position: relative;
height: 2em;
margin: 0.5em 0;
&:before,
&:after {
content: "";
display: block;
position: absolute;
left: 0;
right: 0;
}
&:before {
height: 1em;
top: 1em;
background: linear-gradient(-135deg, lightgray 0.5em, transparent 0) 0 0.5em, linear-gradient( 135deg, lightgray 0.5em, transparent 0) 0 0.5em;
background-position: top left;
background-repeat: repeat-x;
background-size: 1em 1em;
}
&:after {
height: 1em;
top: 0.75em;
background: linear-gradient(-135deg, var(--chat-background-color) 0.5em, transparent 0) 0 0.5em, linear-gradient( 135deg, var(--chat-background-color) 0.5em, transparent 0) 0 0.5em;
background-position: top left;
background-repeat: repeat-x;
background-size: 1em 1em;
}
}
}

View File

@ -0,0 +1,10 @@
import tpl_spinner from 'templates/spinner.js';
import { __ } from 'i18n';
import { html } from 'lit-html';
export default (el) => {
return el.model.get('fetching') ? tpl_spinner({'classes': 'hor_centered'}) :
html`<a @click="${(ev) => el.fetchMissingMessages(ev)}" title="${__('Click to load missing messages')}">
<div class="message mam-placeholder"></div>
</a>`;
}

View File

@ -18,7 +18,6 @@ describe("Message Archive Management", function () {
describe("The XEP-0313 Archive", function () {
it("is queried when the user scrolls up",
mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2}, async function (done, _converse) {
@ -920,6 +919,7 @@ describe("Message Archive Management", function () {
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
const promise = _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'});
await u.waitUntil(() => sent_stanza);
const queryid = sent_stanza.querySelector('query').getAttribute('queryid');

View File

@ -0,0 +1,219 @@
/*global mock, converse */
const { Strophe, u } = converse.env;
describe("Message Archive Management", function () {
describe("A placeholder message", function () {
it("is created to indicate a gap in the history",
mock.initConverse(
['discoInitialized'],
{
'archived_messages_page_size': 2,
'persistent_store': 'localStorage',
'mam_request_all_pages': false
},
async function (done, _converse) {
const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'orchard@chat.shakespeare.lit';
const msgid = u.getUniqueId();
// We put an already cached message in localStorage
const key_prefix = `converse-test-persistent/${_converse.bare_jid}`;
let key = `${key_prefix}/converse.messages-${muc_jid}-${_converse.bare_jid}`;
localStorage.setItem(key, `["converse.messages-${muc_jid}-${_converse.bare_jid}-${msgid}"]`);
key = `${key_prefix}/converse.messages-${muc_jid}-${_converse.bare_jid}-${msgid}`;
const msgtxt = "existing cached message";
localStorage.setItem(key, `{
"body": "${msgtxt}",
"message": "${msgtxt}",
"editable":true,
"from": "${muc_jid}/romeo",
"fullname": "Romeo",
"id": "${msgid}",
"is_archived": false,
"is_only_emojis": false,
"nick": "jc",
"origin_id": "${msgid}",
"received": "2021-06-15T11:17:15.451Z",
"sender": "me",
"stanza_id ${muc_jid}": "1e1c2355-c5b8-4d48-9e33-1310724578c2",
"time": "2021-06-15T11:17:15.424Z",
"type": "groupchat",
"msgid": "${msgid}"
}`);
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid);
let iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop());
const first_msg_id = _converse.connection.getUniqueId();
const second_msg_id = _converse.connection.getUniqueId();
const third_msg_id = _converse.connection.getUniqueId();
let message = u.toStanza(
`<message xmlns="jabber:client"
to="romeo@montague.lit/orchard"
from="${muc_jid}">
<result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${second_msg_id}">
<forwarded xmlns="urn:xmpp:forward:0">
<delay xmlns="urn:xmpp:delay" stamp="2021-06-15T11:18:23Z"/>
<message from="${muc_jid}/some1" type="groupchat">
<body>2nd MAM Message</body>
</message>
</forwarded>
</result>
</message>`);
_converse.connection._dataRecv(mock.createRequest(message));
message = u.toStanza(
`<message xmlns="jabber:client"
to="romeo@montague.lit/orchard"
from="${muc_jid}">
<result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${third_msg_id}">
<forwarded xmlns="urn:xmpp:forward:0">
<delay xmlns="urn:xmpp:delay" stamp="2021-06-15T12:16:23Z"/>
<message from="${muc_jid}/some1" type="groupchat">
<body>3rd MAM Message</body>
</message>
</forwarded>
</result>
</message>`);
_converse.connection._dataRecv(mock.createRequest(message));
// Clear so that we don't match the older query
while (sent_IQs.length) { sent_IQs.pop(); }
let result = u.toStanza(
`<iq type='result' id='${iq_get.getAttribute('id')}'>
<fin xmlns='urn:xmpp:mam:2'>
<set xmlns='http://jabber.org/protocol/rsm'>
<first index='0'>${second_msg_id}</first>
<last>${third_msg_id}</last>
<count>3</count>
</set>
</fin>
</iq>`);
_converse.connection._dataRecv(mock.createRequest(result));
await u.waitUntil(() => view.model.messages.length === 4);
const msg = view.model.messages.at(1);
expect(msg instanceof _converse.MAMPlaceholderMessage).toBe(true);
expect(msg.get('time')).toBe('2021-06-15T11:18:22.999Z');
const placeholder_el = view.querySelector('converse-mam-placeholder');
placeholder_el.firstElementChild.click();
await u.waitUntil(() => view.querySelector('converse-mam-placeholder .spinner'));
iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop());
expect(Strophe.serialize(iq_get)).toBe(
`<iq id="${iq_get.getAttribute('id')}" to="${muc_jid}" type="set" xmlns="jabber:client">`+
`<query queryid="${iq_get.querySelector('query').getAttribute('queryid')}" xmlns="urn:xmpp:mam:2">`+
`<x type="submit" xmlns="jabber:x:data">`+
`<field type="hidden" var="FORM_TYPE"><value>urn:xmpp:mam:2</value></field>`+
`<field var="start"><value>2021-06-15T11:17:15.424Z</value></field>`+
`</x>`+
`<set xmlns="http://jabber.org/protocol/rsm"><before>${view.model.messages.at(2).get(`stanza_id ${muc_jid}`)}</before>`+
`<max>2</max>`+
`</set>`+
`</query>`+
`</iq>`);
message = u.toStanza(
`<message xmlns="jabber:client"
to="romeo@montague.lit/orchard"
from="${muc_jid}">
<result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${first_msg_id}">
<forwarded xmlns="urn:xmpp:forward:0">
<delay xmlns="urn:xmpp:delay" stamp="2021-06-15T11:18:20Z"/>
<message from="${muc_jid}/some1" type="groupchat">
<body>1st MAM Message</body>
</message>
</forwarded>
</result>
</message>`);
_converse.connection._dataRecv(mock.createRequest(message));
// Clear so that we don't match the older query
while (sent_IQs.length) { sent_IQs.pop(); }
result = u.toStanza(
`<iq type='result' id='${iq_get.getAttribute('id')}'>
<fin xmlns='urn:xmpp:mam:2' complete='true'>
<set xmlns='http://jabber.org/protocol/rsm'>
<first index='0'>${first_msg_id}</first>
<last>${first_msg_id}</last>
<count>1</count>
</set>
</fin>
</iq>`);
_converse.connection._dataRecv(mock.createRequest(result));
await u.waitUntil(() => view.model.messages.length === 4);
await u.waitUntil(() => view.querySelector('converse-mam-placeholder') === null);
done();
}));
it("is not created when there isn't a gap because the cached history is empty",
mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2},
async function (done, _converse) {
const sent_IQs = _converse.connection.IQ_stanzas;
const muc_jid = 'orchard@chat.shakespeare.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid);
const iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop());
const first_msg_id = _converse.connection.getUniqueId();
const last_msg_id = _converse.connection.getUniqueId();
let message = u.toStanza(
`<message xmlns="jabber:client"
to="romeo@montague.lit/orchard"
from="${muc_jid}">
<result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${first_msg_id}">
<forwarded xmlns="urn:xmpp:forward:0">
<delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:15:23Z"/>
<message from="${muc_jid}/some1" type="groupchat">
<body>2nd Message</body>
</message>
</forwarded>
</result>
</message>`);
_converse.connection._dataRecv(mock.createRequest(message));
message = u.toStanza(
`<message xmlns="jabber:client"
to="romeo@montague.lit/orchard"
from="${muc_jid}">
<result xmlns="urn:xmpp:mam:2" queryid="${iq_get.querySelector('query').getAttribute('queryid')}" id="${last_msg_id}">
<forwarded xmlns="urn:xmpp:forward:0">
<delay xmlns="urn:xmpp:delay" stamp="2018-01-09T06:16:23Z"/>
<message from="${muc_jid}/some1" type="groupchat">
<body>3rd Message</body>
</message>
</forwarded>
</result>
</message>`);
_converse.connection._dataRecv(mock.createRequest(message));
// Clear so that we don't match the older query
while (sent_IQs.length) { sent_IQs.pop(); }
const result = u.toStanza(
`<iq type='result' id='${iq_get.getAttribute('id')}'>
<fin xmlns='urn:xmpp:mam:2'>
<set xmlns='http://jabber.org/protocol/rsm'>
<first index='0'>${first_msg_id}</first>
<last>${last_msg_id}</last>
<count>3</count>
</set>
</fin>
</iq>`);
_converse.connection._dataRecv(mock.createRequest(result));
await u.waitUntil(() => view.model.messages.length === 2);
expect(true).toBe(true);
done();
}));
});
});

View File

@ -1,5 +1,16 @@
import { fetchArchivedMessages } from '@converse/headless/plugins/mam/utils';
import MAMPlaceholderMessage from '@converse/headless/plugins/mam/placeholder.js';
import { _converse, api } from '@converse/headless/core';
import { fetchArchivedMessages } from '@converse/headless/plugins/mam/utils';
import { html } from 'lit-html';
export function getPlaceholderTemplate (message, tpl) {
if (message instanceof MAMPlaceholderMessage) {
return html`<converse-mam-placeholder .model=${message}></converse-mam-placeholder>`;
} else {
return tpl;
}
}
export async function fetchMessagesOnScrollUp (view) {
if (view.model.messages.length) {
@ -17,7 +28,6 @@ export async function fetchMessagesOnScrollUp (view) {
if (api.settings.get('allow_url_history_change')) {
_converse.router.history.navigate(`#${oldest_message.get('msgid')}`);
}
setTimeout(() => view.model.ui.set('chat-content-spinner-top', false), 250);
}
}

View File

@ -4,6 +4,7 @@ import { api } from "@converse/headless/core";
import { getDayIndicator } from './utils.js';
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { until } from 'lit/directives/until.js';
export default class MessageHistory extends CustomElement {
@ -17,20 +18,28 @@ export default class MessageHistory extends CustomElement {
render () {
const msgs = this.messages;
return msgs.length ? html`${repeat(msgs, m => m.get('id'), m => this.renderMessage(m)) }` : '';
if (msgs.length) {
return repeat(msgs, m => m.get('id'), m => html`${this.renderMessage(m)}`)
} else {
return '';
}
}
renderMessage (model) {
if (model.get('dangling_retraction') || model.get('is_only_key')) {
return '';
}
const day = getDayIndicator(model);
const templates = day ? [day] : [];
const message = html`<converse-chat-message
jid="${this.model.get('jid')}"
mid="${model.get('id')}"></converse-chat-message>`
return [...templates, message];
const template_hook = model.get('template_hook')
if (typeof template_hook === 'string') {
const template_promise = api.hook(template_hook, model, '');
return until(template_promise, '');
} else {
const template = html`<converse-chat-message
jid="${this.model.get('jid')}"
mid="${model.get('id')}"></converse-chat-message>`
const day = getDayIndicator(model);
return day ? [day, template] : template;
}
}
}

View File

@ -5,7 +5,6 @@ import 'shared/registry';
import MessageVersionsModal from 'modals/message-versions.js';
import OccupantModal from 'modals/occupant.js';
import UserDetailsModal from 'modals/user-details.js';
import dayjs from 'dayjs';
import filesize from 'filesize';
import tpl_message from './templates/message.js';
import tpl_spinner from 'templates/spinner.js';
@ -16,7 +15,7 @@ import { getHats } from './utils.js';
import { html } from 'lit';
import { renderAvatar } from 'shared/directives/avatar';
const { Strophe } = converse.env;
const { Strophe, dayjs } = converse.env;
const u = converse.env.utils;
@ -137,23 +136,6 @@ export default class Message extends CustomElement {
this.parentElement.removeChild(this);
}
isFollowup () {
const messages = this.model.collection.models;
const idx = messages.indexOf(this.model);
const prev_model = idx ? messages[idx-1] : null;
if (prev_model === null) {
return false;
}
const date = dayjs(this.model.get('time'));
return this.model.get('from') === prev_model.get('from') &&
!this.model.isMeCommand() &&
!prev_model.isMeCommand() &&
this.model.get('type') !== 'info' &&
prev_model.get('type') !== 'info' &&
date.isBefore(dayjs(prev_model.get('time')).add(10, 'minutes')) &&
!!this.model.get('is_encrypted') === !!prev_model.get('is_encrypted');
}
isRetracted () {
return this.model.get('retracted') || this.model.get('moderated') === 'retracted';
}
@ -173,7 +155,7 @@ export default class Message extends CustomElement {
getExtraMessageClasses () {
const extra_classes = [
this.isFollowup() ? 'chat-msg--followup' : null,
this.model.isFollowup() ? 'chat-msg--followup' : null,
this.model.get('is_delayed') ? 'delayed' : null,
this.model.isMeCommand() ? 'chat-msg--action' : null,
this.isRetracted() ? 'chat-msg--retracted' : null,

View File

@ -1,6 +1,7 @@
import { _converse, api } from '@converse/headless/core';
import dayjs from 'dayjs';
import tpl_new_day from "./templates/new-day.js";
import { _converse, api, converse } from '@converse/headless/core';
const { dayjs } = converse.env;
export function onScrolledDown (model) {
if (!model.isHidden()) {

View File

@ -1,5 +1,4 @@
/**
* @module icons.js
* @copyright Alfredo Medrano Sánchez and the Converse.js contributors
* @description
* Component inspired by the one from fa-icons

View File

@ -366,6 +366,9 @@
}
}
.spinner__container {
width: 100%;
}
.spinner {
animation: spin 2s infinite, linear;
width: 1em;
@ -386,9 +389,8 @@
margin: auto;
}
.hor_centered {
width: 100%;
text-align: center;
display: block;
display: block !important;
margin: 0 auto;
clear: both;
}

View File

@ -1,3 +1,9 @@
import { html } from "lit";
export default (o={}) => html`<span class="spinner fa fa-spinner centered ${o.classes || ''}"/>`
export default (o={}) => {
if (o.classes?.includes('hor_centered')) {
return html`<div class="spinner__container"><span class="spinner fa fa-spinner centered ${o.classes || ''}"/></div>`
} else {
return html`<span class="spinner fa fa-spinner centered ${o.classes || ''}"/>`
}
}

View File

@ -31,7 +31,7 @@
modtools_disable_query: ['moderator', 'participant', 'visitor'],
enable_smacks: true,
// connection_options: { 'worker': '/dist/shared-connection-worker.js' },
persistent_store: 'IndexedDB',
// persistent_store: 'IndexedDB',
message_archiving: 'always',
muc_domain: 'conference.chat.example.org',
muc_respect_autojoin: true,