north-star / commit
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>