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
4 changes: 3 additions & 1 deletion app/(tabs)/journal-editor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Redirect, Stack, useLocalSearchParams } from "expo-router";

import { getSingleRouteParam } from "@/lib/navigation/routeValidators";

export default function JournalEditorTabScreen() {
const { source } = useLocalSearchParams();
const sourceParam = Array.isArray(source) ? source[0] : source;
const sourceParam = getSingleRouteParam(source);

return (
<>
Expand Down
20 changes: 17 additions & 3 deletions app/insights/report/[periodType].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,27 @@ export default function AIInsightReportScreen() {
</AnimatedIconButton>
</View>

{reportState.error ? <ErrorBanner message={reportState.error} /> : null}
{reportState.error ? (
<ErrorBanner
message={
reportState.report
? `Could not update this report. Your previous report is still available. ${reportState.error}`
: reportState.error
}
onRetry={() => {
void reportState.refresh();
}}
retrying={reportState.isLoading}
/>
) : null}

{reportState.isGenerating && reportState.report ? (
{(reportState.isGenerating || reportState.isLoading) &&
reportState.report ? (
<UpdatingBanner periodType={period.type} />
) : null}

{reportState.isLoading && !reportState.report ? (
{(reportState.isLoading || reportState.isGenerating) &&
!reportState.report ? (
<LoadingState periodType={period.type} />
) : reportState.report ? (
<>
Expand Down
3 changes: 2 additions & 1 deletion app/journal/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Redirect, Stack, useLocalSearchParams } from "expo-router";

import { JournalEditorScreen } from "@/components/journal-editor/journal-editor-screen";
import { getSafeRouteId } from "@/lib/navigation/routeValidators";

export default function ExistingJournalEntryScreen() {
const { id } = useLocalSearchParams();
const entryId = Array.isArray(id) ? id[0] : id;
const entryId = getSafeRouteId(id);

if (!entryId) {
return <Redirect href="/journal-history" />;
Expand Down
23 changes: 22 additions & 1 deletion components/achievements/AchievementsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { ScreenEmptyState } from "@/components/states/ScreenEmptyState";
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 type {
Expand Down Expand Up @@ -46,6 +49,7 @@ export function AchievementsScreen() {
const [selectedFilter, setSelectedFilter] =
useState<AchievementFilter>("all");
const [filterToggleWidth, setFilterToggleWidth] = useState(0);
const showHydrationState = useDelayedVisibility(!hasHydrated);
const [slideDirection, setSlideDirection] = useState(1);
const thumbAnimationRef = useRef(new Animated.Value(0));
const listAnimationRef = useRef(new Animated.Value(1));
Expand Down Expand Up @@ -219,7 +223,24 @@ export function AchievementsScreen() {
transform: [{ translateX: listTranslateX }],
}}
>
{filteredAchievements.map((achievement) => (
{!hasHydrated ? (
showHydrationState ? (
<ScreenLoadingState title="Preparing achievements..." />
) : null
) : filteredAchievements.length === 0 ? (
<ScreenEmptyState
message={
selectedFilter === "unlocked"
? "Keep journaling and your milestones will appear here."
: "Try another achievement filter."
}
title={
selectedFilter === "unlocked"
? "No achievements unlocked yet"
: "No achievements match this filter"
}
/>
) : filteredAchievements.map((achievement) => (
<AchievementCard achievement={achievement} key={achievement.id} />
))}
</Animated.View>
Expand Down
43 changes: 34 additions & 9 deletions components/ai-chat/ai-chat-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import {
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { AnimatedIconButton } from "@/components/ui/animated-icon-button";
import { CONNECTION_STATE_COLORS } from "@/constants/theme";
import { useAppDialog } from "@/hooks/useAppDialog";
import { useConnectivity } from "@/hooks/useConnectivity";
import { useDelayedVisibility } from "@/hooks/useDelayedVisibility";
import { generateLocalJournalResponse } from "@/lib/ai/localJournalAssistant";
import { generateRemoteJournalResponse } from "@/lib/ai/remoteJournalAssistant";
import { useJournalStore } from "@/store/journal-store";
Expand Down Expand Up @@ -79,6 +81,7 @@ export function AiChatScreen({
const [keyboardOffset, setKeyboardOffset] = useState(0);
const journalEntries = useJournalStore((state) => state.entries);
const chatMessages = useChatStore((state) => state.messages);
const chatHasHydrated = useChatStore((state) => state.hasHydrated);
const addMessage = useChatStore((state) => state.addMessage);
const clearMessagesForUser = useChatStore(
(state) => state.clearMessagesForUser,
Expand Down Expand Up @@ -107,21 +110,31 @@ export function AiChatScreen({
new Date(messageB.createdAt).getTime(),
);
}, [chatMessages, userId]);
const showChatHydrationState = useDelayedVisibility(!chatHasHydrated);
const visibleMessages =
currentUserMessages.length > 0
chatHasHydrated && currentUserMessages.length > 0
? currentUserMessages
: [
: chatHasHydrated
? [
{
content: "I'm here to sit with you for a moment. No rush, no judgment.",
createdAt: new Date().toISOString(),
id: "welcome",
role: "assistant",
userId: userId ?? "guest",
} satisfies ChatMessage,
];
]
: [];
const isOffline = connectivity.status === "offline";
const connectionStateColors = isOffline
? CONNECTION_STATE_COLORS.offline
: CONNECTION_STATE_COLORS.online;
const canSendMessage =
message.trim().length > 0 && !!userId && !isThinking && !isOffline;
message.trim().length > 0 &&
!!userId &&
chatHasHydrated &&
!isThinking &&
!isOffline;
const shouldUseKeyboardOffset = process.env.EXPO_OS === "android";
const footerKeyboardOffset = shouldUseKeyboardOffset ? keyboardOffset : 0;

Expand All @@ -131,6 +144,11 @@ export function AiChatScreen({
};
}, []);

useEffect(() => {
requestIdRef.current += 1;
setIsThinking(false);
}, [userId]);

useEffect(() => {
if (!shouldUseKeyboardOffset) {
return;
Expand Down Expand Up @@ -350,15 +368,15 @@ export function AiChatScreen({
<View className="flex-row items-center gap-2">
<View
className="flex-row items-center gap-2 rounded-full px-2 py-1"
style={{ backgroundColor: isOffline ? "#FFF7ED" : "#CFF8E6" }}
style={{ backgroundColor: connectionStateColors.background }}
>
<View
className="size-2 rounded-full"
style={{ backgroundColor: isOffline ? "#F97316" : "#10B981" }}
style={{ backgroundColor: connectionStateColors.dot }}
/>
<Text
className="text-[11px] font-bold leading-4"
style={{ color: isOffline ? "#C2410C" : "#047857" }}
style={{ color: connectionStateColors.text }}
>
{isOffline ? "Offline" : "Online"}
</Text>
Expand Down Expand Up @@ -398,13 +416,20 @@ export function AiChatScreen({

<View className="mt-6 gap-6">
{isOffline ? (
<View className="rounded-[20px] bg-[#FFF7ED] px-4 py-3">
<Text className="text-[14px] font-semibold leading-6 text-[#9A3412]">
<View className="rounded-[20px] bg-offline-surface px-4 py-3">
<Text className="text-[14px] font-semibold leading-6 text-offline-text">
Internet is required for AI Chat. Your journal entries are still
available offline.
</Text>
</View>
) : null}
{!chatHasHydrated && showChatHydrationState ? (
<View className="rounded-[20px] bg-white px-4 py-4">
<Text className="text-[14px] font-semibold leading-6 text-[#71717B]">
Preparing your conversation...
</Text>
</View>
) : null}
{visibleMessages.map((chatMessage, index) => (
<ChatBubble
assistantBubbleMaxWidth={assistantBubbleMaxWidth}
Expand Down
8 changes: 5 additions & 3 deletions components/connectivity/OfflineNotice.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { WifiOff } from "lucide-react-native";
import { Text, View } from "react-native";

import { colors } from "@/constants/theme";

export function OfflineNotice({ message }: { message?: string }) {
return (
<View className="flex-row items-start gap-3 rounded-[20px] bg-[#FFF7ED] px-4 py-4">
<View className="flex-row items-start gap-3 rounded-[20px] bg-offline-surface px-4 py-4">
<View className="mt-0.5 size-8 items-center justify-center rounded-full bg-white">
<WifiOff color="#C2410C" size={17} strokeWidth={2.4} />
<WifiOff color={colors.offlineIcon} size={17} strokeWidth={2.4} />
</View>
<Text className="flex-1 text-[14px] font-semibold leading-6 text-[#9A3412]">
<Text className="flex-1 text-[14px] font-semibold leading-6 text-offline-text">
{message ??
"You are offline. Changes will be saved on this device and synced later."}
</Text>
Expand Down
32 changes: 15 additions & 17 deletions components/errors/FeatureErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,26 @@ export class FeatureErrorBoundary extends React.Component<
}

return (
<View className="rounded-[22px] bg-[#FFF1F5] px-4 py-4">
<Text className="text-[16px] font-bold leading-6 text-[#9F1239]">
<View className="rounded-[22px] bg-error-surface px-4 py-4">
<Text className="text-[16px] font-bold leading-6 text-error-text">
{this.props.featureName} is unavailable right now.
</Text>
<Text className="mt-2 text-[14px] leading-6 text-[#71717B]">
<Text className="mt-2 text-[14px] leading-6 text-text-muted">
{this.props.fallbackMessage ??
"This part of DearDiary ran into a problem. The rest of the screen is still available."}
</Text>
{this.props.onRetry ? (
<Pressable
accessibilityRole="button"
className="mt-4 min-h-11 items-center justify-center rounded-full bg-white px-4"
onPress={() => {
this.setState({ error: null });
this.props.onRetry?.();
}}
>
<Text className="text-[14px] font-bold leading-5 text-[#FF2056]">
Retry
</Text>
</Pressable>
) : null}
<Pressable
accessibilityRole="button"
className="mt-4 min-h-11 items-center justify-center rounded-full bg-white px-4"
onPress={() => {
this.setState({ error: null });
this.props.onRetry?.();
}}
>
<Text className="text-[14px] font-bold leading-5 text-brand-primary">
Retry
</Text>
</Pressable>
</View>
);
}
Expand Down
6 changes: 3 additions & 3 deletions components/errors/InlineErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ export function InlineErrorMessage({ error, onRetry }: InlineErrorMessageProps)
return (
<View
accessibilityRole="alert"
className="rounded-[20px] bg-[#FFF1F5] px-4 py-4"
className="rounded-[20px] bg-error-surface px-4 py-4"
>
<Text className="text-[14px] font-semibold leading-6 text-[#9F1239]">
<Text className="text-[14px] font-semibold leading-6 text-error-text">
{error.userMessage}
</Text>
{error.retryable && onRetry ? (
Expand All @@ -22,7 +22,7 @@ export function InlineErrorMessage({ error, onRetry }: InlineErrorMessageProps)
className="mt-3 min-h-10 items-center justify-center rounded-full bg-white px-4"
onPress={onRetry}
>
<Text className="text-[14px] font-bold leading-5 text-[#FF2056]">
<Text className="text-[14px] font-bold leading-5 text-brand-primary">
Retry
</Text>
</Pressable>
Expand Down
2 changes: 1 addition & 1 deletion components/errors/RootErrorFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function RootErrorFallback({ error, retry }: RootErrorFallbackProps) {
{retry ? (
<Pressable
accessibilityRole="button"
className="min-h-[52px] items-center justify-center rounded-full bg-[#FF2056] px-6"
className="min-h-[52px] items-center justify-center rounded-full bg-brand-primary px-6"
onPress={retry}
>
<Text className="text-[16px] font-bold leading-6 text-white">
Expand Down
33 changes: 27 additions & 6 deletions components/home/home-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "@/components/navigation/bottom-tab-bar";
import { images } from "@/constants/images";
import { moodOptions } from "@/data/home";
import { useDelayedVisibility } from "@/hooks/useDelayedVisibility";
import { useJournalStore } from "@/store/journal-store";
import type {
MoodId,
Expand Down Expand Up @@ -80,6 +81,7 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
const entries = useJournalStore((state) => state.entries);
const hasHydrated = useJournalStore((state) => state.hasHydrated);
const [selectedMood, setSelectedMood] = useState("Happy");
const showHydrationState = useDelayedVisibility(!hasHydrated);
const bottomNavHeight = bottomTabBarBaseHeight + insets.bottom;
const displayName = firstName?.trim() || "Aryan";
const greeting = useMemo(() => getGreeting(), []);
Expand Down Expand Up @@ -316,14 +318,18 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {

<View className="gap-5">
{!hasHydrated ? (
<RecentEntriesEmptyState
body="Loading your saved reflections..."
title="Loading entries"
/>
showHydrationState ? (
<RecentEntriesEmptyState
body="Preparing your saved reflections..."
title="Preparing your journal..."
/>
) : null
) : recentJournalEntries.length === 0 ? (
<RecentEntriesEmptyState
body="Start with today's prompt to build your first reflection."
title="No entries yet"
body="Write your first entry and give today a place to live."
ctaLabel="Write an entry"
onCtaPress={() => router.push(journalEditorHref)}
title="Your journal begins here"
/>
) : (
recentJournalEntries.map((entry) => (
Expand Down Expand Up @@ -371,9 +377,13 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {

function RecentEntriesEmptyState({
body,
ctaLabel,
onCtaPress,
title,
}: {
body: string;
ctaLabel?: string;
onCtaPress?: () => void;
title: string;
}) {
return (
Expand All @@ -394,6 +404,17 @@ function RecentEntriesEmptyState({
{title}
</Text>
<Text className="text-[17px] leading-5 text-zinc-950/60">{body}</Text>
{ctaLabel && onCtaPress ? (
<Pressable
accessibilityRole="button"
className="mt-5 min-h-[46px] items-center justify-center rounded-full bg-[#FF2056] px-5"
onPress={onCtaPress}
>
<Text className="text-[15px] font-semibold leading-5 text-white">
{ctaLabel}
</Text>
</Pressable>
) : null}
</View>
);
}
Expand Down
Loading