From 0935a5675da2cff098b697e15d549f1d5be0746c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Tue, 26 May 2026 10:59:36 +0200 Subject: [PATCH] tray: move session-expiry row under profile email, hide separator when daemon unavailable - Relocate the session-expiry row from below the status item to below the profile email so active profile, email, and session deadline form one block. - Rename the label to "Expires in {remaining}" (en/hu/de). - Capture the Connect/Disconnect separator via lastMenuItem and hide it when both action rows are hidden (daemon unavailable), avoiding two adjacent separators with nothing between them. --- .../frontend/src/i18n/locales/de/common.json | 2 +- .../frontend/src/i18n/locales/en/common.json | 2 +- .../frontend/src/i18n/locales/hu/common.json | 2 +- client/ui/tray.go | 51 ++++++++++++++++--- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/client/ui/frontend/src/i18n/locales/de/common.json b/client/ui/frontend/src/i18n/locales/de/common.json index 460e47173..4300ca22c 100644 --- a/client/ui/frontend/src/i18n/locales/de/common.json +++ b/client/ui/frontend/src/i18n/locales/de/common.json @@ -3,7 +3,7 @@ "tray.status.disconnected": "Getrennt", "tray.status.daemonUnavailable": "Nicht aktiv", "tray.status.error": "Fehler", - "tray.session.expiresIn": "Sitzung: {remaining}", + "tray.session.expiresIn": "Läuft ab in {remaining}", "tray.menu.open": "NetBird öffnen", "tray.menu.connect": "Verbinden", diff --git a/client/ui/frontend/src/i18n/locales/en/common.json b/client/ui/frontend/src/i18n/locales/en/common.json index fe544bf5e..b798bcbdf 100644 --- a/client/ui/frontend/src/i18n/locales/en/common.json +++ b/client/ui/frontend/src/i18n/locales/en/common.json @@ -3,7 +3,7 @@ "tray.status.disconnected": "Disconnected", "tray.status.daemonUnavailable": "Not running", "tray.status.error": "Error", - "tray.session.expiresIn": "Session: {remaining}", + "tray.session.expiresIn": "Expires in {remaining}", "tray.menu.open": "Open NetBird", "tray.menu.connect": "Connect", diff --git a/client/ui/frontend/src/i18n/locales/hu/common.json b/client/ui/frontend/src/i18n/locales/hu/common.json index 3af226371..1166894d4 100644 --- a/client/ui/frontend/src/i18n/locales/hu/common.json +++ b/client/ui/frontend/src/i18n/locales/hu/common.json @@ -3,7 +3,7 @@ "tray.status.disconnected": "Lekapcsolva", "tray.status.daemonUnavailable": "Nem fut", "tray.status.error": "Hiba", - "tray.session.expiresIn": "Munkamenet: {remaining}", + "tray.session.expiresIn": "Lejár {remaining} múlva", "tray.menu.open": "NetBird megnyitása", "tray.menu.connect": "Csatlakozás", diff --git a/client/ui/tray.go b/client/ui/tray.go index 58349e57b..7f0ec2f8a 100644 --- a/client/ui/tray.go +++ b/client/ui/tray.go @@ -110,6 +110,11 @@ type Tray struct { sessionExpiresItem *application.MenuItem upItem *application.MenuItem downItem *application.MenuItem + // connectSeparator sits between the profile block and the + // Connect/Disconnect rows. Hidden together with both action rows when + // the daemon is unavailable so the menu doesn't show two adjacent + // separators with nothing between them. + connectSeparator *application.MenuItem exitNodeItem *application.MenuItem networksItem *application.MenuItem profileSubmenu *application.Menu @@ -262,6 +267,12 @@ func (t *Tray) reapplyMenuState() { t.downItem.SetHidden(!connected && !connecting) t.downItem.SetEnabled(connected || connecting) } + if t.connectSeparator != nil { + // Hide the separator above Connect/Disconnect when both rows are + // hidden (happens when the daemon is unavailable) — otherwise the + // menu shows two adjacent separators with nothing between them. + t.connectSeparator.SetHidden(daemonUnavailable) + } if t.exitNodeItem != nil { t.exitNodeItem.SetEnabled(connected) } @@ -338,14 +349,6 @@ func (t *Tray) buildMenu() *application.Menu { SetEnabled(statusRowEnabled()). SetBitmap(iconMenuDotIdle) - // sessionExpiresItem sits directly below the status row so the - // remaining-time label reads as a sub-line of "Connected" etc. Hidden - // until applyStatus sees a non-zero SessionExpiresAt on the daemon - // Status snapshot — peers without SSO tracking or with login expiry - // disabled never reveal this row. - t.sessionExpiresItem = menu.Add("").SetEnabled(false) - t.sessionExpiresItem.SetHidden(true) - menu.AddSeparator() // The tray icon's left-click handler is intentionally unbound (see // NewTray for the rationale), so expose the window through an explicit @@ -367,7 +370,17 @@ func (t *Tray) buildMenu() *application.Menu { // non-empty email for the active profile. t.profileEmailItem = menu.Add("").SetEnabled(false) t.profileEmailItem.SetHidden(true) + // sessionExpiresItem sits below the profile email so the active profile, + // its account email, and the SSO session deadline read as a single block. + // Hidden until applyStatus sees a non-zero SessionExpiresAt on the daemon + // Status snapshot — peers without SSO tracking or with login expiry + // disabled never reveal this row. + t.sessionExpiresItem = menu.Add("").SetEnabled(false) + t.sessionExpiresItem.SetHidden(true) + // AddSeparator returns no reference, so capture the separator by walking + // ItemAt until it returns nil and grabbing the last index. menu.AddSeparator() + t.connectSeparator = lastMenuItem(menu) // Only the action that applies to the current state is visible: Connect // when disconnected, Disconnect when connected. applyStatus swaps them on // each daemon status change. @@ -670,6 +683,12 @@ func (t *Tray) applyStatus(st services.Status) { t.downItem.SetHidden(!connected && !connecting) t.downItem.SetEnabled(connected || connecting) } + if t.connectSeparator != nil { + // Hide the separator above Connect/Disconnect when both rows + // are hidden (daemon unavailable) — otherwise the menu shows + // two adjacent separators with nothing between them. + t.connectSeparator.SetHidden(daemonUnavailable) + } // Exit Node and Resources surface tunnel-routed state, so only // expose them while the tunnel is up. Settings/Debug-Bundle just // need the daemon socket reachable. @@ -1278,3 +1297,19 @@ func titleCase(s string) string { } return strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) } + +// lastMenuItem returns the menu's most recently appended *MenuItem. The Wails +// AddSeparator helper doesn't return one, so to keep a handle on a separator +// (e.g. to toggle SetHidden later) we call AddSeparator and then ask for the +// item at the tail. Walks ItemAt(i) until it returns nil; cheap because menus +// are short. +func lastMenuItem(m *application.Menu) *application.MenuItem { + var last *application.MenuItem + for i := 0; ; i++ { + item := m.ItemAt(i) + if item == nil { + return last + } + last = item + } +}