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 @@
<script src="rsvp.js"></script>
<script src="renderjs.js"></script>
<script src="jiodev.js"></script>
<script src="handlebars.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 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>
<body>
<div class="chat-box">
......@@ -27,7 +43,8 @@
</div>
</div>
<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-right-panel-chat">
<ul class="chat-list">
......
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global window, document, RSVP, rJS, promiseEventListener */
(function (window, document, RSVP, rJS, promiseEventListener) {
/*global window, document, RSVP, rJS, Handlebars,
FastPriorityQueue, promiseEventListener */
(function (window, document, RSVP, rJS, Handlebars,
FastPriorityQueue, promiseEventListener) {
"use strict";
/* Settings required:
......@@ -12,6 +14,10 @@
* - default_dav_url, default: ""
*/
var chat_list_template,
contact_list_template;
/* Check if a string ends with another string.
* Parameters:
* - string: the string that may or may not end with the suffix string
......@@ -40,7 +46,7 @@
/* Reset a text input.
* Parameters:
* - 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
*/
......@@ -51,27 +57,38 @@
}
/* Get the contact element given by a room.
/* Get the creation epoch time of a message.
* Parameters:
* - room: the room for which to find the element in the contact list
* - message: the message whose creation time is desired
* 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) {
return gadget.element.querySelector("#chat-contact-" + nameToId(room));
function getTime(message) {
return new Date(message.time).getTime();
}
/* Get the creation epoch time of a message.
/* Create a new JSON message.
* 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
* Returns: the creation time of the message, in milliseconds since 1970.
* Returns: a message object corresponding to the parameters
*/
function getTime(message) {
return new Date(message.time).getTime();
function createMessage(param_dict) {
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 @@
* Parameters:
* - message: the JavaScript object to display in HTML
* Effects: nothing
* Returns: a HTML representation of the given message
* Returns: a properly escaped HTML representation of the given message
*/
function messageToChat(message) {
var i, j, matches, image, link,
link_string, dot_count, is_image, absolute_url,
chat = document.createElement("li"),
var i, j, matches, link_string, dot_count, is_image, absolute_url,
escaped_content = Handlebars.Utils.escapeExpression(message.content),
chat = {color: message.color, html: ""},
image_extensions = [".jpg", ".png", ".gif", ".bmp", ".tif", ".svg"],
url_regex =
/((?:https?:\/\/)?(?:[\-a-zA-Z0-9_~\/@]+\.)+[\-a-zA-Z0-9_~#?&=\/]+)/g;
if (message.type === "notification") {
chat.appendChild(document.createTextNode(message.content));
chat.style.color = message.color;
chat.html = escaped_content;
} else if (message.type === "message") {
// Put message in the format "[3/24/2017, 11:30:52 AM] user: message"
chat.appendChild(document.createTextNode("["
+ new Date(message.time).toLocaleString() + "] " + message.name + ": "
));
chat.html = "[" + new Date(message.time).toLocaleString()
+ "] " + message.name + ": ";
// Loop through all potential URLs in the message
// matches[i] is the non-link content, matches[i + 1] is the actual link
matches = message.content.split(url_regex);
matches = escaped_content.split(url_regex);
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,
// so ignore it to eliminate acronyms like e.g., i.e., U.S.A., etc.
link_string = matches[i + 1];
dot_count = link_string.match(/\./g).length;
if (2 * dot_count + 1 >= link_string.length) {
chat.appendChild(document.createTextNode(link_string));
chat.html += link_string;
} else {
is_image = false;
......@@ -125,9 +140,7 @@
// Create an image if the URL ends with one of the image extensions
for (j = 0; j < image_extensions.length; j += 1) {
if (stringEndsWith(link_string, image_extensions[j])) {
image = document.createElement("img");
image.src = absolute_url;
chat.appendChild(image);
chat.html += ("<img src='" + absolute_url + "'>");
is_image = true;
break;
}
......@@ -135,17 +148,14 @@
// Otherwise, create a link
if (!is_image) {
link = document.createElement("a");
link.href = absolute_url;
link.innerHTML = link_string;
chat.appendChild(link);
chat.html += "<a href='" + absolute_url + "'>";
chat.html += link_string + "</a>";
}
}
}
// Add the last non-link content and return the resulting chat element
chat.appendChild(document.createTextNode(matches[matches.length - 1]));
chat.style.color = message.color;
// Add the last non-link content
chat.html += matches[matches.length - 1];
}
return chat;
}
......@@ -155,22 +165,34 @@
.setState({
name: null,
// the currently active room, to show in the right panel
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
is_chat: false,
// a set of the names of all active rooms with number of messages in each
// e.g. {small_room: 3, large_room: 1337}
room_set: {},
// a dict of room IDs to the ir names, i.e. {foo_bar_com: "foo@bar.com"}
id_to_name: {},
// 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
// e.g. {read_room: false, unread_room: true}
unread_room_set: {},
// a dict of room names to the list of messages in each,
// i.e. {quiet_room: [message1, message2], busy_room: [message3, ...]}
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: {},
// true to use alert_icon_url, false to use default_icon_url
......@@ -178,10 +200,10 @@
alert_icon_url: "https://pricespy.co.nz/favicon.ico",
default_icon_url: "https://softinst75770.host.vifib.net/"
+ "erp5/web_site_module/web_chat/favicon.ico",
// toggle refresh_chat to force call delayRefresh
refresh_chat: false,
// toggle update to force call onStateChange
update: false
})
......@@ -212,31 +234,28 @@
*/
.onStateChange(function (modification_dict) {
var gadget = this,
i,
room_list = gadget.element.querySelector(".chat-right-panel").children,
contact_list = gadget.element.querySelector(".contact-list").children,
var i, contact_name, contact_class_list, chat_list, chat_list_element,
gadget = this,
contact_list = Object.keys(gadget.state.message_list_dict),
message_list = gadget.state.message_list_dict[gadget.state.room],
favicon_element = document.querySelector("link[rel*='icon']")
|| document.createElement("link");
|| document.createElement("link");
// hide all room gadget 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
// add classes to contact elements
for (i = 0; i < contact_list.length; i += 1) {
if (contact_list[i].classList.contains("current")) {
contact_list[i].classList.remove("current");
contact_list[i].classList.remove("notify");
contact_name = contact_list[i];
contact_class_list = [];
if (gadget.state.unread_room_dict[contact_name]) {
contact_class_list.push("notify");
}
// add styles to contacts with unread messages
if (gadget.state.unread_room_set[contact_list[i].id]) {
contact_list[i].classList.add("notify");
if (contact_name === gadget.state.room) {
contact_class_list.push("current");
}
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
......@@ -249,39 +268,46 @@
favicon_element.rel = "shortcut icon";
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") ||
modification_dict.hasOwnProperty("is_chat")) {
getContactFromRoom(gadget, gadget.state.room).classList.add("current");
// if a chat room is active, show chat panel
gadget.state.room_gadget_element.style.display = "none";
if (gadget.state.is_chat) {
gadget.element.querySelector(".chat-right-panel-chat")
.style.display = "flex";
gadget.element.querySelector(".chat-title")
.textContent = "Room: " + gadget.state.room;
gadget.state.chat_title_element.innerHTML =
"Room: " + gadget.state.room;
gadget.state.chat_box_element.style.display = "flex";
} else {
// otherwise, hide chat panel and show contact panel
gadget.element.querySelector(".chat-right-panel-chat")
.style.display = "none";
gadget.element.querySelector("#room-gadget-" +
nameToId(gadget.state.room)).style.display = "block";
gadget.element.querySelector(".chat-title")
.textContent = "Contact: " + gadget.state.room;
gadget.state.chat_title_element.innerHTML =
"Contact: " + gadget.state.room;
gadget.state.chat_box_element.style.display = "none";
gadget.state.room_gadget_element = gadget.element
.querySelector("#room-gadget-" + nameToId(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
gadget.state.update = false;
// refresh the current room
if (gadget.state.refresh_chat) {
gadget.state.refresh_chat = false;
if (gadget.state.delay_refresh_dict[gadget.state.room]) {
gadget.state.delay_refresh_dict[gadget.state.room].cancel();
}
return gadget.delayRefresh(gadget.state.room, 0);
gadget.delayRefresh(gadget.state.room, 5000);
}
})
......@@ -304,6 +330,18 @@
"jio_configurator",
new RSVP.Queue()
.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({
page_title: "OfficeJS Chat"
});
......@@ -333,7 +371,7 @@
.push(function () {
gadget.element.querySelector(".send-form input[type='text']")
.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.createContact(user_email);
......@@ -349,58 +387,20 @@
* Parameters:
* - room: the name of the contact
* 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
* - show the chat box
*/
.declareMethod("createContact", function (room) {
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()) {
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!";
}
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);
return gadget.createRoom(room);
})
......@@ -441,6 +441,12 @@
})
.push(function () {
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 @@
* Parameters:
* - room: the name of the room to change to
* Effects:
* - changeState: room, room_set[room]
* - hide the room gadget contact panel
* - show the chat box
* - overwrite the chat box with chats from the jIO storage
......@@ -459,9 +464,10 @@
var gadget = this;
return gadget.changeState({room: room, is_chat: true})
.push(function () {
if (gadget.state.room_set[room] === undefined) {
gadget.state.room_set[room] = 0;
if (gadget.state.message_list_dict[room].length === 0) {
return gadget.deployMessage({
name: gadget.state.name,
room: gadget.state.room,
content: gadget.state.name + " has joined.",
color: "orange"
});
......@@ -481,18 +487,12 @@
.declareMethod("deployMessage", function (param_dict) {
var gadget = this,
message;
return gadget.createMessage(param_dict)
.push(function (result) {
message = result;
return gadget.appendMessage(message);
})
.push(function () {
return gadget.storeArchive(message);
})
message = createMessage(param_dict);
gadget.state.message_list_dict[param_dict.room].push(message);
return gadget.storeArchive(message)
.push(function () {
return gadget.changeState({refresh_chat: true});
})
});
})
......@@ -505,53 +505,12 @@
*/
.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,
container = gadget.element.querySelector(".chat-list");
container.appendChild(messageToChat(message));
container.scrollTop = container.scrollHeight;
message;
param_dict.type = "notification";
message = createMessage(param_dict);
gadget.state.message_list_dict[param_dict.room].push(message);
return gadget.changeState({refresh_chat: true});
})
......@@ -590,30 +549,32 @@
.declareMethod("delayRefresh", function (room, delay) {
var gadget = this;
console.log(room, delay);
return new RSVP.Queue()
.push(function () {
return RSVP.all([
RSVP.delay(delay),
// gadget.refreshChat(room)
gadget.refreshChat(room)
]);
})
.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()
.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:
* - room: the room to refresh
* Effects:
* - get a sorted list of all chats in the current room from jIO storage
* - remove the entire chat box and append all chats from jIO storage
* - scroll the chat list down to show the chat element
* - merge the list with the current messages in a priority queue
* - update the current messages with the sorted message queue
*/
.declareMethod("refreshChat", function (room) {
......@@ -633,33 +594,50 @@
}]);
})
.push(function (result_list) {
var i, message, message_list = [],
container = gadget.element.querySelector(".chat-list");
var i, message, new_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) {
try {
message = JSON.parse(result_list.data.rows[i].value.content);
if (message && typeof message === "object") {
message_list.push(message);
message_queue.add(message);
}
} catch (ignore) {}
}
if (room === gadget.state.room) {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
for (i = 0; i < message_list.length; i += 1) {
container.appendChild(messageToChat(message_list[i]));
for (i = 0; i < old_list.length; i += 1) {
if (!message_queue.isEmpty()) {
message = message_queue.poll();
while (getTime(old_list[i]) < getTime(message)
&& i < old_list.length) {
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});
}
})
});
})
......@@ -675,7 +653,6 @@
split = chat.slice(1).split(" "),
command = split.shift(),
argument = split.join(" "),
room_element = getContactFromRoom(gadget, gadget.state.room),
promise_list = [],
help_list = [
"Available commands:",
......@@ -688,10 +665,12 @@
// change to a room that has already been joined
case "join":
if (gadget.state.room_set[argument] !== undefined) {
if (gadget.state.message_list_dict[argument] > 0) {
return gadget.changeRoom(argument);
}
return gadget.deployNotification({
name: gadget.state.name,
room: gadget.state.room,
content: "You must first be connected to room '"
+ argument + "' via a shared jIO storage to join it!",
color: "red"
......@@ -703,13 +682,16 @@
.push(function () {
if (gadget.state.room === gadget.state.name) {
return gadget.deployNotification({
name: gadget.state.name,
room: gadget.state.room,
content: "You cannot leave your own room!",
color: "red"
});
}
gadget.state.room_set[gadget.state.room] = undefined;
room_element.parentNode.removeChild(room_element);
delete gadget.state.message_list_dict[gadget.state.room];
return gadget.deployMessage({
name: gadget.state.name,
room: gadget.state.room,
content: gadget.state.name + " has quit.",
color: "orange"
})
......@@ -726,6 +708,8 @@
case "help":
for (i = 0; i < help_list.length; i += 1) {
promise_list.push(gadget.deployNotification({
name: gadget.state.name,
room: gadget.state.room,
content: help_list[i],
color: "green"
}));
......@@ -734,6 +718,8 @@
default:
return gadget.deployNotification({
name: gadget.state.name,
room: gadget.state.room,
content: "'" + argument + "' is not a valid command.",
color: "red"
});
......@@ -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.
.onEvent("submit", function (event) {
......@@ -748,8 +750,7 @@
content;
switch (event.target.className) {
case "edit-form":
content = resetInputValue(event.target.elements.content);
return gadget.changeState({room: content});
return gadget.changeState({is_chat: false});
case "join-form":
content = resetInputValue(event.target.elements.content);
return gadget.createContact(content);
......@@ -758,7 +759,11 @@
if (content.indexOf("/") === 0) {
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 @@
})
.push(function () {
var promise_list = [], room;
for (room in gadget.state.room_set) {
if (gadget.state.room_set.hasOwnProperty(room)
&& gadget.state.room_set[room] !== undefined) {
for (room in gadget.state.message_list_dict) {
if (gadget.state.message_list_dict.hasOwnProperty(room)
&& gadget.state.message_list_dict[room] > 0) {
promise_list.push(gadget.deployMessage({
name: gadget.state.name,
content: gadget.state.name + " has quit.",
room: room,
color: "orange"
......@@ -790,4 +796,5 @@
});
});
}(window, document, RSVP, rJS, promiseEventListener));
\ No newline at end of file
}(window, document, RSVP, rJS, Handlebars,
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/erp5_page_launcher*
web_page_module/fast_priority_queue_js
web_page_module/gadget_erp5_chat_panel_*
web_page_module/gadget_erp5_chat_webrtc_*
web_page_module/gadget_erp5_nojquery_css
......@@ -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_jio_*_configurator_*
web_page_module/gadget_erp5_page_jio_person_view_*
web_page_module/gadget_erp5_page_sync_*
\ No newline at end of file
web_page_module/gadget_erp5_page_sync_*
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