From 63c73e5bb66e90a05f772dba3bd4ec03a5f42b7b Mon Sep 17 00:00:00 2001 From: Gabi Date: Tue, 27 Aug 2024 01:08:19 -0300 Subject: [PATCH] feat: Internet Resource UI (#6434) Fixes #6047 On mobile platforms the internet resource is rendered with all non-favorite resources, since it was weird to see within the favorite tab, for the system tray platforms it's rendered as part of favorites if there is any favorite so that it's always visible to the user. For mobile platforms the resource is non-clickeable, since the menu shouldn't be of interest(maybe I should add it only for the sites?). For non-mobile there is a sub menu where you can find the sites and the enable/disable. The current label for the resource is a place holder for the screenshots, and can be set by the portal, if the portal doesn't set any name it will just show "Internet Resource". ### Android screenshot ![image](https://github.com/user-attachments/assets/63deb25f-1cd1-4b49-be80-77570e612aa5) ### Linux Screenshot ![image](https://github.com/user-attachments/assets/7b67033d-71ee-4bac-98c8-4c5810bf43a3) ![image](https://github.com/user-attachments/assets/5bdbced5-bacd-4a09-a59c-aa853bb3baa0) ### Windows Screenshot ![image](https://github.com/user-attachments/assets/a3bbebb3-9a18-4b75-9e18-f58b1b61a7a3) ### MacOS screenshot image ### iOS screenshot ![17044](https://github.com/user-attachments/assets/5321c363-5b43-4b1e-ac37-4fd7bdc68e28) --- .../session/ui/ResourceDetailsBottomSheet.kt | 2 +- .../features/session/ui/ResourcesAdapter.kt | 22 ++- .../features/session/ui/SessionActivity.kt | 12 +- .../features/session/ui/ViewResource.kt | 9 +- .../firezone/android/tunnel/model/Resource.kt | 6 +- .../main/res/layout/list_item_resource.xml | 17 +-- rust/connlib/shared/src/callbacks.rs | 6 +- rust/connlib/shared/src/messages/client.rs | 13 +- rust/connlib/tunnel/src/proptest.rs | 1 + .../src-tauri/src/client/gui/system_tray.rs | 144 ++++++------------ .../src/client/gui/system_tray/builder.rs | 40 ++++- .../Sources/FirezoneKit/Models/Resource.swift | 8 +- .../Sources/FirezoneKit/Views/MenuBar.swift | 56 +++++-- .../FirezoneKit/Views/ResourceView.swift | 10 +- .../FirezoneKit/Views/SessionView.swift | 37 +++-- 15 files changed, 218 insertions(+), 165 deletions(-) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceDetailsBottomSheet.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceDetailsBottomSheet.kt index 834979acf..17a16318f 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceDetailsBottomSheet.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceDetailsBottomSheet.kt @@ -83,7 +83,7 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomShe } } else { resourceAddressTextView.setOnClickListener { - copyToClipboard(displayAddress) + copyToClipboard(displayAddress!!) Toast.makeText(requireContext(), "Address copied to clipboard", Toast.LENGTH_SHORT).show() } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourcesAdapter.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourcesAdapter.kt index f6a6a4960..ba8a8a42e 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourcesAdapter.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourcesAdapter.kt @@ -2,6 +2,7 @@ package dev.firezone.android.features.session.ui import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible @@ -29,12 +30,15 @@ internal class ResourcesAdapter(private val activity: SessionActivity) : ListAda ) { val resource = getItem(position) holder.bind(resource) { newResource -> onSwitchToggled(newResource) } - holder.itemView.setOnClickListener { - // Show bottom sheet - val isFavorite = favoriteResources.contains(resource.id) - val fragmentManager = (holder.itemView.context as AppCompatActivity).supportFragmentManager - val bottomSheet = ResourceDetailsBottomSheet(resource) - bottomSheet.show(fragmentManager, "ResourceDetailsBottomSheet") + if (!resource.isInternetResource()) { + holder.itemView.setOnClickListener { + // Show bottom sheet + val isFavorite = favoriteResources.contains(resource.id) + val fragmentManager = + (holder.itemView.context as AppCompatActivity).supportFragmentManager + val bottomSheet = ResourceDetailsBottomSheet(resource) + bottomSheet.show(fragmentManager, "ResourceDetailsBottomSheet") + } } } @@ -48,7 +52,11 @@ internal class ResourcesAdapter(private val activity: SessionActivity) : ListAda onSwitchToggled: (ViewResource) -> Unit, ) { binding.resourceNameText.text = resource.name - binding.addressText.text = resource.address + if (resource.isInternetResource()) { + binding.addressText.visibility = View.GONE + } else { + binding.addressText.text = resource.address + } // Without this the item gets reset when out of view, isn't android wonderful? binding.enableSwitch.setOnCheckedChangeListener(null) binding.enableSwitch.isChecked = resource.enabled diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt index e92010b3e..08ff170fb 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt @@ -101,7 +101,11 @@ internal class SessionActivity : AppCompatActivity() { object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { viewModel.tabSelected(tab.position) - refreshList() + + refreshList { + // TODO: we might want to remember the old position? + binding.rvResourcesList.scrollToPosition(0) + } } override fun onTabUnselected(tab: TabLayout.Tab?) {} @@ -130,7 +134,7 @@ internal class SessionActivity : AppCompatActivity() { viewModel.favoriteResourcesLiveData.value = viewModel.repo.getFavoritesSync() } - private fun refreshList() { + private fun refreshList(afterLoad: () -> Unit = {}) { if (viewModel.forceAllResourcesTab()) { binding.tabLayout.selectTab(binding.tabLayout.getTabAt(SessionViewModel.RESOURCES_TAB_ALL), true) } @@ -141,7 +145,9 @@ internal class SessionActivity : AppCompatActivity() { View.GONE } - resourcesAdapter.submitList(viewModel.resourcesList()) + resourcesAdapter.submitList(viewModel.resourcesList()) { + afterLoad() + } } companion object { diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ViewResource.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ViewResource.kt index 98f9cbc0c..adc0f8559 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ViewResource.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ViewResource.kt @@ -2,12 +2,14 @@ package dev.firezone.android.features.session.ui import dev.firezone.android.tunnel.model.Resource +import dev.firezone.android.tunnel.model.ResourceType import dev.firezone.android.tunnel.model.Site import dev.firezone.android.tunnel.model.StatusEnum data class ViewResource( val id: String, - val address: String, + val type: ResourceType, + val address: String?, val addressDescription: String?, val sites: List?, val name: String, @@ -19,6 +21,7 @@ data class ViewResource( fun Resource.toViewResource(enabled: Boolean): ViewResource { return ViewResource( id = this.id, + type = this.type, address = this.address, addressDescription = this.addressDescription, sites = this.sites, @@ -28,3 +31,7 @@ fun Resource.toViewResource(enabled: Boolean): ViewResource { canBeDisabled = this.canBeDisabled, ) } + +fun ViewResource.isInternetResource(): Boolean { + return this.type == ResourceType.Internet +} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt index 51d2cbf07..2793942e3 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt @@ -9,9 +9,9 @@ import kotlinx.parcelize.Parcelize @JsonClass(generateAdapter = true) @Parcelize data class Resource( - val type: TypeEnum, + val type: ResourceType, val id: String, - val address: String, + val address: String?, @Json(name = "address_description") val addressDescription: String?, val sites: List?, val name: String, @@ -20,7 +20,7 @@ data class Resource( @Json(name = "can_be_disabled") val canBeDisabled: Boolean, ) : Parcelable -enum class TypeEnum { +enum class ResourceType { @Json(name = "dns") DNS, diff --git a/kotlin/android/app/src/main/res/layout/list_item_resource.xml b/kotlin/android/app/src/main/res/layout/list_item_resource.xml index bf7905436..24a71a073 100644 --- a/kotlin/android/app/src/main/res/layout/list_item_resource.xml +++ b/kotlin/android/app/src/main/res/layout/list_item_resource.xml @@ -12,7 +12,7 @@ style="@style/AppTheme.Base.Body1" android:layout_width="match_parent" android:layout_height="wrap_content" - app:layout_constraintEnd_toStartOf="@+id/enable_switch" + app:layout_constraintEnd_toStartOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Resource Name" /> @@ -24,24 +24,17 @@ android:layout_height="wrap_content" android:textColor="@color/neutral_600" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/enable_switch" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/resourceNameText" - app:layout_constraintVertical_bias="0.0" tools:text="Resource Address" /> diff --git a/rust/connlib/shared/src/callbacks.rs b/rust/connlib/shared/src/callbacks.rs index d9585ef4c..f63056ce4 100644 --- a/rust/connlib/shared/src/callbacks.rs +++ b/rust/connlib/shared/src/callbacks.rs @@ -79,7 +79,7 @@ impl ResourceDescription { } } - fn is_internet_resource(&self) -> bool { + pub fn is_internet_resource(&self) -> bool { matches!(self, ResourceDescription::Internet(_)) } } @@ -127,9 +127,6 @@ pub struct ResourceDescriptionInternet { /// Name for display always set to "Internet Resource" pub name: String, - /// Address for display always set to "All internet addresses" - pub address: String, - pub id: ResourceId, pub sites: Vec, @@ -186,7 +183,6 @@ mod tests { fn internet_resource(uuid: &str) -> ResourceDescription { ResourceDescription::Internet(ResourceDescriptionInternet { name: "Internet Resource".to_string(), - address: "All internet addresses".to_string(), id: ResourceId::from_str(uuid).unwrap(), sites: vec![Site { name: "test".to_string(), diff --git a/rust/connlib/shared/src/messages/client.rs b/rust/connlib/shared/src/messages/client.rs index 73f1047c6..27fbe26f4 100644 --- a/rust/connlib/shared/src/messages/client.rs +++ b/rust/connlib/shared/src/messages/client.rs @@ -73,9 +73,18 @@ impl ResourceDescriptionCidr { } } +fn internet_resource_name() -> String { + "<-> Internet Resource".to_string() +} + /// Description of an internet resource. #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ResourceDescriptionInternet { + /// Name of the resource. + /// + /// Used only for display. + #[serde(default = "internet_resource_name")] + pub name: String, /// Resource's id. pub id: ResourceId, /// Sites for the internet resource @@ -88,8 +97,7 @@ pub struct ResourceDescriptionInternet { impl ResourceDescriptionInternet { pub fn with_status(self, status: Status) -> crate::callbacks::ResourceDescriptionInternet { crate::callbacks::ResourceDescriptionInternet { - name: "Internet Resource".to_string(), - address: "All internet addresses".to_string(), + name: self.name, id: self.id, sites: self.sites, can_be_disabled: self.can_be_disabled.unwrap_or_default(), @@ -269,6 +277,7 @@ mod tests { { "id": "1106047c-cd5d-4151-b679-96b93da7383b", "type": "internet", + "name": "Internet Resource", "gateway_groups": [{"name": "test", "id": "eb94482a-94f4-47cb-8127-14fb3afa5516"}], "not": "relevant", "some_other": [ diff --git a/rust/connlib/tunnel/src/proptest.rs b/rust/connlib/tunnel/src/proptest.rs index d95d78ba1..9af848ed7 100644 --- a/rust/connlib/tunnel/src/proptest.rs +++ b/rust/connlib/tunnel/src/proptest.rs @@ -80,6 +80,7 @@ pub fn internet_resource( ) -> impl Strategy { (resource_id(), sites, any::()).prop_map(move |(id, sites, can_be_disabled)| { ResourceDescriptionInternet { + name: "Internet Resource".to_string(), id, sites, can_be_disabled: Some(can_be_disabled), diff --git a/rust/gui-client/src-tauri/src/client/gui/system_tray.rs b/rust/gui-client/src-tauri/src/client/gui/system_tray.rs index fd1bf6467..c51ba8883 100644 --- a/rust/gui-client/src-tauri/src/client/gui/system_tray.rs +++ b/rust/gui-client/src-tauri/src/client/gui/system_tray.rs @@ -12,11 +12,10 @@ use connlib_shared::{ }; use std::collections::HashSet; use tauri::{SystemTray, SystemTrayHandle}; -use url::Url; mod builder; -pub(crate) use builder::{copyable, item, Event, Item, Menu, Window}; +pub(crate) use builder::{item, Event, Menu, Window}; // Figma is the source of truth for the tray icons // @@ -40,6 +39,8 @@ const DISCONNECT_AND_QUIT: &str = "Disconnect and quit Firezone"; const DISABLE: &str = "Disable this resource"; const ENABLE: &str = "Enable this resource"; +pub(crate) const INTERNET_RESOURCE_DESCRIPTION: &str = "All network traffic"; + pub(crate) fn loading() -> SystemTray { SystemTray::new() .with_icon(tauri::Icon::Raw(BUSY_ICON.into())) @@ -68,27 +69,25 @@ pub(crate) struct SignedIn<'a> { } impl<'a> SignedIn<'a> { - fn is_favorite(&self, res: &ResourceDescription) -> bool { - self.favorite_resources.contains(&res.id()) + fn is_favorite(&self, resource: &ResourceId) -> bool { + self.favorite_resources.contains(resource) + } + + fn add_favorite_toggle(&self, submenu: &mut Menu, resource: ResourceId) { + if self.is_favorite(&resource) { + submenu.add_item(item(Event::RemoveFavorite(resource), REMOVE_FAVORITE).selected()); + } else { + submenu.add_item(item(Event::AddFavorite(resource), ADD_FAVORITE)); + } } /// Builds the submenu that has the resource address, name, desc, /// sites online, etc. fn resource_submenu(&self, res: &ResourceDescription) -> Menu { - let mut submenu = Menu::default(); + let mut submenu = Menu::default().resource_description(res); - submenu.add_item(resource_header(res)); - - let mut submenu = submenu - .separator() - .disabled("Resource") - .copyable(res.name()) - .copyable(res.pastable().as_ref()); - - if self.is_favorite(res) { - submenu.add_item(item(Event::RemoveFavorite(res.id()), REMOVE_FAVORITE).selected()); - } else { - submenu.add_item(item(Event::AddFavorite(res.id()), ADD_FAVORITE)); + if !res.is_internet_resource() { + self.add_favorite_toggle(&mut submenu, res.id()); } if res.can_be_disabled() { @@ -123,22 +122,6 @@ impl<'a> SignedIn<'a> { } } -fn resource_header(res: &ResourceDescription) -> Item { - let Some(address_description) = res.address_description() else { - return copyable(&res.pastable()); - }; - - if address_description.is_empty() { - return copyable(&res.pastable()); - } - - let Ok(url) = Url::parse(address_description) else { - return copyable(address_description); - }; - - item(Event::Url(url), format!("<{address_description}>")) -} - #[derive(PartialEq)] pub(crate) enum Icon { /// Must be equivalent to the default app icon, since we assume this is set when we start @@ -247,7 +230,7 @@ fn signed_in(signed_in: &SignedIn) -> Menu { // Always show Resources in the original order for res in resources .iter() - .filter(|res| favorite_resources.contains(&res.id())) + .filter(|res| favorite_resources.contains(&res.id()) || res.is_internet_resource()) { menu = menu.add_submenu(res.name(), signed_in.resource_submenu(res)); } @@ -266,7 +249,7 @@ fn signed_in(signed_in: &SignedIn) -> Menu { // Always show Resources in the original order for res in resources .iter() - .filter(|res| !favorite_resources.contains(&res.id())) + .filter(|res| !favorite_resources.contains(&res.id()) && !res.is_internet_resource()) { submenu = submenu.add_submenu(res.name(), signed_in.resource_submenu(res)); } @@ -449,17 +432,7 @@ mod tests { .add_submenu( "Internet Resource", Menu::default() - .copyable("") - .separator() - .disabled("Resource") - .copyable("Internet Resource") - .copyable("") - .item( - Event::AddFavorite( - ResourceId::from_str("1106047c-cd5d-4151-b679-96b93da7383b").unwrap(), - ), - ADD_FAVORITE, - ) + .disabled(INTERNET_RESOURCE_DESCRIPTION) .separator() .disabled("Site") .copyable("test") @@ -510,48 +483,37 @@ mod tests { .copyable("test") .copyable(GATEWAY_CONNECTED), ) + .add_submenu( + "Internet Resource", + Menu::default() + .disabled(INTERNET_RESOURCE_DESCRIPTION) + .separator() + .disabled("Site") + .copyable("test") + .copyable(ALL_GATEWAYS_OFFLINE), + ) .separator() .add_submenu( OTHER_RESOURCES, - Menu::default() - .add_submenu( - "172.172.0.0/16", - Menu::default() - .copyable("cidr resource") - .separator() - .disabled("Resource") - .copyable("172.172.0.0/16") - .copyable("172.172.0.0/16") - .item( - Event::AddFavorite(ResourceId::from_str( - "73037362-715d-4a83-a749-f18eadd970e6", - )?), - ADD_FAVORITE, - ) - .separator() - .disabled("Site") - .copyable("test") - .copyable(NO_ACTIVITY), - ) - .add_submenu( - "Internet Resource", - Menu::default() - .copyable("") - .separator() - .disabled("Resource") - .copyable("Internet Resource") - .copyable("") - .item( - Event::AddFavorite(ResourceId::from_str( - "1106047c-cd5d-4151-b679-96b93da7383b", - )?), - ADD_FAVORITE, - ) - .separator() - .disabled("Site") - .copyable("test") - .copyable(ALL_GATEWAYS_OFFLINE), - ), + Menu::default().add_submenu( + "172.172.0.0/16", + Menu::default() + .copyable("cidr resource") + .separator() + .disabled("Resource") + .copyable("172.172.0.0/16") + .copyable("172.172.0.0/16") + .item( + Event::AddFavorite(ResourceId::from_str( + "73037362-715d-4a83-a749-f18eadd970e6", + )?), + ADD_FAVORITE, + ) + .separator() + .disabled("Site") + .copyable("test") + .copyable(NO_ACTIVITY), + ), ) .add_bottom_section(DISCONNECT_AND_QUIT); // Skip testing the bottom section, it's simple @@ -623,17 +585,7 @@ mod tests { .add_submenu( "Internet Resource", Menu::default() - .copyable("") - .separator() - .disabled("Resource") - .copyable("Internet Resource") - .copyable("") - .item( - Event::AddFavorite(ResourceId::from_str( - "1106047c-cd5d-4151-b679-96b93da7383b", - )?), - ADD_FAVORITE, - ) + .disabled(INTERNET_RESOURCE_DESCRIPTION) .separator() .disabled("Site") .copyable("test") diff --git a/rust/gui-client/src-tauri/src/client/gui/system_tray/builder.rs b/rust/gui-client/src-tauri/src/client/gui/system_tray/builder.rs index 0671efb7e..a120e0c1c 100644 --- a/rust/gui-client/src-tauri/src/client/gui/system_tray/builder.rs +++ b/rust/gui-client/src-tauri/src/client/gui/system_tray/builder.rs @@ -1,9 +1,11 @@ //! An abstraction over Tauri's system tray menu structs, that implements `PartialEq` for unit testing -use connlib_shared::messages::ResourceId; +use connlib_shared::{callbacks::ResourceDescription, messages::ResourceId}; use serde::{Deserialize, Serialize}; use url::Url; +use super::INTERNET_RESOURCE_DESCRIPTION; + /// A menu that can either be assigned to the system tray directly or used as a submenu in another menu. /// /// Equivalent to `tauri::SystemTrayMenu` @@ -75,6 +77,22 @@ pub(crate) enum Window { Settings, } +fn resource_header(res: &ResourceDescription) -> Item { + let Some(address_description) = res.address_description() else { + return copyable(&res.pastable()); + }; + + if address_description.is_empty() { + return copyable(&res.pastable()); + } + + let Ok(url) = Url::parse(address_description) else { + return copyable(address_description); + }; + + item(Event::Url(url), format!("<{address_description}>")) +} + impl Menu { /// Appends things that always show, like About, Settings, Help, Quit, etc. pub(crate) fn add_bottom_section(self, quit_text: &str) -> Self { @@ -154,6 +172,26 @@ impl Menu { self.add_separator(); self } + + fn internet_resource(self) -> Self { + self.disabled(INTERNET_RESOURCE_DESCRIPTION) + } + + fn resource_body(self, resource: &ResourceDescription) -> Self { + self.separator() + .disabled("Resource") + .copyable(resource.name()) + .copyable(resource.pastable().as_ref()) + } + + pub(crate) fn resource_description(mut self, resource: &ResourceDescription) -> Self { + if resource.is_internet_resource() { + self.internet_resource() + } else { + self.add_item(resource_header(resource)); + self.resource_body(resource) + } + } } impl Item { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift index ef682730d..ca8a49788 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift @@ -25,14 +25,14 @@ public enum ResourceList { public struct Resource: Decodable, Identifiable, Equatable { public let id: String public var name: String - public var address: String + public var address: String? public var addressDescription: String? public var status: ResourceStatus public var sites: [Site] public var type: ResourceType public var canBeDisabled: Bool - public init(id: String, name: String, address: String, addressDescription: String?, status: ResourceStatus, sites: [Site], type: ResourceType, canBeDisabled: Bool) { + public init(id: String, name: String, address: String?, addressDescription: String?, status: ResourceStatus, sites: [Site], type: ResourceType, canBeDisabled: Bool) { self.id = id self.name = name self.address = address @@ -42,6 +42,10 @@ public struct Resource: Decodable, Identifiable, Equatable { self.type = type self.canBeDisabled = canBeDisabled } + + public func isInternetResource() -> Bool { + self.type == ResourceType.internet + } } public enum ResourceStatus: String, Decodable { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift index 17c7c70da..307ae7ed5 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift @@ -418,12 +418,12 @@ public final class MenuBar: NSObject, ObservableObject { // If we have no favorites, then everything is a favorite let hasAnyFavorites = newResources.contains { model.favorites.contains($0.id) } let newFavorites = if (hasAnyFavorites) { - newResources.filter { model.favorites.contains($0.id) } + newResources.filter { model.favorites.contains($0.id) || $0.isInternetResource() } } else { newResources } let newOthers: [Resource] = if hasAnyFavorites { - newResources.filter { !model.favorites.contains($0.id) } + newResources.filter { !model.favorites.contains($0.id) && !$0.isInternetResource() } } else { [] } @@ -511,20 +511,11 @@ public final class MenuBar: NSObject, ObservableObject { model.isResourceEnabled(id) ? "Disable this resource" : "Enable this resource" } - private func createSubMenu(resource: Resource) -> NSMenu { + private func nonInternetResourceHeader(resource: Resource) -> NSMenu { let subMenu = NSMenu() - let resourceAddressDescriptionItem = NSMenuItem() - let resourceSectionItem = NSMenuItem() - let resourceNameItem = NSMenuItem() - let resourceAddressItem = NSMenuItem() - let siteSectionItem = NSMenuItem() - let siteNameItem = NSMenuItem() - let siteStatusItem = NSMenuItem() - let toggleFavoriteItem = NSMenuItem() - let enableToggle = NSMenuItem() - // AddressDescription first -- will be most common action + let resourceAddressDescriptionItem = NSMenuItem() if let addressDescription = resource.addressDescription { resourceAddressDescriptionItem.title = addressDescription @@ -544,7 +535,7 @@ public final class MenuBar: NSObject, ObservableObject { } } else { // Show Address first if addressDescription is missing - resourceAddressDescriptionItem.title = resource.address + resourceAddressDescriptionItem.title = resource.address! // Address is none only for non-internet resource resourceAddressDescriptionItem.action = #selector(resourceValueTapped(_:)) } resourceAddressDescriptionItem.isEnabled = true @@ -553,11 +544,13 @@ public final class MenuBar: NSObject, ObservableObject { subMenu.addItem(NSMenuItem.separator()) + let resourceSectionItem = NSMenuItem() resourceSectionItem.title = "Resource" resourceSectionItem.isEnabled = false subMenu.addItem(resourceSectionItem) // Resource name + let resourceNameItem = NSMenuItem() resourceNameItem.action = #selector(resourceValueTapped(_:)) resourceNameItem.title = resource.name resourceNameItem.toolTip = "Resource name (click to copy)" @@ -566,13 +559,16 @@ public final class MenuBar: NSObject, ObservableObject { subMenu.addItem(resourceNameItem) // Resource address + let resourceAddressItem = NSMenuItem() resourceAddressItem.action = #selector(resourceValueTapped(_:)) - resourceAddressItem.title = resource.address + resourceAddressItem.title = resource.address! resourceAddressItem.toolTip = "Resource address (click to copy)" resourceAddressItem.isEnabled = true resourceAddressItem.target = self subMenu.addItem(resourceAddressItem) + let toggleFavoriteItem = NSMenuItem() + if model.favorites.contains(resource.id) { toggleFavoriteItem.action = #selector(removeFavoriteTapped(_:)) toggleFavoriteItem.title = "Remove from favorites" @@ -587,6 +583,36 @@ public final class MenuBar: NSObject, ObservableObject { toggleFavoriteItem.target = self subMenu.addItem(toggleFavoriteItem) + return subMenu + } + + private func internetResourceHeader(resource: Resource) -> NSMenu { + let subMenu = NSMenu() + let description = NSMenuItem() + + description.title = "All network traffic" + description.isEnabled = false + + subMenu.addItem(description) + + return subMenu + } + + private func resourceHeader(resource: Resource) -> NSMenu { + if resource.isInternetResource() { + internetResourceHeader(resource: resource) + } else { + nonInternetResourceHeader(resource: resource) + } + } + + private func createSubMenu(resource: Resource) -> NSMenu { + let siteSectionItem = NSMenuItem() + let siteNameItem = NSMenuItem() + let siteStatusItem = NSMenuItem() + let enableToggle = NSMenuItem() + + let subMenu = resourceHeader(resource: resource) // Resource enable / disable toggle if resource.canBeDisabled { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift index 929e313c8..2abec972b 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift @@ -39,18 +39,18 @@ struct ResourceView: View { .font(.system(size: 14)) .foregroundColor(.secondary) .frame(width: 80, alignment: .leading) - if let url = URL(string: resource.addressDescription ?? resource.address), + if let url = URL(string: resource.addressDescription ?? resource.address!), let _ = url.host { Button(action: { openURL(url) }) { - Text(resource.addressDescription ?? resource.address) + Text(resource.addressDescription ?? resource.address!) .foregroundColor(.blue) .underline() .font(.system(size: 16)) .contextMenu { Button(action: { - copyToClipboard(resource.addressDescription ?? resource.address) + copyToClipboard(resource.addressDescription ?? resource.address!) }) { Text("Copy address") Image(systemName: "doc.on.doc") @@ -58,10 +58,10 @@ struct ResourceView: View { } } } else { - Text(resource.addressDescription ?? resource.address) + Text(resource.addressDescription ?? resource.address!) .contextMenu { Button(action: { - copyToClipboard(resource.addressDescription ?? resource.address) + copyToClipboard(resource.addressDescription ?? resource.address!) }) { Text("Copy address") Image(systemName: "doc.on.doc") diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift index 7d15d7e6a..7a21d51f1 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift @@ -130,22 +130,35 @@ struct ResourceSection: View { var body: some View { ForEach(resources) { resource in HStack { - NavigationLink { ResourceView(model: model, resource: resource) } - label: { - HStack { - Text(resource.name) - if resource.canBeDisabled { - Spacer() - Toggle("Enabled", isOn: Binding( - get: { model.isResourceEnabled(resource.id) }, - set: { newValue in - model.store.toggleResourceDisabled(resource: resource.id, enabled: newValue) - } - )).labelsHidden() + if !resource.isInternetResource() { + NavigationLink { ResourceView(model: model, resource: resource) } + label: { + ResourceLabel(resource: resource, model: model ) } + } else { + ResourceLabel(resource: resource, model: model) } } .navigationTitle("All Resources") + } + } +} + +struct ResourceLabel: View { + let resource: Resource + @ObservedObject var model: SessionViewModel + + var body: some View { + HStack { + Text(resource.name) + if resource.canBeDisabled { + Spacer() + Toggle("Enabled", isOn: Binding( + get: { model.isResourceEnabled(resource.id) }, + set: { newValue in + model.store.toggleResourceDisabled(resource: resource.id, enabled: newValue) + } + )).labelsHidden() } } }