fix(native): regroup Upcoming when the day or language changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-05 22:45:29 -04:00
1 parent 1dc38f91b9
commit d6abcbd2bb
6 files changed
+71 -14

No files matched your search

+11 -7
View File
@@ -12,6 +12,7 @@ import { EmptyState } from "@/components/ui/empty-state";
import { LoadMoreFooter } from "@/components/ui/load-more-footer";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text";
import { useLocalDay } from "@/hooks/use-local-day";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
import { orpc } from "@/lib/orpc";
import { queryClient } from "@/lib/query-client";
@@ -49,7 +50,8 @@ function FilterChip({
}
export default function UpcomingScreen() {
const { t } = useLingui();
const { t, i18n } = useLingui();
const day = useLocalDay();
const headerTitleStyle = useResolveClassNames("font-display text-foreground text-xl");
const tintColor = useCSSVariable("--color-primary") as string;
const backgroundColor = useCSSVariable("--color-background") as string;
@@ -87,12 +89,14 @@ export default function UpcomingScreen() {
const allItems = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data]);
const sections = useMemo(
() =>
groupByDateBucket(allItems, { past: view === "recent" }).map((b) => ({
key: b.key,
title: b.label,
data: b.items,
})),
[allItems, view],
groupByDateBucket(allItems, { past: view === "recent", today: day, locale: i18n.locale }).map(
(b) => ({
key: b.key,
title: b.label,
data: b.items,
}),
),
[allItems, view, day, i18n.locale],
);
const refreshUpcoming = useCallback(
+24
View File
@@ -0,0 +1,24 @@
import { useEffect, useState } from "react";
import { AppState } from "react-native";
import { msUntilNextLocalMidnight } from "@/lib/local-day";
import { formatLocalDate } from "@sofa/i18n/date-buckets";
/** The current local day (YYYY-MM-DD); updates at midnight and when the app returns to the foreground. */
export function useLocalDay(): string {
const [day, setDay] = useState(() => formatLocalDate(new Date()));
useEffect(() => {
let timer = setTimeout(function tick() {
setDay(formatLocalDate(new Date()));
timer = setTimeout(tick, msUntilNextLocalMidnight());
}, msUntilNextLocalMidnight());
const sub = AppState.addEventListener("change", (state) => {
if (state === "active") setDay(formatLocalDate(new Date()));
});
return () => {
clearTimeout(timer);
sub.remove();
};
}, []);
return day;
}
+9
View File
@@ -0,0 +1,9 @@
import { describe, expect, test } from "vitest";
import { msUntilNextLocalMidnight } from "./local-day";
describe("msUntilNextLocalMidnight", () => {
test("counts down to one second past local midnight", () => {
expect(msUntilNextLocalMidnight(new Date(2026, 0, 5, 23, 59, 0))).toBe(61_000);
});
});
+5
View File
@@ -0,0 +1,5 @@
/** Milliseconds until the next local midnight (+1 s slack). */
export function msUntilNextLocalMidnight(now: Date = new Date()): number {
const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 1);
return next.getTime() - now.getTime();
}
+8 -7
View File
@@ -26,11 +26,11 @@ function getEndOfWeek(today: string): string {
return addDays(today, 6);
}
function getMonthLabel(dateStr: string): string {
function getMonthLabel(dateStr: string, locale: string): string {
// Format a UTC instant in UTC: correct regardless of the formatter's default time zone
// (the native Intl polyfill defaults to UTC; browsers use the device zone).
const d = new Date(`${dateStr}T00:00:00Z`);
return new Intl.DateTimeFormat(i18n.locale, { month: "long", timeZone: "UTC" }).format(d);
return new Intl.DateTimeFormat(locale, { month: "long", timeZone: "UTC" }).format(d);
}
type BucketKey = "today" | "tomorrow" | "this_week" | "next_week" | string;
@@ -53,7 +53,7 @@ function getPastBucketKey(dateStr: string, today: string): BucketKey {
return `month_${dateStr.slice(0, 7)}`;
}
function getBucketLabel(key: BucketKey): string {
function getBucketLabel(key: BucketKey, locale: string): string {
if (key === "today") return i18n._(msg`Today`);
if (key === "tomorrow") return i18n._(msg`Tomorrow`);
if (key === "yesterday") return i18n._(msg`Yesterday`);
@@ -61,17 +61,18 @@ function getBucketLabel(key: BucketKey): string {
if (key === "this_week") return i18n._(msg`This Week`);
if (key === "next_week") return i18n._(msg`Next Week`);
if (key.startsWith("month_")) {
return getMonthLabel(`${key.slice(6)}-01`);
return getMonthLabel(`${key.slice(6)}-01`, locale);
}
return key;
}
export function groupByDateBucket<T extends { date: string }>(
items: T[],
options?: { past?: boolean },
options?: { past?: boolean; today?: string; locale?: string },
): DateBucket<T>[] {
const bucketKeyFor = options?.past ? getPastBucketKey : getBucketKey;
const today = getToday();
const today = options?.today ?? getToday();
const locale = options?.locale ?? i18n.locale;
const bucketMap = new Map<string, { label: string; items: T[] }>();
const bucketOrder: string[] = [];
@@ -79,7 +80,7 @@ export function groupByDateBucket<T extends { date: string }>(
const key = bucketKeyFor(item.date, today);
let bucket = bucketMap.get(key);
if (!bucket) {
bucket = { label: getBucketLabel(key), items: [] };
bucket = { label: getBucketLabel(key, locale), items: [] };
bucketMap.set(key, bucket);
bucketOrder.push(key);
}
+14
View File
@@ -187,4 +187,18 @@ describe("groupByDateBucket past option", () => {
const result = groupByDateBucket([{ date: "2024-06-13" }]);
expect(result[0].key).toBe("tomorrow");
});
test("an explicit today overrides the real date", () => {
const result = groupByDateBucket([{ date: "2026-01-06" }], { today: "2026-01-05" });
expect(result[0].key).toBe("tomorrow");
});
test("an explicit locale drives month labels", () => {
const result = groupByDateBucket([{ date: "2026-02-20" }], {
today: "2026-01-05",
locale: "de",
});
expect(result[0].key).toBe("month_2026-02");
expect(result[0].label).toBe("Februar");
});
});