579 lines
18 KiB
JavaScript
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,
|
|
);
|
|
}
|
|
});
|
|
})();
|