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
This commit is contained in:
@@ -171,6 +171,7 @@
|
|||||||
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */; };
|
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */; };
|
||||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; };
|
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; };
|
||||||
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.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 */; };
|
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
|
||||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||||
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
|
||||||
@@ -587,6 +589,7 @@
|
|||||||
4C75EFA227FA576C0006080F /* Views */ = {
|
4C75EFA227FA576C0006080F /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4CF0ABF42985CD4200D66079 /* Posting */,
|
||||||
4CF0ABDF2981A83000D66079 /* Muting */,
|
4CF0ABDF2981A83000D66079 /* Muting */,
|
||||||
4CC7AAEE297F11B300430951 /* Events */,
|
4CC7AAEE297F11B300430951 /* Events */,
|
||||||
3AA24800297E3DAE0090C62D /* Reposts */,
|
3AA24800297E3DAE0090C62D /* Reposts */,
|
||||||
@@ -846,6 +849,14 @@
|
|||||||
path = AnyCodable;
|
path = AnyCodable;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4CF0ABF42985CD4200D66079 /* Posting */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4CF0ABF52985CD5500D66079 /* UserSearch.swift */,
|
||||||
|
);
|
||||||
|
path = Posting;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
F7F0BA23297892AE009531F3 /* Modifiers */ = {
|
F7F0BA23297892AE009531F3 /* Modifiers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1014,6 +1025,7 @@
|
|||||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
||||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
||||||
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */,
|
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */,
|
||||||
|
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
|
||||||
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
||||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
||||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
|
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ struct ContentView: View {
|
|||||||
case .report(let target):
|
case .report(let target):
|
||||||
MaybeReportView(target: target)
|
MaybeReportView(target: target)
|
||||||
case .post:
|
case .post:
|
||||||
PostView(replying_to: nil, references: [])
|
PostView(replying_to: nil, references: [], damus_state: damus_state!)
|
||||||
case .reply(let event):
|
case .reply(let event):
|
||||||
ReplyView(replying_to: event, damus: damus_state!)
|
ReplyView(replying_to: event, damus: damus_state!)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
|
|||||||
|
|
||||||
struct PostView: View {
|
struct PostView: View {
|
||||||
@State var post: String = ""
|
@State var post: String = ""
|
||||||
|
|
||||||
let replying_to: NostrEvent?
|
|
||||||
@FocusState var focus: Bool
|
@FocusState var focus: Bool
|
||||||
|
|
||||||
|
let replying_to: NostrEvent?
|
||||||
let references: [ReferencedId]
|
let references: [ReferencedId]
|
||||||
|
let damus_state: DamusState
|
||||||
|
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ struct PostView: View {
|
|||||||
TextEditor(text: $post)
|
TextEditor(text: $post)
|
||||||
.focused($focus)
|
.focused($focus)
|
||||||
.textInputAutocapitalization(.sentences)
|
.textInputAutocapitalization(.sentences)
|
||||||
|
|
||||||
if post.isEmpty {
|
if post.isEmpty {
|
||||||
Text(POST_PLACEHOLDER)
|
Text(POST_PLACEHOLDER)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
@@ -82,6 +84,14 @@ struct PostView: View {
|
|||||||
.allowsHitTesting(false)
|
.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() {
|
.onAppear() {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
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())
|
||||||
|
}
|
||||||
|
|||||||
87
damus/Views/Posting/UserSearch.swift
Normal file
87
damus/Views/Posting/UserSearch.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ struct ReplyView: View {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
EventView(damus: damus, event: replying_to, has_action_bar: false)
|
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 {
|
.onAppear {
|
||||||
references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)
|
references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)
|
||||||
|
|||||||
Reference in New Issue
Block a user