353 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const startTime = Date.now();
 | |
| 
 | |
| var conversation = null;
 | |
| 
 | |
| class Conversation {
 | |
|     constructor(messageData, contactName, onMessageReceived, onMessageSent) {
 | |
|         this.messageData = messageData;
 | |
|         this.contactName = contactName;
 | |
| 
 | |
|         this.messageIdx = -1;
 | |
|         // contains both sent and received messages
 | |
|         this.messages = [];
 | |
| 
 | |
|         // callbacks
 | |
|         this.onMessageReceived = onMessageReceived;
 | |
|         this.onMessageSent = onMessageSent;
 | |
|     }
 | |
| 
 | |
|     start() {
 | |
|         document.title = this.contactName;
 | |
|         document.getElementById("header-title").innerHTML = this.contactName;
 | |
| 
 | |
|         // if the first message comes from the other party, 
 | |
|         const firstMessage = this.messageData[0];
 | |
|         const firstMessageIsOurs = firstMessage.character == 0;
 | |
|         if (firstMessageIsOurs) {
 | |
|         } else {
 | |
|             setTimeout(() => {
 | |
|                 this.messageIdx = 0;
 | |
|                 this.pings = 1;
 | |
| 
 | |
|                 const data = this.messageData[this.messageIdx];
 | |
|                 const message = new ReceivedMessage(data);
 | |
|                 this.messages.push(message);
 | |
|                 this.onMessageReceived(message);
 | |
|             }, 3623);
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     peekNextMessageData() {
 | |
|         return this.messageData[this.messageIdx + 1];
 | |
|     }
 | |
| 
 | |
|     // get the text typed into the text box, ready to be sent. this could be blank
 | |
|     getTypedMessageText() {
 | |
|         const messageData = this.peekNextMessageData();
 | |
|         return this.isMessageDataOurs(messageData) ? messageData.text : "";
 | |
|     }
 | |
| 
 | |
|     getLightLag() {
 | |
|         const baseLag = 9.23582;
 | |
| 
 | |
|         // second since opening the page
 | |
|         const elapsed = (Date.now() - startTime) / 1000.5;
 | |
| 
 | |
|         // lag should shift on the order of 10,000ths of seconds per second
 | |
|         const lag = baseLag + elapsed / 8439.123;
 | |
|         return Math.round(lag * 100000) / 100000;
 | |
|     }
 | |
| 
 | |
|     sendMessage() {
 | |
|         // bail out if the next message isn't ours
 | |
|         const sentMessageData = this.messageData[this.messageIdx + 1];
 | |
|         if (!this.isMessageDataOurs(sentMessageData))
 | |
|             return;
 | |
| 
 | |
|         // TODO: error checking? what if we can't send our next message?
 | |
| 
 | |
|         // the message we are sending is the one currently in the text box, so we should construct it
 | |
|         // before advancing the conversation
 | |
|         let oneWayLag = this.getLightLag();
 | |
|         let currentMessage = this.messageData[this.messageIdx];
 | |
|         let nextMessage = this.messageData[this.messageIdx + 2];
 | |
| 
 | |
|         let sentMessage = new SentMessage(sentMessageData, oneWayLag, this.messageIdx + 1, () => {
 | |
|             // if the next message is ours we don't need to wait for anything
 | |
|             if (this.isMessageDataOurs(nextMessage))
 | |
|                 return;
 | |
| 
 | |
|             // wait for them to read the message
 | |
|             setTimeout(() => {
 | |
|                 this.onMessageRead(sentMessage);
 | |
|             }, getRandomDelay(1, 20) * 1000);
 | |
|         });
 | |
| 
 | |
|         this.messages.push(sentMessage);
 | |
| 
 | |
|         // advance conversation with our next message
 | |
|         this.messageIdx++;
 | |
| 
 | |
|         this.onMessageSent(sentMessage);
 | |
|     }
 | |
| 
 | |
|     onMessageRead(sentMessage) {
 | |
|         // set the message status to read
 | |
|         sentMessage.updateStatus("read");
 | |
| 
 | |
|         // we only want to count our messages
 | |
|         const sentMessages = this.messages.filter(m => m.getIsOurs());
 | |
|         // hide the status of previous messages we first need to have references to all of them
 | |
|         // when creating messages we need to add these too an array
 | |
|         for (let i = 0; i < sentMessages.length; i++) {
 | |
|             const message = sentMessages[i];
 | |
|             if (message != sentMessage) {
 | |
|                 message.updateStatus("");
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         this.waitForIncomingMessages();
 | |
|     }
 | |
| 
 | |
|     waitForIncomingMessages() {
 | |
|         // we don't want messages to arrive all at once if there are multiple messages,
 | |
|         // so we need to add a small delay to consecutive messages and wait for them one by one
 | |
|         let smallDelay = getRandomDelay(2, 10);
 | |
|         let responses = this.getResponses(this.messageIdx + 1);
 | |
|         let lightLag = this.getLightLag();
 | |
|         
 | |
|         setTimeout(() => {
 | |
|             setTypingIndicator(true);
 | |
|     
 | |
|             if (responses.length == 0) {
 | |
|                 console.error("got no responses?");
 | |
|             }
 | |
| 
 | |
|             for (let i = 0; i < responses.length; i++) {
 | |
|                 let delaySeconds = lightLag + smallDelay * i;
 | |
|     
 | |
|                 const stopTyping = i == responses.length - 1;
 | |
|     
 | |
|                 setTimeout(() => {
 | |
|                     this.messageIdx++;
 | |
|                     this.pings++;
 | |
|     
 | |
|                     // update the chat with their message
 | |
|                     const data = this.messageData[this.messageIdx];
 | |
|                     const message = new ReceivedMessage(data);
 | |
|                     updateChat(message);
 | |
|     
 | |
|                     if (stopTyping) {
 | |
|                         setTypingIndicator(false);
 | |
|                     }
 | |
|     
 | |
|                 }, delaySeconds * 1000);
 | |
|             }
 | |
|         },getRandomDelay(1, 3));
 | |
|     }
 | |
| 
 | |
|     getResponses(idx) {
 | |
|         // get the messages that aren't ours until we find one that is
 | |
|         let responses = [];
 | |
| 
 | |
|         for (let i = idx; i < this.messageData.length; i++) {
 | |
|             const message = this.messageData[i];
 | |
|             if (this.isMessageDataOurs(message))
 | |
|                 break;
 | |
| 
 | |
|             responses.push(message);
 | |
|         }
 | |
| 
 | |
|         return responses;
 | |
|     }
 | |
| 
 | |
|     // TODO: messages could be instantiated when the conversation is started to remove the isMessageOurs
 | |
|     // query on the raw data?
 | |
|     // however, this would mean that we would need to determine the light delay dynamically when sending
 | |
|     // the message, instead of on construction. this might not be a bad thing? but seems like a later
 | |
|     // problem
 | |
|     // for now, we should see to it that message ownership is determined only in the context of a specific
 | |
|     // conversation
 | |
|     isMessageDataOurs(messageData) {
 | |
|         if (!messageData)
 | |
|             return false;
 | |
| 
 | |
|         return messageData.character == 0;
 | |
|     }
 | |
| }
 | |
| 
 | |
| function getMessageList() {
 | |
|     return document.getElementById("messages");
 | |
| }
 | |
| 
 | |
| // TODO: messges should reference their own elements to remove the need for searching through
 | |
| // the DOM
 | |
| function getMessageElement(messageIdx) {
 | |
|     let list = getMessageList();
 | |
|     let messageElements = list.getElementsByTagName("li");
 | |
|     return messageElements[messageIdx];
 | |
| }
 | |
| 
 | |
| class ReceivedMessage {
 | |
|     constructor(data) {
 | |
|         this.text = data.text;
 | |
|     }
 | |
| 
 | |
|     getIsOurs() {
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     getHtml() {
 | |
|         return `<li><div class="message">
 | |
|             <span class="message-content rounded-rectangle theirs">
 | |
|                 <span class="message-text">${this.text}</span>
 | |
|             </span></div></li>`;
 | |
|     }
 | |
| }
 | |
| 
 | |
| class SentMessage {
 | |
|     constructor(data, oneWayLag, idx, onDelivered) {
 | |
|         this.oneWayLag = oneWayLag;
 | |
|         // TODO: remove idx reference - why should a message know where it sits in a conversation?
 | |
|         // indexing should be the responsibility of the conversation
 | |
|         this.idx = idx;
 | |
|         this.createdTime = Date.now();
 | |
|         this.onDelivered = onDelivered;
 | |
|         this.text = data.text;
 | |
| 
 | |
|         this.updateBarIntervalId = setInterval(() => {
 | |
|             let elapsed = Math.abs(Date.now() - this.createdTime) / 1000;
 | |
|             let progress = elapsed / this.oneWayLag;
 | |
| 
 | |
|             // divide in half to measure the round trip
 | |
|             progress /= 2;
 | |
| 
 | |
|             this.setProgress(progress);
 | |
|         }, 10);
 | |
|     }
 | |
| 
 | |
|     getIsOurs() {
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     getHtml() {
 | |
|         return `<li><div class="message">
 | |
|             <span class="message-content rounded-rectangle ours">
 | |
|                 <div class="progress-bar"><div class="progress"></div></div>
 | |
|                 <span class="message-text">${this.text}</span>
 | |
|             </span></div><p class="message-status">status</p></li>`;
 | |
|     }
 | |
| 
 | |
|     setProgress(amount) {
 | |
|         let thisMessage = getMessageElement(this.idx);
 | |
|         let progressBar = thisMessage.getElementsByClassName("progress")[0];
 | |
| 
 | |
|         if (amount < 0.5) {
 | |
|             const color = "var(--light-red)";
 | |
|             this.updateStatus("in flight", color);
 | |
|             progressBar.style.backgroundColor = color;
 | |
|             amount *= 2;
 | |
|         }
 | |
|         else if (amount < 1) {
 | |
|             const color = "var(--robin-egg-blue)";
 | |
|             this.updateStatus("completing round trip", color);
 | |
|             progressBar.style.backgroundColor = color;
 | |
|             amount -= 0.5;
 | |
|             amount *= 2;
 | |
|         }
 | |
|         else {
 | |
|             const color = "var(--eggshell)";
 | |
|             this.updateStatus("delivered", color);
 | |
|             progressBar.style.backgroundColor = color;
 | |
|             clearInterval(this.updateBarIntervalId);
 | |
|             amount = 0;
 | |
|             this.onDelivered();
 | |
|         }
 | |
|         amount = Math.min(amount, 1);
 | |
|         let percentage = `${amount * 100}%`;
 | |
|         progressBar.style.width = percentage;
 | |
|     }
 | |
| 
 | |
|     updateStatus(newStatus, color) {
 | |
|         let thisMessage = getMessageElement(this.idx);
 | |
|         let statusElement = thisMessage.getElementsByClassName("message-status")[0];
 | |
|         statusElement.innerHTML = newStatus;
 | |
|         statusElement.style.color = color;
 | |
|     }
 | |
| }
 | |
| 
 | |
| 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 getOurNextMessage(idx) {
 | |
|     for (let i = idx; i < conversation.length; i++) {
 | |
|         message = conversation[i];
 | |
|         if (isMessageOurs(message))
 | |
|             return message;
 | |
|     }
 | |
| 
 | |
|     return null;
 | |
| }
 | |
| 
 | |
| function updatePings() {
 | |
|     const title = conversation.contactName;
 | |
|     let newTitle = conversation.pings > 0
 | |
|         ? `(${conversation.pings}) ${title}`
 | |
|         : title;
 | |
|     
 | |
|     document.title = newTitle;
 | |
| }
 | |
| 
 | |
| function clearPings() {
 | |
|     conversation.pings = 0;
 | |
|     updatePings();
 | |
| }
 | |
| 
 | |
| function getRandomDelay(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() {
 | |
|     // we have interacted with the page so remove all pings
 | |
|     clearPings();
 | |
|     conversation.sendMessage();
 | |
| }
 | |
| 
 | |
| function onMessageReceived(message) {
 | |
|     updateChat(message);
 | |
|     setTypingIndicator(false);
 | |
| }
 | |
| 
 | |
| function onMessageSent(message) {
 | |
|     updateChat(message);
 | |
| }
 | |
| 
 | |
| function init(messageData) {
 | |
|     conversation = new Conversation(messageData, "Caesar", onMessageReceived, onMessageSent);
 | |
|     conversation.start();
 | |
| }
 | |
| 
 | |
| fetch("caesar.json")
 | |
|     .then(response => response.json())
 | |
|     .then(json => init(json));
 |