Implement E2EE and message content moderation

This commit is contained in:
Jamie Brunton committed 2026-01-02 20:49:11 +00:00
1 parent e1ff87dc03
commit 198cd80dfd
74 files changed
+12939 -2178

No files matched your search

+58 -3
View File
@@ -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} />
+40 -11
View File
@@ -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
View File
@@ -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>
);
}
+1 -1
View File
@@ -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,
},