diff --git a/android/app/build.gradle b/android/app/build.gradle index 18033c55..7c7a58a9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "olcxja.miarven" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "0.1.0" + versionCode 2 + versionName "0.1.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/src/main/assets/public/blah/en-cat.json b/android/app/src/main/assets/public/blah/en-cat.json index ae56e047..db64989f 100644 --- a/android/app/src/main/assets/public/blah/en-cat.json +++ b/android/app/src/main/assets/public/blah/en-cat.json @@ -290,5 +290,24 @@ "message.delete.failed": "Failed to delete meow", "message.react.failed": "Failed to react to meow", "message.send.failed": "Failed to send meow", - "owner.transferred": "Purrship transferred successfully" -} \ No newline at end of file + "owner.transferred": "Purrship transferred successfully", + "title.voice.chat": "voice chat :3", + "status.voice.connecting": "connecting... :3", + "status.voice.disconnected": "disconnected :3", + "action.call.mute": "mute microphone", + "action.call.camera": "camera", + "action.call.screen": "share screen", + "action.call.disconnect": "disconnect :3", + "action.call.join": "join :3", + "action.call.decline": "decline :c", + "title.incoming.call": "incoming voice chat :3", + "desc.call.in.chat": "{0} in chat :3", + "label.user.volume": "user volume", + "label.stream.volume": "screen volume", + "error.call.unsupported": "no permission or not supported: {0} :c", + "error.call.camera": "camera error: {0} :c", + "error.call.screen": "screen share error: {0} :c", + "error.call.screen.unsupported": "ur browser doesn't support screen sharing :c", + "label.stream.cam": "cam", + "label.stream.screen": "screen" +} diff --git a/android/app/src/main/assets/public/blah/en-us.json b/android/app/src/main/assets/public/blah/en-us.json index 42d98cb7..ee67f52d 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -299,5 +299,24 @@ "message.delete.failed": "Failed to delete message", "message.react.failed": "Failed to react to message", "message.send.failed": "Failed to send message", - "owner.transferred": "Ownership transferred successfully" -} \ No newline at end of file + "owner.transferred": "Ownership transferred successfully", + "title.voice.chat": "Voice Chat", + "status.voice.connecting": "Connecting...", + "status.voice.disconnected": "Disconnected", + "action.call.mute": "Mute Microphone", + "action.call.camera": "Camera", + "action.call.screen": "Share Screen", + "action.call.disconnect": "Disconnect", + "action.call.join": "Join", + "action.call.decline": "Decline", + "title.incoming.call": "Incoming Voice Chat", + "desc.call.in.chat": "{0} in chat", + "label.user.volume": "User Volume", + "label.stream.volume": "Screen Volume", + "error.call.unsupported": "No permission or feature not supported: {0}", + "error.call.camera": "Camera error: {0}", + "error.call.screen": "Screen share error: {0}", + "error.call.screen.unsupported": "Screen sharing is not supported on this browser.", + "label.stream.cam": "Cam", + "label.stream.screen": "Screen" +} diff --git a/android/app/src/main/assets/public/blah/pl-pl.json b/android/app/src/main/assets/public/blah/pl-pl.json index d1fea3af..7d6f14ab 100644 --- a/android/app/src/main/assets/public/blah/pl-pl.json +++ b/android/app/src/main/assets/public/blah/pl-pl.json @@ -299,5 +299,24 @@ "message.delete.failed": "Nie udało się usunąć wiadomości", "message.react.failed": "Nie udało się dodać reakcji", "message.send.failed": "Nie udało się wysłać wiadomości", - "owner.transferred": "Własność przekazana pomyślnie" -} \ No newline at end of file + "owner.transferred": "Własność przekazana pomyślnie", + "title.voice.chat": "Czat głosowy", + "status.voice.connecting": "Łączenie...", + "status.voice.disconnected": "Rozłączono", + "action.call.mute": "Wycisz mikrofon", + "action.call.camera": "Kamera", + "action.call.screen": "Udostępnij ekran", + "action.call.disconnect": "Rozłącz", + "action.call.join": "Dołącz", + "action.call.decline": "Odrzuć", + "title.incoming.call": "Połączenie głosowe", + "desc.call.in.chat": "{0} w rozmowie", + "label.user.volume": "Głośność użytkownika", + "label.stream.volume": "Głośność ekranu", + "error.call.unsupported": "Brak uprawnień lub funkcja nieobsługiwana: {0}", + "error.call.camera": "Błąd kamery: {0}", + "error.call.screen": "Błąd udostępniania ekranu: {0}", + "error.call.screen.unsupported": "Udostępnianie ekranu nie jest obsługiwane w tej przeglądarce.", + "label.stream.cam": "Kamera", + "label.stream.screen": "Ekran" +} diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index ecd51413..f514589c 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -1461,8 +1461,8 @@ function showFixedContextMenu(rect, html) { fixedContextMenu.style.transition = 'none'; fixedContextMenu.style.opacity = '0'; - fixedContextMenu.style.left = '0px'; - fixedContextMenu.style.top = '0px'; + fixedContextMenu.style.left = '0rem'; + fixedContextMenu.style.top = '0rem'; fixedContextMenu.classList.add("show"); @@ -3139,7 +3139,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { let safeType = encodeURIComponent(att.type).replace(/'/g, "%27"); let safeName = encodeURIComponent(att.name).replace(/'/g, "%27"); - attachmentsHtml += `
+ attachmentsHtml += `
${iconSvg}
${escapeHtml(displayName)}
@@ -3653,12 +3653,12 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name, container.dataset.url = blobUrl; if (type.startsWith("image/")) { - container.innerHTML = ``; + container.innerHTML = ``; } else if (type.startsWith("video/")) { - container.innerHTML = ``; + container.innerHTML = ``; } else { container.innerHTML = originalHtml; - container.style.border = "1px solid var(--accent-color)"; + container.style.border = "0.0625rem solid var(--accent-color)"; let a = document.createElement("a"); a.href = blobUrl; a.download = name; @@ -3716,7 +3716,7 @@ function renderAttachmentsPreview() { } html += ` -
+
${iconHtml}
${escapeHtml(file.name)}
-
`; document.body.appendChild(incomingUi); + requestAnimationFrame(() => { + requestAnimationFrame(() => incomingUi.classList.add("show")); + }); + + resolveCallerInfo(roomId).then(({name, pfpUrl}) => { + if (!document.body.contains(incomingUi)) return; + let nameEl = incomingUi.querySelector(".incoming-call-name"); + let avatarEl = incomingUi.querySelector(".incoming-call-avatar"); + if (nameEl) nameEl.innerText = name; + if (avatarEl && pfpUrl) avatarEl.src = pfpUrl; + }); +} + +function dismissIncomingCall() { + let el = document.getElementById("incoming-call-popup"); + if (!el || el.classList.contains("hide")) return; + el.classList.remove("show"); + el.classList.add("hide"); + setTimeout(() => el.remove(), 300); +} + +function joinIncomingCall(roomId) { + dismissIncomingCall(); + initiateVoiceCall(roomId); } async function sendVoiceSignalToUser(targetUserId, roomId) { @@ -503,6 +662,39 @@ async function handleRoomSignal(data) { } } +async function resolveVoiceChatTitle() { + try { + if (currentVoiceCallType === 'groups') { + if (!currentVoiceCallId || !window.fetchEncrypted) return null; + let infoPayload = await fetchEncrypted("group/info", currentVoiceCallId); + if (infoPayload && infoPayload.startsWith("{")) { + return JSON.parse(infoPayload).string2 || null; + } + return null; + } + if (!currentVoiceCallId) return null; + let parts = String(currentVoiceCallId).split('_'); + let myId = `${window.id};${window.host}`; + let targetId = parts[0] === myId ? parts[1] : parts[0]; + if (!targetId) return null; + let name = window.dmUsernameCache ? window.dmUsernameCache[targetId] : null; + if (!name && window.fetchAsync) { + name = await window.fetchAsync(`${window.url}/idtoname?id=${targetId}`); + if (name && !name.startsWith("error:") && window.dmUsernameCache) window.dmUsernameCache[targetId] = name; + } + if (!name || name.startsWith("error:")) return null; + return name.split(':')[0]; + } catch (e) { + return null; + } +} + +async function updateVoiceChatTitle() { + let name = await resolveVoiceChatTitle(); + let el = document.querySelector("#voice-header .voice-title span"); + if (el && name) el.innerText = name; +} + function showVoiceUI() { if (voiceUiContainer) return; @@ -510,40 +702,49 @@ function showVoiceUI() { voiceUiContainer.id = "voice-call-popup"; voiceUiContainer.innerHTML = ` -
-
- Voice Chat +
+
+ ${ICONS.call({width: "1.1rem", height: "1.1rem"})} + ${processBlah("title.voice.chat")} +
+
+ + + ${processBlah("status.voice.connecting")}
-
Connecting...
-
-
+
+
`; - + document.body.appendChild(voiceUiContainer); + requestAnimationFrame(() => { + requestAnimationFrame(() => voiceUiContainer.classList.add("show")); + }); let header = document.getElementById("voice-header"); - let isDragging = false; let startX, startY, initialLeft, initialTop; - - header.addEventListener('mousedown', (e) => { - isDragging = true; + + header.addEventListener('pointerdown', (e) => { + if (e.button !== undefined && e.button !== 0) return; + voiceDragState = true; + try { header.setPointerCapture(e.pointerId); } catch (err) {} let rect = voiceUiContainer.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; @@ -553,17 +754,149 @@ function showVoiceUI() { voiceUiContainer.style.bottom = "auto"; voiceUiContainer.style.left = initialLeft + "px"; voiceUiContainer.style.top = initialTop + "px"; + clampVoiceWindowToViewport(voiceUiContainer); }); - - window.addEventListener('mousemove', (e) => { - if (!isDragging) return; + + header.addEventListener('pointermove', (e) => { + if (!voiceDragState) return; let dx = e.clientX - startX; let dy = e.clientY - startY; voiceUiContainer.style.left = (initialLeft + dx) + "px"; voiceUiContainer.style.top = (initialTop + dy) + "px"; + clampVoiceWindowToViewport(voiceUiContainer); }); - - window.addEventListener('mouseup', () => { isDragging = false; }); + + let stopDrag = () => { voiceDragState = false; }; + header.addEventListener('pointerup', stopDrag); + header.addEventListener('pointercancel', stopDrag); + + ensureVoiceGridObserver(); + + setupVoiceWindowResize(); +} + +function setupVoiceWindowResize() { + const popup = voiceUiContainer; + if (!popup || popup.dataset.resizeInit) return; + popup.dataset.resizeInit = "1"; + + const minW = () => parseFloat(getComputedStyle(popup).minWidth) || 200; + const minH = () => parseFloat(getComputedStyle(popup).minHeight) || 160; + + const applySize = (l, t, w, h) => { + const W = window.innerWidth, H = window.innerHeight; + w = Math.round(Math.min(Math.max(w, minW()), W)); + h = Math.round(Math.min(Math.max(h, minH()), H)); + l = Math.round(Math.min(Math.max(l, 0), W - w)); + t = Math.round(Math.min(Math.max(t, 0), H - h)); + popup.style.right = "auto"; + popup.style.bottom = "auto"; + popup.style.width = w + "px"; + popup.style.height = h + "px"; + popup.style.left = l + "px"; + popup.style.top = t + "px"; + }; + + let edgeResizing = false; + + ["n", "s", "e", "w", "ne", "nw", "se", "sw"].forEach((dir) => { + const handle = document.createElement("div"); + handle.className = `voice-resize-handle voice-resize-${dir}`; + popup.appendChild(handle); + + handle.addEventListener("pointerdown", (e) => { + if (e.button !== undefined && e.button !== 0) return; + e.stopPropagation(); + e.preventDefault(); + edgeResizing = true; + const sx = e.clientX, sy = e.clientY; + const rect = popup.getBoundingClientRect(); + const sw = rect.width, sh = rect.height, sl = rect.left, st = rect.top; + try { handle.setPointerCapture(e.pointerId); } catch (err) {} + + const onMove = (ev) => { + const dx = ev.clientX - sx, dy = ev.clientY - sy; + let w = sw, h = sh, l = sl, t = st; + if (dir.includes("e")) w = sw + dx; + if (dir.includes("s")) h = sh + dy; + if (dir.includes("w")) { + w = sw - dx; + l = sl + (sw - w); + } + if (dir.includes("n")) { + h = sh - dy; + t = st + (sh - h); + } + applySize(l, t, w, h); + }; + const onUp = () => { + edgeResizing = false; + handle.removeEventListener("pointermove", onMove); + handle.removeEventListener("pointerup", onUp); + handle.removeEventListener("pointercancel", onUp); + }; + handle.addEventListener("pointermove", onMove); + handle.addEventListener("pointerup", onUp); + handle.addEventListener("pointercancel", onUp); + }); + }); + + // pinch-to-resize (dwa palce jak zoom) + const pointers = new Map(); + let pinch = null; + let suppressClick = false; + + popup.addEventListener("pointerdown", (e) => { + pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); + if (pointers.size === 2 && !pinch && !voiceDragState && !edgeResizing) { + const [a, b] = [...pointers.values()]; + const rect = popup.getBoundingClientRect(); + pinch = { + dist: Math.max(20, Math.hypot(a.x - b.x, a.y - b.y)), + w: rect.width, + h: rect.height + }; + suppressClick = true; + } + if (pointers.size >= 2 || pinch) { + try { popup.setPointerCapture(e.pointerId); } catch (err) {} + } + }); + + popup.addEventListener("pointermove", (e) => { + if (!pointers.has(e.pointerId)) return; + pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); + if (pointers.size === 2 && pinch) { + const [a, b] = [...pointers.values()]; + const scale = Math.max(0.05, Math.hypot(a.x - b.x, a.y - b.y)) / pinch.dist; + const rect = popup.getBoundingClientRect(); + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + applySize(cx - pinch.w * scale / 2, cy - pinch.h * scale / 2, pinch.w * scale, pinch.h * scale); + } + }); + + const releasePointer = (e) => { + pointers.delete(e.pointerId); + if (pointers.size < 2) pinch = null; + if (pointers.size === 0) setTimeout(() => { suppressClick = false; }, 250); + }; + popup.addEventListener("pointerup", releasePointer); + popup.addEventListener("pointercancel", releasePointer); + + popup.addEventListener("click", (e) => { + if (suppressClick) { + e.stopPropagation(); + e.preventDefault(); + suppressClick = false; + } + }, true); + + if (voiceResizeAbort) voiceResizeAbort.abort(); + voiceResizeAbort = new AbortController(); + window.addEventListener("resize", () => { + clampVoiceWindowToViewport(popup); + }, { signal: voiceResizeAbort.signal }); } function updateVoiceUIUsers() { @@ -574,9 +907,10 @@ function updateVoiceUIUsers() { let div = document.getElementById(`voice-tile-${u}`); if (!div) { div = document.createElement("div"); - div.className = "voice-tile"; + div.className = "voice-tile tile-enter"; div.id = `voice-tile-${u}`; div.onclick = (e) => showVoiceUserSettings(u, e); + setTimeout(() => div.classList.remove("tile-enter"), 400); div.innerHTML = `
${u}
@@ -596,8 +930,8 @@ function updateVoiceUIUsers() { } }); } - if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; - if (!authorName && u === String(window.id)) authorName = window.username; + let resolvedName = authorName && authorName !== u ? authorName : null; + if (!resolvedName && u === String(window.id) && window.username) resolvedName = window.host ? `${window.username}:${window.host}` : window.username; let avatarRes = window.dmPfpCache ? window.dmPfpCache[u] : null; let avatarUrl = avatarRes ? avatarRes : "assets/default_avatar.png"; @@ -614,7 +948,7 @@ function updateVoiceUIUsers() { div.style.backgroundImage = `url('${avatarUrl.replace(/'/g, "\\'")}')`; let nameEl = document.getElementById(`voice-name-${u}`); - if (nameEl) nameEl.innerText = authorName || u; + if (nameEl) nameEl.innerText = resolvedName ? `${resolvedName} (${u})` : String(u); } Array.from(container.children).forEach(child => { @@ -634,15 +968,13 @@ function updateVoiceUIUsers() { let div = document.getElementById(`voice-tile-stream-${decoderId}`); if (!div) { div = document.createElement("div"); - div.className = "voice-tile"; + div.className = "voice-tile tile-enter"; div.id = `voice-tile-stream-${decoderId}`; + div.onclick = (e) => showVoiceUserSettings(clientId, e, true); + setTimeout(() => div.classList.remove("tile-enter"), 400); let mediaEl = streamData.canvas || streamData.video; if (mediaEl) { - mediaEl.style.width = "100%"; - mediaEl.style.height = "100%"; - mediaEl.style.objectFit = "cover"; - mediaEl.style.background = "#000"; div.appendChild(mediaEl); } @@ -654,10 +986,13 @@ function updateVoiceUIUsers() { } let authorName = window.dmUsernameCache ? window.dmUsernameCache[clientId] : null; - if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; - + if (!authorName && clientId === String(window.id) && window.username) authorName = window.host ? `${window.username}:${window.host}` : window.username; + + let streamLabel = processBlah(type == '1' ? "label.stream.cam" : "label.stream.screen"); + let baseName = authorName && authorName !== clientId ? `${authorName} (${clientId})` : clientId; + let nameEl = document.getElementById(`voice-stream-name-${decoderId}`); - if (nameEl) nameEl.innerText = `${authorName || clientId} (${type == '1' ? 'Cam' : 'Screen'})`; + if (nameEl) nameEl.innerText = `${baseName} · ${streamLabel}`; } Array.from(container.children).forEach(child => { @@ -668,26 +1003,110 @@ function updateVoiceUIUsers() { } } }); + + let orderedTiles = []; + for (let u of voiceActiveUsers) { + let userTile = document.getElementById(`voice-tile-${u}`); + if (userTile) orderedTiles.push(userTile); + for (let decoderId in remoteVideos) { + if (decoderId.split('-')[0] === u) { + let streamTile = document.getElementById(`voice-tile-stream-${decoderId}`); + if (streamTile) orderedTiles.push(streamTile); + } + } + } + for (let tile of orderedTiles) { + container.appendChild(tile); + } + + relayoutVoiceTiles(); } -function showVoiceUserSettings(clientId, e) { +let voiceGridRO = null; + +function ensureVoiceGridObserver() { + let container = document.getElementById("voice-users"); + if (!container || !container.isConnected || container.dataset.roInit) return; + container.dataset.roInit = "1"; + if (!voiceGridRO) { + voiceGridRO = new ResizeObserver(() => relayoutVoiceTiles()); + } + voiceGridRO.observe(container); +} + +function relayoutVoiceTiles() { + let container = document.getElementById("voice-users"); + if (!container || !container.isConnected) return; + + const tiles = Array.from(container.children).filter(el => el.classList.contains("voice-tile")); + const n = tiles.length; + if (!n) return; + + const rootFs = parseFloat(getComputedStyle(document.documentElement).fontSize); + const cs = getComputedStyle(container); + const innerW = container.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight); + const innerH = container.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom); + const minCol = 6.25 * rootFs; + const minRow = 7 * rootFs; + const gap = 0.75 * rootFs; + + const maxCols = Math.max(1, Math.floor((innerW + gap) / (minCol + gap))); + const cols = Math.min(n, maxCols); + const rows = Math.ceil(n / cols); + const w = Math.floor((innerW - gap * (cols - 1)) / cols); + const h = Math.floor(Math.max(minRow, (innerH - gap * (rows - 1)) / rows)); + + const sig = `${n}:${cols}:${w}x${h}`; + if (container.dataset.layoutSig === sig) return; + container.dataset.layoutSig = sig; + + container.style.setProperty("--voice-tile-w", w + "px"); + container.style.setProperty("--voice-tile-h", h + "px"); +} + +let voiceSettingsState = null; + +function hideVoiceUserSettings() { + let popup = document.getElementById("voice-user-settings"); + if (popup) popup.remove(); + voiceSettingsState = null; + document.removeEventListener('click', closeVoiceUserSettingsOnOutsideClick); +} + +function closeVoiceUserSettingsOnOutsideClick(ev) { + let popup = document.getElementById("voice-user-settings"); + if (!popup) { + hideVoiceUserSettings(); + return; + } + if (!popup.contains(ev.target)) hideVoiceUserSettings(); +} + +function showVoiceUserSettings(clientId, e, isStream = false) { e.stopPropagation(); - let existing = document.getElementById("voice-user-settings"); - if (existing) existing.remove(); - + + if (voiceSettingsState && voiceSettingsState.clientId === clientId && voiceSettingsState.isStream === isStream) { + hideVoiceUserSettings(); + return; + } + hideVoiceUserSettings(); + + voiceSettingsState = { clientId, isStream }; + let popup = document.createElement("div"); popup.id = "voice-user-settings"; - popup.innerHTML = ` + popup.innerHTML = isStream ? `
- - + +
+ ` : `
- - + +
`; - + document.body.appendChild(popup); let rect = popup.getBoundingClientRect(); let left = e.clientX; @@ -696,17 +1115,15 @@ function showVoiceUserSettings(clientId, e) { if (top + rect.height > window.innerHeight) top = window.innerHeight - rect.height - 10; popup.style.left = left + "px"; popup.style.top = top + "px"; - - const closePopup = (ev) => { - if (!popup.contains(ev.target)) { - popup.remove(); - document.removeEventListener('mousedown', closePopup); - } - }; - setTimeout(() => document.addEventListener('mousedown', closePopup), 10); + requestAnimationFrame(() => { + requestAnimationFrame(() => popup.classList.add("open")); + }); + + document.addEventListener('click', closeVoiceUserSettingsOnOutsideClick); } window.updateVoiceVol = function(clientId, val, type) { + voiceVolumeState[`${type}:${clientId}`] = val; let el = document.getElementById(`val-${type}-${clientId}`); if (el) el.innerText = val + '%'; const audioId = type === 'screen' ? `${clientId}_screen` : clientId; @@ -718,6 +1135,7 @@ window.updateVoiceVol = function(clientId, val, type) { function toggleVoiceMic() { isMicMuted = !isMicMuted; + if (isMicMuted) setTileSpeaking(String(window.id), false); let btn = document.getElementById("voice-btn-mic"); if (btn) { if (isMicMuted) { @@ -729,6 +1147,8 @@ function toggleVoiceMic() { } function endVoiceSession() { + setVoiceStatus("disconnected"); + if (currentVoiceCallId) { let payload = JSON.stringify({ string1: currentVoiceCallType, string2: currentVoiceCallId }); if (window.fetchEncrypted) window.fetchEncrypted("room/voice/leave", payload).catch(()=>{}); @@ -779,9 +1199,24 @@ function endVoiceSession() { currentVoiceCallType = null; currentVoiceCallId = null; + clearAllSpeaking(); + + if (voiceResizeAbort) { + voiceResizeAbort.abort(); + voiceResizeAbort = null; + } + + if (voiceGridRO) { + voiceGridRO.disconnect(); + voiceGridRO = null; + } + if (voiceUiContainer) { - voiceUiContainer.remove(); + let popupEl = voiceUiContainer; voiceUiContainer = null; + popupEl.classList.remove("show"); + popupEl.classList.add("hide"); + setTimeout(() => popupEl.remove(), 350); } } @@ -895,7 +1330,7 @@ async function startHardwareVideo(btnEl, streamPromise, typeLabel, typeId) { } else if (typeId === 1) { camControl = null; } }; return { stop: stopFunction }; - } catch (e) { console.error(e); alert(`No permission or feature not supported: ${typeLabel}`); return null; } + } catch (e) { console.error(e); alert(processBlah(`:error.call.unsupported:${typeLabel}`)); return null; } } function toggleVoiceCam() { @@ -905,7 +1340,7 @@ function toggleVoiceCam() { } else { navigator.mediaDevices.getUserMedia({video: {width: {ideal: 1280}, height: {ideal: 720}}}).then(stream => { startHardwareVideo(btn, Promise.resolve(stream), "Camera", 1).then(ctrl => camControl = ctrl); - }).catch(err => alert("Camera error: " + err.message)); + }).catch(err => alert(processBlah(`:error.call.camera:${err.message}`))); } } @@ -914,11 +1349,11 @@ function toggleVoiceScreen() { if (screenControl) { screenControl.stop(); } else { - if (!navigator.mediaDevices.getDisplayMedia) { alert("Screen sharing is not supported on this browser."); return; } + if (!navigator.mediaDevices.getDisplayMedia) { alert(processBlah("error.call.screen.unsupported")); return; } navigator.mediaDevices.getDisplayMedia({ video: true, audio: { autoGainControl: false, echoCancellation: false, noiseSuppression: false }, systemAudio: "include", selfBrowserSurface: "exclude" }).then(stream => { startHardwareVideo(btn, Promise.resolve(stream), "Screen", 2).then(ctrl => screenControl = ctrl); - }).catch(err => alert("Screen error: " + err.message)); + }).catch(err => alert(processBlah(`:error.call.screen:${err.message}`))); } } diff --git a/electron/assets/icon.png b/electron/assets/icon.png index b50fbd13..a63fdef2 100644 Binary files a/electron/assets/icon.png and b/electron/assets/icon.png differ diff --git a/electron/package.json b/electron/package.json index ee95df90..d07682a5 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "Miarven", - "version": "0.1.0", + "version": "0.1.1", "description": "An Amazing Capacitor App", "author": { "name": "", diff --git a/icons/icon.png b/icons/icon.png index b50fbd13..a63fdef2 100644 Binary files a/icons/icon.png and b/icons/icon.png differ diff --git a/package.json b/package.json index c8778cd2..02b0c6a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "olcxja.miarven", - "version": "0.1.0", + "version": "0.1.1", "description": "Larpix client", "license": "AGPL-3.0", "author": "olcxja's softworks", diff --git a/version.json b/version.json index 931d63ab..863e6940 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "versionCode": 1, - "versionName": "0.1.0", - "npmVersion": "0.1.0" + "versionCode": 2, + "versionName": "0.1.1", + "npmVersion": "0.1.1" } diff --git a/webroot/blah/en-cat.json b/webroot/blah/en-cat.json index ae56e047..db64989f 100644 --- a/webroot/blah/en-cat.json +++ b/webroot/blah/en-cat.json @@ -290,5 +290,24 @@ "message.delete.failed": "Failed to delete meow", "message.react.failed": "Failed to react to meow", "message.send.failed": "Failed to send meow", - "owner.transferred": "Purrship transferred successfully" -} \ No newline at end of file + "owner.transferred": "Purrship transferred successfully", + "title.voice.chat": "voice chat :3", + "status.voice.connecting": "connecting... :3", + "status.voice.disconnected": "disconnected :3", + "action.call.mute": "mute microphone", + "action.call.camera": "camera", + "action.call.screen": "share screen", + "action.call.disconnect": "disconnect :3", + "action.call.join": "join :3", + "action.call.decline": "decline :c", + "title.incoming.call": "incoming voice chat :3", + "desc.call.in.chat": "{0} in chat :3", + "label.user.volume": "user volume", + "label.stream.volume": "screen volume", + "error.call.unsupported": "no permission or not supported: {0} :c", + "error.call.camera": "camera error: {0} :c", + "error.call.screen": "screen share error: {0} :c", + "error.call.screen.unsupported": "ur browser doesn't support screen sharing :c", + "label.stream.cam": "cam", + "label.stream.screen": "screen" +} diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json index 42d98cb7..ee67f52d 100644 --- a/webroot/blah/en-us.json +++ b/webroot/blah/en-us.json @@ -299,5 +299,24 @@ "message.delete.failed": "Failed to delete message", "message.react.failed": "Failed to react to message", "message.send.failed": "Failed to send message", - "owner.transferred": "Ownership transferred successfully" -} \ No newline at end of file + "owner.transferred": "Ownership transferred successfully", + "title.voice.chat": "Voice Chat", + "status.voice.connecting": "Connecting...", + "status.voice.disconnected": "Disconnected", + "action.call.mute": "Mute Microphone", + "action.call.camera": "Camera", + "action.call.screen": "Share Screen", + "action.call.disconnect": "Disconnect", + "action.call.join": "Join", + "action.call.decline": "Decline", + "title.incoming.call": "Incoming Voice Chat", + "desc.call.in.chat": "{0} in chat", + "label.user.volume": "User Volume", + "label.stream.volume": "Screen Volume", + "error.call.unsupported": "No permission or feature not supported: {0}", + "error.call.camera": "Camera error: {0}", + "error.call.screen": "Screen share error: {0}", + "error.call.screen.unsupported": "Screen sharing is not supported on this browser.", + "label.stream.cam": "Cam", + "label.stream.screen": "Screen" +} diff --git a/webroot/blah/pl-pl.json b/webroot/blah/pl-pl.json index d1fea3af..7d6f14ab 100644 --- a/webroot/blah/pl-pl.json +++ b/webroot/blah/pl-pl.json @@ -299,5 +299,24 @@ "message.delete.failed": "Nie udało się usunąć wiadomości", "message.react.failed": "Nie udało się dodać reakcji", "message.send.failed": "Nie udało się wysłać wiadomości", - "owner.transferred": "Własność przekazana pomyślnie" -} \ No newline at end of file + "owner.transferred": "Własność przekazana pomyślnie", + "title.voice.chat": "Czat głosowy", + "status.voice.connecting": "Łączenie...", + "status.voice.disconnected": "Rozłączono", + "action.call.mute": "Wycisz mikrofon", + "action.call.camera": "Kamera", + "action.call.screen": "Udostępnij ekran", + "action.call.disconnect": "Rozłącz", + "action.call.join": "Dołącz", + "action.call.decline": "Odrzuć", + "title.incoming.call": "Połączenie głosowe", + "desc.call.in.chat": "{0} w rozmowie", + "label.user.volume": "Głośność użytkownika", + "label.stream.volume": "Głośność ekranu", + "error.call.unsupported": "Brak uprawnień lub funkcja nieobsługiwana: {0}", + "error.call.camera": "Błąd kamery: {0}", + "error.call.screen": "Błąd udostępniania ekranu: {0}", + "error.call.screen.unsupported": "Udostępnianie ekranu nie jest obsługiwane w tej przeglądarce.", + "label.stream.cam": "Kamera", + "label.stream.screen": "Ekran" +} diff --git a/webroot/main.js b/webroot/main.js index ecd51413..f514589c 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -1461,8 +1461,8 @@ function showFixedContextMenu(rect, html) { fixedContextMenu.style.transition = 'none'; fixedContextMenu.style.opacity = '0'; - fixedContextMenu.style.left = '0px'; - fixedContextMenu.style.top = '0px'; + fixedContextMenu.style.left = '0rem'; + fixedContextMenu.style.top = '0rem'; fixedContextMenu.classList.add("show"); @@ -3139,7 +3139,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { let safeType = encodeURIComponent(att.type).replace(/'/g, "%27"); let safeName = encodeURIComponent(att.name).replace(/'/g, "%27"); - attachmentsHtml += `
+ attachmentsHtml += `
${iconSvg}
${escapeHtml(displayName)}
@@ -3653,12 +3653,12 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name, container.dataset.url = blobUrl; if (type.startsWith("image/")) { - container.innerHTML = ``; + container.innerHTML = ``; } else if (type.startsWith("video/")) { - container.innerHTML = ``; + container.innerHTML = ``; } else { container.innerHTML = originalHtml; - container.style.border = "1px solid var(--accent-color)"; + container.style.border = "0.0625rem solid var(--accent-color)"; let a = document.createElement("a"); a.href = blobUrl; a.download = name; @@ -3716,7 +3716,7 @@ function renderAttachmentsPreview() { } html += ` -
+
${iconHtml}
${escapeHtml(file.name)}
-
`; document.body.appendChild(incomingUi); + requestAnimationFrame(() => { + requestAnimationFrame(() => incomingUi.classList.add("show")); + }); + + resolveCallerInfo(roomId).then(({name, pfpUrl}) => { + if (!document.body.contains(incomingUi)) return; + let nameEl = incomingUi.querySelector(".incoming-call-name"); + let avatarEl = incomingUi.querySelector(".incoming-call-avatar"); + if (nameEl) nameEl.innerText = name; + if (avatarEl && pfpUrl) avatarEl.src = pfpUrl; + }); +} + +function dismissIncomingCall() { + let el = document.getElementById("incoming-call-popup"); + if (!el || el.classList.contains("hide")) return; + el.classList.remove("show"); + el.classList.add("hide"); + setTimeout(() => el.remove(), 300); +} + +function joinIncomingCall(roomId) { + dismissIncomingCall(); + initiateVoiceCall(roomId); } async function sendVoiceSignalToUser(targetUserId, roomId) { @@ -503,6 +662,39 @@ async function handleRoomSignal(data) { } } +async function resolveVoiceChatTitle() { + try { + if (currentVoiceCallType === 'groups') { + if (!currentVoiceCallId || !window.fetchEncrypted) return null; + let infoPayload = await fetchEncrypted("group/info", currentVoiceCallId); + if (infoPayload && infoPayload.startsWith("{")) { + return JSON.parse(infoPayload).string2 || null; + } + return null; + } + if (!currentVoiceCallId) return null; + let parts = String(currentVoiceCallId).split('_'); + let myId = `${window.id};${window.host}`; + let targetId = parts[0] === myId ? parts[1] : parts[0]; + if (!targetId) return null; + let name = window.dmUsernameCache ? window.dmUsernameCache[targetId] : null; + if (!name && window.fetchAsync) { + name = await window.fetchAsync(`${window.url}/idtoname?id=${targetId}`); + if (name && !name.startsWith("error:") && window.dmUsernameCache) window.dmUsernameCache[targetId] = name; + } + if (!name || name.startsWith("error:")) return null; + return name.split(':')[0]; + } catch (e) { + return null; + } +} + +async function updateVoiceChatTitle() { + let name = await resolveVoiceChatTitle(); + let el = document.querySelector("#voice-header .voice-title span"); + if (el && name) el.innerText = name; +} + function showVoiceUI() { if (voiceUiContainer) return; @@ -510,40 +702,49 @@ function showVoiceUI() { voiceUiContainer.id = "voice-call-popup"; voiceUiContainer.innerHTML = ` -
-
- Voice Chat +
+
+ ${ICONS.call({width: "1.1rem", height: "1.1rem"})} + ${processBlah("title.voice.chat")} +
+
+ + + ${processBlah("status.voice.connecting")}
-
Connecting...
-
-
+
+
`; - + document.body.appendChild(voiceUiContainer); + requestAnimationFrame(() => { + requestAnimationFrame(() => voiceUiContainer.classList.add("show")); + }); let header = document.getElementById("voice-header"); - let isDragging = false; let startX, startY, initialLeft, initialTop; - - header.addEventListener('mousedown', (e) => { - isDragging = true; + + header.addEventListener('pointerdown', (e) => { + if (e.button !== undefined && e.button !== 0) return; + voiceDragState = true; + try { header.setPointerCapture(e.pointerId); } catch (err) {} let rect = voiceUiContainer.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; @@ -553,17 +754,149 @@ function showVoiceUI() { voiceUiContainer.style.bottom = "auto"; voiceUiContainer.style.left = initialLeft + "px"; voiceUiContainer.style.top = initialTop + "px"; + clampVoiceWindowToViewport(voiceUiContainer); }); - - window.addEventListener('mousemove', (e) => { - if (!isDragging) return; + + header.addEventListener('pointermove', (e) => { + if (!voiceDragState) return; let dx = e.clientX - startX; let dy = e.clientY - startY; voiceUiContainer.style.left = (initialLeft + dx) + "px"; voiceUiContainer.style.top = (initialTop + dy) + "px"; + clampVoiceWindowToViewport(voiceUiContainer); }); - - window.addEventListener('mouseup', () => { isDragging = false; }); + + let stopDrag = () => { voiceDragState = false; }; + header.addEventListener('pointerup', stopDrag); + header.addEventListener('pointercancel', stopDrag); + + ensureVoiceGridObserver(); + + setupVoiceWindowResize(); +} + +function setupVoiceWindowResize() { + const popup = voiceUiContainer; + if (!popup || popup.dataset.resizeInit) return; + popup.dataset.resizeInit = "1"; + + const minW = () => parseFloat(getComputedStyle(popup).minWidth) || 200; + const minH = () => parseFloat(getComputedStyle(popup).minHeight) || 160; + + const applySize = (l, t, w, h) => { + const W = window.innerWidth, H = window.innerHeight; + w = Math.round(Math.min(Math.max(w, minW()), W)); + h = Math.round(Math.min(Math.max(h, minH()), H)); + l = Math.round(Math.min(Math.max(l, 0), W - w)); + t = Math.round(Math.min(Math.max(t, 0), H - h)); + popup.style.right = "auto"; + popup.style.bottom = "auto"; + popup.style.width = w + "px"; + popup.style.height = h + "px"; + popup.style.left = l + "px"; + popup.style.top = t + "px"; + }; + + let edgeResizing = false; + + ["n", "s", "e", "w", "ne", "nw", "se", "sw"].forEach((dir) => { + const handle = document.createElement("div"); + handle.className = `voice-resize-handle voice-resize-${dir}`; + popup.appendChild(handle); + + handle.addEventListener("pointerdown", (e) => { + if (e.button !== undefined && e.button !== 0) return; + e.stopPropagation(); + e.preventDefault(); + edgeResizing = true; + const sx = e.clientX, sy = e.clientY; + const rect = popup.getBoundingClientRect(); + const sw = rect.width, sh = rect.height, sl = rect.left, st = rect.top; + try { handle.setPointerCapture(e.pointerId); } catch (err) {} + + const onMove = (ev) => { + const dx = ev.clientX - sx, dy = ev.clientY - sy; + let w = sw, h = sh, l = sl, t = st; + if (dir.includes("e")) w = sw + dx; + if (dir.includes("s")) h = sh + dy; + if (dir.includes("w")) { + w = sw - dx; + l = sl + (sw - w); + } + if (dir.includes("n")) { + h = sh - dy; + t = st + (sh - h); + } + applySize(l, t, w, h); + }; + const onUp = () => { + edgeResizing = false; + handle.removeEventListener("pointermove", onMove); + handle.removeEventListener("pointerup", onUp); + handle.removeEventListener("pointercancel", onUp); + }; + handle.addEventListener("pointermove", onMove); + handle.addEventListener("pointerup", onUp); + handle.addEventListener("pointercancel", onUp); + }); + }); + + // pinch-to-resize (dwa palce jak zoom) + const pointers = new Map(); + let pinch = null; + let suppressClick = false; + + popup.addEventListener("pointerdown", (e) => { + pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); + if (pointers.size === 2 && !pinch && !voiceDragState && !edgeResizing) { + const [a, b] = [...pointers.values()]; + const rect = popup.getBoundingClientRect(); + pinch = { + dist: Math.max(20, Math.hypot(a.x - b.x, a.y - b.y)), + w: rect.width, + h: rect.height + }; + suppressClick = true; + } + if (pointers.size >= 2 || pinch) { + try { popup.setPointerCapture(e.pointerId); } catch (err) {} + } + }); + + popup.addEventListener("pointermove", (e) => { + if (!pointers.has(e.pointerId)) return; + pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); + if (pointers.size === 2 && pinch) { + const [a, b] = [...pointers.values()]; + const scale = Math.max(0.05, Math.hypot(a.x - b.x, a.y - b.y)) / pinch.dist; + const rect = popup.getBoundingClientRect(); + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + applySize(cx - pinch.w * scale / 2, cy - pinch.h * scale / 2, pinch.w * scale, pinch.h * scale); + } + }); + + const releasePointer = (e) => { + pointers.delete(e.pointerId); + if (pointers.size < 2) pinch = null; + if (pointers.size === 0) setTimeout(() => { suppressClick = false; }, 250); + }; + popup.addEventListener("pointerup", releasePointer); + popup.addEventListener("pointercancel", releasePointer); + + popup.addEventListener("click", (e) => { + if (suppressClick) { + e.stopPropagation(); + e.preventDefault(); + suppressClick = false; + } + }, true); + + if (voiceResizeAbort) voiceResizeAbort.abort(); + voiceResizeAbort = new AbortController(); + window.addEventListener("resize", () => { + clampVoiceWindowToViewport(popup); + }, { signal: voiceResizeAbort.signal }); } function updateVoiceUIUsers() { @@ -574,9 +907,10 @@ function updateVoiceUIUsers() { let div = document.getElementById(`voice-tile-${u}`); if (!div) { div = document.createElement("div"); - div.className = "voice-tile"; + div.className = "voice-tile tile-enter"; div.id = `voice-tile-${u}`; div.onclick = (e) => showVoiceUserSettings(u, e); + setTimeout(() => div.classList.remove("tile-enter"), 400); div.innerHTML = `
${u}
@@ -596,8 +930,8 @@ function updateVoiceUIUsers() { } }); } - if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; - if (!authorName && u === String(window.id)) authorName = window.username; + let resolvedName = authorName && authorName !== u ? authorName : null; + if (!resolvedName && u === String(window.id) && window.username) resolvedName = window.host ? `${window.username}:${window.host}` : window.username; let avatarRes = window.dmPfpCache ? window.dmPfpCache[u] : null; let avatarUrl = avatarRes ? avatarRes : "assets/default_avatar.png"; @@ -614,7 +948,7 @@ function updateVoiceUIUsers() { div.style.backgroundImage = `url('${avatarUrl.replace(/'/g, "\\'")}')`; let nameEl = document.getElementById(`voice-name-${u}`); - if (nameEl) nameEl.innerText = authorName || u; + if (nameEl) nameEl.innerText = resolvedName ? `${resolvedName} (${u})` : String(u); } Array.from(container.children).forEach(child => { @@ -634,15 +968,13 @@ function updateVoiceUIUsers() { let div = document.getElementById(`voice-tile-stream-${decoderId}`); if (!div) { div = document.createElement("div"); - div.className = "voice-tile"; + div.className = "voice-tile tile-enter"; div.id = `voice-tile-stream-${decoderId}`; + div.onclick = (e) => showVoiceUserSettings(clientId, e, true); + setTimeout(() => div.classList.remove("tile-enter"), 400); let mediaEl = streamData.canvas || streamData.video; if (mediaEl) { - mediaEl.style.width = "100%"; - mediaEl.style.height = "100%"; - mediaEl.style.objectFit = "cover"; - mediaEl.style.background = "#000"; div.appendChild(mediaEl); } @@ -654,10 +986,13 @@ function updateVoiceUIUsers() { } let authorName = window.dmUsernameCache ? window.dmUsernameCache[clientId] : null; - if (authorName && authorName.includes(':')) authorName = authorName.split(':')[0]; - + if (!authorName && clientId === String(window.id) && window.username) authorName = window.host ? `${window.username}:${window.host}` : window.username; + + let streamLabel = processBlah(type == '1' ? "label.stream.cam" : "label.stream.screen"); + let baseName = authorName && authorName !== clientId ? `${authorName} (${clientId})` : clientId; + let nameEl = document.getElementById(`voice-stream-name-${decoderId}`); - if (nameEl) nameEl.innerText = `${authorName || clientId} (${type == '1' ? 'Cam' : 'Screen'})`; + if (nameEl) nameEl.innerText = `${baseName} · ${streamLabel}`; } Array.from(container.children).forEach(child => { @@ -668,26 +1003,110 @@ function updateVoiceUIUsers() { } } }); + + let orderedTiles = []; + for (let u of voiceActiveUsers) { + let userTile = document.getElementById(`voice-tile-${u}`); + if (userTile) orderedTiles.push(userTile); + for (let decoderId in remoteVideos) { + if (decoderId.split('-')[0] === u) { + let streamTile = document.getElementById(`voice-tile-stream-${decoderId}`); + if (streamTile) orderedTiles.push(streamTile); + } + } + } + for (let tile of orderedTiles) { + container.appendChild(tile); + } + + relayoutVoiceTiles(); } -function showVoiceUserSettings(clientId, e) { +let voiceGridRO = null; + +function ensureVoiceGridObserver() { + let container = document.getElementById("voice-users"); + if (!container || !container.isConnected || container.dataset.roInit) return; + container.dataset.roInit = "1"; + if (!voiceGridRO) { + voiceGridRO = new ResizeObserver(() => relayoutVoiceTiles()); + } + voiceGridRO.observe(container); +} + +function relayoutVoiceTiles() { + let container = document.getElementById("voice-users"); + if (!container || !container.isConnected) return; + + const tiles = Array.from(container.children).filter(el => el.classList.contains("voice-tile")); + const n = tiles.length; + if (!n) return; + + const rootFs = parseFloat(getComputedStyle(document.documentElement).fontSize); + const cs = getComputedStyle(container); + const innerW = container.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight); + const innerH = container.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom); + const minCol = 6.25 * rootFs; + const minRow = 7 * rootFs; + const gap = 0.75 * rootFs; + + const maxCols = Math.max(1, Math.floor((innerW + gap) / (minCol + gap))); + const cols = Math.min(n, maxCols); + const rows = Math.ceil(n / cols); + const w = Math.floor((innerW - gap * (cols - 1)) / cols); + const h = Math.floor(Math.max(minRow, (innerH - gap * (rows - 1)) / rows)); + + const sig = `${n}:${cols}:${w}x${h}`; + if (container.dataset.layoutSig === sig) return; + container.dataset.layoutSig = sig; + + container.style.setProperty("--voice-tile-w", w + "px"); + container.style.setProperty("--voice-tile-h", h + "px"); +} + +let voiceSettingsState = null; + +function hideVoiceUserSettings() { + let popup = document.getElementById("voice-user-settings"); + if (popup) popup.remove(); + voiceSettingsState = null; + document.removeEventListener('click', closeVoiceUserSettingsOnOutsideClick); +} + +function closeVoiceUserSettingsOnOutsideClick(ev) { + let popup = document.getElementById("voice-user-settings"); + if (!popup) { + hideVoiceUserSettings(); + return; + } + if (!popup.contains(ev.target)) hideVoiceUserSettings(); +} + +function showVoiceUserSettings(clientId, e, isStream = false) { e.stopPropagation(); - let existing = document.getElementById("voice-user-settings"); - if (existing) existing.remove(); - + + if (voiceSettingsState && voiceSettingsState.clientId === clientId && voiceSettingsState.isStream === isStream) { + hideVoiceUserSettings(); + return; + } + hideVoiceUserSettings(); + + voiceSettingsState = { clientId, isStream }; + let popup = document.createElement("div"); popup.id = "voice-user-settings"; - popup.innerHTML = ` + popup.innerHTML = isStream ? `
- - + +
+ ` : `
- - + +
`; - + document.body.appendChild(popup); let rect = popup.getBoundingClientRect(); let left = e.clientX; @@ -696,17 +1115,15 @@ function showVoiceUserSettings(clientId, e) { if (top + rect.height > window.innerHeight) top = window.innerHeight - rect.height - 10; popup.style.left = left + "px"; popup.style.top = top + "px"; - - const closePopup = (ev) => { - if (!popup.contains(ev.target)) { - popup.remove(); - document.removeEventListener('mousedown', closePopup); - } - }; - setTimeout(() => document.addEventListener('mousedown', closePopup), 10); + requestAnimationFrame(() => { + requestAnimationFrame(() => popup.classList.add("open")); + }); + + document.addEventListener('click', closeVoiceUserSettingsOnOutsideClick); } window.updateVoiceVol = function(clientId, val, type) { + voiceVolumeState[`${type}:${clientId}`] = val; let el = document.getElementById(`val-${type}-${clientId}`); if (el) el.innerText = val + '%'; const audioId = type === 'screen' ? `${clientId}_screen` : clientId; @@ -718,6 +1135,7 @@ window.updateVoiceVol = function(clientId, val, type) { function toggleVoiceMic() { isMicMuted = !isMicMuted; + if (isMicMuted) setTileSpeaking(String(window.id), false); let btn = document.getElementById("voice-btn-mic"); if (btn) { if (isMicMuted) { @@ -729,6 +1147,8 @@ function toggleVoiceMic() { } function endVoiceSession() { + setVoiceStatus("disconnected"); + if (currentVoiceCallId) { let payload = JSON.stringify({ string1: currentVoiceCallType, string2: currentVoiceCallId }); if (window.fetchEncrypted) window.fetchEncrypted("room/voice/leave", payload).catch(()=>{}); @@ -779,9 +1199,24 @@ function endVoiceSession() { currentVoiceCallType = null; currentVoiceCallId = null; + clearAllSpeaking(); + + if (voiceResizeAbort) { + voiceResizeAbort.abort(); + voiceResizeAbort = null; + } + + if (voiceGridRO) { + voiceGridRO.disconnect(); + voiceGridRO = null; + } + if (voiceUiContainer) { - voiceUiContainer.remove(); + let popupEl = voiceUiContainer; voiceUiContainer = null; + popupEl.classList.remove("show"); + popupEl.classList.add("hide"); + setTimeout(() => popupEl.remove(), 350); } } @@ -895,7 +1330,7 @@ async function startHardwareVideo(btnEl, streamPromise, typeLabel, typeId) { } else if (typeId === 1) { camControl = null; } }; return { stop: stopFunction }; - } catch (e) { console.error(e); alert(`No permission or feature not supported: ${typeLabel}`); return null; } + } catch (e) { console.error(e); alert(processBlah(`:error.call.unsupported:${typeLabel}`)); return null; } } function toggleVoiceCam() { @@ -905,7 +1340,7 @@ function toggleVoiceCam() { } else { navigator.mediaDevices.getUserMedia({video: {width: {ideal: 1280}, height: {ideal: 720}}}).then(stream => { startHardwareVideo(btn, Promise.resolve(stream), "Camera", 1).then(ctrl => camControl = ctrl); - }).catch(err => alert("Camera error: " + err.message)); + }).catch(err => alert(processBlah(`:error.call.camera:${err.message}`))); } } @@ -914,11 +1349,11 @@ function toggleVoiceScreen() { if (screenControl) { screenControl.stop(); } else { - if (!navigator.mediaDevices.getDisplayMedia) { alert("Screen sharing is not supported on this browser."); return; } + if (!navigator.mediaDevices.getDisplayMedia) { alert(processBlah("error.call.screen.unsupported")); return; } navigator.mediaDevices.getDisplayMedia({ video: true, audio: { autoGainControl: false, echoCancellation: false, noiseSuppression: false }, systemAudio: "include", selfBrowserSurface: "exclude" }).then(stream => { startHardwareVideo(btn, Promise.resolve(stream), "Screen", 2).then(ctrl => screenControl = ctrl); - }).catch(err => alert("Screen error: " + err.message)); + }).catch(err => alert(processBlah(`:error.call.screen:${err.message}`))); } }