let LocalHistoryId = 0; let members = new Map(); let loadedMessages = new Map(); // messageSt objects /* container: EL, box: EL, offset: number (msgPres) */ 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 getChatWgSz(){ let chatWg = document.getElementById("chat-widget"); return [chatWg.offsetWidth, chatWg.offsetHeight]; } function elSetOffsetInChat(el, offset){ el.style.bottom = String(offset) + "px"; } function isMissingPrimaryMsgHeap(){ return lastMsgId >= 0 && anchoredMsg < 0; } function isMissingTopMsgHeap(){ let [W, H] = getChatWgSz(); return anchoredMsg >= 0 && (highestPoint < H + softZoneSz && visibleMsgSegStart > 0); } function isMissingBottomMsgHeap(){ return anchoredMsg >= 0 && (lowestPoint > - softZoneSz && visibleMsgSegEnd < lastMsgId); } function updateOffsetOfVisibleMsg(msgId, offset){ visibleMessages.get(msgId).container.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).container.offsetHeight; offset += height + 5; } return offset; } function updateOffsetsDown(){ let offset = offsetOfAnchor; for (let curMsg = anchoredMsg + 1; curMsg <= visibleMsgSegEnd; curMsg++){ let height = visibleMessages.get(curMsg).container.offsetHeight; offset -= (height + 5); updateOffsetOfVisibleMsg(curMsg, offset); } return offset; } function updateOffsetsSane(){ if (anchoredMsg < 0) return; highestPoint = updateOffsetsUpToTop(); lowestPoint = updateOffsetsDown(); } function heightOfPreloadGhost(){ let [W, H] = getChatWgSz(); return Math.min(H * 0.9, Math.max(H * 0.69, 30)); } function updateOffsets(){ let spinnerTop = document.getElementById("top-loading"); let spinnerBottom = document.getElementById("bottom-loading"); let SbH = spinnerBottom.offsetHeight; if (anchoredMsg < 0){ hideHTMLElement(spinnerBottom); elSetOffsetInChat(spinnerTop, chatPadding); setElementVisibility(spinnerTop, isMissingPrimaryMsgHeap()); } else { updateOffsetsSane(); let [W, H] = getChatWgSz(); let lowestLowestPoint = isMissingBottomMsgHeap() ? lowestPoint - heightOfPreloadGhost(): lowestPoint; let highestHighestPoint = isMissingTopMsgHeap() ? highestPoint + heightOfPreloadGhost() : highestPoint; if (lowestLowestPoint > chatPadding || (highestHighestPoint - lowestLowestPoint) <= H - chatPadding * 2) { bumpedAtTheBottom = true; offsetOfAnchor += (-lowestLowestPoint + chatPadding); updateOffsetsSane(); } else if (highestHighestPoint < H - chatPadding) { offsetOfAnchor += (-highestHighestPoint + (H - chatPadding)); updateOffsetsSane(); } /* Messages weere updated (and only them). They were talking with ghosts. Now we are trying to show spinners of ghosts */ elSetOffsetInChat(spinnerTop, highestPoint); setElementVisibility(spinnerTop, isMissingTopMsgHeap()); elSetOffsetInChat(spinnerBottom, lowestPoint - SbH); setElementVisibility(spinnerBottom, isMissingBottomMsgHeap()); } } function shouldShowDeleteMesgBtn(messageSt){ return !messageSt.isSystem && messageSt.exists && (messageSt.myRoleHere !== userChatRoleReadOnly) &&( messageSt.myRoleHere === userChatRoleAdmin || messageSt.senderUserId === userinfo.uid); } // todo: fix messageboxes 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 * Supercontainer.container is persistent, Supercontainer.box can change it's class */ function updateMessageSupercontainer(supercontainer, messageSt){ let box = supercontainer.box; setElementVisibility(box.querySelector(".message-box-button-delete"), shouldShowDeleteMesgBtn(messageSt), "inline"); box.className = getMsgFullTypeClassName(messageSt); // Notice, that no check of previous state is performed. Double loading is a rare event, 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 convertMessageStToSupercontainer(messageSt){ let container = document.createElement("div"); container.className = "message-supercontainer"; let box = document.createElement("div"); container.appendChild(box); 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"); }; setElementVisibility(inTopPartButtonDelete, shouldShowDeleteMesgBtn(messageSt), "inline"); 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 {'container': container, 'box': box, 'offset': 0}; } function makeVisible(msgId){ let supercontainer = convertMessageStToSupercontainer(loadedMessages.get(msgId)); const chatWin = document.getElementById("chat-widget"); chatWin.appendChild(supercontainer.container); visibleMessages.set(msgId, supercontainer); } function opaNewMessageSt(messageSt){ let msgId = messageSt.id; if (loadedMessages.has(msgId)){ loadedMessages.set(msgId, messageSt); if (visibleMessages.has(msgId)){ updateMessageSupercontainer(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){ console.log(anchoredMsg, offsetOfAnchor, 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, sc] of visibleMessages){ updateMessageSupercontainer(sc, loadedMessages.get(msgId)); } setElementVisibility(document.getElementById("message-input"), canISendMessages()); } } 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(){ return isMissingPrimaryMsgHeap() || isMissingTopMsgHeap() || isMissingBottomMsgHeap(); } async function tryLoadWhitespaceSingle(){ console.log('tryLoadWhitespaceSingle'); if (isMissingPrimaryMsgHeap()){ await requestMessageNeighbours(-1, "backward"); } else if (isMissingTopMsgHeap()){ await requestMessageNeighbours(visibleMsgSegStart, "backward"); } else if (isMissingBottomMsgHeap()){ await requestMessageNeighbours(visibleMsgSegEnd, "forward"); } } async function loadWhitespaceMultitry(){ if (needToLoadWhitespace()){ cancelMainloopTimeout(); do { try { await tryLoadWhitespaceSingle(); 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; elSetOffsetInChat(document.getElementById("debug-line-lowest"), -softZoneSz); elSetOffsetInChat(document.getElementById("debug-line-highest"), H + softZoneSz); elSetOffsetInChat(document.getElementById("debug-line-top-padding"), H - chatPadding); elSetOffsetInChat(document.getElementById("debug-line-bottom-padding"), chatPadding) }; window.addEventListener("resize", chatWgDebugLinesFnc); chatWgDebugLinesFnc(); configureMsgDeletionPopupButtons(); updateLocalStateFromChatUpdRespBlind(initial_chatUpdResp); setMainloopTimeout(); loadWhitespaceMultitry(); }