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() } } }