diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index f839cb744..2a0233b8b 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 030AC0522A64666C00037155 /* UserSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030AC0512A64666C00037155 /* UserSettingsView.swift */; }; 030D4AE62AA1273200A3393D /* ErrorDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030D4AE52AA1273200A3393D /* ErrorDetails.swift */; }; 030D4AE82AA1278400A3393D /* ErrorDetails+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030D4AE72AA1278400A3393D /* ErrorDetails+Mock.swift */; }; + 031BF9532AB24BAF00F4517F /* SiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031BF9522AB24BAF00F4517F /* SiteVersion.swift */; }; + 031BF9552AB25AFB00F4517F /* SiteVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031BF9542AB25AFB00F4517F /* SiteVersionTests.swift */; }; 032109472AA7C3FC00912DFC /* CommunityLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032109462AA7C3FC00912DFC /* CommunityLabelView.swift */; }; 032109492AA7C41800912DFC /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032109482AA7C41800912DFC /* AvatarView.swift */; }; 034C724F2A82B61200B8A4B8 /* LayoutWidgetTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C724E2A82B61200B8A4B8 /* LayoutWidgetTracker.swift */; }; @@ -42,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 */; }; @@ -382,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 */; }; @@ -457,6 +461,8 @@ 030AC0512A64666C00037155 /* UserSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsView.swift; sourceTree = ""; }; 030D4AE52AA1273200A3393D /* ErrorDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetails.swift; sourceTree = ""; }; 030D4AE72AA1278400A3393D /* ErrorDetails+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ErrorDetails+Mock.swift"; sourceTree = ""; }; + 031BF9522AB24BAF00F4517F /* SiteVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteVersion.swift; sourceTree = ""; }; + 031BF9542AB25AFB00F4517F /* SiteVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteVersionTests.swift; sourceTree = ""; }; 032109462AA7C3FC00912DFC /* CommunityLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLabelView.swift; sourceTree = ""; }; 032109482AA7C41800912DFC /* AvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; 034C724E2A82B61200B8A4B8 /* LayoutWidgetTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutWidgetTracker.swift; sourceTree = ""; }; @@ -488,6 +494,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 = ""; }; @@ -826,7 +834,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 = ""; }; @@ -1010,6 +1018,14 @@ path = User; sourceTree = ""; }; + 031BF9562AB25AFE00F4517F /* Model */ = { + isa = PBXGroup; + children = ( + 031BF9542AB25AFB00F4517F /* SiteVersionTests.swift */, + ); + path = Model; + sourceTree = ""; + }; 032109442AA7C32100912DFC /* User */ = { isa = PBXGroup; children = ( @@ -1072,6 +1088,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 = ( @@ -1360,7 +1386,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 */, @@ -1494,6 +1519,7 @@ 50DBB8DE2A805770002870B1 /* Mocks */, 50C86AB82A7E507200277519 /* Persistence */, 6363D5DA27EE196A00E34822 /* MlemTests.swift */, + 031BF9562AB25AFE00F4517F /* Model */, ); path = MlemTests; sourceTree = ""; @@ -1510,6 +1536,7 @@ 6363D5F327EE1BA900E34822 /* Views */ = { isa = PBXGroup; children = ( + 504ECBA82AB27C4C006C0B96 /* Onboarding */, 6386E03E2A04570F006B3C1D /* Shared */, 6363D5F427EE1BAE00E34822 /* Tabs */, ); @@ -1554,6 +1581,7 @@ isa = PBXGroup; children = ( 637218032A3A2AAD008C4816 /* HierarchicalComment.swift */, + 031BF9522AB24BAF00F4517F /* SiteVersion.swift */, 504ECBAD2AB45B2A006C0B96 /* LemmyURL.swift */, ); path = Internal; @@ -2578,6 +2606,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 */, @@ -2672,6 +2701,7 @@ 03E0B9CA2A62B4A400FED265 /* ContributorsView.swift in Sources */, 637218662A3A2AAD008C4816 /* SavePost.swift in Sources */, CD6F29AC2A78015200F20B6B /* PostRepository+Dependency.swift in Sources */, + 031BF9532AB24BAF00F4517F /* SiteVersion.swift in Sources */, 637218452A3A2AAD008C4816 /* APICommentAggregates.swift in Sources */, 6D80037B2A46458800363206 /* Lazy Load Expanded Post.swift in Sources */, 6372184F2A3A2AAD008C4816 /* APIPerson.swift in Sources */, @@ -2718,6 +2748,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 */, @@ -2751,7 +2782,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 */, @@ -2857,6 +2888,7 @@ 50DBB8E02A805836002870B1 /* MockErrorHandler.swift in Sources */, 6363D5DB27EE196A00E34822 /* MlemTests.swift in Sources */, 50BC1AB92A89744200E3C48B /* CommunityListModelTests.swift in Sources */, + 031BF9552AB25AFB00F4517F /* SiteVersionTests.swift in Sources */, 50CC4A782A9CBDF70074C845 /* TimestampedValueTests.swift in Sources */, 50CC4A822AA0D61F0074C845 /* InstanceMetadataParserTests.swift in Sources */, 50C86ABC2A7E50E200277519 /* PersistenceRepositoryTests.swift in Sources */, @@ -3032,7 +3064,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.hanners.Mlem; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -3073,7 +3105,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.hanners.Mlem; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/Mlem/API/Internal/SiteVersion.swift b/Mlem/API/Internal/SiteVersion.swift new file mode 100644 index 000000000..b49211030 --- /dev/null +++ b/Mlem/API/Internal/SiteVersion.swift @@ -0,0 +1,87 @@ +// +// APISiteVersionNumber.swift +// Mlem +// +// Created by Sjmarf on 09/09/2023. +// +import Foundation + +enum SiteVersion: Equatable { + + case release(major: Int, minor: Int, patch: Int) + case other(String) + case zero + case infinity + + init(_ version: String) { + + let parts = version.split(separator: "-") + if let firstPart = parts.first { + let components = firstPart.split(separator: ".").compactMap { Int($0) } + if components.count == 3 { + self = .release(major: components[0], minor: components[1], patch: components[2]) + } else { + self = .other(version) + } + } else { + self = .other(version) + } + } + + // swiftlint: disable large_tuple + var parts: (Int, Int, Int)? { + switch self { + case .release(let major, let minor, let patch): + return (major, minor, patch) + default: + return nil + } + } + // swiftlint: enable large_tuple +} + +extension SiteVersion: CustomStringConvertible { + var description: String { + switch self { + case .zero: + return "zero" + case .infinity: + return "infinity" + case .release(let major, let minor, let patch): + return "\(major).\(minor).\(patch)" + case .other(let string): + return string + } + } +} + +extension SiteVersion: Codable { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let versionString = try container.decode(String.self) + self.init(versionString) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(String(describing: self)) + } +} + +extension SiteVersion: Comparable { + static func < (lhs: SiteVersion, rhs: SiteVersion) -> Bool { + + switch (lhs, rhs) { + case (.release, .release): + return lhs.parts! < rhs.parts! + + case (.zero, _), (_, .infinity): + return true + + case (_, .zero), (.infinity, _): + return false + default: + return false + } + } +} diff --git a/Mlem/Assets.xcassets/Icons/Classic Lemmy by Eric Andrews.appiconset/Classic Lemmy.png b/Mlem/Assets.xcassets/Icons/Classic Lemmy by Eric Andrews.appiconset/Classic Lemmy.png new file mode 100644 index 000000000..49ef09532 Binary files /dev/null and b/Mlem/Assets.xcassets/Icons/Classic Lemmy by Eric Andrews.appiconset/Classic Lemmy.png differ diff --git a/Mlem/Assets.xcassets/Icons/Classic Lemmy by Eric Andrews.appiconset/Contents.json b/Mlem/Assets.xcassets/Icons/Classic Lemmy by Eric Andrews.appiconset/Contents.json new file mode 100644 index 000000000..02b17258e --- /dev/null +++ b/Mlem/Assets.xcassets/Icons/Classic Lemmy by Eric Andrews.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Classic Lemmy.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mlem/Assets.xcassets/Icons/Funny Conductor Lem By Clays.appiconset/ConductorLem.png b/Mlem/Assets.xcassets/Icons/Train Conductor Lem By Clays.appiconset/ConductorLem.png similarity index 100% rename from Mlem/Assets.xcassets/Icons/Funny Conductor Lem By Clays.appiconset/ConductorLem.png rename to Mlem/Assets.xcassets/Icons/Train Conductor Lem By Clays.appiconset/ConductorLem.png diff --git a/Mlem/Assets.xcassets/Icons/Funny Conductor Lem By Clays.appiconset/Contents.json b/Mlem/Assets.xcassets/Icons/Train Conductor Lem By Clays.appiconset/Contents.json similarity index 100% rename from Mlem/Assets.xcassets/Icons/Funny Conductor Lem By Clays.appiconset/Contents.json rename to Mlem/Assets.xcassets/Icons/Train Conductor Lem By Clays.appiconset/Contents.json diff --git a/Mlem/Extensions/IconSettingsView.swift b/Mlem/Extensions/IconSettingsView.swift index 7290ccfe2..c118be474 100644 --- a/Mlem/Extensions/IconSettingsView.swift +++ b/Mlem/Extensions/IconSettingsView.swift @@ -64,7 +64,7 @@ struct IconSettingsView: View { } return true }.sorted(by: { lhs, rhs in - lhs.name > rhs.name + lhs.name < rhs.name })) return allIcons diff --git a/Mlem/Models/Trackers/SiteInformationTracker.swift b/Mlem/Models/Trackers/SiteInformationTracker.swift index e6f84d977..4f4f081ac 100644 --- a/Mlem/Models/Trackers/SiteInformationTracker.swift +++ b/Mlem/Models/Trackers/SiteInformationTracker.swift @@ -13,11 +13,13 @@ class SiteInformationTracker: ObservableObject { @Dependency(\.apiClient) var apiClient @Published private(set) var enableDownvotes = true + @Published private(set) var version: SiteVersion? func load() { Task { let information = try await apiClient.loadSiteInformation() enableDownvotes = information.siteView.localSite.enableDownvotes + version = SiteVersion(information.version) } } } 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 0d6daedba..3ba96ca78 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() } } } @@ -94,15 +96,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/Views/Shared/Posts/Post Sizes/Large Post.swift b/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift index 84f3435d2..663eca7d5 100644 --- a/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift +++ b/Mlem/Views/Shared/Posts/Post Sizes/Large Post.swift @@ -92,6 +92,12 @@ struct LargePost: View { } } + // REMOVEME: needed for TF hack + @Environment(\.horizontalSizeClass) var horizontalSizeClass + var screenWidth: CGFloat = UIScreen.main.bounds.width - (AppConstants.postAndCommentSpacing * 2) + var imageWidth: CGFloat { horizontalSizeClass == .regular ? screenWidth * 0.8 : screenWidth } + var imageHeight: CGFloat { horizontalSizeClass == .regular ? 600 : screenWidth } + // initializer--used so we can set showNsfwFilterToggle to false when expanded or true when not init( post: PostModel, @@ -202,15 +208,20 @@ struct LargePost: View { if layoutMode != .minimize { CachedImage( url: url, - maxHeight: layoutMode.getMaxHeight(limitHeight), + // maxHeight: layoutMode.getMaxHeight(limitHeight), + // CHANGEME: hack for TF release + fixedSize: CGSize(width: imageWidth, height: imageHeight), dismissCallback: markPostAsRead, cornerRadius: AppConstants.largeItemCornerRadius ) - .frame( - maxWidth: .infinity, - maxHeight: layoutMode.getMaxHeight(limitHeight), - alignment: .top - ) + // CHANGEME: hack for TF release + .frame(height: imageHeight) + .frame(maxWidth: .infinity, alignment: .center) +// .frame( +// maxWidth: .infinity, +// maxHeight: layoutMode.getMaxHeight(limitHeight), +// alignment: .top +// ) .applyNsfwOverlay(post.post.nsfw || post.community.nsfw, canTapFullImage: isExpanded) .clipped() } diff --git a/Mlem/Views/Shared/Website Icon Complex.swift b/Mlem/Views/Shared/Website Icon Complex.swift index 19b18b1ed..bd99e2f35 100644 --- a/Mlem/Views/Shared/Website Icon Complex.swift +++ b/Mlem/Views/Shared/Website Icon Complex.swift @@ -55,14 +55,26 @@ struct WebsiteIconComplex: View { } return "some website" } + + // REMOVEME: needed for TF hack + @Environment(\.horizontalSizeClass) var horizontalSizeClass + var screenWidth: CGFloat = UIScreen.main.bounds.width - (AppConstants.postAndCommentSpacing * 2) + var imageWidth: CGFloat { horizontalSizeClass == .regular ? screenWidth * 0.8 : screenWidth } + var imageHeight: CGFloat { horizontalSizeClass == .regular ? 400 : screenWidth * 0.66 } var body: some View { VStack(spacing: 0) { if shouldShowWebsitePreviews, let thumbnailURL = post.thumbnailImageUrl { - CachedImage(url: thumbnailURL, shouldExpand: false) - .frame(maxHeight: 400) - .applyNsfwOverlay(post.nsfw) - .clipped() + CachedImage( + url: thumbnailURL, + shouldExpand: false, + // CHANGEME: hack for TF release + fixedSize: CGSize(width: imageWidth, height: imageHeight) + ) + // .frame(maxHeight: 400) + .frame(width: imageWidth, height: imageHeight) + .applyNsfwOverlay(post.nsfw) + .clipped() } VStack(alignment: .leading, spacing: AppConstants.postAndCommentSpacing) { diff --git a/Mlem/Views/Tabs/Feeds/Feed View.swift b/Mlem/Views/Tabs/Feeds/Feed View.swift index 87301e30e..4c733d02a 100644 --- a/Mlem/Views/Tabs/Feeds/Feed View.swift +++ b/Mlem/Views/Tabs/Feeds/Feed View.swift @@ -217,7 +217,6 @@ struct FeedView: View { } else { VStack(alignment: .center, spacing: 5) { Image(systemName: "text.bubble") - Text("No posts to be found") } .padding() diff --git a/Mlem/Window.swift b/Mlem/Window.swift index 7a8d58f80..0fffdc630 100644 --- a/Mlem/Window.swift +++ b/Mlem/Window.swift @@ -58,9 +58,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) } @@ -80,6 +78,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() + } + } + } } } diff --git a/MlemTests/Model/SiteVersionTests.swift b/MlemTests/Model/SiteVersionTests.swift new file mode 100644 index 000000000..e7f8d8989 --- /dev/null +++ b/MlemTests/Model/SiteVersionTests.swift @@ -0,0 +1,65 @@ +// +// SiteVersionTests.swift +// MlemTests +// +// Created by Sjmarf on 13/09/2023. +// + +@testable import Mlem +import XCTest + +final class SiteVersionTests: XCTestCase { + + func testStringInitializer() { + // release + XCTAssertEqual(SiteVersion("0.18.2"), .release(major: 0, minor: 18, patch: 2)) + // other + XCTAssertEqual(SiteVersion("0.18.2.4"), .other("0.18.2.4")) + XCTAssertEqual(SiteVersion("0.18"), .other("0.18")) + XCTAssertEqual(SiteVersion("0.a.1"), .other("0.a.1")) + XCTAssertEqual(SiteVersion("ab-cd"), .other("ab-cd")) + XCTAssertEqual(SiteVersion("abc"), .other("abc")) + } + + func testStringDescription() { + // release + XCTAssertEqual( + SiteVersion.release(major: 0, minor: 18, patch: 2).description, + "0.18.2" + ) + // other + XCTAssertEqual( + SiteVersion.other("abc").description, + "abc" + ) + } + + func testComparisons() { + XCTAssertTrue(SiteVersion.release(major: 0, minor: 0, patch: 0) < SiteVersion.release(major: 1, minor: 0, patch: 0)) + XCTAssertTrue(SiteVersion.release(major: 0, minor: 0, patch: 0) < SiteVersion.release(major: 0, minor: 1, patch: 0)) + XCTAssertTrue(SiteVersion.release(major: 0, minor: 0, patch: 0) < SiteVersion.release(major: 0, minor: 0, patch: 1)) + XCTAssertFalse(SiteVersion.release(major: 0, minor: 0, patch: 0) < SiteVersion.release(major: 0, minor: 0, patch: 0)) + XCTAssertFalse(SiteVersion.release(major: 1, minor: 0, patch: 0) < SiteVersion.release(major: 0, minor: 0, patch: 0)) + + XCTAssertTrue( + SiteVersion.release(major: 0, minor: 18, patch: 2) + < SiteVersion.infinity + ) + XCTAssertFalse( + SiteVersion.infinity + < SiteVersion.release(major: 0, minor: 18, patch: 2) + ) + XCTAssertFalse( + SiteVersion.release(major: 0, minor: 18, patch: 2) + < SiteVersion.zero + ) + XCTAssertTrue( + SiteVersion.zero + < SiteVersion.release(major: 0, minor: 18, patch: 2) + ) + XCTAssertTrue( + SiteVersion.zero + < SiteVersion.infinity + ) + } +}