diff --git a/static/app.js b/static/app.js
index 5d0ad15..6bbe240 100644
--- a/static/app.js
+++ b/static/app.js
@@ -30,13 +30,13 @@ const el = {
loginBtn: document.getElementById("login-btn"),
main: document.getElementById("main"),
status: document.getElementById("status"),
-
+
// Guilds
guildList: document.getElementById("guild-list"),
addGuildBtn: document.getElementById("add-guild-btn"),
guildTitle: document.getElementById("guild-title"),
createInviteBtn: document.getElementById("create-invite-btn"),
-
+
// Channels
channelList: document.getElementById("channel-list"),
voiceChannelList: document.getElementById("voice-channel-list"),
@@ -44,36 +44,42 @@ const el = {
addVoiceBtn: document.getElementById("add-voice-btn"),
dmList: document.getElementById("dm-list"),
channelTitle: document.getElementById("channel-title"),
-
+
// Messages
messageList: document.getElementById("message-list"),
messageForm: document.getElementById("message-form"),
messageBody: document.getElementById("message-body"),
-
+
// User Panel
userName: document.getElementById("user-name"),
userAvatar: document.getElementById("user-avatar"),
logoutBtn: document.getElementById("logout-btn"),
-
+
// Voice Connection
voiceConnection: document.getElementById("voice-connection"),
vcChannelName: document.getElementById("vc-channel-name"),
voiceMuteBtn: document.getElementById("voice-mute-btn"),
voiceLeaveBtn: document.getElementById("voice-leave-btn"),
-
+
// Members
memberList: document.getElementById("member-list"),
-
+
// Modals
modalContainer: document.getElementById("modal-container"),
guildForm: document.getElementById("guild-form"),
guildName: document.getElementById("guild-name"),
modalCancel: document.getElementById("modal-cancel"),
-
+
channelModal: document.getElementById("channel-modal"),
channelForm: document.getElementById("channel-form"),
channelName: document.getElementById("channel-name"),
channelModalCancel: document.getElementById("channel-modal-cancel"),
+
+ // Mobile
+ mobileMenuBtn: document.getElementById("mobile-menu-btn"),
+ mobileMembersBtn: document.getElementById("mobile-members-btn"),
+ overlay: document.getElementById("overlay"),
+ utilitySidebar: document.getElementById("utility-sidebar"),
};
// --- API Helpers ---
@@ -93,7 +99,7 @@ async function api(path, options = {}) {
try {
const body = await res.json();
detail = body.error || detail;
- } catch {}
+ } catch { }
throw new Error(`${res.status}: ${detail}`);
}
@@ -127,7 +133,7 @@ function formatDate(isoString) {
const d = new Date(isoString);
const now = new Date();
const isToday = d.toDateString() === now.toDateString();
-
+
if (isToday) {
return `Today at ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
}
@@ -176,14 +182,13 @@ function renderChannels() {
for (const channel of state.channels) {
const row = document.createElement("button");
- row.className = `channel-row ${
- (channel.kind === 'text' && state.selectedTextChannelId === channel.id) ||
- (channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : ""
- }`;
-
+ row.className = `channel-row ${(channel.kind === 'text' && state.selectedTextChannelId === channel.id) ||
+ (channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : ""
+ }`;
+
const iconName = channel.kind === 'text' ? 'hash' : 'volume-2';
row.innerHTML = ` ${escapeHtml(channel.name)}`;
-
+
row.onclick = async () => {
if (channel.kind === 'text') {
state.selectedDmUserId = null;
@@ -202,13 +207,14 @@ function renderChannels() {
renderDMs();
joinVoice();
}
+ if (window.innerWidth <= 768) closeMobileMenus();
};
-
+
if (channel.kind === 'text') {
el.channelList.appendChild(row);
} else {
el.voiceChannelList.appendChild(row);
-
+
const participants = state.voicePresence.get(channel.id) || [];
if (participants.length > 0) {
const pList = document.createElement("div");
@@ -259,11 +265,11 @@ function renderMessages(messages) {
for (const m of messages.slice().reverse()) {
const row = document.createElement("div");
const mDate = new Date(m.created_at);
- const isGrouped = lastAuthorId === m.author_user_id &&
- lastTime && (mDate - lastTime < 300000); // 5 minutes
+ const isGrouped = lastAuthorId === m.author_user_id &&
+ lastTime && (mDate - lastTime < 300000); // 5 minutes
row.className = `msg ${isGrouped ? "msg-grouped" : ""}`;
-
+
const displayName = m.author_display_name || "Unknown User";
if (isGrouped) {
@@ -412,7 +418,7 @@ async function refreshVoicePresence() {
function startVoicePresencePolling() {
if (state.voicePresencePollId) clearInterval(state.voicePresencePollId);
state.voicePresencePollId = setInterval(() => {
- refreshVoicePresence().catch(() => {});
+ refreshVoicePresence().catch(() => { });
}, 3000);
}
@@ -469,7 +475,7 @@ function stopAndClearAudioPipeline() {
for (const track of state.voice.rawStream.getTracks()) track.stop();
}
if (state.voice.audioContext) {
- state.voice.audioContext.close().catch(() => {});
+ state.voice.audioContext.close().catch(() => { });
}
state.voice.localStream = null;
state.voice.rawStream = null;
@@ -615,7 +621,7 @@ async function joinVoice() {
const channel = state.channels.find(c => c.id === state.selectedVoiceChannelId);
el.vcChannelName.textContent = channel ? channel.name : "Voice";
el.voiceConnection.classList.remove("hidden");
- refreshVoicePresence().catch(() => {});
+ refreshVoicePresence().catch(() => { });
};
ws.onmessage = async (event) => {
@@ -636,7 +642,7 @@ async function joinVoice() {
} else if (msg.type === "signal") {
await handleSignal(msg.from_user_id, msg.kind, msg.data);
}
- refreshVoicePresence().catch(() => {});
+ refreshVoicePresence().catch(() => { });
};
ws.onclose = () => {
@@ -646,7 +652,7 @@ async function joinVoice() {
stopAndClearAudioPipeline();
state.voice.joinedChannelId = null;
state.voice.ws = null;
- refreshVoicePresence().catch(() => {});
+ refreshVoicePresence().catch(() => { });
};
}
@@ -665,23 +671,55 @@ function toggleMute() {
lucide.createIcons();
}
+// --- Mobile Logic ---
+
+function toggleMobileMenu() {
+ const isOpen = el.main.classList.contains("menu-open");
+ if (isOpen) {
+ el.main.classList.remove("menu-open");
+ el.overlay.classList.add("hidden");
+ } else {
+ el.main.classList.add("menu-open");
+ el.utilitySidebar.classList.remove("active"); // close members if open
+ el.overlay.classList.remove("hidden");
+ }
+}
+
+function toggleMobileMembers() {
+ const isOpen = el.utilitySidebar.classList.contains("active");
+ if (isOpen) {
+ el.utilitySidebar.classList.remove("active");
+ el.overlay.classList.add("hidden");
+ } else {
+ el.utilitySidebar.classList.add("active");
+ el.main.classList.remove("menu-open"); // close menu if open
+ el.overlay.classList.remove("hidden");
+ }
+}
+
+function closeMobileMenus() {
+ el.main.classList.remove("menu-open");
+ el.utilitySidebar.classList.remove("active");
+ el.overlay.classList.add("hidden");
+}
+
// --- Initialization ---
async function init() {
lucide.createIcons();
el.loginBtn.onclick = () => { location.href = "/auth/login"; };
-
+
el.logoutBtn.onclick = async () => {
await leaveVoice();
await api("/auth/logout", { method: "POST" });
location.reload();
};
-
+
el.addGuildBtn.onclick = () => { el.modalContainer.classList.remove("hidden"); };
el.createInviteBtn.onclick = createInviteLink;
el.modalCancel.onclick = () => { el.modalContainer.classList.add("hidden"); };
-
+
el.addTextBtn.onclick = () => {
if (!state.selectedGuildId) {
alert("Create or select a server first.");
@@ -700,6 +738,11 @@ async function init() {
};
el.channelModalCancel.onclick = () => { el.channelModal.classList.add("hidden"); };
+ // Mobile Listeners
+ if (el.mobileMenuBtn) el.mobileMenuBtn.onclick = toggleMobileMenu;
+ if (el.mobileMembersBtn) el.mobileMembersBtn.onclick = toggleMobileMembers;
+ if (el.overlay) el.overlay.onclick = closeMobileMenus;
+
el.guildForm.onsubmit = async (e) => {
e.preventDefault();
try {
diff --git a/static/index.html b/static/index.html
index ef09364..f5e8b2f 100644
--- a/static/index.html
+++ b/static/index.html
@@ -108,8 +108,15 @@
@@ -122,7 +129,7 @@
-