Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,3 @@ PROMPTS.md
task.md
DB.md
AI.md
UI/
5 changes: 3 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
"expo": {
"name": "DearDiary",
"slug": "dear-diary",
"version": "1.0.0",
"version": "1.0.2",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "deardiary",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"buildNumber": "2",
"supportsTablet": true
},
"android": {
Expand All @@ -20,7 +21,7 @@
],
"icon": "./assets/images/icon.png",
"package": "com.aryan.deardiary",
"versionCode": 1,
"versionCode": 2,
"adaptiveIcon": {
"backgroundColor": "#FFDDE8",
"foregroundImage": "./assets/images/icon.png"
Expand Down
2 changes: 1 addition & 1 deletion app/insights/report/[periodType].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export default function AIInsightReportScreen() {
: reportState.error
}
onRetry={() => {
void reportState.refresh();
void reportState.retry();
}}
retrying={reportState.isLoading}
/>
Expand Down
4 changes: 2 additions & 2 deletions components/app-lock/AppLockScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useAuth } from "@clerk/expo";
import { useClerk } from "@clerk/expo";
import { Feather } from "@expo/vector-icons";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
Expand All @@ -22,7 +22,7 @@ import { useJournalStore } from "@/store/journal-store";

export function AppLockScreen() {
const insets = useSafeAreaInsets();
const { signOut } = useAuth();
const { signOut } = useClerk();
const setActiveUserId = useJournalStore((state) => state.setActiveUserId);
const {
biometricAvailability,
Expand Down
10 changes: 2 additions & 8 deletions components/auth/auth-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { LinearGradient } from "expo-linear-gradient";
import type { Href } from "expo-router";
import { Link, router } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useEffect, useState } from "react";
import { useState } from "react";
import {
ActivityIndicator,
Pressable,
Expand Down Expand Up @@ -87,7 +87,7 @@ export function AuthScreen({
"oauth_google" | "oauth_apple" | null
>(null);
const isLogin = mode === "login";
const { isLoaded: isAuthLoaded, isSignedIn } = useAuth();
const { isLoaded: isAuthLoaded } = useAuth();
const { fetchStatus: signInFetchStatus, signIn } = useSignIn();
const { fetchStatus: signUpFetchStatus, signUp } = useSignUp();
const { startSSOFlow } = useSSO();
Expand All @@ -99,12 +99,6 @@ export function AuthScreen({
const emailFeedback = getEmailFeedback(email);
const passwordFeedback = getPasswordFeedback(password, isLogin);

useEffect(() => {
if (isSignedIn) {
router.replace(homeHref);
}
}, [isSignedIn]);

function showError(message: string) {
showAuthError(showDialog, message);
}
Expand Down
27 changes: 25 additions & 2 deletions components/onboarding/app-launch-gate.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { usePathname } from "expo-router";
import { useCallback, useEffect, useState, type ReactNode } from "react";

import { AppPrivacyCover } from "@/components/app-lock/AppPrivacyCover";
import { SplashScreen } from "@/components/onboarding/splash-screen";
import { useOnboardingStore } from "@/store/onboarding-store";

const hydrationFallbackDelayMs = 1500;
const authCallbackPaths = new Set(["/sso", "/sso-callback"]);

let hasCompletedLaunchSplash = false;

export function AppLaunchGate({ children }: { children: ReactNode }) {
const [hasSplashFinished, setHasSplashFinished] = useState(false);
const pathname = usePathname();
const [hasSplashFinished, setHasSplashFinished] = useState(
hasCompletedLaunchSplash,
);
const [showHydrationFallback, setShowHydrationFallback] = useState(false);
const hasHydrated = useOnboardingStore((state) => state.hasHydrated);

Expand All @@ -23,14 +30,30 @@ export function AppLaunchGate({ children }: { children: ReactNode }) {
return () => clearTimeout(hydrationFallback);
}, [hasHydrated]);

useEffect(() => {
if (!authCallbackPaths.has(pathname)) {
return;
}

hasCompletedLaunchSplash = true;
setHasSplashFinished(true);
}, [pathname]);

const handleSplashAnimationEnd = useCallback(() => {
hasCompletedLaunchSplash = true;
setHasSplashFinished(true);
}, []);

if (!hasSplashFinished) {
const isAuthCallback = authCallbackPaths.has(pathname);

if (!hasSplashFinished && !isAuthCallback) {
return <SplashScreen onAnimationEnd={handleSplashAnimationEnd} />;
}

if (isAuthCallback) {
return children;
}

if (!hasHydrated) {
return showHydrationFallback ? (
<AppPrivacyCover className="flex-1" title="Preparing your journal..." />
Expand Down
5 changes: 3 additions & 2 deletions components/profile/profile-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useAuth, useUser } from "@clerk/expo";
import { useAuth, useClerk, useUser } from "@clerk/expo";
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import Constants from "expo-constants";
import { LinearGradient } from "expo-linear-gradient";
Expand Down Expand Up @@ -88,7 +88,8 @@ const moodEmoji: Record<MoodId, string> = {

export function ProfileScreen() {
const insets = useSafeAreaInsets();
const { getToken, signOut } = useAuth();
const { getToken } = useAuth();
const { signOut } = useClerk();
const { user } = useUser();
const { showDialog } = useAppDialog();
const connectivity = useConnectivity();
Expand Down
5 changes: 3 additions & 2 deletions content/legal/privacyPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export const privacyPolicy = {
{
body: [
"DearDiary is an AI-powered journaling companion. This draft explains what data the app handles and how account deletion works. It requires legal review before public release.",
`Operator: ${legalPlaceholders.companyName}. Website: ${legalPlaceholders.websiteUrl}.`,
`Operator: ${legalPlaceholders.companyName}.`,
`Website: ${legalPlaceholders.websiteUrl}.`
],
title: "Overview",
},
Expand Down Expand Up @@ -58,7 +59,7 @@ export const privacyPolicy = {
},
{
body: [
`For privacy questions or account deletion help, contact ${legalPlaceholders.supportEmail}.`,
`For privacy questions or account deletion help, contact - ${legalPlaceholders.supportEmail}.`,
],
title: "Contact",
},
Expand Down
78 changes: 64 additions & 14 deletions hooks/useAIInsightReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as Crypto from "expo-crypto";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import {
AIInsightReportServiceError,
fetchAIInsightReport,
generateAIInsightReport,
} from "@/lib/insights/aiInsightReportService";
Expand All @@ -12,6 +13,7 @@ import {
type ReportPeriod,
} from "@/lib/insights/reportPeriods";
import { isAIInsightReport } from "@/lib/insights/aiInsightReportMapper";
import { buildReportSourceSnapshotInput } from "@/lib/insights/reportSourceSnapshot";
import { useAutoSync } from "@/hooks/useAutoSync";
import { useConnectivity } from "@/hooks/useConnectivity";
import { useAIInsightReportStore } from "@/store/useAIInsightReportStore";
Expand All @@ -35,6 +37,7 @@ export type UseAIInsightReportResult = {
generate: () => Promise<void>;
regenerate: () => Promise<void>;
refresh: () => Promise<void>;
retry: () => Promise<void>;
};

type UseAIInsightReportOptions = {
Expand Down Expand Up @@ -89,6 +92,9 @@ export function useAIInsightReport(
} | null>(null);
const requestContextVersionRef = useRef(0);
const requestInFlightRef = useRef(false);
const lastFailedOperationRef = useRef<
"generate" | "refresh" | "regenerate" | null
>(null);
const reportRef = useRef<AIInsightReport | null>(report);

const periodEntries = useMemo(
Expand Down Expand Up @@ -204,6 +210,7 @@ export function useAIInsightReport(
const requestVersion = requestContextVersionRef.current;
setIsLoading(true);
setError(null);
lastFailedOperationRef.current = "refresh";

try {
const result = await fetchAIInsightReport({ period });
Expand All @@ -229,6 +236,8 @@ export function useAIInsightReport(
setReport(null);
removeCachedReport(userId, cacheKey);
}

lastFailedOperationRef.current = null;
} catch (refreshError) {
if (!isCurrentRequestContext(requestVersion)) {
return;
Expand Down Expand Up @@ -313,6 +322,7 @@ export function useAIInsightReport(
requestInFlightRef.current = true;
setIsGenerating(true);
setError(null);
lastFailedOperationRef.current = regenerate ? "regenerate" : "generate";

try {
const sourcesAreSynced = await syncPeriodSourcesBeforeGeneration({
Expand Down Expand Up @@ -349,6 +359,7 @@ export function useAIInsightReport(
setReport(generatedReport);
setLegacyReportAvailable(false);
setCachedReport(userId, cacheKey, generatedReport);
lastFailedOperationRef.current = null;
} catch (generationError) {
if (!isCurrentRequestContext(requestVersion)) {
return;
Expand Down Expand Up @@ -392,6 +403,17 @@ export function useAIInsightReport(
refresh,
regenerate: () => requestGeneration(true),
report,
retry: () => {
if (lastFailedOperationRef.current === "regenerate") {
return requestGeneration(true);
}

if (lastFailedOperationRef.current === "generate") {
return requestGeneration(false);
}

return refresh();
},
};
}

Expand All @@ -404,13 +426,39 @@ async function syncPeriodSourcesBeforeGeneration({
runAutoSync: (reason?: "journal_change") => Promise<void>;
userId: string;
}) {
if (!hasUnsyncedPeriodSources({ period, userId })) {
return true;
const periodEntries = getEntriesInPeriod(
useJournalStore
.getState()
.allEntries.filter((entry) => entry.userId === userId),
period,
true,
);
const periodMoodLogs = getMoodLogsInPeriod(
useMoodLogStore.getState().allMoodLogs,
period,
userId,
true,
);
const entryIds = periodEntries.map((entry) => entry.id);
const moodLogIds = periodMoodLogs.map((moodLog) => moodLog.id);

if (entryIds.length > 0) {
useJournalStore.getState().markEntriesPendingSync(userId, entryIds);
}

await runAutoSync("journal_change");
if (moodLogIds.length > 0) {
useMoodLogStore.getState().markMoodLogsPendingSync(userId, moodLogIds);
}

for (let attempt = 0; attempt < 2; attempt += 1) {
await runAutoSync("journal_change");

return !hasUnsyncedPeriodSources({ period, userId });
if (!hasUnsyncedPeriodSources({ period, userId })) {
return true;
}
}

return false;
}

function hasUnsyncedPeriodSources({
Expand All @@ -425,6 +473,7 @@ function hasUnsyncedPeriodSources({
.getState()
.allEntries.filter((entry) => entry.userId === userId),
period,
true,
).some((entry) => entry.syncStatus !== "synced");
const hasUnsyncedMoodLogs = getMoodLogsInPeriod(
useMoodLogStore.getState().allMoodLogs,
Expand All @@ -436,15 +485,19 @@ function hasUnsyncedPeriodSources({
return hasUnsyncedEntries || hasUnsyncedMoodLogs;
}

function getEntriesInPeriod(entries: JournalEntry[], period: ReportPeriod) {
function getEntriesInPeriod(
entries: JournalEntry[],
period: ReportPeriod,
includeDeleted = false,
) {
const periodStart = period.start.getTime();
const periodEnd = period.end.getTime();

return entries.filter((entry) => {
const createdAt = Date.parse(entry.createdAt);

return (
!entry.deletedAt &&
(includeDeleted || !entry.deletedAt) &&
Number.isFinite(createdAt) &&
createdAt >= periodStart &&
createdAt <= periodEnd
Expand Down Expand Up @@ -481,14 +534,7 @@ function getMoodLogsInPeriod(
function getLocalSource(entries: JournalEntry[], moodLogs: MoodLog[]) {
const entryIds = entries.map((entry) => entry.id).sort();
const moodLogIds = moodLogs.map((moodLog) => moodLog.id).sort();
const snapshotInput = [
...entries.map((entry) => `entry:${entry.id}:${entry.updatedAt}`),
...moodLogs.map(
(moodLog) => `mood:${moodLog.id}:${moodLog.updatedAt}`,
),
]
.sort()
.join("|");
const snapshotInput = buildReportSourceSnapshotInput(entries, moodLogs);
const latestUpdatedAt =
[...entries, ...moodLogs]
.map((source) => source.updatedAt)
Expand Down Expand Up @@ -586,6 +632,10 @@ function isTimestampAfter(
}

function getErrorMessage(error: unknown) {
if (error instanceof AIInsightReportServiceError) {
return error.message;
}

return normalizeAppError(error, {
operation: "ai_insight_report",
}).userMessage;
Expand Down
Loading