damus

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

ProfileView.swift (25948B)


      1 //
      2 //  ProfileView.swift
      3 //  damus
      4 //
      5 //  Created by William Casarin on 2022-04-23.
      6 //
      7 
      8 import SwiftUI
      9 
     10 func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> String {
     11     switch fs {
     12     case .follows:
     13         return NSLocalizedString("Unfollow", comment: "Button to unfollow a user.")
     14     case .following:
     15         return NSLocalizedString("Following...", comment: "Label to indicate that the user is in the process of following another user.")
     16     case .unfollowing:
     17         return NSLocalizedString("Unfollowing...", comment: "Label to indicate that the user is in the process of unfollowing another user.")
     18     case .unfollows:
     19         if follows_you {
     20             return NSLocalizedString("Follow Back", comment: "Button to follow a user back.")
     21         } else {
     22             return NSLocalizedString("Follow", comment: "Button to follow a user.")
     23         }
     24     }
     25 }
     26 
     27 func followedByString(_ friend_intersection: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String {
     28     let bundle = bundleForLocale(locale: locale)
     29     let names: [String] = friend_intersection.prefix(3).map { pk in
     30         let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile
     31         return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20)
     32     }
     33 
     34     switch friend_intersection.count {
     35     case 0:
     36         return ""
     37     case 1:
     38         let format = NSLocalizedString("Followed by %@", bundle: bundle, comment: "Text to indicate that the user is followed by one of our follows.")
     39         return String(format: format, locale: locale, names[0])
     40     case 2:
     41         let format = NSLocalizedString("Followed by %@ & %@", bundle: bundle, comment: "Text to indicate that the user is followed by two of our follows.")
     42         return String(format: format, locale: locale, names[0], names[1])
     43     case 3:
     44         let format = NSLocalizedString("Followed by %@, %@ & %@", bundle: bundle, comment: "Text to indicate that the user is followed by three of our follows.")
     45         return String(format: format, locale: locale, names[0], names[1], names[2])
     46     default:
     47         let format = localizedStringFormat(key: "followed_by_three_and_others", locale: locale)
     48         return String(format: format, locale: locale, friend_intersection.count - 3, names[0], names[1], names[2])
     49     }
     50 }
     51 
     52 struct VisualEffectView: UIViewRepresentable {
     53     var effect: UIVisualEffect?
     54     var darkeningOpacity: CGFloat = 0.3 // degree of darkening
     55 
     56     func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView {
     57         let effectView = UIVisualEffectView()
     58         effectView.backgroundColor = UIColor.black.withAlphaComponent(darkeningOpacity)
     59         return effectView
     60     }
     61 
     62     func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) {
     63         uiView.effect = effect
     64         uiView.backgroundColor = UIColor.black.withAlphaComponent(darkeningOpacity)
     65     }
     66 }
     67 
     68 struct ProfileView: View {
     69     let damus_state: DamusState
     70     let pfp_size: CGFloat = 90.0
     71     let bannerHeight: CGFloat = 150.0
     72 
     73     @State var is_zoomed: Bool = false
     74     @State var show_share_sheet: Bool = false
     75     @State var show_qr_code: Bool = false
     76     @State var action_sheet_presented: Bool = false
     77     @State var mute_dialog_presented: Bool = false
     78     @State var filter_state : FilterState = .posts
     79     @State var yOffset: CGFloat = 0
     80 
     81     @StateObject var profile: ProfileModel
     82     @StateObject var followers: FollowersModel
     83     @StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
     84 
     85     init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
     86         self.damus_state = damus_state
     87         self._profile = StateObject(wrappedValue: profile)
     88         self._followers = StateObject(wrappedValue: followers)
     89     }
     90 
     91     init(damus_state: DamusState, pubkey: Pubkey) {
     92         self.damus_state = damus_state
     93         self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
     94         self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey))
     95     }
     96 
     97     @Environment(\.dismiss) var dismiss
     98     @Environment(\.colorScheme) var colorScheme
     99     @Environment(\.presentationMode) var presentationMode
    100 
    101     func imageBorderColor() -> Color {
    102         colorScheme == .light ? DamusColors.white : DamusColors.black
    103     }
    104 
    105     func bannerBlurViewOpacity() -> Double  {
    106         let progress = -(yOffset + navbarHeight) / 100
    107         return Double(-yOffset > navbarHeight ? progress : 0)
    108     }
    109     
    110     func getProfileInfo() -> (String, String) {
    111         let profile_txn = self.damus_state.profiles.lookup(id: profile.pubkey)
    112         let ndbprofile = profile_txn?.unsafeUnownedValue
    113         let displayName = Profile.displayName(profile: ndbprofile, pubkey: profile.pubkey).displayName.truncate(maxLength: 25)
    114         let userName = Profile.displayName(profile: ndbprofile, pubkey: profile.pubkey).username.truncate(maxLength: 25)
    115         return (displayName, "@\(userName)")
    116     }
    117     
    118     func showFollowBtnInBlurrBanner() -> Bool {
    119         damus_state.contacts.follow_state(profile.pubkey) == .unfollows && bannerBlurViewOpacity() > 1.0
    120     }
    121     
    122     func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
    123         var filters = ContentFilters.defaults(damus_state: damus_state)
    124         filters.append(fstate.filter)
    125         switch fstate {
    126         case .posts, .posts_and_replies:
    127             filters.append({ profile.pubkey == $0.pubkey })
    128         case .conversations:
    129             filters.append({ profile.conversation_events.contains($0.id) } )
    130         }
    131         return ContentFilters(filters: filters).filter
    132     }
    133 
    134     var bannerSection: some View {
    135         GeometryReader { proxy -> AnyView in
    136 
    137             let minY = proxy.frame(in: .global).minY
    138 
    139             DispatchQueue.main.async {
    140                 self.yOffset = minY
    141             }
    142 
    143             return AnyView(
    144                 VStack(spacing: 0) {
    145                     ZStack {
    146                         BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
    147                             .aspectRatio(contentMode: .fill)
    148                             .frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight)
    149                             .clipped()
    150 
    151                         VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity())
    152                     }
    153 
    154                     Divider().opacity(bannerBlurViewOpacity())
    155                 }
    156                 .frame(height: minY > 0 ? bannerHeight + minY : nil)
    157                 .offset(y: minY > 0 ? -minY : -minY < navbarHeight ? 0 : -minY - navbarHeight)
    158             )
    159 
    160         }
    161         .frame(height: bannerHeight)
    162         .allowsHitTesting(false)
    163     }
    164 
    165     var navbarHeight: CGFloat {
    166         return 100.0 - (Theme.safeAreaInsets?.top ?? 0)
    167     }
    168 
    169     func navImage(img: String) -> some View {
    170         Image(img)
    171             .frame(width: 33, height: 33)
    172             .background(Color.black.opacity(0.6))
    173             .clipShape(Circle())
    174     }
    175 
    176     var navBackButton: some View {
    177         Button {
    178             presentationMode.wrappedValue.dismiss()
    179         } label: {
    180             navImage(img: "chevron-left")
    181         }
    182     }
    183 
    184     var navActionSheetButton: some View {
    185         Button(action: {
    186             action_sheet_presented = true
    187         }) {
    188             Image(systemName: "ellipsis")
    189                 .frame(width: 33, height: 33)
    190                 .background(Color.black.opacity(0.6))
    191                 .clipShape(Circle())
    192         }
    193         .confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or mute a profile."), isPresented: $action_sheet_presented) {
    194             Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
    195                 show_share_sheet = true
    196             }
    197 
    198             Button(NSLocalizedString("QR Code", comment: "Button to view profile's qr code.")) {
    199                 show_qr_code = true
    200             }
    201 
    202             // Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
    203             if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
    204                 Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
    205                     notify(.report(.user(profile.pubkey)))
    206                 }
    207 
    208                 if damus_state.mutelist_manager.is_muted(.user(profile.pubkey, nil)) {
    209                     Button(NSLocalizedString("Unmute", comment: "Button to unmute a profile.")) {
    210                         guard
    211                             let keypair = damus_state.keypair.to_full(),
    212                             let mutelist = damus_state.mutelist_manager.event
    213                         else {
    214                             return
    215                         }
    216 
    217                         guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .user(profile.pubkey, nil)) else {
    218                             return
    219                         }
    220 
    221                         damus_state.mutelist_manager.set_mutelist(new_ev)
    222                         damus_state.postbox.send(new_ev)
    223                     }
    224                 } else {
    225                     Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
    226                         mute_dialog_presented = true
    227                     }
    228                 }
    229             }
    230         }
    231         .confirmationDialog(NSLocalizedString("Mute", comment: "Title for confirmation dialog to mute a profile."), isPresented: $mute_dialog_presented) {
    232             ForEach(DamusDuration.allCases, id: \.self) { duration in
    233                 Button {
    234                     notify(.mute(.user(profile.pubkey, duration.date_from_now)))
    235                 } label: {
    236                     Text(duration.title)
    237                 }
    238             }
    239         }
    240     }
    241 
    242     func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View {
    243         return ProfileZapLinkView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in
    244             Image(reactions_enabled ? "zap.fill" : "zap")
    245                 .foregroundColor(reactions_enabled ? .orange : Color.primary)
    246                 .profile_button_style(scheme: colorScheme)
    247                 .cornerRadius(24)
    248         }
    249     }
    250     
    251     var dmButton: some View {
    252         let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
    253         return NavigationLink(value: Route.DMChat(dms: dm_model)) {
    254             Image("messages")
    255                 .profile_button_style(scheme: colorScheme)
    256         }
    257     }
    258     
    259     private var followsYouBadge: some View {
    260         Text("Follows you", comment: "Text to indicate that a user is following your profile.")
    261             .padding([.leading, .trailing], 6.0)
    262             .padding([.top, .bottom], 2.0)
    263             .foregroundColor(.gray)
    264             .background {
    265                 RoundedRectangle(cornerRadius: 5.0)
    266                     .foregroundColor(DamusColors.adaptableGrey)
    267             }
    268             .font(.footnote)
    269     }
    270 
    271     func actionSection(record: ProfileRecord?, pubkey: Pubkey) -> some View {
    272         return Group {
    273             if let record,
    274                let profile = record.profile,
    275                let lnurl = record.lnurl,
    276                lnurl != ""
    277             {
    278                 lnButton(unownedProfile: profile, record: record)
    279             }
    280 
    281             dmButton
    282 
    283             if profile.pubkey != damus_state.pubkey {
    284                 FollowButtonView(
    285                     target: profile.get_follow_target(),
    286                     follows_you: profile.follows(pubkey: damus_state.pubkey),
    287                     follow_state: damus_state.contacts.follow_state(profile.pubkey)
    288                 )
    289             } else if damus_state.keypair.privkey != nil {
    290                 NavigationLink(value: Route.EditMetadata) {
    291                     ProfileEditButton(damus_state: damus_state)
    292                 }
    293             }
    294 
    295         }
    296     }
    297 
    298     func pfpOffset() -> CGFloat {
    299         let progress = -yOffset / navbarHeight
    300         let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1)
    301         return offset > 0 ? offset : 0
    302     }
    303 
    304     func pfpScale() -> CGFloat {
    305         let progress = -yOffset / navbarHeight
    306         let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1))
    307         return scale < 1 ? scale : 1
    308     }
    309 
    310     func nameSection(profile_data: ProfileRecord?) -> some View {
    311         return Group {
    312             let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey)
    313 
    314             HStack(alignment: .center) {
    315                 ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
    316                     .padding(.top, -(pfp_size / 2.0))
    317                     .offset(y: pfpOffset())
    318                     .scaleEffect(pfpScale())
    319                     .onTapGesture {
    320                         is_zoomed.toggle()
    321                     }
    322                     .damus_full_screen_cover($is_zoomed, damus_state: damus_state) {
    323                         ProfilePicImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, settings: damus_state.settings, nav: damus_state.nav, shouldShowEditButton: damus_state.pubkey == profile.pubkey)
    324                     }
    325 
    326                 Spacer()
    327 
    328                 if follows_you {
    329                     followsYouBadge
    330                 }
    331 
    332                 actionSection(record: profile_data, pubkey: profile.pubkey)
    333             }
    334 
    335             ProfileNameView(pubkey: profile.pubkey, damus: damus_state)
    336         }
    337     }
    338 
    339     var followersCount: some View {
    340         HStack {
    341             if let followerCount = followers.count {
    342                 let nounString = pluralizedString(key: "followers_count", count: followerCount)
    343                 let nounText = Text(verbatim: nounString).font(.subheadline).foregroundColor(.gray)
    344                 Text("\(Text(verbatim: followerCount.formatted()).font(.subheadline.weight(.medium))) \(nounText)", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
    345             } else {
    346                 Image("download")
    347                     .resizable()
    348                     .frame(width: 20, height: 20)
    349                 Text("Followers", comment: "Label describing followers of a user.")
    350                     .font(.subheadline)
    351                     .foregroundColor(.gray)
    352             }
    353         }
    354     }
    355 
    356     var aboutSection: some View {
    357         VStack(alignment: .leading, spacing: 8.0) {
    358             let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey)
    359             let profile_data = profile_txn?.unsafeUnownedValue
    360 
    361             nameSection(profile_data: profile_data)
    362 
    363             if let about = profile_data?.profile?.about {
    364                 AboutView(state: damus_state, about: about)
    365             }
    366 
    367             if let url = profile_data?.profile?.website_url {
    368                 WebsiteLink(url: url)
    369             }
    370 
    371             HStack {
    372                 if let contact = profile.contacts {
    373                     let contacts = Array(contact.referenced_pubkeys)
    374                     let hashtags = Array(contact.referenced_hashtags)
    375                     let following_model = FollowingModel(damus_state: damus_state, contacts: contacts, hashtags: hashtags)
    376                     NavigationLink(value: Route.Following(following: following_model)) {
    377                         HStack {
    378                             let noun_text = Text(verbatim: "\(pluralizedString(key: "following_count", count: profile.following))").font(.subheadline).foregroundColor(.gray)
    379                             Text("\(Text(verbatim: profile.following.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
    380                         }
    381                     }
    382                     .buttonStyle(PlainButtonStyle())
    383                 }
    384 
    385                 if followers.contacts != nil {
    386                     NavigationLink(value: Route.Followers(followers: followers)) {
    387                         followersCount
    388                     }
    389                     .buttonStyle(PlainButtonStyle())
    390                 } else {
    391                     followersCount
    392                         .onTapGesture {
    393                             UIImpactFeedbackGenerator(style: .light).impactOccurred()
    394                             followers.contacts = []
    395                             followers.subscribe()
    396                         }
    397                 }
    398 
    399                 if let relays = profile.relays {
    400                     // Only open relay config view if the user is logged in with private key and they are looking at their own profile.
    401                     let noun_string = pluralizedString(key: "relays_count", count: relays.keys.count)
    402                     let noun_text = Text(noun_string).font(.subheadline).foregroundColor(.gray)
    403                     let relay_text = Text("\(Text(verbatim: relays.keys.count.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
    404                     if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
    405                         NavigationLink(value: Route.RelayConfig) {
    406                             relay_text
    407                         }
    408                         .buttonStyle(PlainButtonStyle())
    409                     } else {
    410                         NavigationLink(value: Route.UserRelays(relays: Array(relays.keys).sorted())) {
    411                             relay_text
    412                         }
    413                         .buttonStyle(PlainButtonStyle())
    414                     }
    415                 }
    416             }
    417 
    418             if profile.pubkey != damus_state.pubkey {
    419                 let friended_followers = damus_state.contacts.get_friended_followers(profile.pubkey)
    420                 if !friended_followers.isEmpty {
    421                     Spacer()
    422 
    423                     NavigationLink(value: Route.FollowersYouKnow(friendedFollowers: friended_followers, followers: followers)) {
    424                         HStack {
    425                             CondensedProfilePicturesView(state: damus_state, pubkeys: friended_followers, maxPictures: 3)
    426                             let followedByString = followedByString(friended_followers, ndb: damus_state.ndb)
    427                             Text(followedByString)
    428                                 .font(.subheadline).foregroundColor(.gray)
    429                                 .multilineTextAlignment(.leading)
    430                         }
    431                     }
    432                 }
    433             }
    434         }
    435         .padding(.horizontal)
    436     }
    437 
    438     var tabs: [(String, FilterState)] {
    439         var tabs = [
    440             (NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
    441             (NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
    442         ]
    443         if profile.pubkey != damus_state.pubkey && !profile.conversation_events.isEmpty {
    444             tabs.append((NSLocalizedString("Conversations", comment: "Label for filter for seeing notes and replies that involve conversations between the signed in user and the current profile."), FilterState.conversations))
    445         }
    446         return tabs
    447     }
    448 
    449     var body: some View {
    450         ZStack {
    451             ScrollView(.vertical) {
    452                 VStack(spacing: 0) {
    453                     bannerSection
    454                         .zIndex(1)
    455                     
    456                     VStack() {
    457                         aboutSection
    458 
    459                         VStack(spacing: 0) {
    460                             CustomPicker(tabs: tabs, selection: $filter_state)
    461                             Divider()
    462                                 .frame(height: 1)
    463                         }
    464                         .background(colorScheme == .dark ? Color.black : Color.white)
    465 
    466                         if filter_state == FilterState.posts {
    467                             InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts))
    468                         }
    469                         if filter_state == FilterState.posts_and_replies {
    470                             InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies))
    471                         }
    472                         if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty {
    473                             InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.conversations))
    474                         }
    475                     }
    476                     .padding(.horizontal, Theme.safeAreaInsets?.left)
    477                     .zIndex(-yOffset > navbarHeight ? 0 : 1)
    478                 }
    479             }
    480             .padding(.bottom, tabHeight + getSafeAreaBottom())
    481             .ignoresSafeArea()
    482             .navigationTitle("")
    483             .navigationBarBackButtonHidden()
    484             .toolbar {
    485                 ToolbarItem(placement: .topBarLeading) {
    486                     HStack(spacing: 8) {
    487                         navBackButton
    488                             .padding(.top, 5)
    489                             .accentColor(DamusColors.white)
    490                         VStack(alignment: .leading, spacing: -4.5) {
    491                             Text(getProfileInfo().0) // Display name
    492                                 .font(.headline)
    493                                 .foregroundColor(.white)
    494                             Text(getProfileInfo().1) // Username
    495                                 .font(.subheadline)
    496                                 .foregroundColor(.white.opacity(0.8))
    497                         }
    498                         .opacity(bannerBlurViewOpacity())
    499                         .frame(maxWidth: .infinity, alignment: .leading)
    500                         .padding(.top, max(5, 15 + (yOffset / 30)))
    501                     }
    502                 }
    503                 if showFollowBtnInBlurrBanner() {
    504                     ToolbarItem(placement: .topBarTrailing) {
    505                         FollowButtonView(
    506                             target: profile.get_follow_target(),
    507                             follows_you: profile.follows(pubkey: damus_state.pubkey),
    508                             follow_state: damus_state.contacts.follow_state(profile.pubkey)
    509                         )
    510                         .padding(.top, 8)
    511                     }
    512                 } else {
    513                     ToolbarItem(placement: .topBarTrailing) {
    514                         navActionSheetButton
    515                             .padding(.top, 5)
    516                             .accentColor(DamusColors.white)
    517                     }
    518                 }
    519             }
    520             .toolbarBackground(.hidden)
    521             .onReceive(handle_notify(.switched_timeline)) { _ in
    522                 dismiss()
    523             }
    524             .onAppear() {
    525                 check_nip05_validity(pubkey: self.profile.pubkey, profiles: self.damus_state.profiles)
    526                 profile.subscribe()
    527                 //followers.subscribe()
    528             }
    529             .onDisappear {
    530                 profile.unsubscribe()
    531                 followers.unsubscribe()
    532                 // our profilemodel needs a bit more help
    533             }
    534             .sheet(isPresented: $show_share_sheet) {
    535                 let url = URL(string: "https://damus.io/" + profile.pubkey.npub)!
    536                 ShareSheet(activityItems: [url])
    537             }
    538             .damus_full_screen_cover($show_qr_code, damus_state: damus_state) {
    539                 QRCodeView(damus_state: damus_state, pubkey: profile.pubkey)
    540             }
    541 
    542             if damus_state.is_privkey_user {
    543                 PostButtonContainer(is_left_handed: damus_state.settings.left_handed) {
    544                     notify(.compose(.posting(.user(profile.pubkey))))
    545                 }
    546                 .padding(.bottom, tabHeight)
    547             }
    548         }
    549     }
    550 }
    551 
    552 struct ProfileView_Previews: PreviewProvider {
    553     static var previews: some View {
    554         let ds = test_damus_state
    555         ProfileView(damus_state: ds, pubkey: ds.pubkey)
    556     }
    557 }
    558 
    559 extension View {
    560     func profile_button_style(scheme: ColorScheme) -> some View {
    561         self.symbolRenderingMode(.palette)
    562             .font(.system(size: 32).weight(.thin))
    563             .foregroundStyle(scheme == .dark ? .white : .black, scheme == .dark ? .white : .black)
    564     }
    565 }
    566 
    567 @MainActor
    568 func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) {
    569     let profile_txn = profiles.lookup(id: pubkey)
    570 
    571     guard let profile = profile_txn?.unsafeUnownedValue,
    572           let nip05 = profile.nip05,
    573           profiles.is_validated(pubkey) == nil
    574     else {
    575         return
    576     }
    577 
    578     Task.detached(priority: .background) {
    579         let validated = await validate_nip05(pubkey: pubkey, nip05_str: nip05)
    580         if validated != nil {
    581             print("validated nip05 for '\(nip05)'")
    582         }
    583 
    584         Task { @MainActor in
    585             profiles.set_validated(pubkey, nip05: validated)
    586             profiles.nip05_pubkey[nip05] = pubkey
    587             notify(.profile_updated(.remote(pubkey: pubkey)))
    588         }
    589     }
    590 }