Compare commits
1 Commits
b02d34453e
...
9308fded3e
| Author | SHA1 | Date | |
|---|---|---|---|
| 9308fded3e |
@@ -21,6 +21,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
"package": "fr.gyozamancave.postizmobile",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"READ_EXTERNAL_STORAGE",
|
"READ_EXTERNAL_STORAGE",
|
||||||
"WRITE_EXTERNAL_STORAGE",
|
"WRITE_EXTERNAL_STORAGE",
|
||||||
|
|||||||
@@ -99,8 +99,9 @@ export default function ComposeScreen() {
|
|||||||
});
|
});
|
||||||
const data = await uploadRes.json() as PostizUploadResult;
|
const data = await uploadRes.json() as PostizUploadResult;
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
Alert.alert("Upload Failed", "Could not upload image. Please try again.");
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
Alert.alert("Upload Failed", `Could not upload image.\n${msg}`);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
@@ -142,9 +143,11 @@ export default function ComposeScreen() {
|
|||||||
);
|
);
|
||||||
queryClient.invalidateQueries({ queryKey: ["posts"] });
|
queryClient.invalidateQueries({ queryKey: ["posts"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["posts-list"] });
|
queryClient.invalidateQueries({ queryKey: ["posts-list"] });
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||||
Alert.alert("Failed", "Could not submit post. Please try again.");
|
let msg = "Could not submit post.";
|
||||||
|
if (e instanceof Error) msg += `\n${e.message}`;
|
||||||
|
Alert.alert("Failed", msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +18,24 @@ import { PostizPost, usePostiz } from "@/context/PostizContext";
|
|||||||
import { useColors } from "@/hooks/useColors";
|
import { useColors } from "@/hooks/useColors";
|
||||||
import { StatusBadge } from "@/components/StatusBadge";
|
import { StatusBadge } from "@/components/StatusBadge";
|
||||||
|
|
||||||
|
function extractError(err: unknown): string {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const status = err.response?.status;
|
||||||
|
const data = err.response?.data;
|
||||||
|
if (data) {
|
||||||
|
const body =
|
||||||
|
typeof data === "string"
|
||||||
|
? data.slice(0, 200)
|
||||||
|
: (data?.message ?? data?.error ?? JSON.stringify(data)).toString().slice(0, 200);
|
||||||
|
return status ? `HTTP ${status}: ${body}` : body;
|
||||||
|
}
|
||||||
|
if (status) return `HTTP ${status} — ${err.message}`;
|
||||||
|
if (err.message) return err.message;
|
||||||
|
}
|
||||||
|
if (err instanceof Error) return err.message;
|
||||||
|
return "Unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(date: Date): string {
|
function formatDate(date: Date): string {
|
||||||
const y = date.getFullYear();
|
const y = date.getFullYear();
|
||||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
@@ -189,6 +208,9 @@ export default function CalendarScreen() {
|
|||||||
<Text style={[styles.emptyText, { color: colors.mutedForeground }]}>
|
<Text style={[styles.emptyText, { color: colors.mutedForeground }]}>
|
||||||
Failed to load posts
|
Failed to load posts
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text style={[styles.emptyText, { color: colors.error, fontSize: 11 }]} selectable>
|
||||||
|
{extractError(error)}
|
||||||
|
</Text>
|
||||||
<TouchableOpacity onPress={() => refetch()} style={styles.retryBtn}>
|
<TouchableOpacity onPress={() => refetch()} style={styles.retryBtn}>
|
||||||
<Text style={[styles.retryText, { color: colors.primary }]}>Retry</Text>
|
<Text style={[styles.retryText, { color: colors.primary }]}>Retry</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
FlatList,
|
FlatList,
|
||||||
Platform,
|
Platform,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
@@ -11,11 +13,30 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { PostCard } from "@/components/PostCard";
|
import { PostCard } from "@/components/PostCard";
|
||||||
import { PostizPost, usePostiz } from "@/context/PostizContext";
|
import { PostizPost, usePostiz } from "@/context/PostizContext";
|
||||||
import { useColors } from "@/hooks/useColors";
|
import { useColors } from "@/hooks/useColors";
|
||||||
|
|
||||||
|
function extractError(err: unknown): string {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const status = err.response?.status;
|
||||||
|
const data = err.response?.data;
|
||||||
|
if (data) {
|
||||||
|
const body =
|
||||||
|
typeof data === "string"
|
||||||
|
? data.slice(0, 200)
|
||||||
|
: (data?.message ?? data?.error ?? JSON.stringify(data)).toString().slice(0, 200);
|
||||||
|
return status ? `HTTP ${status}: ${body}` : body;
|
||||||
|
}
|
||||||
|
if (status) return `HTTP ${status} — ${err.message}`;
|
||||||
|
if (err.message) return err.message;
|
||||||
|
}
|
||||||
|
if (err instanceof Error) return err.message;
|
||||||
|
return "Unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
type FilterType = "all" | "QUEUE" | "PUBLISHED" | "ERROR" | "DRAFT";
|
type FilterType = "all" | "QUEUE" | "PUBLISHED" | "ERROR" | "DRAFT";
|
||||||
|
|
||||||
const FILTERS: { key: FilterType; label: string }[] = [
|
const FILTERS: { key: FilterType; label: string }[] = [
|
||||||
@@ -74,7 +95,9 @@ export default function PostsScreen() {
|
|||||||
(old ?? []).filter((p) => p.id !== id)
|
(old ?? []).filter((p) => p.id !== id)
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries({ queryKey: ["posts"] });
|
queryClient.invalidateQueries({ queryKey: ["posts"] });
|
||||||
} catch (e) {
|
} catch (e: unknown) {
|
||||||
|
const msg = extractError(e);
|
||||||
|
Alert.alert("Delete failed", msg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,6 +178,9 @@ export default function PostsScreen() {
|
|||||||
<Text style={[styles.emptyTitle, { color: colors.foreground }]}>
|
<Text style={[styles.emptyTitle, { color: colors.foreground }]}>
|
||||||
Failed to load
|
Failed to load
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text style={[styles.emptyText, { color: colors.mutedForeground }]} selectable>
|
||||||
|
{extractError(error)}
|
||||||
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => refetch()}
|
onPress={() => refetch()}
|
||||||
style={[styles.retryBtn, { backgroundColor: colors.primary }]}
|
style={[styles.retryBtn, { backgroundColor: colors.primary }]}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import axios from "axios";
|
||||||
import * as Haptics from "expo-haptics";
|
import * as Haptics from "expo-haptics";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
Platform,
|
Platform,
|
||||||
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -15,10 +17,28 @@ import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { usePostiz } from "@/context/PostizContext";
|
import { usePostiz } from "@/context/PostizContext";
|
||||||
import { useColors } from "@/hooks/useColors";
|
import { useColors } from "@/hooks/useColors";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const DEFAULT_BASE_URL = "https://postiz.gyozamancave.fr/public/v1";
|
const DEFAULT_BASE_URL = "https://postiz.gyozamancave.fr/public/v1";
|
||||||
|
|
||||||
|
function extractAxiosError(err: unknown): string {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const status = err.response?.status;
|
||||||
|
const data = err.response?.data;
|
||||||
|
if (data) {
|
||||||
|
const body =
|
||||||
|
typeof data === "string"
|
||||||
|
? data.slice(0, 200)
|
||||||
|
: (data?.message ?? data?.error ?? JSON.stringify(data)).toString().slice(0, 200);
|
||||||
|
return status ? `HTTP ${status}: ${body}` : body;
|
||||||
|
}
|
||||||
|
if (status) return `HTTP ${status} — ${err.message}`;
|
||||||
|
if (err.code === "ECONNABORTED") return "Request timed out (10s). Check that the URL is reachable.";
|
||||||
|
if (err.message) return err.message;
|
||||||
|
}
|
||||||
|
if (err instanceof Error) return err.message;
|
||||||
|
return "Unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
const colors = useColors();
|
const colors = useColors();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -29,9 +49,8 @@ export default function SettingsScreen() {
|
|||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
const [validating, setValidating] = useState(false);
|
const [validating, setValidating] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [validationStatus, setValidationStatus] = useState<
|
const [validationStatus, setValidationStatus] = useState<"idle" | "ok" | "error">("idle");
|
||||||
"idle" | "ok" | "error"
|
const [errorDetail, setErrorDetail] = useState<string>("");
|
||||||
>("idle");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInputKey(apiKey);
|
setInputKey(apiKey);
|
||||||
@@ -45,19 +64,48 @@ export default function SettingsScreen() {
|
|||||||
}
|
}
|
||||||
setValidating(true);
|
setValidating(true);
|
||||||
setValidationStatus("idle");
|
setValidationStatus("idle");
|
||||||
|
setErrorDetail("");
|
||||||
|
const cleanUrl = inputUrl.trim().replace(/\/$/, "");
|
||||||
|
|
||||||
|
const authVariants = [
|
||||||
|
inputKey.trim(),
|
||||||
|
`Bearer ${inputKey.trim()}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastError: string = "";
|
||||||
|
|
||||||
|
for (const authHeader of authVariants) {
|
||||||
try {
|
try {
|
||||||
await axios.get(`${inputUrl.replace(/\/$/, "")}/integrations`, {
|
await axios.get(`${cleanUrl}/integrations`, {
|
||||||
headers: { Authorization: inputKey.trim() },
|
headers: { Authorization: authHeader },
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
|
maxRedirects: 0,
|
||||||
});
|
});
|
||||||
setValidationStatus("ok");
|
setValidationStatus("ok");
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
} catch {
|
setValidating(false);
|
||||||
|
return;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const status = err.response?.status;
|
||||||
|
if (status === 307 || status === 301 || status === 302 || status === 308) {
|
||||||
|
const location = err.response?.headers?.location ?? "unknown";
|
||||||
|
lastError = `HTTP ${status} redirect → ${location}. The API rejected the request and redirected to login. Check the Authorization header format or the base URL.`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
lastError = `HTTP ${status}: Invalid or expired API key.`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastError = extractAxiosError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorDetail(lastError);
|
||||||
setValidationStatus("error");
|
setValidationStatus("error");
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||||
} finally {
|
|
||||||
setValidating(false);
|
setValidating(false);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -70,8 +118,8 @@ export default function SettingsScreen() {
|
|||||||
await saveSettings(inputKey.trim(), inputUrl.trim().replace(/\/$/, ""));
|
await saveSettings(inputKey.trim(), inputUrl.trim().replace(/\/$/, ""));
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
Alert.alert("Saved", "Settings saved successfully.");
|
Alert.alert("Saved", "Settings saved successfully.");
|
||||||
} catch {
|
} catch (err: unknown) {
|
||||||
Alert.alert("Error", "Failed to save settings.");
|
Alert.alert("Error", `Failed to save settings.\n${extractAxiosError(err)}`);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -91,6 +139,7 @@ export default function SettingsScreen() {
|
|||||||
setInputKey("");
|
setInputKey("");
|
||||||
setInputUrl(DEFAULT_BASE_URL);
|
setInputUrl(DEFAULT_BASE_URL);
|
||||||
setValidationStatus("idle");
|
setValidationStatus("idle");
|
||||||
|
setErrorDetail("");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -104,8 +153,7 @@ export default function SettingsScreen() {
|
|||||||
styles.container,
|
styles.container,
|
||||||
{
|
{
|
||||||
paddingTop: Platform.OS === "web" ? 67 : 24,
|
paddingTop: Platform.OS === "web" ? 67 : 24,
|
||||||
paddingBottom:
|
paddingBottom: Platform.OS === "web" ? 100 : insets.bottom + 40,
|
||||||
Platform.OS === "web" ? 100 : insets.bottom + 40,
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
bottomOffset={60}
|
bottomOffset={60}
|
||||||
@@ -131,28 +179,15 @@ export default function SettingsScreen() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={[styles.label, { color: colors.mutedForeground }]}>
|
<Text style={[styles.label, { color: colors.mutedForeground }]}>BASE URL</Text>
|
||||||
BASE URL
|
<View style={[styles.inputWrap, { backgroundColor: colors.card, borderColor: colors.border }]}>
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.inputWrap,
|
|
||||||
{
|
|
||||||
backgroundColor: colors.card,
|
|
||||||
borderColor: colors.border,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Feather name="globe" size={16} color={colors.mutedForeground} style={styles.inputIcon} />
|
<Feather name="globe" size={16} color={colors.mutedForeground} style={styles.inputIcon} />
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, { color: colors.foreground }]}
|
style={[styles.input, { color: colors.foreground }]}
|
||||||
placeholder="https://postiz.example.com/public/v1"
|
placeholder="https://postiz.example.com/public/v1"
|
||||||
placeholderTextColor={colors.mutedForeground}
|
placeholderTextColor={colors.mutedForeground}
|
||||||
value={inputUrl}
|
value={inputUrl}
|
||||||
onChangeText={(t) => {
|
onChangeText={(t) => { setInputUrl(t); setValidationStatus("idle"); setErrorDetail(""); }}
|
||||||
setInputUrl(t);
|
|
||||||
setValidationStatus("idle");
|
|
||||||
}}
|
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
keyboardType="url"
|
keyboardType="url"
|
||||||
@@ -161,40 +196,24 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={[styles.label, { color: colors.mutedForeground }]}>
|
<Text style={[styles.label, { color: colors.mutedForeground }]}>API KEY</Text>
|
||||||
API KEY
|
<View style={[styles.inputWrap, { backgroundColor: colors.card, borderColor: colors.border }]}>
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.inputWrap,
|
|
||||||
{
|
|
||||||
backgroundColor: colors.card,
|
|
||||||
borderColor: colors.border,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Feather name="key" size={16} color={colors.mutedForeground} style={styles.inputIcon} />
|
<Feather name="key" size={16} color={colors.mutedForeground} style={styles.inputIcon} />
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, { color: colors.foreground }]}
|
style={[styles.input, { color: colors.foreground }]}
|
||||||
placeholder="Enter your API key"
|
placeholder="Enter your API key"
|
||||||
placeholderTextColor={colors.mutedForeground}
|
placeholderTextColor={colors.mutedForeground}
|
||||||
value={inputKey}
|
value={inputKey}
|
||||||
onChangeText={(t) => {
|
onChangeText={(t) => { setInputKey(t); setValidationStatus("idle"); setErrorDetail(""); }}
|
||||||
setInputKey(t);
|
|
||||||
setValidationStatus("idle");
|
|
||||||
}}
|
|
||||||
secureTextEntry={!showKey}
|
secureTextEntry={!showKey}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity onPress={() => setShowKey((v) => !v)} activeOpacity={0.7}>
|
<TouchableOpacity onPress={() => setShowKey((v) => !v)} activeOpacity={0.7}>
|
||||||
<Feather
|
<Feather name={showKey ? "eye-off" : "eye"} size={16} color={colors.mutedForeground} />
|
||||||
name={showKey ? "eye-off" : "eye"}
|
|
||||||
size={16}
|
|
||||||
color={colors.mutedForeground}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{validationStatus === "ok" && (
|
{validationStatus === "ok" && (
|
||||||
<View style={styles.validationRow}>
|
<View style={styles.validationRow}>
|
||||||
<Feather name="check-circle" size={13} color={colors.success} />
|
<Feather name="check-circle" size={13} color={colors.success} />
|
||||||
@@ -203,13 +222,23 @@ export default function SettingsScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{validationStatus === "error" && (
|
{validationStatus === "error" && (
|
||||||
<View style={styles.validationRow}>
|
<View style={[styles.errorBox, { backgroundColor: colors.error + "12", borderColor: colors.error + "30" }]}>
|
||||||
|
<View style={styles.errorHeader}>
|
||||||
<Feather name="x-circle" size={13} color={colors.error} />
|
<Feather name="x-circle" size={13} color={colors.error} />
|
||||||
<Text style={[styles.validationText, { color: colors.error }]}>
|
<Text style={[styles.errorTitle, { color: colors.error }]}>
|
||||||
Could not connect. Check your URL and API key.
|
Could not connect
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{!!errorDetail && (
|
||||||
|
<ScrollView style={styles.errorScroll} nestedScrollEnabled>
|
||||||
|
<Text style={[styles.errorDetail, { color: colors.error }]} selectable>
|
||||||
|
{errorDetail}
|
||||||
|
</Text>
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -217,13 +246,7 @@ export default function SettingsScreen() {
|
|||||||
onPress={handleValidate}
|
onPress={handleValidate}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
disabled={validating}
|
disabled={validating}
|
||||||
style={[
|
style={[styles.validateBtn, { backgroundColor: colors.card, borderColor: colors.border }]}
|
||||||
styles.validateBtn,
|
|
||||||
{
|
|
||||||
backgroundColor: colors.card,
|
|
||||||
borderColor: colors.border,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
{validating ? (
|
{validating ? (
|
||||||
<ActivityIndicator color={colors.primary} size="small" />
|
<ActivityIndicator color={colors.primary} size="small" />
|
||||||
@@ -241,10 +264,7 @@ export default function SettingsScreen() {
|
|||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
style={[
|
style={[styles.saveBtn, { backgroundColor: saving ? colors.muted : colors.primary }]}
|
||||||
styles.saveBtn,
|
|
||||||
{ backgroundColor: saving ? colors.muted : colors.primary },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
{saving ? (
|
{saving ? (
|
||||||
<ActivityIndicator color={colors.primaryForeground} size="small" />
|
<ActivityIndicator color={colors.primaryForeground} size="small" />
|
||||||
@@ -265,9 +285,7 @@ export default function SettingsScreen() {
|
|||||||
style={[styles.clearBtn, { borderColor: colors.destructive + "60" }]}
|
style={[styles.clearBtn, { borderColor: colors.destructive + "60" }]}
|
||||||
>
|
>
|
||||||
<Feather name="log-out" size={14} color={colors.destructive} />
|
<Feather name="log-out" size={14} color={colors.destructive} />
|
||||||
<Text style={[styles.clearText, { color: colors.destructive }]}>
|
<Text style={[styles.clearText, { color: colors.destructive }]}>Disconnect</Text>
|
||||||
Disconnect
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -349,6 +367,29 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: "Inter_400Regular",
|
fontFamily: "Inter_400Regular",
|
||||||
},
|
},
|
||||||
|
errorBox: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 12,
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
errorHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
errorTitle: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Inter_600SemiBold",
|
||||||
|
},
|
||||||
|
errorScroll: {
|
||||||
|
maxHeight: 80,
|
||||||
|
},
|
||||||
|
errorDetail: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontFamily: "Inter_400Regular",
|
||||||
|
lineHeight: 16,
|
||||||
|
},
|
||||||
validateBtn: {
|
validateBtn: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
Reference in New Issue
Block a user