Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Audio video capability #2956

Draft
wants to merge 28 commits into
base: master
Choose a base branch
from
Draft
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5e5fadc
jingle: added the initial boilerplate code for the plugin.
PawBud Jun 16, 2022
387e781
(TBR)CallButton: added an unfunctional call button to the toolbar
PawBud Jun 17, 2022
13b1ef9
(TBR)index: added buttons hook to jingle plugin
PawBud Jun 20, 2022
6d88243
test
PawBud Jun 20, 2022
fb84f10
revert back code to avoid merge conflicts
PawBud Jun 20, 2022
d31b485
(TBR)added a boilerplate modal for jingle call
PawBud Jun 21, 2022
5c4b40d
added the call button at the chat header.
PawBud Jun 23, 2022
3f975d2
(TBR)Jingle modal
PawBud Jun 23, 2022
70a49d3
(TBR)added 2 new custom plugins for jingle
PawBud Jun 27, 2022
309c29d
added a toggling button in the centre of the chat-header
PawBud Jun 29, 2022
97b737d
jingle: added the tests for jingle & styled the header button
PawBud Jun 30, 2022
4eb696e
additional tests for jingle
PawBud Jun 30, 2022
1f6bd12
tests for chat header written
PawBud Jul 4, 2022
cbba9cb
(TBR) added the message initation tests
PawBud Jul 6, 2022
0e19aee
(TBR)added the Incoming pending & outgoing pending states, xep 0353 t…
PawBud Jul 6, 2022
ee9da60
tests: added tests to message initiation
PawBud Jul 10, 2022
6c0f422
(TBR)propose id is now shared with the retract stanza
PawBud Jul 15, 2022
322af18
(TBR) corrected message initiation test
PawBud Jul 18, 2022
bbcded7
changed the describe function text
PawBud Jul 18, 2022
c978385
changed the describe function text
PawBud Jul 19, 2022
1140ad8
(TBR) corrected some tests
PawBud Jul 24, 2022
bbb985e
(TBR) fixed the tests and added the On message hook
PawBud Jul 26, 2022
1235d42
(TBR)added the message retraction feature
PawBud Aug 4, 2022
b3ace4d
chat history: this commit adds the chat history of the jingle call st…
PawBud Aug 8, 2022
4c5b855
(TRB) Retraction improvements
PawBud Aug 24, 2022
7ef19af
(TBR)added a hook for the retraction
PawBud Aug 29, 2022
bec6455
The Reciever's side retraction tests pass
PawBud Aug 30, 2022
8f72210
chat history ui added and tests as well
PawBud Sep 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
@@ -539,6 +539,7 @@ The core, and by default whitelisted, plugins are::
converse-dragresize
converse-fullscreen
converse-headline
converse-jingle
converse-mam
converse-minimize
converse-muc
1 change: 1 addition & 0 deletions docs/source/other_frameworks.rst
Original file line number Diff line number Diff line change
@@ -60,6 +60,7 @@ Below is an example code that wraps converse.js as an angular.js service.
"converse-vcard", // XEP-0054 VCard-temp
"converse-register", // XEP-0077 In-band registration
"converse-ping", // XEP-0199 XMPP Ping
"converse-jingle", // XEP-0166 Support for the Jingle Protocol
"converse-notification", // HTML5 Notifications
"converse-minimize", // Allows chatboxes to be minimized
"converse-dragresize", // Allows chatboxes to be resized by dragging them
2 changes: 2 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -64,6 +64,8 @@ module.exports = function(config) {
{ pattern: "src/plugins/controlbox/tests/controlbox.js", type: 'module' },
{ pattern: "src/plugins/controlbox/tests/login.js", type: 'module' },
{ pattern: "src/plugins/headlines-view/tests/headline.js", type: 'module' },
{ pattern: "src/plugins/jingle/tests/ui.js", type: 'module' },
{ pattern: "src/plugins/jingle/tests/message-initiation.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' },
1 change: 1 addition & 0 deletions src/converse.js
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import "./plugins/controlbox/index.js"; // The control box
import "./plugins/dragresize/index.js"; // Allows chat boxes to be resized by dragging them
import "./plugins/fullscreen/index.js";
import "./plugins/headlines-view/index.js";
import "./plugins/jingle/index.js" // Implements the jingle protocol
import "./plugins/mam-views/index.js";
import "./plugins/minimize/index.js"; // Allows chat boxes to be minimized
import "./plugins/muc-views/index.js"; // Views related to MUC
5 changes: 4 additions & 1 deletion src/headless/plugins/chat/model.js
Original file line number Diff line number Diff line change
@@ -228,6 +228,9 @@ const ChatBox = ModelWithContact.extend({
!this.handleChatMarker(attrs) &&
!(await this.handleRetraction(attrs))
) {
const { handled } = await api.hook('onMessage', this, { handled: false, attrs });
if (handled) return;

this.setEditable(attrs, attrs.time);

if (attrs['chat_state'] && attrs.sender === 'them') {
@@ -583,7 +586,7 @@ const ChatBox = ModelWithContact.extend({
if (attrs.is_tombstone) {
return false;
}
const message = this.messages.findWhere({'origin_id': attrs.retracted_id, 'from': attrs.from});
const message = this.messages.findWhere({ 'origin_id': attrs.retracted_id, 'from': attrs.from });
if (!message) {
attrs['dangling_retraction'] = true;
await this.createMessage(attrs);
6 changes: 3 additions & 3 deletions src/headless/plugins/chat/utils.js
Original file line number Diff line number Diff line change
@@ -106,7 +106,7 @@ export function registerMessageHandlers () {

/**
* Handler method for all incoming single-user chat "message" stanzas.
* @param { MessageAttributes } attrs - The message attributes
* @param { XMLElement } stanza - The message stanza
*/
export async function handleMessageStanza (stanza) {
if (isServerMessage(stanza)) {
@@ -125,8 +125,8 @@ export async function handleMessageStanza (stanza) {
return log.error(attrs.message);
}
// XXX: Need to take XEP-428 <fallback> into consideration
const has_body = !!(attrs.body || attrs.plaintext)
const chatbox = await api.chats.get(attrs.contact_jid, { 'nickname': attrs.nick }, has_body);
const should_create = !!(attrs.body || attrs.plaintext || attrs.jingle_propose)
const chatbox = await api.chats.get(attrs.contact_jid, { 'nickname': attrs.nick }, should_create);
await chatbox?.queueMessage(attrs);
/**
* @typedef { Object } MessageData
2 changes: 1 addition & 1 deletion src/headless/utils/core.js
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ export function isEmptyMessage (attrs) {
return !attrs['oob_url'] &&
!attrs['file'] &&
!(attrs['is_encrypted'] && attrs['plaintext']) &&
!attrs['message'];
!attrs['message'] && !attrs['jingle_propose'];
}

/* We distinguish between UniView and MultiView instances.
1 change: 1 addition & 0 deletions src/plugins/chatview/heading.js
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ export default class ChatHeading extends CustomElement {

initialize () {
this.model = _converse.chatboxes.get(this.jid);
this.listenTo(this.model, 'change:jingle_status', this.requestUpdate);
this.listenTo(this.model, 'change:status', this.requestUpdate);
this.listenTo(this.model, 'vcard:add', this.requestUpdate);
this.listenTo(this.model, 'vcard:change', this.requestUpdate);
4 changes: 4 additions & 0 deletions src/plugins/chatview/styles/chat-head.scss
Original file line number Diff line number Diff line change
@@ -64,6 +64,10 @@
flex-wrap: nowrap;
padding: 0;
}

.chatbox-call-status {
width: 80%;
}

a, a:visited, a:hover, a:not([href]):not([tabindex]) {
&.chatbox-btn {
1 change: 1 addition & 0 deletions src/plugins/chatview/templates/chat-head.js
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ export default (o) => {
<div class="chatbox-title__text" title="${o.jid}">
${ (o.type !== _converse.HEADLINES_TYPE) ? html`<a class="user show-msg-author-modal" @click=${o.showUserDetailsModal}>${ display_name }</a>` : display_name }
</div>
<converse-call-notification class="d-flex flex-row-reverse justify-content-center chatbox-call-status chatbox-title__text" jid=${o.model.get('jid')}></converse-call-notification>
</div>
<div class="chatbox-title__buttons row no-gutters">
${ until(tpl_dropdown_btns(), '') }
2 changes: 1 addition & 1 deletion src/plugins/chatview/tests/chatbox.js
Original file line number Diff line number Diff line change
@@ -297,7 +297,7 @@ describe("Chatboxes", function () {


it("can contain a button for starting a call",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
mock.initConverse(['chatBoxesFetched'], { blacklisted_plugins: ['converse-jingle']}, async function (_converse) {

const { api } = _converse;
await mock.waitForRoster(_converse, 'current');
42 changes: 42 additions & 0 deletions src/plugins/jingle/chat-header-notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { CustomElement } from 'shared/components/element.js';
import { _converse, api } from "@converse/headless/core";
import tpl_header_button from "./templates/header-button.js";
import { JINGLE_CALL_STATUS } from "./constants.js";
import { retractCall, finishCall } from './utils.js';


import './styles/jingle.scss';

export default class CallNotification extends CustomElement {

static get properties() {
return {
'jid': { type: String },
}
}

initialize() {
this.model = _converse.chatboxes.get(this.jid);
this.listenTo(this.model, 'change:jingle_status', () => this.requestUpdate());
}

render() {
return tpl_header_button(this);
}

endCall() {
const jingle_status = this.model.get('jingle_status');
if ( jingle_status === JINGLE_CALL_STATUS.OUTGOING_PENDING ) {
this.model.save('jingle_status', JINGLE_CALL_STATUS.ENDED);
retractCall(this);
return;
}
if ( jingle_status === JINGLE_CALL_STATUS.ACTIVE) {
this.model.save('jingle_status', JINGLE_CALL_STATUS.ENDED);
finishCall(this);
return;
}
}
}

api.elements.define('converse-call-notification', CallNotification);
6 changes: 6 additions & 0 deletions src/plugins/jingle/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const JINGLE_CALL_STATUS = {
INCOMING_PENDING: 0,
OUTGOING_PENDING: 1,
ACTIVE: 2,
ENDED: 3
};
53 changes: 53 additions & 0 deletions src/plugins/jingle/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @description Converse.js plugin which adds XEP-0166 Jingle
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/

import { _converse, converse, api } from '@converse/headless/core.js';
import 'plugins/modal/index.js';
import "./chat-header-notification.js";
import './toolbar-button.js';
import { JINGLE_CALL_STATUS } from './constants.js';
import { html } from "lit";
import { parseJingleMessage, handleRetraction, getJingleTemplate } from './utils.js';

const { Strophe } = converse.env;

Strophe.addNamespace('JINGLE', 'urn:xmpp:jingle:1');
Strophe.addNamespace('JINGLEMESSAGE', 'urn:xmpp:jingle-message:1');
Strophe.addNamespace('JINGLERTP', 'urn:xmpp:jingle:apps:rtp:1');

converse.plugins.add('converse-jingle', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
*
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found. By default it's
* false, which means these plugins are only loaded opportunistically.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ['converse-chatview'],

initialize: function () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
_converse.JINGLE_CALL_STATUS = JINGLE_CALL_STATUS;
_converse.api.listen.on('getToolbarButtons', (toolbar_el, buttons) => {
if (!this.is_groupchat) {
buttons.push(html`
<converse-jingle-toolbar-button jid=${toolbar_el.model.get('jid')}>
</converse-jingle-toolbar-button>
`);
}

return buttons;
});
api.listen.on('parseMessage', parseJingleMessage);
api.listen.on('onMessage', handleRetraction);
api.listen.on('getJingleTemplate', getJingleTemplate);
},
});
18 changes: 18 additions & 0 deletions src/plugins/jingle/modal/jingle-incoming-call-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import BootstrapModal from "plugins/modal/base.js";
import tpl_incoming_call from "../templates/incoming-call.js";

export default BootstrapModal.extend({
id: "start-jingle-call-modal",
persistent: true,

initialize () {
this.items = [];
this.loading_items = false;

BootstrapModal.prototype.initialize.apply(this, arguments);
},

toHTML () {
return tpl_incoming_call();
}
});
9 changes: 9 additions & 0 deletions src/plugins/jingle/styles/jingle.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.conversejs {
.chatbox {
.chat-head {
.jingle-call-initiated-button{
color: var(--chat-head-text-color) !important;
}
}
}
}
26 changes: 26 additions & 0 deletions src/plugins/jingle/templates/header-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { html } from 'lit';
import { __ } from 'i18n';
import { JINGLE_CALL_STATUS } from '../constants';

const tpl_active_call = (o) => {
const button = __('End Call');
return html`
<div>
<a class="jingle-call-initiated-button" @click=${o.endCall}>${ button }</a>
</div>
`;
}

// ${(jingle_status === JINGLE_CALL_STATUS.ACTIVE) ? html`${tpl_active_call(el)}` : html`` }
export default (el) => {
const jingle_status = el.model.get('jingle_status');
return html`
<div>
${(jingle_status === JINGLE_CALL_STATUS.OUTGOING_PENDING) ? html`Calling...` : '' }
</div>
<div>
${(jingle_status === JINGLE_CALL_STATUS.OUTGOING_PENDING) ? tpl_active_call(el) : '' }
${(jingle_status === JINGLE_CALL_STATUS.ENDED) ? html`Call Ended` : '' }
</div>
`;
}
27 changes: 27 additions & 0 deletions src/plugins/jingle/templates/incoming-call.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { html } from 'lit';
import { __ } from 'i18n';

const modal_close_button = html`<button type="button" class="btn btn-secondary" data-dismiss="modal">${__('Close')}</button>`;

export default () => {
const i18n_modal_title = __('Jingle Call');
return html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="muc-list-modal-label">${i18n_modal_title}</h5>
</div>
<div class="modal-body d-flex flex-column">
<span class="modal-alert"></span>
<ul class="available-chatrooms list-group">
</ul>
</div>
<div class="container text-center cl-2">
<button type="button" class="btn btn-success">Audio Call</button>
<button type="button" class="btn btn-primary">Video Call</button>
</div>
<div class="modal-footer">${modal_close_button}</div>
</div>
</div>
`;
}
12 changes: 12 additions & 0 deletions src/plugins/jingle/templates/jingle_chat_history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { __ } from 'i18n';
import { html } from "lit";
import { JINGLE_CALL_STATUS } from "../constants.js";

export default (o) => {
const ended_call = __('Call Ended');
const pending_call = __('Calling');
return html`
${ (o.get('jingle_status') === JINGLE_CALL_STATUS.OUTGOING_PENDING && o.get('jingle_status')!= undefined ) ? html`${pending_call}` : html`${ended_call}` }
`}


22 changes: 22 additions & 0 deletions src/plugins/jingle/templates/toolbar-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { html } from 'lit';
import { __ } from "i18n";
import { JINGLE_CALL_STATUS } from '../constants';

export default (el) => {
const call_color = '--chat-toolbar-btn-color';
const end_call_color = '--chat-toolbar-btn-close-color';
const jingle_status = el.model.get('jingle_status');
let button_color, i18n_start_call;
if (jingle_status === JINGLE_CALL_STATUS.OUTGOING_PENDING || jingle_status === JINGLE_CALL_STATUS.ACTIVE) {
button_color = end_call_color;
i18n_start_call = __('Stop the call');
}
else {
button_color = call_color;
i18n_start_call = __('Start a call');
}
return html`
<button class="toggle-call" @click=${el.toggleJingleCallStatus} title="${i18n_start_call}">
<converse-icon id="temp" color="var(${ button_color })" class="fa fa-phone" size="1em"></converse-icon>
</button>`
}
157 changes: 157 additions & 0 deletions src/plugins/jingle/tests/message-initiation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*global mock, converse */
const u = converse.env.utils;
const sizzle = converse.env.sizzle;

const { Strophe } = converse.env;

fdescribe("A Jingle Message Initiation Request", function () {

describe("from the initiator's perspective", function () {

it("is sent out when one clicks the call button", mock.initConverse(
['chatBoxesFetched'], {}, async function (_converse) {

await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
const call_button = view.querySelector('converse-jingle-toolbar-button button');
call_button.click();
const sent_stanzas = _converse.connection.sent_stanzas;
const stanza = await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`propose[xmlns='${Strophe.NS.JINGLEMESSAGE}']`, s).length).pop());
const propose_id = stanza.querySelector('propose');
expect(Strophe.serialize(stanza)).toBe(
`<message from="${_converse.bare_jid}" `+
`id="${stanza.getAttribute('id')}" `+
`to="${contact_jid}" `+
`type="chat" `+
`xmlns="jabber:client">`+
`<propose id="${propose_id.getAttribute('id')}" xmlns="${Strophe.NS.JINGLEMESSAGE}">`+
`<description media="audio" xmlns="${Strophe.NS.JINGLERTP}"/>`+
`</propose>`+
`<store xmlns="${Strophe.NS.HINTS}"/>`+
`</message>`);
expect(view.model.messages.length).toEqual(1);
}));


it("is ended when the initiator clicks the call button again", mock.initConverse(
['chatBoxesFetched'], {}, async function (_converse) {

await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
// the first click starts the call, and the other one ends it
const call_button = view.querySelector('converse-jingle-toolbar-button button');
call_button.click();
call_button.click();
const sent_stanzas = _converse.connection.sent_stanzas;
const stanza = await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`retract[xmlns='${Strophe.NS.JINGLEMESSAGE}']`, s).length).pop());
const jingle_retraction_id = stanza.querySelector('retract');
expect(Strophe.serialize(stanza)).toBe(
`<message from="${_converse.bare_jid}" `+
`id="${stanza.getAttribute('id')}" `+
`to="${contact_jid}" `+
`type="chat" `+
`xmlns="jabber:client">`+
`<retract id="${jingle_retraction_id.getAttribute('id')}" xmlns="${Strophe.NS.JINGLEMESSAGE}">`+
`<reason xmlns="${Strophe.NS.JINGLE}">`+
`<cancel/>`+
`Retracted`+
`</reason>`+
`</retract>`+
`<store xmlns="${Strophe.NS.HINTS}"/>`+
`</message>`);
// This needs to be fixed
expect(view.model.messages.length).toEqual(1);
}));

it("is ended when the initiator clicks the end call header button", mock.initConverse(
['chatBoxesFetched'], {}, async function (_converse) {

await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
// the first click starts the call, and the other one ends it
const call_button = view.querySelector('converse-jingle-toolbar-button button');
call_button.click();
const header_end_call_button = await u.waitUntil(() => view.querySelector('.jingle-call-initiated-button'));
header_end_call_button.click();
const sent_stanzas = _converse.connection.sent_stanzas;
const stanza = await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`retract[xmlns='${Strophe.NS.JINGLEMESSAGE}']`, s).length).pop());
const jingle_retraction_id = stanza.querySelector('retract');
expect(Strophe.serialize(stanza)).toBe(
`<message from="${_converse.bare_jid}" `+
`id="${stanza.getAttribute('id')}" `+
`to="${contact_jid}" `+
`type="chat" `+
`xmlns="jabber:client">`+
`<retract id="${jingle_retraction_id.getAttribute('id')}" xmlns="${Strophe.NS.JINGLEMESSAGE}">`+
`<reason xmlns="${Strophe.NS.JINGLE}">`+
`<cancel/>`+
`Retracted`+
`</reason>`+
`</retract>`+
`<store xmlns="${Strophe.NS.HINTS}"/>`+
`</message>`);
// This needs to be fixed
expect(view.model.messages.length).toEqual(1);
}));
});

describe("from the receiver's perspective", function () {

it("is received when the initiator clicks the call button", mock.initConverse(
['chatBoxesFetched'], { allow_non_roster_messaging: true }, async function (_converse) {

await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g, '.').toLowerCase() + '@montague.lit';
const propose_id = u.getUniqueId();
const initiator_stanza = u.toStanza(`
<message xmlns='jabber:client'
from='${_converse.bare_jid}'
to='${contact_jid}'
type='chat'>
<propose id="${propose_id}" xmlns="${Strophe.NS.JINGLEMESSAGE}">
<description media="audio" xmlns="${Strophe.NS.JINGLERTP}"/>
</propose>
<store xmlns='${Strophe.NS.HINTS}'/>
</message>`);
_converse.connection._dataRecv(mock.createRequest(initiator_stanza));

const view = await u.waitUntil(() => _converse.chatboxviews.get(contact_jid));
expect(view.model.messages.length).toEqual(1);
}));

it("is received when the initiator clicks the end call button", mock.initConverse(
['chatBoxesFetched'], {}, async function (_converse) {

await mock.waitForRoster(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
const call_button = view.querySelector('converse-jingle-toolbar-button button');
call_button.click();
const sent_stanzas = _converse.connection.sent_stanzas;
const stanza = await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`propose[xmlns='${Strophe.NS.JINGLEMESSAGE}']`, s).length).pop());
const propose_id = stanza.querySelector('propose');
const initiator_stanza = u.toStanza(`
<message xmlns='jabber:client'
from='${_converse.bare_jid}'
to='${contact_jid}'
type='chat'>
<retract id="${propose_id.getAttribute('id')}" xmlns="${Strophe.NS.JINGLEMESSAGE}">
<reason xmlns="${Strophe.NS.JINGLE}">
<cancel/>
<text>Retracted</text>
</reason>
</retract>
<store xmlns='${Strophe.NS.HINTS}'/>
</message>`);
_converse.connection._dataRecv(mock.createRequest(initiator_stanza));
expect(view.model.messages.length).toEqual(1);
}));
});
});
42 changes: 42 additions & 0 deletions src/plugins/jingle/tests/ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* global mock, converse */
const u = converse.env.utils;

describe("A Jingle Status", function () {

it("has been shown in the toolbar",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {

await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
spyOn(_converse.api, "trigger").and.callThrough();
// First check that the button does show
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
const toolbar = view.querySelector('.chat-toolbar');
const call_button = toolbar.querySelector('converse-jingle-toolbar-button button');
// Now check that the state changes
// toggleJingleCallStatus
const chatbox = view.model;
call_button.click();
expect(chatbox.get('jingle_status')).toBe(_converse.JINGLE_CALL_STATUS.OUTGOING_PENDING);
}));

it("has been shown in the chat-header",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {

await mock.waitForRoster(_converse, 'current');
await mock.openControlBox(_converse);
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
spyOn(_converse.api, "trigger").and.callThrough();
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
const chat_head = view.querySelector('.chatbox-title--row');
const chatbox = view.model;
chatbox.save('jingle_status', _converse.JINGLE_CALL_STATUS.OUTGOING_PENDING);
const header_notification = chat_head.querySelector('converse-call-notification');
const call_intialized = await u.waitUntil(() => header_notification.querySelector('.jingle-call-initiated-button'));
call_intialized.click();
expect(chatbox.get('jingle_status') === _converse.JINGLE_CALL_STATUS.ENDED);
}));
});
67 changes: 67 additions & 0 deletions src/plugins/jingle/toolbar-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { CustomElement } from 'shared/components/element.js';
import { converse, _converse, api } from "@converse/headless/core";
import { JINGLE_CALL_STATUS } from "./constants.js";
import tpl_toolbar_button from "./templates/toolbar-button.js";
import { retractCall, finishCall } from './utils.js';

const { Strophe, $msg } = converse.env;
const u = converse.env.utils;

export default class JingleToolbarButton extends CustomElement {

static get properties() {
return {
'jid': { type: String },
}
}

initialize() {
this.model = _converse.chatboxes.get(this.jid);
this.listenTo(this.model, 'change:jingle_status', () => this.requestUpdate());
}

render() {
return tpl_toolbar_button(this);
}
toggleJingleCallStatus() {
const jingle_status = this.model.get('jingle_status');
if ( jingle_status === JINGLE_CALL_STATUS.OUTGOING_PENDING) {
this.model.save('jingle_status', JINGLE_CALL_STATUS.ENDED);
retractCall(this);
return;
}
if ( jingle_status === JINGLE_CALL_STATUS.ACTIVE) {
this.model.save('jingle_status', JINGLE_CALL_STATUS.ENDED);
finishCall(this);
return;
}
if (!jingle_status || jingle_status === JINGLE_CALL_STATUS.ENDED) {
this.model.save('jingle_status', JINGLE_CALL_STATUS.OUTGOING_PENDING);
const propose_id = u.getUniqueId();
const message_id = u.getUniqueId();
api.send(
$msg({
'from': _converse.bare_jid,
'to': this.jid,
'type': 'chat',
'id': message_id,
}).c('propose', {'xmlns': Strophe.NS.JINGLEMESSAGE, 'id': propose_id })
.c('description', {'xmlns': Strophe.NS.JINGLERTP, 'media': 'audio'}).up().up()
.c('store', { 'xmlns': Strophe.NS.HINTS })
);
const attrs = {
'from': _converse.bare_jid,
'to': this.jid,
'type': 'chat',
'msg_id': message_id,
'propose_id': propose_id,
'media': 'audio',
'template_hook': 'getJingleTemplate'
}
this.model.messages.create(attrs);
return;
}
}
}

api.elements.define('converse-jingle-toolbar-button', JingleToolbarButton);
119 changes: 119 additions & 0 deletions src/plugins/jingle/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { converse, api, _converse } from '@converse/headless/core';
import JingleCallModal from "./modal/jingle-incoming-call-modal.js";
import tpl_jingle_chat_history from "./templates/jingle_chat_history.js";

const { Strophe, sizzle, $msg } = converse.env;
const u = converse.env.utils;


/**
* This function merges the incoming attributes and the jingle propose attribute into one.
* It also determines the type of the media i.e. audio or video
* @param { XMLElement } stanza
* @param { Object } attrs
*/
export function parseJingleMessage(stanza, attrs) {
const jingle_propose_type = getJingleProposeType(stanza);
// To do editable: false, retracted_id: {if there is a retractions set it to the id}, retracted(timestamp)
return { ...attrs, ...{ 'jingle_propose': jingle_propose_type, 'jingle_retraction_id': getJingleRetractionID(stanza), 'template_hook': (attrs['template_hook']) ? 'getJingleTemplate' : undefined, 'jingle_status': attrs['jingle_status'] }}
}

function getJingleProposeType(stanza){
const el = sizzle(`propose[xmlns="${Strophe.NS.JINGLEMESSAGE}"] > description`, stanza).pop();
return el?.getAttribute('media');
}

function getJingleRetractionID(stanza){
const el = sizzle(`propose[xmlns="${Strophe.NS.JINGLEMESSAGE}"]`, stanza).pop();
return el?.getAttribute('id');
}

export function getJingleTemplate(model) {
return tpl_jingle_chat_history(model);
}

export function jingleCallInitialized() {
JingleCallModal;
}

/**
* This function simply sends the retraction stanza and modifies the attributes
*/
export function retractCall(context) {
const initiator_message = context.model.messages.findWhere({ 'media': 'audio' });
const propose_id = initiator_message.attributes.propose_id;
const message_id = u.getUniqueId();
api.send(
$msg({
'from': _converse.bare_jid,
'to': context.jid,
'type': 'chat',
id: message_id
}).c('retract', { 'xmlns': Strophe.NS.JINGLEMESSAGE, 'id': propose_id })
.c('reason', { 'xmlns': Strophe.NS.JINGLE })
.c('cancel', {}).up()
.t('Retracted').up().up()
.c('store', { 'xmlns': Strophe.NS.HINTS })
);
const attrs = {
'from': _converse.bare_jid,
'to': context.jid,
'type': 'chat',
'jingle_retraction_id': propose_id,
'msg_id': message_id,
'jingle_status': context.model.get('jingle_status'),
'template_hook': 'getJingleTemplate'
}
context.model.messages.create(attrs);
}

/**
* This function simply sends the stanza that ends the call
*/
export function finishCall(context) {
const message_id = u.getUniqueId();
jcbrand marked this conversation as resolved.
Show resolved Hide resolved
const stanza = $msg({
'from': _converse.bare_jid,
'to': context.jid,
'type': 'chat'
}).c('finish', {'xmlns': Strophe.NS.JINGLEMESSAGE, 'id': context.getAttribute('id')})
.c('reason', {'xmlns': Strophe.NS.JINGLE})
.c('success', {}).up()
.t('Success').up().up()
.c('store', { 'xmlns': Strophe.NS.HINTS })
const attrs = {
'from': _converse.bare_jid,
'to': context.jid,
'type': 'chat',
'msg_id': message_id,
'jingle_status': context.model.get('jingle_status'),
'template_hook': 'getJingleTemplate'
}
context.model.messages.create(attrs);
api.send(stanza);
}


/*
* This is the handler for the 'onMessage' hook
* It inspects the incoming message attributes and checks whether we have is a jingle retraction message
* if it is, then we find the jingle propose message and update it.
* @param { _converse.ChatBox } model
* @param { } data
*/
export async function handleRetraction(model, data) {
const jingle_retraction_id = data.attrs['jingle_retraction_id'];
if (jingle_retraction_id) {
//finding the propose message with the same id as the retraction id
const message = model.messages.findWhere({ 'jingle_propose': 'audio' });
if (message) {
message.save(data.attrs, { 'propose_id': jingle_retraction_id });
data.handled = true;
}
else {
// It is a dangling retraction; we are waiting for the correct propose message
await _converse.ChatBox.createMessage(data.attrs);
}
}
return data;
}
1 change: 1 addition & 0 deletions src/shared/constants.js
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ export const VIEW_PLUGINS = [
'converse-dragresize',
'converse-fullscreen',
'converse-headlines-view',
'converse-jingle',
'converse-mam-views',
'converse-minimize',
'converse-modal',
1 change: 1 addition & 0 deletions src/shared/styles/themes/classic.scss
Original file line number Diff line number Diff line change
@@ -73,6 +73,7 @@
--chat-head-color: var(--green);
--chat-head-text-color: white;
--chat-toolbar-btn-color: var(--green);
--chat-toolbar-btn-close-color: var(--dark-red);
--chat-toolbar-btn-disabled-color: gray;

--toolbar-btn-text-color: white;