diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index dec49e2c..763cd9b0 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -304,6 +304,7 @@ 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; + D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; }; E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; @@ -753,6 +754,7 @@ 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = ""; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = ""; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = ""; }; + D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = ""; }; E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = ""; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = ""; }; @@ -1155,6 +1157,7 @@ 50B5685229F97CB400A23243 /* CredentialHandler.swift */, 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */, 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */, + D2277EE92A089BD5006C3807 /* Router.swift */, ); path = Util; sourceTree = ""; @@ -1307,7 +1310,9 @@ 4CE6DEE427F7A08100C66700 /* Products */, 4CEE2AE62804F57B00AB5EEF /* Frameworks */, ); + indentWidth = 4; sourceTree = ""; + tabWidth = 4; }; 4CE6DEE427F7A08100C66700 /* Products */ = { isa = PBXGroup; @@ -1861,6 +1866,7 @@ 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */, 4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */, 3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */, + D2277EEA2A089BD5006C3807 /* Router.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */, 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */, @@ -2254,6 +2260,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2302,6 +2309,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/damus/ContentView.swift b/damus/ContentView.swift index fae00f24..c4fccbf3 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -97,6 +97,7 @@ struct ContentView: View { @SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies @State private var isSideBarOpened = false var home: HomeModel = HomeModel() + @StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator() let sub_id = UUID().description @@ -287,7 +288,7 @@ struct ContentView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { if let damus = self.damus_state { - NavigationView { + NavigationStack(path: $navigationCoordinator.path) { TabView { // Prevents navbar appearance change on scroll MainContent(damus: damus) .toolbar() { @@ -327,6 +328,12 @@ struct ContentView: View { .overlay( SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation()) ) + .navigationDestination(for: Route.self) { route in + route.view(navigationCordinator: navigationCoordinator) + } + .onReceive(handle_notify(.switched_timeline)) { _ in + navigationCoordinator.popToRoot() + } } .navigationViewStyle(.stack) diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift new file mode 100644 index 00000000..c6b599a7 --- /dev/null +++ b/damus/Util/Router.swift @@ -0,0 +1,115 @@ +// +// Router.swift +// damus +// +// Created by Scott Penrose on 5/7/23. +// + +import SwiftUI + +enum Route: Hashable { + case Profile(damusSate: DamusState, profile: ProfileModel, followers: FollowersModel) + case Followers(damusState: DamusState, environmentObject: FollowersModel) + case Relay(damusState: DamusState, relay: String, showActionButtons: Binding) + case Following(damusState: DamusState, following: FollowingModel) + case MuteList(damusState: DamusState, users: [String]) + case RelayConfig(damusState: DamusState) + case Bookmarks(damusState: DamusState) + case Config(damusState: DamusState) + case EditMetadata(damusState: DamusState) + case DMChat(damusState: DamusState, dms: DirectMessageModel) + case UserRelays(damusState: DamusState, relays: [String]) + + @ViewBuilder + func view(navigationCordinator: NavigationCoordinator) -> some View { + switch self { + case .Profile (let damusState, let profile, let followers): + ProfileView(damus_state: damusState, profile: profile, followers: followers) + case .Followers (let damusState, let environmentObject): + FollowersView(damus_state: damusState) + .environmentObject(environmentObject) + case .Relay (let damusState, let relay, let showActionButtons): + RelayView(state: damusState, relay: relay, showActionButtons: showActionButtons) + case .Following(let damusState, let following): + FollowingView(damus_state: damusState, following: following) + case .MuteList(let damusState, let users): + MutelistView(damus_state: damusState, users: users) + case .RelayConfig(let damusState): + RelayConfigView(state: damusState) + case .Bookmarks(let damusState): + BookmarksView(state: damusState) + case .Config(let damusState): + ConfigView(state: damusState) + case .EditMetadata(let damusState): + EditMetadataView(damus_state: damusState) + case .DMChat(let damusState, let dms): + DMChatView(damus_state: damusState, dms: dms) + case .UserRelays(let damusState, let relays): + UserRelaysView(state: damusState, relays: relays) + } + } + + static func == (lhs: Route, rhs: Route) -> Bool { + switch (lhs, rhs) { + case (.Profile (_, let lhs_profile, _), .Profile(_, let rhs_profile, _)): + return lhs_profile == rhs_profile + case (.Followers (_, _), .Followers (_, _)): + return true + case (.Relay (_, let lhs_relay, _), .Relay (_, let rhs_relay, _)): + return lhs_relay == rhs_relay + case (.Following(_, _), .Following(_, _)): + return true + case (.MuteList(_, let lhs_users), .MuteList(_, let rhs_users)): + return lhs_users == rhs_users + case (.RelayConfig(_), .RelayConfig(_)): + return true + case (.Bookmarks(_), .Bookmarks(_)): + return true + case (.Config(_), .Config(_)): + return true + case (.EditMetadata(_), .EditMetadata(_)): + return true + case (.DMChat(_, let lhs_dms), .DMChat(_, let rhs_dms)): + return lhs_dms.our_pubkey == rhs_dms.our_pubkey + case (.UserRelays(_, let lhs_relays), .UserRelays(_, let rhs_relays)): + return lhs_relays == rhs_relays + default: + return false + } + } + + func hash(into hasher: inout Hasher) { + switch self { + case .Profile(_, let profile, _): + hasher.combine(profile.pubkey) + case .Followers(_, _): + hasher.combine("followers") + case .Relay(_, let relay, _): + hasher.combine(relay) + case .Following(_, _): + hasher.combine("following") + case .MuteList(_, let users): + hasher.combine(users) + case .RelayConfig(_): + hasher.combine("relayConfig") + case .Bookmarks(_): + hasher.combine("bookmarks") + case .Config(_): + hasher.combine("config") + case .EditMetadata(_): + hasher.combine("editMetadata") + case .DMChat(_, let dms): + hasher.combine(dms.our_pubkey) + case .UserRelays(_, let relays): + hasher.combine(relays) + } + } +} + +class NavigationCoordinator: ObservableObject { + @Published var path = [Route]() + + func popToRoot() { + path = [] + } +} diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index 714c1512..ff2f010a 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -77,7 +77,7 @@ struct EditButton: View { @Environment(\.colorScheme) var colorScheme var body: some View { - NavigationLink(destination: EditMetadataView(damus_state: damus_state)) { + NavigationLink(value: Route.EditMetadata(damusState: damus_state)) { Text("Edit", comment: "Button to edit user's profile.") .frame(height: 30) .padding(.horizontal,25) @@ -300,8 +300,7 @@ struct ProfileView: View { var dmButton: some View { let dm_model = damus_state.dms.lookup_or_create(profile.pubkey) - let dmview = DMChatView(damus_state: damus_state, dms: dm_model) - return NavigationLink(destination: dmview) { + return NavigationLink(value: Route.DMChat(damusState: damus_state, dms: dm_model)) { Image("messages") .profile_button_style(scheme: colorScheme) } @@ -325,7 +324,7 @@ struct ProfileView: View { follow_state: damus_state.contacts.follow_state(profile.pubkey) ) } else if damus_state.keypair.privkey != nil { - NavigationLink(destination: EditMetadataView(damus_state: damus_state)) { + NavigationLink(value: Route.EditMetadata(damusState: damus_state)) { EditButton(damus_state: damus_state) } } @@ -404,7 +403,7 @@ struct ProfileView: View { if let contact = profile.contacts { let contacts = contact.referenced_pubkeys.map { $0.ref_id } let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) - NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model)) { + NavigationLink(value: Route.Following(damusState: damus_state, following: following_model)) { HStack { let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray) Text("\(Text(verbatim: profile.following.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") @@ -412,10 +411,9 @@ struct ProfileView: View { } .buttonStyle(PlainButtonStyle()) } - let fview = FollowersView(damus_state: damus_state) - .environmentObject(followers) + if followers.contacts != nil { - NavigationLink(destination: fview) { + NavigationLink(value: Route.Followers(damusState: damus_state, environmentObject: followers)) { followersCount } .buttonStyle(PlainButtonStyle()) @@ -433,12 +431,12 @@ struct ProfileView: View { let noun_text = Text(verbatim: relaysCountString(relays.keys.count)).font(.subheadline).foregroundColor(.gray) let relay_text = Text("\(Text(verbatim: relays.keys.count.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.") if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user { - NavigationLink(destination: RelayConfigView(state: damus_state)) { + NavigationLink(value: Route.RelayConfig(damusState: damus_state)) { relay_text } .buttonStyle(PlainButtonStyle()) } else { - NavigationLink(destination: UserRelaysView(state: damus_state, relays: Array(relays.keys).sorted())) { + NavigationLink(value: Route.UserRelays(damusState: damus_state, relays: Array(relays.keys).sorted())) { relay_text } .buttonStyle(PlainButtonStyle()) diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift index d89096f8..cb83f94a 100644 --- a/damus/Views/SideMenuView.swift +++ b/damus/Views/SideMenuView.swift @@ -45,7 +45,7 @@ struct SideMenuView: View { func SidemenuItems(profile_model: ProfileModel, followers: FollowersModel) -> some View { return VStack(spacing: verticalSpacing) { - NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) { + NavigationLink(value: Route.Profile(damusSate: damus_state, profile: profile_model, followers: followers)) { navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), img: "user") } @@ -64,19 +64,19 @@ struct SideMenuView: View { }*/ } - NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) { + NavigationLink(value: Route.MuteList(damusState: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist))) { navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute") } - NavigationLink(destination: RelayConfigView(state: damus_state)) { + NavigationLink(value: Route.RelayConfig(damusState: damus_state)) { navLabel(title: NSLocalizedString("Relays", comment: "Sidebar menu label for Relays view."), img: "world-relays") } - NavigationLink(destination: BookmarksView(state: damus_state)) { + NavigationLink(value: Route.Bookmarks(damusState: damus_state)) { navLabel(title: NSLocalizedString("Bookmarks", comment: "Sidebar menu label for Bookmarks view."), img: "bookmark") } - NavigationLink(destination: ConfigView(state: damus_state)) { + NavigationLink(value: Route.Config(damusState: damus_state)) { navLabel(title: NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), img: "settings") } } @@ -126,15 +126,15 @@ struct SideMenuView: View { ZStack(alignment: .top) { fillColor() .ignoresSafeArea() - + VStack(alignment: .leading, spacing: 0) { MainSidemenu .simultaneousGesture(TapGesture().onEnded { isSidebarVisible = false }) - + Divider() - + HStack() { Button(action: { //ConfigView(state: damus_state) @@ -150,9 +150,9 @@ struct SideMenuView: View { .frame(maxWidth: .infinity, alignment: .leading) .dynamicTypeSize(.xSmall) }) - + Spacer() - + Button(action: { showQRCode.toggle() }, label: {