damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit c4dfae9ede2b6d2892d81ee7bc5f264b32f2de95
parent bfda0d1b74d7910a989afcb7faa7a56f2017bff9
Author: ericholguin <eric.holguinsanchez@gmail.com>
Date:   Fri,  8 Sep 2023 21:15:03 -0600

relays: update relay view to use new design

Changelog-Changed: Updated relay view
Closes: https://github.com/damus-io/damus/pull/1543

Diffstat:
Mdamus/Views/AddRelayView.swift | 145++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mdamus/Views/Relays/RecommendedRelayView.swift | 85+++++++++++++++++++++++++++++++------------------------------------------------
Mdamus/Views/Relays/RelayConfigView.swift | 152+++++++++++++++++++++++++++----------------------------------------------------
Mdamus/Views/Relays/RelayStatusView.swift | 44+++++++++++++++++++++++++++++++++++++-------
Mdamus/Views/Relays/RelayView.swift | 52+++++++++++++++++++++++-----------------------------
5 files changed, 274 insertions(+), 204 deletions(-)

diff --git a/damus/Views/AddRelayView.swift b/damus/Views/AddRelayView.swift @@ -8,33 +8,148 @@ import SwiftUI struct AddRelayView: View { - @Binding var relay: String + let state: DamusState + @State var new_relay: String = "" + @State var relayAddErrorTitle: String? = nil + @State var relayAddErrorMessage: String? = nil + + @Environment(\.dismiss) var dismiss var body: some View { - ZStack(alignment: .leading) { - HStack{ - TextField(NSLocalizedString("wss://some.relay.com", comment: "Placeholder example for relay server address."), text: $relay) - .padding(2) - .padding(.leading, 25) + VStack { + Text("Add relay", comment: "Title text to indicate user to an add a relay.") + .font(.system(size: 20, weight: .bold)) + .padding(.vertical) + + Divider() + .padding(.bottom) + + HStack { + Label("", image: "copy2") + .onTapGesture { + if let pastedrelay = UIPasteboard.general.string { + self.new_relay = pastedrelay + } + } + TextField(NSLocalizedString("wss://some.relay.com", comment: "Placeholder example for relay server address."), text: $new_relay) .autocorrectionDisabled(true) .textInputAutocapitalization(.never) Label("", image: "close-circle") .foregroundColor(.accentColor) - .padding(.trailing, -25.0) - .opacity((relay == "") ? 0.0 : 1.0) + .opacity((new_relay == "") ? 0.0 : 1.0) .onTapGesture { - self.relay = "" + self.new_relay = "" + } + } + .padding(10) + .background(.secondary.opacity(0.2)) + .cornerRadius(10) + + if let errorMessage = relayAddErrorMessage { + VStack(spacing: 0) { + HStack(alignment: .top) { + Text(relayAddErrorTitle ?? "Error") + .bold() + .foregroundColor(DamusColors.dangerSecondary) + .padding(.leading) + Spacer() + Button(action: { + relayAddErrorTitle = nil // Clear error title + relayAddErrorMessage = nil // Clear error message + self.new_relay = "" + }, label: { + Image("close") + .frame(width: 20, height: 20) + .foregroundColor(DamusColors.dangerSecondary) + }) + .padding(.trailing) } + + Text(errorMessage) + .foregroundColor(DamusColors.dangerSecondary) + .padding(.top, 10) + } + .frame(minWidth: 300, maxWidth: .infinity, minHeight: 120, alignment: .center) + .background { + RoundedRectangle(cornerRadius: 12) + .fill(DamusColors.dangerBorder, strokeBorder: .gray.opacity(0.5), lineWidth: 1) + } } - Label("", image: "copy2") - .padding(.leading, -10) - .onTapGesture { - if let pastedrelay = UIPasteboard.general.string { - self.relay = pastedrelay + Button(action: { + if new_relay.starts(with: "wss://") == false && new_relay.starts(with: "ws://") == false { + new_relay = "wss://" + new_relay + } + + if new_relay.hasSuffix("/") { + new_relay.removeLast(); + } + + guard let url = RelayURL(new_relay), + let ev = state.contacts.event, + let keypair = state.keypair.to_full() else { + return + } + + let info = RelayInfo.rw + let descriptor = RelayDescriptor(url: url, info: info) + + do { + try state.pool.add_relay(descriptor) + relayAddErrorTitle = nil // Clear error title + relayAddErrorMessage = nil // Clear error message + } catch RelayError.RelayAlreadyExists { + relayAddErrorTitle = NSLocalizedString("Duplicate relay", comment: "Title of the duplicate relay error message.") + relayAddErrorMessage = NSLocalizedString("The relay you are trying to add is already added.\nYou're all set!", comment: "An error message that appears when the user attempts to add a relay that has already been added.") + return + } catch { + return + } + + state.pool.connect(to: [new_relay]) + + guard let new_ev = add_relay(ev: ev, keypair: keypair, current_relays: state.pool.our_descriptors, relay: new_relay, info: info) else { + return + } + + process_contact_event(state: state, ev: ev) + + state.pool.send(.event(new_ev)) + + new_relay = "" + + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + + dismiss() + }) { + HStack { + Text(verbatim: "Add relay") + .bold() } + .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) } + .buttonStyle(GradientButtonStyle(padding: 10)) + //.disabled(!new_relay.isValidURL) <--- TODO + .padding(.vertical) + + Spacer() + } + .padding() + } +} + +// TODO +// This works sometimes, in certain cases where the relay is valid it won't allow the user to add it +// Needs improvement +extension String { + var isValidURL: Bool { + let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) { + // it is a link, if the match covers the whole string + return match.range.length == self.utf16.count + } else { + return false } } } @@ -43,6 +158,6 @@ struct AddRelayView_Previews: PreviewProvider { @State static var relay: String = "" static var previews: some View { - AddRelayView(relay: $relay) + AddRelayView(state: test_damus_state()) } } diff --git a/damus/Views/Relays/RecommendedRelayView.swift b/damus/Views/Relays/RecommendedRelayView.swift @@ -12,64 +12,50 @@ struct RecommendedRelayView: View { let relay: String let add_button: Bool + @ObservedObject private var model_cache: RelayModelCache + @Binding var showActionButtons: Bool init(damus: DamusState, relay: String, add_button: Bool = true, showActionButtons: Binding<Bool>) { self.damus = damus self.relay = relay self.add_button = add_button + self.model_cache = damus.relay_model_cache self._showActionButtons = showActionButtons } - var body: some View { - ZStack { - HStack { - if let keypair = damus.keypair.to_full() { - if showActionButtons && add_button { - AddButton(keypair: keypair, showText: false) - } - } - - RelayType(is_paid: damus.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false) - - Text(relay).layoutPriority(1) - - if let meta = damus.relay_model_cache.model(with_relay_id: relay)?.metadata { - NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta)){ - EmptyView() - } - .opacity(0.0) - .disabled(showActionButtons) - - Spacer() - - Image("info") - .resizable() - .frame(width: 20, height: 20) - .foregroundColor(Color.accentColor) - } else { - Spacer() - - Image("question") - .resizable() - .frame(width: 20, height: 20) - .foregroundColor(.gray) - } + var recommended: [RelayDescriptor] { + let rs: [RelayDescriptor] = [] + return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in + if damus.pool.get_relay(x) == nil, let url = RelayURL(x) { + xs.append(RelayDescriptor(url: url, info: .rw)) } } - .swipeActions { - if add_button { - if let keypair = damus.keypair.to_full() { - AddButton(keypair: keypair, showText: false) - .tint(.accentColor) + } + + var body: some View { + VStack { + let meta = model_cache.model(with_relay_id: relay)?.metadata + + RelayPicView(relay: relay, icon: meta?.icon, size: 70, highlight: .none, disable_animation: false) + if let meta = damus.relay_model_cache.model(with_relay_id: relay)?.metadata { + NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta)){ + EmptyView() } + .opacity(0.0) + .disabled(showActionButtons) + } + + HStack { + Text(meta?.name ?? relay.hostname ?? relay) + .lineLimit(1) + } + .contextMenu { + CopyAction(relay: relay) } - } - .contextMenu { - CopyAction(relay: relay) if let keypair = damus.keypair.to_full() { - AddButton(keypair: keypair, showText: true) + AddButton(keypair: keypair) } } } @@ -82,19 +68,14 @@ struct RecommendedRelayView: View { } } - func AddButton(keypair: FullKeypair, showText: Bool) -> some View { + func AddButton(keypair: FullKeypair) -> some View { Button(action: { add_action(keypair: keypair) }) { - if showText { - Text(NSLocalizedString("Connect", comment: "Button to connect to recommended relay server.")) - } - Image("plus-circle") - .resizable() - .frame(width: 20, height: 20) - .foregroundColor(.accentColor) - .padding(.leading, 5) + Text(NSLocalizedString("Add", comment: "Button to add relay server to list.")) + .padding(10) } + .buttonStyle(NeutralButtonStyle()) } func add_action(keypair: FullKeypair) { diff --git a/damus/Views/Relays/RelayConfigView.swift b/damus/Views/Relays/RelayConfigView.swift @@ -9,10 +9,9 @@ import SwiftUI struct RelayConfigView: View { let state: DamusState - @State var new_relay: String = "" @State var relays: [RelayDescriptor] @State private var showActionButtons = false - @State var relayAddErrorMessage: String? = nil + @State var show_add_relay: Bool = false @Environment(\.dismiss) var dismiss @@ -41,118 +40,69 @@ struct RelayConfigView: View { } var MainContent: some View { - Form { - Section { - AddRelayView(relay: $new_relay) - } header: { - HStack { - Text(NSLocalizedString("Connect To Relay", comment: "Label for section for adding a relay server.")) - .font(.system(size: 18, weight: .heavy)) - .padding(.bottom, 5) - } - } footer: { + VStack { + Divider() + + if recommended.count > 0 { VStack { - HStack { - Spacer() - if !new_relay.isEmpty { - Button(NSLocalizedString("Cancel", comment: "Button to cancel out of view adding user inputted relay.")) { - new_relay = "" - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - } - .font(.system(size: 14, weight: .bold)) - .frame(width: 80, height: 30) - .foregroundColor(.white) - .background(LINEAR_GRADIENT) - .clipShape(Capsule()) - .padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0)) - - Button(NSLocalizedString("Add", comment: "Button to confirm adding user inputted relay.")) { - - if new_relay.starts(with: "wss://") == false && new_relay.starts(with: "ws://") == false { - new_relay = "wss://" + new_relay - } - - if new_relay.hasSuffix("/") { - new_relay.removeLast(); - } - - guard let url = RelayURL(new_relay), - let ev = state.contacts.event, - let keypair = state.keypair.to_full() else { - return - } - - let info = RelayInfo.rw - let descriptor = RelayDescriptor(url: url, info: info) - - do { - try state.pool.add_relay(descriptor) - relayAddErrorMessage = nil // Clear error message - } catch RelayError.RelayAlreadyExists { - relayAddErrorMessage = NSLocalizedString("This relay is already in your list", comment: "An error message that appears when the user attempts to add a relay that has already been added.") - return - } catch { - return - } - - state.pool.connect(to: [new_relay]) - - guard let new_ev = add_relay(ev: ev, keypair: keypair, current_relays: state.pool.our_descriptors, relay: new_relay, info: info) else { - return - } - - process_contact_event(state: state, ev: ev) - - state.pool.send(.event(new_ev)) - - new_relay = "" - - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - } - .font(.system(size: 14, weight: .bold)) - .frame(width: 80, height: 30) - .foregroundColor(.white) - .background(LINEAR_GRADIENT) - .clipShape(Capsule()) - .padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0)) + Text("Recommended relays") + .foregroundStyle(DamusLightGradient.gradient) + .padding(10) + .background { + RoundedRectangle(cornerRadius: 15) + .stroke(DamusLightGradient.gradient) } - } - if let errorMessage = relayAddErrorMessage { - HStack { - Spacer() - Text(errorMessage) - .foregroundColor(Color.red) + .padding(.vertical) + + HStack(spacing: 20) { + ForEach(recommended, id: \.url) { r in + RecommendedRelayView(damus: state, relay: r.url.id, showActionButtons: $showActionButtons) } } + .padding() } - } - - Section { - List(Array(relays), id: \.url) { relay in - RelayView(state: state, relay: relay.url.id, showActionButtons: $showActionButtons) - } - } header: { - HStack { - Text(NSLocalizedString("Connected Relays", comment: "Section title for relay servers that are connected.")) - .font(.system(size: 18, weight: .heavy)) - .padding(.bottom, 5) + .frame(minWidth: 250, maxWidth: .infinity, minHeight: 250, alignment: .center) + .background { + RoundedRectangle(cornerRadius: 12) + .fill(DamusLightGradient.gradient.opacity(0.15), strokeBorder: DamusLightGradient.gradient, lineWidth: 1) } + .padding(.horizontal) } - if recommended.count > 0 { - Section { - List(recommended, id: \.url) { r in - RecommendedRelayView(damus: state, relay: r.url.id, showActionButtons: $showActionButtons) + HStack { + Text(NSLocalizedString("My Relays", comment: "Section title for relay servers that the user is connected to.")) + .font(.system(size: 32, weight: .bold)) + + Spacer() + + Button(action: { + show_add_relay.toggle() + }) { + HStack { + Text(verbatim: "Add relay") + .padding(10) } - } header: { - Text(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration")) - .font(.system(size: 18, weight: .heavy)) - .padding(.bottom, 5) } + .buttonStyle(NeutralButtonStyle()) } + .padding(25) + + List(Array(relays), id: \.url) { relay in + RelayView(state: state, relay: relay.url.id, showActionButtons: $showActionButtons) + } + .listStyle(PlainListStyle()) } .navigationTitle(NSLocalizedString("Relays", comment: "Title of relays view")) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) + .sheet(isPresented: $show_add_relay, onDismiss: { self.show_add_relay = false }) { + if #available(iOS 16.0, *) { + AddRelayView(state: state) + .presentationDetents([.height(300)]) + .presentationDragIndicator(.visible) + } else { + AddRelayView(state: state) + } + } .toolbar { if state.keypair.privkey != nil { if showActionButtons { diff --git a/damus/Views/Relays/RelayStatusView.swift b/damus/Views/Relays/RelayStatusView.swift @@ -13,21 +13,51 @@ struct RelayStatusView: View { var body: some View { Group { if connection.isConnecting { - ProgressView() + Text("Connecting") + .font(.caption) + .frame(height: 20) + .padding(.horizontal, 10) + .foregroundColor(DamusColors.warning) + .background(DamusColors.warningQuaternary) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(DamusColors.warningBorder, lineWidth: 1) + ) + } else if connection.isConnected { + Text("Online") + .font(.caption) + .frame(height: 20) + .padding(.horizontal, 10) + .foregroundColor(DamusColors.success) + .background(DamusColors.successQuaternary) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(DamusColors.successBorder, lineWidth: 1) + ) } else { - Image(connection.isConnected ? "globe" : "warning.fill") - .resizable() - .foregroundColor(connection.isConnected ? .green : .red) + Text("Error") + .font(.caption) + .frame(height: 20) + .padding(.horizontal, 10) + .foregroundColor(DamusColors.danger) + .background(DamusColors.dangerQuaternary) + .border(DamusColors.dangerBorder) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(DamusColors.dangerBorder, lineWidth: 1) + ) } } - .frame(width: 20, height: 20) - .padding(.trailing, 5) + .padding(.trailing, 20) } } struct RelayStatusView_Previews: PreviewProvider { static var previews: some View { - let connection = test_damus_state().pool.get_relay("relay")!.connection + let connection = test_damus_state().pool.get_relay("wss://relay.damus.io")!.connection RelayStatusView(connection: connection) } } diff --git a/damus/Views/Relays/RelayView.swift b/damus/Views/Relays/RelayView.swift @@ -28,41 +28,35 @@ struct RelayView: View { if showActionButtons { RemoveButton(privkey: privkey, showText: false) } - else if let relay_connection { - RelayStatusView(connection: relay_connection) - } } - - RelayType(is_paid: state.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false) - - if let meta = model_cache.model(with_relay_id: relay)?.metadata { - Text(relay) - .background( - NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta), label: { - EmptyView() - }).opacity(0.0).disabled(showActionButtons) - ) - - Spacer() - Image("info") - .resizable() - .frame(width: 20, height: 20) - .foregroundColor(Color.accentColor) - } else { + let meta = model_cache.model(with_relay_id: relay)?.metadata + + RelayPicView(relay: relay, icon: meta?.icon, size: 55, highlight: .none, disable_animation: false) + + VStack(alignment: .leading) { + HStack { + Text(meta?.name ?? relay) + .font(.headline) + .padding(.bottom, 2) + RelayType(is_paid: state.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false) + } Text(relay) + .font(.subheadline) + .foregroundColor(.gray) + } + + Spacer() + + if let relay_connection { + RelayStatusView(connection: relay_connection) .background( - NavigationLink(value: Route.RelayDetail(relay: relay, metadata: nil), label: { + NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta), label: { EmptyView() - }).opacity(0.0).disabled(showActionButtons) + }) + .buttonStyle(.plain) + .disabled(showActionButtons) ) - - Spacer() - - Image("question") - .resizable() - .frame(width: 20, height: 20) - .foregroundColor(.gray) } } }