Add NIP-05 favicon to profile names and NIP-05 web of trust feed
Changelog-Added: Added NIP-05 favicon to profile names and NIP-05 web of trust feed Signed-off-by: Terry Yiu <git@tyiu.xyz>
This commit is contained in:
@@ -5,27 +5,27 @@
|
||||
// Created by William Casarin on 2023-01-11.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct NIP05Badge: View {
|
||||
let nip05: NIP05
|
||||
let pubkey: Pubkey
|
||||
let contacts: Contacts
|
||||
let damus_state: DamusState
|
||||
let show_domain: Bool
|
||||
let profiles: Profiles
|
||||
let nip05_domain_favicon: FaviconURL?
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
init(nip05: NIP05, pubkey: Pubkey, contacts: Contacts, show_domain: Bool, profiles: Profiles) {
|
||||
init(nip05: NIP05, pubkey: Pubkey, damus_state: DamusState, show_domain: Bool, nip05_domain_favicon: FaviconURL?) {
|
||||
self.nip05 = nip05
|
||||
self.pubkey = pubkey
|
||||
self.contacts = contacts
|
||||
self.damus_state = damus_state
|
||||
self.show_domain = show_domain
|
||||
self.profiles = profiles
|
||||
self.nip05_domain_favicon = nip05_domain_favicon
|
||||
}
|
||||
|
||||
var nip05_color: Bool {
|
||||
return use_nip05_color(pubkey: pubkey, contacts: contacts)
|
||||
return use_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
|
||||
}
|
||||
|
||||
var Seal: some View {
|
||||
@@ -44,8 +44,23 @@ struct NIP05Badge: View {
|
||||
}
|
||||
}
|
||||
|
||||
var domainBadge: some View {
|
||||
Group {
|
||||
if let nip05_domain_favicon {
|
||||
KFImage(nip05_domain_favicon.source)
|
||||
.imageContext(.favicon, disable_animation: true)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.clipped()
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var username_matches_nip05: Bool {
|
||||
guard let name = profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
|
||||
guard let name = damus_state.profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -65,14 +80,18 @@ struct NIP05Badge: View {
|
||||
HStack(spacing: 2) {
|
||||
Seal
|
||||
|
||||
if show_domain {
|
||||
Text(nip05_string)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
.onTapGesture {
|
||||
if let nip5url = nip05.siteUrl {
|
||||
openURL(nip5url)
|
||||
}
|
||||
}
|
||||
Group {
|
||||
if show_domain {
|
||||
Text(nip05_string)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
}
|
||||
|
||||
if nip05_domain_favicon != nil {
|
||||
domainBadge
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
damus_state.nav.push(route: Route.NIP05DomainEvents(events: NIP05DomainEventsModel(state: damus_state, domain: nip05.host), nip05_domain_favicon: nip05_domain_favicon))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,13 +117,9 @@ struct NIP05Badge_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state
|
||||
VStack {
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: test_pubkey), show_domain: true, profiles: test_state.profiles)
|
||||
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,7 +686,8 @@ struct ContentView: View {
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
@@ -38,6 +38,10 @@ class Contacts {
|
||||
return friends
|
||||
}
|
||||
|
||||
func get_friend_of_friends_list() -> Set<Pubkey> {
|
||||
return friend_of_friends
|
||||
}
|
||||
|
||||
func get_followed_hashtags() -> Set<String> {
|
||||
guard let ev = self.event else { return Set() }
|
||||
return Set(ev.referenced_hashtags.map({ $0.hashtag }))
|
||||
|
||||
@@ -36,9 +36,10 @@ class DamusState: HeadlessDamusState {
|
||||
var purple: DamusPurple
|
||||
var push_notification_client: PushNotificationClient
|
||||
let emoji_provider: EmojiProvider
|
||||
let favicon_cache: FaviconCache
|
||||
private(set) var nostrNetwork: NostrNetworkManager
|
||||
|
||||
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
|
||||
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
|
||||
self.keypair = keypair
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
@@ -68,7 +69,8 @@ class DamusState: HeadlessDamusState {
|
||||
self.quote_reposts = quote_reposts
|
||||
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
||||
self.emoji_provider = emoji_provider
|
||||
|
||||
self.favicon_cache = FaviconCache()
|
||||
|
||||
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
|
||||
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
|
||||
}
|
||||
@@ -126,7 +128,8 @@ class DamusState: HeadlessDamusState {
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -194,7 +197,8 @@ class DamusState: HeadlessDamusState {
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: .empty,
|
||||
quote_reposts: .init(our_pubkey: empty_pub),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
97
damus/Models/NIP05DomainEventsModel.swift
Normal file
97
damus/Models/NIP05DomainEventsModel.swift
Normal file
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// NIP05DomainEventsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 4/11/25.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Foundation
|
||||
|
||||
class NIP05DomainEventsModel: ObservableObject {
|
||||
let state: DamusState
|
||||
var events: EventHolder
|
||||
@Published var loading: Bool = false
|
||||
|
||||
let domain: String
|
||||
var filter: NostrFilter
|
||||
let sub_id = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
let limit: UInt32 = 500
|
||||
|
||||
init(state: DamusState, domain: String) {
|
||||
self.state = state
|
||||
self.domain = domain
|
||||
self.events = EventHolder(on_queue: { ev in
|
||||
preload_events(state: state, events: [ev])
|
||||
})
|
||||
self.filter = NostrFilter()
|
||||
}
|
||||
|
||||
@MainActor func subscribe() {
|
||||
filter.limit = self.limit
|
||||
filter.kinds = [.text, .longform, .highlight]
|
||||
|
||||
var authors = Set<Pubkey>()
|
||||
for pubkey in state.contacts.get_friend_of_friends_list() {
|
||||
let profile_txn = state.profiles.lookup(id: pubkey)
|
||||
|
||||
guard let profile = profile_txn?.unsafeUnownedValue,
|
||||
let nip05_str = profile.nip05,
|
||||
let nip05 = NIP05.parse(nip05_str),
|
||||
nip05.host.caseInsensitiveCompare(domain) == .orderedSame else {
|
||||
continue
|
||||
}
|
||||
|
||||
authors.insert(pubkey)
|
||||
}
|
||||
if authors.isEmpty {
|
||||
return
|
||||
}
|
||||
filter.authors = Array(authors)
|
||||
|
||||
print("subscribing to notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
|
||||
state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
|
||||
loading = true
|
||||
state.nostrNetwork.pool.send(.subscribe(.init(filters: [filter], sub_id: sub_id)))
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||
loading = false
|
||||
print("unsubscribing from notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
|
||||
}
|
||||
|
||||
func add_event(_ ev: NostrEvent) {
|
||||
if !event_matches_filter(ev, filter: filter) {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(state: state, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.events.insert(ev) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
||||
if sub_id == self.sub_id && ev.is_textlike && ev.should_show_event {
|
||||
self.add_event(ev)
|
||||
}
|
||||
}
|
||||
|
||||
guard done else {
|
||||
return
|
||||
}
|
||||
|
||||
self.loading = false
|
||||
|
||||
if sub_id == self.sub_id {
|
||||
guard let txn = NdbTxn(ndb: state.ndb) else { return }
|
||||
load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ class Profiles {
|
||||
@MainActor
|
||||
private var profiles: [Pubkey: ProfileData] = [:]
|
||||
|
||||
// Map of validated NIP-05 address to pubkey.
|
||||
@MainActor
|
||||
var nip05_pubkey: [String: Pubkey] = [:]
|
||||
|
||||
|
||||
@@ -106,7 +106,8 @@ var test_damus_state: DamusState = ({
|
||||
video: .init(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: our_pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: .init()
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
@@ -29,15 +29,15 @@ extension KFOptionSetter {
|
||||
options.onlyLoadFirstFrame = disable_animation
|
||||
|
||||
switch imageContext {
|
||||
case .pfp:
|
||||
options.diskCacheExpiration = .days(60)
|
||||
break
|
||||
case .banner:
|
||||
options.diskCacheExpiration = .days(5)
|
||||
break
|
||||
case .note:
|
||||
options.diskCacheExpiration = .days(1)
|
||||
break
|
||||
case .pfp, .favicon:
|
||||
options.diskCacheExpiration = .days(60)
|
||||
break
|
||||
case .banner:
|
||||
options.diskCacheExpiration = .days(5)
|
||||
break
|
||||
case .note:
|
||||
options.diskCacheExpiration = .days(1)
|
||||
break
|
||||
}
|
||||
|
||||
return self
|
||||
@@ -82,11 +82,14 @@ enum ImageContext {
|
||||
case pfp
|
||||
case banner
|
||||
case note
|
||||
|
||||
case favicon
|
||||
|
||||
func maxMebibyteSize() -> Int {
|
||||
switch self {
|
||||
case .favicon:
|
||||
return 512_000 // 500KiB
|
||||
case .pfp:
|
||||
return 5_242_880 // 5Mib
|
||||
return 5_242_880 // 5MiB
|
||||
case .banner, .note:
|
||||
return 20_971_520 // 20MiB
|
||||
}
|
||||
@@ -94,6 +97,8 @@ enum ImageContext {
|
||||
|
||||
func downsampleSize() -> CGSize {
|
||||
switch self {
|
||||
case .favicon:
|
||||
return CGSize(width: 18, height: 18)
|
||||
case .pfp:
|
||||
return CGSize(width: 200, height: 200)
|
||||
case .banner:
|
||||
|
||||
41
damus/Util/FaviconCache.swift
Normal file
41
damus/Util/FaviconCache.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// FaviconCache.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 5/23/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import FaviconFinder
|
||||
|
||||
class FaviconCache {
|
||||
private var nip05DomainFavicons: [String: [FaviconURL]] = [:]
|
||||
|
||||
@MainActor
|
||||
func lookup(_ domain: String) async -> [FaviconURL] {
|
||||
let lowercasedDomain = domain.lowercased()
|
||||
if let faviconURLs = nip05DomainFavicons[lowercasedDomain] {
|
||||
return faviconURLs
|
||||
}
|
||||
|
||||
guard let siteURL = URL(string: "https://\(lowercasedDomain)"),
|
||||
let faviconURLs = try? await FaviconFinder(
|
||||
url: siteURL,
|
||||
configuration: .init(
|
||||
preferredSource: .ico, // Prefer using common favicon .ico filenames at root level to avoid scraping HTML when possible.
|
||||
preferences: [
|
||||
.html: FaviconFormatType.appleTouchIcon.rawValue,
|
||||
.ico: "favicon.ico",
|
||||
.webApplicationManifestFile: FaviconFormatType.launcherIcon4x.rawValue
|
||||
]
|
||||
)
|
||||
).fetchFaviconURLs()
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
nip05DomainFavicons[lowercasedDomain] = faviconURLs
|
||||
|
||||
return faviconURLs
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by Scott Penrose on 5/7/23.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import SwiftUI
|
||||
|
||||
enum Route: Hashable {
|
||||
@@ -46,6 +47,8 @@ enum Route: Hashable {
|
||||
case Wallet(wallet: WalletModel)
|
||||
case WalletScanner(result: Binding<WalletScanResult>)
|
||||
case FollowersYouKnow(friendedFollowers: [Pubkey], followers: FollowersModel)
|
||||
case NIP05DomainEvents(events: NIP05DomainEventsModel, nip05_domain_favicon: FaviconURL?)
|
||||
case NIP05DomainPubkeys(domain: String, nip05_domain_favicon: FaviconURL?, pubkeys: [Pubkey])
|
||||
|
||||
@ViewBuilder
|
||||
func view(navigationCoordinator: NavigationCoordinator, damusState: DamusState) -> some View {
|
||||
@@ -127,6 +130,10 @@ enum Route: Hashable {
|
||||
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
|
||||
case .Script(let load_model):
|
||||
LoadScript(pool: damusState.nostrNetwork.pool, model: load_model)
|
||||
case .NIP05DomainEvents(let events, let nip05_domain_favicon):
|
||||
NIP05DomainTimelineView(damus_state: damusState, model: events, nip05_domain_favicon: nip05_domain_favicon)
|
||||
case .NIP05DomainPubkeys(let domain, let nip05_domain_favicon, let pubkeys):
|
||||
NIP05DomainPubkeysView(damus_state: damusState, domain: domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +238,12 @@ enum Route: Hashable {
|
||||
case .Script(let model):
|
||||
hasher.combine("script")
|
||||
hasher.combine(model.data.count)
|
||||
case .NIP05DomainEvents(let events, _):
|
||||
hasher.combine("nip05DomainEvents")
|
||||
hasher.combine(events.domain)
|
||||
case .NIP05DomainPubkeys(let domain, _, _):
|
||||
hasher.combine("nip05DomainPubkeys")
|
||||
hasher.combine(domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
damus/Views/NIP05DomainPubkeysView.swift
Normal file
51
damus/Views/NIP05DomainPubkeysView.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// NIP05DomainPubkeysView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 5/23/25.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct NIP05DomainPubkeysView: View {
|
||||
let damus_state: DamusState
|
||||
let domain: String
|
||||
let nip05_domain_favicon: FaviconURL?
|
||||
let pubkeys: [Pubkey]
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(pubkeys, id: \.self) { pk in
|
||||
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
if let nip05_domain_favicon {
|
||||
KFImage(nip05_domain_favicon.source)
|
||||
.imageContext(.favicon, disable_animation: true)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.clipped()
|
||||
}
|
||||
Text(domain)
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
|
||||
let pubkeys = [test_pubkey, test_pubkey_2]
|
||||
NIP05DomainPubkeysView(damus_state: test_damus_state, domain: "damus.io", nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys)
|
||||
}
|
||||
111
damus/Views/NIP05DomainTimelineHeaderView.swift
Normal file
111
damus/Views/NIP05DomainTimelineHeaderView.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// NIP05DomainTimelineHeaderView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 5/16/25.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct NIP05DomainTimelineHeaderView: View {
|
||||
let damus_state: DamusState
|
||||
@ObservedObject var model: NIP05DomainEventsModel
|
||||
let nip05_domain_favicon: FaviconURL?
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
var Icon: some View {
|
||||
ZStack {
|
||||
if let nip05_domain_favicon {
|
||||
KFImage(nip05_domain_favicon.source)
|
||||
.imageContext(.favicon, disable_animation: true)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.clipped()
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var friendsOfFriends: [Pubkey] {
|
||||
// Order it such that the pubkeys that have events come first in the array so that their profile pictures
|
||||
// show first.
|
||||
let pubkeys = model.events.all_events.map { $0.pubkey } + (model.filter.authors ?? [])
|
||||
|
||||
// Filter out duplicates but retain order, and filter out any that do not have a validated NIP-05.
|
||||
return (NSMutableOrderedSet(array: pubkeys).array as? [Pubkey] ?? [])
|
||||
.filter {
|
||||
damus_state.contacts.is_in_friendosphere($0) && damus_state.profiles.is_validated($0) != nil
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
if nip05_domain_favicon != nil {
|
||||
Icon
|
||||
}
|
||||
|
||||
Text(model.domain)
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
.font(.title.bold())
|
||||
.onTapGesture {
|
||||
if let url = URL(string: "https://\(model.domain)") {
|
||||
openURL(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let friendsOfFriends = friendsOfFriends
|
||||
|
||||
HStack {
|
||||
CondensedProfilePicturesView(state: damus_state, pubkeys: friendsOfFriends, maxPictures: 3)
|
||||
let friendsOfFriendsString = friendsOfFriendsString(friendsOfFriends, ndb: damus_state.ndb)
|
||||
Text(friendsOfFriendsString)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
.onTapGesture {
|
||||
if !friendsOfFriends.isEmpty {
|
||||
damus_state.nav.push(route: Route.NIP05DomainPubkeys(domain: model.domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: friendsOfFriends))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func friendsOfFriendsString(_ friendsOfFriends: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String {
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
let names: [String] = friendsOfFriends.prefix(3).map { pk in
|
||||
let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile
|
||||
return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20)
|
||||
}
|
||||
|
||||
switch friendsOfFriends.count {
|
||||
case 0:
|
||||
return "No one in your trusted network is associated with this domain."
|
||||
case 1:
|
||||
let format = NSLocalizedString("Notes from %@", bundle: bundle, comment: "Text to indicate that notes from one pubkey in our trusted network are shown below.")
|
||||
return String(format: format, locale: locale, names[0])
|
||||
case 2:
|
||||
let format = NSLocalizedString("Notes from %@ & %@", bundle: bundle, comment: "Text to indicate that notes from two pubkeys in our trusted network are shown below.")
|
||||
return String(format: format, locale: locale, names[0], names[1])
|
||||
case 3:
|
||||
let format = NSLocalizedString("Notes from %@, %@ & %@", bundle: bundle, comment: "Text to indicate that notes from three pubkeys in our trusted network are shown below.")
|
||||
return String(format: format, locale: locale, names[0], names[1], names[2])
|
||||
default:
|
||||
let format = localizedStringFormat(key: "notes_from_three_and_others", locale: locale)
|
||||
return String(format: format, locale: locale, friendsOfFriends.count - 3, names[0], names[1], names[2])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let model = NIP05DomainEventsModel(state: test_damus_state, domain: "damus.io")
|
||||
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
|
||||
NIP05DomainTimelineHeaderView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
|
||||
}
|
||||
64
damus/Views/NIP05DomainTimelineView.swift
Normal file
64
damus/Views/NIP05DomainTimelineView.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// NIP05DomainTimelineView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 4/11/25.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct NIP05DomainTimelineView: View {
|
||||
let damus_state: DamusState
|
||||
@ObservedObject var model: NIP05DomainEventsModel
|
||||
let nip05_domain_favicon: FaviconURL?
|
||||
|
||||
func nip05_filter(ev: NostrEvent) -> Bool {
|
||||
damus_state.contacts.is_in_friendosphere(ev.pubkey) && damus_state.profiles.is_validated(ev.pubkey) != nil
|
||||
}
|
||||
|
||||
var contentFilters: ContentFilters {
|
||||
var filters = Array<(NostrEvent) -> Bool>()
|
||||
filters.append(contentsOf: ContentFilters.defaults(damus_state: damus_state))
|
||||
filters.append(nip05_filter)
|
||||
return ContentFilters(filters: filters)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let height: CGFloat = 250.0
|
||||
|
||||
TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: contentFilters.filter(ev:)) {
|
||||
ZStack(alignment: .leading) {
|
||||
DamusBackground(maxHeight: height)
|
||||
.mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom))
|
||||
NIP05DomainTimelineHeaderView(damus_state: damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
|
||||
.padding(.leading, 30)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.padding(.bottom, tabHeight)
|
||||
.onAppear {
|
||||
guard model.events.all_events.isEmpty else { return }
|
||||
|
||||
model.subscribe()
|
||||
|
||||
if let pubkeys = model.filter.authors {
|
||||
for pubkey in pubkeys {
|
||||
check_nip05_validity(pubkey: pubkey, profiles: damus_state.profiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
model.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let damus_state = test_damus_state
|
||||
let model = NIP05DomainEventsModel(state: damus_state, domain: "damus.io")
|
||||
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
|
||||
NIP05DomainTimelineView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by William Casarin on 2022-04-16.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import SwiftUI
|
||||
|
||||
enum FriendType {
|
||||
@@ -43,6 +44,7 @@ struct ProfileName: View {
|
||||
@State var nip05: NIP05?
|
||||
@State var donation: Int?
|
||||
@State var purple_account: DamusPurple.Account?
|
||||
@State var nip05_domain_favicon: FaviconURL?
|
||||
|
||||
init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true, supporterBadgeStyle: SupporterBadge.Style = .compact) {
|
||||
self.pubkey = pubkey
|
||||
@@ -61,7 +63,7 @@ struct ProfileName: View {
|
||||
var current_nip05: NIP05? {
|
||||
nip05 ?? damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
|
||||
|
||||
func current_display_name(profile: Profile?) -> DisplayName {
|
||||
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
}
|
||||
@@ -101,7 +103,7 @@ struct ProfileName: View {
|
||||
.fontWeight(prefix == "@" ? .none : .bold)
|
||||
|
||||
if let nip05 = current_nip05 {
|
||||
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, profiles: damus_state.profiles)
|
||||
NIP05Badge(nip05: nip05, pubkey: pubkey, damus_state: damus_state, show_domain: show_nip5_domain, nip05_domain_favicon: nip05_domain_favicon)
|
||||
}
|
||||
|
||||
if let friend = friend_type, current_nip05 == nil {
|
||||
@@ -118,9 +120,15 @@ struct ProfileName: View {
|
||||
|
||||
}
|
||||
.task {
|
||||
if damus_state.purple.enable_purple {
|
||||
self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey)
|
||||
}
|
||||
if damus_state.purple.enable_purple {
|
||||
self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
if let domain = current_nip05?.host {
|
||||
self.nip05_domain_favicon = try? await damus_state.favicon_cache.lookup(domain)
|
||||
.largest()
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { update in
|
||||
if update.pubkey != pubkey {
|
||||
@@ -151,6 +159,24 @@ struct ProfileName: View {
|
||||
let nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
if nip05 != self.nip05 {
|
||||
self.nip05 = nip05
|
||||
|
||||
if let domain = nip05?.host {
|
||||
Task {
|
||||
let favicon = try? await damus_state.favicon_cache.lookup(domain)
|
||||
.filter {
|
||||
if let size = $0.size {
|
||||
return size.width <= 128 && size.height <= 128
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
.largest()
|
||||
|
||||
await MainActor.run {
|
||||
self.nip05_domain_favicon = favicon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if donation != profile.damus_donation {
|
||||
|
||||
@@ -82,6 +82,22 @@
|
||||
<string>Imports</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>notes_from_three_and_others</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@OTHERS@</string>
|
||||
<key>OTHERS</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Notes from %2$@, %3$@, %4$@ & %1$d other in your trusted network</string>
|
||||
<key>other</key>
|
||||
<string>Notes from %2$@, %3$@, %4$@ & %1$d others in your trusted network</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>people_reposted_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
Reference in New Issue
Block a user