mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Implement E2EE and message content moderation
This commit is contained in:
1 parent
e1ff87dc03
commit
198cd80dfd
74 files changed
+12939
-2178
No files matched your search
@@ -5,6 +5,8 @@ import { BlurView } from "expo-blur";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { useAuthStore, useMatchStore, useMessageStore, useSubscriptionStore, usePacksStore } from "../../src/store";
|
||||
import { useDecryptedMessage } from "../../src/hooks";
|
||||
import type { KeysMetadata } from "../../src/lib/encryption";
|
||||
import { colors, gradients, blur, radius, spacing, typography, shadows } from "../../src/theme";
|
||||
import { supabase } from "../../src/lib/supabase";
|
||||
import { isBiometricEnabled } from "../../src/lib/biometricAuth";
|
||||
@@ -16,6 +18,61 @@ import type { Database } from "../../src/types/supabase";
|
||||
|
||||
type Message = Database["public"]["Tables"]["messages"]["Row"];
|
||||
|
||||
function MessageToastPreview({
|
||||
message,
|
||||
currentUserId,
|
||||
}: {
|
||||
message: Message;
|
||||
currentUserId: string;
|
||||
}) {
|
||||
if (message.media_path) {
|
||||
const fallback = message.media_type === 'video' ? 'Sent a video' : 'Sent an image';
|
||||
return (
|
||||
<Text style={styles.messageToastBody} numberOfLines={1}>
|
||||
{fallback}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const version = message.version ?? 1;
|
||||
const isV2 = version === 2;
|
||||
|
||||
const { content, isDecrypting, error } = useDecryptedMessage({
|
||||
message: {
|
||||
id: message.id,
|
||||
content: message.content,
|
||||
version,
|
||||
encrypted_content: message.encrypted_content,
|
||||
encryption_iv: message.encryption_iv,
|
||||
keys_metadata: message.keys_metadata as unknown as KeysMetadata | null,
|
||||
user_id: message.user_id,
|
||||
},
|
||||
currentUserId,
|
||||
});
|
||||
|
||||
if (isDecrypting) {
|
||||
return (
|
||||
<Text style={styles.messageToastBody} numberOfLines={1}>
|
||||
Loading...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Text style={styles.messageToastBody} numberOfLines={1}>
|
||||
Failed to load message
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text style={styles.messageToastBody} numberOfLines={1}>
|
||||
{content ?? (!isV2 ? message.content : null) ?? 'New message'}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
function TabBarBackground() {
|
||||
// Only use BlurView on iOS - Android experimental blur causes white overlay artifacts
|
||||
// The solid rgba background in tabBarStyle provides the visual effect on Android
|
||||
@@ -820,9 +877,7 @@ export default function AppLayout() {
|
||||
<Text style={styles.messageToastTitle} numberOfLines={1}>
|
||||
New message
|
||||
</Text>
|
||||
<Text style={styles.messageToastBody} numberOfLines={1}>
|
||||
{lastMessage.content || "Sent an image"}
|
||||
</Text>
|
||||
<MessageToastPreview message={lastMessage} currentUserId={user?.id ?? ''} />
|
||||
</View>
|
||||
<Pressable onPress={dismissMessageToast} hitSlop={8}>
|
||||
<Ionicons name="close" size={20} color={colors.textTertiary} />
|
||||
|
||||
@@ -45,12 +45,19 @@ function PackRow({ pack, isEnabled, isPremiumLocked, isExpanded, onToggle, onPre
|
||||
</View>
|
||||
|
||||
<View style={styles.packContent}>
|
||||
<View style={styles.packTitleRow}>
|
||||
<Text style={styles.packName} numberOfLines={1}>{pack.name}</Text>
|
||||
{isPremiumLocked && (
|
||||
<Ionicons name="lock-closed" size={14} color={colors.premium.gold} />
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.packTitleRow}>
|
||||
<Text style={styles.packName} numberOfLines={1}>{pack.name}</Text>
|
||||
{pack.is_premium && (
|
||||
<View style={[styles.premiumBadge, isPremiumLocked && styles.premiumBadgeLocked]}>
|
||||
<Ionicons
|
||||
name={isPremiumLocked ? "lock-closed" : "sparkles"}
|
||||
size={12}
|
||||
color={colors.premium.gold}
|
||||
/>
|
||||
<Text style={styles.premiumBadgeText}>PRO</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.packMeta}>
|
||||
{questionCount} {questionCount === 1 ? "question" : "questions"}
|
||||
{isEnabled && <Text style={styles.packMetaEnabled}> · Enabled</Text>}
|
||||
@@ -631,11 +638,33 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
gap: spacing.xs,
|
||||
},
|
||||
packName: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
fontWeight: "500",
|
||||
},
|
||||
packName: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
fontWeight: "500",
|
||||
flexShrink: 1,
|
||||
},
|
||||
premiumBadge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: radius.full,
|
||||
backgroundColor: "rgba(212, 175, 55, 0.12)",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(212, 175, 55, 0.25)",
|
||||
},
|
||||
premiumBadgeLocked: {
|
||||
backgroundColor: "rgba(212, 175, 55, 0.18)",
|
||||
borderColor: "rgba(212, 175, 55, 0.35)",
|
||||
},
|
||||
premiumBadgeText: {
|
||||
...typography.caption2,
|
||||
color: colors.premium.gold,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.6,
|
||||
},
|
||||
packMeta: {
|
||||
...typography.caption1,
|
||||
color: colors.textTertiary,
|
||||
|
||||
+65
-62
@@ -1,52 +1,54 @@
|
||||
// Web polyfills must be imported FIRST before any other React Native imports
|
||||
import "../src/polyfills/web";
|
||||
|
||||
// Initialize Sentry early to capture all errors
|
||||
import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry";
|
||||
initSentry();
|
||||
|
||||
// Initialize Analytics
|
||||
import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
|
||||
initAnalytics();
|
||||
|
||||
// Track cold start immediately
|
||||
Events.appOpened("cold");
|
||||
|
||||
// Import push notification utilities
|
||||
import {
|
||||
clearPushToken,
|
||||
setupNotificationResponseListener,
|
||||
getInitialNotification,
|
||||
handleNotificationResponse,
|
||||
} from "../src/lib/notifications";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Platform, View, AppState, AppStateStatus } from "react-native";
|
||||
|
||||
// Suppress useLayoutEffect warning on server-side rendering
|
||||
if (Platform.OS === "web" && typeof globalThis.window === "undefined") {
|
||||
React.useLayoutEffect = useEffect;
|
||||
// Also silence the warning in case some libraries ignore the monkey-patch
|
||||
const originalError = console.error;
|
||||
console.error = (...args) => {
|
||||
if (typeof args[0] === "string" && args[0].includes("useLayoutEffect does nothing on the server")) {
|
||||
return;
|
||||
}
|
||||
originalError(...args);
|
||||
};
|
||||
}
|
||||
|
||||
import { Stack, router, usePathname } from "expo-router";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
// GestureHandlerRootView is only needed on native platforms
|
||||
// On web, we use a simple View to avoid findNodeHandle errors
|
||||
const GestureHandlerRootView = Platform.OS === "web"
|
||||
? View
|
||||
: require("react-native-gesture-handler").GestureHandlerRootView;
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import * as Linking from "expo-linking";
|
||||
import { useAuthStore } from "../src/store";
|
||||
import { supabase } from "../src/lib/supabase";
|
||||
// Polyfills must be imported FIRST before any other React Native imports
|
||||
import "../src/polyfills/web";
|
||||
import "../src/polyfills/crypto";
|
||||
|
||||
// Initialize Sentry early to capture all errors
|
||||
import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry";
|
||||
initSentry();
|
||||
|
||||
// Initialize Analytics
|
||||
import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
|
||||
initAnalytics();
|
||||
|
||||
// Track cold start immediately
|
||||
Events.appOpened("cold");
|
||||
|
||||
// Import push notification utilities
|
||||
import {
|
||||
clearPushToken,
|
||||
setupNotificationResponseListener,
|
||||
getInitialNotification,
|
||||
handleNotificationResponse,
|
||||
} from "../src/lib/notifications";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Platform, View, AppState, AppStateStatus } from "react-native";
|
||||
|
||||
// Suppress useLayoutEffect warning on server-side rendering
|
||||
if (Platform.OS === "web" && typeof globalThis.window === "undefined") {
|
||||
React.useLayoutEffect = useEffect;
|
||||
// Also silence the warning in case some libraries ignore the monkey-patch
|
||||
const originalError = console.error;
|
||||
console.error = (...args) => {
|
||||
if (typeof args[0] === "string" && args[0].includes("useLayoutEffect does nothing on the server")) {
|
||||
return;
|
||||
}
|
||||
originalError(...args);
|
||||
};
|
||||
}
|
||||
|
||||
import { Stack, router, usePathname } from "expo-router";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
// GestureHandlerRootView is only needed on native platforms
|
||||
// On web, we use a simple View to avoid findNodeHandle errors
|
||||
const GestureHandlerRootView = Platform.OS === "web"
|
||||
? View
|
||||
: require("react-native-gesture-handler").GestureHandlerRootView;
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import * as Linking from "expo-linking";
|
||||
import { useAuthStore } from "../src/store";
|
||||
import { supabase } from "../src/lib/supabase";
|
||||
import { EncryptionKeyInitializer } from "../src/components/EncryptionKeyInitializer";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -207,17 +209,18 @@ export default function RootLayout() {
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<StatusBar style="light" />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
contentStyle: { backgroundColor: "#1a1a2e" },
|
||||
}}
|
||||
/>
|
||||
</GestureHandlerRootView>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<StatusBar style="light" />
|
||||
<EncryptionKeyInitializer />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
contentStyle: { backgroundColor: "#1a1a2e" },
|
||||
}}
|
||||
/>
|
||||
</GestureHandlerRootView>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -467,7 +467,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
heroInner: {
|
||||
paddingTop: STATUS_BAR_HEIGHT + NAV_BAR_HEIGHT + spacing.md,
|
||||
paddingTop: STATUS_BAR_HEIGHT + spacing.md,
|
||||
paddingBottom: spacing.lg,
|
||||
minHeight: HERO_HEIGHT,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user