Skip to content

feat(ui): Add option to disable auto mark as read#13168

Open
SoleroTG wants to merge 1 commit into
nextcloud:mainfrom
SoleroTG:feat/disable-auto-read
Open

feat(ui): Add option to disable auto mark as read#13168
SoleroTG wants to merge 1 commit into
nextcloud:mainfrom
SoleroTG:feat/disable-auto-read

Conversation

@SoleroTG

@SoleroTG SoleroTG commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #13061

Summary

This Pull Request introduces a user preference option to fully disable the "auto mark as read" feature when opening a message in Nextcloud Mail (#13061). When disabled, viewing/skimming messages does not automatically change their status to read, making "mark as read" an explicitly manual action.

Key Changes

  1. Settings Switch: Added an option switch labeled "Automatically mark messages as read when opened" in the App Settings Menu under the "Messages" section.
  2. Preference Storage: Registered auto-mark-as-read user preference key, integrated it into the backend PHP initial state payload (PageController), and synced it with the frontend Vue Pinia store state on application bootstrap (init.js).
  3. Seen Timer Check: Modified the timing logic in ThreadEnvelope.vue (fetchMessage()) to retrieve and check the value of auto-mark-as-read (defaulting to true) before starting the 2000ms delay timer that toggles the envelope to seen.

How to Test

  1. Compile front-end changes (npm run dev or npm run watch).
  2. Open Nextcloud Mail.
  3. Open Mail settings -> Messages.
  4. Disable the toggle: "Automatically mark messages as read when opened".
  5. Select/click an unread message in the envelope list.
  6. Verify that:
    • The message loads and displays its body.
    • The message remains unread (no timer is started, no seen flag is sent to the server).
    • Reloading the page persists the disabled toggle and state correctly.
  7. Enable the toggle and verify that opening an unread message automatically marks it as read after the standard 2-second delay.

Checklist

  • Code passes linting (npm run lint and composer run cs:check)
  • Unit tests pass successfully (npm run test:unit)
  • Signed-off commits (DCO check compliant)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

CC: @GretaD (as you previously expressed interest in this feature/discussion)

Summary by CodeRabbit

  • New Features

    • Added a setting to control when messages are automatically marked as read: immediately, after 3 or 30 seconds, or manually.
    • Applied the selected preference when viewing messages.
  • Bug Fixes

    • Added error handling when saving the automatic read-status preference fails.

@ChristophWurst

Copy link
Copy Markdown
Member

Thank you for the contribution!

@ChristophWurst

Copy link
Copy Markdown
Member

@nimishavijay small decision needed. Our friends have a setting for this as well but with a few options

image

(excuse the German UI, you get the idea 🙊)

Should we keep it binary (on vs off) or also offer a few options like off, instant, 3 seconds and 30 seconds?

@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from 093040c to 97bb494 Compare June 26, 2026 07:25
@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from 97bb494 to 1e84dfc Compare July 9, 2026 22:32
@github-actions

Copy link
Copy Markdown

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

@ChristophWurst

Copy link
Copy Markdown
Member

🏓 @nimishavijay

@nimishavijay

Copy link
Copy Markdown
Member

Initially I thought adding a timer would be overengineering but I checked outlook and they also seem to have a timer setting (support link), so I'm guessing it would be a gap if we don't have it. Your timer options also sound great @ChristophWurst so we could go with that :)

@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from 1e84dfc to 61e41e6 Compare July 22, 2026 21:54
@SoleroTG

Copy link
Copy Markdown
Contributor Author

Thanks @ChristophWurst and @nimishavijay! That makes total sense.

I have updated the PR to replace the binary toggle with the proposed radio group options (Immediately, After 3 seconds, After 30 seconds, and Manually). Backward compatibility for existing preference values has also been included.

It looks like this now:

image

Comment thread src/components/AppSettingsMenu.vue Outdated
@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from 61e41e6 to 1606737 Compare July 23, 2026 18:35
Comment thread src/components/AppSettingsMenu.vue Outdated
Comment thread src/components/AppSettingsMenu.vue Outdated
@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from 1606737 to dbe9df4 Compare July 23, 2026 20:34
@ChristophWurst

Copy link
Copy Markdown
Member

Conflicts

@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from dbe9df4 to 86c544d Compare July 24, 2026 07:07
@SoleroTG

Copy link
Copy Markdown
Contributor Author

@ChristophWurst, Rebased onto current main and resolved the conflicts.

@SoleroTG
SoleroTG requested a review from ChristophWurst July 24, 2026 07:09
Comment thread src/components/AppSettingsMenu.vue Outdated
@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from 86c544d to dd07474 Compare July 24, 2026 10:37
Comment thread src/components/AppSettingsMenu.vue Outdated
Comment on lines +462 to +468
if (value === 'true') {
return '3000'
}
if (value === 'false') {
return '-1'
}
return value

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

true/false are unexpected except for your dev env. this doesn't need handling.

Comment thread src/components/ThreadEnvelope.vue Outdated
@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from dd07474 to 1b70b51 Compare July 24, 2026 11:30
@ChristophWurst

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a configurable auto-mark-as-read preference with immediate, delayed, and manual modes. Server initial state and client store hydration carry the preference, settings persist it, and ThreadEnvelope applies the selected behavior when fetching messages.

Changes

Auto-mark-as-read preference

Layer / File(s) Summary
Preference state propagation
lib/Controller/PageController.php, src/init.js, tests/Unit/Controller/PageControllerTest.php
The preference is added to the initial state, stored in mainStore, and covered by updated controller test expectations.
Settings control and persistence
src/components/AppSettingsMenu.vue
Messages settings adds four timing modes and persists selections with loading-state and error handling.
Preference-driven read behavior
src/components/ThreadEnvelope.vue
Message fetching uses the configured delay, marks immediately for 0, and skips automatic marking for negative values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AppSettingsMenu
  participant mainStore
  participant PageController
  participant ThreadEnvelope
  AppSettingsMenu->>mainStore: Save auto-mark-as-read preference
  PageController->>ThreadEnvelope: Provide initial preference state
  ThreadEnvelope->>ThreadEnvelope: Parse configured delay
  ThreadEnvelope->>ThreadEnvelope: Mark immediately or schedule toggleEnvelopeSeen
Loading

Suggested reviewers: gretad, gretad

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding UI support for auto mark as read options, including disabling it.
Linked Issues check ✅ Passed The PR adds a settings control and logic to fully opt out via the Manually option, satisfying #13061's disable requirement.
Out of Scope Changes check ✅ Passed All changes support the auto-mark-as-read feature and its tests; no unrelated scope is evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/Unit/Controller/PageControllerTest.php (1)

191-208: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Update the exact initial-state expectation.

testIndex() now mocks auto-mark-as-read, but its expected preferences array still omits 'auto-mark-as-read' => '3000'. Because provideInitialState() is asserted with the complete array, this test will fail.

Proposed fix
 					'compact-mode' => 'false'
+					'auto-mark-as-read' => '3000'

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 93a5b030-fd7d-481f-9e9e-20c8565d7cc2

📥 Commits

Reviewing files that changed from the base of the PR and between d88dd80 and 1b70b51.

📒 Files selected for processing (5)
  • lib/Controller/PageController.php
  • src/components/AppSettingsMenu.vue
  • src/components/ThreadEnvelope.vue
  • src/init.js
  • tests/Unit/Controller/PageControllerTest.php

Comment thread src/components/ThreadEnvelope.vue Outdated
Comment on lines +856 to +869
const autoMarkReadSetting = this.mainStore.getPreference('auto-mark-as-read', '3000')
const delay = parseInt(autoMarkReadSetting, 10)

if (!this.envelope.flags.seen && this.hasSeenAcl && delay >= 0) {
if (delay === 0) {
logger.info('Marking message as seen/read immediately')
this.mainStore.toggleEnvelopeSeen({ envelope: this.envelope })
this.seenTimer = undefined
}, 2000)
} else {
logger.info(`Starting timer (${delay}ms) to mark message as seen/read`)
this.seenTimer = setTimeout(() => {
this.mainStore.toggleEnvelopeSeen({ envelope: this.envelope })
this.seenTimer = undefined
}, delay)
}

@coderabbitai coderabbitai Bot Jul 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Guard delayed timers before changing read state.

The timeout callback unconditionally toggles the envelope. A manual read action before the timeout can therefore toggle the message back to unread, and switching to “Manually” while the timer is pending still marks it automatically. Re-check the current seen state and preference before toggling, and clear pending timers when the envelope is collapsed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Checking if the setting changes is overkill.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ChristophWurst, agreed—the pending timer does not need to react to preference changes. I’ll narrow this to only avoiding a stale timer toggling a message back to unread after it was manually marked as read.


✏️ Learnings added
Learnt from: ChristophWurst
Repo: nextcloud/mail PR: 13168
File: src/components/ThreadEnvelope.vue:856-869
Timestamp: 2026-07-24T11:53:01.429Z
Learning: For the configurable auto-mark-as-read timer in `src/components/ThreadEnvelope.vue`, do not require pending timers to react to preference changes; that behavior is considered overkill. Review stale-timer behavior independently from settings changes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

But the manual intervention is an interesting case. If that happened we should not toggle. Example:

  1. User set 30s timeout
  2. User opens email, reads it quickly and 10s decides to mark it read
  3. Use navigates to next email

Email will be marked unread 20s later

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ChristophWurst, agreed. The callback must guard against the message already being read before calling the toggle:

this.seenTimer = setTimeout(() => {
	if (!this.envelope.flags.seen) {
		this.mainStore.toggleEnvelopeSeen({ envelope: this.envelope })
	}
	this.seenTimer = undefined
}, delay)

That prevents the pending 30-second timer from turning a manually read message back into unread, without coupling existing timers to later preference changes.


✏️ Learnings added
Learnt from: ChristophWurst
Repo: nextcloud/mail PR: 13168
File: src/components/ThreadEnvelope.vue:856-869
Timestamp: 2026-07-24T11:54:18.413Z
Learning: In `src/components/ThreadEnvelope.vue`, the configurable auto-mark-as-read timeout must not call `mainStore.toggleEnvelopeSeen` when the envelope has already been manually marked as read before the timer expires. Because `toggleEnvelopeSeen` toggles rather than unconditionally marks as read, its timeout callback must guard with `!this.envelope.flags.seen`. Pending timers do not need to react to preference changes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ChristophWurst, Good Point. Added a check if (!this.envelope.flags.seen) inside the timer callback to prevent toggling the message back to unread if it was already manually marked as read before the timer expires.

@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch 2 times, most recently from df63599 to e860e20 Compare July 24, 2026 13:59
@SoleroTG
SoleroTG requested a review from ChristophWurst July 24, 2026 15:48
Comment thread src/components/ThreadEnvelope.vue Outdated
const delay = parseInt(autoMarkReadSetting, 10)

if (!this.envelope.flags.seen && this.hasSeenAcl && delay >= 0) {
if (delay === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we unify the delay branching here and always go through the same setTimeout path? Using a delay of 0 should preserve the behavior while reducing the number of code paths we need to maintain.

That would make the read-timing logic a bit easier to follow and lowers the chance that one branch drifts from the other over time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @kesselb! Unified the branching to always use the setTimeout path for delay >= 0.

@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from e860e20 to 40b0b0d Compare July 24, 2026 15:57
@SoleroTG
SoleroTG requested a review from kesselb July 24, 2026 15:58
Signed-off-by: SoleroTG <github-29h@solero.quietmail.eu>
@SoleroTG
SoleroTG force-pushed the feat/disable-auto-read branch from 40b0b0d to e261143 Compare July 24, 2026 16:43
const delay = parseInt(autoMarkReadSetting, 10)

if (!this.envelope.flags.seen && this.hasSeenAcl && delay >= 0) {
logger.info(`Starting timer (${delay}ms) to mark message as seen/read`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
logger.info(`Starting timer (${delay}ms) to mark message as seen/read`)
logger.debug(`Starting timer (${delay}ms) to mark message as seen/read`)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Option to fully disable "auto mark as read" when opening a message

4 participants