From 263389b2e669307df7b260fe3e494d856219d54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Tue, 30 Jan 2024 07:42:04 +0000 Subject: [PATCH] purple: add translation setup view the onboarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes the welcome view into a multi-step onboarding process, where it makes it more clear that translations are unlocked, and provides the user with some choices to set it up or not. This flow also makes the translation setup possible on the LN flow Signed-off-by: Daniel D’Aquino Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 8 + .../DamusPurpleNewUserOnboardingView.swift | 39 ++++ .../DamusPurpleTranslationSetupView.swift | 192 ++++++++++++++++++ .../Purple/DamusPurpleURLSheetView.swift | 5 +- damus/Views/Purple/DamusPurpleView.swift | 27 +-- .../Views/Purple/DamusPurpleWelcomeView.swift | 14 +- 6 files changed, 254 insertions(+), 31 deletions(-) create mode 100644 damus/Views/Purple/DamusPurpleNewUserOnboardingView.swift create mode 100644 damus/Views/Purple/DamusPurpleTranslationSetupView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index b0fcc5bd..0932ba0e 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -448,6 +448,8 @@ D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; + D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */; }; + D7373BA82B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */; }; D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */; }; D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */; }; D74AAFC52B1538DF006CF0F4 /* NotificationExtensionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFC42B1538DE006CF0F4 /* NotificationExtensionState.swift */; }; @@ -1342,6 +1344,8 @@ D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = ""; }; D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = ""; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = ""; }; + D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleTranslationSetupView.swift; sourceTree = ""; }; + D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNewUserOnboardingView.swift; sourceTree = ""; }; D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessDamusState.swift; sourceTree = ""; }; D74AAFC42B1538DE006CF0F4 /* NotificationExtensionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationExtensionState.swift; sourceTree = ""; }; D74AAFCB2B155D07006CF0F4 /* MakeZapRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeZapRequest.swift; sourceTree = ""; }; @@ -2576,9 +2580,11 @@ children = ( 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */, D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */, + D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */, D7ADD3DF2B538D4200F104C4 /* DamusPurpleURLSheetView.swift */, D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */, D724D8262B64B40B00ABE789 /* DamusPurpleAccountView.swift */, + D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */, ); path = Purple; sourceTree = ""; @@ -2996,6 +3002,7 @@ 4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */, 4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */, 4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */, + D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */, 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */, 4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */, 4C75EFB92804A2740006080F /* EventView.swift in Sources */, @@ -3114,6 +3121,7 @@ 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4CA352AA2A76BF3A003BB08B /* LocalNotificationNotify.swift in Sources */, D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */, + D7373BA82B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift in Sources */, 4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */, D7CB5D452B116FE800AD4105 /* Contacts+.swift in Sources */, 4CA352A42A76AFF3003BB08B /* UpdateStatsNotify.swift in Sources */, diff --git a/damus/Views/Purple/DamusPurpleNewUserOnboardingView.swift b/damus/Views/Purple/DamusPurpleNewUserOnboardingView.swift new file mode 100644 index 00000000..f46e55b3 --- /dev/null +++ b/damus/Views/Purple/DamusPurpleNewUserOnboardingView.swift @@ -0,0 +1,39 @@ +// +// DamusPurpleNewUserOnboardingView.swift +// damus +// +// Created by Daniel D’Aquino on 2024-01-29. +// + +import SwiftUI + +struct DamusPurpleNewUserOnboardingView: View { + var damus_state: DamusState + @State var current_page: Int = 0 + @Environment(\.dismiss) var dismiss + + func next_page() { + current_page += 1 + } + + var body: some View { + NavigationView { + TabView(selection: $current_page) { + DamusPurpleWelcomeView(next_page: { + self.next_page() + }) + .tag(0) + + DamusPurpleTranslationSetupView(damus_state: damus_state, next_page: { + dismiss() + }) + .tag(1) + } + .ignoresSafeArea() // Necessary to avoid weird white edges + } + } +} + +#Preview { + DamusPurpleNewUserOnboardingView(damus_state: test_damus_state) +} diff --git a/damus/Views/Purple/DamusPurpleTranslationSetupView.swift b/damus/Views/Purple/DamusPurpleTranslationSetupView.swift new file mode 100644 index 00000000..671882cb --- /dev/null +++ b/damus/Views/Purple/DamusPurpleTranslationSetupView.swift @@ -0,0 +1,192 @@ +// +// DamusPurpleTranslationSetupView.swift +// damus +// +// Created by Daniel D’Aquino on 2024-01-29. +// + +import SwiftUI + +fileprivate extension Animation { + static func content() -> Animation { + Animation.easeInOut(duration: 1.5).delay(0) + } + + static func delayed_content() -> Animation { + Animation.easeInOut(duration: 1.5).delay(1) + } +} + +struct DamusPurpleTranslationSetupView: View { + var damus_state: DamusState + var next_page: () -> Void + + @State var start = false + @State var show_settings_change_confirmation_dialog = false + + // MARK: - Helper functions + + func update_user_settings_to_purple() { + if damus_state.settings.translation_service == .none { + set_translation_settings_to_purple() + self.next_page() + } + else { + show_settings_change_confirmation_dialog = true + } + } + + func set_translation_settings_to_purple() { + damus_state.settings.translation_service = .purple + damus_state.settings.auto_translate = true + } + + // MARK: - View layout + + var body: some View { + VStack { + Image("damus-dark-logo") + .resizable() + .frame(width: 50, height: 50) + .clipShape(RoundedRectangle(cornerRadius: 10.0)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(LinearGradient( + colors: [DamusColors.lighterPink.opacity(0.8), .white.opacity(0), DamusColors.deepPurple.opacity(0.6)], + startPoint: .topLeading, + endPoint: .bottomTrailing), lineWidth: 1) + ) + .shadow(radius: 5) + .padding(20) + .opacity(start ? 1.0 : 0.0) + .animation(.content(), value: start) + + Text(NSLocalizedString("You unlocked", comment: "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple" )) + .font(.largeTitle) + .fontWeight(.bold) + .foregroundStyle( + LinearGradient( + colors: [.black, .black, DamusColors.pink, DamusColors.lighterPink], + startPoint: start ? .init(x: -3, y: 4) : .bottomLeading, + endPoint: start ? .topTrailing : .init(x: 3, y: -4) + ) + ) + .scaleEffect(x: start ? 1 : 0.9, y: start ? 1 : 0.9) + .opacity(start ? 1.0 : 0.0) + .animation(.content(), value: start) + + Image(systemName: "globe") + .resizable() + .frame(width: 96, height: 90) + .foregroundStyle( + LinearGradient( + colors: [.black, DamusColors.purple, .white, .white], + startPoint: start ? .init(x: -1, y: 1.5) : .bottomLeading, + endPoint: start ? .topTrailing : .init(x: 10, y: -11) + ) + ) + .animation(Animation.snappy(duration: 2).delay(0), value: start) + .shadow( + color: start ? DamusColors.purple.opacity(0.2) : DamusColors.purple.opacity(0.3), + radius: start ? 30 : 10 + ) + .animation(Animation.snappy(duration: 2).delay(0), value: start) + .scaleEffect(x: start ? 1 : 0.8, y: start ? 1 : 0.8) + .opacity(start ? 1.0 : 0.0) + .animation(Animation.snappy(duration: 2).delay(0), value: start) + + Text(NSLocalizedString("Automatic translations", comment: "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple")) + .font(.headline) + .fontWeight(.bold) + .foregroundStyle( + LinearGradient( + colors: [.black, .black, DamusColors.lighterPink, DamusColors.lighterPink], + startPoint: start ? .init(x: -3, y: 4) : .bottomLeading, + endPoint: start ? .topTrailing : .init(x: 3, y: -4) + ) + ) + .scaleEffect(x: start ? 1 : 0.9, y: start ? 1 : 0.9) + .opacity(start ? 1.0 : 0.0) + .animation(.content(), value: start) + .padding(.top, 10) + + Text(NSLocalizedString("As part of your Damus Purple membership, you get complimentary and automated translations. Would you like to enable Damus Purple translations?\n\nTip: You can always change this later in Settings → Translations", comment: "Message notifying the user that they get auto-translations as part of their service")) + .lineSpacing(5) + .multilineTextAlignment(.center) + .foregroundStyle(.white.opacity(0.8)) + .padding(.horizontal, 20) + .padding(.top, 50) + .padding(.bottom, 20) + .opacity(start ? 1.0 : 0.0) + .animation(.delayed_content(), value: start) + + Button(action: { + self.update_user_settings_to_purple() + }, label: { + HStack { + Spacer() + Text(NSLocalizedString("Enable Purple auto-translations", comment: "Label for button that allows users to enable Damus Purple translations")) + Spacer() + } + }) + .padding(.horizontal, 30) + .buttonStyle(GradientButtonStyle()) + .opacity(start ? 1.0 : 0.0) + .animation(.delayed_content(), value: start) + + Button(action: { + self.next_page() + }, label: { + HStack { + Spacer() + Text(NSLocalizedString("No, thanks", comment: "Label for button that allows users to reject enabling Damus Purple translations")) + Spacer() + } + }) + .padding(.horizontal, 30) + .foregroundStyle(DamusColors.pink) + .opacity(start ? 1.0 : 0.0) + .padding() + .animation(.delayed_content(), value: start) + } + .background(content: { + ZStack { + Rectangle() + .background(.black) + Image("purple-blue-gradient-1") + .offset(CGSize(width: 300.0, height: -0.0)) + .opacity(start ? 1.0 : 0.2) + Image("stars-bg") + .resizable(resizingMode: .stretch) + .frame(width: 500, height: 500) + .offset(x: -100, y: 50) + .scaleEffect(start ? 1 : 0.9) + .animation(.content(), value: start) + Image("purple-blue-gradient-1") + .offset(CGSize(width: 300.0, height: -0.0)) + .opacity(start ? 1.0 : 0.2) + + } + }) + .onAppear(perform: { + withAnimation(.easeOut(duration: 6), { + start = true + }) + }) + .confirmationDialog( + NSLocalizedString("It seems that you already have a translation service configured. Would you like to switch to Damus Purple as your translator?", comment: "Confirmation dialog question asking users if they want their translation settings to be automatically switched to the Damus Purple translation service"), + isPresented: $show_settings_change_confirmation_dialog, + titleVisibility: .visible + ) { + Button(NSLocalizedString("Yes", comment: "User confirm Yes")) { + set_translation_settings_to_purple() + self.next_page() + }.keyboardShortcut(.defaultAction) + Button(NSLocalizedString("No", comment: "User confirm No"), role: .cancel) {} + } + } +} + +#Preview { + DamusPurpleTranslationSetupView(damus_state: test_damus_state, next_page: {}) +} diff --git a/damus/Views/Purple/DamusPurpleURLSheetView.swift b/damus/Views/Purple/DamusPurpleURLSheetView.swift index f37190b9..9189b8c5 100644 --- a/damus/Views/Purple/DamusPurpleURLSheetView.swift +++ b/damus/Views/Purple/DamusPurpleURLSheetView.swift @@ -19,7 +19,10 @@ struct DamusPurpleURLSheetView: View { case .verify_npub(let checkout_id): DamusPurpleVerifyNpubView(damus_state: damus_state, checkout_id: checkout_id) case .welcome(_): - DamusPurpleWelcomeView() + // Forcibly pass the dismiss environment object, + // because SwiftUI has a weird quirk that makes the `dismiss` Environment object unavailable in deeply nested views + // this problem only exists in real devices. + DamusPurpleNewUserOnboardingView(damus_state: damus_state, dismiss: _dismiss) case .landing: DamusPurpleView(damus_state: damus_state) } diff --git a/damus/Views/Purple/DamusPurpleView.swift b/damus/Views/Purple/DamusPurpleView.swift index a4ba4727..7f2c70fd 100644 --- a/damus/Views/Purple/DamusPurpleView.swift +++ b/damus/Views/Purple/DamusPurpleView.swift @@ -106,21 +106,10 @@ struct DamusPurpleView: View { } .ignoresSafeArea(.all) .sheet(isPresented: $show_welcome_sheet, onDismiss: { - update_user_settings_to_purple() shouldDismissView = true }, content: { - DamusPurpleWelcomeView() + DamusPurpleNewUserOnboardingView(damus_state: damus_state) }) - .confirmationDialog( - NSLocalizedString("It seems that you already have a translation service configured. Would you like to switch to Damus Purple as your translator?", comment: "Confirmation dialog question asking users if they want their translation settings to be automatically switched to the Damus Purple translation service"), - isPresented: $show_settings_change_confirmation_dialog, - titleVisibility: .visible - ) { - Button(NSLocalizedString("Yes", comment: "User confirm Yes")) { - set_translation_settings_to_purple() - }.keyboardShortcut(.defaultAction) - Button(NSLocalizedString("No", comment: "User confirm No"), role: .cancel) {} - } .onChange(of: shouldDismissView) { shouldDismissView in if shouldDismissView && !show_settings_change_confirmation_dialog { dismiss() @@ -148,20 +137,6 @@ struct DamusPurpleView: View { } } - func update_user_settings_to_purple() { - if damus_state.settings.translation_service == .none { - set_translation_settings_to_purple() - } - else { - show_settings_change_confirmation_dialog = true - } - } - - func set_translation_settings_to_purple() { - damus_state.settings.translation_service = .purple - damus_state.settings.auto_translate = true - } - func handle_transactions(products: [Product]) async { for await update in StoreKit.Transaction.updates { switch update { diff --git a/damus/Views/Purple/DamusPurpleWelcomeView.swift b/damus/Views/Purple/DamusPurpleWelcomeView.swift index 9dd1acbb..4b297d0a 100644 --- a/damus/Views/Purple/DamusPurpleWelcomeView.swift +++ b/damus/Views/Purple/DamusPurpleWelcomeView.swift @@ -17,6 +17,7 @@ fileprivate extension Animation { struct DamusPurpleWelcomeView: View { @Environment(\.dismiss) var dismiss @State var start = false + var next_page: () -> Void var body: some View { VStack { @@ -80,7 +81,7 @@ struct DamusPurpleWelcomeView: View { .animation(.content(), value: start) Button(action: { - dismiss() + self.next_page() }, label: { HStack { Spacer() @@ -113,15 +114,20 @@ struct DamusPurpleWelcomeView: View { } }) .onAppear(perform: { - withAnimation(.easeOut(duration: 6), { - start = true + // SwiftUI quirk #98332: If I try to trigger an immediate animation, the animation does not work when this view is placed under a TabView. + // Triggering the animation only after a slight delay makes it work. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { + withAnimation(.easeOut(duration: 6), { + start = true + }) }) + }) } } struct DamusPurpleWelcomeView_Previews: PreviewProvider { static var previews: some View { - DamusPurpleWelcomeView() + DamusPurpleWelcomeView(next_page: {}) } }