diff --git a/OpenType-TT/BackOneLogo-Regular-SVG.ttf b/OpenType-TT/BackOneLogo-Regular-SVG.ttf new file mode 100755 index 0000000..68a070e Binary files /dev/null and b/OpenType-TT/BackOneLogo-Regular-SVG.ttf differ diff --git a/OpenType-TT/BackOneLogo-Regular.ttf b/OpenType-TT/BackOneLogo-Regular.ttf new file mode 100755 index 0000000..e2c1b66 Binary files /dev/null and b/OpenType-TT/BackOneLogo-Regular.ttf differ diff --git a/addons/chat/addon.json b/addons/chat/addon.json index f2cc745..6b0ccb6 100644 --- a/addons/chat/addon.json +++ b/addons/chat/addon.json @@ -2,7 +2,7 @@ "id": "chat", "name": "Chat P2P", "version": "1.17.1", - "requires": ["comms-core"], + "requires": ["comms-core", "webhub"], "backoneCore": { "apis": [ "/app/api/peers", diff --git a/addons/chat/frontend/index.js b/addons/chat/frontend/index.js index 14f0d81..9688588 100644 --- a/addons/chat/frontend/index.js +++ b/addons/chat/frontend/index.js @@ -4,12 +4,27 @@ window.BackOneAddons.chat = function chatAddon() { const messagesByPeer = {}; let selectedPeer = null; let root = null; + let unsubChat = null; + + function escapeHtml(s) { + return String(s).replace(/&/g, '&').replace(//g, '>'); + } + + function clearSessionMessages() { + Object.keys(messagesByPeer).forEach(k => delete messagesByPeer[k]); + } function getThread(peerId) { if (!messagesByPeer[peerId]) messagesByPeer[peerId] = []; return messagesByPeer[peerId]; } + function formatTime(ts) { + if (!ts) return ''; + const d = new Date(ts); + return d.toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }); + } + function networkLabel() { const n = BackOneNetworkSession.getActiveNetwork(); return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId(); @@ -32,35 +47,37 @@ window.BackOneAddons.chat = function chatAddon() { header.textContent = net ? `Select a contact on ${net}` : 'Join a network in the header bar'; return; } - header.innerHTML = `${selectedPeer.displayName || selectedPeer.peerId} ${selectedPeer.peerId}${net ? ' · ' + net : ''}`; + header.innerHTML = `${escapeHtml(selectedPeer.displayName || selectedPeer.peerId)} ${escapeHtml(selectedPeer.peerId)}${net ? ' · ' + escapeHtml(net) : ''}`; updateConnectionBar(); } + function selfName() { + const addr = (BackOneNetworkSession.getCoreStatus().address || '').toLowerCase(); + return addr ? addr.slice(0, 10) + '…' : 'me'; + } + return { mount(container) { root = container; + clearSessionMessages(); container.innerHTML = ` -
+
-
-
Chat P2P > Select contact
-
Select a contact
-
+
-
@@ -71,31 +88,63 @@ window.BackOneAddons.chat = function chatAddon() { const thread = container.querySelector('#chat-thread'); const input = container.querySelector('#chat-input'); - const fileInput = container.querySelector('#chat-file'); const sendBtn = container.querySelector('#chat-send'); - function updateInputs() { - const ok = !!BackOneNetworkSession.getNetworkId() && !!selectedPeer; - input.disabled = !ok; - fileInput.disabled = !ok; - sendBtn.disabled = !ok; - renderHeader(); - const bc = root.querySelector('#chat-breadcrumb-peer'); - if (bc) bc.textContent = selectedPeer ? (selectedPeer.displayName || selectedPeer.peerId) : 'Select contact'; - } - const render = () => { if (!selectedPeer) { - thread.innerHTML = '

Choose a peer from the list

'; + thread.innerHTML = '

Choose a contact from the list

'; return; } const messages = getThread(selectedPeer.peerId); - thread.innerHTML = messages.map(m => - `
${m.senderName || m.sender || 'me'}: ${m.body || ''}${m.file ? ` [${m.file}]` : ''}
` - ).join(''); + if (!messages.length) { + thread.innerHTML = '

No messages yet — start the conversation

'; + return; + } + thread.innerHTML = messages.map(m => ` +
+
+ ${escapeHtml(m.senderName || m.sender || 'peer')} + ${escapeHtml(formatTime(m.at))} +
+ ${m.body ? `
${escapeHtml(m.body)}
` : ''} +
`).join(''); thread.scrollTop = thread.scrollHeight; }; + function updateInputs() { + const networkId = BackOneNetworkSession.getNetworkId(); + const ok = !!networkId && !!selectedPeer; + input.disabled = !ok; + sendBtn.disabled = !ok; + renderHeader(); + } + + if (unsubChat) unsubChat(); + unsubChat = BackOneWebHubClient.subscribe('chat_message', (msg) => { + const networkId = BackOneNetworkSession.getNetworkId(); + if (!networkId || (msg.networkId && msg.networkId.toLowerCase() !== networkId)) return; + const senderId = (msg.senderId || '').toLowerCase(); + const peerId = selectedPeer?.peerId?.toLowerCase(); + if (!peerId || senderId !== peerId) { + if (senderId) { + getThread(senderId).push({ + sender: senderId, + senderName: msg.senderName || senderId, + body: msg.body || '', + at: msg.at || Date.now() + }); + } + return; + } + getThread(peerId).push({ + sender: senderId, + senderName: msg.senderName || senderId, + body: msg.body || '', + at: msg.at || Date.now() + }); + render(); + }); + BackOnePeerPicker(container.querySelector('#chat-peer-picker'), { layout: 'sidebar', showActionButton: false, @@ -106,6 +155,7 @@ window.BackOneAddons.chat = function chatAddon() { }, onNetworkChange() { selectedPeer = null; + clearSessionMessages(); updateInputs(); render(); } @@ -116,40 +166,27 @@ window.BackOneAddons.chat = function chatAddon() { render(); }); - sendBtn.onclick = async () => { + sendBtn.onclick = () => { const networkId = BackOneNetworkSession.getNetworkId(); if (!selectedPeer || !networkId) return; const body = input.value.trim(); - const selfName = 'me'; - if (fileInput.files.length) { - const fd = new FormData(); - fd.append('file', fileInput.files[0]); - fd.append('networkId', networkId); - await BackOneAuth.fetchWithAuth(`/controller/comms/network/${networkId}/chat/attachment`, { - method: 'POST', - body: fd - }).catch(() => {}); - getThread(selectedPeer.peerId).push({ - sender: selfName, - senderName: selfName, - body: '', - file: fileInput.files[0].name - }); - fileInput.value = ''; - } - if (body) { - await BackOneAuth.fetchWithAuth(`/controller/comms/network/${networkId}/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ body, targetId: selectedPeer.peerId, networkId }) - }).catch(() => {}); - getThread(selectedPeer.peerId).push({ - sender: selfName, - senderName: selfName, - body - }); - input.value = ''; - } + if (!body) return; + const now = Date.now(); + const name = selfName(); + BackOneWebHubClient.send({ + type: 'chat_message', + networkId, + targetId: selectedPeer.peerId, + senderName: name, + body + }); + getThread(selectedPeer.peerId).push({ + sender: 'me', + senderName: name, + body, + at: now + }); + input.value = ''; render(); }; diff --git a/addons/secure-chat/addon.json b/addons/secure-chat/addon.json new file mode 100644 index 0000000..42dba0d --- /dev/null +++ b/addons/secure-chat/addon.json @@ -0,0 +1,27 @@ +{ + "id": "secure-chat", + "name": "Secure Chat", + "version": "1.17.1", + "requires": ["comms-core", "webhub"], + "backoneCore": { + "apis": [ + "/app/api/peers", + "/app/api/networks", + "POST /app/api/network/{nwid}", + "DELETE /app/api/network/{nwid}", + "/app/api/network/{nwid}", + "/app/api/network/{nwid}/members", + "/app/api/authtoken", + "/app/api/contacts" + ] + }, + "backend": { + "routes": ["/controller/comms/network"], + "websocketEvents": ["secure_chat_message"] + }, + "frontend": { + "entry": "/app/addons/secure-chat/index.js", + "navLabel": "Secure Chat", + "icon": "chat" + } +} diff --git a/addons/secure-chat/frontend/index.js b/addons/secure-chat/frontend/index.js new file mode 100644 index 0000000..c4110f6 --- /dev/null +++ b/addons/secure-chat/frontend/index.js @@ -0,0 +1,186 @@ +window.BackOneAddons = window.BackOneAddons || {}; + +window.BackOneAddons['secure-chat'] = function secureChatAddon() { + const messagesByPeer = {}; + let selectedPeer = null; + let root = null; + let unsubChat = null; + + function escapeHtml(s) { + return String(s).replace(/&/g, '&').replace(//g, '>'); + } + + function clearSessionMessages() { + Object.keys(messagesByPeer).forEach(k => delete messagesByPeer[k]); + } + + function getThread(peerId) { + if (!messagesByPeer[peerId]) messagesByPeer[peerId] = []; + return messagesByPeer[peerId]; + } + + function formatTime(ts) { + if (!ts) return ''; + const d = new Date(ts); + return d.toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }); + } + + function networkLabel() { + const n = BackOneNetworkSession.getActiveNetwork(); + return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId(); + } + + function renderHeader() { + const header = root?.querySelector('#secure-chat-header'); + if (!header) return; + const net = networkLabel(); + if (!selectedPeer) { + header.textContent = net ? `Select a contact on ${net}` : 'Join a network in the header bar'; + return; + } + header.innerHTML = `${escapeHtml(selectedPeer.displayName || selectedPeer.peerId)} ${escapeHtml(selectedPeer.peerId)}${net ? ' · ' + escapeHtml(net) : ''}`; + } + + function selfName() { + const addr = (BackOneNetworkSession.getCoreStatus().address || '').toLowerCase(); + return addr ? addr.slice(0, 10) + '…' : 'me'; + } + + return { + mount(container) { + root = container; + clearSessionMessages(); + container.innerHTML = ` +
+ +
+
+
Select a contact
+
+
+ +
+ +
+
+
+
+
`; + + const thread = container.querySelector('#secure-chat-thread'); + const input = container.querySelector('#secure-chat-input'); + const sendBtn = container.querySelector('#secure-chat-send'); + + const render = () => { + if (!selectedPeer) { + thread.innerHTML = '

Choose a contact from the list

'; + return; + } + const messages = getThread(selectedPeer.peerId); + if (!messages.length) { + thread.innerHTML = '

No messages yet — start the conversation

'; + return; + } + thread.innerHTML = messages.map(m => ` +
+
+ ${escapeHtml(m.senderName || m.sender || 'me')} + ${escapeHtml(formatTime(m.at))} +
+ ${m.body ? `
${escapeHtml(m.body)}
` : ''} +
`).join(''); + thread.scrollTop = thread.scrollHeight; + }; + + function updateInputs() { + const networkId = BackOneNetworkSession.getNetworkId(); + const ok = !!networkId && !!selectedPeer; + input.disabled = !ok; + sendBtn.disabled = !ok; + renderHeader(); + } + + if (unsubChat) unsubChat(); + unsubChat = BackOneWebHubClient.subscribe('chat_message', (msg) => { + const networkId = BackOneNetworkSession.getNetworkId(); + if (!networkId || (msg.networkId && msg.networkId.toLowerCase() !== networkId)) return; + const senderId = (msg.senderId || '').toLowerCase(); + const peerId = selectedPeer?.peerId?.toLowerCase(); + if (!peerId || senderId !== peerId) { + if (senderId) { + getThread(senderId).push({ + sender: senderId, + senderName: msg.senderName || senderId, + body: msg.body || '', + at: msg.at || Date.now() + }); + } + return; + } + getThread(peerId).push({ + sender: senderId, + senderName: msg.senderName || senderId, + body: msg.body || '', + at: msg.at || Date.now() + }); + render(); + }); + + BackOnePeerPicker(container.querySelector('#secure-chat-peer-picker'), { + layout: 'sidebar', + showActionButton: false, + onSelect(peer) { + selectedPeer = peer; + updateInputs(); + render(); + }, + onNetworkChange() { + selectedPeer = null; + clearSessionMessages(); + updateInputs(); + render(); + } + }); + + BackOneNetworkSession.subscribe(() => { + updateInputs(); + render(); + }); + + sendBtn.onclick = () => { + const networkId = BackOneNetworkSession.getNetworkId(); + if (!selectedPeer || !networkId) return; + const body = input.value.trim(); + if (!body) return; + const now = Date.now(); + const name = selfName(); + BackOneWebHubClient.send({ + type: 'chat_message', + networkId, + targetId: selectedPeer.peerId, + senderName: name, + body + }); + getThread(selectedPeer.peerId).push({ + sender: 'me', + senderName: name, + body, + at: now + }); + input.value = ''; + render(); + }; + + updateInputs(); + render(); + } + }; +}; diff --git a/addons/updater/backone-updater b/addons/updater/backone-updater index 2ea7006..2b17b41 100755 --- a/addons/updater/backone-updater +++ b/addons/updater/backone-updater @@ -55,6 +55,9 @@ run_migrations() { 003_*) [ "$scope" = "all" ] || [ "$scope" = "core" ] || continue ;; + 004_*|005_*) + [ "$scope" = "all" ] || [ "$scope" = "core" ] || continue + ;; *) if [ "$scope" != "all" ] && [ "$scope" != "$ADDON_SCOPE" ]; then continue diff --git a/addons/updater/migrations/004_webrtc_ice_defaults.sh b/addons/updater/migrations/004_webrtc_ice_defaults.sh new file mode 100755 index 0000000..e1694a5 --- /dev/null +++ b/addons/updater/migrations/004_webrtc_ice_defaults.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail +BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +if [ -x "${SCRIPT_DIR}/merge-local-conf.sh" ]; then + BACKONE_HOME="$BACKONE_HOME" bash "${SCRIPT_DIR}/merge-local-conf.sh" '{ + "settings": { + "icePreferOverlay": true, + "iceServers": [{"urls": "stun:stun.l.google.com:19302"}], + "iceFallbackTimeoutMs": 8000, + "webhubPort": 9994 + } + }' +fi diff --git a/addons/updater/migrations/005_enable_webhub_defaults.sh b/addons/updater/migrations/005_enable_webhub_defaults.sh new file mode 100755 index 0000000..3053353 --- /dev/null +++ b/addons/updater/migrations/005_enable_webhub_defaults.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail +BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}" +CONF="${BACKONE_HOME}/local.conf" + +python3 - "$CONF" <<'PY' +import json +import os +import sys + +path = sys.argv[1] +defaults = ["comms-core", "webhub", "voice", "video", "chat"] +current = {} +if os.path.isfile(path): + with open(path, encoding="utf-8") as f: + try: + current = json.load(f) + except json.JSONDecodeError: + current = {} +if not isinstance(current, dict): + current = {} +settings = current.setdefault("settings", {}) +if not isinstance(settings, dict): + settings = {} + current["settings"] = settings +enabled = settings.get("enabledAddons") +if enabled is None: + # No explicit list — all installed addons stay enabled. + sys.exit(0) +if not isinstance(enabled, list): + enabled = [] +merged = list(enabled) +for addon in defaults: + if addon not in merged: + merged.append(addon) +settings["enabledAddons"] = merged +os.makedirs(os.path.dirname(path), exist_ok=True) +with open(path, "w", encoding="utf-8") as f: + json.dump(current, f, indent=2) + f.write("\n") +PY + +chown backone:backone "$CONF" 2>/dev/null || true +chmod 644 "$CONF" 2>/dev/null || true diff --git a/addons/video/addon.json b/addons/video/addon.json index 77d85c4..18b8d9e 100644 --- a/addons/video/addon.json +++ b/addons/video/addon.json @@ -2,7 +2,7 @@ "id": "video", "name": "Video Call P2P", "version": "1.17.1", - "requires": ["comms-core", "voice"], + "requires": ["comms-core", "webhub", "voice"], "backoneCore": { "apis": [ "/app/api/peers", diff --git a/addons/video/frontend/index.js b/addons/video/frontend/index.js index 40dda4e..8e7b7dc 100644 --- a/addons/video/frontend/index.js +++ b/addons/video/frontend/index.js @@ -1,20 +1,13 @@ window.BackOneAddons = window.BackOneAddons || {}; window.BackOneAddons.video = function videoAddon() { - let pc = null; let picker = null; let selectedPeer = null; let inCall = false; - let localStream = null; let micEnabled = true; let camEnabled = true; let root = null; - function networkLabel() { - const n = BackOneNetworkSession.getActiveNetwork(); - return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId(); - } - function updateConnectionBar() { const bar = root?.querySelector('#video-conn-bar'); if (!bar || !window.BackOneCommLayout) return; @@ -53,19 +46,40 @@ window.BackOneAddons.video = function videoAddon() { updateConnectionBar(); } - function showPlaceholder(show) { - const ph = root?.querySelector('#video-placeholder'); - const remote = root?.querySelector('#video-remote'); - if (ph) ph.style.display = show ? 'block' : 'none'; - if (remote) remote.style.display = show ? 'none' : 'block'; + function updateCallLayout() { + const layout = root?.querySelector('.comm-layout'); + if (layout) layout.classList.toggle('video-in-call', inCall); } - function hangUp() { - if (pc) { pc.close(); pc = null; } - if (localStream) { - localStream.getTracks().forEach(t => t.stop()); - localStream = null; + function showPlaceholder(show) { + const stage = root?.querySelector('.video-stage'); + const remote = root?.querySelector('#video-remote'); + const phLabel = root?.querySelector('#video-ph-label'); + if (stage) stage.classList.toggle('has-remote', !show); + if (remote) remote.classList.toggle('active', !show); + if (phLabel && show) { + phLabel.textContent = inCall + ? 'Waiting for remote peer…' + : (selectedPeer ? 'Ready to call' : 'Select a peer to start a video call'); } + updateCallLayout(); + } + + function bindStreams() { + const local = root?.querySelector('#video-local'); + const remote = root?.querySelector('#video-remote'); + const localStream = BackOneWebRtcSession.getLocalStream(); + const remoteStream = BackOneWebRtcSession.getRemoteStream(); + if (local && localStream) local.srcObject = localStream; + if (remote && remoteStream && remoteStream.getTracks().length) { + remote.srcObject = remoteStream; + showPlaceholder(false); + root?.querySelector('#video-breadcrumb-state')?.textContent = 'Connected to'; + } + } + + async function hangUp() { + await BackOneWebRtcSession.hangUp(); const local = root?.querySelector('#video-local'); const remote = root?.querySelector('#video-remote'); if (local) local.srcObject = null; @@ -73,6 +87,8 @@ window.BackOneAddons.video = function videoAddon() { inCall = false; micEnabled = true; camEnabled = true; + const breadcrumbState = root?.querySelector('#video-breadcrumb-state'); + if (breadcrumbState) breadcrumbState.textContent = 'Select peer'; showPlaceholder(true); updateCallButtons(); window.BackOneRingingPopup?.dismiss(); @@ -99,20 +115,23 @@ window.BackOneAddons.video = function videoAddon() {
-
Video Call P2P > ${inCall ? 'Connected to' : 'Select peer'} —
+
Video Call P2P > Select peer —
-
-
Remote
-
-
👤
-
—
-
Remote Peer
+
+
+
+
👤
+
—
+
Select a peer to start a video call
+
+
-
+ You
+
Remote
@@ -133,6 +152,7 @@ window.BackOneAddons.video = function videoAddon() { selectedPeer = peer; const phPeer = root.querySelector('#video-ph-peer'); if (phPeer) phPeer.textContent = peer.peerId; + if (!inCall) showPlaceholder(true); updateCallButtons(); }, onNetworkChange() { @@ -142,45 +162,41 @@ window.BackOneAddons.video = function videoAddon() { }); BackOneNetworkSession.subscribe(() => updateCallButtons()); + BackOneWebRtcSession.subscribe((event) => { + if (event === 'call-started') { + inCall = true; + updateCallButtons(); + bindStreams(); + } + if (event === 'remote-track' || event === 'call-accepted') bindStreams(); + if (event === 'call-ended') { + inCall = false; + showPlaceholder(true); + updateCallButtons(); + } + }); callBtn.onclick = async () => { const peer = selectedPeer || picker.getSelectedPeer(); - const networkId = BackOneNetworkSession.getNetworkId(); - if (!peer || !networkId) return; + if (!peer || !BackOneNetworkSession.getNetworkId()) return; try { - pc = new RTCPeerConnection(); - pc.ontrack = (ev) => { - root.querySelector('#video-remote').srcObject = ev.streams[0]; - showPlaceholder(false); - }; - localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); - root.querySelector('#video-local').srcObject = localStream; - localStream.getTracks().forEach(t => pc.addTrack(t, localStream)); - const offer = await pc.createOffer(); - await pc.setLocalDescription(offer); - await BackOneAuth.fetchWithAuth('/controller/comms/call', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: 'video', - networkId, - targetId: peer.peerId, - sdp: offer.sdp - }) - }); inCall = true; - root.querySelector('#video-breadcrumb-state').textContent = 'Connected to'; + root.querySelector('#video-breadcrumb-state').textContent = 'Connecting to'; + updateCallLayout(); updateCallButtons(); + await BackOneWebRtcSession.startOutgoingCall(peer, 'video'); + bindStreams(); } catch (err) { - hangUp(); + await hangUp(); alert(err.message || 'Call failed'); } }; - container.querySelector('#video-hangup').onclick = hangUp; - container.querySelector('#video-hangup-stage').onclick = hangUp; + container.querySelector('#video-hangup').onclick = () => hangUp(); + container.querySelector('#video-hangup-stage').onclick = () => hangUp(); container.querySelector('#video-mic').onclick = () => { + const localStream = BackOneWebRtcSession.getLocalStream(); if (!localStream) return; micEnabled = !micEnabled; localStream.getAudioTracks().forEach(t => { t.enabled = micEnabled; }); @@ -188,6 +204,7 @@ window.BackOneAddons.video = function videoAddon() { }; container.querySelector('#video-cam').onclick = () => { + const localStream = BackOneWebRtcSession.getLocalStream(); if (!localStream) return; camEnabled = !camEnabled; localStream.getVideoTracks().forEach(t => { t.enabled = camEnabled; }); @@ -199,7 +216,10 @@ window.BackOneAddons.video = function videoAddon() { if (stage.requestFullscreen) stage.requestFullscreen(); }; + const active = BackOneWebRtcSession.getActiveCall(); + inCall = !!active && active.callType === 'video'; showPlaceholder(true); + if (inCall) bindStreams(); updateCallButtons(); } }; diff --git a/addons/voice/addon.json b/addons/voice/addon.json index 912271d..77a0927 100644 --- a/addons/voice/addon.json +++ b/addons/voice/addon.json @@ -2,7 +2,7 @@ "id": "voice", "name": "Voice Call P2P", "version": "1.17.1", - "requires": ["comms-core"], + "requires": ["comms-core", "webhub"], "backoneCore": { "apis": [ "/app/api/peers", diff --git a/addons/voice/frontend/index.js b/addons/voice/frontend/index.js index b06003e..6e23e4c 100644 --- a/addons/voice/frontend/index.js +++ b/addons/voice/frontend/index.js @@ -1,13 +1,16 @@ window.BackOneAddons = window.BackOneAddons || {}; window.BackOneAddons.voice = function voiceAddon() { - let pc = null; let picker = null; let selectedPeer = null; let inCall = false; - let localStream = null; let micEnabled = true; let root = null; + let remoteAudio = null; + + function escapeHtml(s) { + return String(s).replace(/&/g, '&').replace(//g, '>'); + } function updateConnectionBar() { const bar = root?.querySelector('#voice-conn-bar'); @@ -34,6 +37,16 @@ window.BackOneAddons.voice = function voiceAddon() { } } + function updateActiveBanner() { + const banner = root?.querySelector('#voice-active-banner'); + const label = root?.querySelector('#voice-active-label'); + if (!banner) return; + banner.style.display = inCall && selectedPeer ? 'flex' : 'none'; + if (label && selectedPeer) { + label.textContent = `In call with ${selectedPeer.displayName || selectedPeer.peerId}`; + } + } + function updateCallButtons() { const callBtn = root?.querySelector('#voice-call'); if (callBtn) callBtn.disabled = !selectedPeer || !BackOneNetworkSession.getNetworkId() || inCall; @@ -45,16 +58,26 @@ window.BackOneAddons.voice = function voiceAddon() { } updateHeadStatus(); updateConnectionBar(); + updateActiveBanner(); } - function hangUp() { - if (pc) { pc.close(); pc = null; } - if (localStream) { - localStream.getTracks().forEach(t => t.stop()); - localStream = null; + function syncInCallState() { + const active = BackOneWebRtcSession.getActiveCall(); + inCall = !!active && (active.callType === 'voice' || !active.callType); + if (inCall && active.peer) { + selectedPeer = active.peer.peerId ? active.peer : selectedPeer; + root?.querySelector('#voice-breadcrumb-state')?.textContent = 'Connected to'; } + updateCallButtons(); + } + + async function hangUp() { + await BackOneWebRtcSession.hangUp(); inCall = false; micEnabled = true; + if (remoteAudio) remoteAudio.srcObject = null; + const breadcrumbState = root?.querySelector('#voice-breadcrumb-state'); + if (breadcrumbState) breadcrumbState.textContent = 'Select peer'; updateCallButtons(); window.BackOneRingingPopup?.dismiss(); } @@ -63,11 +86,11 @@ window.BackOneAddons.voice = function voiceAddon() { mount(container) { root = container; container.innerHTML = ` -
+