diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 4e0965677..62e438af9 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ 504106CD2A744D7F000AAEF8 /* CommentRepository+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504106CC2A744D7F000AAEF8 /* CommentRepository+Dependency.swift */; }; 504ECBAE2AB45B2A006C0B96 /* LemmyURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504ECBAD2AB45B2A006C0B96 /* LemmyURL.swift */; }; 504ECBB12AB4B101006C0B96 /* LemmyURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504ECBB02AB4B101006C0B96 /* LemmyURLTests.swift */; }; + 504ECBAA2AB27C73006C0B96 /* LandingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504ECBA92AB27C73006C0B96 /* LandingPage.swift */; }; + 504ECBAC2AB27CB1006C0B96 /* OnboardingRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504ECBAB2AB27CB1006C0B96 /* OnboardingRoute.swift */; }; 505240E32A86916500EA4558 /* FavoriteCommunitiesTracker+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505240E22A86916500EA4558 /* FavoriteCommunitiesTracker+Dependency.swift */; }; 505240E52A86E32700EA4558 /* CommunityListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505240E42A86E32700EA4558 /* CommunityListModel.swift */; }; 505240E72A88D36D00EA4558 /* SectionIndexTitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505240E62A88D36D00EA4558 /* SectionIndexTitles.swift */; }; @@ -384,7 +386,7 @@ CDDCF6512A677E1B003DA3AC /* FancyTabItemPreferenceKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDCF6502A677E1B003DA3AC /* FancyTabItemPreferenceKeys.swift */; }; CDDCF6532A677F45003DA3AC /* TabSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDCF6522A677F45003DA3AC /* TabSelection.swift */; }; CDDCF6572A678298003DA3AC /* FancyTabBarSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDDCF6562A678298003DA3AC /* FancyTabBarSelection.swift */; }; - CDE3BA872A8C25B000B972E2 /* Onboarding View.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE3BA862A8C25B000B972E2 /* Onboarding View.swift */; }; + CDE3BA872A8C25B000B972E2 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE3BA862A8C25B000B972E2 /* OnboardingView.swift */; }; CDE3BA892A8C64BD00B972E2 /* Collapsible Text Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE3BA882A8C64BD00B972E2 /* Collapsible Text Item.swift */; }; CDE6A80B2A43E9F00062D161 /* CommentSortType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A80A2A43E9F00062D161 /* CommentSortType.swift */; }; CDE6A80D2A45EAB30062D161 /* Embedded Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A80C2A45EAB30062D161 /* Embedded Post.swift */; }; @@ -489,6 +491,8 @@ 504106CC2A744D7F000AAEF8 /* CommentRepository+Dependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CommentRepository+Dependency.swift"; sourceTree = ""; }; 504ECBAD2AB45B2A006C0B96 /* LemmyURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LemmyURL.swift; sourceTree = ""; }; 504ECBB02AB4B101006C0B96 /* LemmyURLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LemmyURLTests.swift; sourceTree = ""; }; + 504ECBA92AB27C73006C0B96 /* LandingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingPage.swift; sourceTree = ""; }; + 504ECBAB2AB27CB1006C0B96 /* OnboardingRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingRoute.swift; sourceTree = ""; }; 505240E22A86916500EA4558 /* FavoriteCommunitiesTracker+Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteCommunitiesTracker+Dependency.swift"; sourceTree = ""; }; 505240E42A86E32700EA4558 /* CommunityListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityListModel.swift; sourceTree = ""; }; 505240E62A88D36D00EA4558 /* SectionIndexTitles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionIndexTitles.swift; sourceTree = ""; }; @@ -827,7 +831,7 @@ CDDCF6502A677E1B003DA3AC /* FancyTabItemPreferenceKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyTabItemPreferenceKeys.swift; sourceTree = ""; }; CDDCF6522A677F45003DA3AC /* TabSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSelection.swift; sourceTree = ""; }; CDDCF6562A678298003DA3AC /* FancyTabBarSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyTabBarSelection.swift; sourceTree = ""; }; - CDE3BA862A8C25B000B972E2 /* Onboarding View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Onboarding View.swift"; sourceTree = ""; }; + CDE3BA862A8C25B000B972E2 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; CDE3BA882A8C64BD00B972E2 /* Collapsible Text Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collapsible Text Item.swift"; sourceTree = ""; }; CDE6A80A2A43E9F00062D161 /* CommentSortType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentSortType.swift; sourceTree = ""; }; CDE6A80C2A45EAB30062D161 /* Embedded Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Embedded Post.swift"; sourceTree = ""; }; @@ -1078,6 +1082,16 @@ path = Model; sourceTree = ""; }; + 504ECBA82AB27C4C006C0B96 /* Onboarding */ = { + isa = PBXGroup; + children = ( + 504ECBA92AB27C73006C0B96 /* LandingPage.swift */, + CDE3BA862A8C25B000B972E2 /* OnboardingView.swift */, + 504ECBAB2AB27CB1006C0B96 /* OnboardingRoute.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; 5064D03B2A6DE05000B22EE3 /* Notifications */ = { isa = PBXGroup; children = ( @@ -1364,7 +1378,6 @@ CDCBD7292A8EC06D00387A2C /* Components */, 6332FDCE27EFDD2E0009A98A /* Accounts Page.swift */, 63F0C7B82A0533C700A18C5D /* Add Account View.swift */, - CDE3BA862A8C25B000B972E2 /* Onboarding View.swift */, CDCBD7252A8D69A200387A2C /* Instance Picker View.swift */, CDCBD7272A8D6B7700387A2C /* Instance Picker View Logic.swift */, CDC65D902A86B830007205E5 /* DeleteAccountView.swift */, @@ -1515,6 +1528,7 @@ 6363D5F327EE1BA900E34822 /* Views */ = { isa = PBXGroup; children = ( + 504ECBA82AB27C4C006C0B96 /* Onboarding */, 6386E03E2A04570F006B3C1D /* Shared */, 6363D5F427EE1BAE00E34822 /* Tabs */, ); @@ -2581,6 +2595,7 @@ 504ECBAE2AB45B2A006C0B96 /* LemmyURL.swift in Sources */, CDA217EA2A63093E00BDA173 /* ReportComment.swift in Sources */, CDA217E82A63029B00BDA173 /* ReportMention.swift in Sources */, + 504ECBAA2AB27C73006C0B96 /* LandingPage.swift in Sources */, 508845CF2A3641160088E483 /* JSONDecoder+Default.swift in Sources */, 637218672A3A2AAD008C4816 /* GetPersonDetails.swift in Sources */, B1A26FE12A44AAB200B91A32 /* Navigation getter.swift in Sources */, @@ -2721,6 +2736,7 @@ 637218432A3A2AAD008C4816 /* APIClient.swift in Sources */, CD82A2572A716D7C00111034 /* PersonRepository+Dependency.swift in Sources */, 63DF71F12A02999C002AC14E /* App Constants.swift in Sources */, + 504ECBAC2AB27CB1006C0B96 /* OnboardingRoute.swift in Sources */, CD82A2532A716B8100111034 /* PersonRepository.swift in Sources */, CD69F55F2A40121D0028D4F7 /* Ellipsis Menu.swift in Sources */, 638535712A1779BC00815781 /* GeneralSettingsView.swift in Sources */, @@ -2754,7 +2770,7 @@ CD6483A62A82FAF200A5AE84 /* ProfileTabLabel.swift in Sources */, CDEBC3252A9A57D200518D9D /* Content Type.swift in Sources */, 6386E0402A045723006B3C1D /* Website Icon Complex.swift in Sources */, - CDE3BA872A8C25B000B972E2 /* Onboarding View.swift in Sources */, + CDE3BA872A8C25B000B972E2 /* OnboardingView.swift in Sources */, 5064D0412A6E63E000B22EE3 /* Task+Notifiable.swift in Sources */, 63F0C7BD2A058CD200A18C5D /* Check if Endpoint Exists.swift in Sources */, E4D4DBA02A7C7B9D00C4F3DE /* Comments.swift in Sources */, diff --git a/Mlem/Views/Onboarding/LandingPage.swift b/Mlem/Views/Onboarding/LandingPage.swift new file mode 100644 index 000000000..cfc5b5e6f --- /dev/null +++ b/Mlem/Views/Onboarding/LandingPage.swift @@ -0,0 +1,63 @@ +// +// LandingPage.swift +// Mlem +// +// Created by mormaer on 14/09/2023. +// +// + +import SwiftUI + +struct LandingPage: View { + @State private var navigationPath = NavigationPath() + + var body: some View { + NavigationStack(path: $navigationPath) { + VStack(spacing: 40) { + Text("Welcome to Mlem!") + .bold() + + LogoView() + + VStack { + newUserButton + existingUserButton + } + } + .padding(.horizontal) + .frame(maxHeight: .infinity) + .navigationDestination(for: OnboardingRoute.self) { route in + switch route { + case .onboard: + OnboardingView(navigationPath: $navigationPath) + case let .login(url): + AddSavedInstanceView(onboarding: true, givenInstance: url?.absoluteString) + } + } + } + } + + @ViewBuilder + var newUserButton: some View { + Button { + navigationPath.append(OnboardingRoute.onboard) + } label: { + Text("I'm new here") + .padding(.vertical, 5) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + } + + @ViewBuilder + var existingUserButton: some View { + Button { + navigationPath.append(OnboardingRoute.login(nil)) + } label: { + Text("I have a Lemmy account") + .padding(.vertical, 5) + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + } +} diff --git a/Mlem/Views/Onboarding/OnboardingRoute.swift b/Mlem/Views/Onboarding/OnboardingRoute.swift new file mode 100644 index 000000000..24fd3f39d --- /dev/null +++ b/Mlem/Views/Onboarding/OnboardingRoute.swift @@ -0,0 +1,14 @@ +// +// OnboardingRoute.swift +// Mlem +// +// Created by mormaer on 14/09/2023. +// +// + +import Foundation + +enum OnboardingRoute: Hashable { + case onboard + case login(URL?) +} diff --git a/Mlem/Views/Shared/Accounts/Onboarding View.swift b/Mlem/Views/Onboarding/OnboardingView.swift similarity index 53% rename from Mlem/Views/Shared/Accounts/Onboarding View.swift rename to Mlem/Views/Onboarding/OnboardingView.swift index fa4673d78..d4602eba1 100644 --- a/Mlem/Views/Shared/Accounts/Onboarding View.swift +++ b/Mlem/Views/Onboarding/OnboardingView.swift @@ -1,5 +1,5 @@ // -// Onboarding View.swift +// OnboardingView.swift // Mlem // // Created by Eric Andrews on 2023-08-15. @@ -10,93 +10,41 @@ import SwiftUI struct OnboardingView: View { enum OnboardingTab { - case welcome, about, instances, addAccount + case about, instances } - @Binding var flow: AppFlow + @Binding var navigationPath: NavigationPath - @State var selectedTab: OnboardingTab = .welcome + @State var selectedTab: OnboardingTab = .about @State var hideNav: Bool = true @State var selectedInstance: InstanceMetadata? var body: some View { TabView(selection: $selectedTab) { - onboardingTab - .tag(OnboardingTab.welcome) - aboutTab .tag(OnboardingTab.about) instancesTab .tag(OnboardingTab.instances) - - AddSavedInstanceView(onboarding: true, givenInstance: selectedInstance?.url.absoluteString) - .tag(OnboardingTab.addAccount) } - .onChange(of: selectedInstance) { _ in - selectedTab = .addAccount + .onChange(of: selectedInstance) { instance in + guard let instanceUrl = instance?.url else { return } + navigationPath.append(OnboardingRoute.login(instanceUrl)) } .animation(.spring(response: 0.5), value: selectedTab) - .tabViewStyle(PageTabViewStyle()) - } - - // MARK: - Onboarding Tab - - @ViewBuilder - var onboardingTab: some View { - VStack(spacing: 40) { - Text("Welcome to Mlem!") - .bold() - - LogoView() - - VStack { - newUserButton - existingUserButton - } - } - .padding(.horizontal) - .frame(maxHeight: .infinity) - } - - @ViewBuilder - var newUserButton: some View { - Button { - selectedTab = .about - } label: { - Text("I'm new here") - .padding(.vertical, 5) - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) + .tabViewStyle(.page(indexDisplayMode: .always)) + .indexViewStyle(.page(backgroundDisplayMode: .always)) } - - @ViewBuilder - var existingUserButton: some View { - Button { - selectedTab = .addAccount - } label: { - Text("I have a Lemmy account") - .padding(.vertical, 5) - .frame(maxWidth: .infinity) - } - .buttonStyle(.bordered) - } - + // MARK: - About Tab @ViewBuilder var aboutTab: some View { ScrollView { VStack(spacing: 40) { - Group { - Text("What is Lemmy?") - .bold() - - Text(.init(whatIsLemmy)) - } - .padding() + Text(.init(whatIsLemmy)) + .padding() VStack(spacing: 0) { Divider() @@ -125,9 +73,10 @@ struct OnboardingView: View { // add a little space for the tab selection indicator Spacer() - .frame(height: 20) + .frame(height: 36) } } + .navigationTitle("What is Lemmy?") } @ViewBuilder @@ -146,5 +95,6 @@ struct OnboardingView: View { var instancesTab: some View { InstancePickerView(selectedInstance: $selectedInstance, onboarding: true) + .padding(.bottom, 36) } } diff --git a/Mlem/Views/Shared/Accounts/Accounts Page.swift b/Mlem/Views/Shared/Accounts/Accounts Page.swift index 899a9e56e..9d39a3ba3 100644 --- a/Mlem/Views/Shared/Accounts/Accounts Page.swift +++ b/Mlem/Views/Shared/Accounts/Accounts Page.swift @@ -25,22 +25,22 @@ struct AccountsPage: View { let instances = Array(accountsTracker.accountsByInstance.keys).sorted() Group { - if instances.isEmpty || isShowingInstanceAdditionSheet { + if instances.isEmpty { AddSavedInstanceView(onboarding: false) } else { List { ForEach(instances, id: \.self) { instance in Section(header: Text(instance)) { ForEach(accountsTracker.accountsByInstance[instance] ?? []) { account in - Button(account.username) { - dismiss() + Button(account.nickname) { setFlow(using: account) + dismiss() } + .disabled(isActiveAccount(account)) .swipeActions { Button("Remove", role: .destructive) { - dismiss() accountsTracker.removeAccount(account: account) - if account == appState.currentActiveAccount { + if isActiveAccount(account) { // if we just deleted the current account we (currently!) have a decision to make if let first = accountsTracker.savedAccounts.first { // if we have another account available, go to that... @@ -54,6 +54,8 @@ struct AccountsPage: View { // no accounts, so go to onboarding setFlow(using: nil) } + + dismiss() } } } @@ -93,15 +95,17 @@ struct AccountsPage: View { return account == currentAccount ? .secondary : .primary } + private func isActiveAccount(_ account: SavedAccount) -> Bool { + guard let currentAccount = appState.currentActiveAccount else { return false } + return account == currentAccount + } + private func setFlow(using account: SavedAccount?) { - // this tiny delay prevents the modal dismiss animation from being cancelled - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - if let account { - setFlow(.account(account)) - return - } - - setFlow(.onboarding) + if let account { + setFlow(.account(account)) + return } + + setFlow(.onboarding) } } diff --git a/Mlem/Views/Shared/Accounts/Add Account View.swift b/Mlem/Views/Shared/Accounts/Add Account View.swift index 9d4e7862a..419f861f0 100644 --- a/Mlem/Views/Shared/Accounts/Add Account View.swift +++ b/Mlem/Views/Shared/Accounts/Add Account View.swift @@ -79,7 +79,9 @@ struct AddSavedInstanceView: View { var body: some View { ScrollView { VStack { - title + if !onboarding { + title + } headerSection } Grid( @@ -89,7 +91,6 @@ struct AddSavedInstanceView: View { ) { formSection }.disabled(viewState == .loading) - footerView } .transaction { transaction in transaction.disablesAnimations = true @@ -97,6 +98,21 @@ struct AddSavedInstanceView: View { .alert(using: $errorAlert) { content in Alert(title: Text(content.title), message: Text(content.message)) } + .toolbar { + if onboarding { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + Task(priority: .userInitiated) { + await tryToAddAccount() + } + } label: { + Text("Submit") + }.disabled(!isReadyToSubmit) + } + } + } + .navigationTitle(Text(onboarding ? "Log in" : "")) + .navigationBarTitleDisplayMode(.inline) } var isReadyToSubmit: Bool { @@ -267,18 +283,6 @@ struct AddSavedInstanceView: View { .dynamicTypeSize(.small ... .accessibility1) } - @ViewBuilder - var footerView: some View { - Text("What is Lemmy?") - .font(.footnote) - .foregroundColor(.blue) - .accessibilityAddTraits(.isLink) - .padding() - .onTapGesture { - openURL(URL(string: "https://join-lemmy.org")!) - } - } - func tryToAddAccount() async { print("Will start the account addition process") @@ -328,10 +332,10 @@ struct AddSavedInstanceView: View { AppConstants.keychain["\(newAccount.id)_accessToken"] = response.jwt accountsTracker.addAccount(account: newAccount) - dismiss() + setFlow(.account(newAccount)) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - setFlow(.account(newAccount)) + if !onboarding { + dismiss() } } catch { handle(error) diff --git a/Mlem/Views/Shared/Accounts/Components/Instance Summary.swift b/Mlem/Views/Shared/Accounts/Components/Instance Summary.swift index 3eb6ae5c7..8087f3cb8 100644 --- a/Mlem/Views/Shared/Accounts/Components/Instance Summary.swift +++ b/Mlem/Views/Shared/Accounts/Components/Instance Summary.swift @@ -98,7 +98,10 @@ struct InstanceSummary: View { Button { _ = URLHandler.handle(signupURL) - selectedInstance = instance + Task { @MainActor in + try await Task.sleep(for: .seconds(0.5)) + selectedInstance = instance + } } label: { Text("Got it, let's go!") } diff --git a/Mlem/Views/Shared/Accounts/Instance Picker View.swift b/Mlem/Views/Shared/Accounts/Instance Picker View.swift index 4ddaacad2..8e25896d0 100644 --- a/Mlem/Views/Shared/Accounts/Instance Picker View.swift +++ b/Mlem/Views/Shared/Accounts/Instance Picker View.swift @@ -30,10 +30,6 @@ struct InstancePickerView: View { var body: some View { ScrollView { LazyVStack(spacing: 0) { - Text("Instances") - .bold() - .padding() - if onboarding { Text(pickInstance) .frame(maxWidth: .infinity) @@ -58,6 +54,7 @@ struct InstancePickerView: View { } } } + .navigationTitle("Instances") .task { instances = await loadInstances() } diff --git a/Mlem/Window.swift b/Mlem/Window.swift index 1aa743452..3c67437f4 100644 --- a/Mlem/Window.swift +++ b/Mlem/Window.swift @@ -56,9 +56,7 @@ struct Window: View { private var content: some View { switch flow { case .onboarding: - NavigationStack { - OnboardingView(flow: $flow) - } + LandingPage() case let .account(account): view(for: account) } @@ -76,6 +74,61 @@ struct Window: View { } private func setFlow(_ flow: AppFlow) { - self.flow = flow + transition(flow) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.flow = flow + } + } + + /// This method changes the current application flow and places a _transition_ view across the active window while + /// - Parameter newFlow: The `AppFlow` that the application should transition into + private func transition(_ newFlow: AppFlow) { + struct TransitionView: View { + let text: String + + var body: some View { + VStack(spacing: 24) { + ProgressView() + .controlSize(.large) + Text(text) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + let transitionText: String + switch newFlow { + case .onboarding: + transitionText = "See you soon 👋" + case let .account(account): + transitionText = "Welcome \(account.nickname) 🚀" + } + + Task { @MainActor in + + let transition = TransitionView(text: transitionText) + guard let transitionView = UIHostingController(rootView: transition).view, + let window = UIApplication.shared.firstKeyWindow else { + return + } + + transitionView.alpha = 0 + window.addSubview(transitionView) + UIView.animate(withDuration: 0.15) { + transitionView.alpha = 1 + } + + transitionView.translatesAutoresizingMaskIntoConstraints = false + transitionView.heightAnchor.constraint(equalTo: window.heightAnchor).isActive = true + transitionView.widthAnchor.constraint(equalTo: window.widthAnchor).isActive = true + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + UIView.animate(withDuration: 0.3) { + transitionView.alpha = 0 + } completion: { _ in + transitionView.removeFromSuperview() + } + } + } } }