// how to we communicate... // * in flight // * arrived! (probably) <- this is the tricky one. we know enough time has passed for the message // * received to have been received, but we are only halfway to the earliest possible // * read acknowledgement. // // we know the length of the roundtrip: it is 2x the light lag. progress bars show a passage of time, // it could scroll: // * along the bottom of the message // * across the message // * as it is being sent, a transluscent red bar grows from right to left (towards their messages) // * while we are waiting for acknowledgement, a similar blue bar brows from left to right // * next to the message messageData = [ // c for character { c: 1, text: "hows space"}, { c: 0, text: "trying to work out if the coffees shit but"}, { c: 0, text: "my sense of taste is just not happening :/"}, { c: 1, text: "maybe you're being spared"}, { c: 0, text: "ur right"}, { c: 0, text: "ship swill is a delicacy to exactly no one"}, { c: 0, text: "at least the caffeine still works"}, { c: 1, text: "lol"}, { c: 1, text: "you probably got your own stash for later though, right?"}, { c: 1, text: "knowing you"}, { c: 0, text: "oh yea i got some on nimbus"}, { c: 0, text: "donchuu worry"}, { c: 0, text: "how about you?"}, { c: 0, text: "new job soon, right?"}, { c: 1, text: "induction tomorrow wish me luckk"}, { c: 0, text: "break a leg!"}, { c: 1, text: ":)"} ]; let pings = 0; 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() { 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.c == 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 `
  • ${this.text}
  • `; } } 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 `
  • ${this.text}

    status

  • `; } 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 ? "Hester 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); document.getElementById("textbox-input").value = conversation.getTypedMessageText(); updatePings(); } function pressSendButton() { // we have interacted with the page so remove all pings clearPings(); conversation.sendMessage(); } function updateLightLag() { const lag = conversation.getLightLag(); const text = lag.toFixed(5) + " seconds"; document.getElementById("delay-text").innerHTML = text; } function startLightLagUpdateLoop() { setInterval(() => { updateLightLag(); }, 1000); } function onMessageReceived(message) { updateChat(message); setTypingIndicator(false); } function onMessageSent(message) { updateChat(message); } function init() { conversation = new Conversation(messageData, "Hester Gomez", onMessageReceived, onMessageSent); conversation.start(); document.title = conversation.contactName; updateLightLag(0); startLightLagUpdateLoop(); } init();