lorem_ipsum/js/messages.js

579 lines
18 KiB
JavaScript

(function () {
const MESSAGE_TYPE_CHALLENGE = "challenge";
const MESSAGE_TYPE_CHALLENGE_RESULT = "challenge-result";
const MESSAGE_POLL_INTERVAL_MS = 30000;
const ACTIVE_CHALLENGE_STORAGE_KEY = "loremIpsumActiveChallenge";
const CHALLENGE_DATA_PREFIX = "[[loremIpsumChallenge:";
let currentMessages = [];
let currentUsers = [];
let messagePollingInterval = null;
function getAuth() {
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
return null;
}
const auth = window.AppAuth.getAuth();
if (!auth || !auth.username || !auth.password) {
return null;
}
return auth;
}
function normalizeUsername(username) {
return String(username || "").trim().toLowerCase();
}
function getMessageService() {
if (!window.config || !window.MessageService) {
return null;
}
return new window.MessageService(window.config);
}
function getUserService() {
if (!window.config || !window.UserService) {
return null;
}
return new window.UserService(window.config);
}
function getChallengeService() {
if (!window.config || !window.ChallengeService) {
return null;
}
return new window.ChallengeService(window.config);
}
function normalizeMessage(message) {
const embeddedChallenge = extractEmbeddedChallenge(message.text ?? "");
const type = message.type ?? MESSAGE_TYPE_CHALLENGE;
const challenge = message.challenge
?? message.result
?? embeddedChallenge.challenge
?? (type === MESSAGE_TYPE_CHALLENGE || type === MESSAGE_TYPE_CHALLENGE_RESULT ? message : null);
return {
id: message.id,
sender: message.sender ?? message.from ?? "",
recipient: message.recipient ?? message.to ?? "",
type: type,
text: embeddedChallenge.text,
read: Boolean(message.read),
createdAt: message.createdAt ?? message.time ?? message.date ?? "",
challenge: challenge,
};
}
function extractEmbeddedChallenge(text) {
const rawText = String(text ?? "");
if (!rawText.startsWith(CHALLENGE_DATA_PREFIX)) {
return {
text: rawText,
challenge: null,
};
}
const endIndex = rawText.indexOf("]]");
if (endIndex === -1) {
return {
text: rawText,
challenge: null,
};
}
const json = rawText.slice(CHALLENGE_DATA_PREFIX.length, endIndex);
const displayText = rawText.slice(endIndex + 2).trim();
try {
return {
text: displayText,
challenge: JSON.parse(json),
};
} catch {
return {
text: rawText,
challenge: null,
};
}
}
function getChallengeId(challenge) {
return challenge?.challengeId ?? challenge?.challenge_id ?? challenge?.id ?? null;
}
function getChallengeChallenger(challenge, fallbackName) {
return challenge?.challenger
?? challenge?.challengerName
?? challenge?.sender
?? challenge?.from
?? fallbackName
?? "";
}
function getChallengeOpponent(challenge, fallbackName) {
return challenge?.opponent
?? challenge?.opponentName
?? challenge?.challengedUser
?? challenge?.recipient
?? challenge?.to
?? fallbackName
?? "";
}
function getOpponentScore(challenge) {
return challenge?.opponentScore
?? challenge?.challengedScore
?? challenge?.challengedUserScore
?? null;
}
function hasScore(value) {
return value !== null && value !== undefined && value !== "";
}
function getChallengeRole(message) {
const auth = getAuth();
const challenge = message.challenge;
if (!auth || !challenge || getChallengeId(challenge) === null) {
return null;
}
const ownName = normalizeUsername(auth.username);
const challenger = normalizeUsername(getChallengeChallenger(challenge, message.sender));
const opponent = normalizeUsername(getChallengeOpponent(challenge, message.recipient));
if (ownName === opponent) {
return "opponent";
}
if (ownName === challenger) {
return "challenger";
}
return null;
}
function canOpponentAcceptChallenge(message) {
if (message.type !== MESSAGE_TYPE_CHALLENGE || !message.challenge) {
return false;
}
const role = getChallengeRole(message);
const challenge = message.challenge;
const opponentScore = getOpponentScore(challenge);
return role === "opponent" && !hasScore(opponentScore) && !hasScore(challenge.challengerScore);
}
function canChallengerPlayChallenge(message) {
const role = getChallengeRole(message);
const challenge = message.challenge;
return role === "challenger"
&& hasScore(getOpponentScore(challenge))
&& !hasScore(challenge.challengerScore);
}
function startChallenge(message, role) {
const challenge = message.challenge;
const challenger = getChallengeChallenger(challenge, message.sender);
const opponent = getChallengeOpponent(challenge, message.recipient);
const auth = getAuth();
const otherUser = role === "opponent" ? challenger : opponent;
sessionStorage.setItem(
ACTIVE_CHALLENGE_STORAGE_KEY,
JSON.stringify({
id: getChallengeId(challenge),
role: role,
challenger: challenger,
opponent: otherUser,
opponentScore: role === "challenger" ? getOpponentScore(challenge) : null,
ownUsername: auth?.username ?? "",
}),
);
if (typeof window.loadPage === "function") {
window.loadPage("play", "nav-play");
}
}
function normalizeUser(user) {
if (typeof user === "string") {
return user;
}
return user?.name ?? user?.username ?? "";
}
function formatMessageTime(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return "";
}
return date.toLocaleString("de-CH", {
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
function setFeedback(message, type) {
const feedback = document.getElementById("messages-feedback");
if (!feedback) {
return;
}
feedback.className = "alert alert-" + type + " mt-3 mb-0";
feedback.textContent = message;
feedback.classList.remove("d-none");
}
function setFormEnabled(enabled) {
const formElements = document.querySelectorAll(
"#challenge-form button, #challenge-form select, #challenge-form textarea, #mark-read-button, #refresh-messages-button",
);
formElements.forEach((element) => {
element.disabled = !enabled;
});
}
function updateMessagesNavState(messages = currentMessages) {
const navLink = document.getElementById("navbar-messages");
if (!navLink) {
return;
}
const auth = getAuth();
const ownName = normalizeUsername(auth?.username);
const hasUnreadMessages = messages.some((message) => {
const isIncoming = normalizeUsername(message.recipient) === ownName
|| normalizeUsername(message.sender) !== ownName;
return isIncoming && !message.read;
});
navLink.classList.toggle("has-unread-messages", hasUnreadMessages);
}
function renderUserList(users) {
const userList = document.getElementById("messages-user-list");
const recipientSelect = document.getElementById("challenge-recipient");
if (!userList || !recipientSelect) {
return;
}
const auth = getAuth();
const ownName = normalizeUsername(auth?.username);
const uniqueUsers = Array.from(new Set(users.map(normalizeUser)))
.filter(Boolean)
.filter((username) => normalizeUsername(username) !== ownName)
.sort((a, b) => a.localeCompare(b, "de-CH"));
currentUsers = uniqueUsers;
userList.innerHTML = "";
recipientSelect.innerHTML = "";
if (uniqueUsers.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.className = "messages-empty";
emptyMessage.textContent = "Keine anderen User gefunden.";
userList.appendChild(emptyMessage);
return;
}
uniqueUsers.forEach((username) => {
const userButton = document.createElement("button");
userButton.type = "button";
userButton.className = "messages-user-button";
userButton.textContent = username;
userButton.addEventListener("click", () => {
recipientSelect.value = username;
document.getElementById("challenge-text")?.focus();
});
userList.appendChild(userButton);
const option = document.createElement("option");
option.value = username;
option.textContent = username;
recipientSelect.appendChild(option);
});
}
function renderMessages(messages = currentMessages) {
const messageList = document.getElementById("message-list");
if (!messageList) {
return;
}
const auth = getAuth();
const ownName = normalizeUsername(auth?.username);
messageList.innerHTML = "";
if (messages.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.className = "messages-empty";
emptyMessage.textContent = "Noch keine Nachrichten vorhanden.";
messageList.appendChild(emptyMessage);
return;
}
messages
.slice()
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.forEach((message) => {
const item = document.createElement("article");
const isOutgoing = normalizeUsername(message.sender) === ownName;
item.className = "message-item";
if (!message.read && !isOutgoing) {
item.classList.add("message-item-unread");
}
const fromToText = isOutgoing
? "An " + message.recipient
: "Von " + message.sender;
const meta = document.createElement("div");
meta.className = "message-meta";
const sender = document.createElement("strong");
sender.textContent = fromToText;
const time = document.createElement("span");
time.textContent = formatMessageTime(message.createdAt);
const text = document.createElement("p");
text.textContent = message.text;
meta.append(sender, time);
item.append(meta, text);
if (message.type === MESSAGE_TYPE_CHALLENGE_RESULT && message.challenge) {
item.appendChild(createChallengeResultGraphic(message.challenge));
}
if (canOpponentAcceptChallenge(message)) {
const acceptButton = document.createElement("button");
acceptButton.type = "button";
acceptButton.className = "btn btn-sm mt-3";
acceptButton.textContent = "Challenge annehmen";
acceptButton.addEventListener("click", () => startChallenge(message, "opponent"));
item.appendChild(acceptButton);
}
if (canChallengerPlayChallenge(message)) {
const playButton = document.createElement("button");
playButton.type = "button";
playButton.className = "btn btn-sm mt-3";
playButton.textContent = "Challenge spielen";
playButton.addEventListener("click", () => startChallenge(message, "challenger"));
item.appendChild(playButton);
}
messageList.appendChild(item);
});
}
function createChallengeResultGraphic(result) {
const graphic = document.createElement("div");
graphic.className = "challenge-result-graphic";
const winnerName = result.winner ?? "Unentschieden";
const isDraw = result.winner === null || result.winner === "draw";
const headline = document.createElement("div");
headline.className = "challenge-result-headline " + (isDraw ? "challenge-result-draw" : "challenge-result-win");
headline.textContent = isDraw ? "Unentschieden" : "Sieger: " + winnerName;
const scores = document.createElement("div");
scores.className = "challenge-result-scores";
const challenger = document.createElement("div");
challenger.className = "challenge-result-score";
challenger.innerHTML = "<strong></strong><span></span>";
challenger.querySelector("strong").textContent = result.challenger ?? "Herausforderer";
challenger.querySelector("span").textContent = String(result.challengerScore ?? "-") + " Punkte";
const opponent = document.createElement("div");
opponent.className = "challenge-result-score";
opponent.innerHTML = "<strong></strong><span></span>";
opponent.querySelector("strong").textContent = result.opponent ?? "Gegner";
opponent.querySelector("span").textContent = String(result.opponentScore ?? "-") + " Punkte";
scores.append(challenger, opponent);
graphic.append(headline, scores);
return graphic;
}
async function loadUsers() {
const auth = getAuth();
const userService = getUserService();
if (!auth || !userService || typeof userService.getUsers !== "function") {
renderUserList([]);
return;
}
const result = await userService.getUsers(auth.username, auth.password);
if (!result.ok || !Array.isArray(result.body)) {
renderUserList([]);
setFeedback("User konnten nicht geladen werden. Backend-Endpunkt GET /users fehlt eventuell noch.", "warning");
return;
}
renderUserList(result.body);
}
async function loadMessages(options = {}) {
const auth = getAuth();
const messageService = getMessageService();
if (!auth) {
currentMessages = [];
renderMessages();
updateMessagesNavState();
setFormEnabled(false);
if (options.showFeedback !== false) {
setFeedback("Bitte zuerst einloggen, um Nachrichten zu nutzen.", "warning");
}
return;
}
if (!messageService) {
setFormEnabled(false);
setFeedback("Message-Service konnte nicht geladen werden.", "danger");
return;
}
const result = await messageService.getMessages(auth.username, auth.password);
if (!result.ok || !Array.isArray(result.body)) {
currentMessages = [];
renderMessages();
updateMessagesNavState();
setFormEnabled(false);
setFeedback("Nachrichten konnten nicht geladen werden. Backend-Endpunkt GET /messages fehlt eventuell noch.", "warning");
return;
}
currentMessages = result.body.map(normalizeMessage);
renderMessages();
updateMessagesNavState();
setFormEnabled(true);
if (options.showFeedback) {
setFeedback("Nachrichten wurden aktualisiert.", "success");
}
}
async function handleChallengeSubmit(event) {
event.preventDefault();
const auth = getAuth();
const challengeService = getChallengeService();
const recipientSelect = document.getElementById("challenge-recipient");
const textInput = document.getElementById("challenge-text");
if (!auth || !challengeService || !recipientSelect || !textInput) {
setFeedback("Bitte zuerst einloggen, um Challenges zu senden.", "warning");
return;
}
const recipient = recipientSelect.value;
const text = textInput.value.trim();
if (!recipient || !text) {
setFeedback("Bitte Empfaenger und Nachricht eingeben.", "warning");
return;
}
const result = await challengeService.postChallenge(
auth.username,
auth.password,
recipient,
text,
);
if (!result.ok) {
setFeedback("Challenge konnte nicht gesendet werden (Status " + result.status + ").", "danger");
return;
}
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
setFeedback(
"Challenge an " + recipient + " wurde gesendet. Der Gegner spielt zuerst; danach bekommst du sein Resultat.",
"success",
);
await loadMessages({ showFeedback: false });
}
async function handleMarkRead() {
const auth = getAuth();
const messageService = getMessageService();
if (!auth || !messageService) {
setFeedback("Bitte zuerst einloggen, um Nachrichten zu markieren.", "warning");
return;
}
const result = await messageService.markAllMessagesAsRead(auth.username, auth.password);
if (!result.ok) {
const unreadMessages = currentMessages.filter((message) => !message.read);
const readResults = await Promise.all(
unreadMessages.map((message) =>
messageService.markMessageAsRead(auth.username, auth.password, message.id),
),
);
if (readResults.some((readResult) => !readResult.ok)) {
setFeedback("Nachrichten konnten nicht als gelesen markiert werden.", "danger");
return;
}
}
setFeedback("Alle Nachrichten wurden als gelesen markiert.", "info");
await loadMessages({ showFeedback: false });
}
async function initMessagesPage() {
const challengeForm = document.getElementById("challenge-form");
const markReadButton = document.getElementById("mark-read-button");
const refreshMessagesButton = document.getElementById("refresh-messages-button");
if (challengeForm) {
challengeForm.addEventListener("submit", handleChallengeSubmit);
}
if (markReadButton) {
markReadButton.addEventListener("click", handleMarkRead);
}
if (refreshMessagesButton) {
refreshMessagesButton.addEventListener("click", () => loadMessages({ showFeedback: true }));
}
setFormEnabled(Boolean(getAuth()));
await loadUsers();
await loadMessages({ showFeedback: false });
}
window.initMessagesPage = initMessagesPage;
window.updateMessagesNavState = function () {
loadMessages({ showFeedback: false }).catch((error) => {
console.error("Nachrichtenstatus konnte nicht geladen werden:", error);
updateMessagesNavState([]);
});
};
document.addEventListener("DOMContentLoaded", () => {
window.updateMessagesNavState();
if (!messagePollingInterval) {
messagePollingInterval = window.setInterval(
window.updateMessagesNavState,
MESSAGE_POLL_INTERVAL_MS,
);
}
});
})();