Commit 0d9586e0 authored by Eugene Shen's avatar Eugene Shen

Correctly reload and rewrite entire chat

parent edcd20f8
/*globals window, document, RSVP, rJS,
URI, location, XMLHttpRequest, console*/
/*jslint indent: 2, maxlen: 80*/
(function (window, document, RSVP, rJS,
XMLHttpRequest, location, console) {
"use strict";
/*
if (navigator.hasOwnProperty('serviceWorker')) {
// Check if a ServiceWorker already controls the site on load
if (!navigator.serviceWorker.controller) {
// Register the ServiceWorker
navigator.serviceWorker.register('gadget_erp5_serviceworker.js');
}
}
*/
var MAIN_SCOPE = "m";
function renderMainGadget(gadget, url, options) {
return gadget.declareGadget(url, {
scope: MAIN_SCOPE
})
.push(function (page_gadget) {
gadget.props.m_options_string = JSON.stringify(options);
if (page_gadget.render === undefined) {
return [page_gadget];
}
return RSVP.all([
page_gadget,
page_gadget.render(options)
]);
})
.push(function (all_result) {
return all_result[0];
});
}
function initHeaderOptions(gadget) {
gadget.props.header_argument_list = {
panel_action: true,
title: gadget.props.application_title || "OfficeJS"
};
}
function route(my_root_gadget, my_scope, my_method, argument_list) {
return RSVP.Queue()
.push(function () {
return my_root_gadget.getDeclaredGadget(my_scope);
})
.push(function (my_gadget) {
if (argument_list) {
return my_gadget[my_method].apply(my_gadget, argument_list);
}
return my_gadget[my_method]();
});
}
function updateHeader(gadget) {
var header_gadget;
return gadget.getDeclaredGadget("header")
.push(function (result) {
header_gadget = result;
return header_gadget.notifySubmitted();
})
.push(function () {
return header_gadget.render(gadget.props.header_argument_list);
});
}
function increaseLoadingCounter(gadget) {
return new RSVP.Queue()
.push(function () {
gadget.props.loading_counter += 1;
if (gadget.props.loading_counter === 1) {
return gadget.getDeclaredGadget("header")
.push(function (header_gadget) {
return header_gadget.notifyLoading();
});
}
});
}
function decreaseLoadingCounter(gadget) {
return new RSVP.Queue()
.push(function () {
gadget.props.loading_counter -= 1;
if (gadget.props.loading_counter < 0) {
gadget.props.loading_counter = 0;
// throw new Error("Unexpected negative loading counter");
}
if (gadget.props.loading_counter === 0) {
return gadget.getDeclaredGadget("header")
.push(function (header_gadget) {
return header_gadget.notifyLoaded();
});
}
});
}
function callJioGadget(gadget, method, param_list) {
var called = false;
return new RSVP.Queue()
.push(function () {
called = true;
return increaseLoadingCounter(gadget);
})
.push(function () {
return gadget.getDeclaredGadget("jio_gadget");
})
.push(function (jio_gadget) {
return jio_gadget[method].apply(jio_gadget, param_list);
})
.push(function (result) {
return decreaseLoadingCounter(gadget)
.push(function () {
return result;
});
}, function (error) {
if (called) {
return decreaseLoadingCounter(gadget)
.push(function () {
throw error;
});
}
throw error;
});
}
function displayErrorContent(gadget, error) {
// Do not break the application in case of errors.
// Display it to the user for now,
// and allow user to go back to the frontpage
var error_text = "";
if (error.target instanceof XMLHttpRequest) {
error_text = error.target.toString() + " " +
error.target.status + " " +
error.target.statusText + "\n" +
error.target.responseURL + "\n\n" +
error.target.getAllResponseHeaders();
} else if (error instanceof Error) {
error_text = error.toString();
} else {
error_text = JSON.stringify(error);
}
console.error(error);
if (error instanceof Error) {
console.error(error.stack);
}
// XXX Improve error rendering
gadget.props.content_element.innerHTML = "<br/><br/><br/><pre></pre>";
gadget.props.content_element.querySelector('pre').textContent =
"Error: " + error_text;
// XXX Notify error
}
function displayError(gadget, error) {
if (error instanceof RSVP.CancellationError) {
return;
}
displayErrorContent(gadget, error);
return gadget.dropGadget(MAIN_SCOPE)
.push(undefined, function () {
// Do not crash the app if the pg gadget in not defined
// ie, keep the original error on screen
return;
});
}
//////////////////////////////////////////
// Page rendering
//////////////////////////////////////////
rJS(window)
.ready(function () {
var gadget = this,
setting_gadget,
setting;
this.props = {
loading_counter: 0,
content_element: this.element.querySelector('.gadget-content')
};
// Configure setting storage
return gadget.getDeclaredGadget("setting_gadget")
.push(function (result) {
setting_gadget = result;
return setting_gadget.createJio({
type: "indexeddb",
database: "setting"
});
})
.push(function () {
return setting_gadget.get("setting")
.push(undefined, function (error) {
if (error.status_code === 404) {
return {};
}
throw error;
});
})
.push(function (result) {
setting = result;
// Extract configuration parameters stored in HTML
// XXX Will work only if top gadget...
var element_list =
document.head
.querySelectorAll("script[data-renderjs-configuration]"),
len = element_list.length,
key,
value,
i;
for (i = 0; i < len; i += 1) {
key = element_list[i].getAttribute('data-renderjs-configuration');
value = element_list[i].textContent;
gadget.props[key] = value;
setting[key] = value;
}
// Calculate erp5 hateoas url
setting.hateoas_url = (new URI(gadget.props.hateoas_url))
.absoluteTo(location.href)
.toString();
return setting_gadget.put("setting", setting);
})
.push(function () {
// Configure jIO storage
return gadget.getDeclaredGadget("jio_gadget");
})
.push(function (jio_gadget) {
if (setting.jio_storage_description) {
return jio_gadget.createJio(setting.jio_storage_description);
} else {
return;
}
})
.push(function () {
return gadget.getDeclaredGadget('panel');
})
.push(function (panel_gadget) {
return panel_gadget.render();
})
.push(function () {
return gadget.getDeclaredGadget('router');
})
.push(function (router_gadget) {
return router_gadget.start();
});
})
//////////////////////////////////////////
// Allow Acquisition
//////////////////////////////////////////
.allowPublicAcquisition("getSetting", function (argument_list) {
var gadget = this,
key = argument_list[0],
default_value = argument_list[1];
return gadget.getDeclaredGadget("setting_gadget")
.push(function (jio_gadget) {
return jio_gadget.get("setting");
})
.push(function (doc) {
return doc[key] || default_value;
})
.push(function (result) {
if (!result) {
throw "The setting '" + argument_list[0] + "' could not be found!"
+ " Please provide a default value to avoid this error,"
+ " either in gadget.getSetting('key', 'default_value'),"
+ " or in the Layout Configuration of your ERP5 Web Site."
+ " Default values must not be falsy, e.g. null or undefined.";
}
return result;
}, function (error) {
if (error.status_code === 404) {
return default_value;
}
throw error;
});
})
.allowPublicAcquisition("setSetting", function (argument_list) {
var jio_gadget,
gadget = this,
key = argument_list[0],
value = argument_list[1];
return gadget.getDeclaredGadget("setting_gadget")
.push(function (result) {
jio_gadget = result;
return jio_gadget.get("setting");
})
.push(undefined, function (error) {
if (error.status_code === 404) {
return {};
}
throw error;
})
.push(function (doc) {
doc[key] = value;
return jio_gadget.put('setting', doc);
});
})
.allowPublicAcquisition("translateHtml", function (argument_list) {
return this.getDeclaredGadget("translation_gadget")
.push(function (translation_gadget) {
return translation_gadget.translateHtml(argument_list[0]);
});
})
// XXX Those methods may be directly integrated into the header,
// as it handles the submit triggering
.allowPublicAcquisition('notifySubmitting', function (argument_list) {
return RSVP.all([
route(this, "header", 'notifySubmitting'),
route(this, "notification", 'notify', argument_list)
]);
})
.allowPublicAcquisition('notifySubmitted', function (argument_list) {
return RSVP.all([
route(this, "header", 'notifySubmitted'),
route(this, "notification", 'notify', argument_list)
]);
})
.allowPublicAcquisition('notifyChange', function (argument_list) {
return RSVP.all([
route(this, "header", 'notifyChange'),
route(this, "notification", 'notify', argument_list)
]);
})
.allowPublicAcquisition('refresh', function () {
var gadget = this;
return gadget.getDeclaredGadget(MAIN_SCOPE)
.push(function (main) {
if (main.render !== undefined) {
return main.render(JSON.parse(gadget.props.m_options_string));
}
}, function () {
return;
});
})
.allowPublicAcquisition("translate", function (argument_list) {
return this.getDeclaredGadget("translation_gadget")
.push(function (translation_gadget) {
return translation_gadget.translate(argument_list[0]);
});
})
.allowPublicAcquisition("redirect", function (param_list) {
return this.getDeclaredGadget('router')
.push(function (router_gadget) {
return router_gadget.redirect.apply(router_gadget, param_list);
});
})
.allowPublicAcquisition('reload', function () {
// XXX does not actually reload the page
return location.reload();
})
.allowPublicAcquisition("getUrlParameter", function (param_list) {
return this.getDeclaredGadget('router')
.push(function (router_gadget) {
return router_gadget.getUrlParameter.apply(router_gadget, param_list);
});
})
.allowPublicAcquisition("getUrlFor", function (param_list) {
return this.getDeclaredGadget('router')
.push(function (router_gadget) {
return router_gadget.getCommandUrlFor.apply(router_gadget,
param_list);
});
})
.allowPublicAcquisition("updateHeader", function (param_list) {
var gadget = this;
initHeaderOptions(gadget);
return this.getDeclaredGadget("translation_gadget")
.push(function (translation_gadget) {
var promise_list = [],
key;
for (key in param_list[0]) {
if (param_list[0].hasOwnProperty(key)) {
gadget.props.header_argument_list[key] = param_list[0][key];
}
}
promise_list.push(translation_gadget.translate(
gadget.props.header_argument_list.title
));
if (gadget.props.header_argument_list.hasOwnProperty('right_title')) {
promise_list.push(translation_gadget.translate(
gadget.props.header_argument_list.right_title
));
}
return RSVP.all(promise_list);
})
.push(function (result_list) {
gadget.props.header_argument_list.title = result_list[0];
if (result_list.length === 2) {
gadget.props.header_argument_list.right_title = result_list[1];
}
// XXX Sven hack: number of _url determine padding for
// subheader on ui-content
var key,
count = 0;
for (key in gadget.props.header_argument_list) {
if (gadget.props.header_argument_list.hasOwnProperty(key)) {
if (key.indexOf('_url') > -1) {
count += 1;
}
}
}
if (count > 2) {
gadget.props.sub_header_class = "ui-has-subheader";
}
});
})
.allowPublicAcquisition('triggerPanel', function () {
return route(this, "panel", "toggle");
})
.allowPublicAcquisition('renderEditorPanel', function (param_list) {
return route(this, "editor_panel", 'render', param_list);
})
.allowPublicAcquisition("jio_allDocs", function (param_list) {
return callJioGadget(this, "allDocs", param_list);
})
.allowPublicAcquisition("jio_remove", function (param_list) {
return callJioGadget(this, "remove", param_list);
})
.allowPublicAcquisition("jio_post", function (param_list) {
return callJioGadget(this, "post", param_list);
})
.allowPublicAcquisition("jio_put", function (param_list) {
return callJioGadget(this, "put", param_list);
})
.allowPublicAcquisition("jio_get", function (param_list) {
return callJioGadget(this, "get", param_list);
})
.allowPublicAcquisition("jio_allAttachments", function (param_list) {
return callJioGadget(this, "allAttachments", param_list);
})
.allowPublicAcquisition("jio_getAttachment", function (param_list) {
return callJioGadget(this, "getAttachment", param_list);
})
.allowPublicAcquisition("jio_putAttachment", function (param_list) {
return callJioGadget(this, "putAttachment", param_list);
})
.allowPublicAcquisition("jio_removeAttachment", function (param_list) {
return callJioGadget(this, "removeAttachment", param_list);
})
.allowPublicAcquisition("jio_repair", function (param_list) {
return callJioGadget(this, "repair", param_list);
})
.allowPublicAcquisition("triggerSubmit", function (param_list) {
return this.getDeclaredGadget(MAIN_SCOPE)
.push(function (main_gadget) {
return main_gadget.triggerSubmit(param_list);
});
})
.allowPublicAcquisition('requireSetting', function (param_list) {
const setting_key = param_list[0];
const redirect_page = param_list[1];
const success_queue = param_list[2];
const gadget = this;
if (!redirect_page) {
return;
} else {
return new RSVP.Queue()
.push(function () {
return gadget.getDeclaredGadget("setting_gadget");
})
.push(function (jio_gadget) {
return jio_gadget.get("setting");
})
.push(function (setting) {
if (!setting[setting_key]) {
return new RSVP.Queue()
.push(function () {
return gadget.getDeclaredGadget("router");
})
.push(function (router_gadget) {
return router_gadget.redirect({
command: 'display',
options: {page: redirect_page},
});
});
} else {
return success_queue;
}
});
}
})
.allowPublicAcquisition("renderApplication", function (param_list) {
return this.render.apply(this, param_list);
})
/////////////////////////////////////////////////////////////////
// declared methods
/////////////////////////////////////////////////////////////////
.onStateChange(function (modification_dict) {
var gadget = this,
route_result = gadget.state;
if (modification_dict.hasOwnProperty('url')) {
return new RSVP.Queue()
.push(function () {
return renderMainGadget(
gadget,
route_result.url,
route_result.options
);
})
.push(function (main_gadget) {
// Append loaded gadget in the page
if (main_gadget !== undefined) {
var element = gadget.props.content_element,
content_container = document.createDocumentFragment();
// content_container.className = "ui-content " +
// (gadget.props.sub_header_class || "");
// reset subheader indicator
delete gadget.props.sub_header_class;
// go to the top of the page
window.scrollTo(0, 0);
// Clear first to DOM, append after to reduce flickering/manip
while (element.firstChild) {
element.removeChild(element.firstChild);
}
content_container.appendChild(main_gadget.element);
element.appendChild(content_container);
return updateHeader(gadget);
// XXX Drop notification
// return header_gadget.notifyLoaded();
}
});
}
// Same subgadget
return gadget.getDeclaredGadget(MAIN_SCOPE)
.push(function (page_gadget) {
return page_gadget.render(gadget.state.options);
})
.push(function () {
return updateHeader(gadget);
});
})
// Render the page
.declareMethod('render', function (route_result, keep_message) {
var gadget = this;
// Reinitialize the loading counter
gadget.props.loading_counter = 0;
// By default, init the header options to be empty
// (ERP5 title by default + sidebar)
initHeaderOptions(gadget);
return new RSVP.Queue()
.push(function () {
return increaseLoadingCounter(gadget);
})
.push(function () {
var promise_list = [
route(gadget, 'panel', 'close'),
route(gadget, 'editor_panel', 'close')
];
if (keep_message !== true) {
promise_list.push(route(gadget, 'notification', 'close'));
}
return RSVP.all(promise_list);
})
.push(function () {
return gadget.changeState({url: route_result.url,
options: route_result.options});
})
.push(function () {
return decreaseLoadingCounter(gadget);
}, function (error) {
return decreaseLoadingCounter(gadget)
.push(function () {
throw error;
});
});
})
/////////////////////////////////
// Handle sub gadgets services
/////////////////////////////////
.allowPublicAcquisition('reportServiceError', function (param_list,
gadget_scope) {
if (gadget_scope === undefined) {
// don't fail in case of dropped subgadget (like previous page)
return;
}
return displayError(this, param_list[0]);
})
.onEvent('submit', function () {
return displayError(this, new Error("Unexpected form submit"));
});
}(window, document, RSVP, rJS,
XMLHttpRequest, location, console));
\ No newline at end of file
......@@ -79,7 +79,7 @@
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>fast_priority_queue.js</string> </value>
<value> <string>erp5_page_launcher.js</string> </value>
</item>
<item>
<key> <string>description</string> </key>
......@@ -89,13 +89,11 @@
</item>
<item>
<key> <string>id</string> </key>
<value> <string>fast_priority_queue_js</string> </value>
<value> <string>erp5_page_launcher_js</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</value>
<value> <string>en</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
......@@ -109,13 +107,11 @@
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Fast Priority Queue JS</string> </value>
<value> <string>OfficeJS Launcher JS</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
<value> <string>001</string> </value>
</item>
</dictionary>
</pickle>
......
/**
* FastPriorityQueue.js : a fast heap-based priority queue in JavaScript.
* (c) the authors
* Licensed under the Apache License, Version 2.0.
*
* Speed-optimized heap-based priority queue for modern browsers and JavaScript engines.
*
* Usage :
Installation (in shell, if you use node):
$ npm install fastpriorityqueue
Running test program (in JavaScript):
// var FastPriorityQueue = require("fastpriorityqueue");// in node
var x = new FastPriorityQueue();
x.add(1);
x.add(0);
x.add(5);
x.add(4);
x.add(3);
x.peek(); // should return 0, leaves x unchanged
x.size; // should return 5, leaves x unchanged
while(!x.isEmpty()) {
console.log(x.poll());
} // will print 0 1 3 4 5
x.trim(); // (optional) optimizes memory usage
*/
"use strict";
var defaultcomparator = function (a, b) {
return a < b;
};
// the provided comparator function should take a, b and return *true* when a < b
function FastPriorityQueue(comparator) {
this.array = [];
this.size = 0;
this.compare = comparator || defaultcomparator;
}
// Add an element the the queue
// runs in O(log n) time
FastPriorityQueue.prototype.add = function (myval) {
var i = this.size;
this.array[this.size] = myval;
this.size += 1;
var p;
var ap;
while (i > 0) {
p = (i - 1) >> 1;
ap = this.array[p];
if (!this.compare(myval, ap)) {
break;
}
this.array[i] = ap;
i = p;
}
this.array[i] = myval;
};
// replace the content of the heap by provided array and "heapifies it"
FastPriorityQueue.prototype.heapify = function (arr) {
this.array = arr;
this.size = arr.length;
var i;
for (i = (this.size >> 1); i >= 0; i--) {
this._percolateDown(i);
}
};
// for internal use
FastPriorityQueue.prototype._percolateUp = function (i) {
var myval = this.array[i];
var p;
var ap;
while (i > 0) {
p = (i - 1) >> 1;
ap = this.array[p];
if (!this.compare(myval, ap)) {
break;
}
this.array[i] = ap;
i = p;
}
this.array[i] = myval;
};
// for internal use
FastPriorityQueue.prototype._percolateDown = function (i) {
var size = this.size;
var hsize = this.size >>> 1;
var ai = this.array[i];
var l;
var r;
var bestc;
while (i < hsize) {
l = (i << 1) + 1;
r = l + 1;
bestc = this.array[l];
if (r < size) {
if (this.compare(this.array[r], bestc)) {
l = r;
bestc = this.array[r];
}
}
if (!this.compare(bestc, ai)) {
break;
}
this.array[i] = bestc;
i = l;
}
this.array[i] = ai;
};
// Look at the top of the queue (a smallest element)
// executes in constant time
//
// This function assumes that the priority queue is
// not empty and the caller is resposible for the check.
// You can use an expression such as
// "isEmpty() ? undefined : peek()"
// if you expect to be calling peek on an empty priority queue.
//
FastPriorityQueue.prototype.peek = function () {
return this.array[0];
};
// remove the element on top of the heap (a smallest element)
// runs in logarithmic time
//
//
// This function assumes that the priority queue is
// not empty, and the caller is responsible for the check.
// You can use an expression such as
// "isEmpty() ? undefined : poll()"
// if you expect to be calling poll on an empty priority queue.
//
// For long-running and large priority queues, or priority queues
// storing large objects, you may want to call the trim function
// at strategic times to recover allocated memory.
FastPriorityQueue.prototype.poll = function () {
var ans = this.array[0];
if (this.size > 1) {
this.array[0] = this.array[--this.size];
this._percolateDown(0 | 0);
} else {
this.size -= 1;
}
return ans;
};
// recover unused memory (for long-running priority queues)
FastPriorityQueue.prototype.trim = function () {
this.array = this.array.slice(0, this.size);
};
// Check whether the heap is empty
FastPriorityQueue.prototype.isEmpty = function () {
return this.size === 0;
};
// just for illustration purposes
var main = function () {
// main code
var x = new FastPriorityQueue(function (a, b) {
return a < b;
});
x.add(1);
x.add(0);
x.add(5);
x.add(4);
x.add(3);
while (!x.isEmpty()) {
console.log(x.poll());
}
};
\ No newline at end of file
......@@ -7,8 +7,7 @@
<script src="rsvp.js"></script>
<script src="renderjs.js"></script>
<script src="jiodev.js"></script>
<script src="gadget_global.js" ></script>
<script src="fast_priority_queue.js"></script>
<script src="gadget_global.js"></script>
<script src="gadget_erp5_page_chat_box.js"></script>
</head>
<body>
......@@ -26,11 +25,7 @@
</form>
<form class="join-form">
<input type="text" name="content" />
<input type="submit" value="Add new contact (join existing room as guest)!" />
</form>
<form class="make-form">
<input type="text" name="content" />
<input type="submit" value="Add new room (make new room as host)!" />
<input type="submit" value="Join room!" />
</form>
</div>
</div>
......
/*jslint nomen: true, indent: 2, maxerr: 300, maxlen: 80*/
/*global window, document, RSVP, rJS, FastPriorityQueue*/
(function (window, document, RSVP, rJS, FastPriorityQueue) {
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global window, document, RSVP, rJS, promiseEventListener */
(function (window, document, RSVP, rJS, promiseEventListener) {
"use strict";
/* Settings required:
* - jio_storage_description, or be redirected
* - user_email
* - user-email
* Settings used:
* - default_jio_type, default: ""
* - default_erp5_url, default: ""
* - default_dav_url, default: ""
*/
/* Abstract Data Types:
* message is a JSON object representing a message with all its metadata.
* chat is messageToChat(message) for displaying an element in the chat log.
* Message Types:
* message is a typical message, with an author and timestamp.
* notification is a system-generated message, with no author.
* bundle is a list of new messages to be added to the local archive.
* request is a dictionary of the times of the last messages of each peer.
/* Check if a string ends with another string.
* Parameters:
* - string: the string that may or may not end with the suffix string
* - suffix: the substring that may or may not be in the end of the string
* Effects: nothing
* Returns: true if string ends with suffix, otherwise false
*/
/* Program Workflow:
* JIO storage (the "archive") can only bepag accessed through two functions:
* - getLocalArchive takes the archive and stores it in the list
* - storeArchive stores a message in the archive
* Local storage (the "list") is always in sorted order and
* can only be accessed through three functions:
* - sendLocalArchive takes the list and sends it over WebRTC
* - storeList stores a message in the list
* - refreshChat sorts the entire list
* The displayed chat log can only be accessed through two functions:
* - appendMessage turns a message into a chat and appends it to the log
* - refreshChat takes the list to overwrite chat log
* Peer interaction via WebRTC can only be accessed through four functions:
* - deployMessage sends a message over WebRTC
* - getMessage gets a message over WebRTC
* - sendLocalArchive sends the list over WebRTC
* - getRemoteArchive gets a list over WebRTC
function stringEndsWith(string, suffix) {
return string.indexOf(suffix, string.length - suffix.length) !== -1;
}
/* Translate an arbitrary name to a querySelector-safe ID.
* Parameters:
* - name: the name to use as an ID
* Effects: nothing
* Returns: name with all non-alphanumeric characters replaced by hyphens
*/
/* Summary:
* - getLocalArchive takes archive, calls storeList, appendMessage on each
* - sendLocalArchive sends the list to peer
* - getRemoteArchive gets a list from peer, calls storeArchive on each
* - deployMessage sends a message to peer, calls getMessage
* - getMessage gets peer, calls storeArchive, storeList, appendMessage
* - storeArchive stores a message in the archive
* - storeList stores a message in the list
* - appendMessage calls messageToChat, appends it to the chat log
* - refreshChat takes the list, overwrites the chat log
*/
function logError(error) {
console.log(error);
function nameToId(name) {
return name.replace(/\W/gi, "-");
}
function logQueue(action) {
return new RSVP.Queue()
.push(function () {
return action;
})
.push(null, logError);
}
function styleElementByQuery(gadget, query, style) {
gadget.element
.querySelector(query).style.display = style;
}
/* Reset a text input.
* Parameters:
* - element: the text input element to reset
* Effects: set the value of the text input to the empty string
* Returns: the previous value of the text input before resetting it
*/
function resetInputValue(element) {
const value = element.value;
var value = element.value;
element.value = "";
return value;
}
function getQueryValue(query_list, query_string) {
for (let i = 0, i_len = query_list.length; i < i_len; i++) {
const query = query_list[i];
if (query_string.indexOf(query + "=") !== -1) {
const start = query_string.indexOf(query + "=") + query.length + 1;
let end = query_string.indexOf("&", start);
if (end === -1) {
end = query_string.length;
}
return query_string.slice(start, end);
}
}
return null;
}
function setQueryValue(query_list, query_string, element) {
const value = getQueryValue(query_list, query_string);
if (value) {
element.value = value;
}
}
/* Set the display style of the element given by a query.
* Parameters:
* - query: the query for which to find the element using querySelector
* - style: the display style to set the element to, example: "block"
* Effects: set the display style of the element to the given style
*/
function pollUntilNotNull(gadget, delay_ms, timeout_ms,
nullableFunction, callbackFunction) {
if (callbackFunction === undefined) {
callbackFunction = function () {};
function styleElementByQuery(gadget, query, style) {
if (gadget.element.querySelector(query)) {
gadget.element.querySelector(query).style.display = style;
}
return new RSVP.Queue()
.push(function () {
return nullableFunction();
})
.push(function (result) {
if (result !== null) {
return callbackFunction(result);
} else {
return RSVP.any([
RSVP.timeout(timeout_ms),
promiseDoWhile(function () {
return new RSVP.Queue()
.push(function () {
return RSVP.delay(delay_ms);
})
.push(function () {
return nullableFunction();
})
.push(function (result) {
if (result === null) {
return null;
} else {
return callbackFunction(result);
}
})
.push(function (nullable) {
return nullable === null;
})
.push(null, logError);
})
]);
}
});
}
/* Translate an arbitrary name to a querySelector-safe ID.
* Parameters:
* - name: the name to use as an ID
* Returns: name with all non-alphanumeric characters replaced by underscores
* Effects: none
*/
function nameToRoom(name) {
return name.replace(/\W/gi, "_");
}
/* Check if a string ends with another string.
/* Get the element given by a contact.
* Parameters:
* - string: the string that may or may not end with the suffix string
* - suffix: the substring that may or may not be in the end of the string
* Returns: true if string ends with suffix, otherwise false
* Effects: none
* - contact: the contact for which to find the element
* Effects: nothing
* Returns: the element corresponding to the given contact
*/
function stringEndsWith(string, suffix) {
return string.indexOf(suffix, string.length - suffix.length) !== -1;
}
/* Get the creation epoch time of a message.
* Parameters:
* - message: the message whose creation time is desired
* Returns: the creation time of the message, in milliseconds since 1970.
* Effects: none
*/
function getTime(message) {
return new Date(message.time).getTime();
function getElementFromContact(gadget, contact) {
return gadget.element.querySelector("#chat-contact-" + nameToId(contact));
}
/* Check if a message is new.
* Parameters:
* - message: the message that may or may not be new
* - last_time: the time in milliseconds since 1970 that splits old from new
* Returns: true if the message was created after last_time, otherwise false
* Effects: none
*/
function isNewMessage(message, last_time) {
return last_time === undefined || last_time < getTime(message);
}
/* Check if two messages are the same.
/* Highlight the current contact.
* Parameters:
* - lhs, rhs: the two messages that may or may not be the same
* Returns: true if both messages exist and have equivalent properties.
* Effects: none
* - contact: the current contact to be highlighted
* - previous_contact: the previously highlighted contact
* Effects:
* - highlight contact by adding the "current" class on its contact element
* - unhighlight previous_contact by removing the "current class"
*/
function isSameMessage(lhs, rhs) {
return lhs !== undefined && rhs !== undefined
&& lhs.name === rhs.name && lhs.content === rhs.content
&& lhs.room === rhs.room && getTime(lhs) === getTime(rhs);
function highlightContact(gadget, contact) {
var contact_element = getElementFromContact(gadget, contact),
previous_element = getElementFromContact(gadget, gadget.state.contact);
contact_element.classList.add("current");
if (previous_element) {
previous_element.classList.remove("current");
}
}
/* Keep messages in chronological order, for FastPriorityQueue.
/* Notify the user.
* Parameters:
* - ascending_order: true if the order is ascending, otherwise false
* Returns: a comparator for sorting messages using FastPriorityQueue
* Effects: none
* - room: the room that triggers the notification
* - notify: true to notify, false to remove notification
* Effects:
* - toggle "notify" class on the contact element corresponding to room
* - toggle the favicon between alert and default icons
*/
function messageTimeCompare(ascending_order) {
if (ascending_order) {
return function (lhs, rhs) {
return getTime(lhs) < getTime(rhs);
};
function notifyStatus(gadget, room, notify) {
var contact_element = getElementFromContact(gadget, room),
favicon_element = document.querySelector("link[rel*='icon']")
|| document.createElement("link");
if (notify) {
contact_element.classList.add("notify");
favicon_element.href = gadget.state.alert_icon_url;
} else {
contact_element.classList.remove("notify");
favicon_element.href = gadget.state.default_icon_url;
}
return function (lhs, rhs) {
return getTime(lhs) > getTime(rhs);
};
favicon_element.type = "image/x-icon";
favicon_element.rel = "shortcut icon";
document.head.appendChild(favicon_element);
}
/* Keep messages in chronologically ascending order, for refreshChat.
/* Get the creation epoch time of a message.
* Parameters:
* - lhs, rhs: two messages to compare during the sorting algorithm
* Returns: the correct value for sorting messages using sort()
* Effects: none
* - message: the message whose creation time is desired
* Effects: nothing
* Returns: the creation time of the message, in milliseconds since 1970.
*/
function messageTimeComparator(lhs, rhs) {
if (getTime(lhs) < getTime(rhs)) {
return -1;
}
if (getTime(lhs) > getTime(rhs)) {
return 1;
}
return 0;
function getTime(message) {
return new Date(message.time).getTime();
}
/* Translate a JSON message to an HTML chat element.
* Parameters:
* - message: the JavaScript object to display in HTML
* Effects: nothing
* Returns: a HTML representation of the given message
* Effects: none
*/
function messageToChat(message) {
var matches, i, i_len, j, j_len, image, link,
var i, j, matches, image, link,
link_string, dot_count, is_image, absolute_url,
chat = document.createElement("li"),
image_extensions = [".jpg", ".png", ".gif", ".bmp", ".tif", ".svg"],
url_regex =
/((?:https?:\/\/)?(?:[\-a-zA-Z0-9_~\/]+\.)+[\-a-zA-Z0-9_~#?&=\/]+)/g;
switch (message.type) {
case "bundle":
break;
case "notification":
/((?:https?:\/\/)?(?:[\-a-zA-Z0-9_~\/@]+\.)+[\-a-zA-Z0-9_~#?&=\/]+)/g;
if (message.type === "notification") {
chat.appendChild(document.createTextNode(message.content));
chat.style.color = message.colour;
return chat;
case "message":
chat.style.color = message.color;
} else if (message.type === "message") {
// Put message in the format "[3/24/2017, 11:30:52 AM] user: message"
chat.appendChild(document.createTextNode("["
+ new Date(message.time).toLocaleString() + "] " + message.name + ": "
));
// Loop through all potential URLs in the message
// matches[i] is the non-link content, matches[i + 1] is the actual link
matches = message.content.split(url_regex);
for (i = 0, i_len = matches.length - 1; i < i_len; i += 2) {
for (i = 0; i < matches.length - 1; i += 2) {
chat.appendChild(document.createTextNode(matches[i]));
link_string = matches[i + 1];
dot_count = link_string.match(/\./g).length;
// If 2d + 1 >= L, then the potential URL has only single letters,
// so ignore it to eliminate acronyms like e.g., i.e., U.S.A., etc.
link_string = matches[i + 1];
dot_count = link_string.match(/\./g).length;
if (2 * dot_count + 1 >= link_string.length) {
chat.appendChild(document.createTextNode(link_string));
} else {
is_image = false;
// Add a protocol to transform relative URLs into absolute URLs
if (link_string.indexOf(":") !== -1) {
absolute_url = link_string;
} else {
absolute_url = "http://" + link_string;
}
// Create an image if the URL ends with one of the image extensions
for (j = 0, j_len = image_extensions.length; j < j_len; j += 1) {
for (j = 0; j < image_extensions.length; j += 1) {
if (stringEndsWith(link_string, image_extensions[j])) {
image = document.createElement("img");
image.src = absolute_url;
......@@ -275,6 +191,7 @@
break;
}
}
// Otherwise, create a link
if (!is_image) {
link = document.createElement("a");
......@@ -284,93 +201,75 @@
}
}
}
// Add the last non-link content and return the resulting chat element
chat.appendChild(document.createTextNode(matches[matches.length - 1]));
chat.style.color = message.colour;
return chat;
chat.style.color = message.color;
}
return chat;
}
rJS(window)
.setState({
name: null,
auth: null,
dropbox_folder: null,
erp5_url: null,
room: null,
element: null,
initialized: false,
// A set of names of current rooms; as a guest, only notify that
// you have joined once you have received the host's remote archive,
// so that your notification doesn't override all previous messages
// room_set["room"] = false if joined, true if also notified
contact: null,
room_set: {},
// A dictionary of messages based on the current room, e.g.
// message_list_dict["room"] = [message1, message2, ...]
message_list_dict: {},
// A dictionary of epoch times of the last messages received, e.g.
// last_message_dict["room"]["name"] = 1234567890
last_message_dict: {},
alert_icon: "https://pricespy.co.nz/favicon.ico",
default_icon: "https://softinst75770.host.vifib.net/"
alert_icon_url: "https://pricespy.co.nz/favicon.ico",
default_icon_url: "https://softinst75770.host.vifib.net/"
+ "erp5/web_site_module/web_chat/favicon.ico"
})
.declareService(function () {
var gadget = this;
return gadget.render();
})
// The following functions are all acquired from erp5_page_launcher.
// The following functions are all acquired from erp5_launcher_nojqm.js
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("getSetting", "getSetting")
.declareAcquiredMethod("requireSetting", "requireSetting")
.declareAcquiredMethod("jio_allDocs", "jio_allDocs")
.declareAcquiredMethod("jio_put", "jio_put")
.declareAcquiredMethod("jio_repair", "jio_repair")
.declareAcquiredMethod("getSetting", "getSetting")
/* Get the contact's jIO ID from their email address.
* Parameters:
* - param_list[0]: the email address of the contact
* Returns: the ID of the contact if found in jIO storage, otherwise null
* Effects: none
*/
.allowPublicAcquisition("getContactByEmail", function (param_list) {
var gadget = this,
email = param_list[0], i, i_len;
return new RSVP.Queue()
.push(function () {
return gadget.jio_allDocs({
limit: [0, 1000000],
query: 'portal_type: "Person"',
sort_on: [["last_modified", "descending"]],
select_list: ["default_email_coordinate_text"],
});
})
.push(function (person_list) {
for (i = 0, i_len = person_list.data.total_rows; i < i_len; i += 1) {
if (person_list.data.rows[i].value
.default_email_coordinate_text === email) {
return person_list.data.rows[i].id;
}
}
return null;
})
.push(null, logError);
// The following function is acquired by gadget_erp5_chat_room.
.allowPublicAcquisition("changeRoom", function (param_list) {
var gadget = this;
return gadget.changeRoom.apply(gadget, param_list);
})
/* Render the gadget
// Set the chat title when the current room changes.
.onStateChange(function (modification_dict) {
var gadget = this;
if (modification_dict.hasOwnProperty("room")) {
styleElementByQuery(gadget,
"#room-gadget-" + nameToId(gadget.state.contact), "none");
styleElementByQuery(gadget, ".chat-right-panel-chat", "flex");
gadget.element.querySelector(".chat-title")
.textContent = "Room: " + modification_dict.room;
} else if (modification_dict.hasOwnProperty("contact")) {
styleElementByQuery(gadget,
"#room-gadget-" + nameToId(gadget.state.contact), "none");
styleElementByQuery(gadget,
"#room-gadget-" + nameToId(modification_dict.contact), "block");
styleElementByQuery(gadget, ".chat-right-panel-chat", "none");
gadget.element.querySelector(".chat-title")
.textContent = "Contact: " + modification_dict.contact;
}
})
/* Render the gadget.
* Parameters:
* - get setting: jio_storage_description, user_email
* - getSetting: user_email, jio_storage_description,
* default_jio_type, default_erp5_url, default_dav_url
* Effects:
* - update header, page_title to "OfficeJS Chat"
* - redirect if no jIO storage available
* - join the user home room whose name is the user name
* - create the user contact, whose name is the user name
*/
.declareMethod("render", function () {
var gadget = this;
return gadget.requireSetting(
......@@ -379,779 +278,432 @@
new RSVP.Queue()
.push(function () {
return gadget.updateHeader({
page_title: "OfficeJS Chat",
page_title: "OfficeJS Chat"
});
})
.push(function () {
return gadget.getSetting("user_email");
return RSVP.all([
gadget.getSetting("user_email"),
gadget.getSetting("jio_storage_description"),
gadget.getSetting("default_jio_type", "none"),
gadget.getSetting("default_erp5_url", "none"),
gadget.getSetting("default_dav_url", "none")
]);
})
.push(function (email) {
gadget.state.room = email.replace(/[^0-9a-z\-]/gi, "-");
gadget.state.name = gadget.state.room;
gadget.element.querySelector(".send-form input[type='text']").onfocus =
function () {
return logQueue(
gadget.notifyStatus(gadget.state.room, false));
};
.push(function (setting_list) {
return gadget.changeState({
name: setting_list[0],
contact: setting_list[0],
local_sub_storage: setting_list[1],
default_jio_type: setting_list[2] === "none" ?
null : setting_list[2],
default_erp5_url: setting_list[3] === "none" ?
null : setting_list[3],
default_dav_url: setting_list[4] === "none" ?
null : setting_list[4]
});
})
.push(function () {
return gadget.createContact({
room: gadget.state.room, role: "host"});
gadget.element.querySelector(".send-form input[type='text']")
.onfocus = function () {
return notifyStatus(gadget, gadget.state.room, false);
};
return gadget.createContact(gadget.state.contact);
})
);
})
/* Send a message via the appropriate WebRTC channel.
/* Create a new contact.
* Parameters:
* - message: the message to send, whose room is the selected WebRTC room
* - source: the WebRTC data channel that sent the original message
* Returns: nothing
* Effects: calls sendMessage on the WebRTC gadget
* - contact: the name of the contact
* Effects:
* - append a new contact to the contact list
* - create a new room gadget
* - show the chat box
*/
.declareMethod("sendMessage", function (message, source) {
.declareMethod("createContact", function (contact) {
var gadget = this;
return new RSVP.Queue()
return gadget.appendContact(contact)
.push(function () {
return gadget.getDeclaredGadget("webrtc_gadget_" + message.room);
})
.push(function (webrtc_gadget) {
return webrtc_gadget.sendMessage.apply(
webrtc_gadget, [message, source]);
return gadget.createRoom(contact);
})
.push(null, logError);
})
.declareMethod("chooseRoom", function (param_dict) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
return gadget.declareGadget(
"gadget_erp5_chat_webrtc.html", {
scope: "webrtc_gadget_" + param_dict.room});
})
.push(function (webrtc_gadget) {
var fields = webrtc_gadget.element
.querySelector(".contact-form").elements;
fields.dropbox_folder.value = param_dict.dropbox_folder
|| gadget.state.dropbox_folder;
fields.erp5_url.value = param_dict.erp5_url
|| gadget.state.erp5_url;
webrtc_gadget.element.querySelector(".auth-form")
.auth.value = param_dict.auth || gadget.state.auth;
gadget.element.querySelector(".chat-right-panel").insertBefore(
webrtc_gadget.element,
gadget.element.querySelector(".chat-max-height-wrapper"));
webrtc_gadget.state.login_dict = {
room: param_dict.room,
name: gadget.state.name,
role: param_dict.role,
};
webrtc_gadget.state.dataChannelOnopen = function () {
return gadget.changeRoom(param_dict.room);
};
webrtc_gadget.state.dataChannelOnmessage =
function (event) {
var message = JSON.parse(event.data);
var source = event.srcElement;
if (message.type === "notification"
|| message.type === "message") {
return new RSVP.Queue()
.push(function () {
return gadget.getMessage(message);
})
.push(function () {
if (param_dict.role === "host") {
return webrtc_gadget.sendMessage(message, source);
} else {
return;
}
})
.push(null, logError);
} else if (message.type === "bundle") {
if (param_dict.role === "host") {
webrtc_gadget.state.archive_amount += 1;
if (webrtc_gadget.state.archive_amount >=
webrtc_gadget.state.guest_amount) {
webrtc_gadget.state.archive_amount = 0;
return new RSVP.Queue()
.push(function () {
return gadget.getRemoteArchive(message);
})
.push(function () {
return gadget.requestRequest();
})
.push(null, logError);
}
} else {
return gadget.getRemoteArchive(message);
}
} else if (message.type === "request") {
return gadget.sendLocalArchive(message, source);
} else if (message.type === "doubler") {
return gadget.sendRequest();
}
};
return webrtc_gadget.render();
})
.push(null, logError);
return gadget.changeState({contact: contact});
});
})
/*
return gadget.createJio({
remote: fields.remote.value,
dropbox_folder: fields.remote_dropbox_folder.value,
erp5_url: fields.remote_erp5_url.value,
dav_url: fields.remote_dav_url.value,
dav_user: fields.remote_dav_user.value,
dav_pass: fields.remote_dav_pass.value,
/* Append a new contact element to the contact list.
* Parameters:
* - contact: the name of the contact
* Effects:
* - if the contact is not blank and not a duplicate, then:
* - create and append a new contact element to the contact list
* - set its ID to a querySelector-safe translation of its name
* - highlight it and add an event listener to highlight it on click
* and either change to the room or the contact
*/
.declareMethod("appendContact", function (contact) {
var gadget = this,
contact_element;
if (!contact.trim()) {
throw "An invisible name is not allowed! You couldn't click on it!";
}
if (getElementFromContact(gadget, contact)) {
throw "A contact with the same name already exists!";
}
contact_element = document.createElement("li");
contact_element.appendChild(document.createTextNode(contact));
contact_element.setAttribute("id", "chat-contact-" + nameToId(contact));
gadget.element.querySelector(".contact-list")
.appendChild(contact_element);
highlightContact(gadget, contact);
contact_element.addEventListener("click", function () {
return notifyStatus(gadget, contact, false)
.push(function () {
highlightContact(gadget, contact);
if (gadget.state.room_set[contact]) {
return gadget.changeRoom(contact);
}
return gadget.changeState({contact: contact});
});
})
.push(function () {
return gadget.jio_repair();
})
*/
/*
}, false);
})
.declareMethod("createJio", function (param_dict) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
var remote_config = {
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: "officejs-chat",
},
},
};
var remote_post = true;
// We want the remote ERP5 storage to be canonical, so true for ERP5.
// Post is not implemented in drivetojiomapping, so false for others.
switch (param_dict.remote) {
case "dropbox":
var start = document.URL.indexOf("access_token=") + 13;
var end = document.URL.indexOf("&", start);
var token = document.URL.slice(start, end);
remote_config = {
type: "query",
sub_storage: {
type: "drivetojiomapping",
sub_storage: {
type: "dropbox",
access_token: token,
root: "dropbox",
},
},
};
remote_post = false;
break;
case "dav":
remote_config = {
type: "query",
sub_storage: {
type: "drivetojiomapping",
sub_storage: {
type: "dav",
url: param_dict.dav_url,
basic_login: btoa(param_dict.dav_user
+ ":" + param_dict.dav_pass),
with_credentials: true,
},
},
};
remote_post = false;
break;
case "erp5":
remote_config = {
type: "erp5",
url: (new URI("hateoas"))
.absoluteTo(param_dict.erp5_url).toString(),
default_view_reference: "view",
};
remote_post = true;
break;
}
return gadget.createJio({
type: "replicate",
use_remote_post: remote_post,
conflict_handling: 2,
/* Create a new room gadget.
* Parameters:
* - room: the name of the room
* Effects:
* - declare a new gadget with scope "room-gadget-" + room
* - set its ID to a querySelector-safe translation of its scope
* - initialize its state and render the room gadget
*/
.declareMethod("createRoom", function (room) {
var gadget = this,
room_gadget;
return gadget.declareGadget("gadget_erp5_chat_room.html", {
scope: "room-gadget-" + room
})
.push(function (sub_gadget) {
room_gadget = sub_gadget;
room_gadget.element.setAttribute("id",
"room-gadget-" + nameToId(room));
gadget.element.querySelector(".chat-right-panel").insertBefore(
room_gadget.element,
gadget.element.querySelector(".chat-max-height-wrapper")
);
return room_gadget.changeState({
room: room,
local_sub_storage: gadget.state.local_sub_storage,
default_jio_type: gadget.state.default_jio_type,
default_erp5_url: gadget.state.default_erp5_url,
default_dav_url: gadget.state.default_dav_url,
query: {
limit: [0, 1000000000],
query: "portal_type: "Person" OR portal_type: "Text Post"",
},
local_sub_storage: {
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: "officejs-chat",
},
},
},
remote_sub_storage: remote_config,
query: 'portal_type: "Text Post"' // AND room: "' + room + '"'
}
});
})
.push(null, logError);
})
*/
// Get all messages stored in archive and add them to the list in order,
// using storeList because the messages are already in the archive
.declareMethod("getLocalArchive", function () {
var gadget = this;
return new RSVP.Queue()
.push(function () {
return gadget.jio_allDocs({
limit: [0, 1000000],
query: 'portal_type: "Text Post"',
sort_on: [["last_modified", "descending"]],
select_list: ["content"],
});
})
.push(function (message_list) {
var message_queue = new FastPriorityQueue(messageTimeCompare(true)),
i, i_len;
for (i = 0, i_len = message_list.data.total_rows; i < i_len; i += 1) {
try {
var message = JSON.parse(list[i]);
if (message && typeof message === "object") {
message_queue.add(message);
}
} catch (error) {}
}
var promise_list = [];
var message_dict = gadget.state.last_message_dict;
while (!message_queue.isEmpty()) {
var message = message_queue.poll();
if (message.room in gadget.state.room_set) {
promise_list.push(gadget.storeList(message));
promise_list.push(gadget.appendMessage(message));
}
}
gadget.state.initialized = true;
return RSVP.all(promise_list);
})
.push(null, logError);
return room_gadget.render();
});
})
// Send all requested messages in the list, in sorted order, to peer
.declareMethod("sendLocalArchive", function (request, source) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
var request_dict = {};
var room;
for (room in request.content.room_set) {
if (request.content.room_set.hasOwnProperty(room)
&& gadget.state.message_list_dict[room]) {
request_dict[room] = [];
var list = gadget.state.message_list_dict[room], i, i_len;
for (i = 0, i_len = list.length; i < i_len; i += 1) {
if (isNewMessage(
list[i], request.content.dict[room][list[i].name])) {
request_dict[room].push(list[i]);
}
}
}
}
return gadget.createMessage({type: "bundle", content: request_dict});
})
.push(function (message) {
return gadget.sendMessage(message, source);
})
.push(null, logError);
})
// Get all new messages from the sorted list of peer,
// add them to both the archive and list using storeArchive,
// and refresh, dedupe, and resort the list using refreshChat
.declareMethod("getRemoteArchive", function (bundle) {
/* Change to a different room.
* Parameters:
* - room: the name of the room to change to
* Effects:
* - changeState: room, room_set[room]
* - hide the room gadget contact panel
* - show the chat box
* - overwrite the chat box with chats from the jIO storage
*/
.declareMethod("changeRoom", function (room) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
var promise_list = [], room;
for (room in bundle.content) {
if (bundle.content.hasOwnProperty(room)) {
var list = gadget.state.message_list_dict[room];
var remote_list = bundle.content[room], i, i_len;
for (i = 0, i_len = remote_list.length; i < i_len; i += 1) {
promise_list.push(gadget.storeList(remote_list[i]));
promise_list.push(gadget.storeArchive(remote_list[i]));
}
}
}
return RSVP.all(promise_list);
})
return gadget.changeState({room: room})
.push(function () {
if (!gadget.state.room_set
[gadget.state.room]) {
gadget.state.room_set
[gadget.state.room] = true;
return gadget.deployMessage({
type: "notification",
content: gadget.state.name + " has joined.",
colour: "orange",
});
} else {
return;
if (!gadget.state.room_set[room]) {
gadget.state.room_set[room] = true;
}
})
.push(function () {
return gadget.refreshChat();
})
.push(null, logError);
});
})
// Ask a peer to send over a request
.declareMethod("requestRequest", function () {
var gadget = this;
return new RSVP.Queue()
.push(function () {
return gadget.createMessage({type: "doubler"});
})
.push(function (message) {
return gadget.sendMessage(message);
})
.push(null, logError);
})
// Send a request to update the local archive
.declareMethod("sendRequest", function () {
var gadget = this;
return pollUntilNotNull(gadget, 1000, 30000, function () {
return gadget.state.initialized;
}, function () {
return new RSVP.Queue()
.push(function () {
return gadget.createMessage({
type: "request",
content: {
room_set: gadget.state.room_set,
dict: gadget.state.last_message_dict,
},
});
})
.push(function (message) {
return gadget.sendMessage(message);
})
.push(null, logError);
});
})
// Create new message from its parameters
.declareMethod("createMessage", function (param_dict) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
return {
type: param_dict.type || "message",
name: param_dict.name || gadget.state.name,
room: param_dict.room || gadget.state.room,
time: param_dict.time || new Date(),
content: param_dict.content || "",
colour: param_dict.colour || "black",
};
})
.push(null, logError);
})
/* Deploy a new message.
* Parameters:
* - param_dict: the parameters to pass to createMessage
* Effects:
* - create a new message
* - append the message to the chat
* - store the message in jIO storage
*/
// Create new message and send it to peer
.declareMethod("deployMessage", function (param_dict) {
var gadget = this;
var message;
return new RSVP.Queue()
.push(function () {
return gadget.createMessage(param_dict);
})
var gadget = this,
message;
return gadget.createMessage(param_dict)
.push(function (result) {
message = result;
return gadget.storeList(message);
})
.push(function () {
return gadget.appendMessage(message);
})
.push(function () {
return gadget.storeArchive(message);
})
.push(function () {
return gadget.sendMessage(message);
})
.push(null, logError);
});
})
// Create new notification and keep it on own machine
/* Deploy a new notification.
* Parameters:
* - param_dict: the parameters to pass to createMessage
* Effects:
* - create a new message
* - append the message to the chat
*/
.declareMethod("deployNotification", function (param_dict) {
var gadget = this;
var notification;
return new RSVP.Queue()
.push(function () {
param_dict.type = "notification";
return gadget.createMessage(param_dict);
})
param_dict.type = "notification";
return gadget.createMessage(param_dict)
.push(function (result) {
notification = result;
return gadget.storeList(notification);
})
.push(function () {
return gadget.appendMessage(notification);
})
.push(null, logError);
return gadget.appendMessage(result);
});
})
// Get message from peer, store it in archive and list
.declareMethod("getMessage", function (message) {
/* Create a new message.
* Parameters:
* - name: the name of the sender of the message
* - room: the room from where the message is sent
* - time: the epoch time at which the message is sent
* - content: the content of the message
* - color: the colour of the message in the chat
* Effects: nothing
* Returns: a message object corresponding to the parameters
*/
.declareMethod("createMessage", function (param_dict) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
return gadget.notifyStatus(message.room, true);
})
.push(function () {
return gadget.storeList(message);
})
.push(function () {
return gadget.appendMessage(message);
})
.push(function () {
return gadget.storeArchive(message);
})
.push(null, logError);
return {
type: param_dict.type || "message",
name: param_dict.name || gadget.state.name,
room: param_dict.room || gadget.state.room,
time: param_dict.time || new Date(),
content: param_dict.content || "",
color: param_dict.color || "black"
};
})
// Store message in the archive
/* Append a message to the chat.
* Parameters:
* - message: the message object to append to the chat
* Effects:
* - create a HTML chat element from the message
* - append the chat element to the chat list
* - scroll the chat list down to show the chat element
*/
.declareMethod("appendMessage", function (message) {
var gadget = this,
container = gadget.element.querySelector(".chat-list");
container.appendChild(messageToChat(message));
container.scrollTop = container.scrollHeight;
})
/* Store a message in jIO storage.
* Parameters:
* - message: the message object to store in jIO storage
* Effects: store the message with all necessary properties into jIO
*/
.declareMethod("storeArchive", function (message) {
var gadget = this;
var id = message.room + "_" + message.name + "_"
+ getTime(message).toString();
return new RSVP.Queue()
.push(function () {
return gadget.jio_put(id, {
var gadget = this,
id = message.room + "_" + message.name + "_"
+ getTime(message).toString();
return gadget.getDeclaredGadget("room-gadget-" + gadget.state.room)
.push(function (room_gadget) {
return room_gadget.wrapJioCall("put", [id, {
portal_type: "Text Post",
parent_relative_url: "post_text_module",
reference: id,
author: message.name,
date_ms: getTime(message),
content: JSON.stringify(message),
});
})
.push(null, logError);
room: message.room,
content: JSON.stringify(message)
}]);
});
})
// Add message to the list
.declareMethod("storeList", function (message) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
if (isNewMessage(message,
gadget.state.last_message_dict
[message.room][message.name])) {
gadget.state.last_message_dict
[message.room][message.name]
= getTime(message);
}
gadget.state
.message_list_dict[message.room].push(message);
})
.push(null, logError);
})
// Appends a message to the chat box
.declareMethod("appendMessage", function (message) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
if (message.room === gadget.state.room) {
var container = gadget.element.querySelector(".chat-list");
container.appendChild(messageToChat(message));
container.scrollTop = container.scrollHeight;
/*
gadget.element
.querySelector(".chat-right-panel").style.height =
gadget.element
.querySelector(".chat-left-panel").style.height;
*/
}
})
.push(null, logError);
})
/* Overwrite the chat box with chats from the jIO storage.
* Parameters: nothing
* Effects:
* - get a sorted list of all chats in the current room from jIO storage
* - remove the entire chat box and append all chats from jIO storage
* - scroll the chat list down to show the chat element
*/
// Sort the list, dedupe, and overwrite the chat box,
// efficient because the archive is originally sorted
.declareMethod("refreshChat", function () {
var gadget = this;
return new RSVP.Queue()
.push(function () {
var container = gadget.element.querySelector(".chat-list");
return gadget.getDeclaredGadget("room-gadget-" + gadget.state.room)
.push(function (room_gadget) {
return room_gadget.wrapJioCall("allDocs", [{
query: 'portal_type: "Text Post" AND room: "'
+ gadget.state.room + '"',
limit: [0, 1000000],
sort_on: [["date_ms", "ascending"]],
select_list: ["content"]
}]);
})
.push(function (result_list) {
var i, message, message_list = [],
container = gadget.element.querySelector(".chat-list");
for (i = 0; i < result_list.data.total_rows; i += 1) {
try {
message = JSON.parse(result_list.data.rows[i].value.content);
if (message && typeof message === "object") {
message_list.push(message);
}
} catch (ignore) {}
}
while (container.firstChild) {
container.removeChild(container.firstChild);
}
var old_list = gadget.state.message_list_dict
[gadget.state.room];
old_list.sort(messageTimeComparator);
var new_list = [];
var last_message, i, i_len;
for (i = 0, i_len = old_list.length; i < i_len; i += 1) {
var message = old_list[i];
if (isSameMessage(last_message, message)) {
continue;
}
last_message = message;
new_list.push(message);
container.appendChild(messageToChat(message));
for (i = 0; i < message_list.length; i += 1) {
container.appendChild(messageToChat(message_list[i]));
}
gadget.state.message_list_dict
[gadget.state.room] = new_list;
container.scrollTop = container.scrollHeight;
/*
gadget.element
.querySelector(".chat-right-panel").style.height =
gadget.element
.querySelector(".chat-left-panel").style.height;
*/
})
.push(null, logError);
});
})
// Notify when a new message appears and denotify when it is seen
.declareMethod("notifyStatus", function (room, notify) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
var favicon_url;
var class_name;
if (notify) {
favicon_url = gadget.state.alert_icon;
class_name = "notify";
} else {
favicon_url = gadget.state.default_icon;
class_name = "current";
}
var link = document.querySelector("link[rel*='icon']")
|| document.createElement("link");
link.type = "image/x-icon";
link.rel = "shortcut icon";
link.href = favicon_url;
document.head.appendChild(link);
var contact = gadget.element.querySelector("#chat-contact-" + room);
contact.className = class_name;
})
.push(null, logError);
})
// Join a different room
.declareMethod("changeRoom", function (room) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
styleElementByQuery(gadget,
"#webrtc-gadget-" + gadget.state.room, "none");
styleElementByQuery(gadget, ".chat-right-panel-chat", "flex");
gadget.element.querySelector(".chat-title")
.textContent = "Room: " + room;
gadget.state.room = room;
if (!(room in gadget.state.room_set)) {
return new RSVP.Queue()
.push(function () {
gadget.state.room_set[room] = false;
gadget.state.message_list_dict[room] = [];
gadget.state.last_message_dict[room] = {};
})
.push(function () {
return gadget.getLocalArchive();
})
.push(function () {
return gadget.requestRequest();
})
.push(null, logError);
}
})
.push(function () {
return gadget.refreshChat();
})
.push(null, logError);
})
/* Parse chat commands.
* Parameters:
* - chat: the plaintext chat message, which starts with "/"
* Effects: parses the command in the chat message and responds accordingly
*/
// Create a new contact
.declareMethod("createContact", function (param_dict) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
return gadget.appendContact(param_dict.room);
})
.push(function () {
return gadget.chooseRoom(param_dict);
})
.push(function () {
return gadget.editContact(param_dict.room);
})
.push(null, logError);
})
.declareMethod("parseCommand", function (chat) {
var gadget = this,
i,
split = chat.slice(1).split(" "),
command = split.shift(),
argument = split.join(" "),
promise_list = [],
help_list = [
"Available commands:",
"/join [room]: connects you to [room]",
"/help: displays this help box",
"/leave: disconnects you from the current room",
"/quit: disconnects from the chat and refreshes the page"
];
switch (command) {
// change to a room that has already been joined
case "join":
if (gadget.state.room_set[argument]) {
return gadget.changeRoom(argument);
}
return gadget.deployNotification({
content: "You must first be connected to room '"
+ argument + "' via a shared jIO storage to join it!",
color: "red"
});
// Add a contact as some HTML element
.declareMethod("appendContact", function (room) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
if (!room.trim()) {
throw "An invisible name is not allowed! You couldn't click on it!";
}
if (gadget.element.querySelector("#chat-contact-" + room)) {
throw "A contact with the same name already exists!";
}
var contact = document.createElement("li");
contact.appendChild(document.createTextNode(room));
contact.setAttribute("id", "chat-contact-" + room);
gadget.element.querySelector(".contact-list").appendChild(contact);
gadget.element.querySelector(
"#chat-contact-" + gadget.state.room).className = "";
contact.className = "current";
contact.addEventListener("click", function (event) {
return new RSVP.Queue()
.push(function () {
return gadget.notifyStatus(room, false);
})
.push(function () {
gadget.element.querySelector(
"#chat-contact-" + gadget.state.room).className = "";
gadget.element.querySelector(
"#chat-contact-" + room).className = "current";
})
.push(function () {
if (gadget.state.room_set[room]) {
return gadget.changeRoom(room);
} else {
return gadget.editContact(room);
}
})
.push(null, logError);
}, false);
return;
})
})
// leave the current room
case "leave":
return new RSVP.Queue()
.push(function () {
if (gadget.state.room === gadget.state.name) {
return gadget.deployNotification({
content: "You cannot leave your own room!",
color: "red"
});
}
gadget.state.room_set[gadget.state.room] = false;
return gadget.deployMessage({
content: gadget.state.name + " has quit.",
color: "orange"
});
})
.push(function () {
return gadget.changeRoom(gadget.state.name);
});
// Edit a contact in the right panel
.declareMethod("editContact", function (room) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
styleElementByQuery(gadget, ".chat-right-panel-chat", "none");
styleElementByQuery(gadget,
"#webrtc-gadget-" + gadget.state.room, "none");
styleElementByQuery(gadget, "#webrtc-gadget-" + room, "block");
gadget.state.room = room;
gadget.element.querySelector(".chat-title")
.textContent = "Contact: " + room;
})
.push(null, logError);
// quit the entire chat
case "quit":
return window.location.reload();
// print a list of available commands
case "help":
for (i = 0; i < help_list.length; i += 1) {
promise_list.push(gadget.deployNotification({
content: help_list[i],
color: "green"
}));
}
return RSVP.all(promise_list);
}
})
// Parse chat commands
.declareMethod("parseCommand", function (chat) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
var split = chat.slice(1).split(" ");
var command = split.shift();
var argument = split.join(" ");
switch (command) {
case "join":
if (gadget.state.room_set[argument]) {
return gadget.changeRoom(argument);
} else {
return gadget.deployNotification({
content: "You must first be connected to room '"
+ argument + "' via WebRTC to join it!",
colour: "red",
});
}
case "leave":
return new RSVP.Queue()
.push(function () {
if (gadget.state.room === gadget.state.name) {
return gadget.deployNotification({
content: "You cannot leave your own room!",
colour: "red"
});
} else {
delete gadget.state.room_set
[gadget.state.room];
return gadget.deployMessage({
type: "notification",
content: gadget.state.name + " has quit.",
colour: "orange",
});
}
})
.push(function () {
return gadget.changeRoom(gadget.state.name);
})
.push(null, logError);
case "quit":
window.location.reload();
break;
case "help":
var help_list = [
"Available commands:",
"/join [room]: connects you to [room]",
"/help: displays this help box",
"/leave: disconnects you from the current room",
"/quit: disconnects from the chat and refreshes the page",
];
var promise_list = [];
return new RSVP.Queue()
.push(function () {
var i, i_len;
for (i = 0, i_len = help_list.length; i < i_len; i += 1) {
promise_list.push(gadget.deployNotification({
content: help_list[i],
colour: "green",
}));
}
return RSVP.all(promise_list);
})
.push(null, logError);
}
});
})
// Call the appropriate function based on the form submitted.
.onEvent("submit", function (event) {
var gadget = this;
var gadget = this,
content;
switch (event.target.className) {
case "sync-form":
return new RSVP.Queue()
.push(function () {
return gadget.jio_repair();
})
.push(function () {
return gadget.getLocalArchive();
})
.push(function () {
return gadget.requestRequest();
})
.push(null, logError);
case "edit-form":
return gadget.editContact(gadget.state.room);
case "join-form":
var contact = resetInputValue(event.target.elements.content);
return gadget.createContact({room: contact, role: "guest"});
case "make-form":
var room = resetInputValue(event.target.elements.content);
return gadget.createContact({room: room, role: "host"});
case "send-form":
var content = resetInputValue(event.target.elements.content);
if (content.indexOf("/") === 0) {
return gadget.parseCommand(content);
} else {
return gadget.deployMessage({content: content});
}
case "sync-form":
return gadget.getDeclaredGadget("room-gadget-" + gadget.state.room)
.push(function (room_gadget) {
return room_gadget.wrapJioCall("repair");
})
.push(function () {
return gadget.refreshChat();
});
case "edit-form":
content = resetInputValue(event.target.elements.content);
return gadget.changeState({contact: content});
case "join-form":
content = resetInputValue(event.target.elements.content);
return gadget.createContact(content);
case "send-form":
content = resetInputValue(event.target.elements.content);
if (content.indexOf("/") === 0) {
return gadget.parseCommand(content);
}
return gadget.deployMessage({content: content})
.push(function () {
return gadget.getDeclaredGadget("room-gadget-" + gadget.state.room);
})
.push(function (room_gadget) {
return room_gadget.wrapJioCall("repair");
})
.push(function () {
return gadget.refreshChat();
})
}
})
/* Send a farewell message when chat is closed.
* Parameters: nothing
* Effects: send a message when chat is closed
*/
.declareService(function () {
var gadget = this;
return new RSVP.Queue()
......@@ -1159,25 +711,19 @@
return promiseEventListener(window, "beforeunload", true);
})
.push(function () {
if (gadget.state.initialized) {
var promise_list = [], room;
for (room in gadget.state.room_set) {
if (gadget.state.room_set.hasOwnProperty(room)
var promise_list = [], room;
for (room in gadget.state.room_set) {
if (gadget.state.room_set.hasOwnProperty(room)
&& gadget.state.room_set[room]) {
promise_list.push(gadget.deployMessage({
type: "notification",
content: gadget.state.name + " has quit.",
room: room,
colour: "orange",
}));
}
promise_list.push(gadget.deployMessage({
content: gadget.state.name + " has quit.",
room: room,
color: "orange"
}));
}
return RSVP.all(promise_list);
} else {
return;
}
})
.push(null, logError);
return RSVP.all(promise_list);
});
});
}(window, document, RSVP, rJS, FastPriorityQueue));
\ No newline at end of file
}(window, document, RSVP, rJS, promiseEventListener));
\ No newline at end of file
......@@ -12,15 +12,15 @@
<form class="connect-form">
<label>E-mail Address:</label>
<input type="email" name="user_email" required="required" />
<label>Default WebRTC Authentication Method:</label>
<label>Default Shared Storage Type:</label>
<br />
<input type="radio" name="default_webrtc_storage" value="erp5">ERP5</input>
<input type="radio" name="default_webrtc_storage" value="dropbox">Dropbox</input>
<input type="radio" name="default_jio_type" value="erp5">ERP5</input>
<input type="radio" name="default_jio_type" value="dav">WebDAV</input>
<br />
<label>Default Dropbox Folder:</label>
<input type="text" name="default_dropbox_folder" placeholder="/Apps/OfficeJS Chat/" />
<label>Default ERP5 URL:</label>
<input type="text" name="default_erp5_url", placeholder="https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/" />
<input type="text" name="default_erp5_url", placeholder="https://softinst75770.host.vifib.net/erp5/web_site_module/hateoas" />
<label>Default WebDAV URL:</label>
<input type="text" name="default_dav_url", placeholder="https://softinst75722.host.vifib.net/share" />
<input type="submit" />
</form>
</body>
......
/* global window, RSVP, rJS */
/* jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global window, RSVP, rJS */
(function (window, RSVP, rJS) {
'use strict';
"use strict";
/* Settings modified:
* - user_email
* - default_webrtc_storage
* - default_dropbox_folder
* - default_jio_type
* - default_erp5_url
* - default_dav_url
*/
/* Store the given connection settings
/* Store the given connection settings.
* Parameters: all from the connect form,
* - user_email: an email, which acts as a username, example: eugene@abc.xyz
* - default_webrtc_storage: the type of storage to select by default
* for sharing WebRTC negotiations, example: dropbox
* - default_dropbox_folder: the Dropbox folder to select by default
* for sharing WebRTC negotiations, example: /Apps/OfficeJS Chat/
* - default_erp5_url: the ERP5 URL to select by default for sharing,
* example: https://softinst75770.host.vifib.net/erp5/web_site_module/
* - default_jio_type: the type of storage to select by default
* for sharing chat messages, example: erp5
* - default_erp5_url: the ERP5 URL to select by default, example:
* https://softinst75770.host.vifib.net/erp5/web_site_module/hateoas
* - default_dav_url: the WebDAV URL to select by default for sharing,
* example: https://softinst75722.host.vifib.net/share
* Effects:
* - set setting: user_email, default_webrtc_storage,
* default_dropbox_folder, default_erp5_url
* - setSetting: user_email, default_jio_type,
* default_erp5_url, default_dav_url
* - redirect to the front page
*/
function setConnectConfiguration(gadget, event) {
const fields = ['user_email', 'default_webrtc_storage',
'default_dropbox_folder', 'default_erp5_url'];
return new RSVP.Queue()
.push(function () {
const queue = new RSVP.Queue();
for (let i = 0, i_len = fields.length; i < i_len; i++) {
const field = fields[i];
if (event.target.hasOwnProperty(field) && event.target[field].value) {
queue.push(function () {
return gadget.setSetting(field, event.target[field].value);
});
}
}
return queue;
})
var i, field, queue = new RSVP.Queue(),
fields = ["user_email", "default_jio_type",
"default_erp5_url", "default_dav_url"],
callSetting = function (setting) {
return function () {
return gadget.setSetting(setting, event.target[setting].value);
};
};
// must call setSetting synchronously; RSVP.all causes race conditions
for (i = 0; i < fields.length; i += 1) {
field = fields[i];
if (event.target.hasOwnProperty(field) && event.target[field].value) {
queue.push(callSetting(field));
}
}
return queue
.push(function () {
return gadget.redirect();
});
}
rJS(window)
// Neither state to set nor ready to initialize
// The following functions are all acquired from erp5_launcher_nojqm.js
.declareAcquiredMethod('updateHeader', 'updateHeader')
.declareAcquiredMethod('redirect', 'redirect')
.declareAcquiredMethod('setSetting', 'setSetting')
/* Render the gadget
// Neither state to set nor ready to initialize.
// The following functions are all acquired from erp5_page_launcher.
.declareAcquiredMethod("updateHeader", "updateHeader")
.declareAcquiredMethod("redirect", "redirect")
.declareAcquiredMethod("setSetting", "setSetting")
/* Render the gadget.
* Parameters: nothing
* Effects:
* - update header, page_title to 'Connect to Chat'
* Effects: update header, page_title to "Connect to Chat"
*/
.declareMethod('render', function () {
.declareMethod("render", function () {
var gadget = this;
return gadget.updateHeader({
page_title: 'Connect to Chat',
page_title: "Connect to Chat",
submit_action: true
});
})
/* Manually click submit button when the right button is clicked,
* so that HTML5 form validation is automatically used
* so that HTML5 form validation is automatically used.
*/
.declareMethod('triggerSubmit', function (event) {
this.element.querySelector('input[type="submit"]').click();
.declareMethod("triggerSubmit", function () {
this.element.querySelector("input[type='submit']").click();
})
// Call setConnectionConfiguration when either proceed button is clicked
.onEvent('submit', function (event) {
const gadget = this;
// Call setConnectionConfiguration when either proceed button is clicked.
.onEvent("submit", function (event) {
var gadget = this;
return setConnectConfiguration(gadget, event);
});
......
......@@ -74,13 +74,13 @@
},
},
};
return RSVP.all([
gadget.setSetting('jio_storage_description', configuration),
gadget.setSetting('redirect_after_reload', {
command: 'display',
options: {page: 'sync'},
})
]);
return gadget.setSetting('jio_storage_description', configuration);
})
.push(function () {
return gadget.setSetting('redirect_after_reload', {
command: 'display',
options: {page: 'sync'},
});
})
.push(function () {
return gadget.reload();
......
/* global window, RSVP, rJS, URI */
/* global window, RSVP, rJS */
/* jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
(function (window, RSVP, rJS, URI) {
(function (window, RSVP, rJS) {
'use strict';
/* Settings required:
......@@ -65,18 +65,17 @@
},
remote_sub_storage: {
type: 'erp5',
erp5_url: erp5_url,
url: (new URI('hateoas')).absoluteTo(erp5_url).toString(),
url: erp5_url,
default_view_reference: 'jio_view',
},
};
return RSVP.all([
gadget.setSetting('jio_storage_description', configuration),
gadget.setSetting('redirect_after_reload', {
command: 'display',
options: {page: 'sync'},
})
]);
return gadget.setSetting('jio_storage_description', configuration);
})
.push(function () {
return gadget.setSetting('redirect_after_reload', {
command: 'display',
options: {page: 'sync'},
});
})
.push(function () {
return gadget.reload();
......@@ -131,7 +130,7 @@
})
.push(function (configuration) {
gadget.element.querySelector('input[name="erp5_url"]')
.value = configuration.remote_sub_storage.erp5_url;
.value = configuration.remote_sub_storage.url;
return;
});
} else {
......@@ -150,4 +149,4 @@
return setErp5Configuration(gadget, event);
});
}(window, RSVP, rJS, URI));
\ No newline at end of file
}(window, RSVP, rJS));
\ No newline at end of file
web_page_module/adapter_js
web_page_module/erp5_page_launcher*
web_page_module/fast_priority_queue_js
web_page_module/gadget_erp5_chat_panel_*
web_page_module/gadget_erp5_chat_webrtc_*
web_page_module/gadget_erp5_nojquery_css
......
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