From 14b7652e0cc6343300fea3259660479f9a016597 Mon Sep 17 00:00:00 2001 From: Gabi Date: Wed, 28 Aug 2024 21:26:30 -0300 Subject: [PATCH] chore(ui): move disable resource into resources details (#6463) To homogenize the UI in mobile and non-mobile, we re-enable the sub-menu for the internet resource, where it shows all resource ## New UI for android ![image](https://github.com/user-attachments/assets/3b9ce82f-1dbd-4ace-b7ba-873a34fd9dec) ![image](https://github.com/user-attachments/assets/16e9b128-532c-467e-b663-ae83195b2730) ## New UI for apple ![98579](https://github.com/user-attachments/assets/8bbda871-1d16-4d99-b873-a26d480821cf) ![99694](https://github.com/user-attachments/assets/f2924a08-8996-4882-867d-7446f7cfbafd) --- .../session/ui/ResourceDetailsBottomSheet.kt | 136 +++++++---- .../features/session/ui/ResourcesAdapter.kt | 40 +-- .../features/session/ui/SessionActivity.kt | 2 +- .../features/session/ui/ViewResource.kt | 2 +- .../firezone/android/tunnel/model/Resource.kt | 1 - .../res/layout/fragment_resource_details.xml | 8 + .../main/res/layout/list_item_resource.xml | 10 - rust/connlib/shared/src/messages/client.rs | 5 +- rust/connlib/tunnel/src/proptest.rs | 2 +- .../FirezoneKit/Views/ResourceView.swift | 231 ++++++++++++------ .../FirezoneKit/Views/SessionView.swift | 28 +-- 11 files changed, 269 insertions(+), 196 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 17a16318f..74182a31d 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 @@ -23,10 +23,9 @@ import com.google.android.material.button.MaterialButton import dev.firezone.android.R import dev.firezone.android.tunnel.model.StatusEnum -class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomSheetDialogFragment() { +class ResourceDetailsBottomSheet(private val resource: ViewResource, private val activity: SessionActivity) : BottomSheetDialogFragment() { private lateinit var view: View private val viewModel: SessionViewModel by activityViewModels() - private var isFavorite: Boolean = false override fun onCreateView( inflater: LayoutInflater, @@ -43,11 +42,6 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomShe this.view = view super.onViewCreated(view, savedInstanceState) - val addToFavoritesBtn: MaterialButton = view.findViewById(R.id.addToFavoritesBtn) - val removeFromFavoritesBtn: MaterialButton = view.findViewById(R.id.removeFromFavoritesBtn) - val resourceNameTextView: TextView = view.findViewById(R.id.tvResourceName) - val resourceAddressTextView: TextView = view.findViewById(R.id.tvResourceAddress) - val resourceAddressDescriptionTextView: TextView = view.findViewById(R.id.tvResourceAddressDescription) val siteNameTextView: TextView = view.findViewById(R.id.tvSiteName) val siteStatusTextView: TextView = view.findViewById(R.id.tvSiteStatus) val statusIndicatorDot: ImageView = view.findViewById(R.id.statusIndicatorDot) @@ -55,43 +49,9 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomShe val siteNameLayout: LinearLayout = view.findViewById(R.id.siteNameLayout) val siteStatusLayout: LinearLayout = view.findViewById(R.id.siteStatusLayout) - addToFavoritesBtn.setOnClickListener { - viewModel.addFavoriteResource(resource.id) - refreshButtons() - } - removeFromFavoritesBtn.setOnClickListener { - viewModel.removeFavoriteResource(resource.id) - refreshButtons() - } - refreshButtons() + resourceHeader() - resourceNameTextView.text = resource.name - val displayAddress = resource.addressDescription ?: resource.address - resourceAddressTextView.text = displayAddress - - if (!resource.addressDescription.isNullOrEmpty()) { - resourceAddressDescriptionTextView.text = resource.addressDescription - resourceAddressDescriptionTextView.visibility = View.VISIBLE - } - - val addressUri = resource.addressDescription?.let { Uri.parse(it) } - if (addressUri != null && addressUri.scheme != null) { - resourceAddressTextView.setTextColor(Color.BLUE) - resourceAddressTextView.setTypeface(null, Typeface.ITALIC) - resourceAddressTextView.setOnClickListener { - openUrl(resource.addressDescription!!) - } - } else { - resourceAddressTextView.setOnClickListener { - copyToClipboard(displayAddress!!) - Toast.makeText(requireContext(), "Address copied to clipboard", Toast.LENGTH_SHORT).show() - } - } - - resourceNameTextView.setOnClickListener { - copyToClipboard(resource.name) - Toast.makeText(requireContext(), "Name copied to clipboard", Toast.LENGTH_SHORT).show() - } + refreshDisableToggleButton() if (!resource.sites.isNullOrEmpty()) { val site = resource.sites.first() @@ -129,6 +89,81 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomShe } } + private fun resourceToggleText(): String { + if (resource.enabled) { + return "Disable this resource" + } else { + return "Enable this resource" + } + } + + private fun resourceHeader() { + if (resource.isInternetResource()) { + internetResourceHeader() + } else { + nonInternetResourceHeader() + } + } + + private fun internetResourceHeader() { + val addToFavoritesBtn: MaterialButton = view.findViewById(R.id.addToFavoritesBtn) + val removeFromFavoritesBtn: MaterialButton = view.findViewById(R.id.removeFromFavoritesBtn) + val resourceNameTextView: TextView = view.findViewById(R.id.tvResourceName) + val resourceAddress: LinearLayout = view.findViewById(R.id.addressSection) + val resourceAddressDescriptionTextView: TextView = view.findViewById(R.id.tvResourceAddressDescription) + val resourceDescriptionLayout: LinearLayout = view.findViewById(R.id.resourceDescriptionLayout) + + addToFavoritesBtn.visibility = View.GONE + removeFromFavoritesBtn.visibility = View.GONE + + resourceNameTextView.text = resource.name + + resourceAddress.visibility = View.GONE + + resourceDescriptionLayout.visibility = View.VISIBLE + resourceAddressDescriptionTextView.text = "All network traffic" + } + + private fun nonInternetResourceHeader() { + val addToFavoritesBtn: MaterialButton = view.findViewById(R.id.addToFavoritesBtn) + val removeFromFavoritesBtn: MaterialButton = view.findViewById(R.id.removeFromFavoritesBtn) + val resourceNameTextView: TextView = view.findViewById(R.id.tvResourceName) + val resourceAddressTextView: TextView = view.findViewById(R.id.tvResourceAddress) + + addToFavoritesBtn.setOnClickListener { + viewModel.addFavoriteResource(resource.id) + refreshButtons() + } + removeFromFavoritesBtn.setOnClickListener { + viewModel.removeFavoriteResource(resource.id) + refreshButtons() + } + refreshButtons() + + resourceNameTextView.text = resource.name + val displayAddress = resource.addressDescription ?: resource.address + resourceAddressTextView.text = displayAddress + + val addressUri = resource.addressDescription?.let { Uri.parse(it) } + if (addressUri != null && addressUri.scheme != null) { + resourceAddressTextView.setTextColor(Color.BLUE) + resourceAddressTextView.setTypeface(null, Typeface.ITALIC) + resourceAddressTextView.setOnClickListener { + openUrl(resource.addressDescription!!) + } + } else { + resourceAddressTextView.setOnClickListener { + copyToClipboard(displayAddress!!) + Toast.makeText(requireContext(), "Address copied to clipboard", Toast.LENGTH_SHORT).show() + } + } + + resourceNameTextView.setOnClickListener { + copyToClipboard(resource.name) + Toast.makeText(requireContext(), "Name copied to clipboard", Toast.LENGTH_SHORT).show() + } + } + private fun refreshButtons() { val addToFavoritesBtn: MaterialButton = view.findViewById(R.id.addToFavoritesBtn) val removeFromFavoritesBtn: MaterialButton = view.findViewById(R.id.removeFromFavoritesBtn) @@ -137,6 +172,21 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomShe removeFromFavoritesBtn.visibility = if (isFavorite) View.VISIBLE else View.GONE } + private fun refreshDisableToggleButton() { + if (resource.canBeDisabled) { + val toggleResourceEnabled: MaterialButton = view.findViewById(R.id.toggleResourceEnabled) + toggleResourceEnabled.visibility = View.VISIBLE + toggleResourceEnabled.text = resourceToggleText() + toggleResourceEnabled.setOnClickListener { + resource.enabled = !resource.enabled + + activity.onViewResourceToggled(resource) + + refreshDisableToggleButton() + } + } + } + private fun copyToClipboard(text: String) { val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("Copied Text", text) 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 ba8a8a42e..3f087ec3d 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 @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -14,8 +13,6 @@ import dev.firezone.android.databinding.ListItemResourceBinding internal class ResourcesAdapter(private val activity: SessionActivity) : ListAdapter( ResourceDiffCallback(), ) { - private var favoriteResources: HashSet = HashSet() - override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, @@ -29,45 +26,24 @@ internal class ResourcesAdapter(private val activity: SessionActivity) : ListAda position: Int, ) { val resource = getItem(position) - holder.bind(resource) { newResource -> onSwitchToggled(newResource) } - 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") - } + holder.bind(resource) + holder.itemView.setOnClickListener { + // Show bottom sheet + val fragmentManager = + (holder.itemView.context as AppCompatActivity).supportFragmentManager + val bottomSheet = ResourceDetailsBottomSheet(resource, activity) + bottomSheet.show(fragmentManager, "ResourceDetailsBottomSheet") } } - private fun onSwitchToggled(resource: ViewResource) { - activity.onViewResourceToggled(resource) - } - class ViewHolder(private val binding: ListItemResourceBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind( - resource: ViewResource, - onSwitchToggled: (ViewResource) -> Unit, - ) { + fun bind(resource: ViewResource) { binding.resourceNameText.text = resource.name 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 - binding.enableSwitch.isVisible = resource.canBeDisabled - - binding.enableSwitch.setOnCheckedChangeListener { - _, isChecked -> - resource.enabled = isChecked - - onSwitchToggled(resource) - } } } 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 08ff170fb..00641d637 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 @@ -20,7 +20,7 @@ import dev.firezone.android.features.settings.ui.SettingsActivity import dev.firezone.android.tunnel.TunnelService @AndroidEntryPoint -internal class SessionActivity : AppCompatActivity() { +class SessionActivity : AppCompatActivity() { private lateinit var binding: ActivitySessionBinding private var tunnelService: TunnelService? = null private var serviceBound = false 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 adc0f8559..75c0831c8 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 @@ -14,7 +14,7 @@ data class ViewResource( val sites: List?, val name: String, val status: StatusEnum, - var enabled: Boolean = true, + var enabled: Boolean, var canBeDisabled: Boolean = true, ) 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 2793942e3..339ecacc3 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 @@ -16,7 +16,6 @@ data class Resource( val sites: List?, val name: String, val status: StatusEnum, - var enabled: Boolean = true, @Json(name = "can_be_disabled") val canBeDisabled: Boolean, ) : Parcelable diff --git a/kotlin/android/app/src/main/res/layout/fragment_resource_details.xml b/kotlin/android/app/src/main/res/layout/fragment_resource_details.xml index 23366dd62..17469d676 100644 --- a/kotlin/android/app/src/main/res/layout/fragment_resource_details.xml +++ b/kotlin/android/app/src/main/res/layout/fragment_resource_details.xml @@ -33,6 +33,7 @@ + + - - diff --git a/rust/connlib/shared/src/messages/client.rs b/rust/connlib/shared/src/messages/client.rs index 27fbe26f4..3443e960e 100644 --- a/rust/connlib/shared/src/messages/client.rs +++ b/rust/connlib/shared/src/messages/client.rs @@ -91,7 +91,8 @@ pub struct ResourceDescriptionInternet { #[serde(rename = "gateway_groups")] pub sites: Vec, /// Whether or not resource can be disabled from UI - pub can_be_disabled: Option, + #[serde(default)] + pub can_be_disabled: bool, } impl ResourceDescriptionInternet { @@ -100,7 +101,7 @@ impl ResourceDescriptionInternet { name: self.name, id: self.id, sites: self.sites, - can_be_disabled: self.can_be_disabled.unwrap_or_default(), + can_be_disabled: self.can_be_disabled, status, } } diff --git a/rust/connlib/tunnel/src/proptest.rs b/rust/connlib/tunnel/src/proptest.rs index 9af848ed7..e8b05b7e5 100644 --- a/rust/connlib/tunnel/src/proptest.rs +++ b/rust/connlib/tunnel/src/proptest.rs @@ -83,7 +83,7 @@ pub fn internet_resource( name: "Internet Resource".to_string(), id, sites, - can_be_disabled: Some(can_be_disabled), + can_be_disabled, } }) } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift index 2abec972b..40fadfc0e 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift @@ -8,6 +8,11 @@ import SwiftUI #if os(iOS) +private func copyToClipboard(_ value: String) { + let pasteboard = UIPasteboard.general + pasteboard.string = value +} + struct ResourceView: View { @ObservedObject var model: SessionViewModel var resource: Resource @@ -15,82 +20,10 @@ struct ResourceView: View { var body: some View { List { - Section(header: Text("Resource")) { - HStack { - Text("NAME") - .bold() - .font(.system(size: 14)) - .foregroundColor(.secondary) - .frame(width: 80, alignment: .leading) - Text(resource.name) - } - .contextMenu { - Button(action: { - copyToClipboard(resource.name) - }) { - Text("Copy name") - Image(systemName: "doc.on.doc") - } - } - - HStack { - Text("ADDRESS") - .bold() - .font(.system(size: 14)) - .foregroundColor(.secondary) - .frame(width: 80, alignment: .leading) - if let url = URL(string: resource.addressDescription ?? resource.address!), - let _ = url.host { - Button(action: { - openURL(url) - }) { - Text(resource.addressDescription ?? resource.address!) - .foregroundColor(.blue) - .underline() - .font(.system(size: 16)) - .contextMenu { - Button(action: { - copyToClipboard(resource.addressDescription ?? resource.address!) - }) { - Text("Copy address") - Image(systemName: "doc.on.doc") - } - } - } - } else { - Text(resource.addressDescription ?? resource.address!) - .contextMenu { - Button(action: { - copyToClipboard(resource.addressDescription ?? resource.address!) - }) { - Text("Copy address") - Image(systemName: "doc.on.doc") - } - } - } - } - - if(model.favorites.ids.contains(resource.id)) { - Button(action: { - model.favorites.remove(resource.id) - }) { - HStack { - Image(systemName: "star") - Text("Remove from favorites") - Spacer() - } - } - } else { - Button(action: { - model.favorites.add(resource.id) - }) { - HStack { - Image(systemName: "star.fill") - Text("Add to favorites") - Spacer() - } - } - } + if resource.isInternetResource() { + InternetResourceHeader(model: model, resource: resource) + } else { + NonInternetResourceHeader(model: model, resource: resource) } if let site = resource.sites.first { @@ -156,10 +89,150 @@ struct ResourceView: View { return .gray } } +} - private func copyToClipboard(_ value: String) { - let pasteboard = UIPasteboard.general - pasteboard.string = value +struct NonInternetResourceHeader: View { + @ObservedObject var model: SessionViewModel + var resource: Resource + @Environment(\.openURL) var openURL + + var body: some View { + Section(header: Text("Resource")) { + HStack { + Text("NAME") + .bold() + .font(.system(size: 14)) + .foregroundColor(.secondary) + .frame(width: 80, alignment: .leading) + Text(resource.name) + } + .contextMenu { + Button(action: { + copyToClipboard(resource.name) + }) { + Text("Copy name") + Image(systemName: "doc.on.doc") + } + } + + HStack { + Text("ADDRESS") + .bold() + .font(.system(size: 14)) + .foregroundColor(.secondary) + .frame(width: 80, alignment: .leading) + if let url = URL(string: resource.addressDescription ?? resource.address!), + let _ = url.host { + Button(action: { + openURL(url) + }) { + Text(resource.addressDescription ?? resource.address!) + .foregroundColor(.blue) + .underline() + .font(.system(size: 16)) + .contextMenu { + Button(action: { + copyToClipboard(resource.addressDescription ?? resource.address!) + }) { + Text("Copy address") + Image(systemName: "doc.on.doc") + } + } + } + } else { + Text(resource.addressDescription ?? resource.address!) + .contextMenu { + Button(action: { + copyToClipboard(resource.addressDescription ?? resource.address!) + }) { + Text("Copy address") + Image(systemName: "doc.on.doc") + } + } + } + } + + if(model.favorites.ids.contains(resource.id)) { + Button(action: { + model.favorites.remove(resource.id) + }) { + HStack { + Image(systemName: "star") + Text("Remove from favorites") + Spacer() + } + } + } else { + Button(action: { + model.favorites.add(resource.id) + }) { + HStack { + Image(systemName: "star.fill") + Text("Add to favorites") + Spacer() + } + } + } + + ToggleResourceEnabledButton(resource: resource, model: model) + + } } } + +struct InternetResourceHeader: View { + @ObservedObject var model: SessionViewModel + var resource: Resource + + var body: some View { + Section(header: Text("Resource")) { + HStack { + Text("NAME") + .bold() + .font(.system(size: 14)) + .foregroundColor(.secondary) + .frame(width: 80, alignment: .leading) + Text(resource.name) + } + + HStack { + Text("DESCRIPTION") + .bold() + .font(.system(size: 14)) + .foregroundColor(.secondary) + .frame(alignment: .leading) + + Text("All network traffic") + } + ToggleResourceEnabledButton(resource: resource, model: model) + } + } +} + +struct ToggleResourceEnabledButton: View { + var resource: Resource + @ObservedObject var model: SessionViewModel + + private func toggleResourceEnabledText() -> String { + if model.isResourceEnabled(resource.id) { + "Disable this resource" + } else { + "Enable this resource" + } + } + + var body: some View { + if resource.canBeDisabled { + Button(action: { + model.store.toggleResourceDisabled(resource: resource.id, enabled: !model.isResourceEnabled(resource.id)) + }) { + HStack { + Text(toggleResourceEnabledText()) + Spacer() + } + } + } + } +} + #endif diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift index 7a21d51f1..989f761d3 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift @@ -130,37 +130,13 @@ struct ResourceSection: View { var body: some View { ForEach(resources) { resource in HStack { - if !resource.isInternetResource() { - NavigationLink { ResourceView(model: model, resource: resource) } + NavigationLink { ResourceView(model: model, resource: resource) } label: { - ResourceLabel(resource: resource, model: model ) + Text(resource.name) } - } 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() - } - } - } -} #endif