Commit fdcbdb06 authored by Eugene Shen's avatar Eugene Shen

Put all DOM modifications into onStateChange

parent 17bb0e4e
/**
* FastPriorityQueue.js : a fast heap-based priority queue in JavaScript.
* (c) the authors
* Licensed under the Apache License, Version 2.0.
*
* Speed-optimized heap-based priority queue for modern browsers and JavaScript engines.
*
* Usage :
Installation (in shell, if you use node):
$ npm install fastpriorityqueue
Running test program (in JavaScript):
// var FastPriorityQueue = require("fastpriorityqueue");// in node
var x = new FastPriorityQueue();
x.add(1);
x.add(0);
x.add(5);
x.add(4);
x.add(3);
x.peek(); // should return 0, leaves x unchanged
x.size; // should return 5, leaves x unchanged
while(!x.isEmpty()) {
console.log(x.poll());
} // will print 0 1 3 4 5
x.trim(); // (optional) optimizes memory usage
*/
"use strict";
var defaultcomparator = function (a, b) {
return a < b;
};
// the provided comparator function should take a, b and return *true* when a < b
function FastPriorityQueue(comparator) {
this.array = [];
this.size = 0;
this.compare = comparator || defaultcomparator;
}
// Add an element the the queue
// runs in O(log n) time
FastPriorityQueue.prototype.add = function (myval) {
var i = this.size;
this.array[this.size] = myval;
this.size += 1;
var p;
var ap;
while (i > 0) {
p = (i - 1) >> 1;
ap = this.array[p];
if (!this.compare(myval, ap)) {
break;
}
this.array[i] = ap;
i = p;
}
this.array[i] = myval;
};
// replace the content of the heap by provided array and "heapifies it"
FastPriorityQueue.prototype.heapify = function (arr) {
this.array = arr;
this.size = arr.length;
var i;
for (i = (this.size >> 1); i >= 0; i--) {
this._percolateDown(i);
}
};
// for internal use
FastPriorityQueue.prototype._percolateUp = function (i) {
var myval = this.array[i];
var p;
var ap;
while (i > 0) {
p = (i - 1) >> 1;
ap = this.array[p];
if (!this.compare(myval, ap)) {
break;
}
this.array[i] = ap;
i = p;
}
this.array[i] = myval;
};
// for internal use
FastPriorityQueue.prototype._percolateDown = function (i) {
var size = this.size;
var hsize = this.size >>> 1;
var ai = this.array[i];
var l;
var r;
var bestc;
while (i < hsize) {
l = (i << 1) + 1;
r = l + 1;
bestc = this.array[l];
if (r < size) {
if (this.compare(this.array[r], bestc)) {
l = r;
bestc = this.array[r];
}
}
if (!this.compare(bestc, ai)) {
break;
}
this.array[i] = bestc;
i = l;
}
this.array[i] = ai;
};
// Look at the top of the queue (a smallest element)
// executes in constant time
//
// This function assumes that the priority queue is
// not empty and the caller is resposible for the check.
// You can use an expression such as
// "isEmpty() ? undefined : peek()"
// if you expect to be calling peek on an empty priority queue.
//
FastPriorityQueue.prototype.peek = function () {
return this.array[0];
};
// remove the element on top of the heap (a smallest element)
// runs in logarithmic time
//
//
// This function assumes that the priority queue is
// not empty, and the caller is responsible for the check.
// You can use an expression such as
// "isEmpty() ? undefined : poll()"
// if you expect to be calling poll on an empty priority queue.
//
// For long-running and large priority queues, or priority queues
// storing large objects, you may want to call the trim function
// at strategic times to recover allocated memory.
FastPriorityQueue.prototype.poll = function () {
var ans = this.array[0];
if (this.size > 1) {
this.array[0] = this.array[--this.size];
this._percolateDown(0 | 0);
} else {
this.size -= 1;
}
return ans;
};
// recover unused memory (for long-running priority queues)
FastPriorityQueue.prototype.trim = function () {
this.array = this.array.slice(0, this.size);
};
// Check whether the heap is empty
FastPriorityQueue.prototype.isEmpty = function () {
return this.size === 0;
};
// just for illustration purposes
var main = function () {
// main code
var x = new FastPriorityQueue(function (a, b) {
return a < b;
});
x.add(1);
x.add(0);
x.add(5);
x.add(4);
x.add(3);
while (!x.isEmpty()) {
console.log(x.poll());
}
};
\ No newline at end of file
<?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>
</tuple>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>fast_priority_queue.js</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>fast_priority_queue_js</string> </value>
</item>
<item>
<key> <string>language</string> </key>
<value>
<none/>
</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>Fast Priority Queue JS</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value>
<none/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -7,8 +7,24 @@ ...@@ -7,8 +7,24 @@
<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="handlebars.js"></script>
<script src="gadget_global.js"></script> <script src="gadget_global.js"></script>
<script src="fast_priority_queue.js"></script>
<script src="gadget_erp5_page_chat_box.js"></script> <script src="gadget_erp5_page_chat_box.js"></script>
<script class="chat-list-template" type="text/x-handlebars-template">
{{#each list}}
<li style="color:{{this.color}};">
{{{this.html}}}
</li>
{{/each}}
</script>
<script class="contact-list-template" type="text/x-handlebars-template">
{{#each list}}
<li id="chat-contact-{{this.id}}" class="chat-contact {{this.class_list}}">
{{this.name}}
</li>
{{/each}}
</script>
</head> </head>
<body> <body>
<div class="chat-box"> <div class="chat-box">
...@@ -27,7 +43,8 @@ ...@@ -27,7 +43,8 @@
</div> </div>
</div> </div>
<div class="chat-right-panel"> <div class="chat-right-panel">
<h3 class="chat-title center"></h3> <h3 class="chat-title center">
</h3>
<div class="chat-max-height-wrapper"> <div class="chat-max-height-wrapper">
<div class="chat-right-panel-chat"> <div class="chat-right-panel-chat">
<ul class="chat-list"> <ul class="chat-list">
......
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */ /*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global window, document, RSVP, rJS, promiseEventListener */ /*global window, document, RSVP, rJS, Handlebars,
(function (window, document, RSVP, rJS, promiseEventListener) { FastPriorityQueue, promiseEventListener */
(function (window, document, RSVP, rJS, Handlebars,
FastPriorityQueue, promiseEventListener) {
"use strict"; "use strict";
/* Settings required: /* Settings required:
...@@ -12,6 +14,10 @@ ...@@ -12,6 +14,10 @@
* - default_dav_url, default: "" * - default_dav_url, default: ""
*/ */
var chat_list_template,
contact_list_template;
/* Check if a string ends with another string. /* Check if a string ends with another string.
* Parameters: * Parameters:
* - string: the string that may or may not end with the suffix string * - string: the string that may or may not end with the suffix string
...@@ -40,7 +46,7 @@ ...@@ -40,7 +46,7 @@
/* Reset a text input. /* Reset a text input.
* Parameters: * Parameters:
* - element: the text input element to reset * - element: the text input element to reset
* Effects: set the value of the text input to the empty string * Effects: set the value of the text input to the empty string
* Returns: the previous value of the text input before resetting it * Returns: the previous value of the text input before resetting it
*/ */
...@@ -51,27 +57,38 @@ ...@@ -51,27 +57,38 @@
} }
/* Get the contact element given by a room. /* Get the creation epoch time of a message.
* Parameters: * Parameters:
* - room: the room for which to find the element in the contact list * - message: the message whose creation time is desired
* Effects: nothing * Effects: nothing
* Returns: the contact element corresponding to the given room * Returns: the creation time of the message, in milliseconds since 1970
*/ */
function getContactFromRoom(gadget, room) { function getTime(message) {
return gadget.element.querySelector("#chat-contact-" + nameToId(room)); return new Date(message.time).getTime();
} }
/* Get the creation epoch time of a message. /* Create a new JSON message.
* Parameters: * Parameters:
* - message: the message whose creation time is desired * - name: the name of the sender of the message
* - room: the room from where the message is sent
* - time: the epoch time at which the message is sent
* - content: the content of the message
* - color: the colour of the message in the chat
* Effects: nothing * Effects: nothing
* Returns: the creation time of the message, in milliseconds since 1970. * Returns: a message object corresponding to the parameters
*/ */
function getTime(message) { function createMessage(param_dict) {
return new Date(message.time).getTime(); return {
type: param_dict.type || "message",
name: param_dict.name,
room: param_dict.room,
time: param_dict.time || new Date(),
content: param_dict.content || "",
color: param_dict.color || "black"
};
} }
...@@ -79,39 +96,37 @@ ...@@ -79,39 +96,37 @@
* Parameters: * Parameters:
* - message: the JavaScript object to display in HTML * - message: the JavaScript object to display in HTML
* Effects: nothing * Effects: nothing
* Returns: a HTML representation of the given message * Returns: a properly escaped HTML representation of the given message
*/ */
function messageToChat(message) { function messageToChat(message) {
var i, j, matches, image, link, var i, j, matches, link_string, dot_count, is_image, absolute_url,
link_string, dot_count, is_image, absolute_url, escaped_content = Handlebars.Utils.escapeExpression(message.content),
chat = document.createElement("li"), chat = {color: message.color, html: ""},
image_extensions = [".jpg", ".png", ".gif", ".bmp", ".tif", ".svg"], image_extensions = [".jpg", ".png", ".gif", ".bmp", ".tif", ".svg"],
url_regex = url_regex =
/((?:https?:\/\/)?(?:[\-a-zA-Z0-9_~\/@]+\.)+[\-a-zA-Z0-9_~#?&=\/]+)/g; /((?:https?:\/\/)?(?:[\-a-zA-Z0-9_~\/@]+\.)+[\-a-zA-Z0-9_~#?&=\/]+)/g;
if (message.type === "notification") { if (message.type === "notification") {
chat.appendChild(document.createTextNode(message.content)); chat.html = escaped_content;
chat.style.color = message.color;
} else if (message.type === "message") { } else if (message.type === "message") {
// Put message in the format "[3/24/2017, 11:30:52 AM] user: message" // Put message in the format "[3/24/2017, 11:30:52 AM] user: message"
chat.appendChild(document.createTextNode("[" chat.html = "[" + new Date(message.time).toLocaleString()
+ new Date(message.time).toLocaleString() + "] " + message.name + ": " + "] " + message.name + ": ";
));
// Loop through all potential URLs in the message // Loop through all potential URLs in the message
// matches[i] is the non-link content, matches[i + 1] is the actual link // matches[i] is the non-link content, matches[i + 1] is the actual link
matches = message.content.split(url_regex); matches = escaped_content.split(url_regex);
for (i = 0; i < matches.length - 1; i += 2) { for (i = 0; i < matches.length - 1; i += 2) {
chat.appendChild(document.createTextNode(matches[i])); chat.html += matches[i];
// If 2d + 1 >= L, then the potential URL has only single letters, // If 2d + 1 >= L, then the potential URL has only single letters,
// so ignore it to eliminate acronyms like e.g., i.e., U.S.A., etc. // so ignore it to eliminate acronyms like e.g., i.e., U.S.A., etc.
link_string = matches[i + 1]; link_string = matches[i + 1];
dot_count = link_string.match(/\./g).length; dot_count = link_string.match(/\./g).length;
if (2 * dot_count + 1 >= link_string.length) { if (2 * dot_count + 1 >= link_string.length) {
chat.appendChild(document.createTextNode(link_string)); chat.html += link_string;
} else { } else {
is_image = false; is_image = false;
...@@ -125,9 +140,7 @@ ...@@ -125,9 +140,7 @@
// Create an image if the URL ends with one of the image extensions // Create an image if the URL ends with one of the image extensions
for (j = 0; j < image_extensions.length; j += 1) { for (j = 0; j < image_extensions.length; j += 1) {
if (stringEndsWith(link_string, image_extensions[j])) { if (stringEndsWith(link_string, image_extensions[j])) {
image = document.createElement("img"); chat.html += ("<img src='" + absolute_url + "'>");
image.src = absolute_url;
chat.appendChild(image);
is_image = true; is_image = true;
break; break;
} }
...@@ -135,17 +148,14 @@ ...@@ -135,17 +148,14 @@
// Otherwise, create a link // Otherwise, create a link
if (!is_image) { if (!is_image) {
link = document.createElement("a"); chat.html += "<a href='" + absolute_url + "'>";
link.href = absolute_url; chat.html += link_string + "</a>";
link.innerHTML = link_string;
chat.appendChild(link);
} }
} }
} }
// Add the last non-link content and return the resulting chat element // Add the last non-link content
chat.appendChild(document.createTextNode(matches[matches.length - 1])); chat.html += matches[matches.length - 1];
chat.style.color = message.color;
} }
return chat; return chat;
} }
...@@ -155,22 +165,34 @@ ...@@ -155,22 +165,34 @@
.setState({ .setState({
name: null, name: null,
// the currently active room, to show in the right panel
room: null, room: null,
// keep track of references to elements in the DOM
// initialize to arbitrary values to avoid errors before loading
chat_title_element: {},
chat_box_element: {style: {}},
room_gadget_element: {style: {}},
// true if the room is a chat box, false if the room is a contact panel // true if the room is a chat box, false if the room is a contact panel
is_chat: false, is_chat: false,
// a set of the names of all active rooms with number of messages in each // a dict of room IDs to the ir names, i.e. {foo_bar_com: "foo@bar.com"}
// e.g. {small_room: 3, large_room: 1337} id_to_name: {},
room_set: {},
// a dict of room names to whether each has unread messages
// i.e. {read_room: false, unread_room: true}
unread_room_dict: {},
// a set of the names of all rooms that have unread messages with boolean // a dict of room names to the list of messages in each,
// e.g. {read_room: false, unread_room: true} // i.e. {quiet_room: [message1, message2], busy_room: [message3, ...]}
unread_room_set: {}, message_list_dict: {},
// a set of XXX // a dict of room names to the number of messages in jIO storage,
// i.e. {quiet_room: 3, busy_room: 429}
message_count_dict: {},
// a dict of room names to their delayRefresh promise queues
// i.e. {room: new RSVP.Queue().push(function () { return ... })}
delay_refresh_dict: {}, delay_refresh_dict: {},
// true to use alert_icon_url, false to use default_icon_url // true to use alert_icon_url, false to use default_icon_url
...@@ -178,10 +200,10 @@ ...@@ -178,10 +200,10 @@
alert_icon_url: "https://pricespy.co.nz/favicon.ico", alert_icon_url: "https://pricespy.co.nz/favicon.ico",
default_icon_url: "https://softinst75770.host.vifib.net/" default_icon_url: "https://softinst75770.host.vifib.net/"
+ "erp5/web_site_module/web_chat/favicon.ico", + "erp5/web_site_module/web_chat/favicon.ico",
// toggle refresh_chat to force call delayRefresh // toggle refresh_chat to force call delayRefresh
refresh_chat: false, refresh_chat: false,
// toggle update to force call onStateChange // toggle update to force call onStateChange
update: false update: false
}) })
...@@ -212,31 +234,28 @@ ...@@ -212,31 +234,28 @@
*/ */
.onStateChange(function (modification_dict) { .onStateChange(function (modification_dict) {
var gadget = this, var i, contact_name, contact_class_list, chat_list, chat_list_element,
i, gadget = this,
room_list = gadget.element.querySelector(".chat-right-panel").children, contact_list = Object.keys(gadget.state.message_list_dict),
contact_list = gadget.element.querySelector(".contact-list").children, message_list = gadget.state.message_list_dict[gadget.state.room],
favicon_element = document.querySelector("link[rel*='icon']") favicon_element = document.querySelector("link[rel*='icon']")
|| document.createElement("link"); || document.createElement("link");
// hide all room gadget elements // add classes to contact elements
for (i = 0; i < room_list.length; i += 1) {
if (room_list[i].getAttribute("data-gadget-url")) {
room_list[i].style.display = "none";
}
}
// remove all styles from contact elements
for (i = 0; i < contact_list.length; i += 1) { for (i = 0; i < contact_list.length; i += 1) {
if (contact_list[i].classList.contains("current")) { contact_name = contact_list[i];
contact_list[i].classList.remove("current"); contact_class_list = [];
contact_list[i].classList.remove("notify"); if (gadget.state.unread_room_dict[contact_name]) {
contact_class_list.push("notify");
} }
if (contact_name === gadget.state.room) {
// add styles to contacts with unread messages contact_class_list.push("current");
if (gadget.state.unread_room_set[contact_list[i].id]) {
contact_list[i].classList.add("notify");
} }
contact_list[i] = {
name: contact_name,
id: nameToId(contact_name),
class_list: contact_class_list.join(" ")
};
} }
// set favicon depending on whether there are new unread messages // set favicon depending on whether there are new unread messages
...@@ -249,39 +268,46 @@ ...@@ -249,39 +268,46 @@
favicon_element.rel = "shortcut icon"; favicon_element.rel = "shortcut icon";
document.head.appendChild(favicon_element); document.head.appendChild(favicon_element);
// add style to the current contact // if a chat room is active, show chat panel
if (modification_dict.hasOwnProperty("room") || if (modification_dict.hasOwnProperty("room") ||
modification_dict.hasOwnProperty("is_chat")) { modification_dict.hasOwnProperty("is_chat")) {
getContactFromRoom(gadget, gadget.state.room).classList.add("current"); gadget.state.room_gadget_element.style.display = "none";
// if a chat room is active, show chat panel
if (gadget.state.is_chat) { if (gadget.state.is_chat) {
gadget.element.querySelector(".chat-right-panel-chat") gadget.state.chat_title_element.innerHTML =
.style.display = "flex"; "Room: " + gadget.state.room;
gadget.element.querySelector(".chat-title") gadget.state.chat_box_element.style.display = "flex";
.textContent = "Room: " + gadget.state.room;
} else { } else {
// otherwise, hide chat panel and show contact panel // otherwise, hide chat panel and show contact panel
gadget.element.querySelector(".chat-right-panel-chat") gadget.state.chat_title_element.innerHTML =
.style.display = "none"; "Contact: " + gadget.state.room;
gadget.element.querySelector("#room-gadget-" + gadget.state.chat_box_element.style.display = "none";
nameToId(gadget.state.room)).style.display = "block"; gadget.state.room_gadget_element = gadget.element
gadget.element.querySelector(".chat-title") .querySelector("#room-gadget-" + nameToId(gadget.state.room));
.textContent = "Contact: " + gadget.state.room; gadget.state.room_gadget_element.style.display = "block";
}
}
// render the contact list and chat list using Handlebars
gadget.element.querySelector(".contact-list").innerHTML =
contact_list_template({list: contact_list});
if (gadget.state.is_chat) {
chat_list = [];
for (i = 0; i < message_list.length; i += 1) {
chat_list.push(messageToChat(message_list[i]));
} }
chat_list_element = gadget.element.querySelector(".chat-list");
chat_list_element.innerHTML = chat_list_template({list: chat_list});
chat_list_element.scrollTop = chat_list_element.scrollHeight;
} }
// set update to false so that setting update to true calls onStateChange // set update to false so that setting update to true calls onStateChange
gadget.state.update = false; gadget.state.update = false;
// refresh the current room // refresh the current room
if (gadget.state.refresh_chat) { if (gadget.state.refresh_chat) {
gadget.state.refresh_chat = false; gadget.state.refresh_chat = false;
if (gadget.state.delay_refresh_dict[gadget.state.room]) { gadget.delayRefresh(gadget.state.room, 5000);
gadget.state.delay_refresh_dict[gadget.state.room].cancel();
}
return gadget.delayRefresh(gadget.state.room, 0);
} }
}) })
...@@ -304,6 +330,18 @@ ...@@ -304,6 +330,18 @@
"jio_configurator", "jio_configurator",
new RSVP.Queue() new RSVP.Queue()
.push(function () { .push(function () {
chat_list_template = Handlebars.compile(
Object.getPrototypeOf(gadget).constructor.__template_element
.querySelector(".chat-list-template").innerHTML
);
contact_list_template = Handlebars.compile(
Object.getPrototypeOf(gadget).constructor.__template_element
.querySelector(".contact-list-template").innerHTML
);
gadget.state.chat_title_element = gadget.element
.querySelector(".chat-title");
gadget.state.chat_box_element = gadget.element
.querySelector(".chat-right-panel-chat");
return gadget.updateHeader({ return gadget.updateHeader({
page_title: "OfficeJS Chat" page_title: "OfficeJS Chat"
}); });
...@@ -333,7 +371,7 @@ ...@@ -333,7 +371,7 @@
.push(function () { .push(function () {
gadget.element.querySelector(".send-form input[type='text']") gadget.element.querySelector(".send-form input[type='text']")
.onfocus = function () { .onfocus = function () {
gadget.state.unread_room_set[gadget.state.room] = false; gadget.state.unread_room_dict[gadget.state.room] = false;
return gadget.changeState({update: true}); return gadget.changeState({update: true});
}; };
return gadget.createContact(user_email); return gadget.createContact(user_email);
...@@ -349,58 +387,20 @@ ...@@ -349,58 +387,20 @@
* Parameters: * Parameters:
* - room: the name of the contact * - room: the name of the contact
* Effects: * Effects:
* - append a new contact to the contact list * - if the name is not blank and not a duplicate, then:
* - create a new contact to be rendered in the contact list
* - create a new room gadget * - create a new room gadget
* - show the chat box
*/ */
.declareMethod("createContact", function (room) { .declareMethod("createContact", function (room) {
var gadget = this; var gadget = this;
return gadget.appendContact(room)
.push(function () {
return gadget.createRoom(room);
})
.push(function () {
return gadget.changeState({room: room, is_chat: false});
});
})
/* Append a new contact element to the contact list.
* Parameters:
* - room: the name of the contact
* Effects:
* - if the name is not blank and not a duplicate, then:
* - create and append a new contact element to the contact list
* - set its ID to a querySelector-safe translation of its name
* - highlight it and add an event listener to highlight it on click
* and either change to the room or the contact panel
*/
.declareMethod("appendContact", function (room) {
var gadget = this,
contact_element;
if (!room.trim()) { if (!room.trim()) {
throw "An invisible name is not allowed! You couldn't click on it!"; throw "An invisible name is not allowed! You couldn't click on it!";
} }
if (getContactFromRoom(gadget, room)) { if (gadget.state.message_list_dict.hasOwnProperty(room)) {
throw "A contact with the same name already exists!"; throw "A contact with the same name already exists!";
} }
return gadget.createRoom(room);
contact_element = document.createElement("li");
contact_element.appendChild(document.createTextNode(room));
contact_element.setAttribute("id", "chat-contact-" + nameToId(room));
gadget.element.querySelector(".contact-list")
.appendChild(contact_element);
contact_element.addEventListener("click", function () {
gadget.state.unread_room_set[room] = false;
if (gadget.state.room_set[room] !== undefined) {
return gadget.changeRoom(room);
}
return gadget.changeState({room: room, is_chat: false, update: true});
}, false);
}) })
...@@ -441,6 +441,12 @@ ...@@ -441,6 +441,12 @@
}) })
.push(function () { .push(function () {
return room_gadget.render(); return room_gadget.render();
})
.push(function () {
gadget.state.message_list_dict[room] = [];
gadget.state.message_count_dict[room] = 0;
gadget.state.id_to_name["chat-contact-" + nameToId(room)] = room;
return gadget.changeState({room: room, is_chat: false});
}); });
}) })
...@@ -449,7 +455,6 @@ ...@@ -449,7 +455,6 @@
* Parameters: * Parameters:
* - room: the name of the room to change to * - room: the name of the room to change to
* Effects: * Effects:
* - changeState: room, room_set[room]
* - hide the room gadget contact panel * - hide the room gadget contact panel
* - show the chat box * - show the chat box
* - overwrite the chat box with chats from the jIO storage * - overwrite the chat box with chats from the jIO storage
...@@ -459,9 +464,10 @@ ...@@ -459,9 +464,10 @@
var gadget = this; var gadget = this;
return gadget.changeState({room: room, is_chat: true}) return gadget.changeState({room: room, is_chat: true})
.push(function () { .push(function () {
if (gadget.state.room_set[room] === undefined) { if (gadget.state.message_list_dict[room].length === 0) {
gadget.state.room_set[room] = 0;
return gadget.deployMessage({ return gadget.deployMessage({
name: gadget.state.name,
room: gadget.state.room,
content: gadget.state.name + " has joined.", content: gadget.state.name + " has joined.",
color: "orange" color: "orange"
}); });
...@@ -481,18 +487,12 @@ ...@@ -481,18 +487,12 @@
.declareMethod("deployMessage", function (param_dict) { .declareMethod("deployMessage", function (param_dict) {
var gadget = this, var gadget = this,
message; message = createMessage(param_dict);
return gadget.createMessage(param_dict) gadget.state.message_list_dict[param_dict.room].push(message);
.push(function (result) { return gadget.storeArchive(message)
message = result;
return gadget.appendMessage(message);
})
.push(function () {
return gadget.storeArchive(message);
})
.push(function () { .push(function () {
return gadget.changeState({refresh_chat: true}); return gadget.changeState({refresh_chat: true});
}) });
}) })
...@@ -505,53 +505,12 @@ ...@@ -505,53 +505,12 @@
*/ */
.declareMethod("deployNotification", function (param_dict) { .declareMethod("deployNotification", function (param_dict) {
var gadget = this;
param_dict.type = "notification";
return gadget.createMessage(param_dict)
.push(function (result) {
return gadget.appendMessage(result);
});
})
/* Create a new message.
* Parameters:
* - name: the name of the sender of the message
* - room: the room from where the message is sent
* - time: the epoch time at which the message is sent
* - content: the content of the message
* - color: the colour of the message in the chat
* Effects: nothing
* Returns: a message object corresponding to the parameters
*/
.declareMethod("createMessage", function (param_dict) {
var gadget = this;
return {
type: param_dict.type || "message",
name: param_dict.name || gadget.state.name,
room: param_dict.room || gadget.state.room,
time: param_dict.time || new Date(),
content: param_dict.content || "",
color: param_dict.color || "black"
};
})
/* Append a message to the chat.
* Parameters:
* - message: the message object to append to the chat
* Effects:
* - create a HTML chat element from the message
* - append the chat element to the chat list
* - scroll the chat list down to show the chat element
*/
.declareMethod("appendMessage", function (message) {
var gadget = this, var gadget = this,
container = gadget.element.querySelector(".chat-list"); message;
container.appendChild(messageToChat(message)); param_dict.type = "notification";
container.scrollTop = container.scrollHeight; message = createMessage(param_dict);
gadget.state.message_list_dict[param_dict.room].push(message);
return gadget.changeState({refresh_chat: true});
}) })
...@@ -590,30 +549,32 @@ ...@@ -590,30 +549,32 @@
.declareMethod("delayRefresh", function (room, delay) { .declareMethod("delayRefresh", function (room, delay) {
var gadget = this; var gadget = this;
console.log(room, delay);
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return RSVP.all([ return RSVP.all([
RSVP.delay(delay), RSVP.delay(delay),
// gadget.refreshChat(room) gadget.refreshChat(room)
]); ]);
}) })
.push(function () { .push(function () {
if (gadget.state.delay_refresh_dict.hasOwnProperty(room)) {
gadget.state.delay_refresh_dict[room].cancel();
}
gadget.state.delay_refresh_dict[room] = new RSVP.Queue() gadget.state.delay_refresh_dict[room] = new RSVP.Queue()
.push(function () { .push(function () {
return gadget.delayRefresh(room, delay + 500); return gadget.delayRefresh(room, delay + 10000);
}); });
}) });
}) })
/* Overwrite the chat box with chats from the jIO storage. /* Refresh the message list with chats from the jIO storage.
* Parameters: * Parameters:
* - room: the room to refresh * - room: the room to refresh
* Effects: * Effects:
* - get a sorted list of all chats in the current room from jIO storage * - get a sorted list of all chats in the current room from jIO storage
* - remove the entire chat box and append all chats from jIO storage * - merge the list with the current messages in a priority queue
* - scroll the chat list down to show the chat element * - update the current messages with the sorted message queue
*/ */
.declareMethod("refreshChat", function (room) { .declareMethod("refreshChat", function (room) {
...@@ -633,33 +594,50 @@ ...@@ -633,33 +594,50 @@
}]); }]);
}) })
.push(function (result_list) { .push(function (result_list) {
var i, message, message_list = [], var i, message, new_list = [],
container = gadget.element.querySelector(".chat-list"); old_list = gadget.state.message_list_dict[room],
message_queue = new FastPriorityQueue(function (lhs, rhs) {
return getTime(lhs) < getTime(rhs);
});
if (result_list.data.total_rows >
gadget.state.message_count_dict[room]) {
gadget.state.message_count_dict[room] = result_list.data.total_rows;
gadget.state.unread_room_dict[room] = true;
if (result_list.data.total_rows > gadget.state.room_set[room]) {
gadget.state.room_set[room] = result_list.data.total_rows;
gadget.state.unread_room_set[room] = true;
for (i = 0; i < result_list.data.total_rows; i += 1) { for (i = 0; i < result_list.data.total_rows; i += 1) {
try { try {
message = JSON.parse(result_list.data.rows[i].value.content); message = JSON.parse(result_list.data.rows[i].value.content);
if (message && typeof message === "object") { if (message && typeof message === "object") {
message_list.push(message); message_queue.add(message);
} }
} catch (ignore) {} } catch (ignore) {}
} }
if (room === gadget.state.room) { for (i = 0; i < old_list.length; i += 1) {
while (container.firstChild) { if (!message_queue.isEmpty()) {
container.removeChild(container.firstChild); message = message_queue.poll();
} while (getTime(old_list[i]) < getTime(message)
for (i = 0; i < message_list.length; i += 1) { && i < old_list.length) {
container.appendChild(messageToChat(message_list[i])); new_list.push(old_list[i]);
i += 1;
}
while (getTime(old_list[i]) === getTime(message)) {
i += 1;
}
new_list.push(message);
} else {
new_list.push(old_list[i]);
} }
container.scrollTop = container.scrollHeight;
} }
while (!message_queue.isEmpty()) {
new_list.push(message_queue.poll());
}
gadget.state.message_list_dict[room] = new_list;
return gadget.changeState({refresh_chat: true}); return gadget.changeState({refresh_chat: true});
} }
}) });
}) })
...@@ -675,7 +653,6 @@ ...@@ -675,7 +653,6 @@
split = chat.slice(1).split(" "), split = chat.slice(1).split(" "),
command = split.shift(), command = split.shift(),
argument = split.join(" "), argument = split.join(" "),
room_element = getContactFromRoom(gadget, gadget.state.room),
promise_list = [], promise_list = [],
help_list = [ help_list = [
"Available commands:", "Available commands:",
...@@ -688,10 +665,12 @@ ...@@ -688,10 +665,12 @@
// change to a room that has already been joined // change to a room that has already been joined
case "join": case "join":
if (gadget.state.room_set[argument] !== undefined) { if (gadget.state.message_list_dict[argument] > 0) {
return gadget.changeRoom(argument); return gadget.changeRoom(argument);
} }
return gadget.deployNotification({ return gadget.deployNotification({
name: gadget.state.name,
room: gadget.state.room,
content: "You must first be connected to room '" content: "You must first be connected to room '"
+ argument + "' via a shared jIO storage to join it!", + argument + "' via a shared jIO storage to join it!",
color: "red" color: "red"
...@@ -703,13 +682,16 @@ ...@@ -703,13 +682,16 @@
.push(function () { .push(function () {
if (gadget.state.room === gadget.state.name) { if (gadget.state.room === gadget.state.name) {
return gadget.deployNotification({ return gadget.deployNotification({
name: gadget.state.name,
room: gadget.state.room,
content: "You cannot leave your own room!", content: "You cannot leave your own room!",
color: "red" color: "red"
}); });
} }
gadget.state.room_set[gadget.state.room] = undefined; delete gadget.state.message_list_dict[gadget.state.room];
room_element.parentNode.removeChild(room_element);
return gadget.deployMessage({ return gadget.deployMessage({
name: gadget.state.name,
room: gadget.state.room,
content: gadget.state.name + " has quit.", content: gadget.state.name + " has quit.",
color: "orange" color: "orange"
}) })
...@@ -726,6 +708,8 @@ ...@@ -726,6 +708,8 @@
case "help": case "help":
for (i = 0; i < help_list.length; i += 1) { for (i = 0; i < help_list.length; i += 1) {
promise_list.push(gadget.deployNotification({ promise_list.push(gadget.deployNotification({
name: gadget.state.name,
room: gadget.state.room,
content: help_list[i], content: help_list[i],
color: "green" color: "green"
})); }));
...@@ -734,6 +718,8 @@ ...@@ -734,6 +718,8 @@
default: default:
return gadget.deployNotification({ return gadget.deployNotification({
name: gadget.state.name,
room: gadget.state.room,
content: "'" + argument + "' is not a valid command.", content: "'" + argument + "' is not a valid command.",
color: "red" color: "red"
}); });
...@@ -741,6 +727,22 @@ ...@@ -741,6 +727,22 @@
}) })
// Call changeRoom or changeState when a chat contact is clicked.
.onEvent("click", function (event) {
var gadget = this,
room;
if (event.target.classList.contains("chat-contact")) {
room = gadget.state.id_to_name[event.target.id];
gadget.state.unread_room_dict[room] = false;
if (gadget.state.message_list_dict[room] > 0) {
return gadget.changeRoom(room);
}
return gadget.changeState({room: room, is_chat: false, update: true});
}
}, false, false)
// Call the appropriate function based on the form submitted. // Call the appropriate function based on the form submitted.
.onEvent("submit", function (event) { .onEvent("submit", function (event) {
...@@ -748,8 +750,7 @@ ...@@ -748,8 +750,7 @@
content; content;
switch (event.target.className) { switch (event.target.className) {
case "edit-form": case "edit-form":
content = resetInputValue(event.target.elements.content); return gadget.changeState({is_chat: false});
return gadget.changeState({room: content});
case "join-form": case "join-form":
content = resetInputValue(event.target.elements.content); content = resetInputValue(event.target.elements.content);
return gadget.createContact(content); return gadget.createContact(content);
...@@ -758,7 +759,11 @@ ...@@ -758,7 +759,11 @@
if (content.indexOf("/") === 0) { if (content.indexOf("/") === 0) {
return gadget.parseCommand(content); return gadget.parseCommand(content);
} }
return gadget.deployMessage({content: content}); return gadget.deployMessage({
name: gadget.state.name,
room: gadget.state.room,
content: content
});
} }
}) })
...@@ -776,10 +781,11 @@ ...@@ -776,10 +781,11 @@
}) })
.push(function () { .push(function () {
var promise_list = [], room; var promise_list = [], room;
for (room in gadget.state.room_set) { for (room in gadget.state.message_list_dict) {
if (gadget.state.room_set.hasOwnProperty(room) if (gadget.state.message_list_dict.hasOwnProperty(room)
&& gadget.state.room_set[room] !== undefined) { && gadget.state.message_list_dict[room] > 0) {
promise_list.push(gadget.deployMessage({ promise_list.push(gadget.deployMessage({
name: gadget.state.name,
content: gadget.state.name + " has quit.", content: gadget.state.name + " has quit.",
room: room, room: room,
color: "orange" color: "orange"
...@@ -790,4 +796,5 @@ ...@@ -790,4 +796,5 @@
}); });
}); });
}(window, document, RSVP, rJS, promiseEventListener)); }(window, document, RSVP, rJS, Handlebars,
\ No newline at end of file FastPriorityQueue, promiseEventListener));
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<?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>
</tuple>
</value>
</item>
<item>
<key> <string>content_md5</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>handlebars.js</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>handlebars_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>Handlebars 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/erp5_page_launcher* web_page_module/erp5_page_launcher*
web_page_module/fast_priority_queue_js
web_page_module/gadget_erp5_chat_panel_* web_page_module/gadget_erp5_chat_panel_*
web_page_module/gadget_erp5_chat_webrtc_* web_page_module/gadget_erp5_chat_webrtc_*
web_page_module/gadget_erp5_nojquery_css web_page_module/gadget_erp5_nojquery_css
...@@ -8,4 +9,5 @@ web_page_module/gadget_erp5_page_contact_* ...@@ -8,4 +9,5 @@ web_page_module/gadget_erp5_page_contact_*
web_page_module/gadget_erp5_page_field_listbox_widget_* web_page_module/gadget_erp5_page_field_listbox_widget_*
web_page_module/gadget_erp5_page_jio_*_configurator_* web_page_module/gadget_erp5_page_jio_*_configurator_*
web_page_module/gadget_erp5_page_jio_person_view_* web_page_module/gadget_erp5_page_jio_person_view_*
web_page_module/gadget_erp5_page_sync_* web_page_module/gadget_erp5_page_sync_*
\ No newline at end of file web_page_module/handlebars_js
\ 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