shift/main.js

374 lines
10 KiB
JavaScript

const startTime = Date.now();
var conversation = null;
class Conversation {
constructor(name) {
this.messages = [];
this.name = romanize(name);
}
setInteractive(isInteractive) {
const children = document.getElementById("textbox").children;
for (let i = 0; i < children.length; i++) {
children[i].disabled = !isInteractive;
}
}
initialize(initialMessages) {
document.title = this.name;
document.getElementById("header-title").innerHTML = this.name;
this.messages = initialMessages;
}
// for the user to send their own messages
sendUserMessage(text) {
const message = new UserMessage(text);
message.updateStatus("sent");
setTimeout(() => {
message.updateStatus("delivered");
this.render();
setTimeout(() => {
message.updateStatus("read");
this.render();
}, 5000);
}, 1000);
this.messages.push(message);
}
// update the current HTML based on messages
render() {
// clear stale HTML
getMessageList().innerHTML = "";
// if there are multiple 'read' messages, we only want the last one to display
// the status. the other ones must have been read, so clear their statuses.
let foundReadMessage = false;
for (let i = this.messages.length - 1; i >= 0; i--) {
const message = this.messages[i];
if (!message.getIsOurs())
continue;
// if we haven't found a read message yet, let's check to see if we have now
if (!foundReadMessage) {
if (message.status == "read") {
foundReadMessage = true;
continue;
}
} else {
// we have found a read message, which means all messages above it should
// have empty status
message.updateStatus("");
}
}
// render message elements
for (let i = 0; i < this.messages.length; i++)
{
const messageRoot = document.getElementById("messages");
const newMessage = this.messages[i];
messageRoot.appendChild(newMessage.getElement());
}
}
}
function getMessageList() {
return document.getElementById("messages");
}
function romanize(text) {
text = text.replaceAll('u', 'v');
text = text.replaceAll('U', 'V');
return text;
}
class AgentMessage {
constructor(text, senderName) {
this.text = text;
this.senderName = senderName;
}
getIsOurs() {
return false;
}
getElement() {
const liElem = document.createElement("li");
liElem.className = "message";
const contentElem = document.createElement("span");
contentElem.className = "message-content rounded-rectangle theirs";
liElem.appendChild(contentElem);
if (this.senderName) {
const nameElem = document.createElement("h3");
nameElem.innerHTML = romanize(this.senderName);
contentElem.appendChild(nameElem);
}
const textElem = document.createElement("span");
textElem.className = "message-text";
textElem.innerHTML = romanize(this.text);
contentElem.appendChild(textElem);
return liElem;
}
}
class UserMessage {
constructor(text) {
this.createdTime = Date.now();
this.text = romanize(text);
this.status = "";
}
getIsOurs() {
return true;
}
getElement() {
const liElem = document.createElement("li");
liElem.className = "message";
const contentElem = document.createElement("span");
contentElem.className = "message-content rounded-rectangle ours";
liElem.appendChild(contentElem);
const textElem = document.createElement("span");
textElem.className = "message-text";
textElem.innerHTML = this.text;
contentElem.appendChild(textElem);
const statusElem = document.createElement("p");
statusElem.className = "message-status";
statusElem.innerHTML = this.status;
liElem.appendChild(statusElem);
return liElem;
}
updateStatus(newStatus) {
this.status = newStatus;
}
}
class SystemMessage {
constructor(text) {
this.text = romanize(text);
}
getIsOurs() {
return false;
}
getElement() {
const liElem = document.createElement("li");
liElem.className = "system-message";
liElem.innerHTML = this.text;
return liElem;
}
}
function setTypingIndicator(isTyping) {
document.getElementById("typing-indicator").innerHTML = isTyping
? `${conversation.contactName} is typing...`
: "";
}
// add the message at the index to the displayed messages
function addMessage(message) {
getMessageList().innerHTML += message.getHtml();
// scroll as far as we can so that messages aren't hidden
window.scrollTo(0, document.body.scrollHeight);
}
function updatePings() {
const title = conversation.name;
let newTitle = conversation.pings > 0
? `(${conversation.pings}) ${title}`
: title;
document.title = newTitle;
}
function clearPings() {
conversation.pings = 0;
updatePings();
}
// returns a decimal value between min and max
function getRandomInRange(min, max) {
const range = max - min;
return min + Math.random() * range;
}
function updateChat(message) {
addMessage(message);
const previewText = conversation.getTypedMessageText();
document.getElementById("textbox-input").value = previewText;
updatePings();
}
function pressSendButton() {
const textBox = document.getElementById("textbox-input");
if (event.type == "keydown" && event.key != "Enter")
{
textBox.value = romanize(textBox.value);
return;
}
// we have interacted with the page so remove all pings
clearPings();
// get the content of the text box
const text = textBox.value;
if (!text)
return;
textBox.value = "";
conversation.sendUserMessage(text);
conversation.render();
// TODO: start process of receiving next message from server (or fake it for now)
}
function onMessageReceived(message) {
updateChat(message);
setTypingIndicator(false);
}
function onMessageSent(message) {
updateChat(message);
}
// probably a bit hacky! but this saves having to do like, state or something in CSS?
// which probably is possible and probably would be the better way to do it, but that
// sounds like a bunch of learning i'm not SUPER in the mood for
function setVisibleOnMobile(element, isVisible) {
let classes = element.className.split().filter(c => c != "");
const invisibleClass = "invisible-on-mobile";
const visibleClass = "visible";
if (isVisible && !classes.includes(visibleClass)) {
const idx = classes.indexOf(invisibleClass);
if (idx != -1) {
classes.splice(idx, 1);
}
classes.push(visibleClass);
} else if (!isVisible && !classes.includes(invisibleClass)) {
const idx = classes.indexOf(visibleClass);
if (idx != -1) {
classes.splice(idx, 1);
}
classes.push(invisibleClass);
}
element.className = classes.join(" ");
}
function showSidePanel() {
// this function can only be called on mobile. the main conversation should be
// hidden and the side conversations panel should take up the whole screen.
const mainPanel = document.getElementById("main-panel");
const conversationListElem = document.getElementById("side-panel");
setVisibleOnMobile(mainPanel, false);
setVisibleOnMobile(conversationListElem, true);
}
function readConversationJson(path, callback) {
fetch(path)
.then(response => response.json())
.then(json => callback(json));
}
function showConversation(path) {
const mainPanel = document.getElementById("main-panel");
const conversationListElem = document.getElementById("side-panel");
setVisibleOnMobile(mainPanel, true);
setVisibleOnMobile(conversationListElem, false);
readConversationJson(path, json => {
conversation = new Conversation(json.title);
const jsonMessages = json.messages;
const participants = json.characters;
let initialMessages = [];
for (let i = 0; i < jsonMessages.length; i++) {
const data = jsonMessages[i];
const text = data.text;
if (data.character == -1) {
const message = new SystemMessage(text);
initialMessages.push(message);
} else if (data.character == 0) {
const message = new UserMessage(text);
message.updateStatus("read");
initialMessages.push(message);
} else {
const message = participants.length > 2
? new AgentMessage(text, participants[data.character])
: new AgentMessage(text);
initialMessages.push(message);
}
}
conversation.initialize(initialMessages);
conversation.setInteractive(json.interactive);
conversation.render();
});
}
function addConversationPreview(path) {
const listRoot = document.getElementById("side-panel");
readConversationJson(path, json => {
const messages = json.messages;
const elem = document.createElement("div");
elem.onclick = () => showConversation(path);
elem.className = "conversation";
const headerElem = document.createElement("h2");
headerElem.innerHTML = romanize(json.title);
elem.appendChild(headerElem);
const previewElem = document.createElement("span");
previewElem.innerHTML = romanize(messages[messages.length - 1].text);
elem.appendChild(previewElem);
listRoot.appendChild(elem);
});
}
function populateConversationList() {
const conversationFiles = [
"caesar.json",
"lucius.json",
"ides-of-march.json",
"lepidus.json",
"publius.json",
"sextus.json"
];
for (let i = 0; i < conversationFiles.length; i++) {
const path = conversationFiles[i];
addConversationPreview(path);
}
}
setTypingIndicator(false);
populateConversationList();
showConversation("ides-of-march.json");