damus

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

commit 9d44ed0bfee34849b6545538224f9eb68609ebca
parent 33383265c813a226d2b2a4049044793358477f31
Author: Jason Jōb <jason.allan.job@gmail.com>
Date:   Tue, 10 Jan 2023 15:12:34 -0800

Profile Banner Images

Changelog-Added: Profile banner images
Closes: #302

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/Models/HomeModel.swift | 7+++++++
Mdamus/Nostr/Nostr.swift | 10++++++++--
Mdamus/Nostr/NostrMetadata.swift | 3++-
Adamus/Views/BannerImageView.swift | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/EditMetadataView.swift | 46++++++++++++++++++++++++++++++++++++++++------
Mdamus/Views/ProfilePicView.swift | 2+-
Mdamus/Views/ProfileView.swift | 9++++-----
8 files changed, 151 insertions(+), 15 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -147,6 +147,7 @@ 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; }; 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; }; 6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; }; + 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; }; @@ -347,6 +348,7 @@ 4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; }; 647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; }; 64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; }; + 9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; }; DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; }; @@ -530,6 +532,7 @@ 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */, 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */, 647D9A8C2968520300A295DE /* SideMenuView.swift */, + 9609F057296E220800069BF3 /* BannerImageView.swift */, 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */, ); path = Views; @@ -904,6 +907,7 @@ 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */, 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, 4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */, + 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */, 4C363A94282704FA006E126D /* Post.swift in Sources */, 4C216F32286E388800040376 /* DMChatView.swift in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -578,6 +578,13 @@ func process_metadata_event(profiles: Profiles, ev: NostrEvent) { } } + let banner = tprof.profile.banner ?? "" + if let _ = URL(string: banner) { + DispatchQueue.main.async { + notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) + } + } + notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) } diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift @@ -10,12 +10,13 @@ import Foundation struct Profile: Codable { var value: [String: String] - init (name: String?, display_name: String?, about: String?, picture: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) { + init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) { self.value = [:] self.name = name self.display_name = display_name self.about = about self.picture = picture + self.banner = banner self.website = website self.lud06 = lud06 self.lud16 = lud16 @@ -42,6 +43,11 @@ struct Profile: Codable { set(s) { value["picture"] = s } } + var banner: String? { + get { return value["banner"]; } + set(s) { value["banner"] = s } + } + var website: String? { get { return value["website"]; } set(s) { value["website"] = s } @@ -95,7 +101,7 @@ struct Profile: Codable { } func make_test_profile() -> Profile { - return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com") + return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com") } func make_ln_url(_ str: String?) -> URL? { diff --git a/damus/Nostr/NostrMetadata.swift b/damus/Nostr/NostrMetadata.swift @@ -15,10 +15,11 @@ struct NostrMetadata: Codable { let website: String? let nip05: String? let picture: String? + let banner: String? let lud06: String? let lud16: String? } func create_account_to_metadata(_ model: CreateAccountModel) -> NostrMetadata { - return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: nil, lud06: nil, lud16: nil) + return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: nil, banner: nil, lud06: nil, lud16: nil) } diff --git a/damus/Views/BannerImageView.swift b/damus/Views/BannerImageView.swift @@ -0,0 +1,85 @@ +// +// BannerImageView.swift +// damus +// +// Created by Jason Jōb on 2023-01-10. +// + +import SwiftUI +import Kingfisher + +struct InnerBannerImageView: View { + let url: URL? + let pubkey: String + + var body: some View { + ZStack { + Color(uiColor: .systemBackground) + + if (url != nil) { + KFAnimatedImage(url) + .callbackQueue(.dispatch(.global(qos: .background))) + .processingQueue(.dispatch(.global(qos: .background))) + .appendProcessor(LargeImageProcessor.shared) + .configure { view in + view.framePreloadCount = 1 + } + .placeholder { _ in + Image("profile-banner").resizable() + } + .scaleFactor(UIScreen.main.scale) + .loadDiskFileSynchronously() + .fade(duration: 0.1) + } else { + Image("profile-banner").resizable() + } + } + } +} + +struct BannerImageView: View { + let pubkey: String + let profiles: Profiles + + @State var banner: String? + + init (pubkey: String, profiles: Profiles, banner: String? = nil) { + self.pubkey = pubkey + self.profiles = profiles + self._banner = State(initialValue: banner) + } + + var body: some View { + InnerBannerImageView(url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles), pubkey: pubkey) + .onReceive(handle_notify(.profile_updated)) { notif in + let updated = notif.object as! ProfileUpdate + + guard updated.pubkey == self.pubkey else { + return + } + + if let bannerImage = updated.profile.banner { + self.banner = bannerImage + } + } + } +} + +func get_banner_url(banner: String?, pubkey: String, profiles: Profiles) -> URL? { + let bannerUrlString = banner ?? profiles.lookup(id: pubkey)?.banner ?? "" + if let url = URL(string: bannerUrlString) { + return url + } + return nil +} + +struct BannerImageView_Previews: PreviewProvider { + static let pubkey = "ca48854ac6555fed8e439ebb4fa2d928410e0eef13fa41164ec45aaaa132d846" + + static var previews: some View { + BannerImageView( + pubkey: pubkey, + profiles: make_preview_profiles(pubkey)) + } +} + diff --git a/damus/Views/EditMetadataView.swift b/damus/Views/EditMetadataView.swift @@ -8,6 +8,7 @@ import SwiftUI let PPM_SIZE: CGFloat = 80.0 +let BANNER_HEIGHT: CGFloat = 150.0; func isHttpsUrl(_ string: String) -> Bool { let urlRegEx = "^https://.*$" @@ -56,12 +57,14 @@ struct EditMetadataView: View { @State var display_name: String @State var about: String @State var picture: String + @State var banner: String @State var nip05: String @State var name: String @State var ln: String @State var website: String @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) var colorScheme init (damus_state: DamusState) { self.damus_state = damus_state @@ -72,10 +75,15 @@ struct EditMetadataView: View { _about = State(initialValue: data?.about ?? "") _website = State(initialValue: data?.website ?? "") _picture = State(initialValue: data?.picture ?? "") + _banner = State(initialValue: data?.banner ?? "") _nip05 = State(initialValue: data?.nip05 ?? "") _ln = State(initialValue: data?.lud16 ?? data?.lud06 ?? "") } + func imageBorderColor() -> Color { + colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack") + } + func save() { let metadata = NostrMetadata( display_name: display_name, @@ -84,6 +92,7 @@ struct EditMetadataView: View { website: website, nip05: nip05.isEmpty ? nil : nip05, picture: picture.isEmpty ? nil : picture, + banner: banner.isEmpty ? nil : banner, lud06: ln.contains("@") ? nil : ln, lud16: ln.contains("@") ? ln : nil ); @@ -99,13 +108,32 @@ struct EditMetadataView: View { return NIP05.parse(nip05) } + var TopSection: some View { + ZStack(alignment: .top) { + GeometryReader { geo in + BannerImageView(pubkey: damus_state.pubkey, profiles: damus_state.profiles) + .aspectRatio(contentMode: .fill) + .frame(width: geo.size.width, height: BANNER_HEIGHT) + .clipped() + }.frame(height: BANNER_HEIGHT) + VStack(alignment: .leading) { + let pfp_size: CGFloat = 90.0 + + HStack(alignment: .center) { + ProfilePicView(pubkey: damus_state.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles) + .offset(y: -(pfp_size/2.0)) // Increase if set a frame + + Spacer() + }.padding(.bottom,-(pfp_size/2.0)) + } + .padding(.horizontal,18) + .padding(.top,BANNER_HEIGHT) + } + } + var body: some View { VStack(alignment: .leading) { - HStack { - Spacer() - InnerProfilePicView(url: URL(string: picture), fallbackUrl: nil, pubkey: damus_state.pubkey, size: PPM_SIZE, highlight: .none) - Spacer() - } + TopSection Form { Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) { TextField("Satoshi Nakamoto", text: $display_name) @@ -126,6 +154,12 @@ struct EditMetadataView: View { .textInputAutocapitalization(.never) } + Section (NSLocalizedString("Banner Image", comment: "Label for Banner Image section of user profile form.")) { + TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $banner) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + Section(NSLocalizedString("Website", comment: "Label for Website section of user profile form.")) { TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website) .autocorrectionDisabled(true) @@ -172,7 +206,7 @@ struct EditMetadataView: View { } } } - .navigationTitle(NSLocalizedString("Edit Profile", comment: "Title of navigation view for Edit Profile.")) + .ignoresSafeArea() } } diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift @@ -182,7 +182,7 @@ func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> UR func make_preview_profiles(_ pubkey: String) -> Profiles { let profiles = Profiles() let picture = "http://cdn.jb55.com/img/red-me.jpg" - let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com") + let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com") let ts_profile = TimestampedProfile(profile: profile, timestamp: 0) profiles.add(id: pubkey, profile: ts_profile) return profiles diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift @@ -194,15 +194,14 @@ struct ProfileView: View { var TopSection: some View { ZStack(alignment: .top) { GeometryReader { geo in - Image("profile-banner") - .resizable() + BannerImageView(pubkey: damus_state.pubkey, profiles: damus_state.profiles) .aspectRatio(contentMode: .fill) - .frame(width: geo.size.width, height: 150) + .frame(width: geo.size.width, height: BANNER_HEIGHT) .clipped() ShareButton .offset(x: geo.size.width - 80.0, y: 50.0 ) - } + }.frame(height: BANNER_HEIGHT) VStack(alignment: .leading) { let data = damus_state.profiles.lookup(id: profile.pubkey) let pfp_size: CGFloat = 90.0 @@ -358,7 +357,7 @@ func test_damus_state() -> DamusState { let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel(), previews: PreviewCache()) - let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io") + let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io") let tsprof = TimestampedProfile(profile: prof, timestamp: 0) damus.profiles.add(id: pubkey, profile: tsprof) return damus