fix: mobile
All checks were successful
/ upload (release) Successful in 22s

This commit is contained in:
pavel 2026-02-13 19:26:21 +01:00
commit b353deffbb
3 changed files with 166 additions and 33 deletions

View file

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

View file

@ -108,8 +108,15 @@
<main class="chat-pane"> <main class="chat-pane">
<header class="chat-header"> <header class="chat-header">
<button id="mobile-menu-btn" class="mobile-toggle-btn hidden" title="Menu">
<i data-lucide="menu"></i>
</button>
<i data-lucide="hash" class="header-icon"></i> <i data-lucide="hash" class="header-icon"></i>
<h3 id="channel-title">Select a channel</h3> <h3 id="channel-title">Select a channel</h3>
<div style="flex:1"></div>
<button id="mobile-members-btn" class="mobile-toggle-btn hidden" title="Members">
<i data-lucide="users"></i>
</button>
</header> </header>
<div id="message-list" class="message-list"></div> <div id="message-list" class="message-list"></div>
@ -122,7 +129,7 @@
</div> </div>
</main> </main>
<aside class="utility-sidebar"> <aside class="utility-sidebar" id="utility-sidebar">
<header class="sidebar-header"> <header class="sidebar-header">
<h2>Members</h2> <h2>Members</h2>
</header> </header>
@ -132,6 +139,8 @@
</aside> </aside>
</div> </div>
<div id="overlay" class="overlay hidden"></div>
<!-- Modals --> <!-- Modals -->
<div id="modal-container" class="modal-container hidden"> <div id="modal-container" class="modal-container hidden">
<div class="modal"> <div class="modal">

View file

@ -515,12 +515,93 @@ input, select { border: 0; outline: none; background: var(--bg-input); color: va
.radio-text strong { font-size: 16px; } .radio-text strong { font-size: 16px; }
.radio-text span { font-size: 12px; color: var(--text-muted); } .radio-text span { font-size: 12px; color: var(--text-muted); }
/* Mobile Overlay */
.overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 90;
opacity: 1;
transition: opacity 0.2s ease;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
visibility: hidden;
}
/* Mobile Toggle Buttons */
.mobile-toggle-btn {
display: none;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
color: var(--text-normal);
background: transparent;
margin-right: 8px;
}
.mobile-toggle-btn:hover {
background: var(--bg-modifier-hover);
border-radius: 4px;
}
@media (max-width: 1100px) { @media (max-width: 1100px) {
.shell { grid-template-columns: 72px 240px 1fr; } .shell { grid-template-columns: 72px 240px 1fr; }
.utility-sidebar { display: none; } .utility-sidebar {
position: fixed;
top: 0; bottom: 0; right: 0;
width: 240px;
z-index: 100;
box-shadow: -2px 0 10px rgba(0,0,0,0.5);
transform: translateX(100%);
transition: transform 0.2s ease;
}
.utility-sidebar.active {
transform: translateX(0);
display: flex;
}
.chat-header .mobile-toggle-btn { display: flex; }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.shell { grid-template-columns: 72px 1fr; } .shell {
.channel-sidebar { display: none; } grid-template-columns: 1fr;
}
/* Hide sidebars by default on mobile */
.server-rail, .channel-sidebar {
position: fixed;
top: 0; bottom: 0;
z-index: 100;
transition: transform 0.2s ease;
}
.server-rail {
left: 0;
width: 72px;
transform: translateX(-100%);
}
.channel-sidebar {
left: 72px;
width: 240px;
transform: translateX(-312px); /* 72 + 240 */
box-shadow: 2px 0 10px rgba(0,0,0,0.5);
}
/* When menu is active */
.shell.menu-open .server-rail {
transform: translateX(0);
}
.shell.menu-open .channel-sidebar {
transform: translateX(0);
display: flex; /* Override display: none from previous rule */
}
/* Adjust chat header for mobile */
.chat-header {
padding: 0 8px;
}
} }