diff --git a/CHANGELOG.md b/CHANGELOG.md index 64f308f..fa86932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,65 @@ +## [0.1.9] - 2022-12-23 + +### Added + + - Added profile edit view + +### Changed + + - Increase like boop intensity + - Don't auto-load follower count + - Don't fetch followers right away + +### Fixed + + - Fix crash on some bolt11 invoices + - Fixed issues when refreshing global view + + +## [0.1.8] - 2022-12-21 + +### Changed + + - Lots of overall design polish (Sam DuBois) + - Added loading shimmering effect (Sam DuBois) + - Show real name next to username in timelines (Sam DuBois) + +### Added + + - Animated gif are now shown inline and in profile pictures (@futurepaul) + - Added ability to copy and share image (@futurepaul) + - Haptic feedback when liking for that sweet dopamine hit (radixrat) + - Hide private key in config, make it easier to copy keys (Nitesh Balusu) + +### Fixed + + - Disable autocorrection for username when creating account + - Fixed issues with the post placeholder + - Disable autocorrection on search + - Disable autocorrection on add relay field + - Parse lightning: prefixes on lightning invoice + - Resize images to fill the space + + +## [0.1.7] - 2022-12-21 + +### Changed + + - Only show inline images from your friends + - Improved look of profile view + + +### Fixed + + - Added ability to dismiss keyboard during account creation + - Fixed crashed on lightning invoices with empty descriptions + - Make dm chat area visible again + + + +[0.1.7]: https://github.com/damus-io/damus/releases/tag/v0.1.7 + ## [0.1.6] - 2022-10-30 ### Added @@ -77,4 +138,3 @@ [0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2 - diff --git a/damus-c/bolt11.c b/damus-c/bolt11.c index 027b0bd..14e078b 100644 --- a/damus-c/bolt11.c +++ b/damus-c/bolt11.c @@ -553,7 +553,7 @@ struct bolt11 *bolt11_decode_nosig(const tal_t *ctx, const char *str, u5 **sig, case 'n': problem = decode_n(b11, &hu5, &data, &data_len, data_length, - have_n); + &have_n); break; case 'x': diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 883eda4..cd4abe1 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -129,6 +129,7 @@ 4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */; }; 4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */; }; 4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */; }; + E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -306,6 +307,7 @@ 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileName.swift; sourceTree = ""; }; 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowView.swift; sourceTree = ""; }; 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventActionBar.swift; sourceTree = ""; }; + E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -473,6 +475,7 @@ 4C216F31286E388800040376 /* DMChatView.swift */, 4C216F33286F5ACD00040376 /* DMView.swift */, 4C06670028FC7C5900038D2A /* RelayView.swift */, + E990020E2955F837003BBC5A /* EditMetadataView.swift */, ); path = Views; sourceTree = ""; @@ -845,6 +848,7 @@ 4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */, 4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */, 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */, + E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, 4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */, 4C06670B28FDE64700038D2A /* damus.c in Sources */, @@ -1039,7 +1043,7 @@ "$(inherited)", "$(PROJECT_DIR)", ); - MARKETING_VERSION = 0.1.8; + MARKETING_VERSION = 0.1.9; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1078,7 +1082,7 @@ "$(inherited)", "$(PROJECT_DIR)", ); - MARKETING_VERSION = 0.1.8; + MARKETING_VERSION = 0.1.9; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/damus/Components/Shimmer.swift b/damus/Components/Shimmer.swift index 38956fc..1ae1c2e 100644 --- a/damus/Components/Shimmer.swift +++ b/damus/Components/Shimmer.swift @@ -41,6 +41,7 @@ struct ShimmeringView: View { _startPoint = .init(wrappedValue: configuration.initialLocation.start) _endPoint = .init(wrappedValue: configuration.initialLocation.end) } + var body: some View { ZStack { content() @@ -71,7 +72,12 @@ public struct ShimmerModifier: ViewModifier { public extension View { - func shimmer(configuration: ShimmerConfiguration = .default) -> some View { - modifier(ShimmerModifier(configuration: configuration)) + + @ViewBuilder func shimmer(configuration: ShimmerConfiguration = .default, _ loading: Bool) -> some View { + if loading { + modifier(ShimmerModifier(configuration: configuration)) + } else { + self + } } } diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift index a361111..b5b69a4 100644 --- a/damus/Models/FollowersModel.swift +++ b/damus/Models/FollowersModel.swift @@ -12,12 +12,19 @@ class FollowersModel: ObservableObject { let target: String var needs_sub: Bool = true - @Published var contacts: [String] = [] + @Published var contacts: [String]? = nil var has_contact: Set = Set() let sub_id: String = UUID().description let profiles_id: String = UUID().description + var count_display: String { + guard let contacts = self.contacts else { + return "?" + } + return "\(contacts.count)"; + } + init(damus_state: DamusState, target: String) { self.damus_state = damus_state self.target = target @@ -49,13 +56,13 @@ class FollowersModel: ObservableObject { contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev ) - contacts.append(ev.pubkey) + contacts?.append(ev.pubkey) has_contact.insert(ev.pubkey) } func load_profiles(relay_id: String) { var filter = NostrFilter.filter_profiles - let authors = find_profiles_to_fetch_pk(profiles: damus_state.profiles, event_pubkeys: contacts) + let authors = find_profiles_to_fetch_pk(profiles: damus_state.profiles, event_pubkeys: contacts ?? []) if authors.isEmpty { return } diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift index 20b59c5..1f89b0b 100644 --- a/damus/Models/ProfileModel.swift +++ b/damus/Models/ProfileModel.swift @@ -7,7 +7,7 @@ import Foundation -class ProfileModel: ObservableObject { +class ProfileModel: ObservableObject, Equatable { @Published var events: [NostrEvent] = [] @Published var contacts: NostrEvent? = nil @Published var following: Int = 0 @@ -31,6 +31,14 @@ class ProfileModel: ObservableObject { self.damus = damus } + static func == (lhs: ProfileModel, rhs: ProfileModel) -> Bool { + return lhs.pubkey == rhs.pubkey + } + + func hash(into hasher: inout Hasher) { + hasher.combine(pubkey) + } + func unsubscribe() { print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)") damus.pool.unsubscribe(sub_id: sub_id) diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift index cc1289c..6526b88 100644 --- a/damus/Nostr/Nostr.swift +++ b/damus/Nostr/Nostr.swift @@ -7,13 +7,88 @@ 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?) { + self.value = [:] + self.name = name + self.display_name = display_name + self.about = about + self.picture = picture + self.website = website + self.lud06 = lud06 + self.lud16 = lud16 + self.nip05 = nip05 + } + + var display_name: String? { + get { return value["display_name"]; } + set(s) { value["display_name"] = s } + } + + var name: String? { + get { return value["name"]; } + set(s) { value["name"] = s } + } + + var about: String? { + get { return value["about"]; } + set(s) { value["about"] = s } + } + + var picture: String? { + get { return value["picture"]; } + set(s) { value["picture"] = s } + } + + var website: String? { + get { return value["website"]; } + set(s) { value["website"] = s } + } + + var lud06: String? { + get { return value["lud06"]; } + set(s) { value["lud06"] = s } + } + + var lud16: String? { + get { return value["lud16"]; } + set(s) { value["lud16"] = s } + } + + var nip05: String? { + get { return value["nip05"]; } + set(s) { value["nip05"] = s } + } + + var lightning_uri: URL? { + return make_ln_url(self.lud06) ?? make_ln_url(self.lud16) + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.value = try container.decode([String: String].self) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(value) + } + + static func displayName(profile: Profile?, pubkey: String) -> String { + return profile?.name ?? abbrev_pubkey(pubkey) + } +} +/* struct Profile: Decodable { let name: String? let display_name: String? let about: String? let picture: String? let website: String? + let nip05: String? let lud06: String? let lud16: String? @@ -25,6 +100,7 @@ struct Profile: Decodable { return profile?.name ?? abbrev_pubkey(pubkey) } } + */ func make_ln_url(_ str: String?) -> URL? { return str.flatMap { URL(string: "lightning:" + $0) } @@ -34,6 +110,3 @@ struct NostrSubscription { let sub_id: String let filter: NostrFilter } - - - diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift index fe7ce89..2bbf9d7 100644 --- a/damus/Nostr/NostrEvent.swift +++ b/damus/Nostr/NostrEvent.swift @@ -274,6 +274,20 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable { self.tags = tags self.created_at = Int64(Date().timeIntervalSince1970) } + + /// Intiialization statement used to specificy ID + /// + /// This is mainly used for contant and testing data + init(id: String, content: String, pubkey: String, kind: Int = 1, tags: [[String]] = []) { + self.id = id + self.sig = "" + + self.content = content + self.pubkey = pubkey + self.kind = kind + self.tags = tags + self.created_at = Int64(Date().timeIntervalSince1970) + } init(from: NostrEvent, content: String? = nil) { self.id = from.id diff --git a/damus/Nostr/NostrMetadata.swift b/damus/Nostr/NostrMetadata.swift index faf5963..0c69f42 100644 --- a/damus/Nostr/NostrMetadata.swift +++ b/damus/Nostr/NostrMetadata.swift @@ -13,8 +13,12 @@ struct NostrMetadata: Codable { let name: String? let about: String? let website: String? + let nip05: String? + let picture: 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) + return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: nil, lud06: nil, lud16: nil) } diff --git a/damus/Util/Constants.swift b/damus/Util/Constants.swift index 54411f6..9db957a 100644 --- a/damus/Util/Constants.swift +++ b/damus/Util/Constants.swift @@ -14,13 +14,15 @@ public class Constants { static let EXAMPLE_DEMOS = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: PUB_KEY, privkey: "privkey"), likes: EventCounter(our_pubkey: PUB_KEY), boosts: EventCounter(our_pubkey: PUB_KEY), contacts: Contacts(), tips: TipCounter(our_pubkey: PUB_KEY), profiles: Profiles(), dms: DirectMessagesModel()) static let EXAMPLE_EVENTS = [ - NostrEvent(content: "Icecream", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), - NostrEvent(content: "This is a test for a really long note that somebody sent because they thought they were super cool or maybe they were just really excited to share something with the world.", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), - NostrEvent(content: "Bonjour Le Monde", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), - NostrEvent(content: "Why am I helping on this app? Because it's fun!", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), - NostrEvent(content: "PIzza", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), - NostrEvent(content: "Hello World! This is so cool!", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), - NostrEvent(content: "Nostr - Damus... Haha get it?", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), + NostrEvent(id: UUID().description, content: "Nostr - Damus... Haha get it? Bonjour Le Monde mon Ami! C'est la tres importante", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), + NostrEvent(id: UUID().description, content: "This is a test for a really long note that somebody sent because they thought they were super cool or maybe they were just really excited to share something with the world.", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), + NostrEvent(id: UUID().description, content: "Bonjour Le Monde", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), + NostrEvent(id: UUID().description, content: "Why am I helping on this app? Because it's fun!", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), + NostrEvent(id: UUID().description, content: "Pizza and Icecream! Pizza and Icecream! Testing Testing! 1 .. 2.. 3..", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), + NostrEvent(id: UUID().description, content: "Hello World! This is so cool!", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), + NostrEvent(id: UUID().description, content: "Nostr - Damus... Haha get it?", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), + NostrEvent(id: UUID().description, content: "Hello World! This is so cool!", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), + NostrEvent(id: UUID().description, content: "Bonjour Le Monde", pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"), ] static let WALLETS = """ diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift index 2f54ca1..eea1e05 100644 --- a/damus/Views/ConfigView.swift +++ b/damus/Views/ConfigView.swift @@ -73,11 +73,11 @@ struct ConfigView: View { CopyButton(is_pk: false) } - + Toggle("Show", isOn: $show_privkey) } } - + Section("Reset") { Button("Logout") { confirm_logout = true @@ -129,7 +129,7 @@ struct ConfigView: View { guard let privkey = state.keypair.privkey else { return } - + let info = RelayInfo.rw guard (try? state.pool.add_relay(url, info: info)) != nil else { @@ -154,6 +154,8 @@ struct ConfigView: View { struct ConfigView_Previews: PreviewProvider { static var previews: some View { - ConfigView(state: test_damus_state()) + NavigationView { + ConfigView(state: test_damus_state()) + } } } diff --git a/damus/Views/EditMetadataView.swift b/damus/Views/EditMetadataView.swift new file mode 100644 index 0000000..4bbe868 --- /dev/null +++ b/damus/Views/EditMetadataView.swift @@ -0,0 +1,173 @@ +// +// EditMetadataView.swift +// damus +// +// Created by Thomas Tastet on 23/12/2022. +// + +import SwiftUI + +let PPM_SIZE: CGFloat = 80.0 + +func isHttpsUrl(_ string: String) -> Bool { + let urlRegEx = "^https://.*$" + let urlTest = NSPredicate(format:"SELF MATCHES %@", urlRegEx) + return urlTest.evaluate(with: string) +} + +func isImage(_ urlString: String) -> Bool { + let imageTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif", "image/tiff", "image/bmp", "image/webp"] + + guard let url = URL(string: urlString) else { + return false + } + + var result = false + let semaphore = DispatchSemaphore(value: 0) + + let task = URLSession.shared.dataTask(with: url) { data, response, error in + if let error = error { + print(error) + semaphore.signal() + return + } + + guard let httpResponse = response as? HTTPURLResponse, + let contentType = httpResponse.allHeaderFields["Content-Type"] as? String else { + semaphore.signal() + return + } + + if imageTypes.contains(contentType.lowercased()) { + result = true + } + + semaphore.signal() + } + + task.resume() + semaphore.wait() + + return result +} + +struct EditMetadataView: View { + let damus_state: DamusState + @State var display_name: String + @State var about: String + @State var picture: String + @State var nip05: String + @State var name: String + @State var ln: String + @State var website: String + + @Environment(\.dismiss) var dismiss + + init (damus_state: DamusState) { + self.damus_state = damus_state + let data = damus_state.profiles.lookup(id: damus_state.pubkey) + + _name = State(initialValue: data?.name ?? "") + _display_name = State(initialValue: data?.display_name ?? "") + _about = State(initialValue: data?.about ?? "") + _website = State(initialValue: data?.website ?? "") + _picture = State(initialValue: data?.picture ?? "") + _nip05 = State(initialValue: data?.nip05 ?? "") + _ln = State(initialValue: data?.lud16 ?? data?.lud06 ?? "") + } + + func save() { + let metadata = NostrMetadata( + display_name: display_name, + name: name, + about: about, + website: website, + nip05: nip05.isEmpty ? nil : nip05, + picture: picture.isEmpty ? nil : picture, + lud06: ln.contains("@") ? ln : nil, + lud16: ln.contains("@") ? nil : ln + ); + + let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata) + + if let metadata_ev = m_metadata_ev { + damus_state.pool.send(.event(metadata_ev)) + } + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + Spacer() + InnerProfilePicView(url: URL(string: picture), pubkey: damus_state.pubkey, size: PPM_SIZE, highlight: .none) + Spacer() + } + Form { + Section("Your Name") { + TextField("Satoshi Nakamoto", text: $display_name) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Section("Username") { + TextField("satoshi", text: $name) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + + } + + Section ("Profile Picture") { + TextField("https://example.com/pic.jpg", text: $picture) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Section("Website") { + TextField("https://jb55.com", text: $website) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Section("About Me") { + ZStack(alignment: .topLeading) { + TextEditor(text: $about) + .textInputAutocapitalization(.sentences) + if about.isEmpty { + Text("Absolute boss") + .offset(x: 0, y: 7) + .foregroundColor(Color(uiColor: .placeholderText)) + } + } + } + + Section("Bitcoin Lightning Tips") { + TextField("Lightning Address or LNURL", text: $ln) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Section(content: { + TextField("example.com", text: $nip05) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + }, header: { + Text("NIP-05 Verification") + }, footer: { + Text("\(name)@\(nip05) will be used for verification") + }) + + Button("Save") { + save() + dismiss() + } + } + } + .navigationTitle("Edit Profile") + } +} + +struct EditMetadataView_Previews: PreviewProvider { + static var previews: some View { + EditMetadataView(damus_state: test_damus_state()) + } +} diff --git a/damus/Views/EventActionBar.swift b/damus/Views/EventActionBar.swift index 872aadb..4bcfe8e 100644 --- a/damus/Views/EventActionBar.swift +++ b/damus/Views/EventActionBar.swift @@ -21,7 +21,7 @@ enum ActionBarSheet: Identifiable { struct EventActionBar: View { let damus_state: DamusState let event: NostrEvent - let generator = UIImpactFeedbackGenerator(style: .light) + let generator = UIImpactFeedbackGenerator(style: .medium) @State var sheet: ActionBarSheet? = nil @State var confirm_boost: Bool = false @StateObject var bar: ActionBarModel diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift index 79a1c45..94f1919 100644 --- a/damus/Views/EventDetailView.swift +++ b/damus/Views/EventDetailView.swift @@ -308,39 +308,6 @@ func scroll_to_event(scroller: ScrollViewProxy, id: String, delay: Double, anima } } - - /* - func OldEventView(proxy: ScrollViewProxy, ev: NostrEvent, highlight: Highlight, collapsed_events: [CollapsedEvent]) -> some View { - Group { - if ev.id == thread.event.id { - EventView(event: ev, highlight: .main, has_action_bar: true) - .onAppear() { - scroll_to_event(scroller: proxy, id: ev.id, delay: 0.5, animate: true) - } - .onTapGesture { - print_event(ev) - let any = any_collapsed(collapsed_events) - if (collapsed && any) || (!collapsed && !any) { - toggle_collapse_thread(scroller: proxy, id: ev.id) - } - } - } else { - if !(self.collapsed && highlight.is_none) { - EventView(event: ev, highlight: collapsed ? .none : highlight, has_action_bar: true) - .onTapGesture { - print_event(ev) - if !collapsed { - toggle_collapse_thread(scroller: proxy, id: ev.id) - } - thread.event = ev - } - } - } - } - } - */ - - extension Collection { /// Returns the element at the specified index if it is within bounds, otherwise nil. diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift index 243eeb1..3caf22a 100644 --- a/damus/Views/FollowingView.swift +++ b/damus/Views/FollowingView.swift @@ -47,12 +47,18 @@ struct FollowersView: View { let profile = damus_state.profiles.lookup(id: whos) ScrollView { LazyVStack(alignment: .leading) { - ForEach(followers.contacts, id: \.self) { pk in + ForEach(followers.contacts ?? [], id: \.self) { pk in FollowUserView(target: .pubkey(pk), damus_state: damus_state) } } } .navigationBarTitle("\(Profile.displayName(profile: profile, pubkey: whos))'s Followers") + .onAppear { + followers.subscribe() + } + .onDisappear { + followers.unsubscribe() + } } } diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index bc14d92..df75aeb 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -77,8 +77,9 @@ struct PostView: View { if post.isEmpty { Text(POST_PLACEHOLDER) .padding(.top, 8) - .padding(.leading, 10) + .padding(.leading, 4) .foregroundColor(Color(uiColor: .placeholderText)) + .allowsHitTesting(false) } } } diff --git a/damus/Views/ProfilePicView.swift b/damus/Views/ProfilePicView.swift index d7c0f55..d6bc33b 100644 --- a/damus/Views/ProfilePicView.swift +++ b/damus/Views/ProfilePicView.swift @@ -32,13 +32,13 @@ func pfp_line_width(_ h: Highlight) -> CGFloat { } } -struct ProfilePicView: View { +struct InnerProfilePicView: View { + @Environment(\.redactionReasons) private var reasons + + let url: URL? let pubkey: String let size: CGFloat let highlight: Highlight - let profiles: Profiles - - @State var picture: String? = nil var PlaceholderColor: Color { return id_to_color(pubkey) @@ -52,30 +52,50 @@ struct ProfilePicView: View { .padding(2) } - var MainContent: some View { + var body: some View { Group { - let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey) - let url = URL(string: pic) - - KFAnimatedImage(url) - .configure { view in - view.framePreloadCount = 1 - } - .placeholder { _ in - Placeholder - } - .cacheOriginalImage() - .scaleFactor(UIScreen.main.scale) - .loadDiskFileSynchronously() - .fade(duration: 0.1) - .frame(width: size, height: size) - .clipShape(Circle()) - .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) + if reasons.isEmpty { + KFAnimatedImage(url) + .configure { view in + view.framePreloadCount = 1 + } + .placeholder { _ in + Placeholder + } + .cacheOriginalImage() + .scaleFactor(UIScreen.main.scale) + .loadDiskFileSynchronously() + .fade(duration: 0.1) + } else { + KFImage(url) + } } + .frame(width: size, height: size) + .clipShape(Circle()) + .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) + } + +} + +struct ProfilePicView: View { + + let pubkey: String + let size: CGFloat + let highlight: Highlight + let profiles: Profiles + + @State var picture: String? + + init (pubkey: String, size: CGFloat, highlight: Highlight, profiles: Profiles, picture: String? = nil) { + self.pubkey = pubkey + self.profiles = profiles + self.size = size + self.highlight = highlight + self._picture = State(initialValue: picture) } var body: some View { - MainContent + InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), pubkey: pubkey, size: size, highlight: highlight) .onReceive(handle_notify(.profile_updated)) { notif in let updated = notif.object as! ProfileUpdate @@ -90,10 +110,18 @@ struct ProfilePicView: View { } } +func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> URL { + let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey) + if let url = URL(string: pic) { + return url + } + return URL(string: robohash(pubkey))! +} + 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) + 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 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 index b924f2e..4b0ccf8 100644 --- a/damus/Views/ProfileView.swift +++ b/damus/Views/ProfileView.swift @@ -77,12 +77,47 @@ struct ProfileNameView: View { } } +struct EditButton: View { + let damus_state: DamusState + + @Environment(\.colorScheme) var colorScheme + + var body: some View { + NavigationLink(destination: EditMetadataView(damus_state: damus_state)) { + Text("Edit") + .padding(.horizontal, 25) + .padding(.vertical, 10) + .font(.caption.weight(.bold)) + .foregroundColor(fillColor()) + .background(emptyColor()) + .cornerRadius(20) + .overlay { + RoundedRectangle(cornerRadius: 16) + .stroke(borderColor(), lineWidth: 1) + } + } + } + + func fillColor() -> Color { + colorScheme == .light ? .black : .white + } + + func emptyColor() -> Color { + colorScheme == .light ? .white : .black + } + + func borderColor() -> Color { + colorScheme == .light ? .black.opacity(0.1) : .white.opacity(0.2) + } +} + struct ProfileView: View { let damus_state: DamusState @State private var selected_tab: ProfileTab = .posts @StateObject var profile: ProfileModel @StateObject var followers: FollowersModel + @State private var showingEditProfile = false @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme @@ -127,7 +162,18 @@ struct ProfileView: View { DMButton - FollowButtonView(target: profile.get_follow_target(), follow_state: damus_state.contacts.follow_state(profile.pubkey)) + + if profile.pubkey != damus_state.pubkey { + FollowButtonView( + target: profile.get_follow_target(), + follow_state: damus_state.contacts.follow_state(profile.pubkey) + ) + } else { + NavigationLink(destination: EditMetadataView(damus_state: damus_state)) { + EditButton(damus_state: damus_state) + } + } + } ProfileNameView(pubkey: profile.pubkey, profile: data, contacts: damus_state.contacts) @@ -155,20 +201,33 @@ struct ProfileView: View { } let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey) .environmentObject(followers) - NavigationLink(destination: fview) { - HStack { - Text("\(followers.contacts.count)") - .font(.subheadline.weight(.medium)) - Text("Followers") - .font(.subheadline) - .foregroundColor(.gray) + if followers.contacts != nil { + NavigationLink(destination: fview) { + FollowersCount } + .buttonStyle(PlainButtonStyle()) + } else { + FollowersCount + .onTapGesture { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + followers.contacts = [] + followers.subscribe() + } } - .buttonStyle(PlainButtonStyle()) } } } + var FollowersCount: some View { + HStack { + Text("\(followers.count_display)") + .font(.subheadline.weight(.medium)) + Text("Followers") + .font(.subheadline) + .foregroundColor(.gray) + } + } + var body: some View { VStack(alignment: .leading) { ScrollView { @@ -187,7 +246,7 @@ struct ProfileView: View { } .onAppear() { profile.subscribe() - followers.subscribe() + //followers.subscribe() } .onDisappear { profile.unsubscribe() @@ -211,7 +270,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(), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel()) - 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") + 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 tsprof = TimestampedProfile(profile: prof, timestamp: 0) damus.profiles.add(id: pubkey, profile: tsprof) return damus diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift index f68bec0..27fae88 100644 --- a/damus/Views/SearchHomeView.swift +++ b/damus/Views/SearchHomeView.swift @@ -42,7 +42,8 @@ struct SearchHomeView: View { var GlobalContent: some View { return TimelineView(events: $model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: { _ in true }) .refreshable { - // Fetch new information by resubscribing to the relay + // Fetch new information by unsubscribing and resubscribing to the relay + model.unsubscribe() model.subscribe() } } @@ -50,7 +51,8 @@ struct SearchHomeView: View { var SearchContent: some View { SearchResultsView(damus_state: damus_state, search: $search) .refreshable { - // Fetch new information by resubscribing to the relay + // Fetch new information by unsubscribing and resubscribing to the relay + model.unsubscribe() model.subscribe() } } @@ -68,9 +70,7 @@ struct SearchHomeView: View { @Environment(\.colorScheme) var colorScheme var body: some View { - VStack { - MainContent - } + MainContent .safeAreaInset(edge: .top) { VStack(spacing: 0) { SearchInput diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift index 83fa3b7..61ecf4b 100644 --- a/damus/Views/TimelineView.swift +++ b/damus/Views/TimelineView.swift @@ -40,25 +40,6 @@ struct InnerTimelineView: View { } } -struct InnerTimelineRedactedView: View { - let events: [NostrEvent] - let damus: DamusState - let show_friend_icon: Bool - - var body: some View { - VStack { - ForEach(events, id: \.id) { event in - EventView(event: event, highlight: .none, has_action_bar: true, damus: damus, show_friend_icon: show_friend_icon) - .buttonStyle(PlainButtonStyle()) - } - } - .shimmer() - .redacted(reason: .placeholder) - .padding(.horizontal) - .disabled(true) - } -} - struct TimelineView: View { @Binding var events: [NostrEvent] @@ -75,13 +56,10 @@ struct TimelineView: View { var MainContent: some View { ScrollViewReader { scroller in ScrollView { - if loading { - InnerTimelineRedactedView(events: Constants.EXAMPLE_EVENTS, damus: damus, show_friend_icon: true) - ProgressView() - .progressViewStyle(.circular) - } else { - InnerTimelineView(events: $events, damus: damus, show_friend_icon: show_friend_icon, filter: filter) - } + InnerTimelineView(events: loading ? .constant(Constants.EXAMPLE_EVENTS) : $events, damus: damus, show_friend_icon: show_friend_icon, filter: loading ? { _ in true } : filter) + .redacted(reason: loading ? .placeholder : []) + .shimmer(loading) + .disabled(loading) } .onReceive(NotificationCenter.default.publisher(for: .scroll_to_top)) { _ in guard let event = events.filter(self.filter).first else {