Commit 6cdfcbfa authored by Eugene Shen's avatar Eugene Shen

Add miscellaneous chat functionality

Abstract WebRTC signalling to prepare for ERP5,
detect client browser unloading and send message,
directly display URLs of images as an actual <img>,
add global "notifications" such as joining and /help,
clean names to avoid OAuth2 state conflicts in the URL,
make Dropbox authentication more robust against failures,
and parse non-image URLs as <a> using a regular expression.
parent 6a2c3380
......@@ -7,6 +7,10 @@ h3 {
p {
margin: 0;
}
img {
max-width: 100%;
max-height: 100%;
}
.chat {
border: 1px solid;
width: 680px;
......
......@@ -68,6 +68,7 @@
<value>
<tuple>
<string>classification/collaborative/team</string>
<string>contributor/person_module/1</string>
</tuple>
</value>
</item>
......
(function (window, document, loopEventListener, rJS, RSVP) {
function cleanId(input_id) {
var reserved = ["_", "&", "=", ",", ";"];
for (var i = 0, i_len = reserved.length; i < i_len; i++) {
input_id = input_id.replace(reserved[i], "-");
}
return input_id;
}
function getQueryValue(query) {
if (document.URL.indexOf(query + "=") != -1) {
var start = document.URL.indexOf(query + "=") + query.length + 1;
......@@ -7,7 +15,7 @@
if (end === -1) {
end = document.URL.length;
}
return document.URL.slice(start, end);
return decodeURIComponent(document.URL.slice(start, end));
} else if (document.URL.indexOf(query + "%3D") != -1) {
var start = document.URL.indexOf(query + "%3D") + query.length + 3;
var end = document.URL.indexOf("%2C", start);
......@@ -17,7 +25,7 @@
end = document.URL.length;
}
}
return document.URL.slice(start, end);
return decodeURIComponent(document.URL.slice(start, end));
} else {
return "";
}
......@@ -32,11 +40,9 @@
})
.push(function (webrtc_gadget) {
var form = my_event.target.elements;
webrtc_gadget.state_parameter_dict.name = form.name.value;
webrtc_gadget.state_parameter_dict.folder = form.folder.value;
// Change underscores to hyphens because underscores are separators
webrtc_gadget.state_parameter_dict.room =
form.room.value.replace("_", "-");
webrtc_gadget.state_parameter_dict.name = cleanId(form.name.value);
webrtc_gadget.state_parameter_dict.folder = cleanId(form.folder.value);
webrtc_gadget.state_parameter_dict.room = cleanId(form.room.value);
webrtc_gadget.state_parameter_dict.role = form.role.value;
var element = my_gadget.state_parameter_dict.element;
while (element.firstChild) {
......
......@@ -68,6 +68,7 @@
<value>
<tuple>
<string>classification/collaborative/team</string>
<string>contributor/person_module/1</string>
</tuple>
</value>
</item>
......
......@@ -68,6 +68,7 @@
<value>
<tuple>
<string>classification/collaborative/team</string>
<string>contributor/person_module/1</string>
</tuple>
</value>
</item>
......
......@@ -3,7 +3,7 @@
/* Abstract Data Types:
* message is a JSON object representing a message with all its metadata.
* message_string is JSON.stringify(message) for sending over WebRTC.
* chat is messageToChat(message) for displaying in the chat log.
* chat is messageToChat(message) for displaying an element in the chat log.
*/
/* Program Workflow:
......@@ -49,17 +49,15 @@
return JSON.stringify(lhs) === JSON.stringify(rhs);
}
// Translate message to chat, in human-readable form
function messageToChat(message) {
return "[" + new Date(message.time).toLocaleString() + "] "
+ message.name + ": " + message.content;
function stringEndsWith(string, suffix) {
return string.indexOf(suffix, string.length - suffix.length) !== -1;
}
// Add new favicon or change existing favicon to image in favicon_url
function changeFavicon(favicon_url) {
var link = document.querySelector("link[rel*='icon']")
|| document.createElement("link")
link.type = "image/x-icon"
|| document.createElement("link");
link.type = "image/x-icon";
link.rel = "shortcut icon";
link.href = favicon_url;
document.head.appendChild(link);
......@@ -74,7 +72,7 @@
} else {
return function (lhs, rhs) {
return getTime(lhs) > getTime(rhs);
}
};
}
}
......@@ -89,6 +87,138 @@
}
}
// Create new message from its type and content
function createMessage(gadget, type, content) {
return {
type: type,
name: gadget.state_parameter_dict.name,
folder: gadget.state_parameter_dict.folder,
room: gadget.state_parameter_dict.room,
time: new Date(),
content: content
};
}
// Translate message to chat, in some HTML element
function messageToChat(message) {
var chat = document.createElement("p");
var image_extensions = [".jpg", ".png", ".gif", ".bmp", ".tif", ".svg"];
var re = /((?:https?:\/\/)?(?:[-a-zA-Z0-9_~\/]+\.)+[-a-zA-Z0-9_~#?&=\/]+)/g;
switch (message.type) {
case "bundle":
break;
case "notification":
chat.appendChild(document.createTextNode(message.content));
return chat;
case "message":
chat.appendChild(document.createTextNode(
"[" + new Date(message.time).toLocaleString() + "] "
+ message.name + ": "));
var matches = message.content.split(re);
for (var i = 0, i_len = matches.length - 1; i < i_len; i += 2) {
chat.appendChild(document.createTextNode(matches[i]));
var link_string = matches[i + 1];
var dot_count = link_string.match(/\./g).length;
// If 2d + 1 >= L, then the string has only single letters
if (2 * dot_count + 1 >= link_string.length) {
chat.appendChild(document.createTextNode(link_string));
} else {
var is_image = false;
var absolute_url;
if (link_string.indexOf(":") != -1) {
absolute_url = link_string;
} else {
absolute_url = "http://" + link_string;
}
for (var j = 0, j_len = image_extensions.length; j < j_len; j++) {
if (stringEndsWith(link_string, image_extensions[j])) {
var image = document.createElement("img");
image.src = absolute_url;
chat.appendChild(image);
is_image = true;
break;
}
}
if (!is_image) {
var link = document.createElement("a");
link.href = absolute_url;
link.innerHTML = link_string;
chat.appendChild(link);
}
}
}
chat.appendChild(document.createTextNode(matches[matches.length - 1]));
return chat;
}
}
// Add message to the list and append chat to chat box
function storeList(gadget, message) {
gadget.state_parameter_dict.message_list.push(message);
}
// Appends a message to the chat
function appendChat(gadget, chat) {
var container = gadget.state_parameter_dict
.element.querySelector(".chat");
container.appendChild(chat);
container.scrollTop = container.scrollHeight;
}
// Sort the list, dedupe, and overwrite the chat box,
// efficient because the archive is originally sorted
function refreshChat(gadget) {
var old_list = gadget.state_parameter_dict.message_list;
var container = gadget.state_parameter_dict.element.querySelector(".chat");
while (container.firstChild) {
container.removeChild(container.firstChild);
}
old_list.sort(messageTimeComparator);
var new_list = [];
var last_message;
for (var i = 0, i_len = old_list.length; i < i_len; i++) {
var message = old_list[i];
if (sameMessage(last_message, message)) {
continue;
}
last_message = message;
new_list.push(message);
container.appendChild(messageToChat(message));
}
container.scrollTop = container.scrollHeight;
gadget.state_parameter_dict.message_list = new_list;
}
// Parse chat commands
function parseCommand(gadget, chat) {
var split = chat.slice(1).split(" ");
var command = split.shift();
var argument = split.join(" ");
switch (command) {
case "rename":
gadget.state_parameter_dict.name = argument;
break;
case "quit":
window.location.reload();
break;
case "help":
var help_message_list = [
"--------------------------------------------------------------------"
+ "-----------------------------------------------------------------",
"Available commands:",
"/rename [name]: changes your nickname to [name]",
"/help: displays this help box",
"/quit: disconnects from the chat and refreshes the page",
"--------------------------------------------------------------------"
+ "-----------------------------------------------------------------",
];
for (var i = 0, i_len = help_message_list.length; i < i_len; i++) {
appendChat(gadget, messageToChat(createMessage(
gadget, "notification", help_message_list[i])));
}
}
}
rJS(window)
.ready(function (gadget) {
return new RSVP.Queue()
......@@ -97,10 +227,9 @@
name: null,
folder: null,
room: null,
// sorted local storage for all messages, the 'list'
message_list: [],
// XXX temporary external URLs
alert_icon: "https://gitlab.com/favicon.ico",
alert_icon: "http://icons.iconarchive.com/icons/iconarchive/"
+ "red-orb-alphabet/128/Exclamation-mark-icon.png",
default_icon: "https://softinst75770.host.vifib.net/"
+ "erp5/web_site_module/web_chat/favicon.ico"
};
......@@ -128,6 +257,10 @@
.push(function () {
return gadget.sendLocalArchive();
})
.push(function () {
return gadget.deployMessage(
"notification", gadget.state_parameter_dict.name + " has joined.");
})
.push(null, logError);
})
......@@ -159,7 +292,6 @@
return RSVP.all(message_string_list);
})
.push(function (message_string_list) {
var promise_list = [];
var message_queue = new FastPriorityQueue(messageTimeCompare(true));
for (var i = 0, i_len = message_string_list.length; i < i_len; i++) {
message_queue.add(JSON.parse(message_string_list[i]));
......@@ -171,13 +303,9 @@
message.room === gadget.state_parameter_dict.room &&
!sameMessage(last_message, message)) {
last_message = message;
promise_list.push(gadget.storeList(message));
storeList(gadget, message);
}
}
return RSVP.all(promise_list);
})
.push(function (res) {
console.log('lg', res.length);
})
.push(null, logError);
})
......@@ -185,16 +313,8 @@
// Send all messages in the list, in sorted order, to peer
.declareMethod('sendLocalArchive', function () {
var gadget = this;
return new RSVP.Queue()
.push(function () {
console.log('ls', gadget.state_parameter_dict.message_list.length);
return gadget.createMessage(
"bundle", gadget.state_parameter_dict.message_list);
})
.push(function (message) {
return gadget.sendMessage(JSON.stringify(message));
})
.push(null, logError);
return gadget.sendMessage(JSON.stringify(createMessage(
gadget, "bundle", gadget.state_parameter_dict.message_list)));
})
// Get all new messages from the sorted list of peer,
......@@ -202,7 +322,6 @@
// and refresh, dedupe, and resort the list using refreshChat
.declareMethod('getRemoteArchive', function (bundle) {
var gadget = this;
console.log('rg', bundle.content.length);
return new RSVP.Queue()
.push(function () {
var list = gadget.state_parameter_dict.message_list;
......@@ -215,168 +334,81 @@
index++;
}
if (index >= list.length || !sameMessage(list[index], message)) {
storeList(gadget, message);
promise_list.push(gadget.storeArchive(message));
promise_list.push(gadget.storeList(message));
}
}
return RSVP.all(promise_list);
})
.push(function () {
return gadget.refreshChat();
return refreshChat(gadget);
})
.push(null, logError);
})
// Create new message and send it to peer
.declareMethod('deployMessage', function (form_input) {
.declareMethod('deployMessage', function (type, content) {
var gadget = this;
var content;
var message;
var message = createMessage(gadget, type, content);
return new RSVP.Queue()
.push(function () {
content = form_input.value;
form_input.value = "";
})
.push(function () {
if (content.indexOf("/") === 0) {
return gadget.parseCommand(content);
} else {
return new RSVP.Queue()
.push(function () {
return gadget.createMessage("message", content);
})
.push(function (result) {
message = result;
return gadget.getMessage(result);
})
.push(function () {
return changeFavicon(gadget.state_parameter_dict.default_icon);
return gadget.getMessage(message);
})
.push(function () {
changeFavicon(gadget.state_parameter_dict.default_icon);
return gadget.sendMessage(JSON.stringify(message));
})
.push(null, logError);
}
})
.push(null, logError);
})
// Get message from peer, store it in archive and list
.declareMethod('getMessage', function (message) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
changeFavicon(gadget.state_parameter_dict.alert_icon);
storeList(gadget, message);
appendChat(gadget, messageToChat(message));
return gadget.storeArchive(message);
})
.push(function () {
return gadget.storeList(message);
})
.push(function () {
return gadget.appendChat(messageToChat(message));
})
.push(function () {
return changeFavicon(gadget.state_parameter_dict.alert_icon);
})
.push(null, logError);
})
// Create new message from its type and content
.declareMethod('createMessage', function (type, content) {
var gadget = this;
return {
type: type,
name: gadget.state_parameter_dict.name,
folder: gadget.state_parameter_dict.folder,
room: gadget.state_parameter_dict.room,
time: new Date(),
content: content
};
})
// Store message in the archive
.declareMethod('storeArchive', function (message) {
var gadget = this;
return gadget.jioApply('putAttachment', "/",
message.folder + "_" + message.room + "_" +
+ message.name + "_" + getTime(message).toString(),
var id = message.folder + "_" + message.room + "_"
+ message.name + "_" + getTime(message).toString();
return gadget.jioApply('putAttachment', "/", id,
new Blob([JSON.stringify(message)], {type: "text"}));
})
// Add message to the list and append chat to chat box
.declareMethod('storeList', function (message) {
var gadget = this;
gadget.state_parameter_dict.message_list.push(message);
})
// Appends a message to the chat
.declareMethod('appendChat', function (chat) {
var gadget = this;
var container = gadget.state_parameter_dict
.element.querySelector(".chat");
var chat_element = document.createElement("p");
chat_element.appendChild(document.createTextNode(chat));
container.appendChild(chat_element);
container.scrollTop = container.scrollHeight;
})
// Sort the list, dedupe, and overwrite the chat box,
// efficient because the archive is originally sorted
.declareMethod('refreshChat', function () {
var gadget = this;
return new RSVP.Queue()
.push(function () {
var old_list = gadget.state_parameter_dict.message_list;
var container = gadget.state_parameter_dict
.element.querySelector(".chat");
while (container.firstChild) {
container.removeChild(container.firstChild);
}
old_list.sort(messageTimeComparator);
var new_list = [];
var last_message;
for (var i = 0, i_len = old_list.length; i < i_len; i++) {
var message = old_list[i];
if (sameMessage(last_message, message)) {
continue;
}
last_message = message;
new_list.push(message);
var chat_element = document.createElement("p");
chat_element.appendChild(
document.createTextNode(messageToChat(message)));
container.appendChild(chat_element);
}
container.scrollTop = container.scrollHeight;
gadget.state_parameter_dict.message_list = new_list;
console.log('old', old_list.length);
console.log('new', new_list.length);
})
.push(null, logError);
})
// Parse chat commands
.declareMethod('parseCommand', function (chat) {
var gadget = this;
var split = chat.slice(1).split(" ");
var command = split.shift();
var argument = split.join(" ");
switch (command) {
case "rename":
gadget.state_parameter_dict.name = argument;
}
})
// Listen for new chats
.declareService(function () {
var gadget = this;
function handleSubmit(event) {
switch (event.target.className) {
case "send-form":
return gadget.deployMessage(event.target.elements.content);
var content = event.target.elements.content.value;
event.target.elements.content.value = "";
if (content.indexOf("/") === 0) {
return parseCommand(gadget, content);
} else {
return gadget.deployMessage("message", content);
}
}
}
return loopEventListener(
gadget.state_parameter_dict.element, "submit", false, handleSubmit);
})
.declareService(function () {
var gadget = this;
return new RSVP.Queue()
.push(function () {
return promiseEventListener(window, "beforeunload", true);
})
.push(function () {
return gadget.sendMessage(JSON.stringify(
createMessage(gadget, "notification",
gadget.state_parameter_dict.name + " has quit.")));
});
});
}(window, document, FastPriorityQueue, loopEventListener, rJS, RSVP));
\ No newline at end of file
......@@ -68,6 +68,7 @@
<value>
<tuple>
<string>classification/collaborative/team</string>
<string>contributor/person_module/1</string>
</tuple>
</value>
</item>
......
......@@ -66,7 +66,7 @@
.push(function () {
return dropbox_gadget.put(folder, {});
})
.push(function () {
.push(null, function () {
return dropbox_gadget.allAttachments(folder);
})
.push(function (attachment_list) {
......@@ -108,21 +108,13 @@
.push(null, logError);
}
function removeDropboxContent(dropbox_gadget, folder, name) {
return new RSVP.Queue()
.push(function () {
return dropbox_gadget.removeAttachment(folder, name);
})
.push(null, logError);
}
function putDropboxContent(dropbox_gadget, folder, name, content) {
return new RSVP.Queue()
.push(function () {
return dropbox_gadget.putAttachment(
folder, name, new Blob([content], {type: "text"}));
})
.push(null, logError);
}
function removeDropboxContent(dropbox_gadget, folder, name) {
return dropbox_gadget.removeAttachment(folder, name);
}
function authenticateDropbox(my_gadget) {
......@@ -138,81 +130,138 @@
.push(function () {
if (my_gadget.state_parameter_dict.role === "host") {
hideElementByClass(my_gadget, ".dropbox-form");
my_gadget.state_parameter_dict.storage_list.dropbox = true;
return authenticateDropboxHost(my_gadget);
return authenticateHost(my_gadget, "dropbox_gadget", {
getOffer: getDropboxOffer,
getContent: getDropboxContent,
putContent: putDropboxContent,
removeContent: removeDropboxContent
});
} else if (my_gadget.state_parameter_dict.role === "guest") {
return authenticateDropboxGuest(my_gadget);
return authenticateGuest(my_gadget, "dropbox_gadget", {
getOffer: getDropboxOffer,
getContent: getDropboxContent,
putContent: putDropboxContent,
removeContent: removeDropboxContent
});
}
})
.push(null, logError);
}
function authenticateDropboxHost(my_gadget) {
function authenticateHost(my_gadget, jio_gadget_name, jio_function_dict) {
var folder = my_gadget.state_parameter_dict.dropbox_folder;
var room = my_gadget.state_parameter_dict.room;
var name;
var dropbox_gadget;
var jio_gadget;
return new RSVP.Queue()
.push(function () {
return my_gadget.getDeclaredGadget("dropbox_gadget");
return my_gadget.getDeclaredGadget(jio_gadget_name);
})
.push(function (jio_gadget) {
dropbox_gadget = jio_gadget;
.push(function (storage_gadget) {
jio_gadget = storage_gadget;
return pollUntilNotNull(500, 7200000, function () {
return getDropboxOffer(dropbox_gadget, folder, room);
console.log("getting offer name");
return jio_function_dict.getOffer(jio_gadget, folder, room);
}, function (offer_name) {
name = offer_name;
});
})
.push(function () {
return pollUntilNotNull(50, 1000, function () {
return getDropboxContent(dropbox_gadget, folder, "offer_" + name);
var num = 10;
return pollUntilNotNull(50, 5000, function () {
num++;
if (num >= 10) {
num = 0;
console.log("getting offer");
}
return jio_function_dict.getContent(
jio_gadget, folder, "offer_" + name);
}, function (guest_offer) {
return sendOffer(my_gadget, guest_offer);
});
}, function () {
console.log("1");
})
.push(function () {
var num = 10;
return pollUntilNotNull(50, 10000, function () {
num++;
if (num >= 10) {
num = 0;
console.log("getting candidate");
}
return my_gadget.state_parameter_dict.candidate;
}, function (host_answer) {
return putDropboxContent(
dropbox_gadget, folder, "answer_" + name, host_answer)
return jio_function_dict.putContent(
jio_gadget, folder, "answer_" + name, host_answer)
});
}, function () {
console.log("2");
})
.push(function () {
return removeDropboxContent(dropbox_gadget, folder, "offer_" + name);
console.log('removing content');
return jio_function_dict.removeContent(
jio_gadget, folder, "offer_" + name);
}, function () {
console.log("3");
})
.push(function () {
return authenticateHost(my_gadget, jio_gadget_name, jio_function_dict);
}, function () {
return authenticateHost(my_gadget, jio_gadget_name, jio_function_dict);
})
.push(null, logError);
}
function authenticateDropboxGuest(my_gadget) {
function authenticateGuest(my_gadget, jio_gadget_name, jio_function_dict) {
var folder = my_gadget.state_parameter_dict.dropbox_folder;
var room = my_gadget.state_parameter_dict.room;
var name;
var dropbox_gadget;
var jio_gadget;
return new RSVP.Queue()
.push(function () {
return my_gadget.getDeclaredGadget("dropbox_gadget");
return my_gadget.getDeclaredGadget(jio_gadget_name);
})
.push(function (jio_gadget) {
dropbox_gadget = jio_gadget;
return pollUntilNotNull(50, 10000, function () {
.push(function (storage_gadget) {
jio_gadget = storage_gadget;
var num = 10;
return pollUntilNotNull(50, 15000, function () {
num++;
if (num >= 10) {
num = 0;
console.log("getting candidate");
}
return my_gadget.state_parameter_dict.candidate;
}, function (guest_offer) {
name = room + "_" + my_gadget.state_parameter_dict.name + ".txt";
return putDropboxContent(
dropbox_gadget, folder, "offer_" + name, guest_offer);
return jio_function_dict.putContent(
jio_gadget, folder, "offer_" + name, guest_offer);
});
}, function () {
console.log("4");
})
.push(function () {
var num = 10;
return pollUntilNotNull(50, 30000, function () {
return getDropboxContent(dropbox_gadget, folder, "answer_" + name);
num++;
if (num >= 10) {
num = 0;
console.log('getting answer');
}
return jio_function_dict.getContent(
jio_gadget, folder, "answer_" + name);
}, function (host_answer) {
return sendAnswer(my_gadget, host_answer);
});
}, function () {
console.log("5");
})
.push(function () {
return removeDropboxContent(dropbox_gadget, folder, "answer_" + name);
console.log("removing content");
return jio_function_dict.removeContent(
jio_gadget, folder, "answer_" + name);
}, function () {
console.log("6");
})
.push(null, logError);
}
......@@ -273,17 +322,6 @@
.push(null, logError);
}
})
.push(function () {
var promise_list = [];
var storage_list = my_gadget.state_parameter_dict.storage_list;
if (storage_list.dropbox) {
promise_list.push(authenticateDropboxHost(my_gadget));
}
if (storage_list.erp5) {
// XXX work in progress
}
return RSVP.all(promise_list);
})
.push(null, logError);
}
......@@ -345,7 +383,7 @@
return my_gadget.getDeclaredGadget("chat_gadget");
})
.push(function (chat_gadget) {
if (message.type === "message") {
if (message.type === "message" || message.type === "notification") {
return new RSVP.Queue()
.push(function () {
return chat_gadget.getMessage(message);
......@@ -427,10 +465,6 @@
dropbox_token: "igeiyv4pkt0y0mm",
document_url: "https://softinst75770.host.vifib.net/"
+ "erp5/web_site_module/web_chat/",
storage_list: {
dropbox: false,
erp5: false
},
ice_config: {
iceServers: [{ url: "stun:stun.1.google.com:19302" }]
},
......@@ -460,12 +494,8 @@
.allowPublicAcquisition('sendMessage', function (message_string) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
return gadget.sendMessage(message_string);
})
.push(null, logError);
})
.declareMethod('sendMessage', function (message_string, source) {
var gadget = this;
......
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