From 7eb8ccae48b0cc18a9dcaa9c3626a02df8e6d919 Mon Sep 17 00:00:00 2001 From: JustZvan Date: Fri, 6 Feb 2026 13:22:33 +0100 Subject: feat: initial commit! --- app/(tabs)/_layout.tsx | 188 +++++++++++++++++++++++++++++ app/(tabs)/alerts.tsx | 114 ++++++++++++++++++ app/(tabs)/contact-detail.tsx | 193 ++++++++++++++++++++++++++++++ app/(tabs)/controls.tsx | 270 ++++++++++++++++++++++++++++++++++++++++++ app/(tabs)/index.tsx | 135 +++++++++++++++++++++ app/(tabs)/settings.tsx | 221 ++++++++++++++++++++++++++++++++++ 6 files changed, 1121 insertions(+) create mode 100644 app/(tabs)/_layout.tsx create mode 100644 app/(tabs)/alerts.tsx create mode 100644 app/(tabs)/contact-detail.tsx create mode 100644 app/(tabs)/controls.tsx create mode 100644 app/(tabs)/index.tsx create mode 100644 app/(tabs)/settings.tsx (limited to 'app/(tabs)') diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..6bd6527 --- /dev/null +++ b/app/(tabs)/_layout.tsx @@ -0,0 +1,188 @@ +import { Ionicons } from "@expo/vector-icons"; +import * as Notifications from "expo-notifications"; +import { Redirect, Tabs, router } from "expo-router"; +import { useEffect, useRef } from "react"; +import { View } from "react-native"; +import { useAuth } from "../../lib/auth"; +import { DeviceProvider } from "../../lib/device"; +import { t } from "../../lib/locales"; +import { + addNotificationReceivedListener, + addNotificationResponseListener, + getLastNotificationResponse, + initializeNotifications, +} from "../../lib/notifications"; +import { colors } from "../../lib/theme"; +import { LoadingScreen } from "../../lib/ui"; + +export default function TabsLayout() { + const { isLoading, isAuthenticated } = useAuth(); + const notificationListener = useRef( + null + ); + const responseListener = useRef(null); + + useEffect(() => { + if (!isAuthenticated) return; + + // Initialize notifications when authenticated + initializeNotifications().then(({ permissionStatus, tokenRegistered }) => { + console.log( + `Notifications initialized: permission=${permissionStatus}, tokenRegistered=${tokenRegistered}` + ); + }); + + // Set up notification listeners + notificationListener.current = addNotificationReceivedListener( + (notification) => { + console.log("Notification received:", notification); + } + ); + + responseListener.current = addNotificationResponseListener((response) => { + console.log("Notification tapped:", response); + const data = response.notification.request.content.data; + + // Navigate based on notification type + if (data?.type === "dangerous_content") { + router.push("/(tabs)/alerts"); + } else if (data?.type === "new_contact") { + // Navigate to contact detail screen with contact info + router.push({ + pathname: "/(tabs)/contact-detail", + params: { + contactName: String(data.contactName || "Unknown"), + contactIdentifier: String(data.contactIdentifier || "Not provided"), + contactType: String(data.contactType || "unknown"), + deviceName: String(data.deviceName || "Unknown Device"), + }, + }); + } + }); + + // Check if app was opened from a notification (cold start) + getLastNotificationResponse().then((response) => { + if (response) { + const data = response.notification.request.content.data; + if (data?.type === "dangerous_content") { + router.push("/(tabs)/alerts"); + } else if (data?.type === "new_contact") { + router.push({ + pathname: "/(tabs)/contact-detail", + params: { + contactName: String(data.contactName || "Unknown"), + contactIdentifier: String( + data.contactIdentifier || "Not provided" + ), + contactType: String(data.contactType || "unknown"), + deviceName: String(data.deviceName || "Unknown Device"), + }, + }); + } + } + }); + + return () => { + if (notificationListener.current) { + notificationListener.current.remove(); + } + if (responseListener.current) { + responseListener.current.remove(); + } + }; + }, [isAuthenticated]); + + if (isLoading) { + return ; + } + + if (!isAuthenticated) { + return ; + } + + return ( + + + + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ( + + ), + }} + /> + + + + ); +} diff --git a/app/(tabs)/alerts.tsx b/app/(tabs)/alerts.tsx new file mode 100644 index 0000000..590fe87 --- /dev/null +++ b/app/(tabs)/alerts.tsx @@ -0,0 +1,114 @@ +import { Ionicons } from "@expo/vector-icons"; +import { useEffect, useState } from "react"; +import { ActivityIndicator, Text, View } from "react-native"; +import { Alert, getAlerts } from "../../api"; +import { t } from "../../lib/locales"; +import { colors } from "../../lib/theme"; +import { Card, Divider, H2, Muted, Pill, Row, Screen } from "../../lib/ui"; + +export default function AlertsScreen() { + const [alerts, setAlerts] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + console.log("AlertsScreen: Fetching alerts..."); + setLoading(true); + getAlerts() + .then((data) => { + console.log("AlertsScreen: Received alerts:", data); + setAlerts(data); + }) + .catch((error) => { + console.error("AlertsScreen: Error fetching alerts:", error); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + return ( + + {loading ? ( + + + + {t("loadingAlerts")} + + + ) : alerts.length === 0 ? ( + + + +

{t("allClearTitle")}

+ + {t("noAlertsToReview")} + +
+
+ ) : ( + alerts.map((alert) => ( + + + + +

{alert.title}

+ {alert.timeLabel} +
+ + } + right={ + + } + /> + + + +

{t("whatHappened")}

+ {alert.whatHappened} + +

{t("whyItMatters")}

+ {alert.whyItMatters} + +

{t("suggestedNextStep")}

+ {alert.suggestedAction} +
+ )) + )} +
+ ); +} diff --git a/app/(tabs)/contact-detail.tsx b/app/(tabs)/contact-detail.tsx new file mode 100644 index 0000000..520cc15 --- /dev/null +++ b/app/(tabs)/contact-detail.tsx @@ -0,0 +1,193 @@ +import { Ionicons } from "@expo/vector-icons"; +import { router, useLocalSearchParams } from "expo-router"; +import { ScrollView, StyleSheet, Text, View } from "react-native"; +import { t } from "../../lib/locales"; +import { colors } from "../../lib/theme"; +import { Button } from "../../lib/ui"; + +export default function ContactDetailScreen() { + const params = useLocalSearchParams<{ + contactName: string; + contactIdentifier: string; + contactType: string; + deviceName: string; + }>(); + + const { contactName, contactIdentifier, contactType, deviceName } = params; + + const getContactTypeIcon = () => { + switch (contactType) { + case "phone": + return "call"; + case "email": + return "mail"; + default: + return "person"; + } + }; + + const getContactTypeLabel = () => { + switch (contactType) { + case "phone": + return t("phoneNumber"); + case "email": + return t("emailAddress"); + default: + return t("identifier"); + } + }; + + return ( + + + + + + {t("newContactAddedTitle")} + {t("newContactAddedSubtitle")} + + + + + + + {t("contactName")} + + {contactName || t("unknown")} + + + + + + + + {getContactTypeLabel()} + + + {contactIdentifier || t("notProvided")} + + + + + + + + + {t("device")} + + + {deviceName || t("unknownDevice")} + + + + + + + + {t("contactAddedInfo").replace( + "{deviceName}", + deviceName || t("unknownDevice"), + )} + + + +