From 948f8c7aa7b14279397b3f0fcc89c5ea029f4e3c Mon Sep 17 00:00:00 2001 From: Bryan1029384756 <23323626+Bryan1029384756@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:48:48 -0600 Subject: [PATCH] feat: Add voice and video stage functionality with multi-platform project setup. --- apps/android/android/app/build.gradle | 2 +- apps/android/package.json | 2 +- apps/electron/package.json | 2 +- apps/web/package.json | 2 +- deploy/server/server.js | 31 ++++++++-- packages/shared/package.json | 2 +- packages/shared/src/components/Sidebar.jsx | 6 +- packages/shared/src/components/VoiceStage.jsx | 57 ++++++++++++++++++- packages/shared/src/contexts/VoiceContext.jsx | 54 ++++++++++++++++-- 9 files changed, 141 insertions(+), 17 deletions(-) diff --git a/apps/android/android/app/build.gradle b/apps/android/android/app/build.gradle index 0c4b20a..4133f4b 100644 --- a/apps/android/android/app/build.gradle +++ b/apps/android/android/app/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 27 - versionName "1.0.32" + versionName "1.0.33" 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/apps/android/package.json b/apps/android/package.json index d901ba5..ce7c905 100644 --- a/apps/android/package.json +++ b/apps/android/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/android", "private": true, - "version": "1.0.32", + "version": "1.0.33", "type": "module", "scripts": { "cap:sync": "npx cap sync", diff --git a/apps/electron/package.json b/apps/electron/package.json index 1ce7a75..cd5197c 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/electron", "private": true, - "version": "1.0.32", + "version": "1.0.33", "description": "Discord Clone - Electron app", "author": "Moyettes", "type": "module", diff --git a/apps/web/package.json b/apps/web/package.json index df2490d..5a32341 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/web", "private": true, - "version": "1.0.32", + "version": "1.0.33", "type": "module", "scripts": { "dev": "vite", diff --git a/deploy/server/server.js b/deploy/server/server.js index 8aa0a04..594b6cb 100644 --- a/deploy/server/server.js +++ b/deploy/server/server.js @@ -16,6 +16,7 @@ const RELEASES_DIR = '/app/releases'; const LOADING_HTML = readFileSync(join(__dirname, 'loading.html'), 'utf-8'); let currentDistPath = null; +let currentCommitHash = null; let building = false; let lastBuildStatus = { status: 'idle', timestamp: null, error: null }; @@ -39,8 +40,15 @@ function run(cmd, opts = {}) { execSync(cmd, { stdio: 'inherit', timeout: 600_000, ...opts }); } -async function triggerBuild() { +async function triggerBuild(webhookCommit) { if (building) return; + + // Fast path: skip clone entirely if webhook provides the commit and it matches + if (webhookCommit && webhookCommit === currentCommitHash) { + log(`Commit ${webhookCommit} is already deployed, skipping build (webhook fast path).`); + return; + } + building = true; lastBuildStatus = { status: 'building', timestamp: Date.now(), error: null }; @@ -54,6 +62,17 @@ async function triggerBuild() { // Clone run(`git clone --depth 1 --branch ${GIT_BRANCH} ${GIT_REPO_URL} ${buildDir}`); + // Read the cloned commit hash + const clonedHash = execSync('git rev-parse HEAD', { cwd: buildDir, encoding: 'utf-8' }).trim(); + log(`Cloned commit: ${clonedHash}`); + + // Skip if this commit is already deployed + if (clonedHash === currentCommitHash) { + log(`Commit ${clonedHash} is already deployed, skipping build.`); + lastBuildStatus = { status: 'skipped', timestamp: Date.now(), error: null }; + return; + } + // Install deps (web workspaces only) run( 'npm ci --workspace=apps/web --workspace=packages/shared --workspace=packages/platform-web --include-workspace-root', @@ -73,7 +92,8 @@ async function triggerBuild() { // Swap — instant, zero downtime const oldDist = currentDistPath; currentDistPath = releaseDir; - log(`Swapped to new release: ${releaseDir}`); + currentCommitHash = clonedHash; + log(`Swapped to new release: ${releaseDir} (commit ${clonedHash})`); // Clean up old release if (oldDist && existsSync(oldDist) && oldDist !== releaseDir) { @@ -105,13 +125,16 @@ app.use(compression()); app.post('/api/webhook', express.json(), (req, res) => { if (building) return res.status(409).json({ error: 'Build already in progress' }); - triggerBuild(); + // Extract commit SHA from Gitea/GitHub push webhook payload + const webhookCommit = req.body?.after || null; + + triggerBuild(webhookCommit); res.json({ message: 'Build triggered' }); }); // Status endpoint app.get('/api/status', (_req, res) => { - res.json({ building, ...lastBuildStatus }); + res.json({ building, commitHash: currentCommitHash, ...lastBuildStatus }); }); // Static file serving + SPA fallback diff --git a/packages/shared/package.json b/packages/shared/package.json index 716c5b3..267da02 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/shared", "private": true, - "version": "1.0.32", + "version": "1.0.33", "type": "module", "main": "src/App.jsx", "dependencies": { diff --git a/packages/shared/src/components/Sidebar.jsx b/packages/shared/src/components/Sidebar.jsx index 6450c97..b5d1167 100644 --- a/packages/shared/src/components/Sidebar.jsx +++ b/packages/shared/src/components/Sidebar.jsx @@ -335,7 +335,7 @@ const liveBadgeStyle = { marginRight: '4px' }; -const ACTIVE_SPEAKER_SHADOW = '0 0 0 0px hsl(134.526, 41.485%, 44.902%), inset 0 0 0 2px hsl(134.526, 41.485%, 44.902%), inset 0 0 0 3px hsl(240, 7.143%, 10.98%)'; +const ACTIVE_SPEAKER_SHADOW = 'rgb(67, 162, 90) 0px 0px 0px 2px, rgb(67, 162, 90) 0px 0px 0px 20px inset, rgb(26, 26, 30) 0px 0px 0px 20px inset'; const VOICE_ACTIVE_COLOR = 'hsl(132.809, 34.902%, 50%)'; async function encryptKeyForUsers(convex, channelId, keyHex, crypto) { @@ -1135,7 +1135,8 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam size={24} style={{ marginRight: 8, - boxShadow: activeSpeakers.has(user.userId) ? ACTIVE_SPEAKER_SHADOW : 'none' + boxShadow: activeSpeakers.has(user.userId) ? ACTIVE_SPEAKER_SHADOW : 'none', + transition: 'box-shadow 0.15s ease', }} /> {user.displayName || user.username} @@ -1182,6 +1183,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam style={{ boxShadow: activeSpeakers.has(user.userId) ? ACTIVE_SPEAKER_SHADOW : 'none', borderRadius: '50%', + transition: 'box-shadow 0.15s ease', }} /> diff --git a/packages/shared/src/components/VoiceStage.jsx b/packages/shared/src/components/VoiceStage.jsx index 0c5370d..46601e0 100644 --- a/packages/shared/src/components/VoiceStage.jsx +++ b/packages/shared/src/components/VoiceStage.jsx @@ -32,6 +32,8 @@ const getUserColor = (username) => { }; // Style constants +const ACTIVE_SPEAKER_SHADOW = 'rgb(67, 162, 90) 0px 0px 0px 2px, rgb(67, 162, 90) 0px 0px 0px 20px inset, rgb(26, 26, 30) 0px 0px 0px 20px inset'; + const LIVE_BADGE_STYLE = { backgroundColor: '#ed4245', borderRadius: '4px', padding: '2px 6px', color: 'white', fontSize: '11px', fontWeight: 'bold', @@ -87,7 +89,7 @@ const ConnectionQualityIcon = ({ quality }) => { const ParticipantTile = ({ participant, username, avatarUrl }) => { const cameraTrack = useParticipantTrack(participant, 'camera'); - const { isPersonallyMuted, voiceStates, connectionQualities } = useVoice(); + const { isPersonallyMuted, voiceStates, connectionQualities, activeSpeakers } = useVoice(); const isMicEnabled = participant.isMicrophoneEnabled; const isPersonalMuted = isPersonallyMuted(participant.identity); const displayName = username || participant.identity; @@ -109,6 +111,8 @@ const ParticipantTile = ({ participant, username, avatarUrl }) => { width: '100%', height: '100%', aspectRatio: '16/9', + boxShadow: activeSpeakers.has(participant.identity) ? ACTIVE_SPEAKER_SHADOW : 'none', + transition: 'box-shadow 0.15s ease', }}> {cameraTrack ? ( @@ -617,6 +621,32 @@ const FocusedStreamView = ({ ); }; +const PreviewParticipantTile = ({ username, displayName, avatarUrl }) => { + const name = displayName || username; + return ( +
+ + + {name} + +
+ ); +}; + // --- Main Component --- const VoiceStage = ({ room, channelId, voiceStates, channelName }) => { @@ -890,8 +920,31 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => { fontSize: '14px', marginBottom: '24px' }}> - No one is currently in voice + {voiceUsers.length === 0 + ? 'No one is currently in voice' + : voiceUsers.length === 1 + ? '1 person is in voice' + : `${voiceUsers.length} people are in voice`}

+ {voiceUsers.length > 0 && ( +
+ {voiceUsers.map(u => ( + + ))} +
+ )}