fix: show a task's due time in local time like events

A task's `due` is wall time in the task's `timeZone`, but the tasks
sheet, the calendar chips and task reminders read it as device time,
so a task due at 17:00 Europe/Berlin showed and fired at 17:00
wherever the phone was. Events already go through `utcStart`.

- resolve the due in its zone with a new `getTaskDueDate` and use it
  for the list, sorting, overdue, the chips and reminder times
- save an edited timed due with the effective time zone, as the event
  editor does, so the converted local time isn't read back in the
  task's old zone
This commit is contained in:
Linus Rath
2026-09-24 19:12:02 +02:00
parent 1b64c14280
commit 6a975b822c
7 changed files with 74 additions and 19 deletions
+16 -12
View File
@@ -24,11 +24,12 @@ import {
Trash2,
X,
} from 'lucide-react-native';
import { format, isBefore, isToday, parseISO, startOfDay } from 'date-fns';
import { format, isBefore, isToday, startOfDay } from 'date-fns';
import type { Calendar, CalendarEvent } from '../../api/types';
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors';
import { getCalendarColor, timePattern, type TimeFormat } from '../../lib/calendar-utils';
import { getCalendarColor, getTaskDueDate, timePattern, type TimeFormat } from '../../lib/calendar-utils';
import { getEffectiveTimeZone } from '../../lib/calendar-timezone';
import { isWritableCalendar } from '../../lib/calendar-editability';
import { useCalendarLocale } from '../../lib/calendar-locale';
import { useSheetDrag } from '../../lib/use-sheet-drag';
@@ -74,9 +75,9 @@ function isCompleted(task: CalendarEvent): boolean {
}
function isOverdue(task: CalendarEvent): boolean {
if (!task.due || isCompleted(task)) return false;
const d = parseISO(task.due);
if (isNaN(d.getTime())) return false;
if (isCompleted(task)) return false;
const d = getTaskDueDate(task);
if (!d) return false;
return isBefore(d, startOfDay(new Date())) && !isToday(d);
}
@@ -89,8 +90,8 @@ function compareTasks(a: CalendarEvent, b: CalendarEvent): number {
const ao = isOverdue(a);
const bo = isOverdue(b);
if (ao !== bo) return ao ? -1 : 1;
const ad = a.due ? parseISO(a.due).getTime() : Infinity;
const bd = b.due ? parseISO(b.due).getTime() : Infinity;
const ad = getTaskDueDate(a)?.getTime() ?? Infinity;
const bd = getTaskDueDate(b)?.getTime() ?? Infinity;
if (ad !== bd) return ad - bd;
const ap = a.priority || 10;
const bp = b.priority || 10;
@@ -113,12 +114,11 @@ function emptyEditor(calendarId: string): EditorState {
}
function editorFromTask(task: CalendarEvent, fallbackCalendarId: string): EditorState {
const due = task.due ? parseISO(task.due) : null;
return {
id: task.id,
title: task.title || '',
description: task.description || '',
due: due && !isNaN(due.getTime()) ? due : null,
due: getTaskDueDate(task),
withTime: !!task.due && !task.showWithoutTime && !/^\d{4}-\d{2}-\d{2}$/.test(task.due),
priority: priorityToLevel(task.priority),
calendarId: Object.keys(task.calendarIds || {})[0] || fallbackCalendarId,
@@ -236,9 +236,14 @@ export function TasksSheet({
if (editor.withTime) {
data.due = format(editor.due, "yyyy-MM-dd'T'HH:mm:ss");
data.showWithoutTime = false;
// The editor shows the due in local time; label it with the zone
// it was entered in, like the event editor, or the task's old zone
// would shift it.
data.timeZone = getEffectiveTimeZone();
} else {
data.due = format(editor.due, "yyyy-MM-dd'T'00:00:00");
data.showWithoutTime = true;
data.timeZone = null;
}
} else if (editor.id) {
data.due = null;
@@ -260,9 +265,8 @@ export function TasksSheet({
};
const dueLabel = (task: CalendarEvent): string | null => {
if (!task.due) return null;
const d = parseISO(task.due);
if (isNaN(d.getTime())) return null;
const d = getTaskDueDate(task);
if (!d || !task.due) return null;
const hasTime = !task.showWithoutTime && !/^\d{4}-\d{2}-\d{2}$/.test(task.due);
return hasTime
? format(d, `EEE, MMM d · ${timePattern(timeFormat)}`, { locale })