1
0
mirror of git://jb55.com/damus synced 2024-09-30 00:40:45 +00:00

onboarding: Suggest first post during onboarding

Testing of standard flow
------------------------

PASS

Device: iPhone 14 Pro simulator
iOS: 17.0
Damus: This commit
Steps:
1. Delete and reinstall Damus
2. Go through onboarding until suggested users appear
3. Click "continue". Should slide into the post view. PASS
4. Post view should look similar to the Figma design file, but with examples as placeholders. PASS
5. Examples should switch every 3 seconds. PASS
6. Typing a first character causes the #introductions hashtag to be automatically added. PASS
7. Uploading an image makes progress view show up and not break layout. PASS
8. Clicking on "post" should post this note and dismiss onboarding view. PASS

Testing of other flows
----------------------

PASS

Device: iPhone 14 Pro simulator
iOS: 17.0
Damus: This commit
Special remark: Made local change to always show the onboarding suggestions, and speed up testing
Coverage:

1. Clicking "skip" on suggested users view will skip into the post view. PASS
2. Clicking "cancel" on post view and then going to the normal post view reveals a blank draft. PASS
3. Clicking "cancel" dismisses onboarding view and does not post anything. PASS
4. Normal post view looks normal (not broken). PASS
5. Changing initial suggested post during onboarding, cancelling the post, and then re-entering normal post view reveals the draft with user modifications. PASS

Changelog-Added: Suggest first post during onboarding
Closes: https://github.com/damus-io/damus/issues/1338
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
Daniel D’Aquino 2023-10-20 18:15:58 +00:00 committed by William Casarin
parent 7c98489904
commit bf43842590
8 changed files with 236 additions and 122 deletions

View File

@ -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 = "<group>"; };
4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditButton.swift; sourceTree = "<group>"; };
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; };
BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = "<group>"; };
BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = "<group>"; };
BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = "<group>"; };
BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; };
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
@ -1121,9 +1116,10 @@
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = "<group>"; };
BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = "<group>"; };
BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = "<group>"; };
BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = "<group>"; };
BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
@ -1134,6 +1130,8 @@
D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = "<group>"; };
D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = "<group>"; };
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = "<group>"; };
D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = "<group>"; };
@ -1142,7 +1140,7 @@
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
F71694E92A662232001F4053 /* SuggestedUsersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUsersView.swift; sourceTree = "<group>"; };
F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestionsView.swift; sourceTree = "<group>"; };
F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUsersViewModel.swift; sourceTree = "<group>"; };
F71694ED2A6624F9001F4053 /* suggested_users.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested_users.json; sourceTree = "<group>"; };
F71694F12A67314D001F4053 /* SuggestedUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUserView.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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

View File

@ -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 {

View File

@ -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 🤙. Whats 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! Im 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! Im a graphic designer during the day and coder at night, but Im 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))
}
}

View File

@ -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))
}
}

View File

@ -63,8 +63,27 @@ 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
TextViewWrapper(
attributedText: $post,
textHeight: $textHeight,
initialTextSuffix: initial_text_suffix,
cursorIndex: newCursorIndex,
getFocusWordForMention: { word, range in
focusWordAttributes = (word, range)
self.newCursorIndex = nil
}, updateCursorPosition: { newCursorIndex in
},
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,8 +352,13 @@ 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)
VStack(alignment: .leading) {
if let prompt_view {
prompt_view()
}
TextEntry
}
}
.id("post")
PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width)
@ -360,6 +405,7 @@ struct PostView: View {
}
Editor(deviceSize: deviceSize)
.padding(.top, 5)
}
}
.frame(maxHeight: searching == nil ? deviceSize.size.height : 70)

View File

@ -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<NSMutableAttributedString>,
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) {
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)
}

View File

@ -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