damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit fd130b78e748c7040989f40969c99017256c9418
parent 0be0273121b8d2351905e4a84fe1fc763ca4e941
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Fri,  5 Jul 2024 12:04:59 -0700

Merge pull request #2308 from ericholguin/simplify-onboarding

ux: Simplify Onboarding
Diffstat:
Mdamus/Models/CreateAccountModel.swift | 16++++++++--------
Mdamus/Views/CreateAccountView.swift | 87++++++++++++++++++++++++++++---------------------------------------------------
Mdamus/Views/LoginView.swift | 18++++++++++++++----
Mdamus/Views/Profile/EditPictureControl.swift | 56++++++++++++++++++++++++++++++++++++++++++++++----------
Mdamus/Views/SaveKeysView.swift | 200++++++++++++++++++++++++++++++-------------------------------------------------
Mdamus/Views/SetupView.swift | 96++++++++++++++++++++++++++-----------------------------------------------------
6 files changed, 206 insertions(+), 267 deletions(-)

diff --git a/damus/Models/CreateAccountModel.swift b/damus/Models/CreateAccountModel.swift @@ -9,31 +9,31 @@ import Foundation class CreateAccountModel: ObservableObject { - @Published var real_name: String = "" - @Published var nick_name: String = "" + @Published var display_name: String = "" + @Published var name: String = "" @Published var about: String = "" @Published var pubkey: Pubkey = .empty @Published var privkey: Privkey = .empty @Published var profile_image: URL? = nil var rendered_name: String { - if real_name.isEmpty { - return nick_name + if display_name.isEmpty { + return name } - return real_name + return display_name } var keypair: Keypair { return Keypair(pubkey: self.pubkey, privkey: self.privkey) } - init(real: String = "", nick: String = "", about: String = "") { + init(display_name: String = "", name: String = "", about: String = "") { let keypair = generate_new_keypair() self.pubkey = keypair.pubkey self.privkey = keypair.privkey - self.real_name = real - self.nick_name = nick + self.display_name = display_name + self.name = name self.about = about } } diff --git a/damus/Views/CreateAccountView.swift b/damus/Views/CreateAccountView.swift @@ -25,68 +25,44 @@ struct CreateAccountView: View { var body: some View { ZStack(alignment: .top) { VStack { + Spacer() VStack(alignment: .center) { - EditPictureControl(uploader: .nostrBuild, pubkey: account.pubkey, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture) - - Text("Public Key", comment: "Label to indicate the public key of the account.") + + EditPictureControl(uploader: .nostrBuild, pubkey: account.pubkey, size: 75, setup: true, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture) + .shadow(radius: 2) + .padding(.top, 100) + + Text("Add Photo", comment: "Label to indicate user can add a photo.") .bold() - .padding() - .onTapGesture { - regen_key() - } - - KeyText($account.pubkey) - .padding(.horizontal, 20) - .onTapGesture { - regen_key() - } - } - .frame(minWidth: 300, maxWidth: .infinity, minHeight: 250, alignment: .center) - .background { - RoundedRectangle(cornerRadius: 12) - .fill(DamusColors.adaptableGrey, strokeBorder: .gray.opacity(0.5), lineWidth: 1) + .foregroundColor(DamusColors.neutral6) } SignupForm { - FormLabel(NSLocalizedString("Display name", comment: "Label to prompt display name entry."), optional: true) - FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.real_name) + FormLabel(NSLocalizedString("Name", comment: "Label to prompt name entry."), optional: false) + .foregroundColor(DamusColors.neutral6) + FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.name) .textInputAutocapitalization(.words) - FormLabel(NSLocalizedString("About", comment: "Label to prompt for about text entry for user to describe about themself."), optional: true) - FormTextInput(NSLocalizedString("Creator(s) of Bitcoin. Absolute legend.", comment: "Example description about Bitcoin creator(s), Satoshi Nakamoto."), text: $account.about) + FormLabel(NSLocalizedString("Bio", comment: "Label to prompt bio entry for user to describe themself."), optional: true) + .foregroundColor(DamusColors.neutral6) + FormTextInput(NSLocalizedString("Absolute legend.", comment: "Example Bio"), text: $account.about) } - .padding(.top, 10) + .padding(.top, 25) Button(action: { nav.push(route: Route.SaveKeys(account: account)) }) { HStack { - Text("Create account now", comment: "Button to create account.") + Text("Next", comment: "Button to continue with account creation.") .fontWeight(.semibold) } .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) } .buttonStyle(GradientButtonStyle()) - .disabled(profileUploadObserver.isLoading) - .opacity(profileUploadObserver.isLoading ? 0.5 : 1) + .disabled(profileUploadObserver.isLoading || account.name.isEmpty) + .opacity(profileUploadObserver.isLoading || account.name.isEmpty ? 0.5 : 1) .padding(.top, 20) - HStack(spacing: 0) { - Text("By signing up, you agree to our ", comment: "Ask the user if they already have an account on Nostr") - .font(.subheadline) - .foregroundColor(Color("DamusMediumGrey")) - - Button(action: { - nav.push(route: Route.EULA) - }, label: { - Text("EULA") - .font(.subheadline) - }) - .padding(.vertical, 5) - - Spacer() - } - LoginPrompt() .padding(.top) @@ -94,8 +70,8 @@ struct CreateAccountView: View { } .padding() } + .background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top) .dismissKeyboardOnTap() - .navigationTitle("Create account") .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) .navigationBarItems(leading: BackNav()) @@ -111,7 +87,7 @@ struct LoginPrompt: View { var body: some View { HStack { Text("Already on Nostr?", comment: "Ask the user if they already have an account on Nostr") - .foregroundColor(Color("DamusMediumGrey")) + .foregroundColor(DamusColors.neutral6) Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) { self.dismiss() @@ -127,8 +103,8 @@ struct BackNav: View { var body: some View { Image("chevron-left") .foregroundColor(DamusColors.adaptableBlack) - .onTapGesture { - self.dismiss() + .onTapGesture { + self.dismiss() } } } @@ -148,20 +124,11 @@ extension View { struct CreateAccountView_Previews: PreviewProvider { static var previews: some View { - let model = CreateAccountModel(real: "", nick: "jb55", about: "") + let model = CreateAccountModel(display_name: "", name: "jb55", about: "") return CreateAccountView(account: model, nav: .init()) } } -func KeyText(_ pubkey: Binding<Pubkey>) -> some View { - let bechkey = bech32_encode(hrp: PUBKEY_HRP, pubkey.wrappedValue.bytes) - return Text(bechkey) - .textSelection(.enabled) - .multilineTextAlignment(.center) - .font(.callout.monospaced()) - .foregroundStyle(DamusLogoGradient.gradient) -} - func FormTextInput(_ title: String, text: Binding<String>) -> some View { return TextField("", text: text) .placeholder(when: text.wrappedValue.isEmpty) { @@ -171,6 +138,10 @@ func FormTextInput(_ title: String, text: Binding<String>) -> some View { .background { RoundedRectangle(cornerRadius: 12) .stroke(.gray.opacity(0.5), lineWidth: 1) + .background { + RoundedRectangle(cornerRadius: 12) + .foregroundColor(.damusAdaptableWhite) + } } .font(.body.bold()) } @@ -183,6 +154,10 @@ func FormLabel(_ title: String, optional: Bool = false) -> some View { Text("optional", comment: "Label indicating that a form input is optional.") .font(.callout) .foregroundColor(DamusColors.mediumGrey) + } else { + Text("required", comment: "Label indicating that a form input is required.") + .font(.callout) + .foregroundColor(DamusColors.mediumGrey) } } } diff --git a/damus/Views/LoginView.swift b/damus/Views/LoginView.swift @@ -62,8 +62,9 @@ struct LoginView: View { var body: some View { ZStack(alignment: .top) { VStack { + Spacer() + SignInHeader() - .padding(.top, 100) SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey) @@ -112,8 +113,9 @@ struct LoginView: View { Spacer() } .padding() + .padding(.bottom, 50) } - .background(DamusBackground(maxHeight: 350), alignment: .top) + .background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top) .onAppear { credential_handler.check_credentials() } @@ -320,9 +322,13 @@ struct KeyInput: View { } .padding(.vertical, 2) .padding(.horizontal, 10) - .overlay { + .background { RoundedRectangle(cornerRadius: 12) .stroke(.gray, lineWidth: 1) + .background { + RoundedRectangle(cornerRadius: 12) + .foregroundColor(.damusAdaptableWhite) + } } } } @@ -337,11 +343,12 @@ struct SignInHeader: View { .padding(.bottom) Text("Sign in", comment: "Title of view to log into an account.") + .foregroundColor(DamusColors.neutral6) .font(.system(size: 32, weight: .bold)) .padding(.bottom, 5) Text("Welcome to the social network you control", comment: "Welcome text") - .foregroundColor(Color("DamusMediumGrey")) + .foregroundColor(DamusColors.neutral6) } } } @@ -353,6 +360,7 @@ struct SignInEntry: View { var body: some View { VStack(alignment: .leading) { Text("Enter your account key", comment: "Prompt for user to enter an account key to login.") + .foregroundColor(DamusColors.neutral6) .fontWeight(.medium) .padding(.top, 30) @@ -444,7 +452,9 @@ struct LoginView_Previews: PreviewProvider { let bech32_pubkey = "KeyInput" Group { LoginView(key: pubkey, nav: .init()) + .previewDevice(PreviewDevice(rawValue: "iPhone SE (3rd generation)")) LoginView(key: bech32_pubkey, nav: .init()) + .previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro Max")) } } } diff --git a/damus/Views/Profile/EditPictureControl.swift b/damus/Views/Profile/EditPictureControl.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Kingfisher class ImageUploadingObserver: ObservableObject { @Published var isLoading: Bool = false @@ -14,6 +15,8 @@ class ImageUploadingObserver: ObservableObject { struct EditPictureControl: View { let uploader: MediaUploader let pubkey: Pubkey + var size: CGFloat? = 25 + var setup: Bool? = false @Binding var image_url: URL? @ObservedObject var uploadObserver: ImageUploadingObserver let callback: (URL?) -> Void @@ -43,20 +46,53 @@ struct EditPictureControl: View { if uploadObserver.isLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: DamusColors.purple)) + .frame(width: size, height: size) .padding(10) .background(DamusColors.white.opacity(0.7)) .clipShape(Circle()) .shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0) - } else { - Image("camera") - .resizable() - .scaledToFit() - .frame(width: 25, height: 25) - .foregroundColor(DamusColors.purple) - .padding(10) - .background(DamusColors.white.opacity(0.7)) + } else if let url = image_url { + KFAnimatedImage(url) + .imageContext(.pfp, disable_animation: false) + .onFailure(fallbackUrl: URL(string: robohash(pubkey)), cacheKey: url.absoluteString) + .cancelOnDisappear(true) + .configure { view in + view.framePreloadCount = 3 + } + .scaledToFill() + .frame(width: (size ?? 25) + 10, height: (size ?? 25) + 10) + .foregroundColor(DamusColors.white) .clipShape(Circle()) - .shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0) + .overlay(Circle().stroke(.white, lineWidth: 4)) + } else { + if setup ?? false { + Image(systemName: "person") + .resizable() + .scaledToFit() + .frame(width: size, height: size) + .foregroundColor(DamusColors.white) + .padding(20) + .clipShape(Circle()) + .background { + Circle() + .fill(PinkGradient, strokeBorder: .white, lineWidth: 4) + } + + } else { + Image("camera") + .resizable() + .scaledToFit() + .frame(width: size, height: size) + .foregroundColor(DamusColors.purple) + .padding(10) + .background(DamusColors.white.opacity(0.7)) + .clipShape(Circle()) + .background { + Circle() + .fill(DamusColors.purple, strokeBorder: .white, lineWidth: 2) + } + } + } } .sheet(isPresented: $show_camera) { @@ -110,7 +146,7 @@ struct EditPictureControl_Previews: PreviewProvider { let observer = ImageUploadingObserver() ZStack { Color.gray - EditPictureControl(uploader: .nostrBuild, pubkey: test_pubkey, image_url: url, uploadObserver: observer) { _ in + EditPictureControl(uploader: .nostrBuild, pubkey: test_pubkey, size: 100, setup: false, image_url: url, uploadObserver: observer) { _ in // } } diff --git a/damus/Views/SaveKeysView.swift b/damus/Views/SaveKeysView.swift @@ -11,8 +11,6 @@ import Security struct SaveKeysView: View { let account: CreateAccountModel let pool: RelayPool = RelayPool(ndb: Ndb()!) - @State var pub_copied: Bool = false - @State var priv_copied: Bool = false @State var loading: Bool = false @State var error: String? = nil @@ -31,81 +29,98 @@ struct SaveKeysView: View { var body: some View { ZStack(alignment: .top) { VStack(alignment: .center) { + + Spacer() + + Image("logo-nobg") + .resizable() + .shadow(color: DamusColors.purple, radius: 2) + .frame(width: 56, height: 56, alignment: .center) + .padding(.top, 20.0) + if account.rendered_name.isEmpty { Text("Welcome!", comment: "Text to welcome user.") - .font(.title.bold()) - .padding(.bottom, 10) + .font(.title) + .fontWeight(.heavy) + .foregroundStyle(DamusLogoGradient.gradient) } else { Text("Welcome, \(account.rendered_name)!", comment: "Text to welcome user.") - .font(.title.bold()) - .padding(.bottom, 10) + .font(.title) + .fontWeight(.heavy) + .foregroundStyle(DamusLogoGradient.gradient) } - Text("Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.", comment: "Reminder to user that they should save their account information.") - .padding(.bottom, 10) + Text("Save your login info?", comment: "Ask user if they want to save their account information.") + .font(.title) + .fontWeight(.heavy) + .foregroundColor(DamusColors.neutral6) + .padding(.top, 5) - Text("Private Key", comment: "Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account.") - .font(.title2.bold()) - .padding(.bottom, 10) + Text("We'll save your account key, so you won't need to enter it manually next time you log in.", comment: "Reminder to user that they should save their account information.") + .font(.system(size: 14)) + .foregroundColor(DamusColors.neutral6) + .padding(.top, 2) + .padding(.bottom, 100) + .multilineTextAlignment(.center) - Text("This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!", comment: "Label to describe that a private key is the user's secret account key and what they should do with it.") - .padding(.bottom, 10) + Spacer() - SaveKeyView(text: account.privkey.nsec, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused) - .padding(.bottom, 10) - - if priv_copied { - if loading { - ProgressView() - .progressViewStyle(.circular) - } else if let err = error { - Text("Error: \(err)", comment: "Error message indicating why saving keys failed.") - .foregroundColor(.red) - - Button(action: { - complete_account_creation(account) - }) { - HStack { - Text("Retry", comment: "Button to retry completing account creation after an error occurred.") - .fontWeight(.semibold) - } - .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) + if loading { + ProgressView() + .progressViewStyle(.circular) + } else if let err = error { + Text("Error: \(err)", comment: "Error message indicating why saving keys failed.") + .foregroundColor(.red) + + Button(action: { + complete_account_creation(account) + }) { + HStack { + Text("Retry", comment: "Button to retry completing account creation after an error occurred.") + .fontWeight(.semibold) } - .buttonStyle(GradientButtonStyle()) - .padding(.top, 20) - } else { - Button(action: { - complete_account_creation(account) - }) { - HStack { - Text("Let's go!", comment: "Button to complete account creation and start using the app.") - .fontWeight(.semibold) - } - .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) + .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) + } + .buttonStyle(GradientButtonStyle()) + .padding(.top, 20) + } else { + + Button(action: { + save_key(account) + complete_account_creation(account) + }) { + HStack { + Text("Save", comment: "Button to save key, complete account creation, and start using the app.") + .fontWeight(.semibold) + } + .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) + } + .buttonStyle(GradientButtonStyle()) + .padding(.top, 20) + + Button(action: { + complete_account_creation(account) + }) { + HStack { + Text("Not now", comment: "Button to not save key, complete account creation, and start using the app.") + .fontWeight(.semibold) } - .buttonStyle(GradientButtonStyle()) - .padding(.top, 20) + .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) } + .buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 12)) + .padding(.top, 20) } } .padding(20) } - .background( - Image("eula-bg") - .resizable() - .blur(radius: 70) - .ignoresSafeArea(), - alignment: .top - ) + .background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top) .navigationBarBackButtonHidden(true) .navigationBarItems(leading: BackNav()) - .onAppear { - // Hack to force keyboard to show up for a short moment and then hiding it to register password autofill flow. - pubkey_focused = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - pubkey_focused = false - } - } + + } + + func save_key(_ account: CreateAccountModel) { + credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey) } func complete_account_creation(_ account: CreateAccountModel) { @@ -122,8 +137,6 @@ struct SaveKeysView: View { } self.pool.register_handler(sub_id: "signup", handler: handle_event) - - credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey) self.loading = true @@ -188,74 +201,13 @@ struct SaveKeysView: View { } } -struct SaveKeyView: View { - let text: String - let textContentType: UITextContentType - @Binding var is_copied: Bool - var focus: FocusState<Bool>.Binding - - func copy_text() { - UIPasteboard.general.string = text - is_copied = true - } - - var body: some View { - HStack { - Spacer() - VStack { - spacerBlock(width: 0, height: 0) - Button(action: copy_text) { - Label("", image: is_copied ? "check-circle.fill" : "copy2") - .foregroundColor(is_copied ? .green : .gray) - .background { - if is_copied { - Circle() - .foregroundColor(.white) - .frame(width: 25, height: 25, alignment: .center) - .padding(.leading, -8) - .padding(.top, 1) - } else { - EmptyView() - } - } - } - } - - TextField("", text: .constant(text)) - .padding(5) - .background { - RoundedRectangle(cornerRadius: 4.0).opacity(0.1) - } - .textSelection(.enabled) - .font(.callout.monospaced()) - .onTapGesture { - copy_text() - // Hack to force keyboard to hide. Showing keyboard on text field is necessary to register password autofill flow but the text itself should not be modified. - DispatchQueue.main.async { - end_editing() - } - } - .textContentType(textContentType) - .deleteDisabled(true) - .focused(focus) - - spacerBlock(width: 0, height: 0) /// set a 'width' > 0 here to vary key Text's aspect ratio - } - } - - @ViewBuilder private func spacerBlock(width: CGFloat, height: CGFloat) -> some View { - Color.orange.opacity(1) - .frame(width: width, height: height) - } -} - struct SaveKeysView_Previews: PreviewProvider { static var previews: some View { - let model = CreateAccountModel(real: "William", nick: "jb55", about: "I'm me") + let model = CreateAccountModel(display_name: "William", name: "jb55", about: "I'm me") SaveKeysView(account: model) } } func create_account_to_metadata(_ model: CreateAccountModel) -> Profile { - return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil) + return Profile(name: model.name, display_name: model.display_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil) } diff --git a/damus/Views/SetupView.swift b/damus/Views/SetupView.swift @@ -28,32 +28,53 @@ struct SetupView: View { .fontWeight(.heavy) .foregroundStyle(DamusLogoGradient.gradient) - Text("The go-to iOS Nostr client", comment: "Quick description of what Damus is") - .foregroundColor(DamusColors.mediumGrey) + Text("The social network you control", comment: "Quick description of what Damus is") + .foregroundColor(DamusColors.neutral6) .padding(.top, 10) - WhatIsNostr() - .padding() - - WhyWeNeedNostr() - .padding() - Spacer() Button(action: { + navigationCoordinator.push(route: Route.CreateAccount) + }) { + HStack { + Text("Create Account", comment: "Button to continue to the create account page.") + .fontWeight(.semibold) + } + .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) + } + .buttonStyle(GradientButtonStyle()) + .padding(.horizontal) + + Button(action: { navigationCoordinator.push(route: Route.Login) }) { HStack { - Text("Let's get started!", comment: "Button to continue to login page.") + Text("Sign In", comment: "Button to continue to login page.") .fontWeight(.semibold) } .frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center) } .buttonStyle(GradientButtonStyle()) .padding() + + HStack(spacing: 0) { + Text("By continuing you agree to our ") + .font(.subheadline) + .foregroundColor(DamusColors.neutral6) + + Button(action: { + navigationCoordinator.push(route: Route.EULA) + }, label: { + Text("EULA", comment: "End User License Agreement") + .font(.subheadline) + }) + .padding(.vertical, 5) + } + .padding(.bottom) } } - .background(DamusBackground(maxHeight: 300), alignment: .top) + .background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top) .navigationDestination(for: Route.self) { route in route.view(navigationCoordinator: navigationCoordinator, damusState: DamusState.empty) } @@ -63,61 +84,6 @@ struct SetupView: View { } } -struct LearnAboutNostrLink: View { - @Environment(\.openURL) var openURL - var body: some View { - HStack { - Button(action: { - openURL(URL(string: "https://nostr.com")!) - }, label: { - Text("Learn more about Nostr", comment: "Button that opens up a webpage where the user can learn more about Nostr.") - .foregroundColor(.accentColor) - }) - - Image(systemName: "arrow.up.right") - .font(.footnote) - .foregroundColor(.accentColor) - } - } -} - -struct WhatIsNostr: View { - var body: some View { - HStack(alignment: .top) { - Image("nostr-logo") - VStack(alignment: .leading) { - Text("What is Nostr?", comment: "Heading text for section describing what is Nostr.") - .fontWeight(.bold) - .padding(.vertical, 10) - - Text("Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network", comment: "Description about what is Nostr.") - .foregroundColor(DamusColors.mediumGrey) - - LearnAboutNostrLink() - .padding(.top, 10) - } - Spacer() - } - } -} - -struct WhyWeNeedNostr: View { - var body: some View { - HStack(alignment: .top) { - Image("lightbulb") - VStack(alignment: .leading) { - Text("Why we need Nostr?", comment: "Heading text for section describing why Nostr is needed.") - .fontWeight(.bold) - .padding(.vertical, 10) - - Text("Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken", comment: "Description about why Nostr is needed.") - .foregroundColor(DamusColors.mediumGrey) - } - Spacer() - } - } -} - struct SetupView_Previews: PreviewProvider { static var previews: some View { Group {