OnboardingSuggestionsView.swift (9788B)
1 // 2 // OnboardingSuggestionsView.swift 3 // damus 4 // 5 // Created by klabo on 7/17/23. 6 // 7 8 import SwiftUI 9 10 fileprivate let first_post_example_1: String = NSLocalizedString("Hello everybody!\n\nThis is my first post on Damus, I am happy to meet you all 🤙. What’s up?\n\n#introductions", comment: "First post example given to the user during onboarding, as a suggestion as to what they could post first") 11 fileprivate let first_post_example_2: String = NSLocalizedString("This is my first post on Nostr 💜. I love drawing and folding Origami!\n\nNice to meet you all! #introductions #plebchain ", comment: "First post example given to the user during onboarding, as a suggestion as to what they could post first") 12 fileprivate let first_post_example_3: String = NSLocalizedString("For #Introductions! I’m a software developer.\n\nMy side interests include languages and I am striving to be a #polyglot - I am a native English speaker and can speak French, German and Japanese.", comment: "First post example given to the user during onboarding, as a suggestion as to what they could post first") 13 fileprivate let first_post_example_4: String = NSLocalizedString("Howdy! I’m a graphic designer during the day and coder at night, but I’m also trying to spend more time outdoors.\n\nHope to meet folks who are on their own journeys to a peaceful and free life!", comment: "First post example given to the user during onboarding, as a suggestion as to what they could post first") 14 15 struct OnboardingSuggestionsView: View { 16 17 @StateObject var model: SuggestedUsersViewModel 18 @State var current_page: Int = 0 19 let first_post_examples: [String] = [first_post_example_1, first_post_example_2, first_post_example_3, first_post_example_4] 20 let initial_text_suffix: String = "\n\n#introductions" 21 22 @Environment(\.dismiss) var dismiss 23 24 func next_page() { 25 withAnimation { 26 current_page += 1 27 } 28 } 29 30 private var canLeaveInterestSelectionPage: Bool { 31 let count = model.interests.count 32 return count > 0 33 } 34 35 /// Save the user's selected interests to NDB 36 private func saveInterestsToNdb() { 37 // Convert the selected interests to hashtags for the NIP51 interest list 38 let interestItems = model.interests.map { interest in 39 NIP51.InterestList.InterestItem.hashtag(interest.rawValue) 40 } 41 42 // Create the interest list 43 let interestList = NIP51.InterestList(interests: Array(interestItems)) 44 45 // Convert to a NostrEvent and send to NDB 46 guard let keypair = model.damus_state.keypair.to_full(), 47 let event = interestList.toNostrEvent(keypair: keypair, timestamp: nil) else { 48 return // Not a big deal, fail silently 49 } 50 51 // Send the event to NostrDB to allow us to retrieve later 52 // Did not send this to the network yet because: 53 // 1. I believe we should add an opt-out/opt-in button. 54 // 2. If we do, and the user accepts to share it, it will be an awkward situation considering: 55 // - We don't show that anywhere else yet 56 // - We don't have other mechanisms to allow the user to edit this yet 57 // 58 // Therefore, it is better to just save it locally, and retrieve this once we build out https://github.com/damus-io/damus/issues/3042 59 model.damus_state.nostrNetwork.pool.send_raw_to_local_ndb(.typical(.event(event))) 60 } 61 62 var body: some View { 63 NavigationView { 64 TabView(selection: $current_page) { 65 InterestSelectionView(damus_state: model.damus_state, next_page: { 66 self.next_page() 67 }, selectedInterests: $model.interests, isNextEnabled: canLeaveInterestSelectionPage) 68 .navigationTitle(NSLocalizedString("Select your interests", comment: "Title for a screen asking the user for interests")) 69 .navigationBarTitleDisplayMode(.inline) 70 .tag(0) 71 72 if canLeaveInterestSelectionPage { 73 74 OnboardingContentSettings(model: model, next_page: self.next_page, settings: model.damus_state.settings, selectedInterests: $model.interests) 75 .navigationTitle(NSLocalizedString("Content settings", comment: "Title for an onboarding screen showing user some content settings")) 76 .navigationBarTitleDisplayMode(.inline) 77 .tag(1) 78 79 SuggestedUsersPageView(model: model, next_page: self.next_page) 80 .navigationTitle(NSLocalizedString("Who to Follow", comment: "Title for a screen displaying suggestions of who to follow")) 81 .navigationBarTitleDisplayMode(.inline) 82 .navigationBarItems(leading: Button(action: { 83 self.next_page() 84 }, label: { 85 Text("Skip", comment: "Button to dismiss the suggested users screen") 86 .font(.subheadline.weight(.semibold)) 87 }) 88 .accessibilityIdentifier(AppAccessibilityIdentifiers.onboarding_sheet_skip_button.rawValue) 89 ) 90 .tag(2) 91 92 PostView( 93 action: .posting(.user(model.damus_state.pubkey)), 94 damus_state: model.damus_state, 95 prompt_view: { 96 AnyView( 97 HStack { 98 Image(systemName: "sparkles") 99 Text("Add your first post", comment: "Prompt given to the user during onboarding, suggesting them to write their first post") 100 } 101 .foregroundColor(.secondary) 102 .font(.callout) 103 .padding(.top, 10) 104 ) 105 }, 106 placeholder_messages: self.first_post_examples, 107 initial_text_suffix: self.initial_text_suffix 108 ) 109 .onReceive(handle_notify(.post)) { _ in 110 // NOTE: Even though PostView already calls `dismiss`, that is not guaranteed to work under deeply nested views. 111 // Thus, we should also call `dismiss` from here (a direct subview of a sheet), which is explicitly supported by Apple. 112 // See https://github.com/damus-io/damus/issues/1726 for more context and information 113 dismiss() 114 } 115 .tag(3) 116 } 117 } 118 .tabViewStyle(.page(indexDisplayMode: .never)) 119 .onChange(of: current_page) { newPage in 120 // If the user just swiped from the interests page (0) to the next page (1), 121 // save their interests to NDB 122 if newPage == 1 && current_page == 1 { 123 saveInterestsToNdb() 124 } 125 } 126 } 127 } 128 } 129 130 fileprivate struct SuggestedUsersPageView: View { 131 var model: SuggestedUsersViewModel 132 var next_page: (() -> Void) 133 134 var body: some View { 135 VStack { 136 if let suggestions = model.suggestions { 137 List { 138 ForEach(suggestions, id: \.self) { followPack in 139 Section { 140 ForEach(followPack.publicKeys, id: \.self) { pk in 141 if let usersInterests = model.interestUserMap[pk], 142 !usersInterests.intersection(model.interests).isEmpty && usersInterests.intersection(model.disinterests).isEmpty, 143 let user = model.suggestedUser(pubkey: pk) { 144 SuggestedUserView(user: user, damus_state: model.damus_state) 145 } 146 } 147 } header: { 148 SuggestedUsersSectionHeader(followPack: followPack, model: model) 149 } 150 } 151 } 152 .listStyle(.plain) 153 } 154 else { 155 ProgressView() 156 } 157 158 Spacer() 159 160 Button(action: { 161 self.next_page() 162 }) { 163 Text("Continue", comment: "Button to dismiss suggested users view and continue to the main app") 164 .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) 165 } 166 .buttonStyle(GradientButtonStyle()) 167 .padding([.leading, .trailing], 24) 168 .padding(.bottom, 16) 169 } 170 } 171 } 172 173 struct SuggestedUsersSectionHeader: View { 174 let followPack: FollowPackEvent 175 let model: SuggestedUsersViewModel 176 var body: some View { 177 HStack { 178 Text(followPack.title ?? NSLocalizedString("Untitled Follow Pack", comment: "Default title for a follow pack if no title is specified")) 179 Spacer() 180 Button(NSLocalizedString("Follow All", comment: "Button to follow all users in this section")) { 181 model.follow(pubkeys: followPack.publicKeys) 182 } 183 .font(.subheadline.weight(.semibold)) 184 } 185 } 186 } 187 188 struct SuggestedUsersView_Previews: PreviewProvider { 189 static var previews: some View { 190 OnboardingSuggestionsView(model: try! SuggestedUsersViewModel(damus_state: test_damus_state)) 191 } 192 }