From 616b19c0644be0d1b0b5ccf4632653f6402e4faf Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 3 Jun 2025 10:49:13 +0300 Subject: [PATCH] [client] Add "Deselect All" Menu Item to Exit Node Menu (#3877) * [client] Enhance exit node menu functionality with deselect all option * Hide exit nodes before removal in recreateExitNodeMenu * recreateExitNodeMenu adding mutex locks * Refetch exit nodes after deselecting all in exit node menu --- client/ui/client_ui.go | 18 +++++---- client/ui/network.go | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index c23b78582..c0c8692c6 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -235,9 +235,11 @@ type serviceClient struct { eventManager *event.Manager - exitNodeMu sync.Mutex - mExitNodeItems []menuHandler - logFile string + exitNodeMu sync.Mutex + mExitNodeItems []menuHandler + exitNodeStates []exitNodeState + mExitNodeDeselectAll *systray.MenuItem + logFile string } type menuHandler struct { @@ -1035,11 +1037,11 @@ func (s *serviceClient) updateConfig() error { lazyConnectionEnabled := s.mLazyConnEnabled.Checked() loginRequest := proto.LoginRequest{ - IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", - ServerSSHAllowed: &sshAllowed, - RosenpassEnabled: &rosenpassEnabled, - DisableAutoConnect: &disableAutoStart, - DisableNotifications: ¬ificationsDisabled, + IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd", + ServerSSHAllowed: &sshAllowed, + RosenpassEnabled: &rosenpassEnabled, + DisableAutoConnect: &disableAutoStart, + DisableNotifications: ¬ificationsDisabled, LazyConnectionEnabled: &lazyConnectionEnabled, } diff --git a/client/ui/network.go b/client/ui/network.go index 435917f30..b3748a89d 100644 --- a/client/ui/network.go +++ b/client/ui/network.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "runtime" + "slices" "sort" "strings" "time" @@ -33,6 +34,11 @@ const ( type filter string +type exitNodeState struct { + id string + selected bool +} + func (s *serviceClient) showNetworksUI() { s.wNetworks = s.app.NewWindow("Networks") s.wNetworks.SetOnClosed(s.cancel) @@ -357,18 +363,45 @@ func (s *serviceClient) updateExitNodes() { } func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { + var exitNodeIDs []exitNodeState + for _, node := range exitNodes { + exitNodeIDs = append(exitNodeIDs, exitNodeState{ + id: node.ID, + selected: node.Selected, + }) + } + + sort.Slice(exitNodeIDs, func(i, j int) bool { + return exitNodeIDs[i].id < exitNodeIDs[j].id + }) + if slices.Equal(s.exitNodeStates, exitNodeIDs) { + log.Debug("Exit node menu already up to date") + return + } + for _, node := range s.mExitNodeItems { node.cancel() + node.Hide() node.Remove() } s.mExitNodeItems = nil + if s.mExitNodeDeselectAll != nil { + s.mExitNodeDeselectAll.Remove() + s.mExitNodeDeselectAll = nil + } if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { s.mExitNode.Remove() s.mExitNode = systray.AddMenuItem("Exit Node", exitNodeMenuDescr) } + var showDeselectAll bool + for _, node := range exitNodes { + if node.Selected { + showDeselectAll = true + } + menuItem := s.mExitNode.AddSubMenuItemCheckbox( node.ID, fmt.Sprintf("Use exit node %s", node.ID), @@ -383,6 +416,32 @@ func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) { go s.handleChecked(ctx, node.ID, menuItem) } + s.exitNodeStates = exitNodeIDs + + if showDeselectAll { + s.mExitNode.AddSeparator() + deselectAllItem := s.mExitNode.AddSubMenuItem("Deselect All", "Deselect All") + s.mExitNodeDeselectAll = deselectAllItem + go func() { + for { + _, ok := <-deselectAllItem.ClickedCh + if !ok { + // channel closed: exit the goroutine + return + } + exitNodes, err := s.handleExitNodeMenuDeselectAll() + if err != nil { + log.Warnf("failed to handle deselect all exit nodes: %v", err) + } else { + s.exitNodeMu.Lock() + s.recreateExitNodeMenu(exitNodes) + s.exitNodeMu.Unlock() + } + } + + }() + } + } func (s *serviceClient) getExitNodes(conn proto.DaemonServiceClient) ([]*proto.Network, error) { @@ -420,6 +479,37 @@ func (s *serviceClient) handleChecked(ctx context.Context, id string, item *syst } } +func (s *serviceClient) handleExitNodeMenuDeselectAll() ([]*proto.Network, error) { + conn, err := s.getSrvClient(defaultFailTimeout) + if err != nil { + return nil, fmt.Errorf("get client: %v", err) + } + + exitNodes, err := s.getExitNodes(conn) + if err != nil { + return nil, fmt.Errorf("get exit nodes: %v", err) + } + + var ids []string + for _, e := range exitNodes { + if e.Selected { + ids = append(ids, e.ID) + } + } + + // deselect selected exit nodes + if err := s.deselectOtherExitNodes(conn, ids); err != nil { + return nil, err + } + + updatedExitNodes, err := s.getExitNodes(conn) + if err != nil { + return nil, fmt.Errorf("re-fetch exit nodes: %v", err) + } + + return updatedExitNodes, nil +} + // Add function to toggle exit node selection func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) error { conn, err := s.getSrvClient(defaultFailTimeout)