diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 82968477f..e14ab2151 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,10 @@ For now `main` is the main branch and code improvements are made in topic branch A maintainer will review your code and merge it when it has the required number of approvals. +## Hot Reloading + +We make use of the [Inject](https://github.com/krzysztofzablocki/Inject) framework for hot reloading debug builds. To set up hot reloading, follow the [documentation](https://github.com/krzysztofzablocki/Inject?tab=readme-ov-file#individual-developer-setup-once-per-machine). + ## Dependency Management We prefer to install dependencies using the Swift Package Manager. diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 846269e0a..614188eef 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -423,7 +423,7 @@ C987F85429BA951E00B44E7A /* ClarityCity-Thin.otf in Resources */ = {isa = PBXBuildFile; fileRef = C987F83129BA951E00B44E7A /* ClarityCity-Thin.otf */; }; C987F85529BA951E00B44E7A /* ClarityCity-Thin.otf in Resources */ = {isa = PBXBuildFile; fileRef = C987F83129BA951E00B44E7A /* ClarityCity-Thin.otf */; }; C987F85B29BA9ED800B44E7A /* Font+Clarity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C987F85729BA981800B44E7A /* Font+Clarity.swift */; }; - C98905A22CD3B8CF00C17EE0 /* Inject in Frameworks */ = {isa = PBXBuildFile; productRef = C98905A12CD3B8CF00C17EE0 /* Inject */; }; + C9887D812D1EF3C400CF9101 /* Inject in Frameworks */ = {isa = PBXBuildFile; productRef = C98905A12CD3B8CF00C17EE0 /* Inject */; }; C98A32272A05795E00E3FA13 /* Task+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98A32262A05795E00E3FA13 /* Task+Timeout.swift */; }; C98A32282A05795E00E3FA13 /* Task+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98A32262A05795E00E3FA13 /* Task+Timeout.swift */; }; C98B8B4029FBF83B009789C8 /* NotificationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98B8B3F29FBF83B009789C8 /* NotificationCard.swift */; }; @@ -1041,6 +1041,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C9887D812D1EF3C400CF9101 /* Inject in Frameworks */, C9DEC068298965270078B43A /* Starscream in Frameworks */, C9FD35132BCED5A6008F8D95 /* NostrSDK in Frameworks */, C9B71DC02A8E9BAD0031ED9F /* SentrySwiftUI in Frameworks */, @@ -2899,6 +2900,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ""; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; @@ -3070,6 +3072,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -3120,6 +3123,11 @@ LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; MACOSX_DEPLOYMENT_TARGET = 13.3; MARKETING_VERSION = 1.0.0; + OTHER_LDFLAGS = ""; + "OTHER_LDFLAGS[sdk=iphonesimulator*]" = ( + "-Xlinker", + "-interposable", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.verse.streams-dev"; PRODUCT_MODULE_NAME = Nos; PRODUCT_NAME = "$(TARGET_NAME) Dev"; @@ -3288,6 +3296,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -3346,6 +3355,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ""; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; diff --git a/Nos.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nos.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7590212f9..6ebc64605 100644 --- a/Nos.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nos.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "f8bae06dbeb84f5992ca8659ee3a2a75550fe8b31994d37a658183a21f1c8131", + "originHash" : "62d037ae150ed2e6e150f06b42119c5158134318243e742971b3ccfa3a3a5323", "pins" : [ { "identity" : "bech32", @@ -37,6 +37,15 @@ "version" : "1.8.2" } }, + { + "identity" : "inject", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzysztofzablocki/Inject.git", + "state" : { + "revision" : "728c56639ecb3df441d51d5bc6747329afabcfc9", + "version" : "1.5.2" + } + }, { "identity" : "libwebp-xcode", "kind" : "remoteSourceControl", diff --git a/Nos/Views/AppView.swift b/Nos/Views/AppView.swift index fd3816d2d..7c503237a 100644 --- a/Nos/Views/AppView.swift +++ b/Nos/Views/AppView.swift @@ -5,7 +5,7 @@ import Inject struct AppView: View { @State var showNewPost = false - @State var newPostContents: String? + @State var newPostContents: String? @Environment(AppController.self) var appController @EnvironmentObject private var router: Router @@ -21,15 +21,126 @@ struct AppView: View { var body: some View { ZStack { - switch appController.currentState { - case .loading: - SplashScreenView() - .ignoresSafeArea() - case .onboarding: + if appController.currentState == .onboarding { OnboardingView(completion: appController.completeOnboarding) - case .loggedIn: - tabView - + } else { + TabView(selection: $router.selectedTab) { + if let author = currentUser.author { + HomeTab(user: author) + .tabItem { + VStack { + let text = Text("homeFeed") + if $router.selectedTab.wrappedValue == .home { + Image.tabIconHomeSelected + text + } else { + Image.tabIconHome + text.foregroundColor(.secondaryTxt) + } + } + } + .toolbarBackground(.visible, for: .tabBar) + .toolbarBackground(Color.cardBgBottom, for: .tabBar) + .tag(AppDestination.home) + .onAppear { + // TODO: Move this somewhere better like CurrentUser when it becomes the source of truth + // for who is logged in + if let keyPair = currentUser.keyPair { + analytics.identify( + with: keyPair, + nip05: currentUser.author?.nip05 + ) + crashReporting.identify(with: keyPair) + } + } + } + + DiscoverTab() + .tabItem { + VStack { + let text = Text("discover") + if $router.selectedTab.wrappedValue == .discover { + Image.tabIconEveryoneSelected + text.foregroundColor(.primaryTxt) + } else { + Image.tabIconEveryone + text.foregroundColor(.secondaryTxt) + } + } + } + .toolbarBackground(.visible, for: .tabBar) + .toolbarBackground(Color.cardBgBottom, for: .tabBar) + .tag(AppDestination.discover) + + VStack {} + .tabItem { + VStack { + Image.newPostButton + Text("post") + } + } + .tag(AppDestination.noteComposer(nil)) + + NotificationsView(user: currentUser.author) + .tabItem { + VStack { + let text = Text("notifications") + if $router.selectedTab.wrappedValue == .notifications { + Image.tabIconNotificationsSelected + text.foregroundColor(.primaryTxt) + } else { + Image.tabIconNotifications + text.foregroundColor(.secondaryTxt) + } + } + } + .toolbarBackground(.visible, for: .tabBar) + .toolbarBackground(Color.cardBgBottom, for: .tabBar) + .tag(AppDestination.notifications) + .badge(pushNotificationService.badgeCount) + + if let author = currentUser.author { + ProfileTab(author: author, path: $router.profilePath) + .tabItem { + VStack { + let text = Text("profileTitle") + if $router.selectedTab.wrappedValue == .profile { + Image.tabProfileSelected + text.foregroundColor(.primaryTxt) + } else { + Image.tabProfile + text.foregroundColor(.secondaryTxt) + } + } + } + .toolbarBackground(.visible, for: .tabBar) + .toolbarBackground(Color.cardBgBottom, for: .tabBar) + .tag(AppDestination.profile) + } + } + .onChange(of: router.selectedTab) { _, newTab in + if case let AppDestination.noteComposer(contents) = newTab { + newPostContents = contents + showNewPost = true + router.selectedTab = lastSelectedTab + } else if !showNewPost { + lastSelectedTab = newTab + } + } + .overlay { + if router.isLoading { + ZStack { + Rectangle().fill(.black.opacity(0.4)) + ProgressView() + } + } + } + .sheet(isPresented: $showNewPost, content: { + NoteComposer(initialContents: newPostContents, isPresented: $showNewPost) + .environment(currentUser) + .interactiveDismissDisabled() + }) + SideMenu( menuWidth: 300, menuOpened: router.sideMenuOpened, @@ -51,99 +162,6 @@ struct AppView: View { .enableInjection() } - private var tabView: some View { - TabView(selection: $router.selectedTab) { - if let author = currentUser.author { - HomeTab(user: author) - .tabItem { - let text = Text("Latest") - if $router.selectedTab.wrappedValue == .home { - Image(systemName: "sparkles") - text.foregroundColor(.primaryTxt) - } else { - Image(systemName: "sparkles") - text.foregroundColor(.secondaryTxt) - } - } - .toolbarBackground(.visible, for: .tabBar) - .toolbarBackground(Color.cardBgBottom, for: .tabBar) - .tag(AppDestination.home) - .onAppear { - // TODO: Move this somewhere better like CurrentUser when it becomes the source of truth - // for who is logged in - if let keyPair = currentUser.keyPair { - analytics.identify( - with: keyPair, - nip05: currentUser.author?.nip05 - ) - crashReporting.identify(with: keyPair) - } - } - } - - DiscoverTab() - .tabItem { - VStack { - let text = Text("discover") - if $router.selectedTab.wrappedValue == .discover { - Image.tabIconEveryoneSelected - text.foregroundColor(.primaryTxt) - } else { - Image.tabIconEveryone - text.foregroundColor(.secondaryTxt) - } - } - } - .toolbarBackground(.visible, for: .tabBar) - .toolbarBackground(Color.cardBgBottom, for: .tabBar) - .tag(AppDestination.discover) - - if let author = currentUser.author { - MyStreamsView(author: author) - .tabItem { - VStack { - let text = Text("My Streams") - if $router.selectedTab.wrappedValue == .myStreams { - Image.tabProfileSelected - text.foregroundColor(.primaryTxt) - } else { - Image.tabProfile - text.foregroundColor(.secondaryTxt) - } - } - } - .toolbarBackground(.visible, for: .tabBar) - .toolbarBackground(Color.cardBgBottom, for: .tabBar) - .tag(AppDestination.myStreams) - } - } - .onChange(of: router.selectedTab) { _, newTab in - if case let AppDestination.noteComposer(contents) = newTab { - newPostContents = contents - showNewPost = true - router.selectedTab = lastSelectedTab - } else if !showNewPost { - lastSelectedTab = newTab - } - } - .overlay { - if router.isLoading { - ZStack { - Rectangle().fill(.black.opacity(0.4)) - ProgressView() - } - } - } - .sheet(isPresented: $showNewPost) { - NoteComposer(initialContents: newPostContents, isPresented: $showNewPost) - .environment(currentUser) - .interactiveDismissDisabled() - } - } -} - -extension AppView { - private func presentNIP05SheetIfNeeded() async { guard let author = currentUser.author, let npub = author.npubString else { return