diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index ad962ff8..e4cce355 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -380,10 +380,6 @@ 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; 50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; }; 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; }; - BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; }; - BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; }; - BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; }; - BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; }; 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; }; 504323A72A34915F006AE6DC /* RelayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504323A62A34915F006AE6DC /* RelayModel.swift */; }; 504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504323A82A3495B6006AE6DC /* RelayModelCache.swift */; }; @@ -423,6 +419,9 @@ BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; }; BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; }; BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; }; + BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; }; + BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; }; + BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; }; BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; }; BA4AB0AE2A63B9270070A32A /* AddEmojiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */; }; BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */; }; @@ -433,9 +432,9 @@ D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; }; D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; }; D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; }; + D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; - D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; }; @@ -446,7 +445,7 @@ E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; }; - F71694EA2A662232001F4053 /* SuggestedUsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694E92A662232001F4053 /* SuggestedUsersView.swift */; }; + F71694EA2A662232001F4053 /* OnboardingSuggestionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */; }; F71694EC2A662292001F4053 /* SuggestedUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */; }; F71694EE2A6624F9001F4053 /* suggested_users.json in Resources */ = {isa = PBXBuildFile; fileRef = F71694ED2A6624F9001F4053 /* suggested_users.json */; }; F71694F22A67314D001F4053 /* SuggestedUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F12A67314D001F4053 /* SuggestedUserView.swift */; }; @@ -944,10 +943,6 @@ 4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttrStringTestExtensions.swift; sourceTree = ""; }; 4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditButton.swift; sourceTree = ""; }; 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = ""; }; - BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = ""; }; - BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = ""; }; - BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = ""; }; - BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = ""; }; 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = ""; }; 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = ""; }; 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = ""; }; @@ -1121,9 +1116,10 @@ BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = ""; }; BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = ""; }; BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = ""; }; + BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = ""; }; + BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = ""; }; + BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = ""; }; BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.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 = ""; }; BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = ""; }; BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = ""; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = ""; }; @@ -1134,6 +1130,8 @@ D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = ""; }; D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = ""; }; 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 = ""; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = ""; }; D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; }; D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = ""; }; @@ -1142,7 +1140,7 @@ E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = ""; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = ""; }; E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = ""; }; - F71694E92A662232001F4053 /* SuggestedUsersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUsersView.swift; sourceTree = ""; }; + F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestionsView.swift; sourceTree = ""; }; F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUsersViewModel.swift; sourceTree = ""; }; F71694ED2A6624F9001F4053 /* suggested_users.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested_users.json; sourceTree = ""; }; F71694F12A67314D001F4053 /* SuggestedUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUserView.swift; sourceTree = ""; }; @@ -1261,7 +1259,6 @@ 4C3EA66C28FF782800C48A62 /* amount.c */, 4C3EA66E28FF787100C48A62 /* overflows.h */, 4C3EA67228FF79F600C48A62 /* structeq.h */, - BA3759952ABCCF360018D73B /* Camera */, 4C3EA67328FF7A2600C48A62 /* cppmagic.h */, 4C3EA67428FF7A5A00C48A62 /* take.c */, 4C3EA67628FF7A9800C48A62 /* talstr.c */, @@ -2333,7 +2330,7 @@ F71694E82A66221E001F4053 /* Onboarding */ = { isa = PBXGroup; children = ( - F71694E92A662232001F4053 /* SuggestedUsersView.swift */, + F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */, F71694F12A67314D001F4053 /* SuggestedUserView.swift */, F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */, F71694ED2A6624F9001F4053 /* suggested_users.json */, @@ -2836,7 +2833,7 @@ BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */, 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */, 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */, - F71694EA2A662232001F4053 /* SuggestedUsersView.swift in Sources */, + F71694EA2A662232001F4053 /* OnboardingSuggestionsView.swift in Sources */, 4C12536A2A76D3850004F4B8 /* RelaysChangedNotify.swift in Sources */, 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 20427736..edda8d50 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -26,7 +26,7 @@ enum Sheets: Identifiable { case select_wallet(SelectWallet) case filter case user_status - case suggestedUsers + case onboardingSuggestions static func zap(target: ZapTarget, lnurl: String) -> Sheets { return .zap(ZapSheet(target: target, lnurl: lnurl)) @@ -45,7 +45,7 @@ enum Sheets: Identifiable { case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id) case .select_wallet: return "select-wallet" case .filter: return "filter" - case .suggestedUsers: return "suggested-users" + case .onboardingSuggestions: return "onboarding-suggestions" } } } @@ -74,7 +74,7 @@ struct ContentView: View { @State private var isSideBarOpened = false var home: HomeModel = HomeModel() @StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator() - @AppStorage("has_seen_suggested_users") private var hasSeenSuggestedUsers = false + @AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false let sub_id = UUID().description @Environment(\.colorScheme) var colorScheme @@ -300,9 +300,9 @@ struct ContentView: View { self.connect() try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers) setup_notifications() - if !hasSeenSuggestedUsers { - active_sheet = .suggestedUsers - hasSeenSuggestedUsers = true + if !hasSeenOnboardingSuggestions { + active_sheet = .onboardingSuggestions + hasSeenOnboardingSuggestions = true } } .sheet(item: $active_sheet) { item in @@ -329,8 +329,8 @@ struct ContentView: View { } else { RelayFilterView(state: damus_state!, timeline: timeline) } - case .suggestedUsers: - SuggestedUsersView(model: SuggestedUsersViewModel(damus_state: damus_state!)) + case .onboardingSuggestions: + OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: damus_state!)) } } .onOpenURL { url in diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift index 97fdc6d5..ab71d839 100644 --- a/damus/Models/DraftsModel.swift +++ b/damus/Models/DraftsModel.swift @@ -7,7 +7,7 @@ import Foundation -class DraftArtifacts { +class DraftArtifacts: Equatable { var content: NSMutableAttributedString var media: [UploadedMedia] @@ -15,6 +15,13 @@ class DraftArtifacts { self.content = content self.media = media } + + static func == (lhs: DraftArtifacts, rhs: DraftArtifacts) -> Bool { + return ( + lhs.media == rhs.media && + lhs.content.string == rhs.content.string // Comparing the text content is not perfect but acceptable in this case because attributes for our post editor are determined purely from text content + ) + } } class Drafts: ObservableObject { diff --git a/damus/Views/Onboarding/OnboardingSuggestionsView.swift b/damus/Views/Onboarding/OnboardingSuggestionsView.swift new file mode 100644 index 00000000..3a4eb307 --- /dev/null +++ b/damus/Views/Onboarding/OnboardingSuggestionsView.swift @@ -0,0 +1,123 @@ +// +// OnboardingSuggestionsView.swift +// damus +// +// Created by klabo on 7/17/23. +// + +import SwiftUI + +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") +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") +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") +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") + +struct OnboardingSuggestionsView: View { + + @StateObject var model: SuggestedUsersViewModel + @State var current_page: Int = 0 + let first_post_examples: [String] = [first_post_example_1, first_post_example_2, first_post_example_3, first_post_example_4] + let initial_text_suffix: String = "\n\n#introductions" + + @Environment(\.presentationMode) private var presentationMode + + func next_page() { + withAnimation { + current_page += 1 + } + } + + var body: some View { + NavigationView { + TabView(selection: $current_page) { + SuggestedUsersPageView(model: model, next_page: self.next_page) + .navigationTitle(NSLocalizedString("Who to Follow", comment: "Title for a screen displaying suggestions of who to follow")) + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(leading: Button(action: { + self.next_page() + }, label: { + Text(NSLocalizedString("Skip", comment: "Button to dismiss the suggested users screen")) + .font(.subheadline.weight(.semibold)) + })) + .tag(0) + + PostView( + action: .posting(.user(model.damus_state.pubkey)), + damus_state: model.damus_state, + prompt_view: { + AnyView( + HStack { + Image(systemName: "sparkles") + Text(NSLocalizedString("Add your first post", comment: "Prompt given to the user during onboarding, suggesting them to write their first post")) + } + .foregroundColor(.secondary) + .font(.callout) + .padding(.top, 10) + ) + }, + placeholder_messages: self.first_post_examples, + initial_text_suffix: self.initial_text_suffix + ) + .tag(1) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + } + } +} + +fileprivate struct SuggestedUsersPageView: View { + var model: SuggestedUsersViewModel + var next_page: (() -> Void) + + var body: some View { + VStack { + List { + ForEach(model.groups) { group in + Section { + ForEach(group.users, id: \.self) { pk in + if let user = model.suggestedUser(pubkey: pk) { + SuggestedUserView(user: user, damus_state: model.damus_state) + } + } + } header: { + SuggestedUsersSectionHeader(group: group, model: model) + } + } + } + .listStyle(.plain) + + Spacer() + + Button(action: { + self.next_page() + }) { + Text(NSLocalizedString("Continue", comment: "Button to dismiss suggested users view and continue to the main app")) + .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) + } + .buttonStyle(GradientButtonStyle()) + .padding([.leading, .trailing], 24) + .padding(.bottom, 16) + } + } +} + +struct SuggestedUsersSectionHeader: View { + let group: SuggestedUserGroup + let model: SuggestedUsersViewModel + var body: some View { + HStack { + Text(group.title.uppercased()) + Spacer() + Button(NSLocalizedString("Follow All", comment: "Button to follow all users in this section")) { + model.follow(pubkeys: group.users) + } + .font(.subheadline.weight(.semibold)) + } + } +} + +struct SuggestedUsersView_Previews: PreviewProvider { + static var previews: some View { + OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: test_damus_state)) + } +} diff --git a/damus/Views/Onboarding/SuggestedUsersView.swift b/damus/Views/Onboarding/SuggestedUsersView.swift deleted file mode 100644 index 4ce268cc..00000000 --- a/damus/Views/Onboarding/SuggestedUsersView.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// SuggestedUsersView.swift -// damus -// -// Created by klabo on 7/17/23. -// - -import SwiftUI - -struct SuggestedUsersView: View { - - @StateObject var model: SuggestedUsersViewModel - - @Environment(\.presentationMode) private var presentationMode - - var body: some View { - NavigationView { - VStack { - List { - ForEach(model.groups) { group in - Section { - ForEach(group.users, id: \.self) { pk in - if let user = model.suggestedUser(pubkey: pk) { - SuggestedUserView(user: user, damus_state: model.damus_state) - } - } - } header: { - SuggestedUsersSectionHeader(group: group, model: model) - } - } - } - .listStyle(.plain) - - Spacer() - - Button(action: { - presentationMode.wrappedValue.dismiss() - }) { - Text(NSLocalizedString("Continue", comment: "Button to dismiss suggested users view and continue to the main app")) - .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) - } - .buttonStyle(GradientButtonStyle()) - .padding([.leading, .trailing], 24) - .padding(.bottom, 16) - } - .navigationTitle(NSLocalizedString("Who to Follow", comment: "Title for a screen displaying suggestions of who to follow")) - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(trailing: Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text(NSLocalizedString("Skip", comment: "Button to dismiss the suggested users screen")) - .font(.subheadline.weight(.semibold)) - })) - } - } -} - -struct SuggestedUsersSectionHeader: View { - let group: SuggestedUserGroup - let model: SuggestedUsersViewModel - var body: some View { - HStack { - Text(group.title.uppercased()) - Spacer() - Button(NSLocalizedString("Follow All", comment: "Button to follow all users in this section")) { - model.follow(pubkeys: group.users) - } - .font(.subheadline.weight(.semibold)) - } - } -} - -struct SuggestedUsersView_Previews: PreviewProvider { - static var previews: some View { - SuggestedUsersView(model: SuggestedUsersViewModel(damus_state: test_damus_state)) - } -} diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index ffcc3003..451007e9 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -62,9 +62,28 @@ struct PostView: View { @StateObject var image_upload: ImageUploadModel = ImageUploadModel() @StateObject var tagModel: TagModel = TagModel() + + @State private var current_placeholder_index = 0 let action: PostAction let damus_state: DamusState + let prompt_view: (() -> AnyView)? + let placeholder_messages: [String] + let initial_text_suffix: String? + + init( + action: PostAction, + damus_state: DamusState, + prompt_view: (() -> AnyView)? = nil, + placeholder_messages: [String]? = nil, + initial_text_suffix: String? = nil + ) { + self.action = action + self.damus_state = damus_state + self.prompt_view = prompt_view + self.placeholder_messages = placeholder_messages ?? [POST_PLACEHOLDER] + self.initial_text_suffix = initial_text_suffix + } @Environment(\.presentationMode) var presentationMode @@ -151,12 +170,10 @@ struct PostView: View { } } .disabled(posting_disabled) - .font(.system(size: 14, weight: .bold)) - .frame(width: 80, height: 30) - .foregroundColor(.white) - .background(LINEAR_GRADIENT) .opacity(posting_disabled ? 0.5 : 1.0) - .clipShape(Capsule()) + .bold() + .buttonStyle(GradientButtonStyle(padding: 10)) + } func isEmpty() -> Bool { @@ -214,12 +231,19 @@ struct PostView: View { var TextEntry: some View { ZStack(alignment: .topLeading) { - TextViewWrapper(attributedText: $post, textHeight: $textHeight, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in - focusWordAttributes = (word, range) - self.newCursorIndex = nil - }, updateCursorPosition: { newCursorIndex in - self.newCursorIndex = newCursorIndex - }) + TextViewWrapper( + attributedText: $post, + textHeight: $textHeight, + initialTextSuffix: initial_text_suffix, + cursorIndex: newCursorIndex, + getFocusWordForMention: { word, range in + focusWordAttributes = (word, range) + self.newCursorIndex = nil + }, + updateCursorPosition: { newCursorIndex in + self.newCursorIndex = newCursorIndex + } + ) .environmentObject(tagModel) .focused($focus) .textInputAutocapitalization(.sentences) @@ -230,22 +254,33 @@ struct PostView: View { .frame(height: get_valid_text_height()) if post.string.isEmpty { - Text(POST_PLACEHOLDER) + Text(self.placeholder_messages[self.current_placeholder_index]) .padding(.top, 8) .padding(.leading, 4) .foregroundColor(Color(uiColor: .placeholderText)) .allowsHitTesting(false) } } + .onAppear { + // Schedule a timer to switch messages every 3 seconds + Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { timer in + withAnimation { + self.current_placeholder_index = (self.current_placeholder_index + 1) % self.placeholder_messages.count + } + } + } } var TopBar: some View { VStack { HStack(spacing: 5.0) { - Button(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note.")) { + Button(action: { self.cancel() - } - .foregroundColor(.primary) + }, label: { + Text(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note.")) + .padding(10) + }) + .buttonStyle(NeutralButtonStyle()) if let error { Text(error) @@ -261,9 +296,14 @@ struct PostView: View { ProgressView(value: progress, total: 1.0) .progressViewStyle(.linear) } + + Divider() + .foregroundColor(DamusColors.neutral3) + .padding(.top, 5) } .frame(height: 30) .padding() + .padding(.top, 15) } func handle_upload(media: MediaUpload) { @@ -312,7 +352,12 @@ struct PostView: View { HStack(alignment: .top) { ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) - TextEntry + VStack(alignment: .leading) { + if let prompt_view { + prompt_view() + } + TextEntry + } } .id("post") @@ -360,6 +405,7 @@ struct PostView: View { } Editor(deviceSize: deviceSize) + .padding(.top, 5) } } .frame(maxHeight: searching == nil ? deviceSize.size.height : 70) diff --git a/damus/Views/TextViewWrapper.swift b/damus/Views/TextViewWrapper.swift index 9ebd140d..35434d89 100644 --- a/damus/Views/TextViewWrapper.swift +++ b/damus/Views/TextViewWrapper.swift @@ -11,6 +11,7 @@ struct TextViewWrapper: UIViewRepresentable { @Binding var attributedText: NSMutableAttributedString @EnvironmentObject var tagModel: TagModel @Binding var textHeight: CGFloat? + let initialTextSuffix: String? let cursorIndex: Int? var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil @@ -74,25 +75,41 @@ struct TextViewWrapper: UIViewRepresentable { } func makeCoordinator() -> Coordinator { - Coordinator(attributedText: $attributedText, getFocusWordForMention: getFocusWordForMention, updateCursorPosition: updateCursorPosition) + Coordinator(attributedText: $attributedText, getFocusWordForMention: getFocusWordForMention, updateCursorPosition: updateCursorPosition, initialTextSuffix: initialTextSuffix) } class Coordinator: NSObject, UITextViewDelegate { @Binding var attributedText: NSMutableAttributedString var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil let updateCursorPosition: ((Int) -> Void) + let initialTextSuffix: String? + var initialTextSuffixWasAdded: Bool = false init(attributedText: Binding, getFocusWordForMention: ((String?, NSRange?) -> Void)?, - updateCursorPosition: @escaping ((Int) -> Void) + updateCursorPosition: @escaping ((Int) -> Void), + initialTextSuffix: String? ) { _attributedText = attributedText self.getFocusWordForMention = getFocusWordForMention self.updateCursorPosition = updateCursorPosition + self.initialTextSuffix = initialTextSuffix } func textViewDidChange(_ textView: UITextView) { - attributedText = NSMutableAttributedString(attributedString: textView.attributedText) + if let initialTextSuffix, !self.initialTextSuffixWasAdded { + self.initialTextSuffixWasAdded = true + var mutable = NSMutableAttributedString(attributedString: textView.attributedText) + let originalRange = textView.selectedRange + addUnattributedText(initialTextSuffix, to: &mutable, inRange: originalRange) + attributedText = mutable + DispatchQueue.main.async { + self.updateCursorPosition(originalRange.location) + } + } + else { + attributedText = NSMutableAttributedString(attributedString: textView.attributedText) + } processFocusedWordForMention(textView: textView) } diff --git a/damusTests/PostViewTests.swift b/damusTests/PostViewTests.swift index 9ca04d20..963cb98e 100644 --- a/damusTests/PostViewTests.swift +++ b/damusTests/PostViewTests.swift @@ -35,6 +35,7 @@ final class PostViewTests: XCTestCase { let textEditorView = TextViewWrapper( attributedText: .constant(NSMutableAttributedString(string: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")), textHeight: textHeightBinding, + initialTextSuffix: nil, cursorIndex: 9, updateCursorPosition: { _ in return } ).environmentObject(tagModel) @@ -157,7 +158,7 @@ func checkMentionLinkEditorHandling( if let expectedNewCursorIndex { XCTAssertEqual(newCursorIndex, expectedNewCursorIndex) } - }) + }, initialTextSuffix: nil) let textView = UITextView() textView.attributedText = content