Commit af967d44 authored by Eugene Shen's avatar Eugene Shen

Implement multi-room messaging

Add font, remove unused JIO Mapping Storage,
split chat box into contacts and chats tabs,
only send join and leave notifications to others,
add message colours, change styles with favicons,
declare new WebRTC gadgets for every room connection,
create new room with their own names for each client,
manage more query values in parent gadget with less code,
generate Dropbox link to stuff URL queries as state info,
prepare parseParams to handle new connections from the panel,
strip whitespaces, delete "my_", remove line breaks with CSS,
understand how queues in .ready() block .declareService() forever,
and finally, use flexbox CSS to completely overhaul look and feel.
parent 01b24062
body { body {
padding: 20px; padding: 20px;
} }
error {
color: orange;
}
h3 { h3 {
color: brown; color: brown;
} }
p { h4 {
margin: 0; margin: 0;
color: red;
} }
img {
max-width: 100%; label {
max-height: 100%; display: block;
} }
input[type="text"] { input[type="text"] {
width: 360px; width: 360px;
} }
.chat { .radio-item {
border: 1px solid; display: inline-block;
width: 680px; }
height: 360px; textarea {
display: block;
}
.chat-box {
display: flex;
width: 100%;
height: 80vh;
}
.chat-left-panel {
display: flex;
flex-direction: column;
flex: 1;
padding: 20px;
border: 1.5px solid;
box-sizing: border-box;
}
.chat-right-panel {
display: flex;
flex-direction: column;
flex: 2;
padding: 20px;
border: 1.5px solid;
box-sizing: border-box;
}
.contact-list {
flex: 1;
padding: 0;
overflow: auto;
}
.chat-list {
flex: 1;
padding: 0;
overflow: auto; overflow: auto;
} }
li {
list-style-type: none;
}
img {
max-width: 100%;
max-height: 100%;
}
.center {
text-align: center;
}
.send-form {
display: flex;
}
.send-form input[type="text"] {
flex: 1;
margin: 0 10px 0 0;
}
.chat-list li {
font-family: "Droid Sans Mono";
font-size: 12px;
}
.contact-list li {
color: blue;
}
.contact-list li:hover {
font-weight: bold;
cursor: pointer;
}
.contact-list li.notify {
font-weight: bold;
color: red;
}
\ No newline at end of file
...@@ -11,74 +11,69 @@ ...@@ -11,74 +11,69 @@
<link rel="stylesheet" href="gadget_erp5_chat.css" /> <link rel="stylesheet" href="gadget_erp5_chat.css" />
</head> </head>
<body> <body>
<div data-gadget-url="gadget_jio.html"
data-gadget-scope="storage_gadget"
data-gadget-sandbox="public"></div>
<h1>OfficeJS Chat</h1> <h1>OfficeJS Chat</h1>
<p class="chat-error">
</p>
<form class="login-form"> <form class="login-form">
<label>Name:</label> <label>Name:</label>
<br />
<input type="text" name="name" required="required" /> <input type="text" name="name" required="required" />
<br />
<label>Folder:</label> <label>Folder:</label>
<br />
<input type="text" name="folder" required="required" /> <input type="text" name="folder" required="required" />
<h3>Remote Storage</h3> <h3>Remote Storage</h3>
<label>
<input type="radio" name="remote" value="erp5" required="required" /> <input type="radio" name="remote" value="erp5" required="required" />
<label>ERP5</label> ERP5
<br /> </label>
<label>
<input type="radio" name="remote" value="dav" required="required" /> <input type="radio" name="remote" value="dav" required="required" />
<label>DAV Storage</label> DAV Storage
<br /> </label>
<label>
<input type="radio" name="remote" value="dropbox" required="required" /> <input type="radio" name="remote" value="dropbox" required="required" />
<label>Dropbox</label> Dropbox
<br /> </label>
<label>
<input type="radio" name="remote" value="local" required="required" checked="checked" /> <input type="radio" name="remote" value="local" required="required" checked="checked" />
<label>Local is Enough</label> Local is Enough
<br /> </label>
<h3>Storage Configuration</h3> <h3>Storage Configuration</h3>
<a href="https://www.dropbox.com/1/oauth2/authorize?client_id=igeiyv4pkt0y0mm&response_type=token&redirect_uri=https://softinst75770.host.vifib.net/erp5/web_site_module/web_chat/">Dropbox OAuth Link</a> <a class="dropbox-link">Dropbox OAuth</a>
<br />
<label>Dropbox folder:</label> <label>Dropbox folder:</label>
<br /> <input type="text" name="remote_dropbox_url" placeholder="/Apps/OfficeJS Chat/.jio_documents" />
<input type="text" name="dropbox_url" value="/Apps/OfficeJS Chat/.jio_documents" />
<br />
<label>ERP5 URL:</label> <label>ERP5 URL:</label>
<br /> <input type="text" name="remote_erp5_url" placeholder="https://softinst75770.host.vifib.net/erp5/web_site_module/" />
<input type="text" name="erp5_url" value="https://softinst75770.host.vifib.net/erp5/web_site_module/" />
<br />
<label>WebDAV URL:</label> <label>WebDAV URL:</label>
<br /> <input type="text" name="remote_dav_url" placeholder="https://softinst75722.host.vifib.net/share" />
<input type="text" name="dav_url" value="https://softinst75722.host.vifib.net/share" />
<br />
<label>WebDAV Username:</label> <label>WebDAV Username:</label>
<br /> <input type="text" name="remote_dav_user" placeholder="eyqs" />
<input type="text" name="dav_user" value="eyqs" />
<br />
<label>WebDAV Password:</label> <label>WebDAV Password:</label>
<br /> <input type="text" name="remote_dav_pass" placeholder="correct horse battery staple" />
<input type="text" name="dav_pass" value="Aoeuidhtns" />
<h3>Default WebRTC Configuration</h3>
<label>Automatically connect to own room:
<input type="checkbox" name="auto" />
</label>
<label>Default storage:</label>
<input type="text" name="auth" placeholder="dropbox" />
<label>Dropbox folder:</label>
<input type="text" name="auth_dropbox_url" placeholder="/Apps/OfficeJS Chat" />
<label>ERP5 URL:</label>
<input type="text" name="auth_erp5_url" placeholder="https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/" />
<br /> <br />
<input type="submit" value="Login!" /> <input type="submit" value="Login!" />
</form> </form>
<form class="room-form"> <form class="room-form">
<h3>Join Rooms</h3>
<input type="text" name="room" required="required" /> <input type="text" name="room" required="required" />
<br /> <br />
<input type="submit" name="host" value="Create a new room!" /> <input type="submit" name="host" value="Create a new room!" />
<input type="submit" name="guest" value="Join an existing room!" /> <input type="submit" name="guest" value="Join an existing room!" />
</form> </form>
<div data-gadget-url="gadget_jio.html"
data-gadget-scope="storage_gadget"
data-gadget-sandbox="public"></div>
<div data-gadget-url="gadget_erp5_chat_webrtc.html"
data-gadget-scope="webrtc_gadget"
data-gadget-sandbox="public" class="webrtc-panel"></div>
<div data-gadget-url="gadget_erp5_chat_panel.html"
data-gadget-scope="chat_gadget"
data-gadget-sandbox="public" class="chat-panel"></div>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
* folder: the root folder for your organization, e.g. nexedi; * folder: the root folder for your organization, e.g. nexedi;
* you can only chat with people with access to the same auth folder * you can only chat with people with access to the same auth folder
* remote: the type of remote storage to use, * remote: the type of remote storage to use,
* either dav, dropbox, erp5, or local. * either dav, dropbox, erp5, or local
* remote_dropbox_url: the remote Dropbox folder to synchronize with, * remote_dropbox_url: the remote Dropbox folder to synchronize with,
* e.g. /Apps/OfficeJS Chat/.jio_documents * e.g. /Apps/OfficeJS Chat/.jio_documents
* remote_erp5_url: the remote ERP5 instance to synchronize with, * remote_erp5_url: the remote ERP5 instance to synchronize with,
...@@ -15,15 +15,13 @@ ...@@ -15,15 +15,13 @@
* 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 * login: if present (e.g. login=foo), will automatically click Login
* room: the chat room you want to connect to, e.g. lobby * auth: the default type of authentication to use for WebRTC rooms,
* role: the type of connection to use, either host or guest; * either dropbox or erp5
* if both room and role are valid, will automatically click the button * auth_dropbox_url: the shared Dropbox folder to act as your auth folder,
* auth: the type of authentication to use, either dropbox or erp5
* if both role and auth are valid, will automatically click Authenticate
* auth_dropbox_url: the shared Dropbox folder to act as the auth folder,
* e.g. /Apps/OfficeJS Chat * e.g. /Apps/OfficeJS Chat
* auth_erp5_url: the shared ERP5 instance to act as the 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) { function logError(error) {
...@@ -38,34 +36,43 @@ ...@@ -38,34 +36,43 @@
.push(null, logError); .push(null, logError);
} }
function showElementByClass(my_gadget, query) { function showElementByClass(gadget, query) {
my_gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(query).style.display = "block"; .querySelector(query).style.display = "block";
} }
function hideElementByClass(my_gadget, query) { function hideElementByClass(gadget, query) {
my_gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(query).style.display = "none"; .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++) {
input_id = input_id.replace(reserved[i], "-"); input_id = input_id.replace(reserved[i], "-");
} }
return input_id; return input_id;
} }
function getQueryValue(query, value) { function getQueryValue(query_list, query_string) {
if (document.URL.indexOf(query + "=") != -1) { for (let i = 0, i_len = query_list.length; i < i_len; i++) {
const start = document.URL.indexOf(query + "=") + query.length + 1; const query = query_list[i];
let end = document.URL.indexOf("&", start); if (query_string.indexOf(query + "=") !== -1) {
const start = query_string.indexOf(query + "=") + query.length + 1;
let end = query_string.indexOf("&", start);
if (end === -1) { if (end === -1) {
end = document.URL.length; end = query_string.length;
} }
return decodeURIComponent(document.URL.slice(start, end)); return query_string.slice(start, end);
} else { }
return value || ""; }
return null;
}
function setQueryValue(query_list, query_string, element) {
value = getQueryValue(query_list, query_string);
if (value) {
element.value = value;
} }
} }
...@@ -73,7 +80,14 @@ ...@@ -73,7 +80,14 @@
.ready(function (gadget) { .ready(function (gadget) {
gadget.state_parameter_dict = { gadget.state_parameter_dict = {
element: null, element: null,
archive_amount: 0, name: null,
folder: null,
auth: null,
dropbox_url: null,
erp5_url: null,
auto: null,
// Dropbox OAuth only allows 500 characters in the state query
max_state_length: 500,
}; };
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
...@@ -81,10 +95,13 @@ ...@@ -81,10 +95,13 @@
}) })
.push(function (element) { .push(function (element) {
gadget.state_parameter_dict.element = element; gadget.state_parameter_dict.element = element;
return gadget.render();
}) })
.push(null, logError); .push(null, logError);
}) })
.declareService(function () {
const gadget = this;
return this.render();
})
.allowPublicAcquisition('wrapJioAccess', function (param_list) { .allowPublicAcquisition('wrapJioAccess', function (param_list) {
const gadget = this; const gadget = this;
...@@ -99,59 +116,136 @@ ...@@ -99,59 +116,136 @@
.push(null, logError); .push(null, logError);
}) })
.allowPublicAcquisition('sendMessage', function (param_dict) { .allowPublicAcquisition('sendMessage', function (param_list) {
const gadget = this; const gadget = this;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return gadget.getDeclaredGadget("webrtc_gadget"); return gadget.getDeclaredGadget(
"webrtc_gadget_" + param_list[0].room);
}) })
.push(function (webrtc_gadget) { .push(function (webrtc_gadget) {
return webrtc_gadget.sendMessage.apply(webrtc_gadget, param_dict); return webrtc_gadget.sendMessage.apply(webrtc_gadget, param_list);
}); })
.push(null, logError);
})
.allowPublicAcquisition('parseParams', function (param_list) {
const gadget = this;
return gadget.parseParams.apply(gadget, param_list);
})
.declareMethod('parseParams', function (param_string) {
const gadget = this;
const param_dict = {};
param_dict.auto = getQueryValue(["auto"], param_string);
param_dict.room = getQueryValue(["room"], param_string);
param_dict.role = getQueryValue(["role"], param_string);
param_dict.auth = getQueryValue(["auth"], param_string);
param_dict.dropbox_url =
getQueryValue(["auth_dropbox_url", "ad"], param_string);
param_dict.erp5_url = getQueryValue(["auth_erp5_url", "ae"], param_string);
return gadget.chooseRoom(param_dict);
}) })
.declareMethod('render', function () { .declareMethod('render', function () {
let url = decodeURIComponent(document.URL);
const gadget = this; const gadget = this;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
hideElementByClass(gadget, ".room-form"); hideElementByClass(gadget, ".room-form");
hideElementByClass(gadget, ".webrtc-panel"); const state_output = getQueryValue(["state"], url);
hideElementByClass(gadget, ".chat-panel"); if (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;
fields.name.value = getQueryValue("name"); // A list of login-form fields and short keys in order of priority
fields.folder.value = getQueryValue("folder"); const query_list = [["name", "m"], ["folder", "f"], ["remote", "r"],
const remote = getQueryValue("remote"); ["remote_dropbox_url", "rd"], ["remote_erp5_url", "re"],
if (remote === "dav" || remote === "dropbox" ["remote_dav_url", "rv"], ["remote_dav_user", "rvu"],
|| remote === "erp5" || remote === "local") { ["remote_dav_pass", "rvp"], ["auth", "a"],
fields.remote.value = remote; ["auth_dropbox_url", "ad"], ["auth_erp5_url", "ae"]]
let state_input = "";
for (let i = 0, i_len = query_list.length; i < i_len; i++) {
const query = query_list[i];
setQueryValue(query, url, fields[query[0]]);
const query_string = "&" + query[1] + "=" + fields[query[0]].value;
if (window.btoa(state_input + query_string).length <
gadget.state_parameter_dict.max_state_length) {
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";
}
} }
fields.dropbox_url.value = getQueryValue( gadget.state_parameter_dict.element.querySelector(".dropbox-link")
"remote_dropbox_url", fields.dropbox_url.value); .href = "https://www.dropbox.com/1/oauth2/authorize"
fields.erp5_url.value = getQueryValue( + "?client_id=igeiyv4pkt0y0mm&response_type=token"
"remote_erp5_url", fields.erp5_url.value); + "&redirect_uri=https://softinst75770.host.vifib.net/erp5/"
fields.dav_url.value = getQueryValue( + "web_site_module/web_chat/&state=" + window.btoa(state_input);
"remote_dav_url", fields.dav_url.value); if (getQueryValue(["login"], url)) {
fields.dav_user.value = getQueryValue(
"remote_dav_user", fields.dav_user.value);
fields.dav_pass.value = getQueryValue(
"remote_dav_pass", fields.dav_pass.value);
if (getQueryValue("login")) {
return gadget.login(); return gadget.login();
} else { } else {
return; return;
} }
}) })
.push(null, logError);
})
.declareMethod('login', function () {
const gadget = this;
let chat_gadget;
return new RSVP.Queue()
.push(function () { .push(function () {
return gadget.declareGadget(
"gadget_erp5_chat_panel.html", {scope: "chat_gadget"});
})
.push(function (subgadget) {
hideElementByClass(gadget, ".login-form");
showElementByClass(gadget, ".room-form");
chat_gadget = subgadget;
gadget.state_parameter_dict.element.insertBefore(
chat_gadget.state_parameter_dict.element,
gadget.state_parameter_dict.element.querySelector(".login-form"));
const fields = gadget.state_parameter_dict.element const fields = gadget.state_parameter_dict.element
.querySelector(".room-form").elements; .querySelector(".login-form").elements;
const room = getQueryValue("room"); gadget.state_parameter_dict.auth = fields.auth.value;
fields.room.value = room; gadget.state_parameter_dict.dropbox_url
const role = getQueryValue("role"); = fields.auth_dropbox_url.value;
if (room && (role === "host" || role === "guest")) { gadget.state_parameter_dict.erp5_url = fields.auth_erp5_url.value;
return gadget.chooseRoom(role); gadget.state_parameter_dict.auto = !!fields.auto.checked;
gadget.state_parameter_dict.name = cleanId(fields.name.value);
gadget.state_parameter_dict.folder = cleanId(fields.folder.value);
chat_gadget.state_parameter_dict.name =
gadget.state_parameter_dict.name;
chat_gadget.state_parameter_dict.folder =
gadget.state_parameter_dict.folder;
// XXX apply drivetojiomapping in custom dropbox_url directory
return gadget.createJio({
remote: fields.remote.value,
dropbox_url: fields.remote_dropbox_url.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,
});
})
// Log each person in as the host of their own room with their name
.push(function () {
return chat_gadget.startRoom();
})
.push(function () {
if (gadget.state_parameter_dict.auto) {
return gadget.parseParams(
"auto=a&role=host&room=" + gadget.state_parameter_dict.name
+ "&"+ decodeURIComponent(document.URL));
} else { } else {
return; return gadget.chooseRoom({
room: gadget.state_parameter_dict.name, role: "host"});
} }
}) })
.push(null, logError); .push(null, logError);
...@@ -245,89 +339,45 @@ ...@@ -245,89 +339,45 @@
.push(null, logError); .push(null, logError);
}) })
.declareMethod('login', function () { .declareMethod('chooseRoom', function (param_dict) {
const gadget = this; const gadget = this;
let rtc_gadget; let webrtc_gadget;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return gadget.getDeclaredGadget("webrtc_gadget"); return gadget.declareGadget(
"gadget_erp5_chat_webrtc.html", {
scope: "webrtc_gadget_" + param_dict.room});
}) })
.push(function (webrtc_gadget) { .push(function (subgadget) {
rtc_gadget = webrtc_gadget; webrtc_gadget = subgadget;
return gadget.getDeclaredGadget("chat_gadget"); return gadget.getDeclaredGadget("chat_gadget");
}) })
.push(function (chat_gadget) { .push(function (chat_gadget) {
const form = gadget.state_parameter_dict.element const fields = webrtc_gadget.state_parameter_dict.element
.querySelector(".login-form").elements;
rtc_gadget.state_parameter_dict.name = cleanId(form.name.value);
chat_gadget.state_parameter_dict.name = cleanId(form.name.value);
rtc_gadget.state_parameter_dict.folder = cleanId(form.folder.value);
chat_gadget.state_parameter_dict.folder = cleanId(form.folder.value);
hideElementByClass(gadget, ".login-form");
showElementByClass(gadget, ".room-form");
// XXX apply drivetojiomapping in custom dropbox_url directory
return gadget.createJio({
remote: form.remote.value,
dropbox_url: form.dropbox_url.value,
erp5_url: form.erp5_url.value,
dav_url: form.dav_url.value,
dav_user: form.dav_user.value,
dav_pass: form.dav_pass.value,
});
})
.push(null, logError);
})
.declareMethod('chooseRoom', function (role) {
const gadget = this;
let rtc_gadget;
return new RSVP.Queue()
.push(function () {
hideElementByClass(gadget, ".room-form");
showElementByClass(gadget, ".webrtc-panel");
return gadget.getDeclaredGadget("webrtc_gadget");
})
.push(function (webrtc_gadget) {
rtc_gadget = webrtc_gadget;
let room = cleanId(gadget.state_parameter_dict.element
.querySelector(".room-form").elements.room.value);
gadget.state_parameter_dict.element
.querySelector(".room-form").elements.room.value = "";
const fields = rtc_gadget.state_parameter_dict.element
.querySelector(".auth-form").elements; .querySelector(".auth-form").elements;
fields.auth.value = getQueryValue("auth"); fields.auth.value = param_dict.auth
fields.dropbox_url.value = getQueryValue( || gadget.state_parameter_dict.auth;
"auth_dropbox_url", fields.dropbox_url.value); fields.dropbox_url.value = param_dict.dropbox_url
fields.erp5_url.value = getQueryValue( || gadget.state_parameter_dict.dropbox_url;
"auth_erp5_url", fields.erp5_url.value); fields.erp5_url.value = param_dict.erp5_url
rtc_gadget.state_parameter_dict.login_dict = { || gadget.state_parameter_dict.erp5_url;
room: room, gadget.state_parameter_dict.element.insertBefore(
role: role, webrtc_gadget.state_parameter_dict.element,
auth: getQueryValue("auth"), 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,
}; };
rtc_gadget.state_parameter_dict.dataChannelOnopen = function () { webrtc_gadget.state_parameter_dict.dataChannelOnopen = function () {
return new RSVP.Queue() return chat_gadget.changeRoom(param_dict.room);
.push(function () {
hideElementByClass(gadget, ".webrtc-panel");
showElementByClass(gadget, ".room-form");
showElementByClass(gadget, ".chat-panel");
return gadget.getDeclaredGadget("chat_gadget");
})
.push(function (chat_gadget) {
chat_gadget.state_parameter_dict.room = room;
return chat_gadget.render();
})
.push(null, logError);
}; };
rtc_gadget.state_parameter_dict.dataChannelOnmessage = webrtc_gadget.state_parameter_dict.dataChannelOnmessage =
function (my_event) { function (event) {
const message = JSON.parse(my_event.data); const message = JSON.parse(event.data);
const source = my_event.srcElement; const source = event.srcElement;
return new RSVP.Queue()
.push(function () {
return gadget.getDeclaredGadget("chat_gadget");
})
.push(function (chat_gadget) {
if (message.type === "notification" if (message.type === "notification"
|| message.type === "message") { || message.type === "message") {
return new RSVP.Queue() return new RSVP.Queue()
...@@ -335,19 +385,19 @@ ...@@ -335,19 +385,19 @@
return chat_gadget.getMessage(message); return chat_gadget.getMessage(message);
}) })
.push(function () { .push(function () {
if (role === "host") { if (param_dict.role === "host") {
return rtc_gadget.sendMessage(message, source); return webrtc_gadget.sendMessage(message, source);
} else { } else {
return; return;
} }
}) })
.push(null, logError); .push(null, logError);
} else if (message.type === "bundle") { } else if (message.type === "bundle") {
if (role === "host") { if (param_dict.role === "host") {
gadget.state_parameter_dict.archive_amount += 1; webrtc_gadget.state_parameter_dict.archive_amount += 1;
if (gadget.state_parameter_dict.archive_amount >= if (webrtc_gadget.state_parameter_dict.archive_amount >=
rtc_gadget.state_parameter_dict.guest_amount) { webrtc_gadget.state_parameter_dict.guest_amount) {
gadget.state_parameter_dict.archive_amount = 0; webrtc_gadget.state_parameter_dict.archive_amount = 0;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return chat_gadget.getRemoteArchive(message); return chat_gadget.getRemoteArchive(message);
...@@ -356,8 +406,6 @@ ...@@ -356,8 +406,6 @@
return chat_gadget.requestRequest(); return chat_gadget.requestRequest();
}) })
.push(null, logError); .push(null, logError);
} else {
return chat_gadget.getRemoteArchive(message);
} }
} else { } else {
return chat_gadget.getRemoteArchive(message); return chat_gadget.getRemoteArchive(message);
...@@ -367,23 +415,39 @@ ...@@ -367,23 +415,39 @@
} else if (message.type === "doubler") { } else if (message.type === "doubler") {
return chat_gadget.sendRequest(); return chat_gadget.sendRequest();
} }
})
.push(null, logError);
}; };
return rtc_gadget.render(); return webrtc_gadget.render();
})
.push(function () {
if (param_dict.auto) {
return webrtc_gadget.authenticate();
} else {
return;
}
}) })
.push(null, logError); .push(null, logError);
}) })
.declareService(function () { .declareService(function () {
const gadget = this; const gadget = this;
function handleSubmit(my_event) { function handleSubmit(event) {
switch (my_event.target.className) { switch (event.target.className) {
case "login-form": case "login-form":
return gadget.login(); return gadget.login();
case "room-form": case "room-form":
return gadget.chooseRoom( return new RSVP.Queue()
my_event.target.querySelector("input[type=submit]:focus").name); .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( return loopEventListener(
......
...@@ -7,21 +7,29 @@ ...@@ -7,21 +7,29 @@
<script src="rsvp.js"></script> <script src="rsvp.js"></script>
<script src="renderjs.js"></script> <script src="renderjs.js"></script>
<script src="jiodev.js"></script> <script src="jiodev.js"></script>
<script src="jio_mappingstorage.js"></script>
<script src="gadget_global.js" ></script> <script src="gadget_global.js" ></script>
<script src="fast_priority_queue.js"></script> <script src="fast_priority_queue.js"></script>
<script src="gadget_erp5_chat_panel.js"></script> <script src="gadget_erp5_chat_panel.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" />
</head> </head>
<body> <body>
<h3>Chat List</h3> <div class="chat-box">
<div class="chat"> <div class="chat-left-panel">
<h4 class="center">Contacts</h4>
<ul class="contact-list"></ul>
<form class="manage-form">
<input type="submit" name="sync" value="Synchronize!" />
<input type="submit" name="contact" value="Add new contact!" />
</form>
</div> </div>
<div class="chat-right-panel">
<h4 class="chat-title center"></h4>
<ul class="chat-list"></ul>
<form class="send-form"> <form class="send-form">
<input type="text" name="content" /> <input type="text" name="content" />
<input type="submit" value="Send!" /> <input type="submit" name="send" value="Send!" />
</form>
<form class="sync-form">
<input type="submit" value="Synchronize!" />
</form> </form>
</div>
</div>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
} }
function pollUntilNotNull( function pollUntilNotNull(
my_gadget, delay_ms, timeout_ms, gadget, delay_ms, timeout_ms,
nullableFunction, callbackFunction) { nullableFunction, callbackFunction) {
if (callbackFunction === undefined) { if (callbackFunction === undefined) {
callbackFunction = function () {}; callbackFunction = function () {};
...@@ -63,9 +63,9 @@ ...@@ -63,9 +63,9 @@
.push(function () { .push(function () {
return nullableFunction(); return nullableFunction();
}) })
.push(function (my_result) { .push(function (result) {
if (my_result !== null) { if (result !== null) {
return callbackFunction(my_result); return callbackFunction(result);
} else { } else {
return RSVP.any([ return RSVP.any([
RSVP.timeout(timeout_ms), RSVP.timeout(timeout_ms),
...@@ -77,11 +77,11 @@ ...@@ -77,11 +77,11 @@
.push(function () { .push(function () {
return nullableFunction(); return nullableFunction();
}) })
.push(function (my_result) { .push(function (result) {
if (my_result === null) { if (result === null) {
return null; return null;
} else { } else {
return callbackFunction(my_result); return callbackFunction(result);
} }
}) })
.push(function (nullable) { .push(function (nullable) {
...@@ -103,7 +103,7 @@ ...@@ -103,7 +103,7 @@
} }
function isNewMessage(message, last_time) { function isNewMessage(message, last_time) {
return last_time === undefined || last_time < getTime(message) return last_time === undefined || last_time < getTime(message);
} }
function isSameMessage(lhs, rhs) { function isSameMessage(lhs, rhs) {
...@@ -112,16 +112,6 @@ ...@@ -112,16 +112,6 @@
&& lhs.room === rhs.room && getTime(lhs) === getTime(rhs); && lhs.room === rhs.room && getTime(lhs) === getTime(rhs);
} }
// Add new favicon or change existing favicon to image in favicon_url
function changeFavicon(favicon_url) {
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);
}
// Keep messages in chronologically ascending or descending order // Keep messages in chronologically ascending or descending order
function messageTimeCompare(ascending_order) { function messageTimeCompare(ascending_order) {
if (ascending_order) { if (ascending_order) {
...@@ -146,21 +136,67 @@ ...@@ -146,21 +136,67 @@
} }
} }
// Create new message from its type and content // Notify when a new message appears and denotify when it is seen
function createMessage(gadget, type, content) { 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 { return {
type: type, type: param_dict.type || "message",
name: gadget.state_parameter_dict.name, name: param_dict.name || gadget.state_parameter_dict.name,
folder: gadget.state_parameter_dict.folder, folder: param_dict.folder || gadget.state_parameter_dict.folder,
room: gadget.state_parameter_dict.room, room: param_dict.room || gadget.state_parameter_dict.room,
time: new Date(), time: param_dict.time || new Date(),
content: content, 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("p"); const chat = document.createElement("li");
const image_extensions = [".jpg", ".png", ".gif", ".bmp", ".tif", ".svg"]; const image_extensions = [".jpg", ".png", ".gif", ".bmp", ".tif", ".svg"];
const url_regex = const url_regex =
/((?:https?:\/\/)?(?:[-a-zA-Z0-9_~\/]+\.)+[-a-zA-Z0-9_~#?&=\/]+)/g; /((?:https?:\/\/)?(?:[-a-zA-Z0-9_~\/]+\.)+[-a-zA-Z0-9_~#?&=\/]+)/g;
...@@ -169,6 +205,7 @@ ...@@ -169,6 +205,7 @@
break; break;
case "notification": case "notification":
chat.appendChild(document.createTextNode(message.content)); chat.appendChild(document.createTextNode(message.content));
chat.style.color = message.colour;
return chat; return chat;
case "message": case "message":
chat.appendChild(document.createTextNode( chat.appendChild(document.createTextNode(
...@@ -208,6 +245,7 @@ ...@@ -208,6 +245,7 @@
} }
} }
chat.appendChild(document.createTextNode(matches[matches.length - 1])); chat.appendChild(document.createTextNode(matches[matches.length - 1]));
chat.style.color = message.colour;
return chat; return chat;
} }
} }
...@@ -226,7 +264,7 @@ ...@@ -226,7 +264,7 @@
function appendMessage(gadget, message) { function appendMessage(gadget, message) {
if (message.room === gadget.state_parameter_dict.room) { if (message.room === gadget.state_parameter_dict.room) {
const container = gadget.state_parameter_dict const container = gadget.state_parameter_dict
.element.querySelector(".chat"); .element.querySelector(".chat-list");
container.appendChild(messageToChat(message)); container.appendChild(messageToChat(message));
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
} }
...@@ -236,7 +274,7 @@ ...@@ -236,7 +274,7 @@
// efficient because the archive is originally sorted // efficient because the archive is originally sorted
function refreshChat(gadget) { function refreshChat(gadget) {
const container = gadget.state_parameter_dict const container = gadget.state_parameter_dict
.element.querySelector(".chat"); .element.querySelector(".chat-list");
while (container.firstChild) { while (container.firstChild) {
container.removeChild(container.firstChild); container.removeChild(container.firstChild);
} }
...@@ -265,31 +303,61 @@ ...@@ -265,31 +303,61 @@
const command = split.shift(); const command = split.shift();
const argument = split.join(" "); const argument = split.join(" ");
switch (command) { switch (command) {
case "rename":
gadget.state_parameter_dict.name = argument;
break;
case "join": case "join":
if (argument in gadget.state_parameter_dict.room_set) {
return gadget.changeRoom(argument); 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": case "quit":
window.location.reload(); window.location.reload();
break; break;
case "help": case "help":
const help_message_list = [ const help_message_list = [
"--------------------------------------------------------------------"
+ "-----------------------------------------------------------------",
"Available commands:", "Available commands:",
"/rename [name]: changes your nickname to [name]",
"/join [room]: connects you to [room]", "/join [room]: connects you to [room]",
"/help: displays this help box", "/help: displays this help box",
"/leave: disconnects you from the current room",
"/quit: disconnects from the chat and refreshes the page", "/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++) { for (let i = 0, i_len = help_message_list.length; i < i_len; i++) {
appendMessage(gadget, createMessage( promise_list.push(gadget.deployNotification({
gadget, "notification", help_message_list[i])); content: help_message_list[i],
colour: "green",
}));
} }
break; return RSVP.all(promise_list);
})
.push(null, logError);
} }
} }
...@@ -302,10 +370,12 @@ ...@@ -302,10 +370,12 @@
room: null, room: null,
name: null, name: null,
element: null, element: null,
initialized: null, initialized: false,
notified_join: false, // A set of names of rooms joined within this folder;
guest_amount: 0, // as a guest, only notify that you have joined once
// A set of names of rooms joined within this folder // 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
room_set: {}, room_set: {},
// A dictionary of messages based on the current room, e.g. // A dictionary of messages based on the current room, e.g.
// message_list_dict["room"] = [message1, message2, ...] // message_list_dict["room"] = [message1, message2, ...]
...@@ -320,49 +390,59 @@ ...@@ -320,49 +390,59 @@
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);
}) })
// Initialize gadget, after initializing name, folder, and room outside .declareMethod('render', function () {})
.declareMethod('render', function () { .declareAcquiredMethod('wrapJioAccess', 'wrapJioAccess')
.declareAcquiredMethod('sendMessage', 'sendMessage')
.declareAcquiredMethod('parseParams', 'parseParams')
// Join a room for the first time
.declareMethod('startRoom', function () {
const gadget = this; const gadget = this;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
gadget.state_parameter_dict.element const room = gadget.state_parameter_dict.name;
.querySelector("input").onfocus = function () { gadget.state_parameter_dict.element.querySelector(".chat-title")
return changeFavicon(gadget.state_parameter_dict.default_icon); .textContent = "Room: " + room;
}; gadget.state_parameter_dict.room = room;
return gadget.changeRoom(gadget.state_parameter_dict.room); 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(null, logError); .push(null, logError);
}) })
.declareAcquiredMethod('wrapJioAccess', 'wrapJioAccess')
.declareAcquiredMethod('sendMessage', 'sendMessage')
// Join a different room in the same folder // Join a different room in the same folder
.declareMethod('changeRoom', function (room) { .declareMethod('changeRoom', function (room) {
const gadget = this; const gadget = this;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
gadget.state_parameter_dict.element.querySelector(".chat-title")
.textContent = "Room: " + room
gadget.state_parameter_dict.room = room; gadget.state_parameter_dict.room = room;
if (!(room in gadget.state_parameter_dict.room_set)) { if (!(room in gadget.state_parameter_dict.room_set)) {
gadget.state_parameter_dict.room_set[room] = true; gadget.state_parameter_dict.room_set[room] = false;
}
if (!(room in gadget.state_parameter_dict.message_list_dict)) {
gadget.state_parameter_dict.message_list_dict[room] = []; gadget.state_parameter_dict.message_list_dict[room] = [];
}
if (!(room in gadget.state_parameter_dict.last_message_dict)) {
gadget.state_parameter_dict.last_message_dict[room] = {}; gadget.state_parameter_dict.last_message_dict[room] = {};
appendContact(gadget, room);
} }
return gadget.getLocalArchive(); return gadget.getLocalArchive();
}) })
.push(function () { .push(function () {
return gadget.requestRequest(); return gadget.requestRequest();
}) })
.push(function () {
return refreshChat(gadget);
})
.push(null, logError); .push(null, logError);
}) })
...@@ -418,14 +498,17 @@ ...@@ -418,14 +498,17 @@
for (let room in request.content.room_set) { for (let room in request.content.room_set) {
request_dict[room] = []; request_dict[room] = [];
const list = gadget.state_parameter_dict.message_list_dict[room]; const list = gadget.state_parameter_dict.message_list_dict[room];
if (list) {
for (let i = 0, i_len = list.length; i < i_len; i++) { for (let i = 0, i_len = list.length; i < i_len; i++) {
if (isNewMessage(list[i], request.content.dict[room][list[i].name])) { if (isNewMessage(list[i], request.content.dict[room][list[i].name])
&& list[i].type !== "notification") {
request_dict[room].push(list[i]); request_dict[room].push(list[i]);
} }
} }
} }
return logQueue(gadget.sendMessage(createMessage( }
gadget, "bundle", request_dict), source)); return logQueue(gadget.sendMessage(createMessage(gadget,
{type: "bundle", content: request_dict}), source));
}) })
// Get all new messages from the sorted list of peer, // Get all new messages from the sorted list of peer,
...@@ -456,10 +539,15 @@ ...@@ -456,10 +539,15 @@
return RSVP.all(promise_list); return RSVP.all(promise_list);
}) })
.push(function () { .push(function () {
if (!gadget.state_parameter_dict.notified_join) { if (!gadget.state_parameter_dict.room_set
gadget.state_parameter_dict.notified_join = true; [gadget.state_parameter_dict.room]) {
return gadget.deployMessage("notification", gadget.state_parameter_dict.room_set
gadget.state_parameter_dict.name + " has joined."); [gadget.state_parameter_dict.room] = true;
return gadget.deployMessage({
type: "notification",
content: gadget.state_parameter_dict.name + " has joined.",
colour: "orange",
});
} else { } else {
return; return;
} }
...@@ -471,24 +559,34 @@ ...@@ -471,24 +559,34 @@
}) })
// Create new message and send it to peer // Create new message and send it to peer
.declareMethod('deployMessage', function (type, content) { .declareMethod('deployMessage', function (param_dict) {
const gadget = this; const gadget = this;
const message = createMessage(gadget, type, content); const message = createMessage(gadget, param_dict);
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return gadget.getMessage(message); storeList(gadget, message);
appendMessage(gadget, message);
return gadget.storeArchive(message);
}) })
.push(function () { .push(function () {
changeFavicon(gadget.state_parameter_dict.default_icon);
return gadget.sendMessage(message); return gadget.sendMessage(message);
}) })
.push(null, logError); .push(null, logError);
}) })
// Create new notification and keep it on own machine
.declareMethod('deployNotification', function (param_dict) {
const gadget = this;
param_dict.type = "notification";
const message = createMessage(gadget, param_dict);
storeList(gadget, message);
appendMessage(gadget, message);
})
// 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;
changeFavicon(gadget.state_parameter_dict.alert_icon); notifyStatus(gadget, message.room, true);
storeList(gadget, message); storeList(gadget, message);
appendMessage(gadget, message); appendMessage(gadget, message);
return logQueue(gadget.storeArchive(message)); return logQueue(gadget.storeArchive(message));
...@@ -515,7 +613,8 @@ ...@@ -515,7 +613,8 @@
// Ask a peer to send over a request // Ask a peer to send over a request
.declareMethod('requestRequest', function () { .declareMethod('requestRequest', function () {
const gadget = this; const gadget = this;
return logQueue(gadget.sendMessage(createMessage(gadget, "doubler", ""))); return logQueue(gadget.sendMessage(createMessage(
gadget, {type: "doubler", content: ""})));
}) })
// Send a request to update the local archive // Send a request to update the local archive
...@@ -524,10 +623,12 @@ ...@@ -524,10 +623,12 @@
return pollUntilNotNull(gadget, 1000, 30000, function () { return pollUntilNotNull(gadget, 1000, 30000, function () {
return gadget.state_parameter_dict.initialized; return gadget.state_parameter_dict.initialized;
}, function () { }, function () {
return gadget.sendMessage(createMessage( return gadget.sendMessage(createMessage(gadget, {
gadget, "request", { type: "request",
content: {
room_set: gadget.state_parameter_dict.room_set, room_set: gadget.state_parameter_dict.room_set,
dict: gadget.state_parameter_dict.last_message_dict, dict: gadget.state_parameter_dict.last_message_dict,
},
})); }));
}); });
}) })
...@@ -536,16 +637,13 @@ ...@@ -536,16 +637,13 @@
.declareService(function () { .declareService(function () {
const gadget = this; const gadget = this;
function handleSubmit(event) { function handleSubmit(event) {
return new RSVP.Queue()
.push(function () {
switch (event.target.className) { switch (event.target.className) {
case "send-form": case "manage-form":
const content = event.target.elements.content.value; switch (event.target
event.target.elements.content.value = ""; .querySelector("input[type=submit]:focus").name) {
if (content.indexOf("/") === 0) { case "sync":
return parseCommand(gadget, content);
} else {
return gadget.deployMessage("message", content);
}
case "sync-form":
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return gadget.wrapJioAccess('repair'); return gadget.wrapJioAccess('repair');
...@@ -557,10 +655,23 @@ ...@@ -557,10 +655,23 @@
return gadget.requestRequest(); return gadget.requestRequest();
}) })
.push(null, logError); .push(null, logError);
case "new":
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/");
} }
case "send-form":
const content = event.target.elements.content.value;
event.target.elements.content.value = "";
if (content.indexOf("/") === 0) {
return parseCommand(gadget, content);
} else {
return gadget.deployMessage({content: content});
} }
return loopEventListener( }
gadget.state_parameter_dict.element, "submit", false, handleSubmit); })
.push(null, logError);
}
return loopEventListener(gadget.state_parameter_dict.element
.querySelector(".send-form"), "submit", false, handleSubmit);
}) })
.declareService(function () { .declareService(function () {
...@@ -571,8 +682,16 @@ ...@@ -571,8 +682,16 @@
}) })
.push(function () { .push(function () {
if (gadget.state_parameter_dict.initialized) { if (gadget.state_parameter_dict.initialized) {
return gadget.sendMessage(createMessage(gadget, "notification", promise_list = [];
gadget.state_parameter_dict.name + " has quit.")); for (room in gadget.state_parameter_dict.room_set) {
promise_list.push(gadget.sendMessage(createMessage(gadget, {
type: "notification",
content: gadget.state_parameter_dict.name + " has quit.",
room: room,
colour: "orange",
})));
}
return RSVP.all(promise_list);
} else { } else {
return; return;
} }
......
...@@ -7,59 +7,44 @@ ...@@ -7,59 +7,44 @@
<script src="rsvp.js"></script> <script src="rsvp.js"></script>
<script src="renderjs.js"></script> <script src="renderjs.js"></script>
<script src="adapter.js"></script> <script src="adapter.js"></script>
<script src="gadget_global.js" ></script> <script src="gadget_global.js"></script>
<script src="gadget_erp5_chat_webrtc.js"></script> <script src="gadget_erp5_chat_webrtc.js"></script>
</head> </head>
<body> <body>
<p class="webrtc-error"> <h3 class="webrtc-heading"></h3>
</p> <p class="error"></p>
<form class="auth-form"> <form class="auth-form">
<label>
<input type="radio" name="auth" value="erp5" required="required" /> <input type="radio" name="auth" value="erp5" required="required" />
<label>ERP5, URL:</label> ERP5, URL:
<br /> <input type="text" name="erp5_url" placeholder="https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/" />
<input type="text" name="erp5_url" value="https://softinst75770.host.vifib.net/erp5/webrtc_rooms_module/" /> </label>
<br /> <label>
<input type="radio" name="auth" value="dropbox" required="required" /> <input type="radio" name="auth" value="dropbox" required="required" />
<label>Dropbox, Folder:</label> Dropbox, Folder:
<br /> <input type="text" name="dropbox_url" placeholder="/Apps/OfficeJS Chat" />
<input type="text" name="dropbox_url" value="/Apps/OfficeJS Chat" /> </label>
<br />
<input type="submit" value="Authenticate!" /> <input type="submit" value="Authenticate!" />
</form> </form>
<form class="host-offer-form"> <form class="host-offer-form">
<label> <label>Paste your guest's offer in this box:</label>
Paste your guest's offer in this box:
</label>
<br />
<textarea rows="10" cols="80" name="send"></textarea> <textarea rows="10" cols="80" name="send"></textarea>
<br />
<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> <p>This is your answer. Send it to your guest!</p>
This is your answer. Send it to your guest! <p class="receive"></p>
</p>
<p class="receive">
</p>
</form> </form>
<form class="guest-offer-form"> <form class="guest-offer-form">
<p> <p>This is your new offer. Send it to your host!</p>
This is your new offer. Send it to your host! <p class="receive"></p>
</p>
<p class="receive">
</p>
<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> <label>Now, paste your host's answer in this box:</label>
Now, paste your host's answer in this box:
</label>
<br />
<textarea rows="10" cols="80" name="send"></textarea> <textarea rows="10" cols="80" name="send"></textarea>
<br />
<input type="submit" value="Paste it!" /> <input type="submit" value="Paste it!" />
</form> </form>
</body> </body>
......
...@@ -14,18 +14,18 @@ ...@@ -14,18 +14,18 @@
.push(null, logError); .push(null, logError);
} }
function showElementByClass(my_gadget, query) { function showElementByClass(gadget, query) {
my_gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(query).style.display = "block"; .querySelector(query).style.display = "block";
} }
function hideElementByClass(my_gadget, query) { function hideElementByClass(gadget, query) {
my_gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(query).style.display = "none"; .querySelector(query).style.display = "none";
} }
function pollUntilNotNull( function pollUntilNotNull(
my_gadget, delay_ms, timeout_ms, gadget, delay_ms, timeout_ms,
nullableFunction, callbackFunction) { nullableFunction, callbackFunction) {
if (callbackFunction === undefined) { if (callbackFunction === undefined) {
callbackFunction = function () {}; callbackFunction = function () {};
...@@ -36,8 +36,8 @@ ...@@ -36,8 +36,8 @@
return RSVP.delay(timeout_ms); return RSVP.delay(timeout_ms);
}) })
.push(function () { .push(function () {
my_gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(".webrtc-error").textContent = .querySelector(".error").textContent =
"Timed out after " + timeout_ms + " ms."; "Timed out after " + timeout_ms + " ms.";
return; return;
}) })
...@@ -50,11 +50,11 @@ ...@@ -50,11 +50,11 @@
.push(function () { .push(function () {
return nullableFunction(); return nullableFunction();
}) })
.push(function (my_result) { .push(function (result) {
if (my_result === null) { if (result === null) {
return null; return null;
} else { } else {
return callbackFunction(my_result); return callbackFunction(result);
} }
}) })
.push(function (nullable) { .push(function (nullable) {
...@@ -67,11 +67,11 @@ ...@@ -67,11 +67,11 @@
// jIO utility functions // jIO utility functions
function createDropboxJio(my_gadget, folder) { function createDropboxJio(gadget, folder) {
let dropbox_gadget; let dropbox_gadget;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return my_gadget.declareGadget( return gadget.declareGadget(
"gadget_jio.html", {scope: "dropbox_gadget"}); "gadget_jio.html", {scope: "dropbox_gadget"});
}) })
.push(function (jio_gadget) { .push(function (jio_gadget) {
...@@ -150,25 +150,25 @@ ...@@ -150,25 +150,25 @@
.removeAttachment(param_dict.folder, param_dict.file_name)); .removeAttachment(param_dict.folder, param_dict.file_name));
} }
function authenticateDropbox(my_gadget) { function authenticateDropbox(gadget) {
if (document.URL.indexOf("access_token=") === -1) { if (document.URL.indexOf("access_token=") === -1) {
my_gadget.state_parameter_dict.element.querySelector(".webrtc-error") gadget.state_parameter_dict.element.querySelector(".error")
.textContent = "Please log in to Dropbox!"; .textContent = "Please log in to Dropbox!";
return; return;
} }
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return createDropboxJio(my_gadget, return createDropboxJio(gadget,
my_gadget.state_parameter_dict.auth_dict.dropbox_folder); gadget.state_parameter_dict.auth_dict.dropbox_folder);
}) })
.push(function () { .push(function () {
const dropbox_param_dict = { const dropbox_param_dict = {
gadget: my_gadget, gadget: gadget,
gadget_name: "dropbox_gadget", gadget_name: "dropbox_gadget",
function_param_dict: { function_param_dict: {
folder: my_gadget.state_parameter_dict.auth_dict.dropbox_folder, folder: gadget.state_parameter_dict.auth_dict.dropbox_folder,
room: my_gadget.state_parameter_dict.login_dict.room, room: gadget.state_parameter_dict.login_dict.room,
name: my_gadget.state_parameter_dict.name, name: gadget.state_parameter_dict.login_dict.name,
file_name: null, file_name: null,
content: null, content: null,
}, },
...@@ -180,20 +180,20 @@ ...@@ -180,20 +180,20 @@
removeContent: removeDropboxContent, removeContent: removeDropboxContent,
}, },
}; };
if (my_gadget.state_parameter_dict.login_dict.role === "host") { if (gadget.state_parameter_dict.login_dict.role === "host") {
return authenticateHost(dropbox_param_dict); return authenticateHost(dropbox_param_dict);
} else if (my_gadget.state_parameter_dict.login_dict.role === "guest") { } else if (gadget.state_parameter_dict.login_dict.role === "guest") {
return authenticateGuest(dropbox_param_dict); return authenticateGuest(dropbox_param_dict);
} }
}) })
.push(null, logError); .push(null, logError);
} }
function createErp5Jio(my_gadget, folder, url) { function createErp5Jio(gadget, folder, url) {
let erp5_gadget; let erp5_gadget;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return my_gadget.declareGadget( return gadget.declareGadget(
"gadget_jio.html", {scope: "erp5_gadget"}); "gadget_jio.html", {scope: "erp5_gadget"});
}) })
.push(function (jio_gadget) { .push(function (jio_gadget) {
...@@ -285,36 +285,36 @@ ...@@ -285,36 +285,36 @@
)); ));
} }
function authenticateErp5(my_gadget) { function authenticateErp5(gadget) {
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return createErp5Jio( return createErp5Jio(
my_gadget, my_gadget.state_parameter_dict.folder, gadget, gadget.state_parameter_dict.login_dict.folder,
my_gadget.state_parameter_dict.auth_dict.erp5_url); gadget.state_parameter_dict.auth_dict.erp5_url);
}) })
.push(function (folder_id) { .push(function (folder_id) {
my_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);
my_gadget.state_parameter_dict.auth_dict.erp5_folder_url = gadget.state_parameter_dict.auth_dict.erp5_folder_url =
my_gadget.state_parameter_dict.auth_dict.erp5_url gadget.state_parameter_dict.auth_dict.erp5_url
+ my_gadget.state_parameter_dict.erp5_folder + "/"; + gadget.state_parameter_dict.erp5_folder + "/";
return my_gadget.getDeclaredGadget("erp5_gadget"); return gadget.getDeclaredGadget("erp5_gadget");
}) })
.push(function (erp5_gadget) { .push(function (erp5_gadget) {
return resetErp5Content(erp5_gadget, { return resetErp5Content(erp5_gadget, {
folder: my_gadget.state_parameter_dict.auth_dict.erp5_folder, folder: gadget.state_parameter_dict.auth_dict.erp5_folder,
url: my_gadget.state_parameter_dict.auth_dict.erp5_folder_url, url: gadget.state_parameter_dict.auth_dict.erp5_folder_url,
}); });
}) })
.push(function () { .push(function () {
const erp5_param_dict = { const erp5_param_dict = {
gadget: my_gadget, gadget: gadget,
gadget_name: "erp5_gadget", gadget_name: "erp5_gadget",
function_param_dict: { function_param_dict: {
folder: my_gadget.state_parameter_dict.auth_dict.erp5_folder, folder: gadget.state_parameter_dict.auth_dict.erp5_folder,
room: my_gadget.state_parameter_dict.login_dict.room, room: gadget.state_parameter_dict.login_dict.room,
name: my_gadget.state_parameter_dict.name, name: gadget.state_parameter_dict.login_dict.name,
url: my_gadget.state_parameter_dict.auth_dict.erp5_folder_url, url: gadget.state_parameter_dict.auth_dict.erp5_folder_url,
file_name: null, file_name: null,
content: null, content: null,
}, },
...@@ -332,9 +332,9 @@ ...@@ -332,9 +332,9 @@
removeContent: removeErp5Content, removeContent: removeErp5Content,
}, },
}; };
if (my_gadget.state_parameter_dict.login_dict.role === "host") { if (gadget.state_parameter_dict.login_dict.role === "host") {
return authenticateHost(erp5_param_dict); return authenticateHost(erp5_param_dict);
} else if (my_gadget.state_parameter_dict.login_dict.role === "guest") { } else if (gadget.state_parameter_dict.login_dict.role === "guest") {
return authenticateGuest(erp5_param_dict); return authenticateGuest(erp5_param_dict);
} }
}) })
...@@ -342,18 +342,18 @@ ...@@ -342,18 +342,18 @@
} }
function authenticateHost(param_dict) { function authenticateHost(param_dict) {
const my_gadget = param_dict.gadget; const gadget = param_dict.gadget;
let file_name; let file_name;
let jio_gadget; let jio_gadget;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
my_gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(".webrtc-error").textContent = ""; .querySelector(".error").textContent = "";
return my_gadget.getDeclaredGadget(param_dict.gadget_name); return gadget.getDeclaredGadget(param_dict.gadget_name);
}) })
.push(function (storage_gadget) { .push(function (storage_gadget) {
jio_gadget = storage_gadget; jio_gadget = storage_gadget;
return pollUntilNotNull(my_gadget, 500, 3600000, function () { return pollUntilNotNull(gadget, 2000, 86400000, function () {
return param_dict.function_dict.getOffer( return param_dict.function_dict.getOffer(
jio_gadget, param_dict.function_param_dict); jio_gadget, param_dict.function_param_dict);
}, function (offer_name) { }, function (offer_name) {
...@@ -361,9 +361,9 @@ ...@@ -361,9 +361,9 @@
}); });
}) })
.push(function () { .push(function () {
return pollUntilNotNull(my_gadget, 50, 30000, function () { return pollUntilNotNull(gadget, 500, 30000, function () {
if (my_gadget.state_parameter_dict.offer_ready === true) { if (gadget.state_parameter_dict.offer_ready === true) {
my_gadget.state_parameter_dict.offer_ready = false; gadget.state_parameter_dict.offer_ready = false;
return true; return true;
} else { } else {
return null; return null;
...@@ -375,22 +375,22 @@ ...@@ -375,22 +375,22 @@
}) })
.push(function () { .push(function () {
param_dict.function_param_dict.file_name = "offer_" + file_name; param_dict.function_param_dict.file_name = "offer_" + file_name;
return pollUntilNotNull(my_gadget, 50, 5000, function () { return pollUntilNotNull(gadget, 500, 10000, function () {
return param_dict.function_dict.getContent( return param_dict.function_dict.getContent(
jio_gadget, param_dict.function_param_dict); jio_gadget, param_dict.function_param_dict);
}, function (guest_offer) { }, function (guest_offer) {
return sendOffer(my_gadget, guest_offer); return sendOffer(gadget, guest_offer);
}); });
}, function (error) { }, function (error) {
if (error.toString().indexOf("Timed out after") === 0) { if (error.toString().indexOf("Timed out after") === 0) {
throw createInitialOffer(my_gadget); throw createInitialOffer(gadget);
} else { } else {
throw error; throw error;
} }
}) })
.push(function () { .push(function () {
return pollUntilNotNull(my_gadget, 50, 10000, function () { return pollUntilNotNull(gadget, 500, 10000, function () {
return my_gadget.state_parameter_dict.candidate; return gadget.state_parameter_dict.candidate;
}, function (host_answer) { }, function (host_answer) {
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;
...@@ -412,19 +412,19 @@ ...@@ -412,19 +412,19 @@
} }
function authenticateGuest(param_dict) { function authenticateGuest(param_dict) {
const my_gadget = param_dict.gadget; const gadget = param_dict.gadget;
let file_name; let file_name;
let jio_gadget; let jio_gadget;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
my_gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(".webrtc-error").textContent = ""; .querySelector(".error").textContent = "";
return my_gadget.getDeclaredGadget(param_dict.gadget_name); return gadget.getDeclaredGadget(param_dict.gadget_name);
}) })
.push(function (storage_gadget) { .push(function (storage_gadget) {
jio_gadget = storage_gadget; jio_gadget = storage_gadget;
return pollUntilNotNull(my_gadget, 50, 15000, function () { return pollUntilNotNull(gadget, 500, 30000, function () {
return my_gadget.state_parameter_dict.candidate; return gadget.state_parameter_dict.candidate;
}, function (guest_offer) { }, function (guest_offer) {
file_name = param_dict.function_param_dict.room + "_" file_name = param_dict.function_param_dict.room + "_"
+ param_dict.function_param_dict.name + ".txt"; + param_dict.function_param_dict.name + ".txt";
...@@ -435,12 +435,12 @@ ...@@ -435,12 +435,12 @@
}); });
}) })
.push(function () { .push(function () {
return pollUntilNotNull(my_gadget, 50, 30000, function () { return pollUntilNotNull(gadget, 500, 30000, function () {
param_dict.function_param_dict.file_name = "answer_" + file_name; param_dict.function_param_dict.file_name = "answer_" + file_name;
return param_dict.function_dict.getContent( return param_dict.function_dict.getContent(
jio_gadget, param_dict.function_param_dict); jio_gadget, param_dict.function_param_dict);
}, function (host_answer) { }, function (host_answer) {
return sendAnswer(my_gadget, host_answer); return sendAnswer(gadget, host_answer);
}); });
}, function () { }, function () {
return param_dict.function_dict.removeContent( return param_dict.function_dict.removeContent(
...@@ -453,155 +453,133 @@ ...@@ -453,155 +453,133 @@
.push(null, logError); .push(null, logError);
} }
function authenticate(my_gadget, storage) {
const form = my_gadget.state_parameter_dict.element
.querySelector(".auth-form").elements;
return new RSVP.Queue()
.push(function () {
let folder = my_gadget.state_parameter_dict.folder;
if (folder[0] !== "/") {
folder = "/" + folder;
}
if (folder[folder.length - 1] !== "/") {
folder = folder + "/";
}
my_gadget.state_parameter_dict.auth_dict = {
dropbox_url: form.dropbox_url.value,
dropbox_folder: form.dropbox_url.value + folder,
erp5_url: form.erp5_url.value,
erp5_folder: null,
erp5_folder_url: null,
};
switch (storage) {
case "dropbox":
return authenticateDropbox(my_gadget);
case "erp5":
return authenticateErp5(my_gadget);
default:
return;
}
});
}
// WebRTC signalling functions // WebRTC signalling functions
function createInitialOffer(my_gadget) { function createInitialOffer(gadget) {
const peer_list = my_gadget.state_parameter_dict.peer_connection_list; const peer_list = gadget.state_parameter_dict.peer_connection_list;
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return new RTCPeerConnection( return new RTCPeerConnection(
my_gadget.state_parameter_dict.ice_config, gadget.state_parameter_dict.ice_config,
my_gadget.state_parameter_dict.ice_constraints); gadget.state_parameter_dict.ice_constraints);
}) })
.push(function (peer_connection) { .push(function (peer_connection) {
if (my_gadget.state_parameter_dict.login_dict.role === "host") { if (gadget.state_parameter_dict.login_dict.role === "host") {
my_gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(".host-offer-form textarea").value = ""; .querySelector(".host-offer-form textarea").value = "";
hideElementByClass(my_gadget, ".host-answer-form"); hideElementByClass(gadget, ".host-answer-form");
showElementByClass(my_gadget, ".host-offer-form"); showElementByClass(gadget, ".host-offer-form");
peer_list.push(peer_connection); peer_list.push(peer_connection);
return setupPeerConnection( return setupPeerConnection(
my_gadget, peer_connection, ".host-answer-form .receive"); gadget, peer_connection, ".host-answer-form .receive");
} else if (my_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(
my_gadget, peer_connection, ".guest-offer-form .receive"); gadget, peer_connection, ".guest-offer-form .receive");
} }
}) })
.push(function () { .push(function () {
if (my_gadget.state_parameter_dict.login_dict.role === "host") { if (gadget.state_parameter_dict.login_dict.role === "host") {
peer_list[peer_list.length - 1].ondatachannel = peer_list[peer_list.length - 1].ondatachannel =
function (my_event) { function (event) {
setupDataChannel(my_gadget, my_event.channel); setupDataChannel(gadget, event.channel);
my_gadget.state_parameter_dict gadget.state_parameter_dict.data_channel_list.push(event.channel);
.data_channel_list.push(my_event.channel);
}; };
return; return;
} else if (my_gadget.state_parameter_dict.login_dict.role === "guest") { } else if (gadget.state_parameter_dict.login_dict.role === "guest") {
const peer_connection = peer_list[0]; const peer_connection = peer_list[0];
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return peer_connection.createDataChannel( return peer_connection.createDataChannel(
my_gadget.state_parameter_dict.login_dict.room + "_" gadget.state_parameter_dict.login_dict.room + "_"
+ my_gadget.state_parameter_dict.name + "_" + gadget.state_parameter_dict.login_dict.name + "_"
+ my_gadget.state_parameter_dict.data_channel_list.length, + gadget.state_parameter_dict.data_channel_list.length,
my_gadget.state_parameter_dict.dc_constraints); gadget.state_parameter_dict.dc_constraints);
}) })
.push(function (channel) { .push(function (channel) {
my_gadget.state_parameter_dict.data_channel_list.push(channel); gadget.state_parameter_dict.data_channel_list.push(channel);
return setupDataChannel(my_gadget, channel); return setupDataChannel(gadget, channel);
}) })
.push(function () { .push(function () {
return peer_connection.createOffer( return peer_connection.createOffer(
function (offer_description) { function (offer_description) {
peer_connection.setLocalDescription(offer_description); peer_connection.setLocalDescription(offer_description);
}, logError, my_gadget.state_parameter_dict.sdp_constraints); }, logError, gadget.state_parameter_dict.sdp_constraints);
}) })
.push(null, logError); .push(null, logError);
} }
}) })
.push(function () { .push(function () {
my_gadget.state_parameter_dict.offer_ready = true; gadget.state_parameter_dict.offer_ready = true;
return; return;
}) })
.push(null, logError); .push(null, logError);
} }
function setupPeerConnection(my_gadget, peer_connection, form_selector) { function setupPeerConnection(gadget, peer_connection, form_selector) {
peer_connection.onicecandidate = function (my_event) { peer_connection.onicecandidate = function (event) {
const candidate = JSON.stringify(peer_connection.localDescription); const candidate = JSON.stringify(peer_connection.localDescription);
my_gadget.state_parameter_dict.candidate = candidate; gadget.state_parameter_dict.candidate = candidate;
my_gadget.state_parameter_dict.element gadget.state_parameter_dict.element
.querySelector(form_selector).textContent = candidate; .querySelector(form_selector).textContent = candidate;
}; };
if (my_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") { if (peer_connection.iceConnectionState === "connected") {
my_gadget.state_parameter_dict.guest_amount += 1; gadget.state_parameter_dict.guest_amount += 1;
} else if (peer_connection.iceConnectionState === "disconnected") { } else if (peer_connection.iceConnectionState === "disconnected") {
my_gadget.state_parameter_dict.guest_amount -= 1; gadget.state_parameter_dict.guest_amount -= 1;
} else if (peer_connection.iceConnectionState === "failed") {
if (my_gadget.state_parameter_dict.login_dict.role === "guest") {
window.location.reload();
} }
};
} else if (gadget.state_parameter_dict.login_dict.role === "guest") {
peer_connection.oniceconnectionstatechange = function() {
if (peer_connection.iceConnectionState === "failed") {
gadget.state_parameter_dict.element
.querySelector(".error").textContent =
"WebRTC connection failed!";
} else if (peer_connection.iceConnectionState === "failed") {
gadget.state_parameter_dict.element
.querySelector(".error").textContent =
"WebRTC connection disconnected!";
} }
}; };
} }
} }
function setupDataChannel(my_gadget, data_channel) { function setupDataChannel(gadget, data_channel) {
if (!my_gadget.state_parameter_dict.connected) { if (!gadget.state_parameter_dict.connected) {
data_channel.onopen = function () { data_channel.onopen = function () {
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
my_gadget.state_parameter_dict.connected = true; gadget.state_parameter_dict.connected = true;
if (my_gadget.state_parameter_dict.login_dict.role === "host") { if (gadget.state_parameter_dict.login_dict.role === "host") {
return createInitialOffer(my_gadget); return createInitialOffer(gadget);
} else if ( } else if (
my_gadget.state_parameter_dict.login_dict.role === "guest") { gadget.state_parameter_dict.login_dict.role === "guest") {
showElementByClass(my_gadget, ".guest-offer-form"); hideElementByClass(gadget, ".guest-offer-form");
hideElementByClass(gadget, ".auth-form");
return; return;
} }
}) })
.push(my_gadget.state_parameter_dict.dataChannelOnopen) .push(gadget.state_parameter_dict.dataChannelOnopen)
.push(null, logError); .push(null, logError);
}; };
} else if (my_gadget.state_parameter_dict.login_dict.role === "host") { } else if (gadget.state_parameter_dict.login_dict.role === "host") {
data_channel.onopen = function () { data_channel.onopen = function () {
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return createInitialOffer(my_gadget); return createInitialOffer(gadget);
}) })
.push(my_gadget.state_parameter_dict.dataChannelOnopen) .push(gadget.state_parameter_dict.dataChannelOnopen)
.push(null, logError); .push(null, logError);
}; };
} }
data_channel.onmessage = data_channel.onmessage =
my_gadget.state_parameter_dict.dataChannelOnmessage; gadget.state_parameter_dict.dataChannelOnmessage;
} }
function sendOffer(my_gadget, offer) { function sendOffer(gadget, offer) {
const peer_list = my_gadget.state_parameter_dict.peer_connection_list; const peer_list = gadget.state_parameter_dict.peer_connection_list;
const peer_connection = peer_list[peer_list.length - 1]; const peer_connection = peer_list[peer_list.length - 1];
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
...@@ -614,18 +592,18 @@ ...@@ -614,18 +592,18 @@
return peer_connection.createAnswer( return peer_connection.createAnswer(
function (answer_description) { function (answer_description) {
peer_connection.setLocalDescription(answer_description); peer_connection.setLocalDescription(answer_description);
}, logError, my_gadget.state_parameter_dict.sdp_constraints); }, logError, gadget.state_parameter_dict.sdp_constraints);
}) })
.push(null, logError); .push(null, logError);
} }
function sendAnswer(my_gadget, answer) { function sendAnswer(gadget, answer) {
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return new RTCSessionDescription(JSON.parse(answer)); return new RTCSessionDescription(JSON.parse(answer));
}) })
.push(function (answer_description) { .push(function (answer_description) {
return my_gadget.state_parameter_dict.peer_connection_list[0] return gadget.state_parameter_dict.peer_connection_list[0]
.setRemoteDescription(answer_description); .setRemoteDescription(answer_description);
}) })
.push(null, logError); .push(null, logError);
...@@ -637,12 +615,11 @@ ...@@ -637,12 +615,11 @@
.ready(function (gadget) { .ready(function (gadget) {
gadget.state_parameter_dict = { gadget.state_parameter_dict = {
element: null, element: null,
name: null,
folder: null,
login_dict: { login_dict: {
folder: null,
room: null, room: null,
name: null,
role: null, role: null,
auth: null,
}, },
auth_dict: { auth_dict: {
dropbox_url: null, dropbox_url: null,
...@@ -654,25 +631,29 @@ ...@@ -654,25 +631,29 @@
candidate: null, candidate: null,
connected: false, connected: false,
offer_ready: false, offer_ready: false,
room_list: [],
data_channel_list: [], data_channel_list: [],
peer_connection_list: [], peer_connection_list: [],
guest_amount: 0, guest_amount: 0,
archive_amount: 0,
ice_config: { ice_config: {
iceServers: [{url: "stun:stun.1.google.com:19302"}] // iceServers: [{url: "stun:stun.1.google.com:19302"}],
// Must use own STUN server in production, but neither
// TURN nor STUN needed for local networks and IPv6
iceServers: [],
}, },
ice_constraints: { ice_constraints: {
optional: [{DtlsSrtpKeyAgreement: true}] mandatory: {googIPv6: true},
optional: [{DtlsSrtpKeyAgreement: true}],
}, },
dc_constraints: { dc_constraints: {
reliable: true reliable: true,
}, },
sdp_constraints: { sdp_constraints: {
optional: [], optional: [],
mandatory: { mandatory: {
OfferToReceiveAudio: false, OfferToReceiveAudio: false,
OfferToReceiveVideo: false OfferToReceiveVideo: false,
} },
}, },
}; };
return new RSVP.Queue() return new RSVP.Queue()
...@@ -697,14 +678,11 @@ ...@@ -697,14 +678,11 @@
} 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"); hideElementByClass(gadget, ".host-offer-form");
} }
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(function () {
if (gadget.state_parameter_dict.login_dict.auth !== null) {
return authenticate(
gadget, gadget.state_parameter_dict.login_dict.auth);
}
})
.push(null, logError); .push(null, logError);
}) })
...@@ -733,23 +711,61 @@ ...@@ -733,23 +711,61 @@
} }
return RSVP.all(promise_list); return RSVP.all(promise_list);
} else if (gadget.state_parameter_dict.login_dict.role === "guest") { } else if (gadget.state_parameter_dict.login_dict.role === "guest") {
if (channel_list[0].readyState !== "open") {
throw "The data channel is not open!";
} else {
return channel_list[0].send(message_string); return channel_list[0].send(message_string);
} }
}
}) })
.push(null, logError); .push(null, logError);
}) })
.declareMethod('authenticate', function () {
const gadget = this;
const fields = gadget.state_parameter_dict.element
.querySelector(".auth-form").elements;
const auth = fields.auth.value;
const dropbox_url = fields.dropbox_url.value;
const erp5_url = fields.erp5_url.value;
return new RSVP.Queue()
.push(function () {
let folder = gadget.state_parameter_dict.login_dict.folder;
if (folder[0] !== "/") {
folder = "/" + folder;
}
if (folder[folder.length - 1] !== "/") {
folder = folder + "/";
}
gadget.state_parameter_dict.auth_dict = {
dropbox_url: dropbox_url,
dropbox_folder: dropbox_url + folder,
erp5_url: erp5_url,
erp5_folder: null,
erp5_folder_url: null,
};
switch (auth) {
case "dropbox":
return authenticateDropbox(gadget);
case "erp5":
return authenticateErp5(gadget);
default:
return;
}
});
})
.declareService(function () { .declareService(function () {
const gadget = this; const gadget = this;
function handleSubmit(my_event) { function handleSubmit(event) {
switch (my_event.target.className) { switch (event.target.className) {
case "auth-form": case "auth-form":
return authenticate(gadget, my_event.target.auth.value); return gadget.authenticate();
case "host-offer-form": case "host-offer-form":
hideElementByClass(gadget, ".host-offer-form"); hideElementByClass(gadget, ".host-offer-form");
showElementByClass(gadget, ".host-answer-form"); showElementByClass(gadget, ".host-answer-form");
let offer = my_event.target.elements.send.value; let offer = event.target.elements.send.value;
my_event.target.elements.send.value = ""; event.target.elements.send.value = "";
return sendOffer(gadget, offer); return sendOffer(gadget, offer);
case "guest-offer-form": case "guest-offer-form":
hideElementByClass(gadget, ".guest-offer-form"); hideElementByClass(gadget, ".guest-offer-form");
...@@ -757,8 +773,8 @@ ...@@ -757,8 +773,8 @@
break; break;
case "guest-answer-form": case "guest-answer-form":
hideElementByClass(gadget, ".guest-answer-form"); hideElementByClass(gadget, ".guest-answer-form");
let answer = my_event.target.elements.send.value; let answer = event.target.elements.send.value;
my_event.target.elements.send.value = ""; event.target.elements.send.value = "";
return sendAnswer(gadget, answer); return sendAnswer(gadget, answer);
} }
} }
......
/*jslint indent:2, maxlen: 80, nomen: true */
/*global jIO, RSVP, UriTemplate, SimpleQuery, ComplexQuery, QueryFactory,
Query*/
(function (jIO, RSVP, UriTemplate, SimpleQuery, ComplexQuery, QueryFactory,
Query) {
"use strict";
function getSubIdEqualSubProperty(storage, value, key) {
var query;
if (storage._no_sub_query_id) {
throw new jIO.util.jIOError('no sub query id active', 404);
}
query = new SimpleQuery({
key: key,
value: value,
type: "simple"
});
if (storage._query.query !== undefined) {
query = new ComplexQuery({
operator: "AND",
query_list: [query, storage._query.query],
type: "complex"
});
}
query = Query.objectToSearchText(query);
return storage._sub_storage.allDocs({
"query": query,
"sort_on": storage._query.sort_on,
"select_list": storage._query.select_list,
"limit": storage._query.limit
})
.push(function (data) {
if (data.data.rows.length === 0) {
throw new jIO.util.jIOError(
"Can not find id",
404
);
}
if (data.data.rows.length > 1) {
throw new TypeError("id must be unique field: " + key
+ ", result:" + data.data.rows.toString());
}
return data.data.rows[0].id;
});
}
/*jslint unparam: true*/
var mapping_function = {
"equalSubProperty": {
"mapToSubProperty": function (property, sub_doc, doc, args, id) {
sub_doc[args] = doc[property];
return args;
},
"mapToMainProperty": function (property, sub_doc, doc, args, sub_id) {
if (sub_doc.hasOwnProperty(args)) {
doc[property] = sub_doc[args];
}
return args;
},
"mapToSubId": function (storage, doc, id, args) {
if (doc !== undefined) {
if (storage._property_for_sub_id &&
doc.hasOwnProperty(storage._property_for_sub_id)) {
return doc[storage._property_for_sub_id];
}
if (doc.hasOwnProperty(args)) {
return doc[args];
}
}
return getSubIdEqualSubProperty(storage, id, storage._map_id[1]);
},
"mapToId": function (storage, sub_doc, sub_id, args) {
return sub_doc[args];
}
},
"equalValue": {
"mapToSubProperty": function (property, sub_doc, doc, args) {
sub_doc[property] = args;
return property;
},
"mapToMainProperty": function (property) {
return property;
}
},
"ignore": {
"mapToSubProperty": function () {
return false;
},
"mapToMainProperty": function (property) {
return property;
}
},
"equalSubId": {
"mapToSubProperty": function (property, sub_doc, doc) {
sub_doc[property] = doc[property];
return property;
},
"mapToMainProperty": function (property, sub_doc, doc, args, sub_id) {
if (sub_id === undefined && sub_doc.hasOwnProperty(property)) {
doc[property] = sub_doc[property];
} else {
doc[property] = sub_id;
}
return property;
},
"mapToSubId": function (storage, doc, id, args) {
return id;
},
"mapToId": function (storage, sub_doc, sub_id) {
return sub_id;
}
},
"keep": {
"mapToSubProperty": function (property, sub_doc, doc) {
sub_doc[property] = doc[property];
return property;
},
"mapToMainProperty": function (property, sub_doc, doc) {
doc[property] = sub_doc[property];
return property;
}
},
"switchPropertyValue": {
"mapToSubProperty": function (property, sub_doc, doc, args) {
sub_doc[args[0]] = args[1][doc[property]];
return args[0];
},
"mapToMainProperty": function (property, sub_doc, doc, args) {
var subvalue, value = sub_doc[args[0]];
for (subvalue in args[1]) {
if (args[1].hasOwnProperty(subvalue)) {
if (value === args[1][subvalue]) {
doc[property] = subvalue;
return property;
}
}
}
}
}
};
/*jslint unparam: false*/
function initializeQueryAndDefaultMapping(storage) {
var property, query_list = [];
for (property in storage._mapping_dict) {
if (storage._mapping_dict.hasOwnProperty(property)) {
if (storage._mapping_dict[property][0] === "equalValue") {
if (storage._mapping_dict[property][1] === undefined) {
throw new jIO.util.jIOError("equalValue has not parameter", 400);
}
storage._default_mapping[property] =
storage._mapping_dict[property][1];
query_list.push(new SimpleQuery({
key: property,
value: storage._mapping_dict[property][1],
type: "simple"
}));
}
if (storage._mapping_dict[property][0] === "equalSubId") {
if (storage._property_for_sub_id !== undefined) {
throw new jIO.util.jIOError(
"equalSubId can be defined one time",
400
);
}
storage._property_for_sub_id = property;
}
}
}
if (storage._query.query !== undefined) {
query_list.push(QueryFactory.create(storage._query.query));
}
if (query_list.length > 1) {
storage._query.query = new ComplexQuery({
type: "complex",
query_list: query_list,
operator: "AND"
});
} else if (query_list.length === 1) {
storage._query.query = query_list[0];
}
}
function MappingStorage(spec) {
this._mapping_dict = spec.property || {};
this._sub_storage = jIO.createJIO(spec.sub_storage);
this._map_all_property = spec.map_all_property !== undefined ?
spec.map_all_property : true;
this._no_sub_query_id = spec.no_sub_query_id;
this._attachment_mapping_dict = spec.attachment || {};
this._query = spec.query || {};
this._map_id = spec.id || ["equalSubId"];
this._id_mapped = (spec.id !== undefined) ? spec.id[1] : false;
if (this._query.query !== undefined) {
this._query.query = QueryFactory.create(this._query.query);
}
this._default_mapping = {};
initializeQueryAndDefaultMapping(this);
}
function getAttachmentId(storage, sub_id, attachment_id, method) {
var mapping_dict = storage._attachment_mapping_dict;
if (mapping_dict !== undefined
&& mapping_dict[attachment_id] !== undefined
&& mapping_dict[attachment_id][method] !== undefined
&& mapping_dict[attachment_id][method].uri_template !== undefined) {
return UriTemplate.parse(
mapping_dict[attachment_id][method].uri_template
).expand({id: sub_id});
}
return attachment_id;
}
function getSubStorageId(storage, id, doc) {
return new RSVP.Queue()
.push(function () {
var map_info = storage._map_id || ["equalSubId"];
if (storage._property_for_sub_id && doc !== undefined &&
doc.hasOwnProperty(storage._property_for_sub_id)) {
return doc[storage._property_for_sub_id];
}
return mapping_function[map_info[0]].mapToSubId(
storage,
doc,
id,
map_info[1]
);
});
}
function mapToSubProperty(storage, property, sub_doc, doc, id) {
var mapping_info = storage._mapping_dict[property] || ["keep"];
return mapping_function[mapping_info[0]].mapToSubProperty(
property,
sub_doc,
doc,
mapping_info[1],
id
);
}
function mapToMainProperty(storage, property, sub_doc, doc, sub_id) {
var mapping_info = storage._mapping_dict[property] || ["keep"];
return mapping_function[mapping_info[0]].mapToMainProperty(
property,
sub_doc,
doc,
mapping_info[1],
sub_id
);
}
function mapToMainDocument(storage, sub_doc, sub_id) {
var doc = {},
property,
property_list = [storage._id_mapped];
for (property in storage._mapping_dict) {
if (storage._mapping_dict.hasOwnProperty(property)) {
property_list.push(mapToMainProperty(
storage,
property,
sub_doc,
doc,
sub_id
));
}
}
if (storage._map_all_property) {
for (property in sub_doc) {
if (sub_doc.hasOwnProperty(property)) {
if (property_list.indexOf(property) < 0) {
doc[property] = sub_doc[property];
}
}
}
}
if (storage._map_for_sub_storage_id !== undefined) {
doc[storage._map_for_sub_storage_id] = sub_id;
}
return doc;
}
function mapToSubstorageDocument(storage, doc, id) {
var sub_doc = {}, property;
for (property in doc) {
if (doc.hasOwnProperty(property)) {
mapToSubProperty(storage, property, sub_doc, doc, id);
}
}
for (property in storage._default_mapping) {
if (storage._default_mapping.hasOwnProperty(property)) {
sub_doc[property] = storage._default_mapping[property];
}
}
if (storage._map_id[0] === "equalSubProperty" && id !== undefined) {
sub_doc[storage._map_id[1]] = id;
}
return sub_doc;
}
function handleAttachment(storage, argument_list, method) {
return getSubStorageId(storage, argument_list[0])
.push(function (sub_id) {
argument_list[0] = sub_id;
argument_list[1] = getAttachmentId(
storage,
sub_id,
argument_list[1],
method
);
return storage._sub_storage[method + "Attachment"].apply(
storage._sub_storage,
argument_list
);
});
}
MappingStorage.prototype.get = function (id) {
var storage = this;
return getSubStorageId(this, id)
.push(function (sub_id) {
return storage._sub_storage.get(sub_id)
.push(function (sub_doc) {
return mapToMainDocument(storage, sub_doc, sub_id);
});
});
};
MappingStorage.prototype.post = function (doc) {
var sub_doc = mapToSubstorageDocument(
this,
doc
),
id = doc[this._property_for_sub_id];
if (this._property_for_sub_id && id !== undefined) {
return this._sub_storage.put(id, sub_doc);
}
if (!this._id_mapped || doc[this._id_mapped] !== undefined) {
return this._sub_storage.post(sub_doc);
}
throw new jIO.util.jIOError(
"post is not supported with id mapped",
400
);
};
MappingStorage.prototype.put = function (id, doc) {
var storage = this,
sub_doc = mapToSubstorageDocument(this, doc, id);
return getSubStorageId(this, id, doc)
.push(function (sub_id) {
return storage._sub_storage.put(sub_id, sub_doc);
})
.push(undefined, function (error) {
if (error instanceof jIO.util.jIOError && error.status_code === 404) {
return storage._sub_storage.post(sub_doc);
}
throw error;
})
.push(function () {
return id;
});
};
MappingStorage.prototype.remove = function (id) {
var storage = this;
return getSubStorageId(this, id)
.push(function (sub_id) {
return storage._sub_storage.remove(sub_id);
})
.push(function () {
return id;
});
};
MappingStorage.prototype.putAttachment = function (id, attachment_id) {
return handleAttachment(this, arguments, "put", id)
.push(function () {
return attachment_id;
});
};
MappingStorage.prototype.getAttachment = function () {
return handleAttachment(this, arguments, "get");
};
MappingStorage.prototype.removeAttachment = function (id, attachment_id) {
return handleAttachment(this, arguments, "remove", id)
.push(function () {
return attachment_id;
});
};
MappingStorage.prototype.allAttachments = function (id) {
var storage = this, sub_id;
return getSubStorageId(storage, id)
.push(function (sub_id_result) {
sub_id = sub_id_result;
return storage._sub_storage.allAttachments(sub_id);
})
.push(function (result) {
var attachment_id,
attachments = {},
mapping_dict = {};
for (attachment_id in storage._attachment_mapping_dict) {
if (storage._attachment_mapping_dict.hasOwnProperty(attachment_id)) {
mapping_dict[getAttachmentId(storage, sub_id, attachment_id, "get")]
= attachment_id;
}
}
for (attachment_id in result) {
if (result.hasOwnProperty(attachment_id)) {
if (mapping_dict.hasOwnProperty(attachment_id)) {
attachments[mapping_dict[attachment_id]] = {};
} else {
attachments[attachment_id] = {};
}
}
}
return attachments;
});
};
MappingStorage.prototype.hasCapacity = function (name) {
return this._sub_storage.hasCapacity(name);
};
MappingStorage.prototype.repair = function () {
return this._sub_storage.repair.apply(this._sub_storage, arguments);
};
MappingStorage.prototype.bulk = function (id_list) {
var storage = this;
function mapId(parameter) {
return getSubStorageId(storage, parameter.parameter_list[0])
.push(function (id) {
return {"method": parameter.method, "parameter_list": [id]};
});
}
return new RSVP.Queue()
.push(function () {
var promise_list = id_list.map(mapId);
return RSVP.all(promise_list);
})
.push(function (id_list_mapped) {
return storage._sub_storage.bulk(id_list_mapped);
})
.push(function (result) {
var mapped_result = [], i;
for (i = 0; i < result.length; i += 1) {
mapped_result.push(mapToMainDocument(
storage,
result[i]
));
}
return mapped_result;
});
};
MappingStorage.prototype.buildQuery = function (option) {
var storage = this,
i,
query,
property,
select_list = [],
sort_on = [];
function mapQuery(one_query) {
var j, query_list = [], key, sub_query;
if (one_query.type === "complex") {
for (j = 0; j < one_query.query_list.length; j += 1) {
sub_query = mapQuery(one_query.query_list[j]);
if (sub_query) {
query_list.push(sub_query);
}
}
one_query.query_list = query_list;
return one_query;
}
key = mapToMainProperty(storage, one_query.key, {}, {});
if (key) {
one_query.key = key;
return one_query;
}
return false;
}
if (option.sort_on !== undefined) {
for (i = 0; i < option.sort_on.length; i += 1) {
property = mapToMainProperty(this, option.sort_on[i][0], {}, {});
if (property && sort_on.indexOf(property) < 0) {
sort_on.push([property, option.sort_on[i][1]]);
}
}
}
if (this._query.sort_on !== undefined) {
for (i = 0; i < this._query.sort_on.length; i += 1) {
property = mapToMainProperty(this, this._query.sort_on[i], {}, {});
if (sort_on.indexOf(property) < 0) {
sort_on.push([property, option.sort_on[i][1]]);
}
}
}
if (option.select_list !== undefined) {
for (i = 0; i < option.select_list.length; i += 1) {
property = mapToMainProperty(this, option.select_list[i], {}, {});
if (property && select_list.indexOf(property) < 0) {
select_list.push(property);
}
}
}
if (this._query.select_list !== undefined) {
for (i = 0; i < this._query.select_list; i += 1) {
property = this._query.select_list[i];
if (select_list.indexOf(property) < 0) {
select_list.push(property);
}
}
}
if (this._id_mapped) {
// modify here for future way to map id
select_list.push(this._id_mapped);
}
if (option.query !== undefined) {
query = mapQuery(QueryFactory.create(option.query));
}
if (this._query.query !== undefined) {
if (query === undefined) {
query = this._query.query;
}
query = new ComplexQuery({
operator: "AND",
query_list: [query, this._query.query],
type: "complex"
});
}
if (query !== undefined) {
query = Query.objectToSearchText(query);
}
return this._sub_storage.allDocs(
{
query: query,
select_list: select_list,
sort_on: sort_on,
limit: option.limit
}
)
.push(function (result) {
var sub_doc, map_info = storage._map_id || ["equalSubId"];
for (i = 0; i < result.data.total_rows; i += 1) {
sub_doc = result.data.rows[i].value;
result.data.rows[i].id =
mapping_function[map_info[0]].mapToId(
storage,
sub_doc,
result.data.rows[i].id,
map_info[1]
);
result.data.rows[i].value =
mapToMainDocument(
storage,
sub_doc
);
}
return result.data.rows;
});
};
jIO.addStorage('mapping', MappingStorage);
}(jIO, RSVP, UriTemplate, SimpleQuery, ComplexQuery, QueryFactory, Query));
\ 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>classification/collaborative/team</string>
<string>contributor/person_module/1</string>
</tuple>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>jio_mappingstorage.js</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>jio_mappingstorage_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>JIO Mapping Storage 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/jio_mappingstorage_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