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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,5 @@ Before every feature:
- Do not install new libraries without approval.

- Don't change the design of the Nav, only add the routing functionality.

- Keep 'leading-6' in all components, and fix if you find any un-matching ones.
2 changes: 1 addition & 1 deletion app/insights/report/[periodType].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ export default function AIInsightReportScreen() {
prompt={reportState.report.narrative.reflectionPrompt}
/>
</>
) : reportState.legacyReportAvailable ? (
) : reportState.error ? null : reportState.legacyReportAvailable ? (
<OlderFormatState
disabled={reportState.isGenerating || !hasEnoughEntries}
hasEnoughEntries={hasEnoughEntries}
Expand Down
16 changes: 12 additions & 4 deletions components/achievements/AchievementWatcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import { useEffect, useMemo, useRef, useState } from "react";

import { useAppDialog } from "@/hooks/useAppDialog";
import { getAchievements } from "@/lib/achievements";
import { useJournalStore } from "@/store/journal-store";
import { useAchievementStore } from "@/store/useAchievementStore";
import {
useJournalHydrationStore,
useJournalStore,
} from "@/store/journal-store";
import {
useAchievementHydrationStore,
useAchievementStore,
} from "@/store/useAchievementStore";
import type { JournalEntry } from "@/types/journal";

type AchievementWatcherProps = {
Expand All @@ -16,8 +22,10 @@ export function AchievementWatcher({ userId }: AchievementWatcherProps) {
const { showDialog } = useAppDialog();
const activeUserId = useJournalStore((state) => state.activeUserId);
const entries = useJournalStore((state) => state.entries);
const journalHasHydrated = useJournalStore((state) => state.hasHydrated);
const achievementHasHydrated = useAchievementStore(
const journalHasHydrated = useJournalHydrationStore(
(state) => state.hasHydrated,
);
const achievementHasHydrated = useAchievementHydrationStore(
(state) => state.hasHydrated,
);
const achievementSyncUserId = useAchievementStore(
Expand Down
29 changes: 25 additions & 4 deletions components/achievements/AchievementsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ import {
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { ScreenEmptyState } from "@/components/states/ScreenEmptyState";
import { ScreenErrorState } from "@/components/states/ScreenErrorState";
import { ScreenLoadingState } from "@/components/states/ScreenLoadingState";
import { AnimatedIconButton } from "@/components/ui/animated-icon-button";
import { useDelayedVisibility } from "@/hooks/useDelayedVisibility";
import { getAchievements } from "@/lib/achievements";
import { useJournalStore } from "@/store/journal-store";
import {
retryJournalStoreHydration,
useJournalHydrationStore,
useJournalStore,
} from "@/store/journal-store";
import type {
AchievementCategory,
AchievementStatus,
Expand All @@ -45,7 +50,12 @@ const categoryLabels: Record<AchievementCategory, string> = {
export function AchievementsScreen() {
const insets = useSafeAreaInsets();
const entries = useJournalStore((state) => state.entries);
const hasHydrated = useJournalStore((state) => state.hasHydrated);
const hasHydrated = useJournalHydrationStore(
(state) => state.hasHydrated,
);
const hydrationError = useJournalHydrationStore(
(state) => state.hydrationError,
);
const [selectedFilter, setSelectedFilter] =
useState<AchievementFilter>("all");
const [filterToggleWidth, setFilterToggleWidth] = useState(0);
Expand Down Expand Up @@ -125,6 +135,10 @@ export function AchievementsScreen() {
router.replace("/profile-tab");
}

function retryJournalHydration() {
retryJournalStoreHydration();
}

return (
<View className="flex-1 bg-white">
<StatusBar hidden />
Expand Down Expand Up @@ -173,7 +187,9 @@ export function AchievementsScreen() {
Achievements
</Text>
<Text className="mt-1 text-[15px] font-medium leading-6 text-[#71717B]">
{hasHydrated
{hydrationError
? "Saved progress could not be loaded"
: hasHydrated
? `${unlockedCount} / ${achievements.length} unlocked`
: "Loading achievements..."}
</Text>
Expand Down Expand Up @@ -223,7 +239,12 @@ export function AchievementsScreen() {
transform: [{ translateX: listTranslateX }],
}}
>
{!hasHydrated ? (
{hydrationError ? (
<ScreenErrorState
error={hydrationError}
onRetry={retryJournalHydration}
/>
) : !hasHydrated ? (
showHydrationState ? (
<ScreenLoadingState title="Preparing achievements..." />
) : null
Expand Down
19 changes: 18 additions & 1 deletion components/ai-chat/ai-chat-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { AnimatedIconButton } from "@/components/ui/animated-icon-button";
import { ScreenErrorState } from "@/components/states/ScreenErrorState";
import { CONNECTION_STATE_COLORS } from "@/constants/theme";
import { useAppDialog } from "@/hooks/useAppDialog";
import { useConnectivity } from "@/hooks/useConnectivity";
Expand Down Expand Up @@ -82,6 +83,7 @@ export function AiChatScreen({
const journalEntries = useJournalStore((state) => state.entries);
const chatMessages = useChatStore((state) => state.messages);
const chatHasHydrated = useChatStore((state) => state.hasHydrated);
const chatHydrationError = useChatStore((state) => state.hydrationError);
const addMessage = useChatStore((state) => state.addMessage);
const clearMessagesForUser = useChatStore(
(state) => state.clearMessagesForUser,
Expand Down Expand Up @@ -112,7 +114,9 @@ export function AiChatScreen({
}, [chatMessages, userId]);
const showChatHydrationState = useDelayedVisibility(!chatHasHydrated);
const visibleMessages =
chatHasHydrated && currentUserMessages.length > 0
chatHydrationError
? []
: chatHasHydrated && currentUserMessages.length > 0
? currentUserMessages
: chatHasHydrated
? [
Expand All @@ -133,6 +137,7 @@ export function AiChatScreen({
message.trim().length > 0 &&
!!userId &&
chatHasHydrated &&
!chatHydrationError &&
!isThinking &&
!isOffline;
const shouldUseKeyboardOffset = process.env.EXPO_OS === "android";
Expand Down Expand Up @@ -204,6 +209,11 @@ export function AiChatScreen({
});
}

function retryChatHydration() {
useChatStore.setState({ hasHydrated: false, hydrationError: null });
void useChatStore.persist.rehydrate();
}

function handleMessageChange(nextMessage: string) {
setMessage(nextMessage);

Expand Down Expand Up @@ -430,6 +440,13 @@ export function AiChatScreen({
</Text>
</View>
) : null}
{chatHydrationError ? (
<ScreenErrorState
compact
error={chatHydrationError}
onRetry={retryChatHydration}
/>
) : null}
{visibleMessages.map((chatMessage, index) => (
<ChatBubble
assistantBubbleMaxWidth={assistantBubbleMaxWidth}
Expand Down
91 changes: 31 additions & 60 deletions components/home/home-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@ import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";
import { useRouter, type Href } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { Pressable, ScrollView, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { HomeMoodCheckInCard } from "@/components/home/mood/HomeMoodCheckInCard";
import {
BottomTabBar,
bottomTabBarBaseHeight,
} from "@/components/navigation/bottom-tab-bar";
import { ScreenErrorState } from "@/components/states/ScreenErrorState";
import { images } from "@/constants/images";
import { moodOptions } from "@/data/home";
import { fallbackMoodMetadata, moodMetadata } from "@/constants/moods";
import { useDelayedVisibility } from "@/hooks/useDelayedVisibility";
import { useJournalStore } from "@/store/journal-store";
import type {
MoodId,
JournalEntry as StoredJournalEntry,
} from "@/types/journal";
import {
retryJournalStoreHydration,
useJournalHydrationStore,
useJournalStore,
} from "@/store/journal-store";
import type { JournalEntry as StoredJournalEntry } from "@/types/journal";

type HomeScreenProps = {
avatarUrl?: string;
Expand Down Expand Up @@ -60,27 +63,16 @@ type HomeRecentEntry = {
title: string;
};

const moodVisuals: Record<MoodId, { backgroundColor: string; emoji: string }> =
{
anxious: { backgroundColor: "#F4EFFA", emoji: "😰" },
calm: { backgroundColor: "#D8EEDB", emoji: "😌" },
grateful: { backgroundColor: "#DDEFFF", emoji: "🙏" },
happy: { backgroundColor: "#FFDDE8", emoji: "😊" },
motivated: { backgroundColor: "#FFE8D8", emoji: "🔥" },
sad: { backgroundColor: "#DDEFFF", emoji: "😔" },
};

const fallbackMoodVisual = {
backgroundColor: "#F4F4F5",
emoji: "✍️",
};

export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
const insets = useSafeAreaInsets();
const router = useRouter();
const entries = useJournalStore((state) => state.entries);
const hasHydrated = useJournalStore((state) => state.hasHydrated);
const [selectedMood, setSelectedMood] = useState("Happy");
const hasHydrated = useJournalHydrationStore(
(state) => state.hasHydrated,
);
const hydrationError = useJournalHydrationStore(
(state) => state.hydrationError,
);
const showHydrationState = useDelayedVisibility(!hasHydrated);
const bottomNavHeight = bottomTabBarBaseHeight + insets.bottom;
const displayName = firstName?.trim() || "Aryan";
Expand Down Expand Up @@ -115,10 +107,16 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
: morningIntentionPrompt;
const intentionBody = !hasHydrated
? "Loading intention..."
: hydrationError
? "Saved intention could not be loaded."
: morningIntention
? getEntryPreview(morningIntention)
: "Tap to write your intention...";

function retryJournalHydration() {
retryJournalStoreHydration();
}

return (
<View className="flex-1 bg-white">
<StatusBar hidden />
Expand Down Expand Up @@ -217,40 +215,7 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
</Pressable>
</View>

<View className="mb-9 gap-4">
<Text className="text-[23px] font-semibold leading-8 text-[#27272A]">
How are you feeling today?
</Text>
<View className="flex-row flex-wrap gap-2.5">
{moodOptions.map((mood) => {
const isSelected = selectedMood === mood.label;

return (
<Pressable
accessibilityRole="button"
className="h-12 flex-row items-center gap-2 rounded-full border px-5"
key={mood.label}
onPress={() => setSelectedMood(mood.label)}
style={{
backgroundColor: mood.backgroundColor,
borderColor: isSelected ? "#FFA1B9" : "transparent",
}}
>
<Text className="text-[18px] leading-6">{mood.emoji}</Text>
<Text
className="text-[16px] leading-6"
style={{
color: isSelected ? colors.primary : "#51515B",
fontWeight: isSelected ? "600" : "500",
}}
>
{mood.label}
</Text>
</Pressable>
);
})}
</View>
</View>
<HomeMoodCheckInCard />

<View className="mb-9 gap-4">
<Text className="text-[23px] font-semibold leading-8 text-[#27272A]">
Expand Down Expand Up @@ -317,7 +282,13 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
</View>

<View className="gap-5">
{!hasHydrated ? (
{hydrationError ? (
<ScreenErrorState
compact
error={hydrationError}
onRetry={retryJournalHydration}
/>
) : !hasHydrated ? (
showHydrationState ? (
<RecentEntriesEmptyState
body="Preparing your saved reflections..."
Expand Down Expand Up @@ -420,7 +391,7 @@ function RecentEntriesEmptyState({
}

function toHomeRecentEntry(entry: StoredJournalEntry): HomeRecentEntry {
const visual = entry.mood ? moodVisuals[entry.mood] : fallbackMoodVisual;
const visual = entry.mood ? moodMetadata[entry.mood] : fallbackMoodMetadata;
const content = entry.content.trim();

return {
Expand Down
Loading