Files
timmy-tower/artifacts/mobile/app/settings.tsx

250 lines
9.1 KiB
TypeScript

import { Stack } from 'expo-router';
import { View, Text, StyleSheet, ScrollView, TextInput, Switch, Pressable, Linking, Platform } from 'react-native';
import { useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
import Constants from 'expo-constants';
import { useTimmy } from '@/context/TimmyContext';
import { Ionicons } from '@expo/vector-icons';
import { ConnectionBadge } from '@/components/ConnectionBadge';
import { Colors } from '@/constants/colors';
const STORAGE_KEYS = {
SERVER_URL: 'settings_server_url',
NOTIFICATIONS_JOB_COMPLETION: 'settings_notifications_job_completion',
NOTIFICATIONS_LOW_BALANCE: 'settings_notifications_low_balance',
NOSTR_PRIVATE_KEY: 'settings_nostr_private_key', // Use SecureStore for this
};
export default function SettingsScreen() {
const { apiBaseUrl, setApiBaseUrl, isConnected, nostrPublicKey, connectNostr, disconnectNostr } = useTimmy();
const C = Colors.dark;
const [serverUrl, setServerUrl] = useState(apiBaseUrl);
const [jobCompletionNotifications, setJobCompletionNotifications] = useState(false);
const [lowBalanceWarning, setLowBalanceWarning] = useState(false);
const [currentNpub, setCurrentNpub] = useState<string | null>(nostrPublicKey);
useEffect(() => {
// Load settings from AsyncStorage and SecureStore
const loadSettings = async () => {
const storedServerUrl = await AsyncStorage.getItem(STORAGE_KEYS.SERVER_URL);
if (storedServerUrl) {
setServerUrl(storedServerUrl);
}
const storedJobCompletion = await AsyncStorage.getItem(STORAGE_KEYS.NOTIFICATIONS_JOB_COMPLETION);
if (storedJobCompletion !== null) {
setJobCompletionNotifications(JSON.parse(storedJobCompletion));
}
const storedLowBalance = await AsyncStorage.getItem(STORAGE_KEYS.NOTIFICATIONS_LOW_BALANCE);
if (storedLowBalance !== null) {
setLowBalanceWarning(JSON.parse(storedLowBalance));
}
// Nostr npub is handled by TimmyContext, so we just use the provided nostrPublicKey
setCurrentNpub(nostrPublicKey);
};
loadSettings();
}, [nostrPublicKey]);
// Update apiBaseUrl in context when serverUrl changes and is saved
useEffect(() => {
if (serverUrl !== apiBaseUrl) {
setApiBaseUrl(serverUrl);
AsyncStorage.setItem(STORAGE_KEYS.SERVER_URL, serverUrl);
}
}, [serverUrl, setApiBaseUrl, apiBaseUrl]);
const handleServerUrlChange = (text: string) => {
setServerUrl(text);
};
const toggleJobCompletionNotifications = async () => {
const newValue = !jobCompletionNotifications;
setJobCompletionNotifications(newValue);
await AsyncStorage.setItem(STORAGE_KEYS.NOTIFICATIONS_JOB_COMPLETION, JSON.stringify(newValue));
};
const toggleLowBalanceWarning = async () => {
const newValue = !lowBalanceWarning;
setLowBalanceWarning(newValue);
await AsyncStorage.setItem(STORAGE_KEYS.NOTIFICATIONS_LOW_BALANCE, JSON.stringify(newValue));
};
const handleConnectNostr = async () => {
// This will ideally link to a dedicated Nostr connection flow
console.log('Connect Nostr button pressed');
// For now, simulate connection if not connected
if (!currentNpub) {
// This is a placeholder. Real implementation would involve generating/importing keys.
const simulatedNpub = 'npub1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
connectNostr(simulatedNpub, 'private_key_placeholder'); // Pass a placeholder private key
setCurrentNpub(simulatedNpub);
// In a real app, the private key would be securely stored and managed by the context
// For now, just a placeholder to show connected state
}
};
const handleDisconnectNostr = async () => {
await disconnectNostr();
setCurrentNpub(null);
};
const appVersion = Constants.expoConfig?.version || 'N/A';
const buildCommitHash = Constants.expoConfig?.extra?.gitCommitHash || 'N/A';
const giteaRepoUrl = 'http://143.198.27.163:3000/replit/timmy-tower';
const openGiteaLink = () => {
Linking.openURL(giteaRepoUrl);
};
return (
<View style={styles.container}>
<Stack.Screen options={{ title: 'Settings', headerShown: true, headerStyle: { backgroundColor: C.surface }, headerTintColor: C.text }} />
<ScrollView contentContainerStyle={styles.scrollContent}>
<Text style={styles.sectionHeader}>Connection</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>Server URL</Text>
<View style={styles.serverUrlContainer}>
<TextInput
style={[styles.input, { color: C.text, backgroundColor: C.field }]} // Apply text and background color from Colors
value={serverUrl}
onChangeText={handleServerUrlChange}
placeholder="Enter server URL"
placeholderTextColor={C.textMuted}
autoCapitalize="none"
autoCorrect={false}
/>
<ConnectionBadge isConnected={isConnected} />
</View>
</View>
<Text style={styles.sectionHeader}>Notifications</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>Job Completion Push Notifications</Text>
<Switch
trackColor={{ false: C.surface, true: C.accentGlow }}
thumbColor={Platform.OS === 'android' ? C.text : ''}
ios_backgroundColor={C.field}
onValueChange={toggleJobCompletionNotifications}
value={jobCompletionNotifications}
/>
</View>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>Low Balance Warning</Text>
<Switch
trackColor={{ false: C.surface, true: C.accentGlow }}
thumbColor={Platform.OS === 'android' ? C.text : ''}
ios_backgroundColor={C.field}
onValueChange={toggleLowBalanceWarning}
value={lowBalanceWarning}
/>
</View>
<Text style={styles.sectionHeader}>Identity</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>Nostr Public Key</Text>
<Text style={[styles.settingValue, { color: C.textMuted }]}>
{currentNpub ? `${currentNpub.substring(0, 10)}...${currentNpub.substring(currentNpub.length - 5)}` : 'Not connected'}
</Text>
</View>
<View style={styles.buttonContainer}>
{!currentNpub ? (
<Pressable onPress={handleConnectNostr} style={({ pressed }) => [styles.button, { backgroundColor: C.accent, opacity: pressed ? 0.8 : 1 }]}>
<Text style={[styles.buttonText, { color: C.textInverted }]}>Connect Nostr</Text>
</Pressable>
) : (
<Pressable onPress={handleDisconnectNostr} style={({ pressed }) => [styles.button, { backgroundColor: C.destructive, opacity: pressed ? 0.8 : 1 }]}>
<Text style={[styles.buttonText, { color: C.textInverted }]}>Disconnect Nostr</Text>
</Pressable>
)}
</View>
<Text style={styles.sectionHeader}>About</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>App Version</Text>
<Text style={[styles.settingValue, { color: C.text }]}>{appVersion}</Text>
</View>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>Build Commit Hash</Text>
<Text style={[styles.settingValue, { color: C.text }]}>{buildCommitHash}</Text>
</View>
<Pressable onPress={openGiteaLink} style={({ pressed }) => [styles.linkButton, { opacity: pressed ? 0.8 : 1 }]}>
<Ionicons name="link" size={16} color={C.text} />
<Text style={[styles.linkButtonText, { color: C.link }]}>View project on Gitea</Text>
</Pressable>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.dark.background, // Use background color from Colors
},
scrollContent: {
padding: 20,
paddingBottom: 40,
},
sectionHeader: {
fontSize: 18,
fontWeight: 'bold',
color: Colors.dark.text,
marginTop: 20,
marginBottom: 10,
},
settingItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 0.5,
borderBottomColor: Colors.dark.border,
},
settingLabel: {
fontSize: 16,
color: Colors.dark.text,
flex: 1,
},
settingValue: {
fontSize: 16,
},
serverUrlContainer: {
flexDirection: 'row',
alignItems: 'center',
flex: 2,
},
input: {
flex: 1,
borderWidth: 1,
borderColor: Colors.dark.border,
borderRadius: 8,
padding: 8,
fontSize: 14,
marginRight: 10,
},
buttonContainer: {
marginTop: 20,
alignItems: 'flex-start',
},
button: {
paddingVertical: 10,
paddingHorizontal: 15,
borderRadius: 8,
},
buttonText: {
fontSize: 16,
fontWeight: 'bold',
},
linkButton: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 15,
paddingVertical: 8,
},
linkButtonText: {
marginLeft: 5,
fontSize: 16,
},
});