Commit b9917283 authored by Eugene Shen's avatar Eugene Shen

Squeeze WebRTC authentication into chat panel

Make WebRTC gadget spawn a new hidden div inside the chat panel
instead of indefinitely expanding downwards, highlight current room,
remove automatic login and authentication, add field for hateoas URL,
move all utility functions such as logError into gadget_global.js,
add hasOwnProperty, send join/quit notifications in bundles once more,
fix cannot-make-second-connection bug by resetting host candidate,
change declareService to submit onEvent, split submit buttons among
separate forms, and declare chat panel functions as gadget methods.
parent af967d44
body { body {
padding: 20px; padding: 20px;
} }
error { .error {
color: orange; color: red;
} }
h3 { h3 {
color: brown; color: brown;
...@@ -16,7 +16,7 @@ label { ...@@ -16,7 +16,7 @@ label {
display: block; display: block;
} }
input[type="text"] { input[type="text"] {
width: 360px; width: 50%;
} }
.radio-item { .radio-item {
display: inline-block; display: inline-block;
...@@ -28,7 +28,7 @@ textarea { ...@@ -28,7 +28,7 @@ textarea {
.chat-box { .chat-box {
display: flex; display: flex;
width: 100%; width: 100%;
height: 80vh; min-height: 80vh;
} }
.chat-left-panel { .chat-left-panel {
display: flex; display: flex;
...@@ -46,6 +46,11 @@ textarea { ...@@ -46,6 +46,11 @@ textarea {
border: 1.5px solid; border: 1.5px solid;
box-sizing: border-box; box-sizing: border-box;
} }
.chat-right-panel-chat {
display: flex;
flex-direction: inherit;
flex: 1;
}
.contact-list { .contact-list {
flex: 1; flex: 1;
padding: 0; padding: 0;
...@@ -88,4 +93,7 @@ img { ...@@ -88,4 +93,7 @@ img {
.contact-list li.notify { .contact-list li.notify {
font-weight: bold; font-weight: bold;
color: red; color: red;
}
.contact-list li.current {
color: orange;
} }
\ No newline at end of file
...@@ -55,25 +55,16 @@ ...@@ -55,25 +55,16 @@
<input type="text" name="remote_dav_pass" placeholder="correct horse battery staple" /> <input type="text" name="remote_dav_pass" placeholder="correct horse battery staple" />
<h3>Default WebRTC Configuration</h3> <h3>Default WebRTC Configuration</h3>
<label>Automatically connect to own room:
<input type="checkbox" name="auto" />
</label>
<label>Default storage:</label> <label>Default storage:</label>
<input type="text" name="auth" placeholder="dropbox" /> <input type="text" name="auth" placeholder="dropbox" />
<label>Dropbox folder:</label> <label>Dropbox folder:</label>
<input type="text" name="auth_dropbox_url" placeholder="/Apps/OfficeJS Chat" /> <input type="text" name="auth_dropbox_url" placeholder="/Apps/OfficeJS Chat" />
<label>ERP5 URL:</label> <label>ERP5 URL:</label>
<input type="text" name="auth_erp5_url" placeholder="https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/" /> <input type="text" name="auth_erp5_url" placeholder="https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/" />
<label>ERP5 Hateoas:</label>
<input type="text" name="auth_hateoas_url", placeholder="https://softinst75770.host.vifib.net/erp5/web_site_module/hateoas" />
<br /> <br />
<input type="submit" value="Login!" /> <input type="submit" name="login" value="Login!" />
</form>
<form class="room-form">
<h3>Join Rooms</h3>
<input type="text" name="room" required="required" />
<br />
<input type="submit" name="host" value="Create a new room!" />
<input type="submit" name="guest" value="Join an existing room!" />
</form> </form>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -14,38 +14,14 @@ ...@@ -14,38 +14,14 @@
* e.g. https://softinst75722.host.vifib.net/share * e.g. https://softinst75722.host.vifib.net/share
* remote_dav_user: your WebDAV username, e.g. eyqs * remote_dav_user: your WebDAV username, e.g. eyqs
* remote_dav_pass: your WebDAV password, e.g. correct horse battery staple * remote_dav_pass: your WebDAV password, e.g. correct horse battery staple
* login: if present (e.g. login=foo), will automatically click Login
* auth: the default type of authentication to use for WebRTC rooms, * auth: the default type of authentication to use for WebRTC rooms,
* either dropbox or erp5 * either dropbox or erp5
* auth_dropbox_url: the shared Dropbox folder to act as your auth folder, * auth_dropbox_url: the shared Dropbox folder to act as your auth folder,
* e.g. /Apps/OfficeJS Chat * e.g. /Apps/OfficeJS Chat
* auth_erp5_url: the shared ERP5 instance to act as your auth folder, * auth_erp5_url: the shared ERP5 instance to act as your auth folder,
* e.g. https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/ * e.g. https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/
* auto: if preset (e.g. auto=go), will automatically click Authenticate
*/ */
function logError(error) {
console.log(error);
}
function logQueue(action) {
return new RSVP.Queue()
.push(function () {
return action;
})
.push(null, logError);
}
function showElementByClass(gadget, query) {
gadget.state_parameter_dict.element
.querySelector(query).style.display = "block";
}
function hideElementByClass(gadget, query) {
gadget.state_parameter_dict.element
.querySelector(query).style.display = "none";
}
function cleanId(input_id) { function cleanId(input_id) {
const reserved = ["_", "&", "=", ",", ";", "\\"]; const reserved = ["_", "&", "=", ",", ";", "\\"];
for (let i = 0, i_len = reserved.length; i < i_len; i++) { for (let i = 0, i_len = reserved.length; i < i_len; i++) {
...@@ -85,7 +61,7 @@ ...@@ -85,7 +61,7 @@
auth: null, auth: null,
dropbox_url: null, dropbox_url: null,
erp5_url: null, erp5_url: null,
auto: null, hateoas_url: null,
// Dropbox OAuth only allows 500 characters in the state query // Dropbox OAuth only allows 500 characters in the state query
max_state_length: 500, max_state_length: 500,
}; };
...@@ -100,7 +76,7 @@ ...@@ -100,7 +76,7 @@
}) })
.declareService(function () { .declareService(function () {
const gadget = this; const gadget = this;
return this.render(); return gadget.render();
}) })
.allowPublicAcquisition('wrapJioAccess', function (param_list) { .allowPublicAcquisition('wrapJioAccess', function (param_list) {
...@@ -129,22 +105,90 @@ ...@@ -129,22 +105,90 @@
.push(null, logError); .push(null, logError);
}) })
.allowPublicAcquisition('parseParams', function (param_list) { .allowPublicAcquisition('chooseRoom', function (param_list) {
const gadget = this;
return gadget.parseParams.apply(gadget, param_list);
})
.declareMethod('parseParams', function (param_string) {
const gadget = this; const gadget = this;
const param_dict = {}; const param_dict = param_list[0];
param_dict.auto = getQueryValue(["auto"], param_string); let webrtc_gadget;
param_dict.room = getQueryValue(["room"], param_string); return new RSVP.Queue()
param_dict.role = getQueryValue(["role"], param_string); .push(function () {
param_dict.auth = getQueryValue(["auth"], param_string); return gadget.declareGadget(
param_dict.dropbox_url = "gadget_erp5_chat_webrtc.html", {
getQueryValue(["auth_dropbox_url", "ad"], param_string); scope: "webrtc_gadget_" + param_dict.room});
param_dict.erp5_url = getQueryValue(["auth_erp5_url", "ae"], param_string); })
return gadget.chooseRoom(param_dict); .push(function (subgadget) {
webrtc_gadget = subgadget;
return gadget.getDeclaredGadget("chat_gadget");
})
.push(function (chat_gadget) {
const fields = webrtc_gadget.state_parameter_dict.element
.querySelector(".contact-form").elements;
fields.dropbox_url.value = param_dict.dropbox_url
|| gadget.state_parameter_dict.dropbox_url;
fields.erp5_url.value = param_dict.erp5_url
|| gadget.state_parameter_dict.erp5_url;
fields.hateoas_url.value = param_dict.hateoas_url
|| gadget.state_parameter_dict.hateoas_url;
webrtc_gadget.state_parameter_dict.element.querySelector(".auth-form")
.auth.value = param_dict.auth || gadget.state_parameter_dict.auth;
chat_gadget.state_parameter_dict.element
.querySelector(".chat-right-panel").insertBefore(
webrtc_gadget.state_parameter_dict.element,
chat_gadget.state_parameter_dict.element
.querySelector(".chat-right-panel-chat"));
webrtc_gadget.state_parameter_dict.login_dict = {
folder: gadget.state_parameter_dict.folder,
room: param_dict.room,
name: gadget.state_parameter_dict.name,
role: param_dict.role,
};
webrtc_gadget.state_parameter_dict.dataChannelOnopen = function () {
return chat_gadget.changeRoom(param_dict.room);
};
webrtc_gadget.state_parameter_dict.dataChannelOnmessage =
function (event) {
const message = JSON.parse(event.data);
const source = event.srcElement;
if (message.type === "notification"
|| message.type === "message") {
return new RSVP.Queue()
.push(function () {
return chat_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_parameter_dict.archive_amount += 1;
if (webrtc_gadget.state_parameter_dict.archive_amount >=
webrtc_gadget.state_parameter_dict.guest_amount) {
webrtc_gadget.state_parameter_dict.archive_amount = 0;
return new RSVP.Queue()
.push(function () {
return chat_gadget.getRemoteArchive(message);
})
.push(function () {
return chat_gadget.requestRequest();
})
.push(null, logError);
}
} else {
return chat_gadget.getRemoteArchive(message);
}
} else if (message.type === "request") {
return chat_gadget.sendLocalArchive(message, source);
} else if (message.type === "doubler") {
return chat_gadget.sendRequest();
}
};
return webrtc_gadget.render();
})
.push(null, logError);
}) })
.declareMethod('render', function () { .declareMethod('render', function () {
...@@ -152,20 +196,20 @@ ...@@ -152,20 +196,20 @@
const gadget = this; const gadget = this;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
hideElementByClass(gadget, ".room-form");
const state_output = getQueryValue(["state"], url); const state_output = getQueryValue(["state"], url);
if (state_output) { if (state_output) {
url += window.atob(state_output); url += window.atob(state_output);
} }
const fields = gadget.state_parameter_dict.element const fields = gadget.state_parameter_dict.element
.querySelector(".login-form").elements; .querySelector(".login-form").elements;
let state_input = "";
// A list of login-form fields and short keys in order of priority // A list of login-form fields and short keys in order of priority
const query_list = [["name", "m"], ["folder", "f"], ["remote", "r"], const query_list = [["name", "m"], ["folder", "f"], ["remote", "r"],
["remote_dropbox_url", "rd"], ["remote_erp5_url", "re"], ["remote_dropbox_url", "rd"], ["remote_erp5_url", "re"],
["remote_dav_url", "rv"], ["remote_dav_user", "rvu"], ["remote_dav_url", "rv"], ["remote_dav_user", "rvu"],
["remote_dav_pass", "rvp"], ["auth", "a"], ["remote_dav_pass", "rvp"], ["auth", "a"],
["auth_dropbox_url", "ad"], ["auth_erp5_url", "ae"]] ["auth_dropbox_url", "ad"], ["auth_erp5_url", "ae"],
let state_input = ""; ["auth_hateoas_url", "ah"]]
for (let i = 0, i_len = query_list.length; i < i_len; i++) { for (let i = 0, i_len = query_list.length; i < i_len; i++) {
const query = query_list[i]; const query = query_list[i];
setQueryValue(query, url, fields[query[0]]); setQueryValue(query, url, fields[query[0]]);
...@@ -175,23 +219,12 @@ ...@@ -175,23 +219,12 @@
state_input += query_string; state_input += query_string;
} }
} }
if (getQueryValue(["auto", "o"], url)) {
fields.auto.checked = "Checked";
if (window.btoa(state_input + "&o=o").length <
gadget.state_parameter_dict.max_state_length) {
state_input += "&o=o";
}
}
gadget.state_parameter_dict.element.querySelector(".dropbox-link") gadget.state_parameter_dict.element.querySelector(".dropbox-link")
.href = "https://www.dropbox.com/1/oauth2/authorize" .href = "https://www.dropbox.com/1/oauth2/authorize"
+ "?client_id=igeiyv4pkt0y0mm&response_type=token" + "?client_id=igeiyv4pkt0y0mm&response_type=token"
+ "&redirect_uri=https://softinst75770.host.vifib.net/erp5/" + "&redirect_uri=https://softinst75770.host.vifib.net/erp5/"
+ "web_site_module/web_chat/&state=" + window.btoa(state_input); + "web_site_module/web_chat/&state=" + window.btoa(state_input);
if (getQueryValue(["login"], url)) { return;
return gadget.login();
} else {
return;
}
}) })
.push(null, logError); .push(null, logError);
}) })
...@@ -205,8 +238,7 @@ ...@@ -205,8 +238,7 @@
"gadget_erp5_chat_panel.html", {scope: "chat_gadget"}); "gadget_erp5_chat_panel.html", {scope: "chat_gadget"});
}) })
.push(function (subgadget) { .push(function (subgadget) {
hideElementByClass(gadget, ".login-form"); styleElementByQuery(gadget, ".login-form", "none");
showElementByClass(gadget, ".room-form");
chat_gadget = subgadget; chat_gadget = subgadget;
gadget.state_parameter_dict.element.insertBefore( gadget.state_parameter_dict.element.insertBefore(
chat_gadget.state_parameter_dict.element, chat_gadget.state_parameter_dict.element,
...@@ -214,10 +246,11 @@ ...@@ -214,10 +246,11 @@
const fields = gadget.state_parameter_dict.element const fields = gadget.state_parameter_dict.element
.querySelector(".login-form").elements; .querySelector(".login-form").elements;
gadget.state_parameter_dict.auth = fields.auth.value; gadget.state_parameter_dict.auth = fields.auth.value;
gadget.state_parameter_dict.dropbox_url gadget.state_parameter_dict.dropbox_url =
= fields.auth_dropbox_url.value; fields.auth_dropbox_url.value;
gadget.state_parameter_dict.erp5_url = fields.auth_erp5_url.value; gadget.state_parameter_dict.erp5_url = fields.auth_erp5_url.value;
gadget.state_parameter_dict.auto = !!fields.auto.checked; gadget.state_parameter_dict.hateoas_url =
fields.auth_hateoas_url.value;
gadget.state_parameter_dict.name = cleanId(fields.name.value); gadget.state_parameter_dict.name = cleanId(fields.name.value);
gadget.state_parameter_dict.folder = cleanId(fields.folder.value); gadget.state_parameter_dict.folder = cleanId(fields.folder.value);
chat_gadget.state_parameter_dict.name = chat_gadget.state_parameter_dict.name =
...@@ -234,19 +267,12 @@ ...@@ -234,19 +267,12 @@
dav_pass: fields.remote_dav_pass.value, dav_pass: fields.remote_dav_pass.value,
}); });
}) })
// Log each person in as the host of their own room with their name
.push(function () { .push(function () {
return chat_gadget.startRoom(); return chat_gadget.render();
}) })
.push(function () { .push(function () {
if (gadget.state_parameter_dict.auto) { return chat_gadget.createContact({
return gadget.parseParams( room: gadget.state_parameter_dict.name, role: "host"});
"auto=a&role=host&room=" + gadget.state_parameter_dict.name
+ "&"+ decodeURIComponent(document.URL));
} else {
return gadget.chooseRoom({
room: gadget.state_parameter_dict.name, role: "host"});
}
}) })
.push(null, logError); .push(null, logError);
}) })
...@@ -339,119 +365,12 @@ ...@@ -339,119 +365,12 @@
.push(null, logError); .push(null, logError);
}) })
.declareMethod('chooseRoom', function (param_dict) { .onEvent('submit', function (event) {
const gadget = this; const gadget = this;
let webrtc_gadget; switch (event.target.className) {
return new RSVP.Queue() case "login-form":
.push(function () { return gadget.login();
return gadget.declareGadget(
"gadget_erp5_chat_webrtc.html", {
scope: "webrtc_gadget_" + param_dict.room});
})
.push(function (subgadget) {
webrtc_gadget = subgadget;
return gadget.getDeclaredGadget("chat_gadget");
})
.push(function (chat_gadget) {
const fields = webrtc_gadget.state_parameter_dict.element
.querySelector(".auth-form").elements;
fields.auth.value = param_dict.auth
|| gadget.state_parameter_dict.auth;
fields.dropbox_url.value = param_dict.dropbox_url
|| gadget.state_parameter_dict.dropbox_url;
fields.erp5_url.value = param_dict.erp5_url
|| gadget.state_parameter_dict.erp5_url;
gadget.state_parameter_dict.element.insertBefore(
webrtc_gadget.state_parameter_dict.element,
gadget.state_parameter_dict.element
.querySelector(".room-form").nextSibling);
webrtc_gadget.state_parameter_dict.login_dict = {
folder: gadget.state_parameter_dict.folder,
room: param_dict.room,
name: gadget.state_parameter_dict.name,
role: param_dict.role,
};
webrtc_gadget.state_parameter_dict.dataChannelOnopen = function () {
return chat_gadget.changeRoom(param_dict.room);
};
webrtc_gadget.state_parameter_dict.dataChannelOnmessage =
function (event) {
const message = JSON.parse(event.data);
const source = event.srcElement;
if (message.type === "notification"
|| message.type === "message") {
return new RSVP.Queue()
.push(function () {
return chat_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_parameter_dict.archive_amount += 1;
if (webrtc_gadget.state_parameter_dict.archive_amount >=
webrtc_gadget.state_parameter_dict.guest_amount) {
webrtc_gadget.state_parameter_dict.archive_amount = 0;
return new RSVP.Queue()
.push(function () {
return chat_gadget.getRemoteArchive(message);
})
.push(function () {
return chat_gadget.requestRequest();
})
.push(null, logError);
}
} else {
return chat_gadget.getRemoteArchive(message);
}
} else if (message.type === "request") {
return chat_gadget.sendLocalArchive(message, source);
} else if (message.type === "doubler") {
return chat_gadget.sendRequest();
}
};
return webrtc_gadget.render();
})
.push(function () {
if (param_dict.auto) {
return webrtc_gadget.authenticate();
} else {
return;
}
})
.push(null, logError);
})
.declareService(function () {
const gadget = this;
function handleSubmit(event) {
switch (event.target.className) {
case "login-form":
return gadget.login();
case "room-form":
return new RSVP.Queue()
.push(function () {
return gadget.chooseRoom({
room: event.target.elements.room.value,
role: event.target
.querySelector("input[type=submit]:focus").name
});
})
.push(function () {
event.target.elements.room.value = "";
return;
})
.push(null, logError);
}
} }
return loopEventListener(
gadget.state_parameter_dict.element, "submit", false, handleSubmit);
}); });
}(window, document, loopEventListener, rJS, RSVP)); }(window, document, loopEventListener, rJS, RSVP));
\ No newline at end of file
...@@ -17,18 +17,30 @@ ...@@ -17,18 +17,30 @@
<div class="chat-left-panel"> <div class="chat-left-panel">
<h4 class="center">Contacts</h4> <h4 class="center">Contacts</h4>
<ul class="contact-list"></ul> <ul class="contact-list"></ul>
<form class="manage-form"> <form class="sync-form">
<input type="submit" name="sync" value="Synchronize!" /> <input type="submit" value="Synchronize!" />
<input type="submit" name="contact" value="Add new contact!" /> </form>
<form class="edit-form">
<input type="submit" value="Edit contact!" />
</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)!" />
</form> </form>
</div> </div>
<div class="chat-right-panel"> <div class="chat-right-panel">
<h4 class="chat-title center"></h4> <h4 class="chat-title center"></h4>
<ul class="chat-list"></ul> <div class="chat-right-panel-chat">
<form class="send-form"> <ul class="chat-list"></ul>
<input type="text" name="content" /> <form class="send-form">
<input type="submit" name="send" value="Send!" /> <input type="text" name="content" />
</form> <input type="submit" name="send" value="Send!" />
</form>
</div>
</div> </div>
</div> </div>
</body> </body>
......
...@@ -41,59 +41,6 @@ ...@@ -41,59 +41,6 @@
* - refreshChat takes the list, overwrites the chat log * - refreshChat takes the list, overwrites the chat log
*/ */
function logError(error) {
console.log(error);
}
function logQueue(action) {
return new RSVP.Queue()
.push(function () {
return action;
})
.push(null, logError);
}
function pollUntilNotNull(
gadget, delay_ms, timeout_ms,
nullableFunction, callbackFunction) {
if (callbackFunction === undefined) {
callbackFunction = function () {};
}
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);
})
]);
}
});
}
function stringEndsWith(string, suffix) { function stringEndsWith(string, suffix) {
return string.indexOf(suffix, string.length - suffix.length) !== -1; return string.indexOf(suffix, string.length - suffix.length) !== -1;
} }
...@@ -136,64 +83,6 @@ ...@@ -136,64 +83,6 @@
} }
} }
// Notify when a new message appears and denotify when it is seen
function notifyStatus(gadget, room, notify) {
let favicon_url;
let class_name;
if (notify) {
favicon_url = gadget.state_parameter_dict.alert_icon;
class_name = "notify";
} else {
favicon_url = gadget.state_parameter_dict.default_icon;
class_name = "";
}
const 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);
const contact_list = gadget.state_parameter_dict
.element.querySelector(".contact-list").children;
for (let i = 0, i_len = contact_list.length; i < i_len; i++) {
if (contact_list[i].textContent === room) {
contact_list[i].className = class_name;
}
}
}
// Add a contact as some HTML element
function appendContact(gadget, room) {
const container = gadget.state_parameter_dict
.element.querySelector(".contact-list");
const contact = document.createElement("li");
contact.appendChild(document.createTextNode(room));
contact.addEventListener('click', function (event) {
notifyStatus(gadget, contact.textContent, false);
return gadget.changeRoom(room);
}, false);
container.appendChild(contact);
}
// Create a contact and connect to them
function createContact(gadget, room, param_string) {
appendContact(gadget, room);
return gadget.parseParams(param_string);
}
// Create new message from its parameters
function createMessage(gadget, param_dict) {
return {
type: param_dict.type || "message",
name: param_dict.name || gadget.state_parameter_dict.name,
folder: param_dict.folder || gadget.state_parameter_dict.folder,
room: param_dict.room || gadget.state_parameter_dict.room,
time: param_dict.time || new Date(),
content: param_dict.content || "",
colour: param_dict.colour || "black",
};
}
// Translate message to chat, in some HTML element // Translate message to chat, in some HTML element
function messageToChat(message) { function messageToChat(message) {
const chat = document.createElement("li"); const chat = document.createElement("li");
...@@ -250,117 +139,6 @@ ...@@ -250,117 +139,6 @@
} }
} }
// Add message to the list
function storeList(gadget, message) {
if (isNewMessage(message, gadget.state_parameter_dict.last_message_dict
[message.room][message.name])) {
gadget.state_parameter_dict.last_message_dict[message.room][message.name]
= getTime(message);
}
gadget.state_parameter_dict.message_list_dict[message.room].push(message);
}
// Appends a message to the chat box
function appendMessage(gadget, message) {
if (message.room === gadget.state_parameter_dict.room) {
const container = gadget.state_parameter_dict
.element.querySelector(".chat-list");
container.appendChild(messageToChat(message));
container.scrollTop = container.scrollHeight;
}
}
// Sort the list, dedupe, and overwrite the chat box,
// efficient because the archive is originally sorted
function refreshChat(gadget) {
const container = gadget.state_parameter_dict
.element.querySelector(".chat-list");
while (container.firstChild) {
container.removeChild(container.firstChild);
}
const old_list = gadget.state_parameter_dict.message_list_dict
[gadget.state_parameter_dict.room];
old_list.sort(messageTimeComparator);
const new_list = [];
let last_message;
for (let i = 0, i_len = old_list.length; i < i_len; i++) {
const message = old_list[i];
if (isSameMessage(last_message, message)) {
continue;
}
last_message = message;
new_list.push(message);
container.appendChild(messageToChat(message));
}
container.scrollTop = container.scrollHeight;
gadget.state_parameter_dict.message_list_dict
[gadget.state_parameter_dict.room] = new_list;
}
// Parse chat commands
function parseCommand(gadget, chat) {
const split = chat.slice(1).split(" ");
const command = split.shift();
const argument = split.join(" ");
switch (command) {
case "join":
if (argument in gadget.state_parameter_dict.room_set) {
return gadget.changeRoom(argument);
} else {
return gadget.deployNotification({
content: 'First connect to room "' + argument + '" via WebRTC!',
colour: "red",
});
}
case "leave":
return new RSVP.Queue()
.push(function () {
if (gadget.state_parameter_dict.room ===
gadget.state_parameter_dict.name) {
return gadget.deployNotification({
content: "You cannot leave your own room!",
colour: "red"
});
} else {
delete gadget.state_parameter_dict.room_set
[gadget.state_parameter_dict.room];
return gadget.deployMessage({
type: "notification",
content: gadget.state_parameter_dict.name + " has quit.",
colour: "orange",
});
}
})
.push(function () {
return gadget.changeRoom(gadget.state_parameter_dict.name);
})
.push(null, logError);
case "quit":
window.location.reload();
break;
case "help":
const help_message_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",
];
promise_list = [];
return new RSVP.Queue()
.push(function () {
for (let i = 0, i_len = help_message_list.length; i < i_len; i++) {
promise_list.push(gadget.deployNotification({
content: help_message_list[i],
colour: "green",
}));
}
return RSVP.all(promise_list);
})
.push(null, logError);
}
}
rJS(window) rJS(window)
.ready(function (gadget) { .ready(function (gadget) {
return new RSVP.Queue() return new RSVP.Queue()
...@@ -390,58 +168,27 @@ ...@@ -390,58 +168,27 @@
return gadget.getElement(); return gadget.getElement();
}) })
.push(function (element) { .push(function (element) {
element.querySelector(".send-form input").onfocus = function () {
notifyStatus(gadget, gadget.state_parameter_dict.room, false);
};
gadget.state_parameter_dict.element = element; gadget.state_parameter_dict.element = element;
return;
}) })
.push(null, logError); .push(null, logError);
}) })
.declareMethod('render', function () {})
.declareAcquiredMethod('wrapJioAccess', 'wrapJioAccess') .declareAcquiredMethod('wrapJioAccess', 'wrapJioAccess')
.declareAcquiredMethod('sendMessage', 'sendMessage') .declareAcquiredMethod('sendMessage', 'sendMessage')
.declareAcquiredMethod('parseParams', 'parseParams') .declareAcquiredMethod('chooseRoom', 'chooseRoom')
// Join a room for the first time // Join a room for the first time
.declareMethod('startRoom', function () { .declareMethod('render', function () {
const gadget = this; const gadget = this;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
const room = gadget.state_parameter_dict.name; gadget.state_parameter_dict.element
gadget.state_parameter_dict.element.querySelector(".chat-title") .querySelector(".send-form input").onfocus = function () {
.textContent = "Room: " + room; return logQueue(
gadget.state_parameter_dict.room = room; gadget.notifyStatus(gadget.state_parameter_dict.room, false));
gadget.state_parameter_dict.room_set[room] = false; };
gadget.state_parameter_dict.message_list_dict[room] = []; gadget.state_parameter_dict.room = gadget.state_parameter_dict.name;
gadget.state_parameter_dict.last_message_dict[room] = {};
appendContact(gadget, room);
return gadget.getLocalArchive();
})
.push(null, logError);
})
// Join a different room in the same folder
.declareMethod('changeRoom', function (room) {
const gadget = this;
return new RSVP.Queue()
.push(function () {
gadget.state_parameter_dict.element.querySelector(".chat-title")
.textContent = "Room: " + room
gadget.state_parameter_dict.room = room;
if (!(room in gadget.state_parameter_dict.room_set)) {
gadget.state_parameter_dict.room_set[room] = false;
gadget.state_parameter_dict.message_list_dict[room] = [];
gadget.state_parameter_dict.last_message_dict[room] = {};
appendContact(gadget, room);
}
return gadget.getLocalArchive();
})
.push(function () {
return gadget.requestRequest();
})
.push(function () {
return refreshChat(gadget);
}) })
.push(null, logError); .push(null, logError);
}) })
...@@ -472,21 +219,18 @@ ...@@ -472,21 +219,18 @@
} }
} catch (error) {} } catch (error) {}
} }
let last_message; const promise_list = [];
const message_dict = gadget.state_parameter_dict.last_message_dict; const message_dict = gadget.state_parameter_dict.last_message_dict;
while (!message_queue.isEmpty()) { while (!message_queue.isEmpty()) {
const message = message_queue.poll(); const message = message_queue.poll();
if (message.folder === gadget.state_parameter_dict.folder if (message.folder === gadget.state_parameter_dict.folder
&& (message.room in gadget.state_parameter_dict.room_set) && (message.room in gadget.state_parameter_dict.room_set)) {
&& !isSameMessage(last_message, message) && isNewMessage( promise_list.push(gadget.storeList(message));
message, message_dict[message.room][message.name])) { promise_list.push(gadget.appendMessage(message));
last_message = message;
storeList(gadget, message);
appendMessage(gadget, message);
} }
} }
gadget.state_parameter_dict.initialized = true; gadget.state_parameter_dict.initialized = true;
return; return RSVP.all(promise_list);
}) })
.push(null, logError); .push(null, logError);
}) })
...@@ -494,21 +238,28 @@ ...@@ -494,21 +238,28 @@
// Send all requested messages in the list, in sorted order, to peer // Send all requested messages in the list, in sorted order, to peer
.declareMethod('sendLocalArchive', function (request, source) { .declareMethod('sendLocalArchive', function (request, source) {
const gadget = this; const gadget = this;
const request_dict = {}; return new RSVP.Queue()
for (let room in request.content.room_set) { .push(function () {
request_dict[room] = []; const request_dict = {};
const list = gadget.state_parameter_dict.message_list_dict[room]; for (let room in request.content.room_set) {
if (list) { if (request.content.room_set.hasOwnProperty(room)
for (let i = 0, i_len = list.length; i < i_len; i++) { && gadget.state_parameter_dict.message_list_dict[room]) {
if (isNewMessage(list[i], request.content.dict[room][list[i].name]) request_dict[room] = [];
&& list[i].type !== "notification") { const list = gadget.state_parameter_dict.message_list_dict[room];
request_dict[room].push(list[i]); for (let i = 0, i_len = list.length; i < i_len; i++) {
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});
} })
return logQueue(gadget.sendMessage(createMessage(gadget, .push(function (message) {
{type: "bundle", content: request_dict}), source)); return gadget.sendMessage(message, source);
})
.push(null, logError);
}) })
// Get all new messages from the sorted list of peer, // Get all new messages from the sorted list of peer,
...@@ -520,19 +271,12 @@ ...@@ -520,19 +271,12 @@
.push(function () { .push(function () {
const promise_list = []; const promise_list = [];
for (let room in bundle.content) { for (let room in bundle.content) {
const list = gadget.state_parameter_dict.message_list_dict[room]; if (bundle.content.hasOwnProperty(room)) {
const remote_list = bundle.content[room]; const list = gadget.state_parameter_dict.message_list_dict[room];
let index = 0; const remote_list = bundle.content[room];
for (let i = 0, i_len = remote_list.length; i < i_len; i++) { for (let i = 0, i_len = remote_list.length; i < i_len; i++) {
const message = remote_list[i]; promise_list.push(gadget.storeList(remote_list[i]));
while (index < list.length promise_list.push(gadget.storeArchive(remote_list[i]));
&& getTime(list[index]) < getTime(message)) {
index++;
}
if (index >= list.length
|| !isSameMessage(list[index], message)) {
storeList(gadget, message);
promise_list.push(gadget.storeArchive(message));
} }
} }
} }
...@@ -553,7 +297,61 @@ ...@@ -553,7 +297,61 @@
} }
}) })
.push(function () { .push(function () {
return refreshChat(gadget); return gadget.refreshChat();
})
.push(null, logError);
})
// Ask a peer to send over a request
.declareMethod('requestRequest', function () {
const 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 () {
const gadget = this;
return pollUntilNotNull(gadget, 1000, 30000, function () {
return gadget.state_parameter_dict.initialized;
}, function () {
return new RSVP.Queue()
.push(function () {
return gadget.createMessage({
type: "request",
content: {
room_set: gadget.state_parameter_dict.room_set,
dict: gadget.state_parameter_dict.last_message_dict,
},
});
})
.push(function (message) {
return gadget.sendMessage(message);
})
.push(null, logError);
});
})
// Create new message from its parameters
.declareMethod('createMessage', function (param_dict) {
const gadget = this;
return new RSVP.Queue()
.push(function () {
return {
type: param_dict.type || "message",
name: param_dict.name || gadget.state_parameter_dict.name,
folder: param_dict.folder || gadget.state_parameter_dict.folder,
room: param_dict.room || gadget.state_parameter_dict.room,
time: param_dict.time || new Date(),
content: param_dict.content || "",
colour: param_dict.colour || "black",
};
}) })
.push(null, logError); .push(null, logError);
}) })
...@@ -561,11 +359,19 @@ ...@@ -561,11 +359,19 @@
// Create new message and send it to peer // Create new message and send it to peer
.declareMethod('deployMessage', function (param_dict) { .declareMethod('deployMessage', function (param_dict) {
const gadget = this; const gadget = this;
const message = createMessage(gadget, param_dict); let message;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
storeList(gadget, message); return gadget.createMessage(param_dict);
appendMessage(gadget, message); })
.push(function (result) {
message = result;
return gadget.storeList(message);
})
.push(function () {
return gadget.appendMessage(message);
})
.push(function () {
return gadget.storeArchive(message); return gadget.storeArchive(message);
}) })
.push(function () { .push(function () {
...@@ -577,101 +383,370 @@ ...@@ -577,101 +383,370 @@
// Create new notification and keep it on own machine // Create new notification and keep it on own machine
.declareMethod('deployNotification', function (param_dict) { .declareMethod('deployNotification', function (param_dict) {
const gadget = this; const gadget = this;
param_dict.type = "notification"; let notification;
const message = createMessage(gadget, param_dict); return new RSVP.Queue()
storeList(gadget, message); .push(function () {
appendMessage(gadget, message); 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);
}) })
// Get message from peer, store it in archive and list // Get message from peer, store it in archive and list
.declareMethod('getMessage', function (message) { .declareMethod('getMessage', function (message) {
const gadget = this; const gadget = this;
notifyStatus(gadget, message.room, true); return new RSVP.Queue()
storeList(gadget, message); .push(function () {
appendMessage(gadget, message); return gadget.notifyStatus(message.room, true);
return logQueue(gadget.storeArchive(message)); })
.push(function () {
return gadget.storeList(message);
})
.push(function () {
return gadget.appendMessage(message);
})
.push(function () {
return gadget.storeArchive(message);
})
.push(null, logError);
}) })
// Store message in the archive // Store message in the archive
.declareMethod('storeArchive', function (message) { .declareMethod('storeArchive', function (message) {
const gadget = this; const gadget = this;
const id = message.folder + "_" + message.room + "_"
+ message.name + "_" + getTime(message).toString();
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return logQueue(gadget.wrapJioAccess('put', id, { const id = message.folder + "_" + message.room + "_"
+ message.name + "_" + getTime(message).toString();
return gadget.wrapJioAccess('put', id, {
portal_type: "Text Post", portal_type: "Text Post",
parent_relative_url: "post_text_module", parent_relative_url: "post_text_module",
reference: id, reference: id,
author: message.name, author: message.name,
date_ms: getTime(message), date_ms: getTime(message),
content: JSON.stringify(message), content: JSON.stringify(message),
})); });
}); })
.push(null, logError);
}) })
// Ask a peer to send over a request // Add message to the list
.declareMethod('requestRequest', function () { .declareMethod('storeList', function (message) {
const gadget = this; const gadget = this;
return logQueue(gadget.sendMessage(createMessage( return new RSVP.Queue()
gadget, {type: "doubler", content: ""}))); .push(function () {
if (isNewMessage(message,
gadget.state_parameter_dict.last_message_dict
[message.room][message.name])) {
gadget.state_parameter_dict.last_message_dict
[message.room][message.name]
= getTime(message);
}
gadget.state_parameter_dict
.message_list_dict[message.room].push(message);
})
.push(null, logError);
}) })
// Send a request to update the local archive // Appends a message to the chat box
.declareMethod('sendRequest', function () { .declareMethod('appendMessage', function (message) {
const gadget = this; const gadget = this;
return pollUntilNotNull(gadget, 1000, 30000, function () { return new RSVP.Queue()
return gadget.state_parameter_dict.initialized; .push(function () {
}, function () { if (message.room === gadget.state_parameter_dict.room) {
return gadget.sendMessage(createMessage(gadget, { const container = gadget.state_parameter_dict
type: "request", .element.querySelector(".chat-list");
content: { container.appendChild(messageToChat(message));
room_set: gadget.state_parameter_dict.room_set, container.scrollTop = container.scrollHeight;
dict: gadget.state_parameter_dict.last_message_dict, }
}, })
})); .push(null, logError);
});
}) })
// Listen for new chats // Sort the list, dedupe, and overwrite the chat box,
.declareService(function () { // efficient because the archive is originally sorted
.declareMethod('refreshChat', function () {
const gadget = this; const gadget = this;
function handleSubmit(event) { return new RSVP.Queue()
return new RSVP.Queue() .push(function () {
.push(function () { const container = gadget.state_parameter_dict
switch (event.target.className) { .element.querySelector(".chat-list");
case "manage-form": while (container.firstChild) {
switch (event.target container.removeChild(container.firstChild);
.querySelector("input[type=submit]:focus").name) { }
case "sync": const old_list = gadget.state_parameter_dict.message_list_dict
return new RSVP.Queue() [gadget.state_parameter_dict.room];
.push(function () { old_list.sort(messageTimeComparator);
return gadget.wrapJioAccess('repair'); const new_list = [];
}) let last_message;
.push(function () { for (let i = 0, i_len = old_list.length; i < i_len; i++) {
return gadget.getLocalArchive(); const message = old_list[i];
}) if (isSameMessage(last_message, message)) {
.push(function () { continue;
return gadget.requestRequest(); }
}) last_message = message;
.push(null, logError); new_list.push(message);
case "new": container.appendChild(messageToChat(message));
return createContact(gadget, "guest", "room=guest&role=guest&auth=dropbox&dropbox_url=/Apps/OfficeJS Chat/&erp5_url=https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/"); }
} container.scrollTop = container.scrollHeight;
case "send-form": gadget.state_parameter_dict.message_list_dict
const content = event.target.elements.content.value; [gadget.state_parameter_dict.room] = new_list;
event.target.elements.content.value = ""; })
if (content.indexOf("/") === 0) { .push(null, logError);
return parseCommand(gadget, content); })
// Notify when a new message appears and denotify when it is seen
.declareMethod('notifyStatus', function (room, notify) {
const gadget = this;
return new RSVP.Queue()
.push(function () {
let favicon_url;
let class_name;
if (notify) {
favicon_url = gadget.state_parameter_dict.alert_icon;
class_name = "notify";
} else {
favicon_url = gadget.state_parameter_dict.default_icon;
class_name = "";
}
const 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);
const contact = gadget.state_parameter_dict.element
.querySelector("#chat-contact-" + room);
if (contact.className === "current") {
contact.className = class_name || "current";
} else {
contact.className = class_name;
}
})
.push(null, logError);
})
// Join a different room in the same folder
.declareMethod('changeRoom', function (room) {
const gadget = this;
return new RSVP.Queue()
.push(function () {
styleElementByQuery(gadget,
"#webrtc-gadget-" + gadget.state_parameter_dict.room, "none");
styleElementByQuery(gadget, ".chat-right-panel-chat", "flex");
gadget.state_parameter_dict.element.querySelector(".chat-title")
.textContent = "Room: " + room;
gadget.state_parameter_dict.room = room;
if (!(room in gadget.state_parameter_dict.room_set)) {
return new RSVP.Queue()
.push(function () {
gadget.state_parameter_dict.room_set[room] = false;
gadget.state_parameter_dict.message_list_dict[room] = [];
gadget.state_parameter_dict.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);
})
// Create a new contact
.declareMethod('createContact', function (param_dict) {
const gadget = this;
return new RSVP.Queue()
// XXX: load params here too
.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);
})
// Add a contact as some HTML element
.declareMethod('appendContact', function (room) {
const 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.state_parameter_dict.element
.querySelector("#chat-contact-" + room)) {
throw "A contact with the same name already exists!";
}
const contact = document.createElement("li");
contact.appendChild(document.createTextNode(room));
contact.setAttribute("id", "chat-contact-" + room);
gadget.state_parameter_dict.element
.querySelector(".contact-list").appendChild(contact);
gadget.state_parameter_dict.element.querySelector(
"#chat-contact-" + gadget.state_parameter_dict.room).className = "";
contact.className = "current";
contact.addEventListener('click', function (event) {
return new RSVP.Queue()
.push(function () {
return gadget.notifyStatus(room, false);
})
.push(function () {
gadget.state_parameter_dict.element.querySelector(
"#chat-contact-" + gadget.state_parameter_dict.room)
.className = "";
gadget.state_parameter_dict.element
.querySelector("#chat-contact-" + room).className = "current";
})
.push(function () {
if (gadget.state_parameter_dict.room_set[room]) {
return gadget.changeRoom(room);
} else { } else {
return gadget.deployMessage({content: content}); return gadget.editContact(room);
} }
} })
}) .push(null, logError);
.push(null, logError); }, false);
return;
})
})
// Edit a contact in the right panel
.declareMethod('editContact', function (room) {
const gadget = this;
return new RSVP.Queue()
.push(function () {
styleElementByQuery(gadget, ".chat-right-panel-chat", "none");
styleElementByQuery(gadget,
"#webrtc-gadget-" + gadget.state_parameter_dict.room, "none");
styleElementByQuery(gadget, "#webrtc-gadget-" + room, "block");
gadget.state_parameter_dict.room = room;
gadget.state_parameter_dict.element.querySelector(".chat-title")
.textContent = "Contact: " + room;
})
.push(null, logError);
})
// Parse chat commands
.declareMethod('parseCommand', function (chat) {
const gadget = this;
return new RSVP.Queue()
.push(function () {
const split = chat.slice(1).split(" ");
const command = split.shift();
const argument = split.join(" ");
switch (command) {
case "join":
if (gadget.state_parameter_dict.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_parameter_dict.room ===
gadget.state_parameter_dict.name) {
return gadget.deployNotification({
content: "You cannot leave your own room!",
colour: "red"
});
} else {
delete gadget.state_parameter_dict.room_set
[gadget.state_parameter_dict.room];
return gadget.deployMessage({
type: "notification",
content: gadget.state_parameter_dict.name + " has quit.",
colour: "orange",
});
}
})
.push(function () {
return gadget.changeRoom(gadget.state_parameter_dict.name);
})
.push(null, logError);
case "quit":
window.location.reload();
break;
case "help":
const help_message_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",
];
const promise_list = [];
return new RSVP.Queue()
.push(function () {
for (let i = 0, i_len = help_message_list.length;
i < i_len; i++) {
promise_list.push(gadget.deployNotification({
content: help_message_list[i],
colour: "green",
}));
}
return RSVP.all(promise_list);
})
.push(null, logError);
}
});
})
.onEvent('submit', function (event) {
const gadget = this;
switch (event.target.className) {
case "sync-form":
return new RSVP.Queue()
.push(function () {
return gadget.wrapJioAccess('repair');
})
.push(function () {
return gadget.getLocalArchive();
})
.push(function () {
return gadget.requestRequest();
})
.push(null, logError);
case "edit-form":
return gadget.editContact(gadget.state_parameter_dict.room);
case "join-form":
const contact = resetInputValue(event.target.elements.content);
return gadget.createContact({room: contact, role: "guest"});
case "make-form":
const room = resetInputValue(event.target.elements.content);
return gadget.createContact({room: room, role: "host"});
case "auth-form":
return gadget.connectContact();
case "send-form":
const content = resetInputValue(event.target.elements.content);
if (content.indexOf("/") === 0) {
return gadget.parseCommand(content);
} else {
return gadget.deployMessage({content: content});
}
} }
return loopEventListener(gadget.state_parameter_dict.element
.querySelector(".send-form"), "submit", false, handleSubmit);
}) })
.declareService(function () { .declareService(function () {
...@@ -682,20 +757,24 @@ ...@@ -682,20 +757,24 @@
}) })
.push(function () { .push(function () {
if (gadget.state_parameter_dict.initialized) { if (gadget.state_parameter_dict.initialized) {
promise_list = []; const promise_list = [];
for (room in gadget.state_parameter_dict.room_set) { for (let room in gadget.state_parameter_dict.room_set) {
promise_list.push(gadget.sendMessage(createMessage(gadget, { if (gadget.state_parameter_dict.room_set.hasOwnProperty(room)
type: "notification", && gadget.state_parameter_dict.room_set[room]) {
content: gadget.state_parameter_dict.name + " has quit.", promise_list.push(gadget.deployMessage({
room: room, type: "notification",
colour: "orange", content: gadget.state_parameter_dict.name + " has quit.",
}))); room: room,
colour: "orange",
}));
}
} }
return RSVP.all(promise_list); return RSVP.all(promise_list);
} else { } else {
return; return;
} }
}); })
.push(null, logError);
}); });
}(window, document, FastPriorityQueue, loopEventListener, rJS, RSVP)); }(window, document, FastPriorityQueue, loopEventListener, rJS, RSVP));
\ No newline at end of file
...@@ -11,40 +11,53 @@ ...@@ -11,40 +11,53 @@
<script src="gadget_erp5_chat_webrtc.js"></script> <script src="gadget_erp5_chat_webrtc.js"></script>
</head> </head>
<body> <body>
<h3 class="webrtc-heading"></h3>
<p class="error"></p> <p class="error"></p>
<p class="status"></p>
<form class="contact-form">
<label>Name:</label>
<input type="text" name="name" />
<label>Folder:</label>
<input type="text" name="folder" />
<label>Dropbox folder:</label>
<input type="text" name="dropbox_url" placeholder="/Apps/OfficeJS Chat" />
<label>ERP5 URL:</label>
<input type="text" name="erp5_url" placeholder="https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/" />
<label>Hateoas URL:</label>
<input type="text" name="hateoas_url" placeholder="https://softinst75770.host.vifib.net/erp5/web_site_module/hateoas" />
<br />
<input type="submit" value="Update information!" />
</form>
<form class="auth-form"> <form class="auth-form">
<label> <label>
<input type="radio" name="auth" value="erp5" required="required" /> <input type="radio" name="auth" value="erp5" required="required" />
ERP5, URL: ERP5
<input type="text" name="erp5_url" placeholder="https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/" />
</label> </label>
<label> <label>
<input type="radio" name="auth" value="dropbox" required="required" /> <input type="radio" name="auth" value="dropbox" required="required" />
Dropbox, Folder: Dropbox
<input type="text" name="dropbox_url" placeholder="/Apps/OfficeJS Chat" />
</label> </label>
<input type="submit" value="Authenticate!" /> <input type="submit" value="Authenticate!" />
</form> </form>
<form class="host-offer-form"> <form class="host-offer-form">
<label>Paste your guest's offer in this box:</label> <label>Paste your guest's offer in this box:</label>
<textarea rows="10" cols="80" name="send"></textarea> <textarea rows="5" cols="80" name="send"></textarea>
<input type="submit" value="Paste it!" /> <input type="submit" value="Paste it!" />
</form> </form>
<form class="host-answer-form"> <form class="host-answer-form">
<p>This is your answer. Send it to your guest!</p> <p>This is your answer. Send it to your guest!</p>
<p class="receive"></p> <textarea rows="5" cols="80" name="receive" readonly></textarea>
</form> </form>
<form class="guest-offer-form"> <form class="guest-offer-form">
<p>This is your new offer. Send it to your host!</p> <p>This is your new offer. Send it to your host!</p>
<p class="receive"></p> <textarea rows="5" cols="80" name="receive" readonly></textarea>
<input type="submit" value="I sent it to my host." /> <input type="submit" value="I sent it to my host." />
</form> </form>
<form class="guest-answer-form"> <form class="guest-answer-form">
<label>Now, paste your host's answer in this box:</label> <label>Now, paste your host's answer in this box:</label>
<textarea rows="10" cols="80" name="send"></textarea> <textarea rows="5" cols="80" name="send"></textarea>
<input type="submit" value="Paste it!" /> <input type="submit" value="Paste it!" />
</form> </form>
</body> </body>
......
(function (window, document, rJS, RSVP) { (function (window, document, rJS, RSVP) {
// Miscellaneous utility functions
function logError(error) {
console.log(error);
}
function logQueue(action) {
return new RSVP.Queue()
.push(function () {
return action;
})
.push(null, logError);
}
function showElementByClass(gadget, query) {
gadget.state_parameter_dict.element
.querySelector(query).style.display = "block";
}
function hideElementByClass(gadget, query) {
gadget.state_parameter_dict.element
.querySelector(query).style.display = "none";
}
function pollUntilNotNull(
gadget, delay_ms, timeout_ms,
nullableFunction, callbackFunction) {
if (callbackFunction === undefined) {
callbackFunction = function () {};
}
return RSVP.any([
RSVP.Queue()
.push(function () {
return RSVP.delay(timeout_ms);
})
.push(function () {
gadget.state_parameter_dict.element
.querySelector(".error").textContent =
"Timed out after " + timeout_ms + " ms.";
return;
})
.push(null, logError),
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);
})
]);
}
// jIO utility functions // jIO utility functions
function createDropboxJio(gadget, folder) { function createDropboxJio(gadget, folder) {
...@@ -102,8 +37,10 @@ ...@@ -102,8 +37,10 @@
.push(function (attachment_list) { .push(function (attachment_list) {
const promise_list = []; const promise_list = [];
for (let file_name in attachment_list) { for (let file_name in attachment_list) {
promise_list.push(dropbox_gadget if (attachment_list.hasOwnProperty(file_name)) {
.removeAttachment(param_dict.folder, file_name)); promise_list.push(dropbox_gadget
.removeAttachment(param_dict.folder, file_name));
}
} }
return RSVP.all(promise_list); return RSVP.all(promise_list);
}); });
...@@ -116,7 +53,8 @@ ...@@ -116,7 +53,8 @@
}) })
.push(function (attachment_list) { .push(function (attachment_list) {
for (let file_name in attachment_list) { for (let file_name in attachment_list) {
if (file_name.indexOf("offer_" + param_dict.room + "_") === 0) { if (attachment_list.hasOwnProperty(file_name)
&& file_name.indexOf("offer_" + param_dict.room + "_") === 0) {
return file_name.slice(6); return file_name.slice(6);
} }
} }
...@@ -200,7 +138,7 @@ ...@@ -200,7 +138,7 @@
erp5_gadget = jio_gadget; erp5_gadget = jio_gadget;
return erp5_gadget.createJio({ return erp5_gadget.createJio({
type: "erp5", type: "erp5",
url: (new URI("hateoas")).absoluteTo(url).toString(), url: url,
default_view_reference: "view", default_view_reference: "view",
}); });
}) })
...@@ -227,9 +165,6 @@ ...@@ -227,9 +165,6 @@
parent_relative_url: "webrtc_rooms_module", parent_relative_url: "webrtc_rooms_module",
}); });
}) })
.push(function (result) {
return result;
})
.push(null, logError); .push(null, logError);
} }
...@@ -290,14 +225,14 @@ ...@@ -290,14 +225,14 @@
.push(function () { .push(function () {
return createErp5Jio( return createErp5Jio(
gadget, gadget.state_parameter_dict.login_dict.folder, gadget, gadget.state_parameter_dict.login_dict.folder,
gadget.state_parameter_dict.auth_dict.erp5_url); gadget.state_parameter_dict.auth_dict.hateoas_url);
}) })
.push(function (folder_id) { .push(function (folder_id) {
gadget.state_parameter_dict.auth_dict.erp5_folder = gadget.state_parameter_dict.auth_dict.erp5_folder =
folder_id.slice(folder_id.indexOf("/") + 1); folder_id.slice(folder_id.indexOf("/") + 1);
gadget.state_parameter_dict.auth_dict.erp5_folder_url = gadget.state_parameter_dict.auth_dict.erp5_folder_url =
gadget.state_parameter_dict.auth_dict.erp5_url gadget.state_parameter_dict.auth_dict.erp5_url
+ gadget.state_parameter_dict.erp5_folder + "/"; + gadget.state_parameter_dict.auth_dict.erp5_folder + "/";
return gadget.getDeclaredGadget("erp5_gadget"); return gadget.getDeclaredGadget("erp5_gadget");
}) })
.push(function (erp5_gadget) { .push(function (erp5_gadget) {
...@@ -392,6 +327,7 @@ ...@@ -392,6 +327,7 @@
return pollUntilNotNull(gadget, 500, 10000, function () { return pollUntilNotNull(gadget, 500, 10000, function () {
return gadget.state_parameter_dict.candidate; return gadget.state_parameter_dict.candidate;
}, function (host_answer) { }, function (host_answer) {
gadget.state_parameter_dict.candidate = null;
param_dict.function_param_dict.file_name = "answer_" + file_name; param_dict.function_param_dict.file_name = "answer_" + file_name;
param_dict.function_param_dict.content = host_answer; param_dict.function_param_dict.content = host_answer;
return param_dict.function_dict.putContent( return param_dict.function_dict.putContent(
...@@ -400,15 +336,18 @@ ...@@ -400,15 +336,18 @@
}) })
.push(null, logError) .push(null, logError)
.push(function () { .push(function () {
param_dict.function_param_dict.file_name = "offer_" + file_name; if (file_name) {
return param_dict.function_dict.removeContent( param_dict.function_param_dict.file_name = "offer_" + file_name;
jio_gadget, param_dict.function_param_dict); return param_dict.function_dict.removeContent(
jio_gadget, param_dict.function_param_dict);
} else {
return;
}
}) })
.push(null, logError) .push(null, logError)
.push(function () { .push(function () {
return authenticateHost(param_dict); return authenticateHost(param_dict);
}) })
.push(null, logError);
} }
function authenticateGuest(param_dict) { function authenticateGuest(param_dict) {
...@@ -442,13 +381,16 @@ ...@@ -442,13 +381,16 @@
}, function (host_answer) { }, function (host_answer) {
return sendAnswer(gadget, host_answer); return sendAnswer(gadget, host_answer);
}); });
}, function () {
return param_dict.function_dict.removeContent(
jio_gadget, param_dict.function_param_dict);
}) })
.push(null, logError)
.push(function () { .push(function () {
return param_dict.function_dict.removeContent( if (file_name) {
jio_gadget, param_dict.function_param_dict); param_dict.function_param_dict.file_name = "answer_" + file_name;
return param_dict.function_dict.removeContent(
jio_gadget, param_dict.function_param_dict);
} else {
return;
}
}) })
.push(null, logError); .push(null, logError);
} }
...@@ -467,15 +409,15 @@ ...@@ -467,15 +409,15 @@
if (gadget.state_parameter_dict.login_dict.role === "host") { if (gadget.state_parameter_dict.login_dict.role === "host") {
gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(".host-offer-form textarea").value = ""; .querySelector(".host-offer-form textarea").value = "";
hideElementByClass(gadget, ".host-answer-form"); styleElementByQuery(gadget, ".host-answer-form", "none");
showElementByClass(gadget, ".host-offer-form"); styleElementByQuery(gadget, ".host-offer-form", "block");
peer_list.push(peer_connection); peer_list.push(peer_connection);
return setupPeerConnection( return setupPeerConnection(
gadget, peer_connection, ".host-answer-form .receive"); gadget, peer_connection, ".host-answer-form textarea");
} else if (gadget.state_parameter_dict.login_dict.role === "guest") { } else if (gadget.state_parameter_dict.login_dict.role === "guest") {
peer_list[0] = peer_connection; peer_list[0] = peer_connection;
return setupPeerConnection( return setupPeerConnection(
gadget, peer_connection, ".guest-offer-form .receive"); gadget, peer_connection, ".guest-offer-form textarea");
} }
}) })
.push(function () { .push(function () {
...@@ -521,18 +463,26 @@ ...@@ -521,18 +463,26 @@
const candidate = JSON.stringify(peer_connection.localDescription); const candidate = JSON.stringify(peer_connection.localDescription);
gadget.state_parameter_dict.candidate = candidate; gadget.state_parameter_dict.candidate = candidate;
gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(form_selector).textContent = candidate; .querySelector(form_selector).value = candidate;
}; };
if (gadget.state_parameter_dict.login_dict.role === "host") { if (gadget.state_parameter_dict.login_dict.role === "host") {
peer_connection.oniceconnectionstatechange = function() { peer_connection.oniceconnectionstatechange = function () {
if (peer_connection.iceConnectionState === "connected") { return new RSVP.Queue()
gadget.state_parameter_dict.guest_amount += 1; .push(function () {
} else if (peer_connection.iceConnectionState === "disconnected") { if (peer_connection.iceConnectionState === "connected") {
gadget.state_parameter_dict.guest_amount -= 1; gadget.state_parameter_dict.guest_amount += 1;
} } else if (peer_connection.iceConnectionState === "disconnected") {
gadget.state_parameter_dict.guest_amount -= 1;
}
return;
})
}; };
} else if (gadget.state_parameter_dict.login_dict.role === "guest") { } else if (gadget.state_parameter_dict.login_dict.role === "guest") {
peer_connection.oniceconnectionstatechange = function() { peer_connection.oniceconnectionstatechange = function () {
gadget.state_parameter_dict.element
.querySelector(".status").textContent =
"WebRTC connection status: "
+ peer_connection.iceConnectionState + ".";
if (peer_connection.iceConnectionState === "failed") { if (peer_connection.iceConnectionState === "failed") {
gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(".error").textContent = .querySelector(".error").textContent =
...@@ -556,8 +506,8 @@ ...@@ -556,8 +506,8 @@
return createInitialOffer(gadget); return createInitialOffer(gadget);
} else if ( } else if (
gadget.state_parameter_dict.login_dict.role === "guest") { gadget.state_parameter_dict.login_dict.role === "guest") {
hideElementByClass(gadget, ".guest-offer-form"); styleElementByQuery(gadget, ".guest-offer-form", "none");
hideElementByClass(gadget, ".auth-form"); styleElementByQuery(gadget, ".auth-form", "none");
return; return;
} }
}) })
...@@ -671,16 +621,15 @@ ...@@ -671,16 +621,15 @@
const gadget = this; const gadget = this;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
hideElementByClass(gadget, ".host-answer-form"); gadget.state_parameter_dict.element.setAttribute("id",
hideElementByClass(gadget, ".guest-answer-form"); "webrtc-gadget-" + gadget.state_parameter_dict.login_dict.room);
styleElementByQuery(gadget, ".host-answer-form", "none");
styleElementByQuery(gadget, ".guest-answer-form", "none");
if (gadget.state_parameter_dict.login_dict.role === "host") { if (gadget.state_parameter_dict.login_dict.role === "host") {
hideElementByClass(gadget, ".guest-offer-form"); styleElementByQuery(gadget, ".guest-offer-form", "none");
} else if (gadget.state_parameter_dict.login_dict.role === "guest") { } else if (gadget.state_parameter_dict.login_dict.role === "guest") {
hideElementByClass(gadget, ".host-offer-form"); styleElementByQuery(gadget, ".host-offer-form", "none");
} }
gadget.state_parameter_dict.element.querySelector(".webrtc-heading")
.textContent = "WebRTC Negotiations for: "
+ gadget.state_parameter_dict.login_dict.room;
return createInitialOffer(gadget); return createInitialOffer(gadget);
}) })
.push(null, logError); .push(null, logError);
...@@ -723,11 +672,13 @@ ...@@ -723,11 +672,13 @@
.declareMethod('authenticate', function () { .declareMethod('authenticate', function () {
const gadget = this; const gadget = this;
const auth = gadget.state_parameter_dict.element
.querySelector(".auth-form").elements.auth.value;
const fields = gadget.state_parameter_dict.element const fields = gadget.state_parameter_dict.element
.querySelector(".auth-form").elements; .querySelector(".contact-form").elements;
const auth = fields.auth.value;
const dropbox_url = fields.dropbox_url.value; const dropbox_url = fields.dropbox_url.value;
const erp5_url = fields.erp5_url.value; const erp5_url = fields.erp5_url.value;
const hateoas_url = fields.hateoas_url.value;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
let folder = gadget.state_parameter_dict.login_dict.folder; let folder = gadget.state_parameter_dict.login_dict.folder;
...@@ -743,6 +694,7 @@ ...@@ -743,6 +694,7 @@
erp5_url: erp5_url, erp5_url: erp5_url,
erp5_folder: null, erp5_folder: null,
erp5_folder_url: null, erp5_folder_url: null,
hateoas_url: hateoas_url,
}; };
switch (auth) { switch (auth) {
case "dropbox": case "dropbox":
...@@ -755,31 +707,24 @@ ...@@ -755,31 +707,24 @@
}); });
}) })
.declareService(function () { .onEvent('submit', function (event) {
const gadget = this; const gadget = this;
function handleSubmit(event) { switch (event.target.className) {
switch (event.target.className) { case "auth-form":
case "auth-form": return gadget.authenticate();
return gadget.authenticate(); case "host-offer-form":
case "host-offer-form": styleElementByQuery(gadget, ".host-offer-form", "none");
hideElementByClass(gadget, ".host-offer-form"); styleElementByQuery(gadget, ".host-answer-form", "block");
showElementByClass(gadget, ".host-answer-form"); return sendOffer(gadget, resetInputValue(event.target.elements.send));
let offer = event.target.elements.send.value; case "guest-offer-form":
event.target.elements.send.value = ""; styleElementByQuery(gadget, ".guest-offer-form", "none");
return sendOffer(gadget, offer); styleElementByQuery(gadget, ".guest-answer-form", "block");
case "guest-offer-form": break;
hideElementByClass(gadget, ".guest-offer-form"); case "guest-answer-form":
showElementByClass(gadget, ".guest-answer-form"); styleElementByQuery(gadget, ".guest-answer-form", "none");
break; return sendAnswer(
case "guest-answer-form": gadget, resetInputValue(event.target.elements.send));
hideElementByClass(gadget, ".guest-answer-form");
let answer = event.target.elements.send.value;
event.target.elements.send.value = "";
return sendAnswer(gadget, answer);
}
} }
return loopEventListener(
gadget.state_parameter_dict.element, "submit", false, handleSubmit);
}); });
}(window, document, rJS, RSVP)); }(window, document, rJS, RSVP));
\ No newline at end of file
/*global window, RSVP, FileReader */
/*jslint indent: 2, maxerr: 3, unparam: true */
(function (window, RSVP, FileReader) {
"use strict";
window.loopEventListener = function (target, type, useCapture, callback,
prevent_default) {
//////////////////////////
// Infinite event listener (promise is never resolved)
// eventListener is removed when promise is cancelled/rejected
//////////////////////////
var handle_event_callback,
callback_promise;
if (prevent_default === undefined) {
prevent_default = true;
}
function cancelResolver() {
if ((callback_promise !== undefined) &&
(typeof callback_promise.cancel === "function")) {
callback_promise.cancel();
}
}
function canceller() {
if (handle_event_callback !== undefined) {
target.removeEventListener(type, handle_event_callback, useCapture);
}
cancelResolver();
}
function itsANonResolvableTrap(resolve, reject) {
var result;
handle_event_callback = function (evt) {
if (prevent_default) {
evt.stopPropagation();
evt.preventDefault();
}
cancelResolver();
try {
result = callback(evt);
} catch (e) {
result = RSVP.reject(e);
}
callback_promise = result;
new RSVP.Queue()
.push(function () {
return result;
})
.push(undefined, function (error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
target.addEventListener(type, handle_event_callback, useCapture);
}
return new RSVP.Promise(itsANonResolvableTrap, canceller);
};
window.promiseEventListener = function (target, type, useCapture) {
//////////////////////////
// Resolve the promise as soon as the event is triggered
// eventListener is removed when promise is cancelled/resolved/rejected
//////////////////////////
var handle_event_callback;
function canceller() {
target.removeEventListener(type, handle_event_callback, useCapture);
}
function resolver(resolve) {
handle_event_callback = function (evt) {
canceller();
evt.stopPropagation();
evt.preventDefault();
resolve(evt);
return false;
};
target.addEventListener(type, handle_event_callback, useCapture);
}
return new RSVP.Promise(resolver, canceller);
};
window.promiseReadAsText = function (file) {
return new RSVP.Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function (evt) {
resolve(evt.target.result);
};
reader.onerror = function (evt) {
reject(evt);
};
reader.readAsText(file);
});
};
window.promiseDoWhile = function (loopFunction, input) {
// calls loopFunction(input) until it returns a non positive value
// this queue is to protect the inner loop queue from the
// `promiseDoWhile` caller, avoiding it to enqueue the inner
// loop queue.
return new RSVP.Queue()
.push(function () {
// here is the inner loop queue
var loop_queue = new RSVP.Queue();
function iterate(previous_iteration_result) {
if (!previous_iteration_result) {
return input;
}
loop_queue.push(iterate);
return loopFunction(input);
}
return loop_queue
.push(function () {
return loopFunction(input);
})
.push(iterate);
});
};
window.logError = function (error) {
console.log(error);
};
window.logQueue = function (action) {
return new RSVP.Queue()
.push(function () {
return action;
})
.push(null, logError);
};
window.styleElementByQuery = function (gadget, query, style) {
gadget.state_parameter_dict.element
.querySelector(query).style.display = style;
}
window.resetInputValue = function (element) {
const value = element.value;
element.value = "";
return value;
};
window.pollUntilNotNull = function (
gadget, delay_ms, timeout_ms,
nullableFunction, callbackFunction) {
if (callbackFunction === undefined) {
callbackFunction = function () {};
}
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);
})
]);
}
});
};
}(window, RSVP, FileReader));
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Web Script" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_Access_contents_information_Permission</string> </key>
<value>
<tuple>
<string>Anonymous</string>
<string>Assignee</string>
<string>Assignor</string>
<string>Associate</string>
<string>Auditor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Add_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Change_local_roles_Permission</string> </key>
<value>
<tuple>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_Modify_portal_content_Permission</string> </key>
<value>
<tuple>
<string>Assignee</string>
<string>Assignor</string>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>_View_Permission</string> </key>
<value>
<tuple>
<string>Anonymous</string>
<string>Assignee</string>
<string>Assignor</string>
<string>Associate</string>
<string>Auditor</string>
<string>Manager</string>
<string>Owner</string>
</tuple>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>contributor/person_module/1</string>
<string>classification/collaborative/team</string>
</tuple>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>gadget_global.js</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>rjs_gadget_global_js</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value> <string>en</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Web Script</string> </value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>gadget_global.js</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>001</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
web_page_module/adapter_js web_page_module/adapter_js
web_page_module/fast_priority_queue_js web_page_module/fast_priority_queue_js
web_page_module/gadget_erp5_chat* web_page_module/gadget_erp5_chat*
web_page_module/rjs_gadget_global_js
web_site_module/web_chat/** web_site_module/web_chat/**
\ No newline at end of file
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