fuzzyworld

north-star / commit

bug fixes and a favicon addition

acdf71c9f0ee0401a447ac3f0db018da5c604e79 · Pavel Safronov · 2026-03-25 12:10:34 -0700 · parents 223a90a985df

bug fixes and a favicon addition
 mobile/App.tsx                             |  54 +++++++++--------
 mobile/src/platform/geolocation.ts         |   2 +-
 mobile/src/screens/room_session_screen.tsx |  91 +++++++++++++++++++++++++++--
 src/assets/favicon.png                     | Bin 0 -> 1530 bytes
 src/index.html                             |   2 +-
 5 files changed, 118 insertions(+), 31 deletions(-)

diff --git a/mobile/App.tsx b/mobile/App.tsx
index 38a5165..307bc01 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -5,7 +5,7 @@ import type { SymmetricKey } from './src/core/crypto';
 import { loadProfile, saveProfile, loadLastRoom, saveLastRoom } from './src/platform/profile';
 import { OnboardingScreen } from './src/screens/onboarding_screen';
 import { RoomSessionScreen } from './src/screens/room_session_screen';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
 import { requestLocationPermission, watchPosition, type LatLon } from './src/platform/geolocation';
 
 const createDefaultDisplayName = () => `mobile-${Math.floor(1000 + Math.random() * 9000)}`;
@@ -55,30 +55,34 @@ export default function App() {
   };
 
   return (
-    <SafeAreaView style={styles.root}>
-      <StatusBar style="dark" />
-      {!activeRoom && (
-        <OnboardingScreen
-          displayName={displayName}
-          displayColor={displayColor}
-          onChangeDisplayName={setDisplayName}
-          onChangeDisplayColor={setDisplayColor}
-          onStartRoom={handleStartRoom}
-          lastRoomHash={lastRoomHash}
-        />
-      )}
-      {!!activeRoom && (
-        <RoomSessionScreen
-          roomHash={activeRoom.roomHash}
-          roomKey={activeRoom.roomKey}
-          displayName={displayName}
-          displayColor={displayColor}
-          position={position}
-          locationStatus={locationStatus}
-          onLeaveRoom={() => setActiveRoom(null)}
-        />
-      )}
-    </SafeAreaView>
+    <SafeAreaProvider>
+      <SafeAreaView style={styles.root}>
+        <StatusBar style="dark" />
+        {!activeRoom && (
+          <OnboardingScreen
+            displayName={displayName}
+            displayColor={displayColor}
+            onChangeDisplayName={setDisplayName}
+            onChangeDisplayColor={setDisplayColor}
+            onStartRoom={handleStartRoom}
+            lastRoomHash={lastRoomHash}
+          />
+        )}
+        {!!activeRoom && (
+          <RoomSessionScreen
+            roomHash={activeRoom.roomHash}
+            roomKey={activeRoom.roomKey}
+            displayName={displayName}
+            displayColor={displayColor}
+            position={position}
+            locationStatus={locationStatus}
+            onChangeDisplayName={setDisplayName}
+            onChangeDisplayColor={setDisplayColor}
+            onLeaveRoom={() => setActiveRoom(null)}
+          />
+        )}
+      </SafeAreaView>
+    </SafeAreaProvider>
   );
 }
 
diff --git a/mobile/src/platform/geolocation.ts b/mobile/src/platform/geolocation.ts
index 25162c0..1c38f43 100644
--- a/mobile/src/platform/geolocation.ts
+++ b/mobile/src/platform/geolocation.ts
@@ -45,6 +45,6 @@ export const watchPosition = (
         });
 
     return () => {
-        subscription?.remove();
+        try { subscription?.remove(); } catch { /* expo-location web cleanup bug */ }
     };
 };
diff --git a/mobile/src/screens/room_session_screen.tsx b/mobile/src/screens/room_session_screen.tsx
index 08739fb..7ffc618 100644
--- a/mobile/src/screens/room_session_screen.tsx
+++ b/mobile/src/screens/room_session_screen.tsx
@@ -3,11 +3,13 @@ import {
     ActivityIndicator,
     Image,
     PanResponder,
+    Platform,
     Pressable,
     ScrollView,
     Share,
     StyleSheet,
     Text,
+    TextInput,
     View
 } from 'react-native';
 import AsyncStorage from '@react-native-async-storage/async-storage';
@@ -31,6 +33,13 @@ const NORTH_YARDS_LAT_OFFSET = 91.44 / 111320;
 // Approximate height of a friend row (pt) used for drag-index calculations
 const ROW_HEIGHT = 44;
 
+const PRESET_COLORS = [
+    '#e53935', '#f06292', '#ab47bc', '#7986cb', '#42a5f5',
+    '#26c6da', '#26a69a', '#66bb6a', '#d4e157', '#ffca28',
+    '#ffa726', '#ff7043', '#8d6e63', '#78909c', '#546e7a',
+    '#3366cc', '#6a1b9a', '#00695c', '#558b2f', '#4e342e',
+];
+
 type RoomSessionScreenProps = {
     roomHash: string;
     roomKey: SymmetricKey | null;
@@ -38,6 +47,8 @@ type RoomSessionScreenProps = {
     displayColor: string;
     position: LatLon | null;
     locationStatus: 'idle' | 'denied' | 'watching';
+    onChangeDisplayName: (name: string) => void;
+    onChangeDisplayColor: (color: string) => void;
     onLeaveRoom: () => void;
 };
 
@@ -56,6 +67,8 @@ export function RoomSessionScreen({
     displayColor,
     position,
     locationStatus,
+    onChangeDisplayName,
+    onChangeDisplayColor,
     onLeaveRoom
 }: RoomSessionScreenProps) {
     const [firebaseSession, setFirebaseSession] = useState<FirebaseSession | null>(null);
@@ -74,6 +87,7 @@ export function RoomSessionScreen({
     const [scrollEnabled, setScrollEnabled] = useState(true);
     const scrollRef = useRef<ScrollView>(null);
     const dragActiveRef = useRef<{ fromIndex: number; toIndex: number } | null>(null);
+    const [profileExpanded, setProfileExpanded] = useState(false);
 
     const parsedRoom = useMemo(() => parseRoomFromHash(roomHash), [roomHash]);
     const inviteUrl = `${PUBLIC_SETTINGS.links.gitlabPagesBaseUrl}#${roomHash}`;
@@ -211,8 +225,9 @@ export function RoomSessionScreen({
         }).catch(() => { /* ignore */ });
     }, [hiddenStorageKey]);
 
-    // Raw sensor subscriptions for debug display
+    // Raw sensor subscriptions for debug display (native only — expo-sensors is not supported on web)
     useEffect(() => {
+        if (Platform.OS === 'web') return;
         Magnetometer.setUpdateInterval(200);
         Accelerometer.setUpdateInterval(200);
         const subMag = Magnetometer.addListener(({ x, y, z }) => setRawMag({ x, y, z }));
@@ -311,8 +326,7 @@ export function RoomSessionScreen({
                 />
                 <View style={styles.headerRow}>
                     <View>
-                        <Text style={styles.title}>Room Session</Text>
-                        <Text style={styles.subtitle}>Room id: {parsedRoom?.roomId ?? 'invalid'}</Text>
+                        <Text style={styles.title}>Room: {parsedRoom?.roomId ?? 'invalid'}</Text>
                     </View>
                     <Pressable onPress={onLeaveRoom} style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}>
                         <Text style={styles.secondaryButtonText}>Leave</Text>
@@ -334,7 +348,7 @@ export function RoomSessionScreen({
                     {locationStatus === 'watching' && !position && <Text style={styles.valueMuted}>waiting for GPS…</Text>}
                     <View style={{ flex: 1 }} />
                     <Pressable onPress={calibrate} style={({ pressed }) => [styles.calibrateButton, pressed && styles.buttonPressed]}>
-                        <Text style={styles.calibrateButtonText}>Calibrate</Text>
+                        <Text style={styles.calibrateButtonText}>Calibrate North</Text>
                     </Pressable>
                 </View>
                 <ArrowsPanel friends={arrowFriends} />
@@ -377,6 +391,42 @@ export function RoomSessionScreen({
                 {!!syncError && <Text style={styles.errorText}>{syncError}</Text>}
             </View>
 
+            {/* ── Profile card ── */}
+            <View style={styles.card}>
+                <Pressable onPress={() => setProfileExpanded(e => !e)} style={styles.profileHeader}>
+                    <View style={[styles.friendDot, { backgroundColor: normalizeColor(displayColor), width: 16, height: 16, borderRadius: 8 }]} />
+                    <Text style={styles.label}>{displayName || 'Your profile'}</Text>
+                    <View style={{ flex: 1 }} />
+                    <Text style={styles.valueMuted}>{profileExpanded ? '▲' : '▼'}</Text>
+                </Pressable>
+                {profileExpanded && (
+                    <View style={styles.profileBody}>
+                        <TextInput
+                            style={styles.nameInput}
+                            value={displayName}
+                            onChangeText={onChangeDisplayName}
+                            placeholder="Your name"
+                            placeholderTextColor="#9a8f84"
+                            maxLength={40}
+                            returnKeyType="done"
+                        />
+                        <View style={styles.swatchGrid}>
+                            {PRESET_COLORS.map((color) => (
+                                <Pressable
+                                    key={color}
+                                    onPress={() => onChangeDisplayColor(color)}
+                                    style={[
+                                        styles.swatch,
+                                        { backgroundColor: color },
+                                        normalizeColor(displayColor) === color && styles.swatchSelected
+                                    ]}
+                                />
+                            ))}
+                        </View>
+                    </View>
+                )}
+            </View>
+
             {/* ── Invite QR ── */}
             <View style={styles.card}>
                 <View style={styles.qrWrap}>
@@ -579,6 +629,39 @@ const styles = StyleSheet.create({
         textDecorationLine: 'underline',
         fontWeight: '500'
     },
+    profileHeader: {
+        flexDirection: 'row',
+        alignItems: 'center',
+        gap: 10
+    },
+    profileBody: {
+        marginTop: 12,
+        gap: 12
+    },
+    nameInput: {
+        borderWidth: 1,
+        borderColor: '#c7b79d',
+        borderRadius: 8,
+        paddingHorizontal: 12,
+        paddingVertical: 8,
+        fontSize: 15,
+        color: '#2e2a23',
+        backgroundColor: '#f5f0e8'
+    },
+    swatchGrid: {
+        flexDirection: 'row',
+        flexWrap: 'wrap',
+        gap: 8
+    },
+    swatch: {
+        width: 32,
+        height: 32,
+        borderRadius: 6
+    },
+    swatchSelected: {
+        borderWidth: 3,
+        borderColor: '#2e2a23'
+    },
     debugText: {
         fontSize: 12,
         color: '#5b554c'
diff --git a/src/assets/favicon.png b/src/assets/favicon.png
new file mode 100644
index 0000000..1439ba2
Binary files /dev/null and b/src/assets/favicon.png differ
diff --git a/src/index.html b/src/index.html
index 49185ee..b1c3917 100644
--- a/src/index.html
+++ b/src/index.html
@@ -19,7 +19,7 @@
     <meta name="description"
         content="Friend Compass shows live arrows pointing toward your friends. No account needed, end-to-end encrypted, works on any phone." />
 
-    <link rel="icon" href="./assets/compass.png" />
+    <link rel="icon" href="./assets/favicon.png" type="image/png" sizes="32x32" />
     <link rel="stylesheet" href="style.css" />
 
 </head>