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