feat(desktop): reveal contextual message timestamps on hover - #3681
feat(desktop): reveal contextual message timestamps on hover#3681Sun-GLiang wants to merge 6 commits into
Conversation
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
b7019e3 to
22dacd3
Compare
|
Could a maintainer please trigger CI for this PR? Thanks! |
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed at 22dacd3cc2. One [P2], one [P3], plus a gate note.
The shape of this is good: the date-precision policy lives in Core as a pure function, the reveal is CSS-only with opacity so the transcript never reflows, :focus-within is there alongside :hover so keyboard users are not excluded, and the visible run is aria-hidden with one complete absolute label exposed to assistive technology instead of letting a screen reader read a bare "23:20". The midnight refresh is real rather than a polling interval.
[P2] The one visible string is assembled by two formatters that resolve different locales
presentConversationMessageTimestamp formats with uiLocaleToIntlLocale(locale) — the app's UI locale, zh-CN or en. It then computes datePrefix by slicing the formatted parts up to (but not including) the hour part, on the assumption that the clock will continue exactly where the prefix stopped.
The clock does not come from that formatter. ConversationMessageTimestamp renders <Timestamp format="time"> from @astryxdesign/core, and formatInstant calls new Intl.DateTimeFormat(undefined, TIME_OPTIONS) — undefined resolves to the host locale, not the app's.
So the two halves disagree whenever the UI locale differs from the OS locale, which is precisely the case the UI locale setting exists to serve. With the app set to Chinese on an en-US machine, a message from earlier this year renders as 8月24日 + 11:20 PM. The absoluteLabel read to assistive technology is built from the app locale, so the screen reader and the screen also disagree.
There is a quieter failure in the same seam. prefixBeforeClock returns '' when no hour part is found, and slices strictly before the hour otherwise. In any locale ordering that does not place the date before the clock, the date silently disappears from the visible label — the user sees only a time, with no indication that the message is from another day or another year. The hidden label still carries the full date, so nothing looks broken; the information is simply gone from the screen.
Please format the whole visible string from one formatter. Either render presentation.fallbackText directly instead of composing a prefix with a separately formatted clock, or pass the resolved app locale into the Astryx component if it accepts one. As written, the prefix and the clock are only guaranteed to agree on machines whose OS locale happens to match the app's.
[P3] One timer per message
ConversationMessageTimestamp schedules its own setTimeout to the next local midnight, and the effect depends on now, so every mounted message holds a timer and they all fire together at midnight. On a long transcript that is a few thousand timers to re-render every message at the same instant, for a change that affects only messages crossing a day boundary. A single shared midnight tick — a context or a module-level subscription — would give the same behaviour at one timer.
Gate: no check has run on this head at all. Never-run is not green, so this cannot be approved yet regardless of the findings.
简体中文
已在 22dacd3cc2 上审查。一条 [P2]、一条 [P3],外加一条门禁说明。
整体形状是好的:日期精度策略作为纯函数放在 Core;显隐纯靠 CSS opacity,所以 transcript 不会重排;:focus-within 与 :hover 并列,键盘用户没有被排除在外;可见部分标了 aria-hidden,另外向辅助技术暴露一个完整的绝对时间,而不是让读屏软件念一句光秃秃的"23:20"。跨午夜刷新是真的定时,而不是轮询。
[P2] 同一个可见字符串,是由两个解析出不同 locale 的格式化器拼出来的
presentConversationMessageTimestamp 用 uiLocaleToIntlLocale(locale) 格式化——也就是应用的 UI locale,zh-CN 或 en。然后它把格式化后的 parts 切到 hour 之前得到 datePrefix,前提假设是:时钟会恰好从前缀结束的地方接上。
但时钟并不来自那个格式化器。ConversationMessageTimestamp 渲染的是 @astryxdesign/core 的 <Timestamp format="time">,而 formatInstant 调用的是 new Intl.DateTimeFormat(undefined, TIME_OPTIONS)——undefined 解析到的是宿主locale,不是应用的。
于是只要 UI locale 与操作系统 locale 不一致,两半就会打架——而这恰恰正是 UI locale 这个设置存在的意义。应用设为中文、机器是 en-US 时,今年早些时候的一条消息会渲染成 8月24日 + 11:20 PM。而念给辅助技术的 absoluteLabel 用的是应用locale,因此读屏与屏幕显示同样对不上。
同一处接缝还有一个更安静的失败。prefixBeforeClock 在找不到 hour part 时返回 '',否则严格切到 hour 之前。在任何"时钟不在日期之后"的 locale 排序下,日期会从可见标签里悄悄消失——用户只看到一个时间,完全看不出这条消息来自另一天甚至另一年。隐藏标签里仍带着完整日期,所以表面上没有任何异常;信息只是从屏幕上没了。
请让整个可见字符串出自同一个格式化器:要么直接渲染 presentation.fallbackText,不要用"前缀 + 另行格式化的时钟"拼;要么把解析后的应用 locale 传进 Astryx 组件(如果它接受)。按现在的写法,前缀与时钟只有在"操作系统 locale 恰好等于应用 locale"的机器上才保证一致。
[P3] 每条消息一个定时器
ConversationMessageTimestamp 各自 setTimeout 到下一个本地午夜,且该 effect 依赖 now,因此每条已挂载的消息都持有一个定时器,并且它们会在午夜同时触发。长 transcript 上就是几千个定时器在同一瞬间把每条消息重渲染一遍,而真正受影响的只有跨天边界的那几条。用一个共享的午夜 tick(context 或模块级订阅)能以一个定时器得到同样的行为。
门禁:这个 head 上一条 check 都没跑过。「没跑过」不等于绿,所以无论上述发现如何,现在都不具备 approve 条件。
Use one UI-locale formatter for the complete visible timestamp and share one midnight refresh across mounted messages. Generated-by: Codex
|
Addressed the review in
Local verification on this head: Core 657/657, UI 233/233, timestamp Electron E2E 1/1, lint 2,677 files, format 1,617 files, full build, all-workspace typecheck, and The PR still reports no checks because the fork workflows have not been approved; that gate still requires a maintainer. @Astro-Han, please re-review the updated head when available. |
Generated-by: Codex
Regenerate the Astryx surface inventory after combining the latest UI surface changes with contextual message timestamps. Generated-by: Codex
Summary
Closes #3678
Visual evidence
Before — the time is permanently visible:
After, at rest — the timestamp and actions are visually hidden without moving the message:
After, on hover — the same-day shape contains only the time:
Historical messages remain quiet at rest:
When revealed, yesterday and five-days-ago messages include month, day, and time:
These screenshots are captured from the real Desktop Electron renderer; they are not generative images.
Verification
npm --workspace @maka/core test— passednpm --workspace @maka/ui test— 233 passed, 0 failednpm --workspace @maka/desktop run e2e -- e2e/message-timestamp-visibility.spec.ts— 1 passed, covering hover, focus, same-day precision, and stable geometrynpm run lint— 2,677 files passednpm run format:check— 1,617 files passednpm run build— passednpm run typecheck— passed across all workspacesnpx knip --workspace apps/desktopandnpx knip --workspace packages/ui— passedgit diff --check upstream/main...HEAD— passednpm testpassed every workspace except@maka/runtime; Desktop passed 1,368/1,368. The remaining failure is the existing macOS executable-root assertioncanonicalizes macOS Bash cwd and exposes the runtime executable roots, which expects-DEXECUTABLE_ROOT_1=/usr/localunder this machine's/usr/local/bin/node. The isolated test reproduces it, and this branch has no diff underpackages/runtime.The new Core, UI, style-contract, and Electron tests were developed red-first and fail without the timestamp presenter, adapter, and reveal rules introduced here.
AI use
Select exactly one:
Tool(s) and scope: OpenAI Codex helped refine the implementation plan; implemented the reusable timestamp presenter, UI adapter, interaction styling, and red-to-green tests; captured the Electron evidence; ran verification; and performed a preliminary AI code review. That review does not replace the repository's required independent human review. Every affected commit carries a
Generated-by: Codextrailer. The human contributor remains responsible for the change, evidence, and submission.Checklist
Does this PR entail a change in behavior?