damus

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

commit b6b6d033a8775b44a71a74c457f4fc5c3e211304
parent 819d7496b23d6b58006bd8b946ea51a5b507803d
Author: Swift <scoder1747@gmail.com>
Date:   Sun, 22 Jan 2023 13:56:14 -0500

User tagging and autocompletion

Co-authored-by: William Casarin <jb55@jb55.com>
Changelog-Added: User tagging and autocompletion in posts
Closes: #347
Fixes: #411, #63

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 12++++++++++++
Mdamus/ContentView.swift | 2+-
Mdamus/Views/PostView.swift | 34++++++++++++++++++++++++++++++++--
Adamus/Views/Posting/UserSearch.swift | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/ReplyView.swift | 2+-
5 files changed, 133 insertions(+), 4 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -171,6 +171,7 @@ 4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */; }; 4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; }; 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; }; + 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; }; 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; }; 6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; }; @@ -426,6 +427,7 @@ 4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = "<group>"; }; 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; }; 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; }; + 4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; }; 4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; }; 5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; }; 6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; }; @@ -587,6 +589,7 @@ 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( + 4CF0ABF42985CD4200D66079 /* Posting */, 4CF0ABDF2981A83000D66079 /* Muting */, 4CC7AAEE297F11B300430951 /* Events */, 3AA24800297E3DAE0090C62D /* Reposts */, @@ -846,6 +849,14 @@ path = AnyCodable; sourceTree = "<group>"; }; + 4CF0ABF42985CD4200D66079 /* Posting */ = { + isa = PBXGroup; + children = ( + 4CF0ABF52985CD5500D66079 /* UserSearch.swift */, + ); + path = Posting; + sourceTree = "<group>"; + }; F7F0BA23297892AE009531F3 /* Modifiers */ = { isa = PBXGroup; children = ( @@ -1014,6 +1025,7 @@ 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, 4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */, + 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */, 4C363AA828297703006E126D /* InsertSort.swift in Sources */, 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */, 4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -302,7 +302,7 @@ struct ContentView: View { case .report(let target): MaybeReportView(target: target) case .post: - PostView(replying_to: nil, references: []) + PostView(replying_to: nil, references: [], damus_state: damus_state!) case .reply(let event): ReplyView(replying_to: event, damus: damus_state!) } diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift @@ -16,10 +16,11 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex struct PostView: View { @State var post: String = "" - - let replying_to: NostrEvent? @FocusState var focus: Bool + + let replying_to: NostrEvent? let references: [ReferencedId] + let damus_state: DamusState @Environment(\.presentationMode) var presentationMode @@ -74,6 +75,7 @@ struct PostView: View { TextEditor(text: $post) .focused($focus) .textInputAutocapitalization(.sentences) + if post.isEmpty { Text(POST_PLACEHOLDER) .padding(.top, 8) @@ -82,6 +84,14 @@ struct PostView: View { .allowsHitTesting(false) } } + + // This if-block observes @ for tagging + if let searching = get_searching_string(post) { + VStack { + Spacer() + UserSearch(damus_state: damus_state, search: searching, post: $post) + }.zIndex(1) + } } .onAppear() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -92,3 +102,23 @@ struct PostView: View { } } +func get_searching_string(_ post: String) -> String? { + guard let last_word = post.components(separatedBy: .whitespacesAndNewlines).last else { + return nil + } + + guard last_word.count >= 2 else { + return nil + } + + guard last_word.first! == "@" else { + return nil + } + + // don't include @npub... strings + guard last_word.count != 64 else { + return nil + } + + return String(last_word.dropFirst()) +} diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift @@ -0,0 +1,87 @@ +// +// UserAutocompletion.swift +// damus +// +// Created by William Casarin on 2023-01-28. +// + +import SwiftUI + +struct SearchedUser: Identifiable { + let petname: String? + let profile: Profile? + let pubkey: String + + var id: String { + return pubkey + } +} + +struct UserSearch: View { + let damus_state: DamusState + let search: String + @Binding var post: String + + var users: [SearchedUser] { + guard let contacts = damus_state.contacts.event else { + return [] + } + + return search_users(profiles: damus_state.profiles, tags: contacts.tags, search: search) + } + + var body: some View { + ScrollView { + LazyVStack { + ForEach(users) { user in + UserView(damus_state: damus_state, pubkey: user.pubkey) + .onTapGesture { + guard let pk = bech32_pubkey(user.pubkey) else { + return + } + post = post.replacingOccurrences(of: "@"+search, with: "@"+pk) + } + } + } + } + } +} + +struct UserSearch_Previews: PreviewProvider { + static let search: String = "jb55" + @State static var post: String = "some @jb55" + + static var previews: some View { + UserSearch(damus_state: test_damus_state(), search: search, post: $post) + } +} + + +func search_users(profiles: Profiles, tags: [[String]], search: String) -> [SearchedUser] { + var seen_user = Set<String>() + return tags.reduce(into: Array<SearchedUser>()) { arr, tag in + guard tag.count >= 2 && tag[0] == "p" else { + return + } + + let pubkey = tag[1] + guard !seen_user.contains(pubkey) else { + return + } + seen_user.insert(pubkey) + + var petname: String? = nil + if tag.count >= 4 { + petname = tag[3] + } + + let profile = profiles.lookup(id: pubkey) + + guard ((petname?.hasPrefix(search) ?? false) || (profile?.name?.hasPrefix(search) ?? false)) else { + return + } + + let searched_user = SearchedUser(petname: petname, profile: profile, pubkey: pubkey) + arr.append(searched_user) + } +} diff --git a/damus/Views/ReplyView.swift b/damus/Views/ReplyView.swift @@ -47,7 +47,7 @@ struct ReplyView: View { ScrollView { EventView(damus: damus, event: replying_to, has_action_bar: false) } - PostView(replying_to: replying_to, references: references) + PostView(replying_to: replying_to, references: gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to), damus_state: damus) } .onAppear { references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)