diff --git a/kkkmeet-server.el b/kkkmeet-server.el index b9fd29a..d3518e3 100644 --- a/kkkmeet-server.el +++ b/kkkmeet-server.el @@ -180,19 +180,21 @@ When KEEP-OPEN is nil, close PROCESS after writing." (defun kkkmeet-server--remove-peer (process) "Remove PROCESS from its room and notify remaining peers." (let ((room-id (process-get process :kkkmeet-room-id)) - (peer-id (process-get process :kkkmeet-peer-id))) + (peer-id (process-get process :kkkmeet-peer-id)) + (name (or (process-get process :kkkmeet-name) "A participant"))) (when (and room-id peer-id) (let ((room (gethash room-id kkkmeet-server--rooms))) (when room (remhash peer-id room) (kkkmeet-server--broadcast - room-id peer-id `((type . "peer-left") (peerId . ,peer-id))) + room-id peer-id `((type . "peer-left") (peerId . ,peer-id) (name . ,name))) (when (equal (alist-get 'peerId (gethash room-id kkkmeet-server--presentation-states)) peer-id) (remhash room-id kkkmeet-server--presentation-states) (kkkmeet-server--broadcast room-id peer-id `((type . "presentation-state") (from . ,peer-id) + (name . ,name) (sharing . :false)))) (when (= (hash-table-count room) 0) (remhash room-id kkkmeet-server--rooms) diff --git a/public/app.js b/public/app.js index 2064388..15a2ce3 100644 --- a/public/app.js +++ b/public/app.js @@ -10,6 +10,7 @@ const nameDialogSubmit = document.querySelector("#name-dialog-submit"); const initialNameForm = document.querySelector("#initial-name-form"); const initialNameInput = document.querySelector("#initial-name-input"); const statusText = document.querySelector("#status"); +const activityStatusText = document.querySelector("#activity-status"); const youtubeForm = document.querySelector("#youtube-form"); const youtubeInput = document.querySelector("#youtube-input"); const youtubeShell = document.querySelector("#youtube-shell"); @@ -73,6 +74,7 @@ let youtubeReadyResolvers = []; let watchApplySequence = 0; let youtubePlaybackRequestId = 0; let watchPlaylist = []; +let remoteParticipantCount = 0; const peers = new Map(); function persistentClientId() { @@ -133,6 +135,26 @@ function setStatus(message) { statusText.textContent = message; } +function participantDisplayName(name, fallback = "Someone") { + return normalizeName(String(name || "")) || fallback; +} + +function setSessionSummary() { + if (!peerId) { + setStatus(displayName ? `Ready as ${displayName}.` : "Enter your name to join the call."); + return; + } + const suffix = remoteParticipantCount === 1 ? "participant" : "participants"; + setStatus(`You are ${displayName}. ${remoteParticipantCount} other ${suffix}.`); +} + +function setActivity(name, action) { + const actor = participantDisplayName(name); + const message = `${actor} ${action}`; + activityStatusText.textContent = message; + console.info(`[kkkmeet activity] ${message}`); +} + function normalizeName(name) { return name.trim().replace(/\s+/g, " ").slice(0, 48); } @@ -177,6 +199,7 @@ function setDisplayName(nextName) { displayName = normalizeName(nextName); localStorage.setItem(nameStorageKey, displayName); localNameLabel.textContent = displayName ? `${displayName} (you)` : "You"; + setSessionSummary(); } function openNameDialog(mode) { @@ -528,6 +551,7 @@ async function sendWatchState({ updatedAt: Date.now() }) }); + setActivity(displayName, "synced YouTube playback."); } function currentWatchState(playing) { @@ -1150,7 +1174,9 @@ async function answerPeer(message) { async function handleSignal(message) { if (message.type === "welcome") { peerId = message.peerId; - setStatus(`Joined the call as ${displayName}. ${message.peers.length} other participant(s).`); + remoteParticipantCount = message.peers.length; + setSessionSummary(); + setActivity(displayName, "joined."); if (message.watchState) { const watchShouldPlay = message.watchState.action === "play" || (!message.watchState.action && Boolean(message.watchState.playing)); @@ -1175,13 +1201,20 @@ async function handleSignal(message) { if (message.type === "peer-joined") { updateRemoteLabel(message.peerId, message.name); announceParticipantJoined(message.name); - setStatus(`${message.name || "A participant"} joined.`); + remoteParticipantCount += 1; + setSessionSummary(); + setActivity(message.name, "joined."); return; } if (message.type === "peer-left") { + const peerName = message.name || + peers.get(message.peerId)?.name || + `Participant ${message.peerId?.slice(0, 4) || ""}`.trim(); closePeer(message.peerId); - setStatus("A participant left."); + remoteParticipantCount = Math.max(0, remoteParticipantCount - 1); + setSessionSummary(); + setActivity(peerName, "left."); return; } @@ -1197,21 +1230,22 @@ async function handleSignal(message) { allowJoinedYoutubeStartPrompt = false; } await applyWatchState(message, { allowStartPrompt }); - setStatus(`${message.name || "Someone"} synced YouTube playback.`); + setActivity(message.name, "synced YouTube playback."); return; } if (message.type === "presentation-state") { setRemotePresentation(message.from, message.sharing, message.name); - setStatus(message.sharing - ? `${message.name || "Someone"} is sharing their screen.` - : `${message.name || "Someone"} stopped sharing their screen.`); + setActivity( + message.name, + message.sharing ? "started sharing their screen." : "stopped sharing their screen." + ); return; } if (message.type === "voice-announcement") { speakAnnouncement(message); - setStatus(`${message.name || "Someone"} sent an announcement.`); + setActivity(message.name, "sent an announcement."); return; } @@ -1260,8 +1294,9 @@ function leaveCall() { events?.close(); events = undefined; peerId = ""; + remoteParticipantCount = 0; for (const remotePeerId of peers.keys()) closePeer(remotePeerId); - setStatus(displayName ? `Ready as ${displayName}.` : "Enter your name to join the call."); + setSessionSummary(); } function setTrackEnabled(kind, enabled) { @@ -1356,7 +1391,7 @@ async function sendVoiceAnnouncement(text) { await sendRoomEvent({ type: "voice-announcement", text: announcementText }); speakAnnouncement({ text: announcementText }); - setStatus("Announcement sent."); + setActivity(displayName, "sent an announcement."); } async function startScreenShare() { @@ -1368,6 +1403,7 @@ async function startScreenShare() { await replaceOutboundVideoTrack(screenTrack); shareButton.textContent = "Stop Sharing"; await sendRoomEvent({ type: "presentation-state", sharing: true }); + setActivity(displayName, "started sharing their screen."); screenTrack.onended = () => { stopScreenShare().catch((error) => { @@ -1389,6 +1425,7 @@ async function stopScreenShare() { } shareButton.textContent = "Share Screen"; await sendRoomEvent({ type: "presentation-state", sharing: false }); + setActivity(displayName, "stopped sharing their screen."); } initialNameForm.addEventListener("submit", (event) => { diff --git a/public/index.html b/public/index.html index 4bc51d7..7031314 100644 --- a/public/index.html +++ b/public/index.html @@ -12,6 +12,7 @@

kkkmeet

Enter your name to join the call.

+

diff --git a/public/styles.css b/public/styles.css index 4c84c4d..2314547 100644 --- a/public/styles.css +++ b/public/styles.css @@ -62,6 +62,13 @@ h1 { font-size: 14px; } +#activity-status { + min-height: 18px; + margin-top: 3px; + color: #d5dbe3; + font-size: 13px; +} + #rename-button { min-width: 92px; } diff --git a/server.js b/server.js index 5a72c48..1b6b9e4 100644 --- a/server.js +++ b/server.js @@ -84,11 +84,13 @@ function removePeer(roomId, peerId) { const room = rooms.get(roomId); if (!room) return; + const peer = room.get(peerId); + const name = peer?.name || "A participant"; room.delete(peerId); - broadcast(roomId, peerId, { type: "peer-left", peerId }); + broadcast(roomId, peerId, { type: "peer-left", peerId, name }); if (presentationStates.get(roomId)?.peerId === peerId) { presentationStates.delete(roomId); - broadcast(roomId, peerId, { type: "presentation-state", from: peerId, sharing: false }); + broadcast(roomId, peerId, { type: "presentation-state", from: peerId, name, sharing: false }); } if (room.size === 0) {