client/systray: don't repeat account name for single-user tailnets (#19930)

Single-user tailnets often have the same tailnet display name as login
name.

This change omits the duplication when matching, and skips the
user-switching submenu when only one account is configured, to clean up
the account display a little bit.

Fixes #16889

Signed-off-by: Evan Lowry <evan@tailscale.com>
This commit is contained in:
Evan Lowry
2026-06-01 15:25:45 -03:00
committed by GitHub
parent d961e44856
commit 4f07a071e7
2 changed files with 62 additions and 22 deletions
+35 -22
View File
@@ -292,21 +292,23 @@ func (menu *Menu) rebuild() {
accounts := systray.AddMenuItem(account, "")
setRemoteIcon(accounts, menu.curProfile.UserProfile.ProfilePicURL)
time.Sleep(newMenuDelay)
for _, profile := range menu.allProfiles {
title := profileTitle(profile)
var item *systray.MenuItem
if profile.ID == menu.curProfile.ID {
item = accounts.AddSubMenuItemCheckbox(title, "", true)
} else {
item = accounts.AddSubMenuItem(title, "")
}
setRemoteIcon(item, profile.UserProfile.ProfilePicURL)
onClick(ctx, item, func(ctx context.Context) {
select {
case <-ctx.Done():
case menu.accountsCh <- profile.ID:
if len(menu.allProfiles) > 1 {
for _, profile := range menu.allProfiles {
title := profileTitle(profile)
var item *systray.MenuItem
if profile.ID == menu.curProfile.ID {
item = accounts.AddSubMenuItemCheckbox(title, "", true)
} else {
item = accounts.AddSubMenuItem(title, "")
}
})
setRemoteIcon(item, profile.UserProfile.ProfilePicURL)
onClick(ctx, item, func(ctx context.Context) {
select {
case <-ctx.Done():
case menu.accountsCh <- profile.ID:
}
})
}
}
}
@@ -352,16 +354,27 @@ func (menu *Menu) rebuild() {
// profileTitle returns the title string for a profile menu item.
func profileTitle(profile ipn.LoginProfile) string {
title := profile.Name
tailnet := ""
if profile.NetworkProfile.DomainName != "" {
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
// windows and mac don't support multi-line menu
title += " (" + profile.NetworkProfile.DisplayNameOrDefault() + ")"
} else {
title += "\n" + profile.NetworkProfile.DisplayNameOrDefault()
}
tailnet = profile.NetworkProfile.DisplayNameOrDefault()
}
return title
// windows and mac don't support multi-line menu items.
multiline := runtime.GOOS != "windows" && runtime.GOOS != "darwin"
return formatProfileTitle(profile.Name, tailnet, multiline)
}
// formatProfileTitle builds a profile menu label from a login name and an
// optional tailnet name. The tailnet portion is omitted when it matches the
// login name, so single-user tailnets don't show the same string twice.
func formatProfileTitle(name, tailnet string, multiline bool) string {
if tailnet == "" || strings.EqualFold(name, tailnet) {
return name
}
if multiline {
return name + "\n" + tailnet
}
return name + " (" + tailnet + ")"
}
var (
+27
View File
@@ -13,6 +13,33 @@ import (
"tailscale.com/types/key"
)
func TestProfileTitleMultiline(t *testing.T) {
t.Parallel()
tests := []struct {
name string
login string
tailnet string
multiline bool
want string
}{
{"no_tailnet", "alice@example.com", "", true, "alice@example.com"},
{"dup_exact", "example.com", "example.com", true, "example.com"},
{"dup_casefold", "Example.com", "example.com", false, "Example.com"},
{"distinct_multiline", "alice@example.com", "example.com", true, "alice@example.com\nexample.com"},
{"distinct_singleline", "alice@example.com", "example.com", false, "alice@example.com (example.com)"},
{"empty", "", "", true, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := formatProfileTitle(tt.login, tt.tailnet, tt.multiline); got != tt.want {
t.Errorf("profileTitleMultiline; got %v, want %v", got, tt.want)
}
})
}
}
func TestRecommendedIsActive(t *testing.T) {
t.Parallel()