mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: show today and the now-line in the calendar time zone
The calendar placed events by the calendar time-zone setting, but "today", the now-line and the "Today" button still went by the device clock. With the setting away from the device zone, the grid could mark yesterday as today, draw the now-line hours off and jump to the wrong day. - the views, the agenda's "Today"/"Tomorrow" headers and today anchor, the selected day, the scroll window's start and the "Today" button use `displayNow`, `isDisplayToday`, `isDisplayTomorrow` and `displayNowMinutes` - a long press at an hour already hands the editor a calendar-zone time, which it converts when it saves - changing the setting re-buckets the days and reloads the loaded range, since the server reads floating times and the range bounds in that zone
This commit is contained in:
@@ -9,7 +9,8 @@ import {
|
|||||||
type LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
type ViewToken,
|
type ViewToken,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { format, isSameDay, isToday, isTomorrow, type Locale } from 'date-fns';
|
import { format, type Locale } from 'date-fns';
|
||||||
|
import { displayNow, isDisplayToday, isDisplayTomorrow } from '../../lib/calendar-timezone';
|
||||||
import type { Calendar, CalendarEvent } from '../../api/types';
|
import type { Calendar, CalendarEvent } from '../../api/types';
|
||||||
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||||
import { useColors } from '../../theme/colors';
|
import { useColors } from '../../theme/colors';
|
||||||
@@ -59,8 +60,9 @@ function formatDayHeader(
|
|||||||
t: (key: string, fallback?: string) => string,
|
t: (key: string, fallback?: string) => string,
|
||||||
locale: Locale,
|
locale: Locale,
|
||||||
): string {
|
): string {
|
||||||
if (isToday(date)) return t('calendar.events.today_header', 'Today');
|
// Today and tomorrow on a clock in the calendar's time zone.
|
||||||
if (isTomorrow(date)) return t('calendar.events.tomorrow_header', 'Tomorrow');
|
if (isDisplayToday(date)) return t('calendar.events.today_header', 'Today');
|
||||||
|
if (isDisplayTomorrow(date)) return t('calendar.events.tomorrow_header', 'Tomorrow');
|
||||||
return format(date, 'EEEE, MMM d', { locale });
|
return format(date, 'EEEE, MMM d', { locale });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ export function AgendaView({
|
|||||||
|
|
||||||
const sections = React.useMemo<DaySection[]>(
|
const sections = React.useMemo<DaySection[]>(
|
||||||
() =>
|
() =>
|
||||||
buildAgendaDays(index, loaded, new Date()).map((day) => ({
|
buildAgendaDays(index, loaded, displayNow()).map((day) => ({
|
||||||
...day,
|
...day,
|
||||||
key: dayKey(day.date),
|
key: dayKey(day.date),
|
||||||
title: formatDayHeader(day.date, t, locale),
|
title: formatDayHeader(day.date, t, locale),
|
||||||
@@ -310,7 +312,7 @@ export function AgendaView({
|
|||||||
maxToRenderPerBatch={12}
|
maxToRenderPerBatch={12}
|
||||||
windowSize={11}
|
windowSize={11}
|
||||||
renderSectionHeader={({ section }) => {
|
renderSectionHeader={({ section }) => {
|
||||||
const today = isSameDay(section.date, new Date());
|
const today = isDisplayToday(section.date);
|
||||||
return (
|
return (
|
||||||
<View style={styles.sectionHeader}>
|
<View style={styles.sectionHeader}>
|
||||||
<Text style={[styles.sectionTitle, today && styles.sectionTitleToday]}>
|
<Text style={[styles.sectionTitle, today && styles.sectionTitleToday]}>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
windowWeekStarts,
|
windowWeekStarts,
|
||||||
} from '../../lib/calendar-month-scroll';
|
} from '../../lib/calendar-month-scroll';
|
||||||
import { useCalendarLocale } from '../../lib/calendar-locale';
|
import { useCalendarLocale } from '../../lib/calendar-locale';
|
||||||
|
import { displayNow } from '../../lib/calendar-timezone';
|
||||||
import {
|
import {
|
||||||
MONTH_ROW_HEIGHT,
|
MONTH_ROW_HEIGHT,
|
||||||
MONTH_ROW_HEIGHT_CHIPS,
|
MONTH_ROW_HEIGHT_CHIPS,
|
||||||
@@ -173,7 +174,8 @@ function MonthScrollViewInner({
|
|||||||
}, [focus, window, weeks.length, opts, rowHeight]);
|
}, [focus, window, weeks.length, opts, rowHeight]);
|
||||||
|
|
||||||
const selectedKey = dayKey(selectedDate);
|
const selectedKey = dayKey(selectedDate);
|
||||||
const todayKey = dayKey(new Date());
|
// Today on a clock in the calendar's time zone.
|
||||||
|
const todayKey = dayKey(displayNow());
|
||||||
|
|
||||||
const renderItem = React.useCallback(
|
const renderItem = React.useCallback(
|
||||||
({ item: days }: ListRenderItemInfo<Date[]>) => {
|
({ item: days }: ListRenderItemInfo<Date[]>) => {
|
||||||
@@ -182,7 +184,7 @@ function MonthScrollViewInner({
|
|||||||
days={days}
|
days={days}
|
||||||
activeMask={monthMask(days, activeMonth)}
|
activeMask={monthMask(days, activeMonth)}
|
||||||
selectedIndex={dayIndexIn(days, selectedDate)}
|
selectedIndex={dayIndexIn(days, selectedDate)}
|
||||||
todayIndex={dayIndexIn(days, new Date())}
|
todayIndex={dayIndexIn(days, displayNow())}
|
||||||
index={index}
|
index={index}
|
||||||
calendars={calendars}
|
calendars={calendars}
|
||||||
weekStartsOn={weekStartsOn}
|
weekStartsOn={weekStartsOn}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from '../../lib/calendar-utils';
|
} from '../../lib/calendar-utils';
|
||||||
import { dayIndexIn, monthKeyOf, monthMask } from '../../lib/calendar-month-scroll';
|
import { dayIndexIn, monthKeyOf, monthMask } from '../../lib/calendar-month-scroll';
|
||||||
import { useCalendarLocale } from '../../lib/calendar-locale';
|
import { useCalendarLocale } from '../../lib/calendar-locale';
|
||||||
|
import { displayNow } from '../../lib/calendar-timezone';
|
||||||
|
|
||||||
type WeekStart = 0 | 1 | 6;
|
type WeekStart = 0 | 1 | 6;
|
||||||
|
|
||||||
@@ -258,7 +259,8 @@ function MonthViewInner({
|
|||||||
return out;
|
return out;
|
||||||
}, [currentDate, weekStartsOn]);
|
}, [currentDate, weekStartsOn]);
|
||||||
const activeMonth = monthKeyOf(currentDate);
|
const activeMonth = monthKeyOf(currentDate);
|
||||||
const today = new Date();
|
// Today on a clock in the calendar's time zone.
|
||||||
|
const today = displayNow();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.grid}>
|
<View style={styles.grid}>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
type TimedEventLayout,
|
type TimedEventLayout,
|
||||||
} from '../../lib/calendar-utils';
|
} from '../../lib/calendar-utils';
|
||||||
import type { CalendarFocus, DayRange } from '../../lib/calendar-scroll-window';
|
import type { CalendarFocus, DayRange } from '../../lib/calendar-scroll-window';
|
||||||
|
import { displayNow, displayNowMinutes } from '../../lib/calendar-timezone';
|
||||||
import {
|
import {
|
||||||
buildAllDaySegments,
|
buildAllDaySegments,
|
||||||
headerColumnRange,
|
headerColumnRange,
|
||||||
@@ -262,21 +263,18 @@ function TimeGridScrollViewInner({
|
|||||||
// Start the hours at the one before now, like the paged week view.
|
// Start the hours at the one before now, like the paged week view.
|
||||||
const vScrollRef = React.useRef<ScrollView>(null);
|
const vScrollRef = React.useRef<ScrollView>(null);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const target = Math.max(0, (new Date().getHours() - 1) * HOUR_HEIGHT);
|
const target = Math.max(0, (displayNow().getHours() - 1) * HOUR_HEIGHT);
|
||||||
const frame = requestAnimationFrame(() => {
|
const frame = requestAnimationFrame(() => {
|
||||||
vScrollRef.current?.scrollTo({ y: target, animated: false });
|
vScrollRef.current?.scrollTo({ y: target, animated: false });
|
||||||
});
|
});
|
||||||
return () => cancelAnimationFrame(frame);
|
return () => cancelAnimationFrame(frame);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [nowMinutes, setNowMinutes] = React.useState(() => {
|
// The now-line and "today" follow a clock in the calendar's time zone.
|
||||||
const now = new Date();
|
const [nowMinutes, setNowMinutes] = React.useState(displayNowMinutes);
|
||||||
return now.getHours() * 60 + now.getMinutes();
|
|
||||||
});
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const now = new Date();
|
setNowMinutes(displayNowMinutes());
|
||||||
setNowMinutes(now.getHours() * 60 + now.getMinutes());
|
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -284,6 +282,8 @@ function TimeGridScrollViewInner({
|
|||||||
const handleLongPressAt = React.useCallback(
|
const handleLongPressAt = React.useCallback(
|
||||||
(day: Date, hour: number) => {
|
(day: Date, hour: number) => {
|
||||||
if (!onCreateAtTime) return;
|
if (!onCreateAtTime) return;
|
||||||
|
// A display date (that hour in the calendar's zone); the editor turns
|
||||||
|
// it into the real instant when it saves (eventTimeFieldsToSave).
|
||||||
const date = new Date(day);
|
const date = new Date(day);
|
||||||
date.setHours(hour, 0, 0, 0);
|
date.setHours(hour, 0, 0, 0);
|
||||||
onCreateAtTime(date);
|
onCreateAtTime(date);
|
||||||
@@ -291,7 +291,7 @@ function TimeGridScrollViewInner({
|
|||||||
[onCreateAtTime],
|
[onCreateAtTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
const todayKey = dayKey(new Date());
|
const todayKey = dayKey(displayNow());
|
||||||
const renderItem = React.useCallback(
|
const renderItem = React.useCallback(
|
||||||
({ item: day }: ListRenderItemInfo<Date>) => (
|
({ item: day }: ListRenderItemInfo<Date>) => (
|
||||||
<DayColumn
|
<DayColumn
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
Pressable,
|
Pressable,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { addDays, format, isSameDay, isToday, startOfWeek } from 'date-fns';
|
import { addDays, format, isSameDay, startOfWeek } from 'date-fns';
|
||||||
|
import { displayNow, displayNowMinutes, isDisplayToday } from '../../lib/calendar-timezone';
|
||||||
import type { Calendar, CalendarEvent } from '../../api/types';
|
import type { Calendar, CalendarEvent } from '../../api/types';
|
||||||
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||||
import { useColors } from '../../theme/colors';
|
import { useColors } from '../../theme/colors';
|
||||||
@@ -106,21 +107,18 @@ function WeekViewInner({
|
|||||||
});
|
});
|
||||||
}, [weekDays, index]);
|
}, [weekDays, index]);
|
||||||
|
|
||||||
const [nowMinutes, setNowMinutes] = React.useState(() => {
|
// The now-line and "today" follow a clock in the calendar's time zone.
|
||||||
const now = new Date();
|
const [nowMinutes, setNowMinutes] = React.useState(displayNowMinutes);
|
||||||
return now.getHours() * 60 + now.getMinutes();
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const now = new Date();
|
setNowMinutes(displayNowMinutes());
|
||||||
setNowMinutes(now.getHours() * 60 + now.getMinutes());
|
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const target = Math.max(0, (new Date().getHours() - 1) * HOUR_HEIGHT);
|
const target = Math.max(0, (displayNow().getHours() - 1) * HOUR_HEIGHT);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
scrollRef.current?.scrollTo({ y: target, animated: false });
|
scrollRef.current?.scrollTo({ y: target, animated: false });
|
||||||
});
|
});
|
||||||
@@ -128,13 +126,14 @@ function WeekViewInner({
|
|||||||
|
|
||||||
const handleSlotLongPress = (day: Date, hour: number) => {
|
const handleSlotLongPress = (day: Date, hour: number) => {
|
||||||
if (!onCreateAtTime) return;
|
if (!onCreateAtTime) return;
|
||||||
|
// A display date; the editor converts it when it saves.
|
||||||
const date = new Date(day);
|
const date = new Date(day);
|
||||||
date.setHours(hour, 0, 0, 0);
|
date.setHours(hour, 0, 0, 0);
|
||||||
onCreateAtTime(date);
|
onCreateAtTime(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dayHeader = (day: Date) => {
|
const dayHeader = (day: Date) => {
|
||||||
const today = isToday(day);
|
const today = isDisplayToday(day);
|
||||||
const selected = isSameDay(day, selectedDate);
|
const selected = isSameDay(day, selectedDate);
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -244,7 +243,7 @@ function WeekViewInner({
|
|||||||
<View style={styles.dayCols}>
|
<View style={styles.dayCols}>
|
||||||
{weekDays.map((day, dayIndex) => {
|
{weekDays.map((day, dayIndex) => {
|
||||||
const layouted = layoutsByDay[dayIndex];
|
const layouted = layoutsByDay[dayIndex];
|
||||||
const todayCol = isToday(day);
|
const todayCol = isDisplayToday(day);
|
||||||
return (
|
return (
|
||||||
<View key={day.toISOString()} style={styles.dayCol}>
|
<View key={day.toISOString()} style={styles.dayCol}>
|
||||||
{HOURS.map((h) => (
|
{HOURS.map((h) => (
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
// Device in New York, calendar in Berlin. At 23:30 UTC on 15 January it is
|
||||||
|
// still the 15th (18:30) on the device but already the 16th (00:30) on the
|
||||||
|
// calendar's clock.
|
||||||
|
vi.hoisted(() => {
|
||||||
|
process.env.TZ = 'America/New_York';
|
||||||
|
});
|
||||||
|
|
||||||
|
import { useSettingsStore } from '../../stores/settings-store';
|
||||||
|
import {
|
||||||
|
displayNow,
|
||||||
|
displayNowMinutes,
|
||||||
|
isDisplayToday,
|
||||||
|
isDisplayTomorrow,
|
||||||
|
} from '../calendar-timezone';
|
||||||
|
import { buildAgendaDays } from '../calendar-agenda';
|
||||||
|
import { eventTimeFieldsToSave } from '../event-editor-times';
|
||||||
|
|
||||||
|
const NOW = new Date('2026-01-15T23:30:00Z');
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers({ now: NOW });
|
||||||
|
useSettingsStore.setState({ calendarTimeZone: 'Europe/Berlin' });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
useSettingsStore.setState({ calendarTimeZone: 'auto' });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('today in the calendar time zone', () => {
|
||||||
|
it('reads now on a clock in the calendar zone', () => {
|
||||||
|
const now = displayNow();
|
||||||
|
expect([now.getDate(), now.getHours(), now.getMinutes()]).toEqual([16, 0, 30]);
|
||||||
|
expect(displayNowMinutes()).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks the calendar zone\'s today and tomorrow, not the device\'s', () => {
|
||||||
|
expect(isDisplayToday(new Date(2026, 0, 16))).toBe(true);
|
||||||
|
expect(isDisplayToday(new Date(2026, 0, 15))).toBe(false);
|
||||||
|
expect(isDisplayTomorrow(new Date(2026, 0, 17))).toBe(true);
|
||||||
|
expect(isDisplayTomorrow(new Date(2026, 0, 16))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('follows the device again when the calendar does', () => {
|
||||||
|
useSettingsStore.setState({ calendarTimeZone: 'auto' });
|
||||||
|
expect(displayNow().getDate()).toBe(15);
|
||||||
|
expect(displayNowMinutes()).toBe(18 * 60 + 30);
|
||||||
|
expect(isDisplayToday(new Date(2026, 0, 15))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('anchors the agenda on the calendar zone\'s today', () => {
|
||||||
|
const days = buildAgendaDays(new Map(), { start: new Date(2026, 0, 10), end: new Date(2026, 0, 20) }, displayNow());
|
||||||
|
expect(days.map((d) => d.date.getDate())).toEqual([16]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an event from a long press at that hour of the calendar zone', () => {
|
||||||
|
// What the grids hand the editor for a long press at 09:00 on the 16th.
|
||||||
|
const start = new Date(2026, 0, 16);
|
||||||
|
start.setHours(9, 0, 0, 0);
|
||||||
|
const end = new Date(start.getTime() + 60 * 60_000);
|
||||||
|
expect(eventTimeFieldsToSave({ allDay: false, start, end })).toEqual({
|
||||||
|
start: '2026-01-16T09:00:00',
|
||||||
|
duration: 'PT1H',
|
||||||
|
timeZone: 'Europe/Berlin',
|
||||||
|
showWithoutTime: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -222,10 +222,26 @@ export function displayNow(): Date {
|
|||||||
return toDisplayDate(new Date());
|
return toDisplayDate(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameLocalDay(a: Date, b: Date): boolean {
|
||||||
|
return a.getFullYear() === b.getFullYear()
|
||||||
|
&& a.getMonth() === b.getMonth()
|
||||||
|
&& a.getDate() === b.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
/** Is the display date `date` on today's calendar day in the calendar's zone? */
|
/** Is the display date `date` on today's calendar day in the calendar's zone? */
|
||||||
export function isDisplayToday(date: Date): boolean {
|
export function isDisplayToday(date: Date): boolean {
|
||||||
|
return sameLocalDay(date, displayNow());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Is the display date `date` on tomorrow's calendar day in the calendar's zone? */
|
||||||
|
export function isDisplayTomorrow(date: Date): boolean {
|
||||||
|
const tomorrow = displayNow();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
return sameLocalDay(date, tomorrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minutes since midnight on a clock in the calendar's zone: where the grids draw the now-line. */
|
||||||
|
export function displayNowMinutes(): number {
|
||||||
const now = displayNow();
|
const now = displayNow();
|
||||||
return date.getFullYear() === now.getFullYear()
|
return now.getHours() * 60 + now.getMinutes();
|
||||||
&& date.getMonth() === now.getMonth()
|
|
||||||
&& date.getDate() === now.getDate();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
endOfWeek,
|
endOfWeek,
|
||||||
addDays,
|
addDays,
|
||||||
subDays,
|
subDays,
|
||||||
isToday,
|
|
||||||
addMonths,
|
addMonths,
|
||||||
subMonths,
|
subMonths,
|
||||||
addWeeks,
|
addWeeks,
|
||||||
@@ -37,6 +36,7 @@ import {
|
|||||||
type Locale,
|
type Locale,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { useCalendarLocale } from '../lib/calendar-locale';
|
import { useCalendarLocale } from '../lib/calendar-locale';
|
||||||
|
import { displayNow, isDisplayToday } from '../lib/calendar-timezone';
|
||||||
import { spacing, radius, typography, type ThemePalette } from '../theme/tokens';
|
import { spacing, radius, typography, type ThemePalette } from '../theme/tokens';
|
||||||
import { useColors } from '../theme/colors';
|
import { useColors } from '../theme/colors';
|
||||||
import { Button } from '../components';
|
import { Button } from '../components';
|
||||||
@@ -172,8 +172,10 @@ export default function CalendarScreen() {
|
|||||||
const setSharedCalendarColor = useSettingsStore((s) => s.setSharedCalendarColor);
|
const setSharedCalendarColor = useSettingsStore((s) => s.setSharedCalendarColor);
|
||||||
const removeSharedCalendarColor = useSettingsStore((s) => s.removeSharedCalendarColor);
|
const removeSharedCalendarColor = useSettingsStore((s) => s.removeSharedCalendarColor);
|
||||||
const contacts = useContactsStore((s) => s.contacts);
|
const contacts = useContactsStore((s) => s.contacts);
|
||||||
|
const calendarTimeZone = useSettingsStore((s) => s.calendarTimeZone);
|
||||||
|
|
||||||
const [selectedDate, setSelectedDate] = React.useState(() => new Date());
|
// "Today" is the day on a clock in the calendar's time zone.
|
||||||
|
const [selectedDate, setSelectedDate] = React.useState(displayNow);
|
||||||
const initialViewMode: ViewMode =
|
const initialViewMode: ViewMode =
|
||||||
calendarDefaultView === 'week' || calendarDefaultView === 'day' || calendarDefaultView === 'agenda'
|
calendarDefaultView === 'week' || calendarDefaultView === 'day' || calendarDefaultView === 'agenda'
|
||||||
? calendarDefaultView
|
? calendarDefaultView
|
||||||
@@ -190,11 +192,11 @@ export default function CalendarScreen() {
|
|||||||
() => ({ weekStartsOn: calendarFirstDayOfWeek }),
|
() => ({ weekStartsOn: calendarFirstDayOfWeek }),
|
||||||
[calendarFirstDayOfWeek],
|
[calendarFirstDayOfWeek],
|
||||||
);
|
);
|
||||||
const [focus, setFocus] = React.useState<CalendarFocus>(() => ({ date: new Date(), nonce: 0 }));
|
const [focus, setFocus] = React.useState<CalendarFocus>(() => ({ date: displayNow(), nonce: 0 }));
|
||||||
// The day the scrolled view shows at its top, while it differs from the focus.
|
// The day the scrolled view shows at its top, while it differs from the focus.
|
||||||
const [visibleDate, setVisibleDate] = React.useState<Date | null>(null);
|
const [visibleDate, setVisibleDate] = React.useState<Date | null>(null);
|
||||||
const [windowState, setWindowState] = React.useState<ScrollWindowState>(
|
const [windowState, setWindowState] = React.useState<ScrollWindowState>(
|
||||||
() => freshScrollWindowState(initialViewMode, new Date()),
|
() => freshScrollWindowState(initialViewMode, displayNow()),
|
||||||
);
|
);
|
||||||
// The sideways-scrolling week and day grids assume a left-to-right strip;
|
// The sideways-scrolling week and day grids assume a left-to-right strip;
|
||||||
// right-to-left layouts keep them paged.
|
// right-to-left layouts keep them paged.
|
||||||
@@ -402,7 +404,21 @@ export default function CalendarScreen() {
|
|||||||
}, [allEvents, hiddenCalendarIds]);
|
}, [allEvents, hiddenCalendarIds]);
|
||||||
// Pre-index events by day once. Child views do O(1) map lookups per cell
|
// Pre-index events by day once. Child views do O(1) map lookups per cell
|
||||||
// instead of re-filtering the full event list with parseISO per day.
|
// instead of re-filtering the full event list with parseISO per day.
|
||||||
const eventsByDay = React.useMemo(() => buildEventDayIndex(events), [events]);
|
// The days events fall on depend on the calendar's time zone too.
|
||||||
|
const eventsByDay = React.useMemo(
|
||||||
|
() => buildEventDayIndex(events),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[events, calendarTimeZone],
|
||||||
|
);
|
||||||
|
|
||||||
|
// A new calendar time zone also changes how the server reads floating
|
||||||
|
// times and the range bounds: reload what is loaded.
|
||||||
|
const loadedTimeZoneRef = React.useRef(calendarTimeZone);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (loadedTimeZoneRef.current === calendarTimeZone) return;
|
||||||
|
loadedTimeZoneRef.current = calendarTimeZone;
|
||||||
|
void refresh();
|
||||||
|
}, [calendarTimeZone, refresh]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
void hydrate();
|
void hydrate();
|
||||||
@@ -492,7 +508,7 @@ export default function CalendarScreen() {
|
|||||||
}, [viewMode, visibleDate, focus.date, jumpTo]);
|
}, [viewMode, visibleDate, focus.date, jumpTo]);
|
||||||
|
|
||||||
const goToday = React.useCallback(() => {
|
const goToday = React.useCallback(() => {
|
||||||
jumpTo(new Date());
|
jumpTo(displayNow());
|
||||||
}, [jumpTo]);
|
}, [jumpTo]);
|
||||||
|
|
||||||
// Switching views keeps the period on screen in view.
|
// Switching views keeps the period on screen in view.
|
||||||
@@ -906,7 +922,7 @@ export default function CalendarScreen() {
|
|||||||
[t, removeCalendar, reportError],
|
[t, removeCalendar, reportError],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSelectedToday = isToday(selectedDate);
|
const isSelectedToday = isDisplayToday(selectedDate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
|
|||||||
Reference in New Issue
Block a user