diff --git a/HellionChat/Resources/Language.ca.resx b/HellionChat/Resources/Language.ca.resx
index 2eee568..7d47e2f 100644
--- a/HellionChat/Resources/Language.ca.resx
+++ b/HellionChat/Resources/Language.ca.resx
@@ -527,10 +527,10 @@
Finestra emergent
- Hide timestamps when redundant
+ Amaga les marques de temps redundants
- Hide timestamps when previous messages have the same timestamp.
+ Amaga la marca de temps quan el missatge anterior ja en té la mateixa.
Show title bar for popped-out tabs
diff --git a/HellionChat/Resources/Language.it.resx b/HellionChat/Resources/Language.it.resx
index cfff142..5bae064 100644
--- a/HellionChat/Resources/Language.it.resx
+++ b/HellionChat/Resources/Language.it.resx
@@ -527,10 +527,10 @@
Pop out
- Hide timestamps when redundant
+ Nascondi gli orari ridondanti
- Hide timestamps when previous messages have the same timestamp.
+ Nasconde l'orario quando il messaggio precedente ha già lo stesso.
Show title bar for popped-out tabs
diff --git a/HellionChat/Ui/Components/MessageList.cs b/HellionChat/Ui/Components/MessageList.cs
index 8756ab6..f11d73e 100644
--- a/HellionChat/Ui/Components/MessageList.cs
+++ b/HellionChat/Ui/Components/MessageList.cs
@@ -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 _drawCompactRow;
- private readonly Action _drawCardRow;
+ private readonly Action _drawCompactRow;
+ private readonly Action _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 messages,
- Action drawRow,
+ Action 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 messages,
- Action drawRow
+ Action 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);
diff --git a/HellionChat/Ui/Components/RepeatedTimestamp.cs b/HellionChat/Ui/Components/RepeatedTimestamp.cs
new file mode 100644
index 0000000..a9f1a19
--- /dev/null
+++ b/HellionChat/Ui/Components/RepeatedTimestamp.cs
@@ -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);
+}
diff --git a/HellionChat/Ui/Components/Settings/Tabs/ChatTab.cs b/HellionChat/Ui/Components/Settings/Tabs/ChatTab.cs
index 46a47fc..88fa1b2 100644
--- a/HellionChat/Ui/Components/Settings/Tabs/ChatTab.cs
+++ b/HellionChat/Ui/Components/Settings/Tabs/ChatTab.cs
@@ -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