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:
parent
14f0ed43c5
commit
dc711d494f
@ -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
|
||||
|
||||
|
2
Makefile
2
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
|
||||
|
@ -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
1
package-lock.json
generated
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
14
src/headless/plugins/mam/placeholder.js
Normal file
14
src/headless/plugins/mam/placeholder.js
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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': "",
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
33
src/plugins/mam-views/placeholder.js
Normal file
33
src/plugins/mam-views/placeholder.js
Normal 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);
|
31
src/plugins/mam-views/styles/placeholder.scss
Normal file
31
src/plugins/mam-views/styles/placeholder.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
10
src/plugins/mam-views/templates/placeholder.js
Normal file
10
src/plugins/mam-views/templates/placeholder.js
Normal 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>`;
|
||||
}
|
@ -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');
|
||||
|
||||
|
219
src/plugins/mam-views/tests/placeholder.js
Normal file
219
src/plugins/mam-views/tests/placeholder.js
Normal 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();
|
||||
}));
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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()) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 || ''}"/>`
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user