When clicking on a thread with unread mails the message view pane is populated with the thread contents (the mails) as expected. When the timer kicks in to auto-mark the mails as read the message view pane is redrawn completely, causing a short flicker to nothing being displayed & then back to the same contents that were visible just 0.5 seconds ago.
This is especially bad when the message is an HTML message that isn't styled in my preferred dark theme:
initial flash to white background
start reading mails, eyes adjust to brightness
auto-mark read timer kicks in, marks thread as read
message view pane is emptied, flashing to dark background
eyes immediately start to relax due to the low brightness
not even 1s later the message contents pane are displayed again, flashing to white again
Pretty annoying, and especially noteworthy if you set the auto-marking to "immediately".
I'd prefer that message view pane update not to happen at all. Only the message list pane should be updated to reflect the thread's new "read" status.
Thanks.
This is with ihasmail at cf14ff8f73f9ff27c98add5d8e3e4ad78d7b02ec
Rebuilt from: notification email, session transcript. Description text is incomplete. Opening date is approximate.
When clicking on a thread with unread mails the message view pane is populated with the thread contents (the mails) as expected. When the timer kicks in to auto-mark the mails as read the message view pane is redrawn completely, causing a short flicker to _nothing_ being displayed & then back to the same contents that were visible just 0.5 seconds ago.
This is especially bad when the message is an HTML message that isn't styled in my preferred dark theme:
- initial flash to white background
- start reading mails, eyes adjust to brightness
- auto-mark read timer kicks in, marks thread as read
- message view pane is emptied, flashing to dark background
- eyes immediately start to relax due to the low brightness
- not even 1s later the message contents pane are displayed again, flashing to white _again_
Pretty annoying, and especially noteworthy if you set the auto-marking to "immediately".
I'd prefer that message view pane update not to happen at all. Only the message _list_ pane should be updated to reflect the thread's new "read" status.
Thanks.
This is with ihasmail at cf14ff8f73f9ff27c98add5d8e3e4ad78d7b02ec
<sub>Rebuilt from: notification email, session transcript. Description text is incomplete. Opening date is approximate.</sub>
Howdy, I'll jump on this shortly. Doing groupware/collab enhancements today once I get to a stopping point I can take a look at this.
Date not recovered.
Howdy, I'll jump on this shortly. Doing groupware/collab enhancements today once I get to a stopping point I can take a look at this.
<sub>Date not recovered.</sub>
Fixed in #104, though not quite the way you asked — worth explaining the difference, because it is deliberate.
What was happening. The pane was not re-mounting. The message body was being thrown away and rebuilt, and it came down to one dependency. HtmlBody writes the message into a shadow root inside an effect, and that effect had the click handler in its dependency list. The handler is a useCallback over an onShowImages prop that the parent was passing as an arrow created inline — so it was a new function on every render, so the effect ran on every render, and every render replaced the rendered message with an identical one. Marking as read is exactly such a render: the store hands back a new email object and the thread re-renders around it.
That is why it flashed to nothing and back, and why an HTML message that brings its own colours flashed white → dark → white.
What changed. The listener moved into its own effect, attached to the shadow root rather than to its contents — the root survives the rewriting anyway. A handler that changes identity now costs a listener swap and nothing else. The onShowImages prop is stable now too, but the split is the actual fix: it is what keeps the body safe from the next handler that changes.
Measured rather than eyeballed, since a flicker is the sort of thing your eyes will agree with you about. Holding a node from inside the shadow root across the transition, three-message thread, auto-mark set to immediately: before, 21 childList mutations on the root and the held node detached and replaced; after, no mutations at all and the same node still attached.
Where this differs from your request. You asked that the message view pane not update at all, and only the list reflect the new read status. I did not do that. The pane still re-renders — it just no longer rebuilds the body while doing so.
The reason is #69. The unread markers you see beside each message are drawn from a set captured when the thread was opened, precisely so the mark-read timer cannot take them away mid-read. That set, and anything else in the pane that tracks live state, needs the render to keep working. Freezing the pane against updates would fix the flicker by reintroducing a version of the problem #69 was opened for.
So the render stays and the expensive part of it goes. The visible result should be what you were after: nothing moves, nothing flashes, and the list updates.
One thing you did not report that the same change fixes — expanding quoted text and then having it collapse when the timer fired. setQuoteOpen(false) lived in that same effect and was resetting on every render.
Fixed in #104, though not quite the way you asked — worth explaining the difference, because it is deliberate.
**What was happening.** The pane was not re-mounting. The message *body* was being thrown away and rebuilt, and it came down to one dependency. `HtmlBody` writes the message into a shadow root inside an effect, and that effect had the click handler in its dependency list. The handler is a `useCallback` over an `onShowImages` prop that the parent was passing as an arrow created inline — so it was a new function on every render, so the effect ran on every render, and every render replaced the rendered message with an identical one. Marking as read is exactly such a render: the store hands back a new email object and the thread re-renders around it.
That is why it flashed to nothing and back, and why an HTML message that brings its own colours flashed white → dark → white.
**What changed.** The listener moved into its own effect, attached to the shadow root rather than to its contents — the root survives the rewriting anyway. A handler that changes identity now costs a listener swap and nothing else. The `onShowImages` prop is stable now too, but the split is the actual fix: it is what keeps the body safe from the next handler that changes.
Measured rather than eyeballed, since a flicker is the sort of thing your eyes will agree with you about. Holding a node from inside the shadow root across the transition, three-message thread, auto-mark set to immediately: before, 21 childList mutations on the root and the held node detached and replaced; after, no mutations at all and the same node still attached.
**Where this differs from your request.** You asked that the message view pane not update at all, and only the list reflect the new read status. I did not do that. The pane still re-renders — it just no longer rebuilds the body while doing so.
The reason is #69. The unread markers you see beside each message are drawn from a set captured when the thread was opened, precisely so the mark-read timer cannot take them away mid-read. That set, and anything else in the pane that tracks live state, needs the render to keep working. Freezing the pane against updates would fix the flicker by reintroducing a version of the problem #69 was opened for.
So the render stays and the expensive part of it goes. The visible result should be what you were after: nothing moves, nothing flashes, and the list updates.
One thing you did not report that the same change fixes — expanding quoted text and then having it collapse when the timer fired. `setQuoteOpen(false)` lived in that same effect and was resetting on every render.
Following up, because my last comment described the first fix as the whole answer and it was not. There were two causes; you were seeing both.
The one I described.HtmlBody writes the message into a shadow root inside an effect, and that effect had the click handler in its dependencies. The handler was a useCallback over a prop the parent passed as an inline arrow, so it was a new function on every render, so the effect ran on every render and rebuilt the rendered message each time. Fixed in #104: the listener moved to its own effect on the shadow root, which survives the rewriting anyway.
That removed a real part of it — 21 childList mutations on the root per mark-read, down to none — but you were right that it was not all of it.
The one I missed. The pane was also emptying and refilling, and that is a different bug in a different file. When the server reports an email as updated, the store was dropping its cached full copy so the next read would fetch it again. The reading pane renders only the emails it holds in full, so dropping one took the message out of the open thread until the refetch put it back. Marking as read triggers exactly that, because the server echoes our own change back as an update — and the gap is a network round trip, which is why it is obvious against a real server.
Fixed in #106 by keeping the copy. Nothing is lost: RFC 8621 makes every property of an Email immutable except keywords and mailboxIds, since the id is derived from the content, and both of those are already merged over the cached copy by the refresh that follows.
Both are deployed. The reporter of the follow-up confirms the pane now sits still, including with auto-mark set to immediately, which is where it was worst.
Why the first attempt only got half of it, since it is the more useful part of this. The mock server this is developed against never ran the second code path at all: Email/set announced no state change, and Email/changes returned empty arrays whatever had happened. So every check ever made against it was against a server that never reported the change being made — the half of the bug that lives in the reconciliation could not be reproduced, and I "verified" a partial fix against a mock that agreed with me. That is fixed separately in #105, so the next person to touch this can reproduce it rather than reason about it.
One thing you did not report that went with it: expanding quoted text and then having it collapse when the timer fired. setQuoteOpen(false) lived in the rebuilt effect.
Thanks for pushing back on the first fix rather than taking it.
Following up, because my last comment described the first fix as the whole answer and it was not. There were two causes; you were seeing both.
**The one I described.** `HtmlBody` writes the message into a shadow root inside an effect, and that effect had the click handler in its dependencies. The handler was a `useCallback` over a prop the parent passed as an inline arrow, so it was a new function on every render, so the effect ran on every render and rebuilt the rendered message each time. Fixed in #104: the listener moved to its own effect on the shadow root, which survives the rewriting anyway.
That removed a real part of it — 21 childList mutations on the root per mark-read, down to none — but you were right that it was not all of it.
**The one I missed.** The pane was also emptying and refilling, and that is a different bug in a different file. When the server reports an email as updated, the store was dropping its cached full copy so the next read would fetch it again. The reading pane renders only the emails it holds in full, so dropping one took the message out of the open thread until the refetch put it back. Marking as read triggers exactly that, because the server echoes our own change back as an update — and the gap is a network round trip, which is why it is obvious against a real server.
Fixed in #106 by keeping the copy. Nothing is lost: RFC 8621 makes every property of an Email immutable except `keywords` and `mailboxIds`, since the id is derived from the content, and both of those are already merged over the cached copy by the refresh that follows.
Both are deployed. The reporter of the follow-up confirms the pane now sits still, including with auto-mark set to immediately, which is where it was worst.
**Why the first attempt only got half of it**, since it is the more useful part of this. The mock server this is developed against never ran the second code path at all: `Email/set` announced no state change, and `Email/changes` returned empty arrays whatever had happened. So every check ever made against it was against a server that never reported the change being made — the half of the bug that lives in the reconciliation could not be reproduced, and I "verified" a partial fix against a mock that agreed with me. That is fixed separately in #105, so the next person to touch this can reproduce it rather than reason about it.
One thing you did not report that went with it: expanding quoted text and then having it collapse when the timer fired. `setQuoteOpen(false)` lived in the rebuilt effect.
Thanks for pushing back on the first fix rather than taking it.
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
When clicking on a thread with unread mails the message view pane is populated with the thread contents (the mails) as expected. When the timer kicks in to auto-mark the mails as read the message view pane is redrawn completely, causing a short flicker to nothing being displayed & then back to the same contents that were visible just 0.5 seconds ago.
This is especially bad when the message is an HTML message that isn't styled in my preferred dark theme:
Pretty annoying, and especially noteworthy if you set the auto-marking to "immediately".
I'd prefer that message view pane update not to happen at all. Only the message list pane should be updated to reflect the thread's new "read" status.
Thanks.
This is with ihasmail at cf14ff8f73f9ff27c98add5d8e3e4ad78d7b02ec
Rebuilt from: notification email, session transcript. Description text is incomplete. Opening date is approximate.
Howdy, I'll jump on this shortly. Doing groupware/collab enhancements today once I get to a stopping point I can take a look at this.
Date not recovered.
No worries, mate. Appreciate all the improvements, not just the ones I ask for 😁
Fixed in #104, though not quite the way you asked — worth explaining the difference, because it is deliberate.
What was happening. The pane was not re-mounting. The message body was being thrown away and rebuilt, and it came down to one dependency.
HtmlBodywrites the message into a shadow root inside an effect, and that effect had the click handler in its dependency list. The handler is auseCallbackover anonShowImagesprop that the parent was passing as an arrow created inline — so it was a new function on every render, so the effect ran on every render, and every render replaced the rendered message with an identical one. Marking as read is exactly such a render: the store hands back a new email object and the thread re-renders around it.That is why it flashed to nothing and back, and why an HTML message that brings its own colours flashed white → dark → white.
What changed. The listener moved into its own effect, attached to the shadow root rather than to its contents — the root survives the rewriting anyway. A handler that changes identity now costs a listener swap and nothing else. The
onShowImagesprop is stable now too, but the split is the actual fix: it is what keeps the body safe from the next handler that changes.Measured rather than eyeballed, since a flicker is the sort of thing your eyes will agree with you about. Holding a node from inside the shadow root across the transition, three-message thread, auto-mark set to immediately: before, 21 childList mutations on the root and the held node detached and replaced; after, no mutations at all and the same node still attached.
Where this differs from your request. You asked that the message view pane not update at all, and only the list reflect the new read status. I did not do that. The pane still re-renders — it just no longer rebuilds the body while doing so.
The reason is #69. The unread markers you see beside each message are drawn from a set captured when the thread was opened, precisely so the mark-read timer cannot take them away mid-read. That set, and anything else in the pane that tracks live state, needs the render to keep working. Freezing the pane against updates would fix the flicker by reintroducing a version of the problem #69 was opened for.
So the render stays and the expensive part of it goes. The visible result should be what you were after: nothing moves, nothing flashes, and the list updates.
One thing you did not report that the same change fixes — expanding quoted text and then having it collapse when the timer fired.
setQuoteOpen(false)lived in that same effect and was resetting on every render.Following up, because my last comment described the first fix as the whole answer and it was not. There were two causes; you were seeing both.
The one I described.
HtmlBodywrites the message into a shadow root inside an effect, and that effect had the click handler in its dependencies. The handler was auseCallbackover a prop the parent passed as an inline arrow, so it was a new function on every render, so the effect ran on every render and rebuilt the rendered message each time. Fixed in #104: the listener moved to its own effect on the shadow root, which survives the rewriting anyway.That removed a real part of it — 21 childList mutations on the root per mark-read, down to none — but you were right that it was not all of it.
The one I missed. The pane was also emptying and refilling, and that is a different bug in a different file. When the server reports an email as updated, the store was dropping its cached full copy so the next read would fetch it again. The reading pane renders only the emails it holds in full, so dropping one took the message out of the open thread until the refetch put it back. Marking as read triggers exactly that, because the server echoes our own change back as an update — and the gap is a network round trip, which is why it is obvious against a real server.
Fixed in #106 by keeping the copy. Nothing is lost: RFC 8621 makes every property of an Email immutable except
keywordsandmailboxIds, since the id is derived from the content, and both of those are already merged over the cached copy by the refresh that follows.Both are deployed. The reporter of the follow-up confirms the pane now sits still, including with auto-mark set to immediately, which is where it was worst.
Why the first attempt only got half of it, since it is the more useful part of this. The mock server this is developed against never ran the second code path at all:
Email/setannounced no state change, andEmail/changesreturned empty arrays whatever had happened. So every check ever made against it was against a server that never reported the change being made — the half of the bug that lives in the reconciliation could not be reproduced, and I "verified" a partial fix against a mock that agreed with me. That is fixed separately in #105, so the next person to touch this can reproduce it rather than reason about it.One thing you did not report that went with it: expanding quoted text and then having it collapse when the timer fired.
setQuoteOpen(false)lived in the rebuilt effect.Thanks for pushing back on the first fix rather than taking it.