Merge branch 'master' into add-wallet-modal
This commit is contained in:
@@ -41,6 +41,7 @@ struct ShimmeringView<Content: View>: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> = 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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
173
damus/Views/EditMetadataView.swift
Normal file
173
damus/Views/EditMetadataView.swift
Normal file
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user