feat(chat): stop repeating the same minute on every line

The comparison value comes from the message data, not from a variable carried
between rows. In 1.5.6 the loop walked every message and skipped invisible ones
with a dummy, so what it remembered was the last *visible* stamp. The virtualised
list only iterates the visible window, so the row above that window was never
drawn at all -- a carried variable would hold whatever was on screen before the
last scroll, and the first stamp after every jump would be wrong.

No predecessor means draw. Scrolling into the middle of a log would otherwise
swallow the only stamp on screen.

Both draw paths now pass an index; the linear one was a foreach and had none.

The setting is on by default and existed with translations in twenty-three
languages -- Catalan and Italian had kept the English string, so those two are
done now. It needs no fingerprint entry: the column stays reserved when the stamp
is suppressed, so hiding one changes no row's height.
This commit is contained in:
2026-08-19 12:07:39 +02:00
parent b63e1eda9b
commit 80ec7450c8
5 changed files with 54 additions and 19 deletions
+2 -2
View File
@@ -527,10 +527,10 @@
<value>Finestra emergent</value>
</data>
<data name="Options_HideSameTimestamps_Name">
<value>Hide timestamps when redundant</value>
<value>Amaga les marques de temps redundants</value>
</data>
<data name="Options_HideSameTimestamps_Description">
<value>Hide timestamps when previous messages have the same timestamp.</value>
<value>Amaga la marca de temps quan el missatge anterior ja en té la mateixa.</value>
</data>
<data name="Options_ShowPopOutTitleBar_Name">
<value>Show title bar for popped-out tabs</value>
+2 -2
View File
@@ -527,10 +527,10 @@
<value>Pop out</value>
</data>
<data name="Options_HideSameTimestamps_Name">
<value>Hide timestamps when redundant</value>
<value>Nascondi gli orari ridondanti</value>
</data>
<data name="Options_HideSameTimestamps_Description">
<value>Hide timestamps when previous messages have the same timestamp.</value>
<value>Nasconde l'orario quando il messaggio precedente ha già lo stesso.</value>
</data>
<data name="Options_ShowPopOutTitleBar_Name">
<value>Show title bar for popped-out tabs</value>
+23 -15
View File
@@ -34,8 +34,8 @@ internal sealed class MessageList
// Bound once. A method group off an instance method captures `this` and is
// not cached by Roslyn, so `compact ? DrawCompactRow : DrawCardRow` would
// allocate a delegate on every frame of every window.
private readonly Action<Message> _drawCompactRow;
private readonly Action<Message> _drawCardRow;
private readonly Action<Message, string?> _drawCompactRow;
private readonly Action<Message, string?> _drawCardRow;
// Reused across frames: at MessageManager.MessageDisplayLimit a fresh array
// per frame is 40 KB of garbage, and A2 put the default density on this
@@ -261,15 +261,20 @@ internal sealed class MessageList
// Draws the stamp into its column and leaves the cursor at the text column,
// whether or not anything was drawn.
private void DrawTimestampCell(Message message)
private void DrawTimestampCell(Message message, string? previousStamp)
{
var origin = ImGui.GetCursorPos();
if (_stampVisible)
var stamp = FormatTimestamp(message.Date);
var draw =
_stampVisible
&& RepeatedTimestamp.ShouldDraw(Plugin.Config.HideSameTimestamps, stamp, previousStamp);
if (draw)
{
ImGui.SetCursorPosY(origin.Y + _metaDrop);
using (MetaFace().Push())
ImGui.TextUnformatted(FormatTimestamp(message.Date));
ImGui.TextUnformatted(stamp);
ImGui.SameLine(0f, 0f);
}
@@ -328,7 +333,7 @@ internal sealed class MessageList
_scrollToBottomRequested = true;
}
private void DrawCompactRow(Message message)
private void DrawCompactRow(Message message, string? previousStamp)
{
// B2-1/B2-2: render the sender through DrawChunks (the name-aware path
// that applies WorldSuffixMode/NameFormMode via ForDisplay), not as a
@@ -336,7 +341,7 @@ internal sealed class MessageList
// channel brackets/colon as ChunkSource.None wrappers (MessageManager
// .cs:300-314), so the separator is rendered by the chunks. 1.5.6 parity
// (ChatLogWindow.cs:1965: DrawChunks(message.Sender) + SameLine).
DrawTimestampCell(message);
DrawTimestampCell(message, previousStamp);
if (message.Sender.Count == 0)
{
@@ -365,7 +370,7 @@ internal sealed class MessageList
private void DrawRows(
Tab tab,
IReadOnlyList<Message> messages,
Action<Message> drawRow,
Action<Message, string?> drawRow,
bool frozen
)
{
@@ -438,7 +443,9 @@ internal sealed class MessageList
// the text instead of needing a draw-channel detour.
DrawRowSurface(msg, heights[i]);
drawRow(msg);
// From the data, not from a variable carried between rows: this loop
// starts at FirstVisible, so the row above the window was never drawn.
drawRow(msg, i > 0 ? FormatTimestamp(messages[i - 1].Date) : null);
if (frozen)
continue;
@@ -531,7 +538,7 @@ internal sealed class MessageList
private void DrawLinearAndMeasure(
Guid tabId,
IReadOnlyList<Message> messages,
Action<Message> drawRow
Action<Message, string?> drawRow
)
{
// No row has a cached height on this frame, so the surface cannot be
@@ -540,12 +547,13 @@ internal sealed class MessageList
// frame after every resize.
using var surfaces = StyleEngine.RowSurfaceScope.Push();
foreach (var msg in messages)
for (var i = 0; i < messages.Count; i++)
{
var msg = messages[i];
var before = ImGui.GetCursorPosY();
var top = ImGui.GetCursorScreenPos();
drawRow(msg);
drawRow(msg, i > 0 ? FormatTimestamp(messages[i - 1].Date) : null);
var after = ImGui.GetCursorPosY();
var height = after - before;
@@ -556,7 +564,7 @@ internal sealed class MessageList
}
}
private void DrawCardRow(Message message)
private void DrawCardRow(Message message, string? previousStamp)
{
// B2-1/B2-2: sender via DrawChunks (name-aware path), on its own line
// with content below — 1.5.6 card parity (ChatLogWindow.cs:1913, no
@@ -568,7 +576,7 @@ internal sealed class MessageList
// densities; only a message with a sender gets the two-line treatment.
if (message.Sender.Count == 0)
{
DrawTimestampCell(message);
DrawTimestampCell(message, previousStamp);
using (ItalicFace().Push())
_chunkRenderer.DrawChunks(
message.Content,
@@ -580,7 +588,7 @@ internal sealed class MessageList
return;
}
DrawTimestampCell(message);
DrawTimestampCell(message, previousStamp);
using (SenderFace().Push())
_chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f);
@@ -0,0 +1,20 @@
namespace HellionChat.Ui.Components;
// TEST-MIRROR: Ui/RepeatedTimestampTests.cs
//
// Whether a row draws its timestamp, given the one above it.
//
// The comparison value has to come from the message data, not from a drawing
// state carried between rows. In 1.5.6 the loop walked every message and skipped
// the invisible ones with a dummy, so the "last stamp" it remembered was the last
// visible one. The virtualised list only iterates the visible window, so a row at
// the top of that window has a predecessor in the data that was never drawn --
// and a state variable would hold whatever was on screen before the scroll.
//
// No predecessor means draw. Scrolling into the middle of a log would otherwise
// swallow the only stamp on screen.
internal static class RepeatedTimestamp
{
internal static bool ShouldDraw(bool enabled, string current, string? previous) =>
!enabled || previous is null || !string.Equals(current, previous, StringComparison.Ordinal);
}
@@ -39,6 +39,13 @@ internal sealed class ChatTab
() => Plugin.Config.Use24HourClock,
v => Plugin.Config.Use24HourClock = v
);
_w.ToggleRow(
ImGui.GetID("chat.display.samestamps"u8),
Language.Options_HideSameTimestamps_Name,
Language.Options_HideSameTimestamps_Description,
() => Plugin.Config.HideSameTimestamps,
v => Plugin.Config.HideSameTimestamps = v
);
// Descriptions move out of the help markers and onto the row. They
// were written to be read; a (?) the user has to hover is where an