diff --git a/src/Commands/CreateOrEditTaskParser.ts b/src/Commands/CreateOrEditTaskParser.ts index 45b91f8472..1af0d806e6 100644 --- a/src/Commands/CreateOrEditTaskParser.ts +++ b/src/Commands/CreateOrEditTaskParser.ts @@ -2,6 +2,7 @@ import { TasksFile } from '../Scripting/TasksFile'; import { Status } from '../Statuses/Status'; import { OnCompletion } from '../Task/OnCompletion'; import { Task } from '../Task/Task'; +import { momentAdjusted } from '../DateTime/DateAdjusted'; import { DateFallback } from '../DateTime/DateFallback'; import { StatusRegistry } from '../Statuses/StatusRegistry'; import { TaskLocation } from '../Task/TaskLocation'; @@ -12,7 +13,7 @@ import { TaskRegularExpressions } from '../Task/TaskRegularExpressions'; function getDefaultCreatedDate() { const { setCreatedDate } = getSettings(); - return setCreatedDate ? window.moment() : null; + return setCreatedDate ? momentAdjusted() : null; } function shouldUpdateCreatedDateForTask(task: Task) { diff --git a/src/Config/Settings.ts b/src/Config/Settings.ts index a905b060fb..bbd20ae9a9 100644 --- a/src/Config/Settings.ts +++ b/src/Config/Settings.ts @@ -70,6 +70,7 @@ export interface Settings { setCreatedDate: boolean; setDoneDate: boolean; setCancelledDate: boolean; + nextDayStartHour: number; autoSuggestInEditor: boolean; autoSuggestMinMatch: number; autoSuggestMaxItems: number; @@ -106,6 +107,7 @@ const defaultSettings: Readonly = { setCreatedDate: false, setDoneDate: true, setCancelledDate: true, + nextDayStartHour: 0, autoSuggestInEditor: true, autoSuggestMinMatch: 0, autoSuggestMaxItems: 20, diff --git a/src/Config/SettingsTab.ts b/src/Config/SettingsTab.ts index 2dfaf3a465..36fe6e1daa 100644 --- a/src/Config/SettingsTab.ts +++ b/src/Config/SettingsTab.ts @@ -331,6 +331,31 @@ export class SettingsTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName(i18n.t('settings.dates.nextDayStartHour.name')) + .setDesc( + SettingsTab.createFragmentWithHTML( + '

' + + i18n.t('settings.dates.nextDayStartHour.description') + + '

' + + i18n.t('settings.changeRequiresRestart') + + '

' + + this.seeTheDocumentation( + 'https://publish.obsidian.md/tasks/Getting+Started/Dates#Next+day+start+at', + ), + ), + ) + .addSlider((slider) => { + const settings = getSettings(); + slider.setLimits(0, 23, 1); + slider.setValue(settings.nextDayStartHour); + slider.setDynamicTooltip(); + slider.onChange(async (value) => { + updateSettings({ nextDayStartHour: value }); + await this.plugin.saveSettings(); + }); + }); + // --------------------------------------------------------------------------- new Setting(containerEl).setName(i18n.t('settings.datesFromFileNames.heading')).setHeading(); // --------------------------------------------------------------------------- diff --git a/src/DateTime/DateAdjusted.ts b/src/DateTime/DateAdjusted.ts new file mode 100644 index 0000000000..fbaa69dadc --- /dev/null +++ b/src/DateTime/DateAdjusted.ts @@ -0,0 +1,10 @@ +import type { Moment } from 'moment'; +import { getSettings } from '../Config/Settings'; + +/** + * Returns the current moment, adjusted for the "Next day start hour" setting. + */ +export function momentAdjusted(): Moment { + const { nextDayStartHour } = getSettings(); + return window.moment().subtract(nextDayStartHour, 'hours'); +} diff --git a/src/DateTime/DateRange.ts b/src/DateTime/DateRange.ts index 91d1af2853..1a55ea0097 100644 --- a/src/DateTime/DateRange.ts +++ b/src/DateTime/DateRange.ts @@ -1,4 +1,5 @@ import type { Moment } from 'moment'; +import { momentAdjusted } from './DateAdjusted'; /** * Represent an inclusive span of time between two days at 00:00 local time. @@ -41,8 +42,8 @@ export class DateRange { const unitOfTime = range === 'week' ? 'isoWeek' : range; return new DateRange( - window.moment().startOf(unitOfTime).startOf('day'), - window.moment().endOf(unitOfTime).startOf('day'), + momentAdjusted().startOf(unitOfTime).startOf('day'), + momentAdjusted().endOf(unitOfTime).startOf('day'), ); } diff --git a/src/DateTime/DateTools.ts b/src/DateTime/DateTools.ts index fa9b61b9e7..f2fcd9553b 100644 --- a/src/DateTime/DateTools.ts +++ b/src/DateTime/DateTools.ts @@ -1,4 +1,5 @@ import * as chrono from 'chrono-node'; +import { momentAdjusted } from './DateAdjusted'; export function compareByDate(a: moment.Moment | null, b: moment.Moment | null): -1 | 0 | 1 { if (a !== null && b === null) { @@ -77,7 +78,7 @@ export function parseTypedDateForDisplayUsingFutureDate( typedDate: string, forwardOnly: boolean, ): string { - return parseTypedDateForDisplay(fieldName, typedDate, forwardOnly ? new Date() : undefined); + return parseTypedDateForDisplay(fieldName, typedDate, forwardOnly ? momentAdjusted().toDate() : undefined); } /** @@ -87,7 +88,7 @@ export function parseTypedDateForDisplayUsingFutureDate( */ export function parseTypedDateForSaving(typedDate: string, forwardDate: boolean): moment.Moment | null { let date: moment.Moment | null = null; - const parsedDate = chrono.parseDate(typedDate, new Date(), { forwardDate }); + const parsedDate = chrono.parseDate(typedDate, momentAdjusted().toDate(), { forwardDate }); if (parsedDate !== null) { date = window.moment(parsedDate); } diff --git a/src/DateTime/Postponer.ts b/src/DateTime/Postponer.ts index 0fe12b9eda..b4070d0408 100644 --- a/src/DateTime/Postponer.ts +++ b/src/DateTime/Postponer.ts @@ -1,6 +1,7 @@ import type { Moment, unitOfTime } from 'moment'; import { capitalizeFirstLetter } from '../lib/StringHelpers'; import { Task } from '../Task/Task'; +import { momentAdjusted } from './DateAdjusted'; import { DateFallback } from './DateFallback'; import { TasksDate } from './TasksDate'; import type { AllTaskDateFields, HappensDate } from './DateFieldTypes'; @@ -81,7 +82,7 @@ export function createFixedDateTask( timeUnit: unitOfTime.DurationConstructor, amount: number, ) { - const dateToPostpone = window.moment(); + const dateToPostpone = momentAdjusted(); return createPostponedTaskFromDate(dateToPostpone, task, dateFieldToPostpone, timeUnit, amount); } @@ -164,7 +165,7 @@ export function postponeMenuItemTitle(task: Task, amount: number, timeUnit: unit */ export function fixedDateMenuItemTitle(task: Task, amount: number, timeUnit: unitOfTime.DurationConstructor) { const updatedDateType = getDateFieldToPostpone(task)!; - const dateToUpdate = window.moment().startOf('day'); + const dateToUpdate = momentAdjusted().startOf('day'); return postponeMenuItemTitleFromDate(updatedDateType, dateToUpdate, amount, timeUnit); } @@ -207,7 +208,7 @@ export function postponeMenuItemTitleFromDate( const formattedNewDate = postponedDate.format('ddd Do MMM'); const amountOrArticle = amount != 1 ? amount : 'a'; - if (dateToUpdate.isSameOrBefore(window.moment(), 'day')) { + if (dateToUpdate.isSameOrBefore(momentAdjusted(), 'day')) { const updatedDateDisplayText = prettyPrintDateFieldName(updatedDateType); const title = amount >= 0 diff --git a/src/DateTime/TasksDate.ts b/src/DateTime/TasksDate.ts index 44eef875df..abf95226c8 100644 --- a/src/DateTime/TasksDate.ts +++ b/src/DateTime/TasksDate.ts @@ -2,6 +2,7 @@ import type { DurationInputArg2, Moment, unitOfTime } from 'moment'; import { Notice } from 'obsidian'; import { PropertyCategory } from '../lib/PropertyCategory'; import { TaskRegularExpressions } from '../Task/TaskRegularExpressions'; +import { momentAdjusted } from './DateAdjusted'; /** * TasksDate encapsulates a date, for simplifying the JavaScript expressions users need to @@ -59,7 +60,7 @@ export class TasksDate { public get category(): PropertyCategory { // begin-snippet: use-moment-in-src - const today = window.moment(); + const today = momentAdjusted(); // end-snippet const date = this.moment; if (!date) { @@ -96,7 +97,7 @@ export class TasksDate { // - is the same for all dates with the same 'fromNow()' name, // - sorts in ascending order of the date. - const now = window.moment(); + const now = momentAdjusted(); const earlier = date.isSameOrBefore(now, 'day'); const startDateOfThisGroup = this.fromNowStartDateOfGroup(date, earlier, now); const splitPastAndFutureDates = earlier ? 1 : 3; @@ -124,7 +125,7 @@ export class TasksDate { public postpone(unitOfTime: unitOfTime.DurationConstructor = 'days', amount: number = 1) { if (!this._date) throw new Notice('Cannot postpone a null date'); - const today = window.moment().startOf('day'); + const today = momentAdjusted().startOf('day'); // According to the moment.js docs, isBefore is not stable so we use !isSameOrAfter: https://momentjs.com/docs/#/query/is-before/ const isDateBeforeToday = !this._date.isSameOrAfter(today, 'day'); diff --git a/src/Renderer/QueryRenderer.ts b/src/Renderer/QueryRenderer.ts index 01f23929e7..9e0d370a02 100644 --- a/src/Renderer/QueryRenderer.ts +++ b/src/Renderer/QueryRenderer.ts @@ -18,6 +18,7 @@ import { getTaskLineAndFile, replaceTaskWithTasks } from '../Obsidian/File'; import { TaskModal } from '../Obsidian/TaskModal'; import type { TasksEvents } from '../Obsidian/TasksEvents'; import { TasksFile } from '../Scripting/TasksFile'; +import { momentAdjusted } from '../DateTime/DateAdjusted'; import { DateFallback } from '../DateTime/DateFallback'; import type { Task } from '../Task/Task'; import { type BacklinksEventHandler, type EditButtonClickHandler, QueryResultsRenderer } from './QueryResultsRenderer'; @@ -257,11 +258,10 @@ class QueryRenderChild extends MarkdownRenderChild { * to "now". */ private reloadQueryAtMidnight(): void { - const midnight = new Date(); - midnight.setHours(24, 0, 0, 0); - const now = new Date(); + const midnight = momentAdjusted().add(1, 'days').startOf('day'); + const now = momentAdjusted(); - const millisecondsToMidnight = midnight.getTime() - now.getTime(); + const millisecondsToMidnight = midnight.diff(now, 'milliseconds'); this.queryReloadTimeout = setTimeout(() => { this.queryResultsRenderer.query = getQueryForQueryRenderer( diff --git a/src/Renderer/TaskFieldRenderer.ts b/src/Renderer/TaskFieldRenderer.ts index 4f62f20ad8..6932e65c2b 100644 --- a/src/Renderer/TaskFieldRenderer.ts +++ b/src/Renderer/TaskFieldRenderer.ts @@ -3,6 +3,7 @@ import type { Moment } from 'moment'; import type { TaskLayoutComponent } from '../Layout/TaskLayoutOptions'; import { PriorityTools } from '../lib/PriorityTools'; import type { Task } from '../Task/Task'; +import { momentAdjusted } from '../DateTime/DateAdjusted'; /** * A renderer for individual {@link Task} fields in an HTML context. @@ -67,7 +68,7 @@ export class TaskFieldHTMLData { const DAY_VALUE_OVER_RANGE_POSTFIX = 'far'; function dateToAttribute(date: Moment) { - const today = window.moment().startOf('day'); + const today = momentAdjusted().startOf('day'); const diffDays = today.diff(date, 'days'); if (isNaN(diffDays)) { diff --git a/src/Renderer/TaskLineRenderer.ts b/src/Renderer/TaskLineRenderer.ts index b75602ab75..aa787f98cb 100644 --- a/src/Renderer/TaskLineRenderer.ts +++ b/src/Renderer/TaskLineRenderer.ts @@ -9,6 +9,7 @@ import { StatusRegistry } from '../Statuses/StatusRegistry'; import { Task } from '../Task/Task'; import { TaskRegularExpressions } from '../Task/TaskRegularExpressions'; import { StatusMenu } from '../ui/Menus/StatusMenu'; +import { momentAdjusted } from '../DateTime/DateAdjusted'; import type { AllTaskDateFields } from '../DateTime/DateFieldTypes'; import { defaultTaskSaver, showMenu } from '../ui/Menus/TaskEditingMenu'; import { promptForDate } from '../ui/Menus/DatePicker'; @@ -430,7 +431,7 @@ export class TaskLineRenderer { function toTooltipDate({ signifier, date }: { signifier: string; date: Moment }): string { return `${signifier} ${date.format(TaskRegularExpressions.dateFormat)} (${date.from( - window.moment().startOf('day'), + momentAdjusted().startOf('day'), )})`; } diff --git a/src/Task/Recurrence.ts b/src/Task/Recurrence.ts index 1e7e5dfb05..a756c171c9 100644 --- a/src/Task/Recurrence.ts +++ b/src/Task/Recurrence.ts @@ -2,6 +2,7 @@ import type { Moment } from 'moment'; // end-snippet import { RRule } from 'rrule'; +import { momentAdjusted } from '../DateTime/DateAdjusted'; import type { Occurrence } from './Occurrence'; export class Recurrence { @@ -38,7 +39,7 @@ export class Recurrence { if (!baseOnToday && referenceDate !== null) { options.dtstart = window.moment(referenceDate).startOf('day').utc(true).toDate(); } else { - options.dtstart = window.moment().startOf('day').utc(true).toDate(); + options.dtstart = momentAdjusted().startOf('day').utc(true).toDate(); } const rrule = new RRule(options); @@ -73,7 +74,7 @@ export class Recurrence { * * @param today - Optional date representing the completion date. Defaults to today. */ - public next(today = window.moment()): Occurrence | null { + public next(today = momentAdjusted()): Occurrence | null { const nextReferenceDate = this.nextReferenceDate(today); if (nextReferenceDate === null) { diff --git a/src/Task/Task.ts b/src/Task/Task.ts index d8c01b1c0d..fe108e652e 100644 --- a/src/Task/Task.ts +++ b/src/Task/Task.ts @@ -9,6 +9,7 @@ import { StatusType } from '../Statuses/StatusConfiguration'; import { PriorityTools } from '../lib/PriorityTools'; import { logging } from '../lib/logging'; import { logEndOfTaskEdit, logStartOfTaskEdit } from '../lib/LogTasksHelper'; +import { momentAdjusted } from '../DateTime/DateAdjusted'; import { DateFallback } from '../DateTime/DateFallback'; import { ListItem } from './ListItem'; import type { Occurrence } from './Occurrence'; @@ -343,7 +344,7 @@ export class Task extends ListItem { * However, any created date on a new recurrence is, for now, calculated from the * actual current date, rather than this parameter. */ - public handleNewStatus(newStatus: Status, today = window.moment()): Task[] { + public handleNewStatus(newStatus: Status, today = momentAdjusted()): Task[] { if (newStatus.identicalTo(this.status)) { // There is no need to create a new Task object if the new status behaviour is identical to the current one. return [this]; @@ -419,7 +420,7 @@ export class Task extends ListItem { const { setCreatedDate } = getSettings(); let createdDate: moment.Moment | null = null; if (setCreatedDate) { - createdDate = window.moment(); + createdDate = momentAdjusted(); } // In case the task being toggled was previously cancelled, ensure the new task has no cancelled date: const cancelledDate = null; @@ -471,7 +472,7 @@ export class Task extends ListItem { return this.putRecurrenceInUsersOrder(newTasks); } - public handleNewStatusWithRecurrenceInUsersOrder(newStatus: Status, today = window.moment()): Task[] { + public handleNewStatusWithRecurrenceInUsersOrder(newStatus: Status, today = momentAdjusted()): Task[] { const logger = logging.getLogger('tasks.Task'); logger.debug( `changed task ${this.taskLocation.path} ${this.taskLocation.lineNumber} ${this.originalMarkdown} status to '${newStatus.symbol}'`, diff --git a/src/Task/Urgency.ts b/src/Task/Urgency.ts index 525d851e90..336c7546e9 100644 --- a/src/Task/Urgency.ts +++ b/src/Task/Urgency.ts @@ -1,3 +1,4 @@ +import { momentAdjusted } from '../DateTime/DateAdjusted'; import type { Task } from './Task'; export class Urgency { @@ -13,7 +14,7 @@ export class Urgency { if (task.dueDate?.isValid()) { // Map a range of 21 days to the value 0.2 - 1.0 - const startOfToday = window.moment().startOf('day'); + const startOfToday = momentAdjusted().startOf('day'); const daysOverdue = Math.round(startOfToday.diff(task.dueDate) / Urgency.milliSecondsPerDay); let dueMultiplier: number; @@ -30,13 +31,13 @@ export class Urgency { } if (task.scheduledDate?.isValid()) { - if (window.moment().isSameOrAfter(task.scheduledDate)) { + if (momentAdjusted().isSameOrAfter(task.scheduledDate)) { urgency += 1 * Urgency.scheduledCoefficient; } } if (task.startDate?.isValid()) { - if (window.moment().isBefore(task.startDate)) { + if (momentAdjusted().isBefore(task.startDate)) { urgency += 1 * Urgency.startedCoefficient; } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index eeb4383c6c..6523ea96d4 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -107,6 +107,10 @@ }, "changeRequiresRestart": "REQUIRES RESTART.", "dates": { + "nextDayStartHour": { + "description": "Sets the hour at which the new day begins. Interacting with tasks after midnight but before this hour is treated as if interacting the day before.", + "name": "Next day starts at" + }, "cancelledDate": { "description": "Enabling this will add a timestamp ❌ YYYY-MM-DD at the end when a task is toggled to cancelled.", "name": "Set cancelled date on every cancelled task" diff --git a/src/ui/EditInstructions/DateInstructions.ts b/src/ui/EditInstructions/DateInstructions.ts index a11f5f3e11..82aad9390c 100644 --- a/src/ui/EditInstructions/DateInstructions.ts +++ b/src/ui/EditInstructions/DateInstructions.ts @@ -2,6 +2,7 @@ import type { unitOfTime } from 'moment'; import type { AllTaskDateFields } from '../../DateTime/DateFieldTypes'; import { Task } from '../../Task/Task'; import { postponeMenuItemTitleFromDate, removeDateMenuItemTitleForField } from '../../DateTime/Postponer'; +import { momentAdjusted } from '../../DateTime/DateAdjusted'; import { TasksDate } from '../../DateTime/TasksDate'; import type { TaskEditingInstruction } from './TaskEditingInstruction'; import { MenuDividerInstruction } from './MenuDividerInstruction'; @@ -57,7 +58,7 @@ export class SetRelativeTaskDate extends SetTaskDate { amount: number, timeUnit: unitOfTime.DurationConstructor, ) { - const currentDate = taskDueToday[dateFieldToEdit] ?? window.moment(); + const currentDate = taskDueToday[dateFieldToEdit] ?? momentAdjusted(); const title = postponeMenuItemTitleFromDate(dateFieldToEdit, currentDate, amount, timeUnit); const newDate = new TasksDate(window.moment(currentDate)).postpone(timeUnit, amount).toDate(); @@ -132,7 +133,7 @@ export function allLifeCycleDateInstructions(field: AllTaskDateFields, task: Tas * @param factor - +1 means today or future dates; -1 = today or earlier dates. */ function allDateInstructions(task: Task, field: AllTaskDateFields, factor: number) { - const today = window.moment().startOf('day'); + const today = momentAdjusted().startOf('day'); const todayAsDate = today.toDate(); const todayAsTasksDate = new TasksDate(today.clone()); diff --git a/src/ui/EditableTask.ts b/src/ui/EditableTask.ts index 25c60b325b..77c744e5ac 100644 --- a/src/ui/EditableTask.ts +++ b/src/ui/EditableTask.ts @@ -1,4 +1,5 @@ import { GlobalFilter } from '../Config/GlobalFilter'; +import { momentAdjusted } from '../DateTime/DateAdjusted'; import { parseTypedDateForSaving } from '../DateTime/DateTools'; import { PriorityTools } from '../lib/PriorityTools'; import { replaceTaskWithTasks } from '../Obsidian/File'; @@ -252,7 +253,7 @@ export class EditableTask { } // Otherwise, use the current date. - return window.moment(); + return momentAdjusted(); } public parseAndValidateRecurrence() { diff --git a/src/ui/Menus/DatePicker.ts b/src/ui/Menus/DatePicker.ts index b8e974b1a5..98d218b350 100644 --- a/src/ui/Menus/DatePicker.ts +++ b/src/ui/Menus/DatePicker.ts @@ -1,6 +1,7 @@ import flatpickr from 'flatpickr'; import type { Task } from '../../Task/Task'; import { RemoveTaskDate, SetTaskDate } from '../EditInstructions/DateInstructions'; +import { momentAdjusted } from '../../DateTime/DateAdjusted'; import type { AllTaskDateFields } from '../../DateTime/DateFieldTypes'; import type { TaskSaver } from './TaskEditingMenu'; @@ -21,7 +22,7 @@ export function promptForDate( // TODO figure out how Today's date is determined: if Obsidian is left // running overnight, the flatpickr modal shows the previous day as Today. const fp = flatpickr(parentElement, { - defaultDate: currentValue ? currentValue.format('YYYY-MM-DD') : new Date(), + defaultDate: currentValue ? currentValue.format('YYYY-MM-DD') : momentAdjusted().toDate(), disableMobile: true, enableTime: false, // Optional: Enable time picker dateFormat: 'Y-m-d', // Adjust the date and time format as needed @@ -52,7 +53,7 @@ export function promptForDate( // Create "Today" button addButton(buttonContainer, instance, task, taskSaver, 'Today', () => { - const today = new Date(); + const today = momentAdjusted().toDate(); return new SetTaskDate(dateFieldToEdit, today).apply(task); });