fuzzyworld

north-star / commit

minor ui changes

018eb20f13341294f6f7e1b016489cc975773c30 ยท Pavel Safronov ยท 2026-03-18 23:08:06 -0700 ยท parents 1ca0cf1b8ceb

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>
     );