north-star / commit
minor ui changes
mobile/src/screens/room_screen.tsx | 423 ----------------------------- mobile/src/screens/room_session_screen.tsx | 17 +- 2 files changed, 4 insertions(+), 436 deletions(-) diff --git a/mobile/src/screens/room_screen.tsx b/mobile/src/screens/room_screen.tsx deleted file mode 100644 index a2b9021..0000000 --- a/mobile/src/screens/room_screen.tsx +++ /dev/null @@ -1,423 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { - ActivityIndicator, - Pressable, - ScrollView, - StyleSheet, - Text, - TextInput, - View -} from 'react-native'; -import { formatDistance, getBearingDeg, getDistanceMeters, getRelativeBearingDeg } from '../core/bearing'; -import { buildRoomHash, parseRoomFromHash } from '../core/room'; -import { generateRoomKey, exportRoomKey, importRoomKey, type SymmetricKey } from '../core/crypto'; -import { watchHeading } from '../platform/heading'; -import { loadProfile, saveProfile } from '../platform/profile'; -import { subscribeToFriends, upsertSelfFriend, type MobileFriendRecord } from '../features/friends/sync'; -import { initFirebaseSession, type FirebaseSession } from '../platform/firebase'; -import { requestLocationPermission, watchPosition, type LatLon } from '../platform/geolocation'; -import { PUBLIC_SETTINGS } from '../public_settings'; - -const MOBILE_SYNC_INTERVAL_MS = 10000; - -const randomRoomId = () => { - const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; - let out = ''; - for (let i = 0; i < PUBLIC_SETTINGS.room.idLength; i += 1) { - out += alphabet[Math.floor(Math.random() * alphabet.length)]; - } - return out; -}; - -const createDefaultDisplayName = () => `mobile-${Math.floor(1000 + Math.random() * 9000)}`; - -const normalizeColor = (value: string) => { - const candidate = value.trim().toLowerCase(); - if (!/^#[0-9a-f]{6}$/.test(candidate)) return '#3366cc'; - return candidate; -}; - -export function RoomScreen() { - const [roomHashInput, setRoomHashInput] = useState(''); - const [activeRoomHash, setActiveRoomHash] = useState<string | null>(null); - const [firebaseSession, setFirebaseSession] = useState<FirebaseSession | null>(null); - const [isConnectingFirebase, setIsConnectingFirebase] = useState(false); - const [firebaseError, setFirebaseError] = useState<string | null>(null); - - const [displayName, setDisplayName] = useState(createDefaultDisplayName()); - const [displayColor, setDisplayColor] = useState('#3366cc'); - const [isRoomSyncEnabled, setIsRoomSyncEnabled] = useState(false); - const [friends, setFriends] = useState<MobileFriendRecord[]>([]); - const [roomSyncError, setRoomSyncError] = useState<string | null>(null); - const [position, setPosition] = useState<LatLon | null>(null); - const [locationStatus, setLocationStatus] = useState<'idle' | 'denied' | 'watching'>('idle'); - const [headingDeg, setHeadingDeg] = useState(0); - const [roomKey, setRoomKey] = useState<SymmetricKey | null>(null); - - const parsedRoom = useMemo(() => { - if (!activeRoomHash) return null; - return parseRoomFromHash(activeRoomHash); - }, [activeRoomHash]); - - // Load saved profile on first mount - useEffect(() => { - loadProfile().then((saved) => { - if (saved) { - setDisplayName(saved.name); - setDisplayColor(saved.color); - } - }).catch(() => { /* ignore storage errors on load */ }); - }, []); - - // Persist profile whenever name or color changes (debounced 1 s) - useEffect(() => { - const id = setTimeout(() => { - void saveProfile(displayName, displayColor); - }, 1000); - return () => clearTimeout(id); - }, [displayName, displayColor]); - - const createRoom = async () => { - const roomId = randomRoomId(); - const key = generateRoomKey(); - setRoomKey(key); - - const keyB64 = await exportRoomKey(key); - const hash = buildRoomHash(roomId, keyB64); - setActiveRoomHash(hash); - setRoomHashInput(''); - - // Log the URL for the web app to join this room - const webAppUrl = `http://localhost:5173/#${hash}`; - console.log(`๐ฑ Encrypted room created! Join on web app:\n${webAppUrl}`); - console.log(`Replace localhost:5173 with your web app URL if needed.`); - }; - - const joinFromInput = async () => { - const normalized = roomHashInput.trim().replace(/^#/, ''); - const parsed = parseRoomFromHash(normalized); - if (!parsed) return; - setActiveRoomHash(buildRoomHash(parsed.roomId, parsed.roomKeyBase64)); - setRoomHashInput(''); - - // Import encryption key if present - if (parsed.roomKeyBase64) { - const key = await importRoomKey(parsed.roomKeyBase64); - setRoomKey(key); - } else { - setRoomKey(null); - } - }; - - const connectFirebase = async () => { - setIsConnectingFirebase(true); - setFirebaseError(null); - try { - const session = await initFirebaseSession(); - setFirebaseSession(session); - } catch (error) { - const nextError = error instanceof Error ? error.message : 'Unknown Firebase error'; - setFirebaseError(nextError); - } finally { - setIsConnectingFirebase(false); - } - }; - - useEffect(() => { - if (!isRoomSyncEnabled) return; - - let stopWatching: (() => void) | null = null; - - const start = async () => { - const granted = await requestLocationPermission(); - if (!granted) { - setLocationStatus('denied'); - return; - } - setLocationStatus('watching'); - stopWatching = watchPosition( - (nextPosition) => setPosition(nextPosition), - (message) => setRoomSyncError(message) - ); - }; - - void start(); - - return () => { - stopWatching?.(); - setLocationStatus('idle'); - }; - }, [isRoomSyncEnabled]); - - useEffect(() => { - if (!isRoomSyncEnabled) return; - const stop = watchHeading( - (deg) => setHeadingDeg(deg), - (msg) => setRoomSyncError(msg) - ); - return stop; - }, [isRoomSyncEnabled]); - - useEffect(() => { - if (!isRoomSyncEnabled || !firebaseSession || !parsedRoom) { - setFriends([]); - return; - } - - let isDisposed = false; - const roomId = parsedRoom.roomId; - - const writeSelf = async (currentPosition: LatLon) => { - try { - await upsertSelfFriend({ - db: firebaseSession.db, - roomId, - uid: firebaseSession.uid, - name: displayName.trim() || createDefaultDisplayName(), - color: normalizeColor(displayColor), - position: currentPosition, - roomKey - }); - } catch (error) { - const nextError = error instanceof Error ? error.message : 'Unknown room sync write error'; - if (!isDisposed) setRoomSyncError(nextError); - } - }; - - setRoomSyncError(null); - if (position) void writeSelf(position); - - const intervalId = setInterval(() => { - if (position) void writeSelf(position); - }, MOBILE_SYNC_INTERVAL_MS); - - const unsubscribe = subscribeToFriends({ - db: firebaseSession.db, - roomId, - roomKey, - onUpdate: (nextFriends) => { - if (!isDisposed) setFriends(nextFriends); - }, - onError: (error) => { - if (!isDisposed) setRoomSyncError(error.message); - } - }); - - return () => { - isDisposed = true; - clearInterval(intervalId); - unsubscribe(); - }; - }, [displayColor, displayName, firebaseSession, isRoomSyncEnabled, parsedRoom, position, roomKey]); - - return ( - <ScrollView contentContainerStyle={styles.container}> - <Text style={styles.title}>Friend Compass Mobile</Text> - <Text style={styles.subtitle}>Migration slice: room creation and realtime sync</Text> - - <View style={styles.card}> - <Text style={styles.label}>Create a room</Text> - <Pressable onPress={createRoom} style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}> - <Text style={styles.buttonText}>Create new room</Text> - </Pressable> - </View> - - <View style={styles.card}> - <Text style={styles.label}>Join from room hash</Text> - <TextInput - value={roomHashInput} - onChangeText={setRoomHashInput} - autoCapitalize="none" - autoCorrect={false} - placeholder="example: ab12cd34" - placeholderTextColor="#7a7469" - style={styles.input} - /> - <Pressable onPress={joinFromInput} style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}> - <Text style={styles.buttonText}>Join room</Text> - </Pressable> - </View> - - <View style={styles.resultCard}> - <Text style={styles.label}>Current room state</Text> - {!activeRoomHash && <Text style={styles.valueMuted}>No room selected yet.</Text>} - {!!activeRoomHash && ( - <> - <Text style={styles.value}>Hash: {activeRoomHash}</Text> - <Text style={styles.value}>Room id: {parsedRoom?.roomId ?? 'invalid'}</Text> - <Text style={styles.value}>Encryption: {roomKey ? '๐ enabled' : 'none'}</Text> - </> - )} - </View> - - <View style={styles.card}> - <Text style={styles.label}>Firebase session</Text> - <Pressable - onPress={connectFirebase} - disabled={isConnectingFirebase} - style={({ pressed }) => [styles.button, (pressed || isConnectingFirebase) && styles.buttonPressed]} - > - <Text style={styles.buttonText}> - {isConnectingFirebase ? 'Connecting...' : firebaseSession ? 'Reconnect Firebase' : 'Connect Firebase'} - </Text> - </Pressable> - {isConnectingFirebase && <ActivityIndicator color="#7a4f2a" />} - {!!firebaseSession && <Text style={styles.value}>Signed in uid: {firebaseSession.uid}</Text>} - {!!firebaseError && <Text style={styles.errorText}>Firebase error: {firebaseError}</Text>} - </View> - - <View style={styles.card}> - <Text style={styles.label}>Realtime room sync</Text> - <TextInput - value={displayName} - onChangeText={setDisplayName} - autoCapitalize="none" - autoCorrect={false} - placeholder="display name" - placeholderTextColor="#7a7469" - style={styles.input} - /> - <TextInput - value={displayColor} - onChangeText={setDisplayColor} - autoCapitalize="none" - autoCorrect={false} - placeholder="#3366cc" - placeholderTextColor="#7a7469" - style={styles.input} - /> - <Pressable - onPress={() => setIsRoomSyncEnabled((prev) => !prev)} - disabled={!firebaseSession || !parsedRoom} - style={({ pressed }) => [ - styles.button, - (!firebaseSession || !parsedRoom || pressed) && styles.buttonPressed - ]} - > - <Text style={styles.buttonText}> - {isRoomSyncEnabled ? 'Stop room sync' : 'Start room sync'} - </Text> - </Pressable> - {!firebaseSession && <Text style={styles.valueMuted}>Connect Firebase first.</Text>} - {!!firebaseSession && !parsedRoom && <Text style={styles.valueMuted}>Create or join a room first.</Text>} - {locationStatus === 'denied' && <Text style={styles.errorText}>Location permission denied.</Text>} - {locationStatus === 'watching' && ( - position - ? <Text style={styles.value}>๐ {position.lat.toFixed(5)}, {position.lon.toFixed(5)}</Text> - : <Text style={styles.valueMuted}>Waiting for GPS fix...</Text> - )} - <Text style={styles.value}>Participants in room: {friends.length}</Text> - {friends.map((friend) => { - const bearingDeg = position && friend.position - ? getBearingDeg(position, friend.position) - : null; - const arrowDeg = bearingDeg !== null - ? getRelativeBearingDeg(bearingDeg, headingDeg) - : null; - const dist = position && friend.position - ? formatDistance(getDistanceMeters(position, friend.position)) - : null; - return ( - <View key={friend.id} style={styles.friendRow}> - <View style={[styles.friendDot, { backgroundColor: friend.color ?? '#888888' }]} /> - <Text style={[styles.value, styles.friendName]}>{friend.name || friend.id}</Text> - {dist !== null && <Text style={styles.friendDist}>{dist}</Text>} - {arrowDeg !== null && ( - <Text style={[styles.friendArrow, { transform: [{ rotate: `${arrowDeg}deg` }] }]}>โ</Text> - )} - </View> - ); - })} - {!!roomSyncError && <Text style={styles.errorText}>Room sync error: {roomSyncError}</Text>} - </View> - </ScrollView> - ); -} - -const styles = StyleSheet.create({ - container: { - paddingHorizontal: 20, - paddingVertical: 28, - gap: 14 - }, - title: { - fontSize: 28, - fontWeight: '700', - color: '#2e2a23' - }, - subtitle: { - fontSize: 15, - color: '#5b554c' - }, - card: { - backgroundColor: '#ece6da', - borderRadius: 14, - padding: 16, - gap: 10 - }, - resultCard: { - backgroundColor: '#dbe3df', - borderRadius: 14, - padding: 16, - gap: 6 - }, - label: { - fontSize: 14, - fontWeight: '600', - color: '#2e2a23' - }, - input: { - backgroundColor: '#f6f1e7', - borderRadius: 10, - paddingHorizontal: 12, - paddingVertical: 10, - color: '#2e2a23' - }, - button: { - backgroundColor: '#7a4f2a', - borderRadius: 10, - alignItems: 'center', - paddingVertical: 12 - }, - buttonPressed: { - opacity: 0.85 - }, - buttonText: { - color: '#f7eee2', - fontSize: 15, - fontWeight: '700' - }, - value: { - fontSize: 14, - color: '#2e2a23' - }, - valueMuted: { - fontSize: 14, - color: '#5b554c' - }, - errorText: { - fontSize: 14, - color: '#8b2f2f' - }, - friendRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 8 - }, - friendDot: { - width: 12, - height: 12, - borderRadius: 6 - }, - friendName: { - flex: 1 - }, - friendDist: { - fontSize: 13, - color: '#7a7469' - }, - friendArrow: { - fontSize: 24, - color: '#2e2a23' - } -}); \ No newline at end of file diff --git a/mobile/src/screens/room_session_screen.tsx b/mobile/src/screens/room_session_screen.tsx index 8f1a0c7..112411d 100644 --- a/mobile/src/screens/room_session_screen.tsx +++ b/mobile/src/screens/room_session_screen.tsx @@ -309,19 +309,10 @@ export function RoomSessionScreen({ {/* โโ Invite QR โโ */} <View style={styles.card}> - <Pressable - onPress={() => setShowQr((v) => !v)} - style={({ pressed }) => [styles.qrToggleRow, pressed && styles.buttonPressed]} - > - <Text style={styles.label}>Invite others</Text> - <Text style={styles.qrToggleChevron}>{showQr ? 'โฒ' : 'โผ'}</Text> - </Pressable> - {showQr && ( - <View style={styles.qrWrap}> - <QRCode value={inviteUrl} size={200} /> - <Text style={styles.valueMuted}>Scan to join on web or mobile</Text> - </View> - )} + <View style={styles.qrWrap}> + <Text style={styles.valueMuted}>Invite others to join this room</Text> + <QRCode value={inviteUrl} size={200} /> + </View> </View> </ScrollView> );