let LocalHistoryId = 0; let members = new Map(); let loadedMessages = new Map(); // messageSt objects let visibleMessages = new Map(); // HTMLElement objects let anchoredMsg = -1; let visibleMsgSegStart = -1; let visibleMsgSegEnd = -2; let offsetOfAnchor = 500; let highestPoint = null; let lowestPoint = null; let lastMsgId = -1; let myRoleHere = null; // Dung local state updates should be updated first // Would start with true if opened `/chat/<>` let bumpedAtTheBottom = false; // Hidden variable. When deletion window popup is active // Persists from popup activation until popup deactivation let storeHiddenMsgIdForDeletionWin = -1; // Positive in production, negative for debug let softZoneSz = -150; let chatPadding = 300; function genSentBase(){ return { 'chatUpdReq': { 'LocalHistoryId': LocalHistoryId, 'chatId': openedchat.id } }; } function genSentBaseGMN(){ let Sent = genSentBase(); Sent.amount = 1; return Sent; } function updateOffsetOfVisibleMsg(msgId, offset){ visibleMessages.get(msgId).style.bottom = String(offset) + "px"; } function updateOffsetsUpToTop(){ let offset = offsetOfAnchor; for (let curMsg = anchoredMsg; curMsg >= visibleMsgSegStart; curMsg--){ updateOffsetOfVisibleMsg(curMsg, offset); let height = visibleMessages.get(curMsg).offsetHeight; offset += height + 5; } return offset; } function updateOffsetsDown(){ let offset = offsetOfAnchor; for (let curMsg = anchoredMsg + 1; curMsg <= visibleMsgSegEnd; curMsg++){ let height = visibleMessages.get(curMsg).offsetHeight; offset -= (height + 5); updateOffsetOfVisibleMsg(curMsg, offset); } return offset; } function updateOffsetsSane(){ if (anchoredMsg < 0) return; highestPoint = updateOffsetsUpToTop(); lowestPoint = updateOffsetsDown(); } function updateOffsets(){ if (anchoredMsg < 0) return; updateOffsetsSane() let winTop = document.getElementById("chat-widget").offsetHeight; if (lowestPoint > chatPadding || (highestPoint - lowestPoint) <= winTop){ bumpedAtTheBottom = true; anchoredMsg = visibleMsgSegEnd; offsetOfAnchor = chatPadding; updateOffsetsSane(); } else if (highestPoint < winTop - chatPadding) { console.log("Advancing by " + (winTop - chatPadding - highestPoint)) offsetOfAnchor += (winTop - chatPadding - highestPoint); updateOffsetsSane(); } } function shouldShowDeleteMesgBtn(messageSt){ return !messageSt.isSystem && messageSt.exists && ( messageSt.myRoleHere === userChatRoleAdmin || messageSt.senderUserId === userinfo.uid); } function getMsgTypeClassSenderBased(messageSt){ if (messageSt.isSystem) return "message-box-system" if (messageSt.senderUserId === userinfo.uid) return "message-box-mine" return "message-box-alien"; } function getMsgFullTypeClassName(messageSt){ return getMsgTypeClassSenderBased(messageSt) + (messageSt.exists ? "" : " message-box-deleted"); } /* Two things can be updated: messages existance and delete button visibility */ function updateMessageBox(id, box, messageSt){ box.querySelector(".message-box-button-delete").style.display = shouldShowDeleteMesgBtn(messageSt) ? "block" : "none"; box.className = getMsgFullTypeClassName(messageSt); // Notice, that no check of previous state is performed. Double loading is a rare event, // and I can afford to be slow if (!messageSt.exists) box.querySelector(".message-box-msg").innerText = msgErased; } function decodeSystemMessage(text){ let [subject, verb, object] = text.split(','); let subjectId = Number(subject); let objectId = Number(object); let subjectRef = members.has(subjectId) ? members.get(subjectId).nickname : "???"; let objectRef = members.has(objectId) ? members.get(objectId).nickname : "???"; if (verb === "kicked"){ return subjectRef + " kicked " + objectRef; } else if (verb === "summoned"){ return subjectRef + " summoned " + objectId; } else if (verb === "left"){ return subjectRef + " left chat"; } else if (verb === "joined"){ return subjectId + " joined chat"; } else if (verb === "created"){ return subjectId + " created this chat"; } return "... Bad log ..."; } function convertMessageStToBox(messageSt){ let box = document.createElement("div"); box.className = getMsgFullTypeClassName(messageSt); let ID = messageSt.id; let topPart = document.createElement("div"); box.appendChild(topPart); topPart.className = "message-box-top"; if (!members.has(messageSt.senderUserId)) throw new Error("First - update members"); let senderMemberSt = members.get(messageSt.senderUserId); let senderProfileURI = "/user/" + senderMemberSt.nickname; let inTopPartSenderName = document.createElement("a"); topPart.appendChild(inTopPartSenderName); inTopPartSenderName.className = "message-box-sender-name"; inTopPartSenderName.innerText = senderMemberSt.name; inTopPartSenderName.href = senderProfileURI; let inTopPartSenderNickname = document.createElement("a"); topPart.appendChild(inTopPartSenderNickname); inTopPartSenderNickname.className = "message-box-sender-name message-box-sender-shortname" inTopPartSenderNickname.innerText = senderMemberSt.nickname; inTopPartSenderNickname.href = senderProfileURI; let inTopPartButtonDelete = document.createElement("img"); topPart.appendChild(inTopPartButtonDelete); inTopPartButtonDelete.className = "message-box-button message-box-button-delete"; inTopPartButtonDelete.src = "/assets/img/delete.svg"; inTopPartButtonDelete.onclick = (ev) => { if (ev.button !== 0) return; let msgText = box.querySelector(".message-box-msg").innerText; let previewText = senderMemberSt.nickname + ":\n" + msgText; if (previewText.length > 1000) previewText = previewText.substring(0, 1000 - 3); document.getElementById("win-deletion-msg-preview").innerText = previewText; storeHiddenMsgIdForDeletionWin = ID; activatePopupWindowById("msg-deletion-win"); }; let inTopPartButtonGetLink = document.createElement("img"); topPart.appendChild(inTopPartButtonGetLink); inTopPartButtonGetLink.className = "message-box-button"; inTopPartButtonGetLink.src = "/assets/img/link.svg"; inTopPartButtonGetLink.onclick = (ev) => { if (ev.button !== 0) return; let URI = window.location.host + "/chat/" + openedchat.nickname + "/m/" + String(ID); document.getElementById("message-input").innerText += (" " + URI + ""); console.log("Tried to get link on message " + ID); }; let msgPart = document.createElement("p"); box.appendChild(msgPart); msgPart.className = "message-box-msg"; if (messageSt.exists){ if (messageSt.isSystem) msgPart.innerText = decodeSystemMessage(messageSt.text); else msgPart.innerText = messageSt.text; } else msgPart.innerText = msgErased; return box; } function makeVisible(msgId){ let box = convertMessageStToBox(loadedMessages.get(msgId)); const chatWin = document.getElementById("chat-widget"); chatWin.appendChild(box); visibleMessages.set(msgId, box); } function opaNewMessageSt(messageSt){ let msgId = messageSt.id; if (loadedMessages.has(msgId)){ loadedMessages.set(msgId, messageSt); if (visibleMessages.has(msgId)){ updateMessageBox(msgId, visibleMessages.get(msgId), messageSt); } } else { loadedMessages.set(msgId, messageSt); if (anchoredMsg < 0){ anchoredMsg = msgId; visibleMsgSegStart = msgId; visibleMsgSegEnd = msgId; makeVisible(msgId); } else if (msgId + 1 === visibleMsgSegStart) { visibleMsgSegStart--; makeVisible(msgId); while (loadedMessages.has(visibleMsgSegStart - 1)){ visibleMsgSegStart--; makeVisible(visibleMsgSegStart); } } else if (msgId - 1 === visibleMsgSegEnd){ visibleMsgSegEnd++; makeVisible(msgId); while (loadedMessages.has(visibleMsgSegEnd + 1)){ visibleMsgSegEnd++; makeVisible(visibleMsgSegEnd); } } } } function canISendMessages(){ return myRoleHere === userChatRoleRegular || myRoleHere === userChatRoleAdmin; } function updateLocalStateFromChatUpdRespBlind(chatUpdResp){ LocalHistoryId = chatUpdResp.HistoryId; for (let memberSt of chatUpdResp.members){ let id = memberSt.userId; if (id === userinfo.uid && myRoleHere !== memberSt.roleHere) { myRoleHere = memberSt.roleHere; for (let [msgId, box] of visibleMessages){ updateMessageBox(msgId, loadedMessages.get(msgId), box); } document.getElementById("message-input").style.display = (canISendMessages() ? "block" : "none"); } } for (let memberSt of chatUpdResp.members){ let id = memberSt.userId; members.set(id, memberSt); } lastMsgId = chatUpdResp.lastMsgId; for (let messageSt of chatUpdResp.messages){ opaNewMessageSt(messageSt); } updateOffsets(); } function updateLocalStateFromRecvBlind(Recv){ updateLocalStateFromChatUpdRespBlind(Recv.chatUpdResp); } async function requestMessageNeighbours(fromMsg, direction){ let Sent = genSentBaseGMN(); Sent.msgId = fromMsg; Sent.direction = direction; let Recv = await apiRequest("getMessageNeighbours", Sent); updateLocalStateFromRecvBlind(Recv); // Blind to non-loaded whitespaces } function needToLoadWhitespace(){ let winTop = document.getElementById("chat-widget").offsetHeight; if (anchoredMsg === -1){ if (lastMsgId >= 0) console.log("NEEDED 1", anchoredMsg, lastMsgId); return lastMsgId >= 0; } else if (highestPoint < winTop + softZoneSz && visibleMsgSegStart > 0){ console.log("NEEDED 2", visibleMsgSegStart); return true; } else if (lowestPoint > 0 - softZoneSz && visibleMsgSegEnd < lastMsgId){ console.log("NEEDED 3"); return true; } return false; } async function tryLoadWhitespace(){ let winTop = document.getElementById("chat-widget").offsetHeight; console.log(anchoredMsg, lastMsgId); if (anchoredMsg === -1){ if (lastMsgId !== -1){ await requestMessageNeighbours(-1, "backward"); } } else if (highestPoint < winTop + softZoneSz && visibleMsgSegStart > 0){ await requestMessageNeighbours(visibleMsgSegStart, "backward"); } else if (lowestPoint > 0 - softZoneSz && visibleMsgSegEnd < lastMsgId){ await requestMessageNeighbours(visibleMsgSegEnd, "forward"); } } async function loadWhitespaceMultitry(){ if (needToLoadWhitespace()){ cancelMainloopTimeout(); do { console.trace(); console.log("Normalnie ludi ne spyat"); try { await tryLoadWhitespace(); await sleep(100); } catch (e) { console.error(e); await sleep(1500); } } while (needToLoadWhitespace()); setMainloopTimeout(); } } async function updateLocalStateFromRecv(Recv){ updateLocalStateFromRecvBlind(Recv); await loadWhitespaceMultitry(); } async function safeApiRequestWithLocalStUpdate(type, Sent, errMsg){ try { let Recv = await apiRequest(type, Sent) await updateLocalStateFromRecv(Recv); } catch(e) { console.error(e); alert(errMsg); } } function configureMsgDeletionPopupButtons(){ document.getElementById("msg-deletion-yes").onclick = function(ev){ if (ev.button !== 0) return; deactivateActivePopup(); let Sent = genSentBase(); Sent.id = storeHiddenMsgIdForDeletionWin; safeApiRequestWithLocalStUpdate("deleteMessage", Sent, "Failed to delete message"); }; document.getElementById("msg-deletion-no").onclick = function (ev){ if (ev.button !== 0) return; deactivateActivePopup(); } } __mainloopDelayMs = 1000; async function UPDATE(){ let Recv = await apiRequest("chatPollEvents", genSentBase()); await updateLocalStateFromRecv(Recv); } // __guestMainloopPollerAction = UPDATE(); window.onload = function (){ console.log("Page was loaded"); document.body.addEventListener("wheel", function (event) { // event.preventDefault(); bumpedAtTheBottom = false; offsetOfAnchor += event.deltaY / 3; updateOffsets(); loadWhitespaceMultitry().then(dopDopYesYes); }); document.getElementById("message-input").addEventListener("keyup", function (event) { if (event.ctrlKey && event.key === 'Enter'){ let textarea = document.getElementById("message-input"); let text = String(textarea.innerText); console.log(text); textarea.innerText = ""; let Sent = genSentBase(); Sent.content = {}; Sent.content.text = text; safeApiRequestWithLocalStUpdate("sendMessage", Sent, "Failed to send message"); } }); let chatWg = document.getElementById("chat-widget"); let chatWgDebugLinesFnc = function (){ let H = chatWg.offsetHeight; document.getElementById("debug-line-lowest").style.bottom = String(-softZoneSz) + "px"; document.getElementById("debug-line-highest").style.bottom = String(H + softZoneSz) + "px"; }; window.addEventListener("resize", chatWgDebugLinesFnc); chatWgDebugLinesFnc(); configureMsgDeletionPopupButtons(); updateLocalStateFromChatUpdRespBlind(initial_chatUpdResp); setMainloopTimeout(); loadWhitespaceMultitry(); }