Skip to content

Conversation

allexendar
Copy link

@allexendar allexendar commented Sep 30, 2025

Types of changes

Changes visible to users:

  • New feature (prefix: feat - non-breaking change which adds functionality)
  • Breaking change (prefix: feat!! or fix!! - fix or feature that would cause existing functionality to not work as expected)
    • Issue/discussion:
  • Bug fix (prefix: fix - non-breaking change which fixes an issue)
    • Issue/discussion:
  • Translation (prefix: i18n - additions or improvements to the translations - see Support a new language)
  • Documentation (prefix: docs - improvements to any documentation content for users)
  • Sample vault (prefix: vault - improvements to the Tasks-Demo sample vault)
  • Contributing Guidelines (prefix: contrib - any improvements to documentation content for contributors - see Contributing to Tasks)

Internal changes:

  • Refactor (prefix: refactor - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
  • Tests (prefix: test - additions and improvements to unit tests and the smoke tests)
  • Infrastructure (prefix: chore - examples include GitHub Actions, issue templates)

Description

First draft of "Allow Users to Define Start of Day for Task Completion and Recurrence".

More detail in my comment: #3395 (comment).

Motivation and Context

How has this been tested?

No change-specific tests yet. Only existing yarn test call.

Screenshots (if appropriate)

Checklist

Terms

Copy link
Collaborator

@claremacrae claremacrae left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many thanks for doing this @allexendar - it's a great start.

I've added a few observations from an initial review of the code, and trying out the UI - hoping to start a discussion about what next....

Unfortunately I can't stay up beyond midnight tonight to test it out today.

Comment on lines +45 to +46
momentAdjusted().startOf(unitOfTime).startOf('day'),
momentAdjusted().endOf(unitOfTime).startOf('day'),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintenance-wise, I think this PR is going to be a bit fragile over time.

  • It is repetitious
  • Every person who works on this code in future is going to have to know that they mustn't use window.moment() directly.
  • And/or every reviewer is going to have to remember to look out for window.moment() uses

I started rolling out a TasksDate but unfortunately not managed to make time on fully adopting it to replace all storage and direct access to moment objects.

Too much is in my head only, currently, about what needs to be done to finish adopting it...

I am not totally opposed to the approach in this PR, but personally, I would have imagined that standardising time-and-date storage and access would be a preferable first step, before implementing this feature.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think whatever method we use there'd be potential to not respect user's setting of nextDayStartHour (unless we centralize time source). Personally, I think that just adding a note in "Using Moment as a type in src/" should be enough. That's one of the first entries I read before typing moment anywhere so I think it's prominent enough.

I agree that centralized TasksDate would be ideal but perhaps this is already a turn in that direction? We'd have centralized momentAdjusted (or some other name) procedure which could later just be replaced with TasksDate version.

"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.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please could it expand on what it means by "interacting"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to emphasize "all interactions with obsidian-tasks plugin", i.e. "all responses-to-user-action ...". Meaning that, yes, completing tasks "is treated as if interacting the day before", but also so is using the "td"/"tm" shortcuts, playing with datetime picker, etc.

Comment on lines +345 to +347
.addSlider((slider) => {
const settings = getSettings();
slider.setLimits(0, 23, 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find the slider UI very intuitive, although that is mainly an Obsidian thing, rather than specific to Tasks.

Please have a think about whether the (recently-added) RESTART REQUIRED label will be required on this, for now. Like, will search queries need to be re-triggered if the setting is changed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Adding "RESTART REQUIRED" would be necessary. Queries would only need to retrigger if current local time is between nextDayStartHour_OLD:00 and nextDayStartHour_NEW:00 (or reverse) though.

I don't think anyone will change this setting often, this is a 'set to 4AM and forget' feature. Anki uses 4AM as default and I don't think many users ever change it. Of course, we'd want to keep 0 as default to preserve backwards compatibility.

slider.setDynamicTooltip();
slider.onChange(async (value) => {
updateSettings({ nextDayStartHour: value });
await this.plugin.saveSettings();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What impact will this feature have on the "Reload the vault at midnight" facility in Tasks?

The idea of that feature is that all queries are reloaded a few seconds after midnight, so that things like "due today" are re-interpreted with the new date.

Except that with this feature set to +2, the reload at midnight will have no effect...

And instead presumably the queries should all be reloaded at a few seconds after 2am instead - but for now, users will need to know to do that themselves manually.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the only problem I foresaw. I guess there are two options.

  1. Change now.subtract(1, 'day').endOf('day'); to now.subtract(nextDayStartHour, 'hours');
    This would mean that reloadQueryAtMidnight would run at true midnight (uselessly) and then again nextDayStartHour hours later. Perhaps the best option would be to just do now.subtract(nextDayStartHour, 'hours'); for all calls to this function. If nextDayStartHour = 0, nothing happens. Otherwise, we preserve the continuity of time if any caller is actually interested in that.

  2. Keep now.subtract(1, 'day').endOf('day'); but make reloadQueryAtMidnight aware of nextDayStartHour setting.
    I'd really wanted to evade this and make as few external procedures aware of the setting.

* this function will return a moment object for the end of the *previous* day.
* Otherwise, it returns the current, unaltered moment.
*/
export function momentAdjusted(): Moment {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There really needs to be tests for this, and updated versions of the existing tests for features that have been modified in this PR - to demonstrate how the behaviour has changed.

For example, demonstration of how this feature interacts in time-zones other than GMT... (I haven't yet been able to get my head around that)

)
.addSlider((slider) => {
const settings = getSettings();
slider.setLimits(0, 23, 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this setting relate to the user's time zone, if at all?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't. It should only look at local time value.

export function momentAdjusted(): Moment {
const { nextDayStartHour } = getSettings();
const now = window.moment();
if (now.hour() < nextDayStartHour) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when clocks change? There needs to be tests for this - including in timezones close to the international date line...

I have written up the results of my previous experiments in timezones in tests:

https://publish.obsidian.md/tasks-contributing/Testing/Testing+and+Time+Zones

Added "RESTART REQUIRED" to settings UI. Ran `yarn run lint` on code.
Changed modified moment from `.subtract(1, 'day').endOf('day')` to
`.subtract(nextDayStartHour, 'hours')` to preserve time continuity.
Copy link

@claremacrae
Copy link
Collaborator

I have thought about the changes in this PR a lot more, and having edited in the link to the original issue above, have come to the realisation that this is doing more than the agreed changes in that request...

#3395 is specifically and intentionally only Allow Users to Define Start of Day for Task Completion and Recurrence.

That is a behaviour that is very specific and clear for users to reason about and to document.

This PR is doing many more things that that, making it:

  • hard to reason about, for example in understanding the implication on search behaviour.
    • As one example of many, I am particularly concerned about happens today not always honouring today's date
    • There will also be confusion because any filter/group/sort by function instructions that use moment() will give different results from the built-in ones, where users have modified this are setting
  • hard to test manually
  • hard to write automated tests for

@allexendar
Copy link
Author

I agree that this commit is doing more than #3395 asks for.

I disagree that it is less clear.
If you separate out the happens today and task completion start of day then you deliberately need to track what exactly gets changed as clock turns midnight ("I am looking at tomorrow's tasks but if I click done the date will say some other date that I can't see in my system clock … which can cause the task to disappear from my list based on my filters …").
It is simpler to just define "Start of Day" and have all behaviors that depend on given day initiate when given day starts.

This design already exists in other software, e.g., Anki:

  • Review a card at 3AM (with 4AM cutoff) ⇒ it's counted as reviewed yesterday
  • Add a card at 1AM (with 4AM cutoff) ⇒ it's counted as added yesterday

Why would testing be hard?

  • Add task at 1AM local time on 2025-10-02 with nextDayStartHour=2 ⇒ added date should say 2025-10-01.
  • Add task at 3AM local time on 2025-10-02 with nextDayStartHour=2 ⇒ added date should say 2025-10-02.
  • Query is run at 5AM local…
  • All existing tests at nextDayStartHour=0 should pass as they always did…

etc?

confusion because any filter/group/sort by function instructions that use moment() will give different results from the built-in ones

By default we'd have nextDayStartHour=0 — so nothing changes. If users deliberately go into settings, read the docs and change the feature, is it not reasonable to expect them to also edit their time manipulation code? They know what the code does after all so the fact that nothing changed shouldn't be surprising to them?


Thanks for the very rapid and considerate responses. As a maintainer, you're probably more attuned to long-term impacts than me. I'm fine with us thinking about this for the next few days and then closing if we conclude this approach is unsound. I'll be testing the changes on my local vault.

@claremacrae
Copy link
Collaborator

I am not saying that we would never change the search facility.

I am trying to say that based on years of experience, I strongly prefer small, focussed PRs that do one thing that is releasable.

Once the facility described in the feature request is done, there can be further experimentation and discussion about how to change search.

Why would testing be hard?

In case you haven’t seen it, I’ve written up a fair amount about writing of automated tests in this repo.

https://publish.obsidian.md/tasks-contributing/Testing/About+Testing

Every changed line of code here would need at least one new test that would fail if that changed line were reverted.

@claremacrae
Copy link
Collaborator

One other observation - based on my knowledge of the code.

More big advantages of just doing what is requested in the Feature Request:

  • this feature would no longer require a vault reload when the time setting is changed
  • and therefore also the “what to do at midnight” question disappears for this PR too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants