Commit e3ebde97 authored by JC Brand's avatar JC Brand

Move converse-chat plugin into folder

parent 01e03fc6
......@@ -4482,9 +4482,9 @@
}
},
"@octokit/openapi-types": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-1.2.2.tgz",
"integrity": "sha512-vrKDLd/Rq4IE16oT+jJkDBx0r29NFkdkU8GwqVSP4RajsAvP23CMGtFhVK0pedUhAiMvG1bGnFcTC/xCKaKgmw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.0.tgz",
"integrity": "sha512-J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnPw==",
"dev": true
},
"@octokit/plugin-enterprise-rest": {
......@@ -4628,12 +4628,12 @@
}
},
"@octokit/types": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.0.3.tgz",
"integrity": "sha512-6y0Emzp+uPpdC5QLzUY1YRklvqiZBMTOz2ByhXdmTFlc3lNv8Mi28dX1U1b4scNtFMUa3tkpjofNFJ5NqMJaZw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.0.tgz",
"integrity": "sha512-bMWBmg77MQTiRkOVyf50qK3QECWOEy43rLy/6fTWZ4HEwAhNfqzMcjiBDZAowkILwTrFvzE1CpP6gD0MuPHS+A==",
"dev": true,
"requires": {
"@octokit/openapi-types": "^1.2.0",
"@octokit/openapi-types": "^2.0.0",
"@types/node": ">= 8"
}
},
......@@ -22471,9 +22471,9 @@
},
"dependencies": {
"ws": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
"integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==",
"optional": true
}
}
......
......@@ -80,7 +80,7 @@ describe("A Chat Message", function () {
await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500);
// Test that pressing the down arrow cancels message correction
expect(textarea.value).toBe('');
await u.waitUntil(() => textarea.value === '')
view.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
......
......@@ -7,7 +7,7 @@ import "./plugins/bookmarks.js"; // XEP-0199 XMPP Ping
import "./plugins/bosh.js"; // XEP-0206 BOSH
import "./plugins/caps.js"; // XEP-0115 Entity Capabilities
import "./plugins/carbons.js"; // XEP-0280 Message Carbons
import "./plugins/chat.js"; // RFC-6121 Instant messaging
import "./plugins/chat/index.js"; // RFC-6121 Instant messaging
import "./plugins/chatboxes.js";
import "./plugins/disco.js"; // XEP-0030 Service discovery
import "./plugins/headlines.js"; // Support for headline messages
......
This diff is collapsed.
import { _converse, api } from "../../core.js";
import log from "../../log.js";
export default {
/**
* The "chats" namespace (used for one-on-one chats)
*
* @namespace api.chats
* @memberOf api
*/
chats: {
/**
* @method api.chats.create
* @param {string|string[]} jid|jids An jid or array of jids
* @param {object} [attrs] An object containing configuration attributes.
*/
async create (jids, attrs) {
if (typeof jids === 'string') {
if (attrs && !attrs?.fullname) {
const contact = await api.contacts.get(jids);
attrs.fullname = contact?.attributes?.fullname;
}
const chatbox = api.chats.get(jids, attrs, true);
if (!chatbox) {
log.error("Could not open chatbox for JID: "+jids);
return;
}
return chatbox;
}
if (Array.isArray(jids)) {
return Promise.all(jids.forEach(async jid => {
const contact = await api.contacts.get(jids);
attrs.fullname = contact?.attributes?.fullname;
return api.chats.get(jid, attrs, true).maybeShow();
}));
}
log.error("chats.create: You need to provide at least one JID");
return null;
},
/**
* Opens a new one-on-one chat.
*
* @method api.chats.open
* @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [attrs.minimized] - Should the chat be created in minimized state.
* @param {Boolean} [force=false] - By default, a minimized
* chat won't be maximized (in `overlayed` view mode) and in
* `fullscreen` view mode a newly opened chat won't replace
* another chat already in the foreground.
* Set `force` to `true` if you want to force the chat to be
* maximized or shown.
* @returns {Promise} Promise which resolves with the
* _converse.ChatBox representing the chat.
*
* @example
* // To open a single chat, provide the JID of the contact you're chatting with in that chat:
* converse.plugins.add('myplugin', {
* initialize: function() {
* const _converse = this._converse;
* // Note, buddy@example.org must be in your contacts roster!
* api.chats.open('buddy@example.com').then(chat => {
* // Now you can do something with the chat model
* });
* }
* });
*
* @example
* // To open an array of chats, provide an array of JIDs:
* converse.plugins.add('myplugin', {
* initialize: function () {
* const _converse = this._converse;
* // Note, these users must first be in your contacts roster!
* api.chats.open(['buddy1@example.com', 'buddy2@example.com']).then(chats => {
* // Now you can do something with the chat models
* });
* }
* });
*/
async open (jids, attrs, force) {
if (typeof jids === 'string') {
const chat = await api.chats.get(jids, attrs, true);
if (chat) {
return chat.maybeShow(force);
}
return chat;
} else if (Array.isArray(jids)) {
return Promise.all(
jids.map(j => api.chats.get(j, attrs, true).then(c => c && c.maybeShow(force)))
.filter(c => c)
);
}
const err_msg = "chats.open: You need to provide at least one JID";
log.error(err_msg);
throw new Error(err_msg);
},
/**
* Retrieves a chat or all chats.
*
* @method api.chats.get
* @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [create=false] - Whether the chat should be created if it's not found.
* @returns { Promise<_converse.ChatBox> }
*
* @example
* // To return a single chat, provide the JID of the contact you're chatting with in that chat:
* const model = await api.chats.get('buddy@example.com');
*
* @example
* // To return an array of chats, provide an array of JIDs:
* const models = await api.chats.get(['buddy1@example.com', 'buddy2@example.com']);
*
* @example
* // To return all open chats, call the method without any parameters::
* const models = await api.chats.get();
*
*/
async get (jids, attrs={}, create=false) {
async function _get (jid) {
let model = await api.chatboxes.get(jid);
if (!model && create) {
model = await api.chatboxes.create(jid, attrs, _converse.ChatBox);
} else {
model = (model && model.get('type') === _converse.PRIVATE_CHAT_TYPE) ? model : null;
if (model && Object.keys(attrs).length) {
model.save(attrs);
}
}
return model;
}
if (jids === undefined) {
const chats = await api.chatboxes.get();
return chats.filter(c => (c.get('type') === _converse.PRIVATE_CHAT_TYPE));
} else if (typeof jids === 'string') {
return _get(jids);
}
return Promise.all(jids.map(jid => _get(jid)));
}
}
}
/**
* @module converse-chat
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import ChatBox from './model.js';
import MessageMixin from './message.js';
import ModelWithContact from './model-with-contact.js';
import chat_api from './api.js';
import log from '../../log.js';
import st from '../../utils/stanza';
import { Collection } from "@converse/skeletor/src/collection";
import { _converse, api, converse } from '../../core.js';
const { Strophe, sizzle, utils } = converse.env;
const u = converse.env.utils;
async function handleErrorMessage (stanza) {
const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
if (utils.isSameBareJID(from_jid, _converse.bare_jid)) {
return;
}
const chatbox = await api.chatboxes.get(from_jid);
chatbox?.handleErrorMessageStanza(stanza);
}
converse.plugins.add('converse-chat', {
/* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are called "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* It's possible however to make optional dependencies non-optional.
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ['converse-chatboxes', 'converse-disco'],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
Object.assign(api, chat_api);
// Configuration values for this plugin
// ====================================
// Refer to docs/source/configuration.rst for explanations of these
// configuration settings.
api.settings.extend({
'allow_message_corrections': 'all',
'allow_message_retraction': 'all',
'allow_message_styling': true,
'auto_join_private_chats': [],
'clear_messages_on_reconnection': false,
'filter_by_resource': false,
'send_chat_state_notifications': true
});
_converse.Message = ModelWithContact.extend(MessageMixin);
_converse.Messages = Collection.extend({
model: _converse.Message,
comparator: 'time'
});
_converse.ChatBox = ChatBox;
/**
* Handler method for all incoming single-user chat "message" stanzas.
* @private
* @method _converse#handleMessageStanza
* @param { MessageAttributes } attrs - The message attributes
*/
_converse.handleMessageStanza = async function (stanza) {
if (st.isServerMessage(stanza)) {
// Prosody sends headline messages with type `chat`, so we need to filter them out here.
const from = stanza.getAttribute('from');
return log.info(`handleMessageStanza: Ignoring incoming server message from JID: ${from}`);
}
const attrs = await st.parseMessage(stanza, _converse);
if (u.isErrorObject(attrs)) {
attrs.stanza && log.error(attrs.stanza);
return log.error(attrs.message);
}
const has_body = !!sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length;
const chatbox = await api.chats.get(attrs.contact_jid, { 'nickname': attrs.nick }, has_body);
await chatbox?.queueMessage(attrs);
/**
* @typedef { Object } MessageData
* An object containing the original message stanza, as well as the
* parsed attributes.
* @property { XMLElement } stanza
* @property { MessageAttributes } stanza
* @property { ChatBox } chatbox
*/
const data = { stanza, attrs, chatbox };
/**
* Triggered when a message stanza is been received and processed.
* @event _converse#message
* @type { object }
* @property { module:converse-chat~MessageData } data
*/
api.trigger('message', data);
};
function registerMessageHandlers () {
_converse.connection.addHandler(
stanza => {
if (sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop()) {
// MAM messages are handled in converse-mam.
// We shouldn't get MAM messages here because
// they shouldn't have a `type` attribute.
log.warn(`Received a MAM message with type "chat".`);
return true;
}
_converse.handleMessageStanza(stanza);
return true;
},
null,
'message',
'chat'
);
_converse.connection.addHandler(
stanza => {
// Message receipts are usually without the `type` attribute. See #1353
if (stanza.getAttribute('type') !== null) {
// TODO: currently Strophe has no way to register a handler
// for stanzas without a `type` attribute.
// We could update it to accept null to mean no attribute,
// but that would be a backward-incompatible change
return true; // Gets handled above.
}
_converse.handleMessageStanza(stanza);
return true;
},
Strophe.NS.RECEIPTS,
'message'
);
_converse.connection.addHandler(
stanza => {
handleErrorMessage(stanza);
return true;
},
null,
'message',
'error'
);
}
function autoJoinChats () {
// Automatically join private chats, based on the
// "auto_join_private_chats" configuration setting.
api.settings.get('auto_join_private_chats').forEach(jid => {
if (_converse.chatboxes.where({ 'jid': jid }).length) {
return;
}
if (typeof jid === 'string') {
api.chats.open(jid);
} else {
log.error('Invalid jid criteria specified for "auto_join_private_chats"');
}
});
/**
* Triggered once any private chats have been automatically joined as
* specified by the `auto_join_private_chats` setting.
* See: https://conversejs.org/docs/html/configuration.html#auto-join-private-chats
* @event _converse#privateChatsAutoJoined
* @example _converse.api.listen.on('privateChatsAutoJoined', () => { ... });
* @example _converse.api.waitUntil('privateChatsAutoJoined').then(() => { ... });
*/
api.trigger('privateChatsAutoJoined');
}
/************************ BEGIN Route Handlers ************************/
function openChat (jid) {
if (!utils.isValidJID(jid)) {
return log.warn(`Invalid JID "${jid}" provided in URL fragment`);
}
api.chats.open(jid);
}
_converse.router.route('converse/chat?jid=:jid', openChat);
/************************ END Route Handlers ************************/
/************************ BEGIN Event Handlers ************************/
api.listen.on('chatBoxesFetched', autoJoinChats);
api.listen.on('presencesInitialized', registerMessageHandlers);
api.listen.on('clearSession', async () => {
if (_converse.shouldClearCache()) {
await Promise.all(
_converse.chatboxes.map(c => c.messages && c.messages.clearStore({ 'silent': true }))
);
const filter = o => o.get('type') !== _converse.CONTROLBOX_TYPE;
_converse.chatboxes.clearStore({ 'silent': true }, filter);
}
});
/************************ END Event Handlers ************************/
}
});
import ModelWithContact from './model-with-contact.js';
import log from '../../log.js';
import { _converse, api, converse } from '../../core.js';
const u = converse.env.utils;
const { Strophe } = converse.env;
/**
* Mixin which turns a `ModelWithContact` model into a non-MUC message. These can be either `chat` messages or `headline` messages.
* @mixin
* @namespace _converse.Message
* @memberOf _converse
* @example const msg = new _converse.Message({'message': 'hello world!'});
*/
const MessageMixin = {
defaults () {
return {
'msgid': u.getUniqueId(),
'time': new Date().toISOString(),
'is_ephemeral': false
};
},
async initialize () {
if (!this.checkValidity()) {
return;
}
this.initialized = u.getResolveablePromise();
if (this.get('type') === 'chat') {
ModelWithContact.prototype.initialize.apply(this, arguments);
this.setRosterContact(Strophe.getBareJidFromJid(this.get('from')));
}
if (this.get('file')) {
this.on('change:put', this.uploadFile, this);
}
this.setTimerForEphemeralMessage();
/**
* Triggered once a {@link _converse.Message} has been created and initialized.
* @event _converse#messageInitialized
* @type { _converse.Message}
* @example _converse.api.listen.on('messageInitialized', model => { ... });
*/
await api.trigger('messageInitialized', this, { 'Synchronous': true });
this.initialized.resolve();
},
/**
* Sets an auto-destruct timer for this message, if it's is_ephemeral.
* @private
* @method _converse.Message#setTimerForEphemeralMessage
* @returns { Boolean } - Indicates whether the message is
* ephemeral or not, and therefore whether the timer was set or not.
*/
setTimerForEphemeralMessage () {
const setTimer = () => {
this.ephemeral_timer = window.setTimeout(this.safeDestroy.bind(this), 10000);
};
if (this.isEphemeral()) {
setTimer();
return true;
} else {
this.on('change:is_ephemeral', () =>
this.isEphemeral() ? setTimer() : clearTimeout(this.ephemeral_timer)
);
return false;
}
},
checkValidity () {
if (Object.keys(this.attributes).length === 3) {
// XXX: This is an empty message with only the 3 default values.
// This seems to happen when saving a newly created message
// fails for some reason.
// TODO: This is likely fixable by setting `wait` when
// creating messages. See the wait-for-messages branch.
this.validationError = 'Empty message';
this.safeDestroy();
return false;
}
return true;
},
/**
* Determines whether this messsage may be retracted by the current user.
* @private
* @method _converse.Messages#mayBeRetracted
* @returns { Boolean }
*/
mayBeRetracted () {
const is_own_message = this.get('sender') === 'me';
return is_own_message && ['all', 'own'].includes(api.settings.get('allow_message_retraction'));
},
safeDestroy () {
try {
this.destroy();
} catch (e) {
log.error(e);
}
},
isEphemeral () {
return this.get('is_ephemeral');
},
getDisplayName () {
if (this.get('type') === 'groupchat') {
return this.get('nick');
} else if (this.contact) {
return this.contact.getDisplayName();
} else if (this.vcard) {
return this.vcard.getDisplayName();
} else {
return this.get('from');
}
},
getMessageText () {
const { __ } = _converse;
if (this.get('is_encrypted')) {
return this.get('plaintext') || this.get('body') || __('Undecryptable OMEMO message');
}
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
* @private
* @method _converse.Message#sendSlotRequestStanza
*/
sendSlotRequestStanza () {
if (!this.file) {
return Promise.reject(new Error('file is undefined'));
}
const iq = converse.env
.$iq({
'from': _converse.jid,
'to': this.get('slot_request_url'),
'type': 'get'
})
.c('request', {
'xmlns': Strophe.NS.HTTPUPLOAD,
'filename': this.file.name,
'size': this.file.size,
'content-type': this.file.type
});
return api.sendIQ(iq);
},
async getRequestSlotURL () {
const { __ } = _converse;
let stanza;
try {
stanza = await this.sendSlotRequestStanza();
} catch (e) {
log.error(e);
return this.save({
'type': 'error',
'message': __('Sorry, could not determine upload URL.'),
'is_ephemeral': true
});
}
const slot = stanza.querySelector('slot');
if (slot) {
this.save({
'get': slot.querySelector('get').getAttribute('url'),
'put': slot.querySelector('put').getAttribute('url')
});
} else {
return this.save({
'type': 'error',
'message': __('Sorry, could not determine file upload URL.'),
'is_ephemeral': true
});
}
},
uploadFile () {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
log.info('Status: ' + xhr.status);
if (xhr.status === 200 || xhr.status === 201) {
this.save({
'upload': _converse.SUCCESS,
'oob_url': this.get('get'),
'message': this.get('get')
});
} else {
xhr.onerror();
}
}
};
xhr.upload.addEventListener(
'progress',
evt => {
if (evt.lengthComputable) {
this.set('progress', evt.loaded / evt.total);
}
},
false
);
xhr.onerror = () => {
const { __ } = _converse;
let message;
if (xhr.responseText) {
message = __(
'Sorry, could not succesfully upload your file. Your server’s response: "%1$s"',
xhr.responseText
);
} else {
message = __('Sorry, could not succesfully upload your file.');
}
this.save({
'type': 'error',
'upload': _converse.FAILURE,
'message': message,
'is_ephemeral': true
});
};
xhr.open('PUT', this.get('put'), true);
xhr.setRequestHeader('Content-type', this.file.type);
xhr.send(this.file);
}
};
export default MessageMixin;
import { converse } from "../../core.js";
import { Model } from '@converse/skeletor/src/model.js';
const u = converse.env.utils;
const ModelWithContact = Model.extend({
initialize () {
this.rosterContactAdded = u.getResolveablePromise();
},
async setRosterContact (jid) {
const contact = await api.contacts.get(jid);
if (contact) {
this.contact = contact;
this.set('nickname', contact.get('nickname'));
this.rosterContactAdded.resolve();
}
}
});
export default ModelWithContact;
This diff is collapsed.
......@@ -4,7 +4,7 @@
* @license Mozilla Public License (MPLv2)
* @description Implements the non-view logic for XEP-0045 Multi-User Chat
*/
import "./chat";
import "./chat/index.js";
import "./disco";
import "./emoji/index.js";
import { Collection } from "@converse/skeletor/src/collection";
......@@ -398,7 +398,6 @@ converse.plugins.add('converse-muc', {
* @memberOf _converse
*/
_converse.ChatRoom = _converse.ChatBox.extend({
messagesCollection: _converse.ChatRoomMessages,
defaults () {
return {
......@@ -595,6 +594,10 @@ converse.plugins.add('converse-muc', {
this.announceReconnection();
},
getMessagesCollection () {
return new _converse.ChatRoomMessages();
},
restoreSession () {
const id = `muc.session-${_converse.bare_jid}-${this.get('jid')}`;
this.session = new MUCSession({id});
......
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