fix(privacy): the header gave away what the log was hiding

Screenshot mode anonymises sender names in the message list. The channel header
I added yesterday sat above that list and showed two things it should not.

The home world, on the right. It narrows a player down almost as far as the
character name does, and the mode exists so a picture can be shared.

Worse, the tab name on the left. AutoTellTabsService builds a tell tab's name as
"Player@World", so a tell conversation had the partner's name and world set in
tracked caps directly above a log where every message had been anonymised. The
one place a reader looks first was the one place still naming them.

The name is suppressed only where it actually names someone -- a tab with a tell
target set. General or Trade stay readable, because they identify nobody, and a
self-named tab is the user's own text.

Found by asking what the new surface shows rather than by a test failing. Nothing
here was failing.
This commit is contained in:
2026-08-19 11:42:13 +02:00
parent e810d2479d
commit 56a9f3f474
3 changed files with 27 additions and 7 deletions
@@ -68,7 +68,13 @@ internal static class ChannelHeader
var icon = Components.Sidebar.ResolveTabIcon(tab);
var track = TrackRaw * scale;
var detailTrack = DetailTrackRaw * scale;
var showName = mode is ChannelHeaderMode.Full;
// Screenshot mode hides the name of a tell tab, because that name IS the
// conversation partner: AutoTellTabsService builds it as "Player@World".
// Drawing it in tracked caps above a log whose messages are anonymised
// would give away in the header exactly what the log is hiding.
var namesAPartner = tab.TellTarget?.IsSet() == true;
var showName =
mode is ChannelHeaderMode.Full && !(Plugin.Config.ScreenshotMode && namesAPartner);
// ToUpperInvariant allocates, so only where the name is actually drawn.
var name = showName ? tab.Name.ToUpperInvariant() : string.Empty;
@@ -92,7 +98,12 @@ internal static class ChannelHeader
using ((detail.WhereIsTranslated ? body : meta).Push())
whereRun = DrawListExtensions.MeasureTrackedText(detail.Where, detailTrack);
var rest = ChannelHeaderDetailParts.Separator + detail.Clock;
// No separator with nothing in front of it -- screenshot mode leaves the
// clock standing alone.
var rest =
detail.Where.Length == 0
? detail.Clock
: ChannelHeaderDetailParts.Separator + detail.Clock;
float restRun;
using (meta.Push())
@@ -102,7 +113,7 @@ internal static class ChannelHeader
var inset = InsetRaw * scale;
var plan = ChannelHeaderLayout.Plan(
mode,
showName ? ChannelHeaderMode.Full : ChannelHeaderMode.DetailOnly,
width - inset * 2f,
ImGui.GetContentRegionAvail().Y - height - reservedBelow,
nameRun,
@@ -196,7 +207,8 @@ internal static class ChannelHeader
world,
DateTimeOffset.Now,
Plugin.Config.Use24HourClock,
Resources.HellionStrings.ChannelHeader_NotLoggedIn
Resources.HellionStrings.ChannelHeader_NotLoggedIn,
Plugin.Config.ScreenshotMode
);
}
@@ -31,14 +31,21 @@ internal static class ChannelHeaderDetail
string? world,
DateTimeOffset now,
bool use24Hour,
string fallback
string fallback,
bool hideWhere
)
{
var missing = string.IsNullOrWhiteSpace(world);
var clock = use24Hour
? now.ToString("HH:mm", CultureInfo.InvariantCulture)
: now.ToString("h:mm tt", CultureInfo.InvariantCulture);
// Screenshot mode. A home world names the player almost as precisely as
// the character name does, and the whole point of that mode is that a
// picture can be shared. The clock stays -- it identifies nobody.
if (hideWhere)
return new ChannelHeaderDetailParts(string.Empty, clock, false);
var missing = string.IsNullOrWhiteSpace(world);
return new ChannelHeaderDetailParts(missing ? fallback : world!, clock, missing);
}
}
@@ -96,7 +96,8 @@ internal sealed class WidgetGalleryWindow : Window
_headerLoggedOut ? null : "Ravana",
DateTimeOffset.Now,
Plugin.Config.Use24HourClock,
Resources.HellionStrings.ChannelHeader_NotLoggedIn
Resources.HellionStrings.ChannelHeader_NotLoggedIn,
Plugin.Config.ScreenshotMode
);
ChannelHeader.Draw(_headerSample, mode, fonts, detail, 0f);