- {viewMode === 'matrix' ? 'hover bars to decode • click + to transmit' : 'click to listen • click + to speak'}
+
{viewMode === 'matrix' ? 'hover bars to decode • click + to transmit' : 'click to listen • click + to speak'}
+
visit /roomname to join a room
);
diff --git a/src/pages/RoomPage.tsx b/src/pages/RoomPage.tsx
new file mode 100644
index 0000000..be0d350
--- /dev/null
+++ b/src/pages/RoomPage.tsx
@@ -0,0 +1,203 @@
+import { useParams } from 'react-router-dom';
+import { useEffect, useMemo } from 'react';
+import { useVoiceNotes } from '@/hooks/useVoiceNotes';
+import { useAutoAccount } from '@/hooks/useAutoAccount';
+import { usePublishProfile } from '@/hooks/usePublishProfile';
+import { VoiceNote } from '@/components/VoiceNote';
+import { MatrixVoiceBar } from '@/components/MatrixVoiceBar';
+import { RecordButton } from '@/components/RecordButton';
+import { useViewMode } from '@/contexts/ViewModeContext';
+
+export function RoomPage() {
+ const { room } = useParams<{ room: string }>();
+ const { user, isReady } = useAutoAccount();
+ const { data: voiceNotes, isLoading } = useVoiceNotes(room);
+ const { viewMode, setViewMode } = useViewMode();
+
+ // Publish profile metadata
+ usePublishProfile();
+
+ // Memoize voice notes to prevent unnecessary re-renders
+ const memoizedVoiceNotes = useMemo(() => voiceNotes || [], [voiceNotes]);
+
+ useEffect(() => {
+ // Add animation styles once
+ const style = document.createElement('style');
+ style.textContent = `
+ @keyframes fadeIn {
+ 0% {
+ opacity: 0;
+ transform: translate(-50%, -50%) scale(0.8);
+ }
+ 100% {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ }
+ }
+
+ @keyframes toastFadeIn {
+ 0% {
+ opacity: 0;
+ transform: translate(-50%, 20px) scale(0.9);
+ filter: blur(4px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translate(-50%, 0) scale(1);
+ filter: blur(0);
+ }
+ }
+
+ @keyframes breathe {
+ 0%, 100% {
+ transform: translate(-50%, -50%) scale(1);
+ opacity: inherit;
+ }
+ 50% {
+ transform: translate(-50%, -50%) scale(1.15);
+ opacity: calc(inherit * 1.3);
+ }
+ }
+
+ @keyframes slowPulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ }
+
+ @keyframes matrixFall {
+ 0% {
+ transform: translateX(-50%) translateY(-100%);
+ opacity: 0;
+ }
+ 10% {
+ opacity: 1;
+ }
+ 90% {
+ opacity: 1;
+ }
+ 100% {
+ transform: translateX(-50%) translateY(100vh);
+ opacity: 0;
+ }
+ }
+ `;
+ document.head.appendChild(style);
+
+ return () => {
+ document.head.removeChild(style);
+ };
+ }, []);
+
+ if (!isReady) {
+ return (
+
+ {/* Mode selector */}
+
+
+
+
+
+
+
+ {/* Room indicator */}
+
+
+ {/* User info */}
+
+
you are
+
{user?.name}
+
+
+ {/* Voice notes canvas */}
+
+ {viewMode === 'cosmos' ? (
+ // Cosmos mode - floating orbs
+ <>
+ {memoizedVoiceNotes.map((voiceNote) => (
+
+ ))}
+ >
+ ) : (
+ // Matrix mode - vertical bars
+
+ {/* Matrix background effect */}
+
+
+ {memoizedVoiceNotes.map((voiceNote, index) => (
+
+ ))}
+
+ )}
+
+ {isLoading && (
+
+
+ {viewMode === 'matrix' ? 'connecting to matrix...' : 'listening to the void...'}
+
+
+ )}
+
+ {!isLoading && memoizedVoiceNotes.length === 0 && (
+
+
+
{viewMode === 'matrix' ? 'no signals detected' : 'the cosmos is silent'}
+
{viewMode === 'matrix' ? 'upload your voice' : 'be the first voice'}
+
+
+ )}
+
+
+ {/* Record button */}
+
+
+ {/* Instructions */}
+
+ {viewMode === 'matrix' ? 'hover bars to decode • click + to transmit' : 'click to listen • click + to speak'}
+
+
+ );
+}
\ No newline at end of file