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: {}) } }