iu9-ca-web-chat/assets/js/chat.js

452 lines
16 KiB
JavaScript
Raw Normal View History

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){
2024-09-05 08:30:40 +00:00
return !messageSt.isSystem && messageSt.exists && (myRoleHere !== userChatRoleReadOnly) &&(
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
* Supercontainer.container is persistent, Supercontainer.box can change it's class */
function updateMessageSupercontainer(supercontainer, messageSt){
let box = supercontainer.box;
2024-09-05 08:30:40 +00:00
if (messageSt.isSystem)
return;
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"){
2024-09-05 08:30:40 +00:00
return subjectRef + " summoned " + objectRef;
} else if (verb === "left"){
return subjectRef + " left chat";
} else if (verb === "created"){
2024-09-05 08:30:40 +00:00
return subjectRef + " 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;
2024-09-05 08:30:40 +00:00
if (messageSt.isSystem){
2024-09-05 08:30:40 +00:00
} else {
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();
}