From dc711d494f9fb6fbe6d1f520f3cef8a609f18910 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 15 Jun 2021 12:17:02 +0200 Subject: [PATCH] Add a placeholder to indicate a gap in the message history The user can click the placeholder to fill in the gap. --- CHANGES.md | 1 + Makefile | 2 +- karma.conf.js | 1 + package-lock.json | 1 + src/headless/plugins/chat/message.js | 51 +++- src/headless/plugins/mam/index.js | 5 +- src/headless/plugins/mam/placeholder.js | 14 ++ src/headless/plugins/mam/utils.js | 59 ++++- .../chatview/tests/http-file-upload.js | 2 +- src/plugins/chatview/tests/messages.js | 2 - src/plugins/mam-views/index.js | 4 +- src/plugins/mam-views/placeholder.js | 33 +++ src/plugins/mam-views/styles/placeholder.scss | 31 +++ .../mam-views/templates/placeholder.js | 10 + src/plugins/mam-views/tests/mam.js | 2 +- src/plugins/mam-views/tests/placeholder.js | 219 ++++++++++++++++++ src/plugins/mam-views/utils.js | 14 +- src/shared/chat/message-history.js | 25 +- src/shared/chat/message.js | 22 +- src/shared/chat/utils.js | 5 +- src/shared/components/icons.js | 1 - src/shared/styles/_core.scss | 6 +- src/templates/spinner.js | 8 +- webpack.html | 2 +- 24 files changed, 458 insertions(+), 62 deletions(-) create mode 100644 src/headless/plugins/mam/placeholder.js create mode 100644 src/plugins/mam-views/placeholder.js create mode 100644 src/plugins/mam-views/styles/placeholder.scss create mode 100644 src/plugins/mam-views/templates/placeholder.js create mode 100644 src/plugins/mam-views/tests/placeholder.js diff --git a/CHANGES.md b/CHANGES.md index f650fdeb7..4dddfb558 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/Makefile b/Makefile index 1d3bb4716..e4df03ff1 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/karma.conf.js b/karma.conf.js index 5a58e332e..11aa210fa 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -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' }, diff --git a/package-lock.json b/package-lock.json index 63007be5a..f197704c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/src/headless/plugins/chat/message.js b/src/headless/plugins/chat/message.js index f805bc9e2..ed6830647 100644 --- a/src/headless/plugins/chat/message.js +++ b/src/headless/plugins/chat/message.js @@ -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 diff --git a/src/headless/plugins/mam/index.js b/src/headless/plugins/mam/index.js index 4c93b528b..e0246a3d0 100644 --- a/src/headless/plugins/mam/index.js +++ b/src/headless/plugins/mam/index.js @@ -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)); diff --git a/src/headless/plugins/mam/placeholder.js b/src/headless/plugins/mam/placeholder.js new file mode 100644 index 000000000..ba046ab6c --- /dev/null +++ b/src/headless/plugins/mam/placeholder.js @@ -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 + }; + } +} diff --git a/src/headless/plugins/mam/utils.js b/src/headless/plugins/mam/utils.js index c4e73c70e..d739ccbdf 100644 --- a/src/headless/plugins/mam/utils.js +++ b/src/headless/plugins/mam/utils.js @@ -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) { diff --git a/src/plugins/chatview/tests/http-file-upload.js b/src/plugins/chatview/tests/http-file-upload.js index ce57bfd31..10ab54b91 100644 --- a/src/plugins/chatview/tests/http-file-upload.js +++ b/src/plugins/chatview/tests/http-file-upload.js @@ -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': "", diff --git a/src/plugins/chatview/tests/messages.js b/src/plugins/chatview/tests/messages.js index b48d0a7fc..444f940e9 100644 --- a/src/plugins/chatview/tests/messages.js +++ b/src/plugins/chatview/tests/messages.js @@ -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) { diff --git a/src/plugins/mam-views/index.js b/src/plugins/mam-views/index.js index 45a79e520..13359f921 100644 --- a/src/plugins/mam-views/index.js +++ b/src/plugins/mam-views/index.js @@ -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); } }); diff --git a/src/plugins/mam-views/placeholder.js b/src/plugins/mam-views/placeholder.js new file mode 100644 index 000000000..f94da2171 --- /dev/null +++ b/src/plugins/mam-views/placeholder.js @@ -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); diff --git a/src/plugins/mam-views/styles/placeholder.scss b/src/plugins/mam-views/styles/placeholder.scss new file mode 100644 index 000000000..c0adf865c --- /dev/null +++ b/src/plugins/mam-views/styles/placeholder.scss @@ -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; + } + } +} diff --git a/src/plugins/mam-views/templates/placeholder.js b/src/plugins/mam-views/templates/placeholder.js new file mode 100644 index 000000000..5d1a54d56 --- /dev/null +++ b/src/plugins/mam-views/templates/placeholder.js @@ -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` +
+
`; +} diff --git a/src/plugins/mam-views/tests/mam.js b/src/plugins/mam-views/tests/mam.js index 747849838..200a5e63f 100644 --- a/src/plugins/mam-views/tests/mam.js +++ b/src/plugins/mam-views/tests/mam.js @@ -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'); diff --git a/src/plugins/mam-views/tests/placeholder.js b/src/plugins/mam-views/tests/placeholder.js new file mode 100644 index 000000000..d0964a9b2 --- /dev/null +++ b/src/plugins/mam-views/tests/placeholder.js @@ -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( + ` + + + + + 2nd MAM Message + + + + `); + _converse.connection._dataRecv(mock.createRequest(message)); + + message = u.toStanza( + ` + + + + + 3rd MAM 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( + ` + + + ${second_msg_id} + ${third_msg_id} + 3 + + + `); + _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( + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + `2021-06-15T11:17:15.424Z`+ + ``+ + `${view.model.messages.at(2).get(`stanza_id ${muc_jid}`)}`+ + `2`+ + ``+ + ``+ + ``); + + message = u.toStanza( + ` + + + + + 1st MAM 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( + ` + + + ${first_msg_id} + ${first_msg_id} + 1 + + + `); + _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( + ` + + + + + 2nd Message + + + + `); + _converse.connection._dataRecv(mock.createRequest(message)); + + message = u.toStanza( + ` + + + + + 3rd 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( + ` + + + ${first_msg_id} + ${last_msg_id} + 3 + + + `); + _converse.connection._dataRecv(mock.createRequest(result)); + await u.waitUntil(() => view.model.messages.length === 2); + expect(true).toBe(true); + done(); + })); + }); +}); diff --git a/src/plugins/mam-views/utils.js b/src/plugins/mam-views/utils.js index 335e69551..fb03792d8 100644 --- a/src/plugins/mam-views/utils.js +++ b/src/plugins/mam-views/utils.js @@ -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``; + } 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); } } diff --git a/src/shared/chat/message-history.js b/src/shared/chat/message-history.js index 1b5e1dd90..65093272c 100644 --- a/src/shared/chat/message-history.js +++ b/src/shared/chat/message-history.js @@ -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`` - - 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`` + const day = getDayIndicator(model); + return day ? [day, template] : template; + } } } diff --git a/src/shared/chat/message.js b/src/shared/chat/message.js index e6664a1a3..a3702300a 100644 --- a/src/shared/chat/message.js +++ b/src/shared/chat/message.js @@ -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, diff --git a/src/shared/chat/utils.js b/src/shared/chat/utils.js index ebdd7bc99..5ff3b193d 100644 --- a/src/shared/chat/utils.js +++ b/src/shared/chat/utils.js @@ -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()) { diff --git a/src/shared/components/icons.js b/src/shared/components/icons.js index 8877e6ba9..9fcbad67c 100644 --- a/src/shared/components/icons.js +++ b/src/shared/components/icons.js @@ -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 diff --git a/src/shared/styles/_core.scss b/src/shared/styles/_core.scss index c0d484b82..23dd5c856 100644 --- a/src/shared/styles/_core.scss +++ b/src/shared/styles/_core.scss @@ -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; } diff --git a/src/templates/spinner.js b/src/templates/spinner.js index 491befc06..e26a5eec8 100644 --- a/src/templates/spinner.js +++ b/src/templates/spinner.js @@ -1,3 +1,9 @@ import { html } from "lit"; -export default (o={}) => html`` +export default (o={}) => { + if (o.classes?.includes('hor_centered')) { + return html`
` + } else { + return html`` + } +} diff --git a/webpack.html b/webpack.html index 0f79a1cb3..08c2fe1b8 100644 --- a/webpack.html +++ b/webpack.html @@ -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,