Add nip05 search
Changelog-Added: Added ability to lookup users by nip05 identifiers
This commit is contained in:
@@ -181,6 +181,7 @@
|
|||||||
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
|
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
|
||||||
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
|
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
|
||||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
|
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
|
||||||
|
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */; };
|
||||||
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
|
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
|
||||||
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
|
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
|
||||||
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; };
|
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; };
|
||||||
@@ -566,6 +567,7 @@
|
|||||||
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
|
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
|
||||||
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
|
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
|
||||||
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
|
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
|
||||||
|
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncedOnChange.swift; sourceTree = "<group>"; };
|
||||||
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
|
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
|
||||||
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
|
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
|
||||||
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
|
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
|
||||||
@@ -938,6 +940,7 @@
|
|||||||
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
|
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
|
||||||
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
|
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
|
||||||
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
|
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
|
||||||
|
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1593,6 +1596,7 @@
|
|||||||
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
|
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
|
||||||
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
|
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
|
||||||
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
|
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
|
||||||
|
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
|
||||||
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
||||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
|
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
|
||||||
|
|||||||
@@ -675,6 +675,7 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
|
|||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
profiles.validated[ev.pubkey] = validated
|
profiles.validated[ev.pubkey] = validated
|
||||||
|
profiles.nip05_pubkey[nip05] = ev.pubkey
|
||||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import UIKit
|
|||||||
class Profiles {
|
class Profiles {
|
||||||
var profiles: [String: TimestampedProfile] = [:]
|
var profiles: [String: TimestampedProfile] = [:]
|
||||||
var validated: [String: NIP05] = [:]
|
var validated: [String: NIP05] = [:]
|
||||||
|
var nip05_pubkey: [String: String] = [:]
|
||||||
var zappers: [String: String] = [:]
|
var zappers: [String: String] = [:]
|
||||||
|
|
||||||
func is_validated(_ pk: String) -> NIP05? {
|
func is_validated(_ pk: String) -> NIP05? {
|
||||||
|
|||||||
69
damus/Util/DebouncedOnChange.swift
Normal file
69
damus/Util/DebouncedOnChange.swift
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// https://github.com/Tunous/DebouncedOnChange/blob/5670ea13e8ad33e9cc3197f6d13ce492dc0e46ab/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
/// Adds a modifier for this view that fires an action only when a time interval in seconds represented by
|
||||||
|
/// `debounceTime` elapses between value changes.
|
||||||
|
///
|
||||||
|
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
|
||||||
|
/// action /// will be scheduled to run after that time passes again. This mean that the action will only execute
|
||||||
|
/// after changes to the value /// stay unmodified for the specified `debounceTime` in seconds.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - value: The value to check against when determining whether to run the closure.
|
||||||
|
/// - debounceTime: The time in seconds to wait after each value change before running `action` closure.
|
||||||
|
/// - action: A closure to run when the value changes.
|
||||||
|
/// - Returns: A view that fires an action after debounced time when the specified value changes.
|
||||||
|
public func onChange<Value>(
|
||||||
|
of value: Value,
|
||||||
|
debounceTime: TimeInterval,
|
||||||
|
perform action: @escaping (_ newValue: Value) -> Void
|
||||||
|
) -> some View where Value: Equatable {
|
||||||
|
self.modifier(DebouncedChangeViewModifier(trigger: value, debounceTime: debounceTime, action: action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DebouncedChangeViewModifier<Value>: ViewModifier where Value: Equatable {
|
||||||
|
let trigger: Value
|
||||||
|
let debounceTime: TimeInterval
|
||||||
|
let action: (Value) -> Void
|
||||||
|
|
||||||
|
@State private var debouncedTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content.onChange(of: trigger) { value in
|
||||||
|
debouncedTask?.cancel()
|
||||||
|
debouncedTask = Task.delayed(seconds: debounceTime) { @MainActor in
|
||||||
|
action(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Task {
|
||||||
|
|
||||||
|
/// Asynchronously runs the given `operation` in its own task after the specified number of `seconds`.
|
||||||
|
///
|
||||||
|
/// The operation will be executed after specified number of `seconds` passes. You can cancel the task earlier
|
||||||
|
/// for the operation to be skipped.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - time: Delay time in seconds.
|
||||||
|
/// - operation: The operation to execute.
|
||||||
|
/// - Returns: Handle to the task which can be cancelled.
|
||||||
|
@discardableResult
|
||||||
|
public static func delayed(
|
||||||
|
seconds: TimeInterval,
|
||||||
|
operation: @escaping @Sendable () async -> Void
|
||||||
|
) -> Self where Success == Void, Failure == Never {
|
||||||
|
Self {
|
||||||
|
do {
|
||||||
|
try await Task<Never, Never>.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
||||||
|
await operation()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,11 +39,20 @@ enum NIP05Validation {
|
|||||||
case valid
|
case valid
|
||||||
}
|
}
|
||||||
|
|
||||||
func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? {
|
struct FetchedNIP05 {
|
||||||
|
let response: NIP05Response
|
||||||
|
let nip05: NIP05Response
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch_nip05_str(nip05_str: String) async -> NIP05Response? {
|
||||||
guard let nip05 = NIP05.parse(nip05_str) else {
|
guard let nip05 = NIP05.parse(nip05_str) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await fetch_nip05(nip05: nip05)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch_nip05(nip05: NIP05) async -> NIP05Response? {
|
||||||
guard let url = nip05.url else {
|
guard let url = nip05.url else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -57,6 +66,18 @@ func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? {
|
||||||
|
guard let nip05 = NIP05.parse(nip05_str) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let decoded = await fetch_nip05(nip05: nip05) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
guard let stored_pk = decoded.names[nip05.username] else {
|
guard let stored_pk = decoded.names[nip05.username] else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ enum SearchState {
|
|||||||
enum SearchType {
|
enum SearchType {
|
||||||
case event
|
case event
|
||||||
case profile
|
case profile
|
||||||
|
case nip05
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchingEventView: View {
|
struct SearchingEventView: View {
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
let evid: String
|
let evid: String
|
||||||
let search_type: SearchType
|
let search_type: SearchType
|
||||||
|
|
||||||
@State var search_state: SearchState = .searching
|
@State var search_state: SearchState = .searching
|
||||||
|
|
||||||
var bech32_evid: String {
|
var bech32_evid: String {
|
||||||
@@ -35,6 +37,8 @@ struct SearchingEventView: View {
|
|||||||
|
|
||||||
var search_name: String {
|
var search_name: String {
|
||||||
switch search_type {
|
switch search_type {
|
||||||
|
case .nip05:
|
||||||
|
return "nip05"
|
||||||
case .profile:
|
case .profile:
|
||||||
return "profile"
|
return "profile"
|
||||||
case .event:
|
case .event:
|
||||||
@@ -67,9 +71,39 @@ struct SearchingEventView: View {
|
|||||||
Text("\(search_name.capitalized) not found", comment: "When a note or profile is not found when searching for it via its note id")
|
Text("\(search_name.capitalized) not found", comment: "When a note or profile is not found when searching for it via its note id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onChange(of: evid, debounceTime: 0.5) { evid in
|
||||||
|
self.search_state = .searching
|
||||||
|
|
||||||
switch search_type {
|
switch search_type {
|
||||||
|
case .nip05:
|
||||||
|
if let pk = state.profiles.nip05_pubkey[evid] {
|
||||||
|
if state.profiles.lookup(id: pk) != nil {
|
||||||
|
self.search_state = .found_profile(pk)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Task.init {
|
||||||
|
guard let nip05 = NIP05.parse(evid) else {
|
||||||
|
self.search_state = .not_found
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let nip05_resp = await fetch_nip05(nip05: nip05) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.search_state = .not_found
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let pk = nip05_resp.names[nip05.username] else {
|
||||||
|
self.search_state = .not_found
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.search_state = .found_profile(pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case .event:
|
case .event:
|
||||||
if let ev = state.events.lookup(evid) {
|
if let ev = state.events.lookup(evid) {
|
||||||
self.search_state = .found(ev)
|
self.search_state = .found(ev)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ enum Search {
|
|||||||
case hashtag(String)
|
case hashtag(String)
|
||||||
case profile(String)
|
case profile(String)
|
||||||
case note(String)
|
case note(String)
|
||||||
|
case nip05(String)
|
||||||
case hex(String)
|
case hex(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +42,10 @@ struct SearchResultsView: View {
|
|||||||
NavigationLink(destination: dst) {
|
NavigationLink(destination: dst) {
|
||||||
Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.")
|
Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .nip05(let addr):
|
||||||
|
SearchingEventView(state: damus_state, evid: addr, search_type: .nip05)
|
||||||
|
|
||||||
case .profile(let prof):
|
case .profile(let prof):
|
||||||
let decoded = try? bech32_decode(prof)
|
let decoded = try? bech32_decode(prof)
|
||||||
let hex = hex_encode(decoded!.data)
|
let hex = hex_encode(decoded!.data)
|
||||||
@@ -95,6 +100,12 @@ func search_for_string(profiles: Profiles, _ new: String) -> Search? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let splitted = new.split(separator: "@")
|
||||||
|
|
||||||
|
if splitted.count == 2 {
|
||||||
|
return .nip05(new)
|
||||||
|
}
|
||||||
|
|
||||||
if new.first! == "#" {
|
if new.first! == "#" {
|
||||||
let ht = String(new.dropFirst().filter{$0 != " "})
|
let ht = String(new.dropFirst().filter{$0 != " "})
|
||||||
return .hashtag(ht)
|
return .hashtag(ht)
|
||||||
|
|||||||
Reference in New Issue
Block a user