Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ee970ea9e | |||
| e7fe4ab9b4 | |||
| c146bab08a | |||
| d1cced8d54 | |||
| 8849b6105c | |||
| 3a0acfaba1 | |||
| 0ec2b05070 | |||
| 130bbfafb4 | |||
| ffc75772f9 | |||
| 5b3fac70ed | |||
| 53e3f6d86b | |||
| c28ab7a57c | |||
| 09ce3af11e | |||
| e42c09883a | |||
| 77e3924809 | |||
| 3511b1ee91 | |||
| 78a62c8ef0 | |||
| 8b96b9f4e6 | |||
| 649a857c3a | |||
| cdae2c7558 | |||
| 3639110c51 | |||
|
186668512e
|
|||
|
f63666fae2
|
|||
|
68d25059b1
|
|||
|
9aef6b7f5b
|
|||
|
d2e712575f
|
|||
|
bf9674e6e4
|
|||
|
4815390cbe
|
|||
|
6ce903f1f6
|
|||
|
b2c91ffce4
|
|||
|
ae335b18bf
|
|||
|
6391819fb2
|
|||
|
5d0e56b7c7
|
|||
|
50ccc7bd7f
|
|||
|
b3a6bcf3b2
|
|||
|
38b2988bbe
|
|||
|
446c541dcb
|
|||
|
31fd48ee52
|
|||
| b35cc33c32 | |||
| 9510290c29 | |||
| 3b1238b9c7 | |||
| 3bec23ecac | |||
| 7b678228b6 | |||
| b1292d4562 | |||
| a62d782fe5 | |||
| 81b07eb339 | |||
| 02f88398b9 | |||
| e80961cc09 | |||
| bd7721dc26 | |||
| 6d974bf71c | |||
| aeeb817735 | |||
| b5e7033958 | |||
| bdc843f30f | |||
| a823fa8e14 | |||
| 9232386c15 | |||
| 3c1547718c | |||
| b67a7f3e9e | |||
| 92850d4f64 | |||
| 6323eafd7e | |||
| b8fe826b58 | |||
| 841c49238f | |||
| 7ab612e3d9 | |||
| 6d8a27688f | |||
| 765385319a | |||
| 342c49a3e5 | |||
| 25860e7bb2 | |||
| 6962f2b462 | |||
|
e48ce4c6c5
|
|||
|
1cb311cc2c
|
|||
|
401846abe4
|
|||
|
16ef393350
|
|||
|
d5742f8e4c
|
|||
|
319063f823
|
|||
|
5b13cf5634
|
|||
|
da10b908b3
|
|||
|
4568935bc5
|
|||
|
467404a55e
|
|||
|
fcfe1e4558
|
|||
|
c9d87a1b9a
|
|||
|
35ebf4dfc2
|
|||
|
bc3c256d22
|
|||
|
0e10e74496
|
|||
|
ebe9097f73
|
|||
|
d61a11b647
|
|||
|
d980cc1f8e
|
|||
|
bd6056ce2e
|
|||
|
cf48fda8d0
|
|||
|
dc344cd28c
|
|||
|
358610575f
|
|||
|
a7869fccbb
|
|||
|
a50903f90a
|
|||
|
9243705995
|
|||
|
db4dd9eee9
|
|||
| 22f2aba969 | |||
| 98f2777fda | |||
| 102ce43216 | |||
| 0c148c8a1f | |||
| 3cccb2eb6b | |||
| af4949e26a | |||
| 5bb7e95624 | |||
| 814bcf694f | |||
| b0382c61b1 | |||
| e2650a8bfc | |||
| ac39a53b33 | |||
| fb356cdf0b | |||
| 238e89ce16 | |||
| 6e041c79f7 | |||
| 6ef4b60d14 | |||
|
054bec2d9a
|
|||
|
943a46a343
|
|||
|
17381f6b94
|
|||
|
18c88de407
|
|||
|
99d21fc89b
|
|||
|
db5c86a0d1
|
|||
|
736ec6fb9e
|
|||
|
fa2327325a
|
|||
|
4fdf048040
|
|||
|
273538bd36
|
|||
|
0980c8c040
|
|||
|
f0bfdeaa5a
|
|||
|
ab7c5c18e3
|
|||
|
6ae95ab5ec
|
|||
|
eec630b2b0
|
|||
| 2b3d86968d | |||
| 935a6cae7a | |||
| d4940d8386 | |||
|
caa4bfe864
|
|||
|
a87ba73160
|
@@ -6,6 +6,7 @@ _[Please provide a summary of the changes in this PR.]_
|
|||||||
|
|
||||||
- [ ] I have read (or I am familiar with) the [Contribution Guidelines](../docs/CONTRIBUTING.md)
|
- [ ] I have read (or I am familiar with) the [Contribution Guidelines](../docs/CONTRIBUTING.md)
|
||||||
- [ ] I have tested the changes in this PR
|
- [ ] I have tested the changes in this PR
|
||||||
|
- [ ] I have opened or referred to an existing github issue related to this change.
|
||||||
- [ ] My PR is either small, or I have split it into smaller logical commits that are easier to review
|
- [ ] My PR is either small, or I have split it into smaller logical commits that are easier to review
|
||||||
- [ ] I have added the signoff line to all my commits. See [Signing off your work](../docs/CONTRIBUTING.md#sign-your-work---the-developers-certificate-of-origin)
|
- [ ] I have added the signoff line to all my commits. See [Signing off your work](../docs/CONTRIBUTING.md#sign-your-work---the-developers-certificate-of-origin)
|
||||||
- [ ] I have added appropriate changelog entries for the changes in this PR. See [Adding changelog entries](../docs/CONTRIBUTING.md#add-changelog-changed-changelog-fixed-etc)
|
- [ ] I have added appropriate changelog entries for the changes in this PR. See [Adding changelog entries](../docs/CONTRIBUTING.md#add-changelog-changed-changelog-fixed-etc)
|
||||||
|
|||||||
@@ -1,3 +1,62 @@
|
|||||||
|
## [1.13.1] - 2025-03-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue where threads would not load properly (Daniel D’Aquino)
|
||||||
|
|
||||||
|
|
||||||
|
[1.13.1]: https://github.com/damus-io/damus/releases/tag/v1.13.1
|
||||||
|
|
||||||
|
|
||||||
|
## [1.13] - 2025-03-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added local persistence of note drafts (Daniel D’Aquino)
|
||||||
|
- Added user-friendly error view for errors around the app that would not fit in other places (Daniel D’Aquino)
|
||||||
|
- Coinos connection button in Wallet view (ericholguin)
|
||||||
|
- Added Alby Go to mobile wallets selection menu (Tomek ⚡ K)
|
||||||
|
- Minor accessibility improvements around picture editing and onboarding (Daniel D’Aquino)
|
||||||
|
- Profile image cropping tools (Daniel D’Aquino)
|
||||||
|
- Added Conversations tab to profiles (Terry Yiu)
|
||||||
|
- Added profile pictures to push notifications (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Don't show reposts for the same note more than once in your home feed (William Casarin)
|
||||||
|
- Improved profile image bandwidth optimization (Daniel D’Aquino)
|
||||||
|
- Improved reliability of picture selector (Daniel D’Aquino)
|
||||||
|
- Changed spaces to newlines in new posts to provide cleaner separation between text, uploaded media, and quoted notes (Terry Yiu)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed issue where some push notifications would not open in the app and leave users confused (Daniel D’Aquino)
|
||||||
|
- Fixed issue where app would need a restart for new NWC wallets to work (Daniel D’Aquino)
|
||||||
|
- Fixed overly sensitive horizontal swipe on thread chat view (Daniel D’Aquino)
|
||||||
|
- Trim whitespaces from Lightning addresses (Terry Yiu)
|
||||||
|
- Fixed translation export script by upgrading nostr-sdk-swift dependency to support Mac Catalyst (Terry Yiu)
|
||||||
|
- Fixed issue where users continue to receive push notifications after logout (Daniel D’Aquino)
|
||||||
|
- Fixed an issue where events on a thread view would occasionally disappear (Daniel D’Aquino)
|
||||||
|
- Improved robustness of the URL handler (Daniel D’Aquino)
|
||||||
|
- Translate notes even if they are in a preferred language but not the current language as that is what users expect (Terry Yiu)
|
||||||
|
- Cancel ongoing uploading operations after the user cancels the post (Swift Coder)
|
||||||
|
- Fixed link and photo sharing support on macOS (Swift Coder)
|
||||||
|
- Fix bug where profile view was showing more than just the notes and replies on the notes / notes & replies tabs (Terry Yiu)
|
||||||
|
- Fixed reposts banner to be localizable (Terry Yiu)
|
||||||
|
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed language filtering from Universe feed because language detection can be inaccurate (Terry Yiu)
|
||||||
|
- Removed mystery tabs meant to fix tab switching bug that no longer exists (Terry Yiu)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.13](https://github.com/damus-io/damus/releases/tag/v1.13): https://github.com/damus-io/damus/releases/tag/v1.13
|
||||||
|
|
||||||
|
|
||||||
## [1.12.3] - 2025-02-06
|
## [1.12.3] - 2025-02-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.developer.usernotifications.communication</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ struct NotificationExtensionState: HeadlessDamusState {
|
|||||||
let lnurls: LNUrls
|
let lnurls: LNUrls
|
||||||
|
|
||||||
init?() {
|
init?() {
|
||||||
guard let ndb = try? Ndb(owns_db_file: false) else { return nil }
|
guard let ndb = Ndb(owns_db_file: false) else { return nil }
|
||||||
self.ndb = ndb
|
self.ndb = ndb
|
||||||
|
|
||||||
guard let keypair = get_saved_keypair() else { return nil }
|
guard let keypair = get_saved_keypair() else { return nil }
|
||||||
|
|||||||
@@ -5,15 +5,32 @@
|
|||||||
// Created by Daniel D’Aquino on 2023-11-10.
|
// Created by Daniel D’Aquino on 2023-11-10.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
|
import ImageIO
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
import Intents
|
||||||
|
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
|
|
||||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||||
var bestAttemptContent: UNMutableNotificationContent?
|
var bestAttemptContent: UNMutableNotificationContent?
|
||||||
|
|
||||||
|
private func configureKingfisherCache() {
|
||||||
|
guard let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachePath = groupURL.appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME)
|
||||||
|
if let cache = try? ImageCache(name: "sharedCache", cacheDirectoryURL: cachePath) {
|
||||||
|
KingfisherManager.shared.cache = cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||||
|
configureKingfisherCache()
|
||||||
|
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
|
|
||||||
guard let nostr_event_json = request.content.userInfo["nostr_event"] as? String,
|
guard let nostr_event_json = request.content.userInfo["nostr_event"] as? String,
|
||||||
@@ -40,9 +57,16 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let txn = state.ndb.lookup_profile(nostr_event.pubkey)
|
let sender_profile = {
|
||||||
let profile = txn?.unsafeUnownedValue?.profile
|
let txn = state.ndb.lookup_profile(nostr_event.pubkey)
|
||||||
let name = Profile.displayName(profile: profile, pubkey: nostr_event.pubkey).displayName
|
let profile = txn?.unsafeUnownedValue?.profile
|
||||||
|
let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))!
|
||||||
|
return ProfileBuf(picture: picture,
|
||||||
|
name: profile?.name,
|
||||||
|
display_name: profile?.display_name,
|
||||||
|
nip05: profile?.nip05)
|
||||||
|
}()
|
||||||
|
let sender_pubkey = nostr_event.pubkey
|
||||||
|
|
||||||
// Don't show notification details that match mute list.
|
// Don't show notification details that match mute list.
|
||||||
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
|
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
|
||||||
@@ -56,7 +80,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
contentHandler(content)
|
contentHandler(content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
|
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
|
||||||
Log.debug("should_display_notification failed", for: .push_notifications)
|
Log.debug("should_display_notification failed", for: .push_notifications)
|
||||||
// We should not display notification for this event. Suppress notification.
|
// We should not display notification for this event. Suppress notification.
|
||||||
@@ -65,7 +89,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
contentHandler(request.content)
|
contentHandler(request.content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
|
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
|
||||||
Log.debug("generate_local_notification_object failed", for: .push_notifications)
|
Log.debug("generate_local_notification_object failed", for: .push_notifications)
|
||||||
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
||||||
@@ -74,15 +98,58 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
contentHandler(request.content)
|
contentHandler(request.content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: name, notify: notification_object, state: state) else {
|
let sender_dn = DisplayName(name: sender_profile.name, display_name: sender_profile.display_name, pubkey: sender_pubkey)
|
||||||
|
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: sender_dn.displayName, notify: notification_object, state: state) else {
|
||||||
|
|
||||||
Log.debug("NotificationFormatter.format_message failed", for: .push_notifications)
|
Log.debug("NotificationFormatter.format_message failed", for: .push_notifications)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
contentHandler(improvedContent)
|
do {
|
||||||
|
var options: [AnyHashable: Any] = [:]
|
||||||
|
if let imageSource = CGImageSourceCreateWithURL(sender_profile.picture as CFURL, nil),
|
||||||
|
let uti = CGImageSourceGetType(imageSource) {
|
||||||
|
options[UNNotificationAttachmentOptionsTypeHintKey] = uti
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachment = try UNNotificationAttachment(identifier: sender_profile.picture.absoluteString, url: sender_profile.picture, options: options)
|
||||||
|
improvedContent.attachments = [attachment]
|
||||||
|
} catch {
|
||||||
|
Log.error("failed to get notification attachment: %s", for: .push_notifications, error.localizedDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = nostr_event.known_kind
|
||||||
|
|
||||||
|
// these aren't supported yet
|
||||||
|
if !(kind == .text || kind == .dm) {
|
||||||
|
contentHandler(improvedContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// rich communication notifications for kind1, dms, etc
|
||||||
|
|
||||||
|
let message_intent = await message_intent_from_note(ndb: state.ndb,
|
||||||
|
sender_profile: sender_profile,
|
||||||
|
content: improvedContent.body,
|
||||||
|
note: nostr_event,
|
||||||
|
our_pubkey: state.keypair.pubkey)
|
||||||
|
|
||||||
|
improvedContent.threadIdentifier = nostr_event.thread_id().hex()
|
||||||
|
improvedContent.categoryIdentifier = "COMMUNICATION"
|
||||||
|
|
||||||
|
let interaction = INInteraction(intent: message_intent, response: nil)
|
||||||
|
interaction.direction = .incoming
|
||||||
|
do {
|
||||||
|
try await interaction.donate()
|
||||||
|
let updated = try improvedContent.updating(from: message_intent)
|
||||||
|
contentHandler(updated)
|
||||||
|
} catch {
|
||||||
|
Log.error("failed to donate interaction: %s", for: .push_notifications, error.localizedDescription)
|
||||||
|
contentHandler(improvedContent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,3 +162,162 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ProfileBuf {
|
||||||
|
let picture: URL
|
||||||
|
let name: String?
|
||||||
|
let display_name: String?
|
||||||
|
let nip05: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
func message_intent_from_note(ndb: Ndb, sender_profile: ProfileBuf, content: String, note: NdbNote, our_pubkey: Pubkey) async -> INSendMessageIntent {
|
||||||
|
let sender_pk = note.pubkey
|
||||||
|
let sender = await profile_to_inperson(name: sender_profile.name,
|
||||||
|
display_name: sender_profile.display_name,
|
||||||
|
picture: sender_profile.picture.absoluteString,
|
||||||
|
nip05: sender_profile.nip05,
|
||||||
|
pubkey: sender_pk,
|
||||||
|
our_pubkey: our_pubkey)
|
||||||
|
|
||||||
|
let conversationIdentifier = note.thread_id().hex()
|
||||||
|
var recipients: [INPerson] = []
|
||||||
|
var pks: [Pubkey] = []
|
||||||
|
let meta = INSendMessageIntentDonationMetadata()
|
||||||
|
|
||||||
|
// gather recipients
|
||||||
|
if let recipient_note_id = note.direct_replies() {
|
||||||
|
let replying_to = ndb.lookup_note(recipient_note_id)
|
||||||
|
if let replying_to_pk = replying_to?.unsafeUnownedValue?.pubkey {
|
||||||
|
meta.isReplyToCurrentUser = replying_to_pk == our_pubkey
|
||||||
|
|
||||||
|
if replying_to_pk != sender_pk {
|
||||||
|
// we push the actual person being replied to first
|
||||||
|
pks.append(replying_to_pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pubkeys = Array(note.referenced_pubkeys)
|
||||||
|
meta.recipientCount = pubkeys.count
|
||||||
|
if pubkeys.contains(sender_pk) {
|
||||||
|
meta.recipientCount -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for pk in pubkeys.prefix(3) {
|
||||||
|
if pk == sender_pk || pks.contains(pk) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !meta.isReplyToCurrentUser && pk == our_pubkey {
|
||||||
|
meta.mentionsCurrentUser = true
|
||||||
|
}
|
||||||
|
|
||||||
|
pks.append(pk)
|
||||||
|
}
|
||||||
|
|
||||||
|
for pk in pks {
|
||||||
|
let recipient = await pubkey_to_inperson(ndb: ndb, pubkey: pk, our_pubkey: our_pubkey)
|
||||||
|
recipients.append(recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we enable default formatting this way
|
||||||
|
var groupName = INSpeakableString(spokenPhrase: "")
|
||||||
|
|
||||||
|
// otherwise we just say its a DM
|
||||||
|
if note.known_kind == .dm {
|
||||||
|
groupName = INSpeakableString(spokenPhrase: "DM")
|
||||||
|
}
|
||||||
|
|
||||||
|
let intent = INSendMessageIntent(recipients: recipients,
|
||||||
|
outgoingMessageType: .outgoingMessageText,
|
||||||
|
content: content,
|
||||||
|
speakableGroupName: groupName,
|
||||||
|
conversationIdentifier: conversationIdentifier,
|
||||||
|
serviceName: "kind\(note.kind)",
|
||||||
|
sender: sender,
|
||||||
|
attachments: nil)
|
||||||
|
intent.donationMetadata = meta
|
||||||
|
|
||||||
|
// this is needed for recipients > 0
|
||||||
|
if let img = sender.image {
|
||||||
|
intent.setImage(img, forParameterNamed: \.speakableGroupName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubkey_to_inperson(ndb: Ndb, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
|
||||||
|
let profile_txn = ndb.lookup_profile(pubkey)
|
||||||
|
let profile = profile_txn?.unsafeUnownedValue?.profile
|
||||||
|
let name = profile?.name
|
||||||
|
let display_name = profile?.display_name
|
||||||
|
let nip05 = profile?.nip05
|
||||||
|
let picture = profile?.picture
|
||||||
|
|
||||||
|
return await profile_to_inperson(name: name,
|
||||||
|
display_name: display_name,
|
||||||
|
picture: picture,
|
||||||
|
nip05: nip05,
|
||||||
|
pubkey: pubkey,
|
||||||
|
our_pubkey: our_pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch_pfp(picture: URL) async throws -> RetrieveImageResult {
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: picture)) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let img):
|
||||||
|
continuation.resume(returning: img)
|
||||||
|
case .failure(let error):
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func profile_to_inperson(name: String?, display_name: String?, picture: String?, nip05: String?, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
|
||||||
|
let npub = pubkey.npub
|
||||||
|
let handle = INPersonHandle(value: npub, type: .unknown)
|
||||||
|
var aliases: [INPersonHandle] = []
|
||||||
|
|
||||||
|
if let nip05 {
|
||||||
|
aliases.append(INPersonHandle(value: nip05, type: .emailAddress))
|
||||||
|
}
|
||||||
|
|
||||||
|
let nostrName = DisplayName(name: name, display_name: display_name, pubkey: pubkey)
|
||||||
|
let nameComponents = nostrName.nameComponents()
|
||||||
|
let displayName = nostrName.displayName
|
||||||
|
let contactIdentifier = npub
|
||||||
|
let customIdentifier = npub
|
||||||
|
let suggestionType = INPersonSuggestionType.socialProfile
|
||||||
|
|
||||||
|
var image: INImage? = nil
|
||||||
|
|
||||||
|
if let picture,
|
||||||
|
let url = URL(string: picture),
|
||||||
|
let img = try? await fetch_pfp(picture: url),
|
||||||
|
let imgdata = img.data()
|
||||||
|
{
|
||||||
|
image = INImage(imageData: imgdata)
|
||||||
|
} else {
|
||||||
|
Log.error("Failed to fetch pfp (%s) for %s", for: .push_notifications, picture ?? "nil", displayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
let person = INPerson(personHandle: handle,
|
||||||
|
nameComponents: nameComponents,
|
||||||
|
displayName: displayName,
|
||||||
|
image: image,
|
||||||
|
contactIdentifier: contactIdentifier,
|
||||||
|
customIdentifier: customIdentifier,
|
||||||
|
isMe: pubkey == our_pubkey,
|
||||||
|
suggestionType: suggestionType
|
||||||
|
)
|
||||||
|
|
||||||
|
return person
|
||||||
|
}
|
||||||
|
|
||||||
|
func robohash(_ pk: Pubkey) -> String {
|
||||||
|
return "https://robohash.org/" + pk.hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+32
-3
@@ -1,3 +1,32 @@
|
|||||||
dependencies: [
|
// swift-tools-version: 6.0
|
||||||
.Package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
]
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "damus",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v16),
|
||||||
|
.macOS(.v12)
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "damus",
|
||||||
|
targets: ["damus"]),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "damus",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "secp256k1", package: "secp256k1.swift")
|
||||||
|
],
|
||||||
|
path: "damus"),
|
||||||
|
.testTarget(
|
||||||
|
name: "damusTests",
|
||||||
|
dependencies: ["damus"],
|
||||||
|
path: "damusTests"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|||||||
+180
-36
@@ -22,6 +22,7 @@
|
|||||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
|
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
|
||||||
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
|
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
|
||||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
|
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
|
||||||
|
3A96E3FE2D6BCE3800AE1630 /* RepostedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */; };
|
||||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
|
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
|
||||||
@@ -188,6 +189,7 @@
|
|||||||
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; };
|
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; };
|
||||||
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; };
|
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; };
|
||||||
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */; };
|
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */; };
|
||||||
|
4C5726BA2D72C6FA00E7FF82 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C5726B92D72C6FA00E7FF82 /* Kingfisher */; };
|
||||||
4C59B98C2A76C2550032FFEB /* ProfileUpdatedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C59B98B2A76C2550032FFEB /* ProfileUpdatedNotify.swift */; };
|
4C59B98C2A76C2550032FFEB /* ProfileUpdatedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C59B98B2A76C2550032FFEB /* ProfileUpdatedNotify.swift */; };
|
||||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
|
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
|
||||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
|
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
|
||||||
@@ -205,7 +207,7 @@
|
|||||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
|
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
|
||||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
|
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
|
||||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
|
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
|
||||||
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A542A7E91FE005E6031 /* LongPostTests.swift */; };
|
4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A542A7E91FE005E6031 /* LargeEventTests.swift */; };
|
||||||
4C684A572A7FFAE6005E6031 /* UrlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A562A7FFAE6005E6031 /* UrlTests.swift */; };
|
4C684A572A7FFAE6005E6031 /* UrlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A562A7FFAE6005E6031 /* UrlTests.swift */; };
|
||||||
4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; };
|
4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; };
|
||||||
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */; };
|
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */; };
|
||||||
@@ -406,10 +408,22 @@
|
|||||||
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
|
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
|
||||||
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */; };
|
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */; };
|
||||||
5C7389B12B6EFA7100781E0A /* ProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B02B6EFA7100781E0A /* ProxyView.swift */; };
|
5C7389B12B6EFA7100781E0A /* ProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B02B6EFA7100781E0A /* ProxyView.swift */; };
|
||||||
|
5C8498022D5D150000F74FEB /* ZapExplainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */; };
|
||||||
|
5C8498032D5D150000F74FEB /* ZapExplainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */; };
|
||||||
|
5C8498042D5D150000F74FEB /* ZapExplainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */; };
|
||||||
5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */; };
|
5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */; };
|
||||||
5CB017212D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; };
|
5CB017212D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; };
|
||||||
5CB017222D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; };
|
5CB017222D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; };
|
||||||
5CB017232D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; };
|
5CB017232D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; };
|
||||||
|
5CB017252D42C5C400A9ED05 /* TransactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017242D42C5BD00A9ED05 /* TransactionsView.swift */; };
|
||||||
|
5CB017262D42C5C400A9ED05 /* TransactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017242D42C5BD00A9ED05 /* TransactionsView.swift */; };
|
||||||
|
5CB017272D42C5C400A9ED05 /* TransactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017242D42C5BD00A9ED05 /* TransactionsView.swift */; };
|
||||||
|
5CB0172D2D42C76A00A9ED05 /* BalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0172C2D42C76600A9ED05 /* BalanceView.swift */; };
|
||||||
|
5CB0172E2D42C76A00A9ED05 /* BalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0172C2D42C76600A9ED05 /* BalanceView.swift */; };
|
||||||
|
5CB0172F2D42C76A00A9ED05 /* BalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0172C2D42C76600A9ED05 /* BalanceView.swift */; };
|
||||||
|
5CB017312D4422DB00A9ED05 /* NWCSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017302D4422D600A9ED05 /* NWCSettings.swift */; };
|
||||||
|
5CB017322D4422DB00A9ED05 /* NWCSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017302D4422D600A9ED05 /* NWCSettings.swift */; };
|
||||||
|
5CB017332D4422DB00A9ED05 /* NWCSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017302D4422D600A9ED05 /* NWCSettings.swift */; };
|
||||||
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */; };
|
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */; };
|
||||||
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */; };
|
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */; };
|
||||||
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */; };
|
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */; };
|
||||||
@@ -628,7 +642,6 @@
|
|||||||
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
|
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
|
||||||
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
|
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
|
||||||
82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
|
82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
|
||||||
82D6FB6F2CD99F7900C925F4 /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
|
|
||||||
82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
||||||
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
||||||
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
||||||
@@ -919,7 +932,6 @@
|
|||||||
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
||||||
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
||||||
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
||||||
BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
|
|
||||||
BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; };
|
BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; };
|
||||||
BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; };
|
BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; };
|
||||||
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; };
|
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; };
|
||||||
@@ -1078,11 +1090,30 @@
|
|||||||
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; };
|
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; };
|
||||||
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
|
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
|
||||||
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
|
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
|
||||||
|
D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; };
|
||||||
|
D733F9E22D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; };
|
||||||
|
D733F9E32D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; };
|
||||||
|
D733F9E52D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
|
||||||
|
D733F9E62D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
|
||||||
|
D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
|
||||||
|
D733F9E82D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
|
||||||
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; };
|
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; };
|
||||||
D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; };
|
D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; };
|
||||||
D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */; };
|
D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */; };
|
||||||
D7373BA82B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */; };
|
D7373BA82B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */; };
|
||||||
D7373BAA2B68A65A00F7783D /* PurpleAccountUpdateNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA92B68A65A00F7783D /* PurpleAccountUpdateNotify.swift */; };
|
D7373BAA2B68A65A00F7783D /* PurpleAccountUpdateNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA92B68A65A00F7783D /* PurpleAccountUpdateNotify.swift */; };
|
||||||
|
D73B74E12D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; };
|
||||||
|
D73B74E22D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; };
|
||||||
|
D73B74E32D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; };
|
||||||
|
D73BDB0D2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; };
|
||||||
|
D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; };
|
||||||
|
D73BDB102D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; };
|
||||||
|
D73BDB142D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; };
|
||||||
|
D73BDB152D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; };
|
||||||
|
D73BDB162D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; };
|
||||||
|
D73BDB182D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; };
|
||||||
|
D73BDB192D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; };
|
||||||
|
D73BDB1A2D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; };
|
||||||
D73E5E162C6A9619007EB227 /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
|
D73E5E162C6A9619007EB227 /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
|
||||||
D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
|
D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
|
||||||
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
||||||
@@ -1192,7 +1223,6 @@
|
|||||||
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
||||||
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
|
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
|
||||||
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
|
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
|
||||||
D73E5E8D2C6A97F4007EB227 /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
|
|
||||||
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
||||||
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
||||||
D73E5E902C6A97F4007EB227 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
D73E5E902C6A97F4007EB227 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
||||||
@@ -1479,6 +1509,18 @@
|
|||||||
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; };
|
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; };
|
||||||
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */; };
|
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */; };
|
||||||
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */; };
|
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */; };
|
||||||
|
D78F080C2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
||||||
|
D78F080D2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
||||||
|
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
||||||
|
D78F080F2D7F78EF00FC6C75 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F080B2D7F78EB00FC6C75 /* Request.swift */; };
|
||||||
|
D78F08112D7F78F900FC6C75 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08102D7F78F600FC6C75 /* Response.swift */; };
|
||||||
|
D78F08122D7F78F900FC6C75 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08102D7F78F600FC6C75 /* Response.swift */; };
|
||||||
|
D78F08132D7F78F900FC6C75 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08102D7F78F600FC6C75 /* Response.swift */; };
|
||||||
|
D78F08142D7F78F900FC6C75 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08102D7F78F600FC6C75 /* Response.swift */; };
|
||||||
|
D78F08172D7F7F7500FC6C75 /* NIP04.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08162D7F7F6C00FC6C75 /* NIP04.swift */; };
|
||||||
|
D78F08182D7F7F7500FC6C75 /* NIP04.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08162D7F7F6C00FC6C75 /* NIP04.swift */; };
|
||||||
|
D78F08192D7F7F7500FC6C75 /* NIP04.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08162D7F7F6C00FC6C75 /* NIP04.swift */; };
|
||||||
|
D78F081A2D7F803100FC6C75 /* NIP04.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78F08162D7F7F6C00FC6C75 /* NIP04.swift */; };
|
||||||
D798D21A2B0856CC00234419 /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
|
D798D21A2B0856CC00234419 /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
|
||||||
D798D21B2B0856F200234419 /* NdbTagsIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDD1AE12A6B3074001CD4DF /* NdbTagsIterator.swift */; };
|
D798D21B2B0856F200234419 /* NdbTagsIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDD1AE12A6B3074001CD4DF /* NdbTagsIterator.swift */; };
|
||||||
D798D21C2B0857E400234419 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
|
D798D21C2B0857E400234419 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
|
||||||
@@ -1623,6 +1665,9 @@
|
|||||||
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||||
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||||
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||||
|
D7DB930A2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
|
||||||
|
D7DB930B2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
|
||||||
|
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
|
||||||
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
|
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
|
||||||
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
||||||
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
||||||
@@ -1805,6 +1850,7 @@
|
|||||||
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
|
3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedTests.swift; sourceTree = "<group>"; };
|
||||||
3A994C4C2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
3A994C4C2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
3A994C4D2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
3A994C4D2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
3A994C4E2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
|
3A994C4E2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
@@ -2146,7 +2192,7 @@
|
|||||||
4C64305B2A945AFF00B0C0E9 /* MusicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicController.swift; sourceTree = "<group>"; };
|
4C64305B2A945AFF00B0C0E9 /* MusicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicController.swift; sourceTree = "<group>"; };
|
||||||
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
|
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
|
||||||
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
|
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
|
||||||
4C684A542A7E91FE005E6031 /* LongPostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPostTests.swift; sourceTree = "<group>"; };
|
4C684A542A7E91FE005E6031 /* LargeEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeEventTests.swift; sourceTree = "<group>"; };
|
||||||
4C684A562A7FFAE6005E6031 /* UrlTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlTests.swift; sourceTree = "<group>"; };
|
4C684A562A7FFAE6005E6031 /* UrlTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlTests.swift; sourceTree = "<group>"; };
|
||||||
4C687C202A5F7ED00092C550 /* DamusBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusBackground.swift; sourceTree = "<group>"; };
|
4C687C202A5F7ED00092C550 /* DamusBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusBackground.swift; sourceTree = "<group>"; };
|
||||||
4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeaderView.swift; sourceTree = "<group>"; };
|
4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeaderView.swift; sourceTree = "<group>"; };
|
||||||
@@ -2365,8 +2411,12 @@
|
|||||||
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
|
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
|
||||||
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinkGradient.swift; sourceTree = "<group>"; };
|
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinkGradient.swift; sourceTree = "<group>"; };
|
||||||
5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = "<group>"; };
|
5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = "<group>"; };
|
||||||
|
5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapExplainer.swift; sourceTree = "<group>"; };
|
||||||
5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingTimelineView.swift; sourceTree = "<group>"; };
|
5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingTimelineView.swift; sourceTree = "<group>"; };
|
||||||
5CB017202D2D985800A9ED05 /* CoinosButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinosButton.swift; sourceTree = "<group>"; };
|
5CB017202D2D985800A9ED05 /* CoinosButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinosButton.swift; sourceTree = "<group>"; };
|
||||||
|
5CB017242D42C5BD00A9ED05 /* TransactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsView.swift; sourceTree = "<group>"; };
|
||||||
|
5CB0172C2D42C76600A9ED05 /* BalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceView.swift; sourceTree = "<group>"; };
|
||||||
|
5CB017302D4422D600A9ED05 /* NWCSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWCSettings.swift; sourceTree = "<group>"; };
|
||||||
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEvent.swift; sourceTree = "<group>"; };
|
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEvent.swift; sourceTree = "<group>"; };
|
||||||
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = "<group>"; };
|
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = "<group>"; };
|
||||||
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDescription.swift; sourceTree = "<group>"; };
|
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDescription.swift; sourceTree = "<group>"; };
|
||||||
@@ -2408,7 +2458,6 @@
|
|||||||
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
|
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
|
||||||
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
|
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
|
||||||
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
|
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
|
||||||
BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = "<group>"; };
|
|
||||||
BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = "<group>"; };
|
BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = "<group>"; };
|
||||||
BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = "<group>"; };
|
BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = "<group>"; };
|
||||||
BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
|
BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
|
||||||
@@ -2444,10 +2493,16 @@
|
|||||||
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilterTests.swift; sourceTree = "<group>"; };
|
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilterTests.swift; sourceTree = "<group>"; };
|
||||||
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
|
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
|
||||||
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
|
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
|
||||||
|
D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = "<group>"; };
|
||||||
|
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnownedNdbNote.swift; sourceTree = "<group>"; };
|
||||||
D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusFullScreenCover.swift; sourceTree = "<group>"; };
|
D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusFullScreenCover.swift; sourceTree = "<group>"; };
|
||||||
D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleTranslationSetupView.swift; sourceTree = "<group>"; };
|
D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleTranslationSetupView.swift; sourceTree = "<group>"; };
|
||||||
D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNewUserOnboardingView.swift; sourceTree = "<group>"; };
|
D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNewUserOnboardingView.swift; sourceTree = "<group>"; };
|
||||||
D7373BA92B68A65A00F7783D /* PurpleAccountUpdateNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleAccountUpdateNotify.swift; sourceTree = "<group>"; };
|
D7373BA92B68A65A00F7783D /* PurpleAccountUpdateNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleAccountUpdateNotify.swift; sourceTree = "<group>"; };
|
||||||
|
D73B74E02D8365B40067BDBC /* ExtraFonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtraFonts.swift; sourceTree = "<group>"; };
|
||||||
|
D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrNetworkManager.swift; sourceTree = "<group>"; };
|
||||||
|
D73BDB132D71215F00D69970 /* UserRelayListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelayListManager.swift; sourceTree = "<group>"; };
|
||||||
|
D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelayListErrors.swift; sourceTree = "<group>"; };
|
||||||
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAliases.swift; sourceTree = "<group>"; };
|
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAliases.swift; sourceTree = "<group>"; };
|
||||||
D73E5F802C6AA07A007EB227 /* HighlighterExtensionAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlighterExtensionAliases.swift; sourceTree = "<group>"; };
|
D73E5F802C6AA07A007EB227 /* HighlighterExtensionAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlighterExtensionAliases.swift; sourceTree = "<group>"; };
|
||||||
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessDamusState.swift; sourceTree = "<group>"; };
|
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessDamusState.swift; sourceTree = "<group>"; };
|
||||||
@@ -2474,6 +2529,9 @@
|
|||||||
D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; };
|
D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; };
|
||||||
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorMath.swift; sourceTree = "<group>"; };
|
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorMath.swift; sourceTree = "<group>"; };
|
||||||
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = "<group>"; };
|
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = "<group>"; };
|
||||||
|
D78F080B2D7F78EB00FC6C75 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
|
||||||
|
D78F08102D7F78F600FC6C75 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
|
||||||
|
D78F08162D7F7F6C00FC6C75 /* NIP04.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP04.swift; sourceTree = "<group>"; };
|
||||||
D798D21D2B0858BB00234419 /* MigratedTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedTypes.swift; sourceTree = "<group>"; };
|
D798D21D2B0858BB00234419 /* MigratedTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedTypes.swift; sourceTree = "<group>"; };
|
||||||
D798D2272B085CDA00234419 /* NdbNote+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NdbNote+.swift"; sourceTree = "<group>"; };
|
D798D2272B085CDA00234419 /* NdbNote+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NdbNote+.swift"; sourceTree = "<group>"; };
|
||||||
D798D22B2B086C7400234419 /* NostrEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrEvent+.swift"; sourceTree = "<group>"; };
|
D798D22B2B086C7400234419 /* NostrEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrEvent+.swift"; sourceTree = "<group>"; };
|
||||||
@@ -2503,6 +2561,7 @@
|
|||||||
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = nip44.vectors.json; sourceTree = "<group>"; };
|
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = nip44.vectors.json; sourceTree = "<group>"; };
|
||||||
D7DB1FF22D5AC5E400CF06DA /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSES; sourceTree = "<group>"; };
|
D7DB1FF22D5AC5E400CF06DA /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSES; sourceTree = "<group>"; };
|
||||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Undistractor.swift; sourceTree = "<group>"; };
|
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Undistractor.swift; sourceTree = "<group>"; };
|
||||||
|
D7DB93092D69485A00DA1EE5 /* NIP65.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP65.swift; sourceTree = "<group>"; };
|
||||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
||||||
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
|
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
|
||||||
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
|
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
|
||||||
@@ -2608,6 +2667,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
4C5726BA2D72C6FA00E7FF82 /* Kingfisher in Frameworks */,
|
||||||
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */,
|
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */,
|
||||||
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
|
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
|
||||||
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
|
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
|
||||||
@@ -2706,6 +2766,7 @@
|
|||||||
4C0A3F8D280F63FF000448DE /* Models */ = {
|
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D73BDB122D71212600D69970 /* NostrNetworkManager */,
|
||||||
D74F43082B23F09300425B75 /* Purple */,
|
D74F43082B23F09300425B75 /* Purple */,
|
||||||
BA3759882ABCCDE30018D73B /* Camera */,
|
BA3759882ABCCDE30018D73B /* Camera */,
|
||||||
4C190F1E2A535FC200027FD5 /* Zaps */,
|
4C190F1E2A535FC200027FD5 /* Zaps */,
|
||||||
@@ -3218,6 +3279,10 @@
|
|||||||
4C7D095A2A098C5C00943473 /* Wallet */ = {
|
4C7D095A2A098C5C00943473 /* Wallet */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */,
|
||||||
|
5CB017302D4422D600A9ED05 /* NWCSettings.swift */,
|
||||||
|
5CB0172C2D42C76600A9ED05 /* BalanceView.swift */,
|
||||||
|
5CB017242D42C5BD00A9ED05 /* TransactionsView.swift */,
|
||||||
4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */,
|
4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */,
|
||||||
4C7D095D2A098C5D00943473 /* WalletView.swift */,
|
4C7D095D2A098C5D00943473 /* WalletView.swift */,
|
||||||
4C7D09672A0AE9B200943473 /* NWCScannerView.swift */,
|
4C7D09672A0AE9B200943473 /* NWCScannerView.swift */,
|
||||||
@@ -3243,11 +3308,12 @@
|
|||||||
4C7FF7D628233637009601DB /* Util */ = {
|
4C7FF7D628233637009601DB /* Util */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D73B74E02D8365B40067BDBC /* ExtraFonts.swift */,
|
||||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */,
|
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */,
|
||||||
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */,
|
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */,
|
||||||
E04A37C52B544F090029650D /* URIParsing.swift */,
|
E04A37C52B544F090029650D /* URIParsing.swift */,
|
||||||
4C1D4FB02A7958E60024F453 /* VersionInfo.swift */,
|
4C1D4FB02A7958E60024F453 /* VersionInfo.swift */,
|
||||||
4C7D09612A098D0E00943473 /* WalletConnect.swift */,
|
D78F080A2D7F78B000FC6C75 /* WalletConnect */,
|
||||||
4C198DF329F88D23004C165C /* Images */,
|
4C198DF329F88D23004C165C /* Images */,
|
||||||
4C198DEA29F88C6B004C165C /* BlurHash */,
|
4C198DEA29F88C6B004C165C /* BlurHash */,
|
||||||
4CE4F0F329D779B5005914DB /* PostBox.swift */,
|
4CE4F0F329D779B5005914DB /* PostBox.swift */,
|
||||||
@@ -3297,7 +3363,6 @@
|
|||||||
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */,
|
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */,
|
||||||
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
|
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
|
||||||
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
|
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
|
||||||
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */,
|
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3316,6 +3381,7 @@
|
|||||||
4C9054862A6AEB4500811EEC /* nostrdb */ = {
|
4C9054862A6AEB4500811EEC /* nostrdb */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */,
|
||||||
4C47928D2A9939BD00489948 /* flatcc */,
|
4C47928D2A9939BD00489948 /* flatcc */,
|
||||||
4C478E2A2A9935D300489948 /* bindings */,
|
4C478E2A2A9935D300489948 /* bindings */,
|
||||||
4CE9FBBB2A6B3D9C007E485C /* Test */,
|
4CE9FBBB2A6B3D9C007E485C /* Test */,
|
||||||
@@ -3607,8 +3673,10 @@
|
|||||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D7DB93082D69478400DA1EE5 /* NIP65 */,
|
||||||
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
|
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
|
||||||
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
|
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
|
||||||
|
D78F08152D7F7F5F00FC6C75 /* NIP04 */,
|
||||||
4C45E5002BED4CE10025A428 /* NIP10 */,
|
4C45E5002BED4CE10025A428 /* NIP10 */,
|
||||||
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
|
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
|
||||||
4CA3529C2A76AE47003BB08B /* Notify */,
|
4CA3529C2A76AE47003BB08B /* Notify */,
|
||||||
@@ -3675,7 +3743,7 @@
|
|||||||
4C19AE542A5D977400C90DB7 /* HashtagTests.swift */,
|
4C19AE542A5D977400C90DB7 /* HashtagTests.swift */,
|
||||||
3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */,
|
3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */,
|
||||||
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */,
|
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */,
|
||||||
4C684A542A7E91FE005E6031 /* LongPostTests.swift */,
|
4C684A542A7E91FE005E6031 /* LargeEventTests.swift */,
|
||||||
4C684A562A7FFAE6005E6031 /* UrlTests.swift */,
|
4C684A562A7FFAE6005E6031 /* UrlTests.swift */,
|
||||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */,
|
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */,
|
||||||
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */,
|
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */,
|
||||||
@@ -3689,6 +3757,7 @@
|
|||||||
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */,
|
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */,
|
||||||
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
|
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
|
||||||
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
|
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
|
||||||
|
3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */,
|
||||||
);
|
);
|
||||||
path = damusTests;
|
path = damusTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3848,7 +3917,6 @@
|
|||||||
children = (
|
children = (
|
||||||
BA3759902ABCCEBA0018D73B /* CameraModel.swift */,
|
BA3759902ABCCEBA0018D73B /* CameraModel.swift */,
|
||||||
BA3759912ABCCEBA0018D73B /* CameraService.swift */,
|
BA3759912ABCCEBA0018D73B /* CameraService.swift */,
|
||||||
BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */,
|
|
||||||
BA3759892ABCCDE30018D73B /* ImageResizer.swift */,
|
BA3759892ABCCDE30018D73B /* ImageResizer.swift */,
|
||||||
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */,
|
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */,
|
||||||
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */,
|
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */,
|
||||||
@@ -3908,6 +3976,17 @@
|
|||||||
path = Mocking;
|
path = Mocking;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D73BDB122D71212600D69970 /* NostrNetworkManager */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */,
|
||||||
|
D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */,
|
||||||
|
D73BDB132D71215F00D69970 /* UserRelayListManager.swift */,
|
||||||
|
D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */,
|
||||||
|
);
|
||||||
|
path = NostrNetworkManager;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D74EA08C2D2E26E6002290DD /* ErrorHandling */ = {
|
D74EA08C2D2E26E6002290DD /* ErrorHandling */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -3948,6 +4027,25 @@
|
|||||||
path = Chat;
|
path = Chat;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D78F080A2D7F78B000FC6C75 /* WalletConnect */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D78F08102D7F78F600FC6C75 /* Response.swift */,
|
||||||
|
D78F080B2D7F78EB00FC6C75 /* Request.swift */,
|
||||||
|
4C7D09612A098D0E00943473 /* WalletConnect.swift */,
|
||||||
|
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */,
|
||||||
|
);
|
||||||
|
path = WalletConnect;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D78F08152D7F7F5F00FC6C75 /* NIP04 */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D78F08162D7F7F6C00FC6C75 /* NIP04.swift */,
|
||||||
|
);
|
||||||
|
path = NIP04;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = {
|
D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -3985,6 +4083,14 @@
|
|||||||
path = NIP44;
|
path = NIP44;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D7DB93082D69478400DA1EE5 /* NIP65 */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D7DB93092D69485A00DA1EE5 /* NIP65.swift */,
|
||||||
|
);
|
||||||
|
path = NIP65;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E06336A72B7582D600A88E6B /* Assets */ = {
|
E06336A72B7582D600A88E6B /* Assets */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -4171,6 +4277,7 @@
|
|||||||
D789D11F2AFEFBF20083A7AB /* secp256k1 */,
|
D789D11F2AFEFBF20083A7AB /* secp256k1 */,
|
||||||
D7EDED302B1290B80018B19C /* MarkdownUI */,
|
D7EDED302B1290B80018B19C /* MarkdownUI */,
|
||||||
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */,
|
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */,
|
||||||
|
4C5726B92D72C6FA00E7FF82 /* Kingfisher */,
|
||||||
);
|
);
|
||||||
productName = DamusNotificationService;
|
productName = DamusNotificationService;
|
||||||
productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */;
|
productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */;
|
||||||
@@ -4373,6 +4480,7 @@
|
|||||||
4C3DCC762A9FE9EC0091E592 /* NdbTxn.swift in Sources */,
|
4C3DCC762A9FE9EC0091E592 /* NdbTxn.swift in Sources */,
|
||||||
4CEF958D2A9CE650000F901B /* verifier.c in Sources */,
|
4CEF958D2A9CE650000F901B /* verifier.c in Sources */,
|
||||||
4C32B9342A9AD01A00DC3548 /* NdbProfile.swift in Sources */,
|
4C32B9342A9AD01A00DC3548 /* NdbProfile.swift in Sources */,
|
||||||
|
D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
|
||||||
4C32B9332A99845B00DC3548 /* Ndb.swift in Sources */,
|
4C32B9332A99845B00DC3548 /* Ndb.swift in Sources */,
|
||||||
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */,
|
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */,
|
||||||
4C4793082A993E8900489948 /* refmap.c in Sources */,
|
4C4793082A993E8900489948 /* refmap.c in Sources */,
|
||||||
@@ -4398,6 +4506,7 @@
|
|||||||
D7CBD1D42B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift in Sources */,
|
D7CBD1D42B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift in Sources */,
|
||||||
4C32B9572A9AD44700DC3548 /* Root.swift in Sources */,
|
4C32B9572A9AD44700DC3548 /* Root.swift in Sources */,
|
||||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
||||||
|
5C8498032D5D150000F74FEB /* ZapExplainer.swift in Sources */,
|
||||||
504323A72A34915F006AE6DC /* RelayModel.swift in Sources */,
|
504323A72A34915F006AE6DC /* RelayModel.swift in Sources */,
|
||||||
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
|
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
|
||||||
4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */,
|
4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */,
|
||||||
@@ -4419,6 +4528,7 @@
|
|||||||
F757933A29D7AECD007DEAC1 /* MediaPicker.swift in Sources */,
|
F757933A29D7AECD007DEAC1 /* MediaPicker.swift in Sources */,
|
||||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
|
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
|
||||||
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */,
|
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */,
|
||||||
|
D7DB930B2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||||
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
||||||
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
|
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
|
||||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||||
@@ -4505,12 +4615,12 @@
|
|||||||
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
|
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
|
||||||
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
|
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
|
||||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||||
|
D73BDB1A2D71311900D69970 /* UserRelayListErrors.swift in Sources */,
|
||||||
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
|
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
|
||||||
4C86F7C42A76C44C00EC0817 /* ZappingNotify.swift in Sources */,
|
4C86F7C42A76C44C00EC0817 /* ZappingNotify.swift in Sources */,
|
||||||
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
||||||
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
|
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
|
||||||
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
||||||
BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */,
|
|
||||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */,
|
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */,
|
||||||
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
||||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
||||||
@@ -4542,6 +4652,7 @@
|
|||||||
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */,
|
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */,
|
||||||
D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */,
|
D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */,
|
||||||
50A16FFF2AA76A0900DFEC1F /* DamusVideoCoordinator.swift in Sources */,
|
50A16FFF2AA76A0900DFEC1F /* DamusVideoCoordinator.swift in Sources */,
|
||||||
|
D733F9E32D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
|
||||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
||||||
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
|
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
|
||||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
|
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
|
||||||
@@ -4596,6 +4707,7 @@
|
|||||||
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
|
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
|
||||||
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
|
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
|
||||||
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
|
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
|
||||||
|
D78F08142D7F78F900FC6C75 /* Response.swift in Sources */,
|
||||||
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
|
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
|
||||||
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
|
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
|
||||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
|
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
|
||||||
@@ -4621,6 +4733,7 @@
|
|||||||
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
|
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
|
||||||
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */,
|
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */,
|
||||||
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */,
|
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */,
|
||||||
|
D73B74E12D8365BA0067BDBC /* ExtraFonts.swift in Sources */,
|
||||||
4C7D09622A098D0E00943473 /* WalletConnect.swift in Sources */,
|
4C7D09622A098D0E00943473 /* WalletConnect.swift in Sources */,
|
||||||
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
||||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||||
@@ -4648,6 +4761,7 @@
|
|||||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||||
D73E5F7F2C6AA066007EB227 /* DamusAliases.swift in Sources */,
|
D73E5F7F2C6AA066007EB227 /* DamusAliases.swift in Sources */,
|
||||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayerView.swift in Sources */,
|
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayerView.swift in Sources */,
|
||||||
|
D73BDB152D71216500D69970 /* UserRelayListManager.swift in Sources */,
|
||||||
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
|
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
|
||||||
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */,
|
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */,
|
||||||
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
|
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
|
||||||
@@ -4657,6 +4771,7 @@
|
|||||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||||
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */,
|
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */,
|
||||||
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */,
|
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */,
|
||||||
|
5CB017252D42C5C400A9ED05 /* TransactionsView.swift in Sources */,
|
||||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||||
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
|
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
|
||||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||||
@@ -4706,6 +4821,8 @@
|
|||||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
||||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
|
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
|
||||||
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */,
|
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */,
|
||||||
|
5CB017312D4422DB00A9ED05 /* NWCSettings.swift in Sources */,
|
||||||
|
D78F080D2D7F78EF00FC6C75 /* Request.swift in Sources */,
|
||||||
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */,
|
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */,
|
||||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
|
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
|
||||||
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */,
|
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */,
|
||||||
@@ -4785,6 +4902,7 @@
|
|||||||
4C9146FE2A2A87C200DDEA40 /* nostrscript.c in Sources */,
|
4C9146FE2A2A87C200DDEA40 /* nostrscript.c in Sources */,
|
||||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||||
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
|
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
|
||||||
|
D78F08182D7F7F7500FC6C75 /* NIP04.swift in Sources */,
|
||||||
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */,
|
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */,
|
||||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
|
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
|
||||||
F71694EC2A662292001F4053 /* SuggestedUsersViewModel.swift in Sources */,
|
F71694EC2A662292001F4053 /* SuggestedUsersViewModel.swift in Sources */,
|
||||||
@@ -4803,6 +4921,7 @@
|
|||||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
||||||
4C9147002A2A891E00DDEA40 /* error.c in Sources */,
|
4C9147002A2A891E00DDEA40 /* error.c in Sources */,
|
||||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
||||||
|
5CB0172F2D42C76A00A9ED05 /* BalanceView.swift in Sources */,
|
||||||
4C1253602A76CF890004F4B8 /* ScrollToTopNotify.swift in Sources */,
|
4C1253602A76CF890004F4B8 /* ScrollToTopNotify.swift in Sources */,
|
||||||
4CA3529E2A76AE67003BB08B /* FollowNotify.swift in Sources */,
|
4CA3529E2A76AE67003BB08B /* FollowNotify.swift in Sources */,
|
||||||
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
||||||
@@ -4846,6 +4965,7 @@
|
|||||||
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
|
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
|
||||||
D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */,
|
D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */,
|
||||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||||
|
D733F9E82D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
||||||
D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */,
|
D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */,
|
||||||
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
|
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
|
||||||
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */,
|
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */,
|
||||||
@@ -4882,6 +5002,7 @@
|
|||||||
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
|
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
|
||||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||||
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
||||||
|
3A96E3FE2D6BCE3800AE1630 /* RepostedTests.swift in Sources */,
|
||||||
4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */,
|
4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */,
|
||||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||||
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */,
|
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */,
|
||||||
@@ -4904,7 +5025,7 @@
|
|||||||
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */,
|
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */,
|
||||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
|
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
|
||||||
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
|
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
|
||||||
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */,
|
4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */,
|
||||||
E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */,
|
E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -4946,6 +5067,7 @@
|
|||||||
82D6FABC2CD99F7900C925F4 /* refmap.c in Sources */,
|
82D6FABC2CD99F7900C925F4 /* refmap.c in Sources */,
|
||||||
82D6FABD2CD99F7900C925F4 /* verifier.c in Sources */,
|
82D6FABD2CD99F7900C925F4 /* verifier.c in Sources */,
|
||||||
82D6FABE2CD99F7900C925F4 /* NdbProfile.swift in Sources */,
|
82D6FABE2CD99F7900C925F4 /* NdbProfile.swift in Sources */,
|
||||||
|
D78F08112D7F78F900FC6C75 /* Response.swift in Sources */,
|
||||||
82D6FABF2CD99F7900C925F4 /* NdbTagIterator.swift in Sources */,
|
82D6FABF2CD99F7900C925F4 /* NdbTagIterator.swift in Sources */,
|
||||||
82D6FAC02CD99F7900C925F4 /* NdbNote.swift in Sources */,
|
82D6FAC02CD99F7900C925F4 /* NdbNote.swift in Sources */,
|
||||||
82D6FAC12CD99F7900C925F4 /* AsciiCharacter.swift in Sources */,
|
82D6FAC12CD99F7900C925F4 /* AsciiCharacter.swift in Sources */,
|
||||||
@@ -4982,6 +5104,7 @@
|
|||||||
82D6FAE02CD99F7900C925F4 /* DisplayTabBarNotify.swift in Sources */,
|
82D6FAE02CD99F7900C925F4 /* DisplayTabBarNotify.swift in Sources */,
|
||||||
82D6FAE12CD99F7900C925F4 /* BroadcastNotify.swift in Sources */,
|
82D6FAE12CD99F7900C925F4 /* BroadcastNotify.swift in Sources */,
|
||||||
82D6FAE22CD99F7900C925F4 /* ComposeNotify.swift in Sources */,
|
82D6FAE22CD99F7900C925F4 /* ComposeNotify.swift in Sources */,
|
||||||
|
D73B74E22D8365BA0067BDBC /* ExtraFonts.swift in Sources */,
|
||||||
82D6FAE32CD99F7900C925F4 /* FollowedNotify.swift in Sources */,
|
82D6FAE32CD99F7900C925F4 /* FollowedNotify.swift in Sources */,
|
||||||
82D6FAE42CD99F7900C925F4 /* FollowNotify.swift in Sources */,
|
82D6FAE42CD99F7900C925F4 /* FollowNotify.swift in Sources */,
|
||||||
82D6FAE52CD99F7900C925F4 /* LikedNotify.swift in Sources */,
|
82D6FAE52CD99F7900C925F4 /* LikedNotify.swift in Sources */,
|
||||||
@@ -5014,6 +5137,7 @@
|
|||||||
82D6FAFE2CD99F7900C925F4 /* Pubkey.swift in Sources */,
|
82D6FAFE2CD99F7900C925F4 /* Pubkey.swift in Sources */,
|
||||||
82D6FAFF2CD99F7900C925F4 /* NoteId.swift in Sources */,
|
82D6FAFF2CD99F7900C925F4 /* NoteId.swift in Sources */,
|
||||||
82D6FB002CD99F7900C925F4 /* Referenced.swift in Sources */,
|
82D6FB002CD99F7900C925F4 /* Referenced.swift in Sources */,
|
||||||
|
5CB0172D2D42C76A00A9ED05 /* BalanceView.swift in Sources */,
|
||||||
82D6FB012CD99F7900C925F4 /* Block.swift in Sources */,
|
82D6FB012CD99F7900C925F4 /* Block.swift in Sources */,
|
||||||
82D6FB022CD99F7900C925F4 /* MigratedTypes.swift in Sources */,
|
82D6FB022CD99F7900C925F4 /* MigratedTypes.swift in Sources */,
|
||||||
82D6FB032CD99F7900C925F4 /* DamusDuration.swift in Sources */,
|
82D6FB032CD99F7900C925F4 /* DamusDuration.swift in Sources */,
|
||||||
@@ -5021,6 +5145,7 @@
|
|||||||
82D6FB052CD99F7900C925F4 /* MusicController.swift in Sources */,
|
82D6FB052CD99F7900C925F4 /* MusicController.swift in Sources */,
|
||||||
82D6FB062CD99F7900C925F4 /* UserStatusView.swift in Sources */,
|
82D6FB062CD99F7900C925F4 /* UserStatusView.swift in Sources */,
|
||||||
82D6FB072CD99F7900C925F4 /* UserStatus.swift in Sources */,
|
82D6FB072CD99F7900C925F4 /* UserStatus.swift in Sources */,
|
||||||
|
5CB017262D42C5C400A9ED05 /* TransactionsView.swift in Sources */,
|
||||||
82D6FB082CD99F7900C925F4 /* UserStatusSheet.swift in Sources */,
|
82D6FB082CD99F7900C925F4 /* UserStatusSheet.swift in Sources */,
|
||||||
82D6FB092CD99F7900C925F4 /* SearchHeaderView.swift in Sources */,
|
82D6FB092CD99F7900C925F4 /* SearchHeaderView.swift in Sources */,
|
||||||
82D6FB0A2CD99F7900C925F4 /* DamusGradient.swift in Sources */,
|
82D6FB0A2CD99F7900C925F4 /* DamusGradient.swift in Sources */,
|
||||||
@@ -5049,6 +5174,7 @@
|
|||||||
82D6FB212CD99F7900C925F4 /* SelectableText.swift in Sources */,
|
82D6FB212CD99F7900C925F4 /* SelectableText.swift in Sources */,
|
||||||
82D6FB222CD99F7900C925F4 /* DamusColors.swift in Sources */,
|
82D6FB222CD99F7900C925F4 /* DamusColors.swift in Sources */,
|
||||||
82D6FB232CD99F7900C925F4 /* ThiccDivider.swift in Sources */,
|
82D6FB232CD99F7900C925F4 /* ThiccDivider.swift in Sources */,
|
||||||
|
D733F9E22D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
|
||||||
82D6FB242CD99F7900C925F4 /* IconLabel.swift in Sources */,
|
82D6FB242CD99F7900C925F4 /* IconLabel.swift in Sources */,
|
||||||
82D6FB252CD99F7900C925F4 /* TruncatedText.swift in Sources */,
|
82D6FB252CD99F7900C925F4 /* TruncatedText.swift in Sources */,
|
||||||
82D6FB262CD99F7900C925F4 /* SupporterBadge.swift in Sources */,
|
82D6FB262CD99F7900C925F4 /* SupporterBadge.swift in Sources */,
|
||||||
@@ -5063,9 +5189,11 @@
|
|||||||
82D6FB2F2CD99F7900C925F4 /* BlurHashDecode.swift in Sources */,
|
82D6FB2F2CD99F7900C925F4 /* BlurHashDecode.swift in Sources */,
|
||||||
82D6FB302CD99F7900C925F4 /* PostBox.swift in Sources */,
|
82D6FB302CD99F7900C925F4 /* PostBox.swift in Sources */,
|
||||||
82D6FB312CD99F7900C925F4 /* KFOptionSetter+.swift in Sources */,
|
82D6FB312CD99F7900C925F4 /* KFOptionSetter+.swift in Sources */,
|
||||||
|
D73BDB162D71216500D69970 /* UserRelayListManager.swift in Sources */,
|
||||||
82D6FB322CD99F7900C925F4 /* FillAndStroke.swift in Sources */,
|
82D6FB322CD99F7900C925F4 /* FillAndStroke.swift in Sources */,
|
||||||
82D6FB332CD99F7900C925F4 /* Array.swift in Sources */,
|
82D6FB332CD99F7900C925F4 /* Array.swift in Sources */,
|
||||||
82D6FB342CD99F7900C925F4 /* VectorMath.swift in Sources */,
|
82D6FB342CD99F7900C925F4 /* VectorMath.swift in Sources */,
|
||||||
|
5C8498022D5D150000F74FEB /* ZapExplainer.swift in Sources */,
|
||||||
82D6FB352CD99F7900C925F4 /* OffsetExtension.swift in Sources */,
|
82D6FB352CD99F7900C925F4 /* OffsetExtension.swift in Sources */,
|
||||||
82D6FB362CD99F7900C925F4 /* RelayFilters.swift in Sources */,
|
82D6FB362CD99F7900C925F4 /* RelayFilters.swift in Sources */,
|
||||||
82D6FB372CD99F7900C925F4 /* RelayModelCache.swift in Sources */,
|
82D6FB372CD99F7900C925F4 /* RelayModelCache.swift in Sources */,
|
||||||
@@ -5111,6 +5239,7 @@
|
|||||||
82D6FB5E2CD99F7900C925F4 /* CredentialHandler.swift in Sources */,
|
82D6FB5E2CD99F7900C925F4 /* CredentialHandler.swift in Sources */,
|
||||||
82D6FB5F2CD99F7900C925F4 /* KeyboardVisible.swift in Sources */,
|
82D6FB5F2CD99F7900C925F4 /* KeyboardVisible.swift in Sources */,
|
||||||
82D6FB602CD99F7900C925F4 /* StringUtil.swift in Sources */,
|
82D6FB602CD99F7900C925F4 /* StringUtil.swift in Sources */,
|
||||||
|
D78F08172D7F7F7500FC6C75 /* NIP04.swift in Sources */,
|
||||||
82D6FB612CD99F7900C925F4 /* Router.swift in Sources */,
|
82D6FB612CD99F7900C925F4 /* Router.swift in Sources */,
|
||||||
82D6FB622CD99F7900C925F4 /* Log.swift in Sources */,
|
82D6FB622CD99F7900C925F4 /* Log.swift in Sources */,
|
||||||
82D6FB632CD99F7900C925F4 /* AVPlayer+Additions.swift in Sources */,
|
82D6FB632CD99F7900C925F4 /* AVPlayer+Additions.swift in Sources */,
|
||||||
@@ -5125,7 +5254,6 @@
|
|||||||
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */,
|
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */,
|
||||||
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */,
|
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */,
|
||||||
82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */,
|
82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */,
|
||||||
82D6FB6F2CD99F7900C925F4 /* CameraService+Extensions.swift in Sources */,
|
|
||||||
82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */,
|
82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */,
|
||||||
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */,
|
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */,
|
||||||
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */,
|
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */,
|
||||||
@@ -5205,6 +5333,7 @@
|
|||||||
82D6FBBA2CD99F7900C925F4 /* NostrRequest.swift in Sources */,
|
82D6FBBA2CD99F7900C925F4 /* NostrRequest.swift in Sources */,
|
||||||
82D6FBBB2CD99F7900C925F4 /* Profiles.swift in Sources */,
|
82D6FBBB2CD99F7900C925F4 /* Profiles.swift in Sources */,
|
||||||
82D6FBBC2CD99F7900C925F4 /* NostrKind.swift in Sources */,
|
82D6FBBC2CD99F7900C925F4 /* NostrKind.swift in Sources */,
|
||||||
|
D733F9E62D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
||||||
82D6FBBD2CD99F7900C925F4 /* NostrLink.swift in Sources */,
|
82D6FBBD2CD99F7900C925F4 /* NostrLink.swift in Sources */,
|
||||||
82D6FBBE2CD99F7900C925F4 /* WebSocket.swift in Sources */,
|
82D6FBBE2CD99F7900C925F4 /* WebSocket.swift in Sources */,
|
||||||
82D6FBBF2CD99F7900C925F4 /* ReferencedId.swift in Sources */,
|
82D6FBBF2CD99F7900C925F4 /* ReferencedId.swift in Sources */,
|
||||||
@@ -5238,6 +5367,7 @@
|
|||||||
82D6FBE02CD99F7900C925F4 /* ReactionsSettingsView.swift in Sources */,
|
82D6FBE02CD99F7900C925F4 /* ReactionsSettingsView.swift in Sources */,
|
||||||
82D6FBE12CD99F7900C925F4 /* NotificationSettingsView.swift in Sources */,
|
82D6FBE12CD99F7900C925F4 /* NotificationSettingsView.swift in Sources */,
|
||||||
82D6FBE22CD99F7900C925F4 /* AppearanceSettingsView.swift in Sources */,
|
82D6FBE22CD99F7900C925F4 /* AppearanceSettingsView.swift in Sources */,
|
||||||
|
D7DB930A2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||||
82D6FBE32CD99F7900C925F4 /* KeySettingsView.swift in Sources */,
|
82D6FBE32CD99F7900C925F4 /* KeySettingsView.swift in Sources */,
|
||||||
82D6FBE42CD99F7900C925F4 /* ZapSettingsView.swift in Sources */,
|
82D6FBE42CD99F7900C925F4 /* ZapSettingsView.swift in Sources */,
|
||||||
82D6FBE52CD99F7900C925F4 /* TranslationSettingsView.swift in Sources */,
|
82D6FBE52CD99F7900C925F4 /* TranslationSettingsView.swift in Sources */,
|
||||||
@@ -5292,6 +5422,7 @@
|
|||||||
82D6FC132CD99F7900C925F4 /* FriendIcon.swift in Sources */,
|
82D6FC132CD99F7900C925F4 /* FriendIcon.swift in Sources */,
|
||||||
82D6FC142CD99F7900C925F4 /* CondensedProfilePicturesView.swift in Sources */,
|
82D6FC142CD99F7900C925F4 /* CondensedProfilePicturesView.swift in Sources */,
|
||||||
82D6FC152CD99F7900C925F4 /* ProfileEditButton.swift in Sources */,
|
82D6FC152CD99F7900C925F4 /* ProfileEditButton.swift in Sources */,
|
||||||
|
D73BDB102D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
|
||||||
82D6FC162CD99F7900C925F4 /* RelayPaidDetail.swift in Sources */,
|
82D6FC162CD99F7900C925F4 /* RelayPaidDetail.swift in Sources */,
|
||||||
82D6FC172CD99F7900C925F4 /* RelayAuthenticationDetail.swift in Sources */,
|
82D6FC172CD99F7900C925F4 /* RelayAuthenticationDetail.swift in Sources */,
|
||||||
82D6FC182CD99F7900C925F4 /* RelaySoftwareDetail.swift in Sources */,
|
82D6FC182CD99F7900C925F4 /* RelaySoftwareDetail.swift in Sources */,
|
||||||
@@ -5340,10 +5471,12 @@
|
|||||||
82D6FC432CD99F7900C925F4 /* ReactionView.swift in Sources */,
|
82D6FC432CD99F7900C925F4 /* ReactionView.swift in Sources */,
|
||||||
82D6FC442CD99F7900C925F4 /* EventActionBar.swift in Sources */,
|
82D6FC442CD99F7900C925F4 /* EventActionBar.swift in Sources */,
|
||||||
82D6FC452CD99F7900C925F4 /* EventDetailBar.swift in Sources */,
|
82D6FC452CD99F7900C925F4 /* EventDetailBar.swift in Sources */,
|
||||||
|
D78F080C2D7F78EF00FC6C75 /* Request.swift in Sources */,
|
||||||
82D6FC462CD99F7900C925F4 /* ShareAction.swift in Sources */,
|
82D6FC462CD99F7900C925F4 /* ShareAction.swift in Sources */,
|
||||||
82D6FC472CD99F7900C925F4 /* RepostAction.swift in Sources */,
|
82D6FC472CD99F7900C925F4 /* RepostAction.swift in Sources */,
|
||||||
82D6FC482CD99F7900C925F4 /* ShareActionButton.swift in Sources */,
|
82D6FC482CD99F7900C925F4 /* ShareActionButton.swift in Sources */,
|
||||||
82D6FC492CD99F7900C925F4 /* BigButton.swift in Sources */,
|
82D6FC492CD99F7900C925F4 /* BigButton.swift in Sources */,
|
||||||
|
D73BDB182D71311900D69970 /* UserRelayListErrors.swift in Sources */,
|
||||||
82D6FC4A2CD99F7900C925F4 /* AddRelayView.swift in Sources */,
|
82D6FC4A2CD99F7900C925F4 /* AddRelayView.swift in Sources */,
|
||||||
82D6FC4B2CD99F7900C925F4 /* BlocksView.swift in Sources */,
|
82D6FC4B2CD99F7900C925F4 /* BlocksView.swift in Sources */,
|
||||||
D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */,
|
D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */,
|
||||||
@@ -5365,6 +5498,7 @@
|
|||||||
82D6FC5A2CD99F7900C925F4 /* QRScanNSECView.swift in Sources */,
|
82D6FC5A2CD99F7900C925F4 /* QRScanNSECView.swift in Sources */,
|
||||||
82D6FC5B2CD99F7900C925F4 /* NoteContentView.swift in Sources */,
|
82D6FC5B2CD99F7900C925F4 /* NoteContentView.swift in Sources */,
|
||||||
82D6FC5C2CD99F7900C925F4 /* PostButton.swift in Sources */,
|
82D6FC5C2CD99F7900C925F4 /* PostButton.swift in Sources */,
|
||||||
|
5CB017322D4422DB00A9ED05 /* NWCSettings.swift in Sources */,
|
||||||
82D6FC5D2CD99F7900C925F4 /* PostView.swift in Sources */,
|
82D6FC5D2CD99F7900C925F4 /* PostView.swift in Sources */,
|
||||||
82D6FC5E2CD99F7900C925F4 /* AttachMediaUtility.swift in Sources */,
|
82D6FC5E2CD99F7900C925F4 /* AttachMediaUtility.swift in Sources */,
|
||||||
82D6FC5F2CD99F7900C925F4 /* MediaPicker.swift in Sources */,
|
82D6FC5F2CD99F7900C925F4 /* MediaPicker.swift in Sources */,
|
||||||
@@ -5514,8 +5648,9 @@
|
|||||||
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */,
|
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */,
|
||||||
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */,
|
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */,
|
||||||
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */,
|
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */,
|
||||||
D73E5E8D2C6A97F4007EB227 /* CameraService+Extensions.swift in Sources */,
|
D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
||||||
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */,
|
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */,
|
||||||
|
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */,
|
||||||
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */,
|
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */,
|
||||||
D773BC602C6D538500349F0A /* CommentItem.swift in Sources */,
|
D773BC602C6D538500349F0A /* CommentItem.swift in Sources */,
|
||||||
D73E5E902C6A97F4007EB227 /* VideoCaptureProcessor.swift in Sources */,
|
D73E5E902C6A97F4007EB227 /* VideoCaptureProcessor.swift in Sources */,
|
||||||
@@ -5530,6 +5665,7 @@
|
|||||||
D73E5E992C6A97F4007EB227 /* Liked.swift in Sources */,
|
D73E5E992C6A97F4007EB227 /* Liked.swift in Sources */,
|
||||||
D73E5E9A2C6A97F4007EB227 /* ProfileUpdate.swift in Sources */,
|
D73E5E9A2C6A97F4007EB227 /* ProfileUpdate.swift in Sources */,
|
||||||
D73E5E9B2C6A97F4007EB227 /* PostBlock.swift in Sources */,
|
D73E5E9B2C6A97F4007EB227 /* PostBlock.swift in Sources */,
|
||||||
|
5CB017332D4422DB00A9ED05 /* NWCSettings.swift in Sources */,
|
||||||
D73E5E9C2C6A97F4007EB227 /* Reply.swift in Sources */,
|
D73E5E9C2C6A97F4007EB227 /* Reply.swift in Sources */,
|
||||||
D73E5E9D2C6A97F4007EB227 /* SearchModel.swift in Sources */,
|
D73E5E9D2C6A97F4007EB227 /* SearchModel.swift in Sources */,
|
||||||
D73E5E9E2C6A97F4007EB227 /* NostrFilter+Hashable.swift in Sources */,
|
D73E5E9E2C6A97F4007EB227 /* NostrFilter+Hashable.swift in Sources */,
|
||||||
@@ -5537,6 +5673,7 @@
|
|||||||
D73E5F912C6AA71B007EB227 /* InputDismissKeyboard.swift in Sources */,
|
D73E5F912C6AA71B007EB227 /* InputDismissKeyboard.swift in Sources */,
|
||||||
D73E5E9F2C6A97F4007EB227 /* CreateAccountModel.swift in Sources */,
|
D73E5E9F2C6A97F4007EB227 /* CreateAccountModel.swift in Sources */,
|
||||||
D73E5EA12C6A97F4007EB227 /* SignalModel.swift in Sources */,
|
D73E5EA12C6A97F4007EB227 /* SignalModel.swift in Sources */,
|
||||||
|
5CB017272D42C5C400A9ED05 /* TransactionsView.swift in Sources */,
|
||||||
D73E5EA22C6A97F4007EB227 /* FollowTarget.swift in Sources */,
|
D73E5EA22C6A97F4007EB227 /* FollowTarget.swift in Sources */,
|
||||||
D73E5EA32C6A97F4007EB227 /* BookmarksManager.swift in Sources */,
|
D73E5EA32C6A97F4007EB227 /* BookmarksManager.swift in Sources */,
|
||||||
D73E5EA42C6A97F4007EB227 /* EventsModel.swift in Sources */,
|
D73E5EA42C6A97F4007EB227 /* EventsModel.swift in Sources */,
|
||||||
@@ -5544,6 +5681,7 @@
|
|||||||
D73E5EA62C6A97F4007EB227 /* FollowersModel.swift in Sources */,
|
D73E5EA62C6A97F4007EB227 /* FollowersModel.swift in Sources */,
|
||||||
D73E5EA72C6A97F4007EB227 /* SearchHomeModel.swift in Sources */,
|
D73E5EA72C6A97F4007EB227 /* SearchHomeModel.swift in Sources */,
|
||||||
D73E5EA82C6A97F4007EB227 /* DirectMessageModel.swift in Sources */,
|
D73E5EA82C6A97F4007EB227 /* DirectMessageModel.swift in Sources */,
|
||||||
|
D78F08132D7F78F900FC6C75 /* Response.swift in Sources */,
|
||||||
D73E5EA92C6A97F4007EB227 /* Report.swift in Sources */,
|
D73E5EA92C6A97F4007EB227 /* Report.swift in Sources */,
|
||||||
D73E5EAA2C6A97F4007EB227 /* ZapsModel.swift in Sources */,
|
D73E5EAA2C6A97F4007EB227 /* ZapsModel.swift in Sources */,
|
||||||
D73E5EAB2C6A97F4007EB227 /* DraftsModel.swift in Sources */,
|
D73E5EAB2C6A97F4007EB227 /* DraftsModel.swift in Sources */,
|
||||||
@@ -5619,10 +5757,12 @@
|
|||||||
D73E5EF22C6A97F4007EB227 /* DamusPurpleURLSheetView.swift in Sources */,
|
D73E5EF22C6A97F4007EB227 /* DamusPurpleURLSheetView.swift in Sources */,
|
||||||
D73E5EF32C6A97F4007EB227 /* DamusPurpleVerifyNpubView.swift in Sources */,
|
D73E5EF32C6A97F4007EB227 /* DamusPurpleVerifyNpubView.swift in Sources */,
|
||||||
D73E5EF42C6A97F4007EB227 /* DamusPurpleAccountView.swift in Sources */,
|
D73E5EF42C6A97F4007EB227 /* DamusPurpleAccountView.swift in Sources */,
|
||||||
|
5CB0172E2D42C76A00A9ED05 /* BalanceView.swift in Sources */,
|
||||||
D73E5EF52C6A97F4007EB227 /* DamusPurpleNewUserOnboardingView.swift in Sources */,
|
D73E5EF52C6A97F4007EB227 /* DamusPurpleNewUserOnboardingView.swift in Sources */,
|
||||||
D73E5EF62C6A97F4007EB227 /* SearchingEventView.swift in Sources */,
|
D73E5EF62C6A97F4007EB227 /* SearchingEventView.swift in Sources */,
|
||||||
D73E5EF72C6A97F4007EB227 /* PullDownSearch.swift in Sources */,
|
D73E5EF72C6A97F4007EB227 /* PullDownSearch.swift in Sources */,
|
||||||
D73E5EF82C6A97F4007EB227 /* NotificationsView.swift in Sources */,
|
D73E5EF82C6A97F4007EB227 /* NotificationsView.swift in Sources */,
|
||||||
|
D73B74E32D8365BA0067BDBC /* ExtraFonts.swift in Sources */,
|
||||||
D73E5EF92C6A97F4007EB227 /* EventGroupView.swift in Sources */,
|
D73E5EF92C6A97F4007EB227 /* EventGroupView.swift in Sources */,
|
||||||
D73E5EFA2C6A97F4007EB227 /* NotificationItemView.swift in Sources */,
|
D73E5EFA2C6A97F4007EB227 /* NotificationItemView.swift in Sources */,
|
||||||
D73E5EFB2C6A97F4007EB227 /* ProfilePicturesView.swift in Sources */,
|
D73E5EFB2C6A97F4007EB227 /* ProfilePicturesView.swift in Sources */,
|
||||||
@@ -5635,6 +5775,7 @@
|
|||||||
D73E5F012C6A97F4007EB227 /* ZapTypePicker.swift in Sources */,
|
D73E5F012C6A97F4007EB227 /* ZapTypePicker.swift in Sources */,
|
||||||
D73E5F022C6A97F4007EB227 /* ZapUserView.swift in Sources */,
|
D73E5F022C6A97F4007EB227 /* ZapUserView.swift in Sources */,
|
||||||
D73E5F032C6A97F4007EB227 /* ProfileZapLinkView.swift in Sources */,
|
D73E5F032C6A97F4007EB227 /* ProfileZapLinkView.swift in Sources */,
|
||||||
|
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||||
D73E5F042C6A97F4007EB227 /* AboutView.swift in Sources */,
|
D73E5F042C6A97F4007EB227 /* AboutView.swift in Sources */,
|
||||||
D73E5F052C6A97F4007EB227 /* ProfileName.swift in Sources */,
|
D73E5F052C6A97F4007EB227 /* ProfileName.swift in Sources */,
|
||||||
D73E5F062C6A97F4007EB227 /* ProfilePictureSelector.swift in Sources */,
|
D73E5F062C6A97F4007EB227 /* ProfilePictureSelector.swift in Sources */,
|
||||||
@@ -5675,8 +5816,10 @@
|
|||||||
D73E5F272C6A97F4007EB227 /* TimeDot.swift in Sources */,
|
D73E5F272C6A97F4007EB227 /* TimeDot.swift in Sources */,
|
||||||
D73E5F282C6A97F4007EB227 /* EventTop.swift in Sources */,
|
D73E5F282C6A97F4007EB227 /* EventTop.swift in Sources */,
|
||||||
D73E5F292C6A97F4007EB227 /* ReplyDescription.swift in Sources */,
|
D73E5F292C6A97F4007EB227 /* ReplyDescription.swift in Sources */,
|
||||||
|
D73BDB0D2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
|
||||||
D73E5F2A2C6A97F4007EB227 /* RelativeTime.swift in Sources */,
|
D73E5F2A2C6A97F4007EB227 /* RelativeTime.swift in Sources */,
|
||||||
D73E5F732C6A9885007EB227 /* TestData.swift in Sources */,
|
D73E5F732C6A9885007EB227 /* TestData.swift in Sources */,
|
||||||
|
D78F08192D7F7F7500FC6C75 /* NIP04.swift in Sources */,
|
||||||
D73E5F2B2C6A97F4007EB227 /* ReplyPart.swift in Sources */,
|
D73E5F2B2C6A97F4007EB227 /* ReplyPart.swift in Sources */,
|
||||||
D73E5F2C2C6A97F4007EB227 /* ProxyView.swift in Sources */,
|
D73E5F2C2C6A97F4007EB227 /* ProxyView.swift in Sources */,
|
||||||
D73E5F2D2C6A97F4007EB227 /* SelectedEventView.swift in Sources */,
|
D73E5F2D2C6A97F4007EB227 /* SelectedEventView.swift in Sources */,
|
||||||
@@ -5712,6 +5855,7 @@
|
|||||||
D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */,
|
D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */,
|
||||||
D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */,
|
D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */,
|
||||||
D73E5F492C6A97F5007EB227 /* ConfigView.swift in Sources */,
|
D73E5F492C6A97F5007EB227 /* ConfigView.swift in Sources */,
|
||||||
|
D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
|
||||||
D73E5F4A2C6A97F5007EB227 /* CreateAccountView.swift in Sources */,
|
D73E5F4A2C6A97F5007EB227 /* CreateAccountView.swift in Sources */,
|
||||||
D73E5F7A2C6A9C55007EB227 /* NotificationFormatter.swift in Sources */,
|
D73E5F7A2C6A9C55007EB227 /* NotificationFormatter.swift in Sources */,
|
||||||
D73E5F4B2C6A97F5007EB227 /* DirectMessagesView.swift in Sources */,
|
D73E5F4B2C6A97F5007EB227 /* DirectMessagesView.swift in Sources */,
|
||||||
@@ -5747,6 +5891,7 @@
|
|||||||
D73E5F6A2C6A97F5007EB227 /* ReportView.swift in Sources */,
|
D73E5F6A2C6A97F5007EB227 /* ReportView.swift in Sources */,
|
||||||
D73E5F6C2C6A97F5007EB227 /* RepostsView.swift in Sources */,
|
D73E5F6C2C6A97F5007EB227 /* RepostsView.swift in Sources */,
|
||||||
D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
|
D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
|
||||||
|
D73BDB142D71216500D69970 /* UserRelayListManager.swift in Sources */,
|
||||||
D73E5F6D2C6A97F5007EB227 /* Launch.storyboard in Sources */,
|
D73E5F6D2C6A97F5007EB227 /* Launch.storyboard in Sources */,
|
||||||
D73E5F6F2C6A97F5007EB227 /* RelayFilterView.swift in Sources */,
|
D73E5F6F2C6A97F5007EB227 /* RelayFilterView.swift in Sources */,
|
||||||
D703D78A2C670C8A00A400EA /* LibreTranslateServer.swift in Sources */,
|
D703D78A2C670C8A00A400EA /* LibreTranslateServer.swift in Sources */,
|
||||||
@@ -5766,6 +5911,7 @@
|
|||||||
D703D7A32C670E1D00A400EA /* nostr_bech32.c in Sources */,
|
D703D7A32C670E1D00A400EA /* nostr_bech32.c in Sources */,
|
||||||
D703D7992C670DF900A400EA /* sha256.c in Sources */,
|
D703D7992C670DF900A400EA /* sha256.c in Sources */,
|
||||||
D703D7972C670DED00A400EA /* wasm.c in Sources */,
|
D703D7972C670DED00A400EA /* wasm.c in Sources */,
|
||||||
|
5C8498042D5D150000F74FEB /* ZapExplainer.swift in Sources */,
|
||||||
D703D7842C670C4700A400EA /* SequenceUtils.swift in Sources */,
|
D703D7842C670C4700A400EA /* SequenceUtils.swift in Sources */,
|
||||||
D703D7912C670D1E00A400EA /* DisplayName.swift in Sources */,
|
D703D7912C670D1E00A400EA /* DisplayName.swift in Sources */,
|
||||||
D703D7B02C6710A500A400EA /* Root.swift in Sources */,
|
D703D7B02C6710A500A400EA /* Root.swift in Sources */,
|
||||||
@@ -5797,6 +5943,7 @@
|
|||||||
D703D7A52C670E3E00A400EA /* mdb.c in Sources */,
|
D703D7A52C670E3E00A400EA /* mdb.c in Sources */,
|
||||||
D703D76B2C670B3100A400EA /* Referenced.swift in Sources */,
|
D703D76B2C670B3100A400EA /* Referenced.swift in Sources */,
|
||||||
D703D7952C670DE600A400EA /* hash_u5.c in Sources */,
|
D703D7952C670DE600A400EA /* hash_u5.c in Sources */,
|
||||||
|
D73BDB192D71311900D69970 /* UserRelayListErrors.swift in Sources */,
|
||||||
D703D7582C670A6000A400EA /* Id.swift in Sources */,
|
D703D7582C670A6000A400EA /* Id.swift in Sources */,
|
||||||
5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */,
|
5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */,
|
||||||
D703D76E2C670B4900A400EA /* NdbTagsIterator.swift in Sources */,
|
D703D76E2C670B4900A400EA /* NdbTagsIterator.swift in Sources */,
|
||||||
@@ -5891,6 +6038,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */,
|
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */,
|
||||||
|
D733F9E52D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
||||||
D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */,
|
D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */,
|
||||||
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */,
|
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */,
|
||||||
D71AD9002CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
|
D71AD9002CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
|
||||||
@@ -5978,6 +6126,7 @@
|
|||||||
D798D2232B0859B700234419 /* KeychainStorage.swift in Sources */,
|
D798D2232B0859B700234419 /* KeychainStorage.swift in Sources */,
|
||||||
D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
|
D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
|
||||||
D7CE1B272B0BE224002EDAD4 /* bech32_util.c in Sources */,
|
D7CE1B272B0BE224002EDAD4 /* bech32_util.c in Sources */,
|
||||||
|
D78F08122D7F78F900FC6C75 /* Response.swift in Sources */,
|
||||||
D7CCFC102B05880F00323D86 /* Id.swift in Sources */,
|
D7CCFC102B05880F00323D86 /* Id.swift in Sources */,
|
||||||
D7CB5D532B1174E900AD4105 /* DeepLPlan.swift in Sources */,
|
D7CB5D532B1174E900AD4105 /* DeepLPlan.swift in Sources */,
|
||||||
D7EDED282B1180940018B19C /* ImageUploadModel.swift in Sources */,
|
D7EDED282B1180940018B19C /* ImageUploadModel.swift in Sources */,
|
||||||
@@ -5987,9 +6136,11 @@
|
|||||||
D7CE1B332B0BE6DE002EDAD4 /* Nostr.swift in Sources */,
|
D7CE1B332B0BE6DE002EDAD4 /* Nostr.swift in Sources */,
|
||||||
D7CE1B3D2B0BE719002EDAD4 /* Verifiable.swift in Sources */,
|
D7CE1B3D2B0BE719002EDAD4 /* Verifiable.swift in Sources */,
|
||||||
D7CE1B382B0BE719002EDAD4 /* VeriferOptions.swift in Sources */,
|
D7CE1B382B0BE719002EDAD4 /* VeriferOptions.swift in Sources */,
|
||||||
|
D78F080F2D7F78EF00FC6C75 /* Request.swift in Sources */,
|
||||||
D7CCFC152B05891000323D86 /* Referenced.swift in Sources */,
|
D7CCFC152B05891000323D86 /* Referenced.swift in Sources */,
|
||||||
D7CE1B2B2B0BE243002EDAD4 /* hex.c in Sources */,
|
D7CE1B2B2B0BE243002EDAD4 /* hex.c in Sources */,
|
||||||
D798D2222B08598A00234419 /* ReferencedId.swift in Sources */,
|
D798D2222B08598A00234419 /* ReferencedId.swift in Sources */,
|
||||||
|
D78F081A2D7F803100FC6C75 /* NIP04.swift in Sources */,
|
||||||
D7B76C912C82507F003A16CB /* NIP98AuthenticatedRequest.swift in Sources */,
|
D7B76C912C82507F003A16CB /* NIP98AuthenticatedRequest.swift in Sources */,
|
||||||
D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */,
|
D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */,
|
||||||
D7CE1B192B0BE132002EDAD4 /* builder.c in Sources */,
|
D7CE1B192B0BE132002EDAD4 /* builder.c in Sources */,
|
||||||
@@ -6221,7 +6372,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 5;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
@@ -6244,7 +6395,7 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||||
MARKETING_VERSION = 1.10;
|
MARKETING_VERSION = 1.14;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@@ -6290,7 +6441,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 5;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@@ -6309,7 +6460,7 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||||
MARKETING_VERSION = 1.10;
|
MARKETING_VERSION = 1.14;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -6328,8 +6479,8 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -6356,9 +6507,9 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)",
|
"$(PROJECT_DIR)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.13;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||||
@@ -6380,8 +6531,8 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -6408,9 +6559,9 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)",
|
"$(PROJECT_DIR)",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.13;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||||
@@ -6496,7 +6647,6 @@
|
|||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CODE_SIGN_ENTITLEMENTS = "share extension/share extension.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "share extension/share extension.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -6515,7 +6665,6 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.13;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -6534,7 +6683,6 @@
|
|||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CODE_SIGN_ENTITLEMENTS = "share extension/share extension.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "share extension/share extension.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -6549,7 +6697,6 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.13;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -6568,7 +6715,6 @@
|
|||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CODE_SIGN_ENTITLEMENTS = "highlighter action extension/highlighter action extension.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "highlighter action extension/highlighter action extension.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -6583,7 +6729,6 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.13;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -6603,7 +6748,6 @@
|
|||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CODE_SIGN_ENTITLEMENTS = "highlighter action extension/highlighter action extension.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "highlighter action extension/highlighter action extension.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -6618,7 +6762,6 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.13;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -6637,7 +6780,6 @@
|
|||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
|
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@@ -6652,7 +6794,6 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.13;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -6672,7 +6813,6 @@
|
|||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
|
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@@ -6687,7 +6827,6 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 1.13;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -6773,7 +6912,7 @@
|
|||||||
repositoryURL = "https://github.com/tyiu/EmojiPicker.git";
|
repositoryURL = "https://github.com/tyiu/EmojiPicker.git";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 0.1.1;
|
minimumVersion = 0.2.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||||
@@ -6781,7 +6920,7 @@
|
|||||||
repositoryURL = "https://github.com/onevcat/Kingfisher";
|
repositoryURL = "https://github.com/onevcat/Kingfisher";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 7.0.0;
|
minimumVersion = 8.3.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
|
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
|
||||||
@@ -6866,6 +7005,11 @@
|
|||||||
package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
|
package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
|
||||||
productName = MarkdownUI;
|
productName = MarkdownUI;
|
||||||
};
|
};
|
||||||
|
4C5726B92D72C6FA00E7FF82 /* Kingfisher */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||||
|
productName = Kingfisher;
|
||||||
|
};
|
||||||
4C649880286E0EE300EAE2B3 /* secp256k1 */ = {
|
4C649880286E0EE300EAE2B3 /* secp256k1 */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "085cf0f645323bf77edb52886489bf77b309a0a2d2b78a54beaf8520b540d596",
|
"originHash" : "06318d35ee2e6bd681b95591e67da33a9461b48a3c652e58bd9d1a6f0d82bdac",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "codescanner",
|
"identity" : "codescanner",
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/tyiu/EmojiKit",
|
"location" : "https://github.com/tyiu/EmojiKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
|
"revision" : "47a4b1402de26be0299dcb4d667c1faaf21a7874",
|
||||||
"version" : "0.1.2"
|
"version" : "0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/tyiu/EmojiPicker.git",
|
"location" : "https://github.com/tyiu/EmojiPicker.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
|
"revision" : "3f48903721eae223238ff0af17c22d6373d33813",
|
||||||
"version" : "0.1.1"
|
"version" : "0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/onevcat/Kingfisher",
|
"location" : "https://github.com/onevcat/Kingfisher",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
|
"revision" : "4c6b067f96953ee19526e49e4189403a2be21fb3",
|
||||||
"version" : "7.6.1"
|
"version" : "8.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 216 B After Width: | Height: | Size: 216 B |
@@ -84,7 +84,7 @@ struct NoteZapButton: View {
|
|||||||
print("cancel_zap: we already have a real zap, can't cancel")
|
print("cancel_zap: we already have a real zap, can't cancel")
|
||||||
break
|
break
|
||||||
case .pending(let pzap):
|
case .pending(let pzap):
|
||||||
guard let res = cancel_zap(zap: pzap, box: damus_state.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
|
guard let res = cancel_zap(zap: pzap, box: damus_state.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
|
||||||
|
|
||||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||||
return
|
return
|
||||||
@@ -179,7 +179,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only take the first 10 because reasons
|
// Only take the first 10 because reasons
|
||||||
let relays = Array(damus_state.pool.our_descriptors.prefix(10))
|
let relays = Array(damus_state.nostrNetwork.pool.our_descriptors.prefix(10))
|
||||||
let content = comment ?? ""
|
let content = comment ?? ""
|
||||||
|
|
||||||
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
|
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
|
||||||
@@ -232,7 +232,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
|||||||
flusher = .once({ pe in
|
flusher = .once({ pe in
|
||||||
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
|
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
|
await WalletConnect.send_donation_zap(pool: damus_state.nostrNetwork.pool, postbox: damus_state.nostrNetwork.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
|||||||
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
|
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
|
||||||
let delay = damus_state.settings.nozaps ? nil : 5.0
|
let delay = damus_state.settings.nozaps ? nil : 5.0
|
||||||
|
|
||||||
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
|
let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, invoice: inv, delay: delay, on_flush: flusher)
|
||||||
|
|
||||||
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
|
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
|
||||||
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
|
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
|
||||||
|
|||||||
@@ -10,36 +10,42 @@ import SwiftUI
|
|||||||
struct Reposted: View {
|
struct Reposted: View {
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
let pubkey: Pubkey
|
let pubkey: Pubkey
|
||||||
let target: NoteId
|
let target: NostrEvent
|
||||||
@State var reposts: Int
|
@State var reposts: Int
|
||||||
|
|
||||||
init(damus: DamusState, pubkey: Pubkey, target: NoteId) {
|
init(damus: DamusState, pubkey: Pubkey, target: NostrEvent) {
|
||||||
self.damus = damus
|
self.damus = damus
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.target = target
|
self.target = target
|
||||||
self.reposts = damus.boosts.counts[target] ?? 1
|
self.reposts = damus.boosts.counts[target.id] ?? 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
Image("repost")
|
Image("repost")
|
||||||
.foregroundColor(Color.gray)
|
.foregroundColor(Color.gray)
|
||||||
ProfileName(pubkey: pubkey, damus: damus, show_nip5_domain: false)
|
|
||||||
.foregroundColor(Color.gray)
|
// Show profile picture of the reposter only if the reposter is not the author of the reposted note.
|
||||||
NavigationLink(value: Route.Reposts(reposts: .reposts(state: damus, target: target))) {
|
if pubkey != target.pubkey {
|
||||||
let other_reposts = reposts - 1
|
ProfilePicView(pubkey: pubkey, size: eventview_pfp_size(.small), highlight: .none, profiles: damus.profiles, disable_animation: damus.settings.disable_animation)
|
||||||
if other_reposts > 0 {
|
.onTapGesture {
|
||||||
Text(" and \(other_reposts) others reposted", comment: "Text indicating that the note was reposted (i.e. re-shared) by multiple people")
|
show_profile_action_sheet_if_enabled(damus_state: damus, pubkey: pubkey)
|
||||||
.foregroundColor(Color.gray)
|
}
|
||||||
} else {
|
.onLongPressGesture(minimumDuration: 0.1) {
|
||||||
Text("reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
.foregroundColor(Color.gray)
|
damus.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(value: Route.Reposts(reposts: .reposts(state: damus, target: target.id))) {
|
||||||
|
Text(people_reposted_text(profiles: damus.profiles, pubkey: pubkey, reposts: reposts))
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.update_stats), perform: { note_id in
|
.onReceive(handle_notify(.update_stats), perform: { note_id in
|
||||||
guard note_id == target else { return }
|
guard note_id == target.id else { return }
|
||||||
let repost_count = damus.boosts.counts[target]
|
let repost_count = damus.boosts.counts[target.id]
|
||||||
if let repost_count, reposts != repost_count {
|
if let repost_count, reposts != repost_count {
|
||||||
reposts = repost_count
|
reposts = repost_count
|
||||||
}
|
}
|
||||||
@@ -47,9 +53,25 @@ struct Reposted: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func people_reposted_text(profiles: Profiles, pubkey: Pubkey, reposts: Int, locale: Locale = Locale.current) -> String {
|
||||||
|
guard reposts > 0 else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let bundle = bundleForLocale(locale: locale)
|
||||||
|
let other_reposts = reposts - 1
|
||||||
|
let display_name = event_author_name(profiles: profiles, pubkey: pubkey)
|
||||||
|
|
||||||
|
if other_reposts == 0 {
|
||||||
|
return String(format: NSLocalizedString("%@ reposted", bundle: bundle, comment: "Text indicating that the note was reposted (i.e. re-shared)."), locale: locale, display_name)
|
||||||
|
} else {
|
||||||
|
return String(format: localizedStringFormat(key: "people_reposted_count", locale: locale), locale: locale, other_reposts, display_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Reposted_Previews: PreviewProvider {
|
struct Reposted_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let test_state = test_damus_state
|
let test_state = test_damus_state
|
||||||
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note.id)
|
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,12 +94,12 @@ struct SelectableText: View {
|
|||||||
case show_mute_word_view(highlighted_text: String)
|
case show_mute_word_view(highlighted_text: String)
|
||||||
|
|
||||||
func should_show_highlight_post_view() -> Bool {
|
func should_show_highlight_post_view() -> Bool {
|
||||||
guard case .show_highlight_post_view(let highlighted_text) = self else { return false }
|
guard case .show_highlight_post_view = self else { return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func should_show_mute_word_view() -> Bool {
|
func should_show_mute_word_view() -> Bool {
|
||||||
guard case .show_mute_word_view(let highlighted_text) = self else { return false }
|
guard case .show_mute_word_view = self else { return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,16 +119,23 @@ struct SelectableText: View {
|
|||||||
fileprivate class TextView: UITextView {
|
fileprivate class TextView: UITextView {
|
||||||
var postHighlight: (String) -> Void
|
var postHighlight: (String) -> Void
|
||||||
var muteWord: (String) -> Void
|
var muteWord: (String) -> Void
|
||||||
|
private let enableHighlighting: Bool
|
||||||
|
|
||||||
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void) {
|
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void, enableHighlighting: Bool) {
|
||||||
self.postHighlight = postHighlight
|
self.postHighlight = postHighlight
|
||||||
self.muteWord = muteWord
|
self.muteWord = muteWord
|
||||||
|
self.enableHighlighting = enableHighlighting
|
||||||
|
|
||||||
super.init(frame: frame, textContainer: textContainer)
|
super.init(frame: frame, textContainer: textContainer)
|
||||||
|
|
||||||
|
if enableHighlighting {
|
||||||
|
self.delegate = self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||||
if action == #selector(highlightText(_:)) {
|
if action == #selector(highlightText(_:)) {
|
||||||
@@ -142,23 +149,44 @@ fileprivate class TextView: UITextView {
|
|||||||
return super.canPerformAction(action, withSender: sender)
|
return super.canPerformAction(action, withSender: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSelectedText() -> String? {
|
private func getSelectedText() -> String? {
|
||||||
guard let selectedRange = self.selectedTextRange else { return nil }
|
guard let selectedRange = self.selectedTextRange else { return nil }
|
||||||
return self.text(in: selectedRange)
|
return self.text(in: selectedRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func highlightText(_ sender: Any?) {
|
@objc private func highlightText(_ sender: Any?) {
|
||||||
guard let selectedText = self.getSelectedText() else { return }
|
guard let selectedText = self.getSelectedText() else { return }
|
||||||
self.postHighlight(selectedText)
|
self.postHighlight(selectedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func muteText(_ sender: Any?) {
|
@objc private func muteText(_ sender: Any?) {
|
||||||
guard let selectedText = self.getSelectedText() else { return }
|
guard let selectedText = self.getSelectedText() else { return }
|
||||||
self.muteWord(selectedText)
|
self.muteWord(selectedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TextView: UITextViewDelegate {
|
||||||
|
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
||||||
|
guard enableHighlighting,
|
||||||
|
let selectedTextRange = self.selectedTextRange,
|
||||||
|
let selectedText = self.text(in: selectedTextRange),
|
||||||
|
!selectedText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let highlightAction = UIAction(title: NSLocalizedString("Highlight", comment: "Context menu action to highlight the selected text as context to draft a new note."), image: UIImage(systemName: "highlighter")) { [weak self] _ in
|
||||||
|
self?.postHighlight(selectedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
let muteAction = UIAction(title: NSLocalizedString("Mute", comment: "Context menu action to mute the selected word."), image: UIImage(systemName: "speaker.slash")) { [weak self] _ in
|
||||||
|
self?.muteWord(selectedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIMenu(children: suggestedActions + [highlightAction, muteAction])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||||
|
|
||||||
let attributedString: AttributedString
|
let attributedString: AttributedString
|
||||||
@@ -172,7 +200,7 @@ fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
|||||||
@Binding var height: CGFloat
|
@Binding var height: CGFloat
|
||||||
|
|
||||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
||||||
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord)
|
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord, enableHighlighting: enableHighlighting)
|
||||||
view.isEditable = false
|
view.isEditable = false
|
||||||
view.dataDetectorTypes = .all
|
view.dataDetectorTypes = .all
|
||||||
view.isSelectable = true
|
view.isSelectable = true
|
||||||
@@ -183,11 +211,6 @@ fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
|||||||
view.textContainerInset.right = 1.0
|
view.textContainerInset.right = 1.0
|
||||||
view.textAlignment = textAlignment
|
view.textAlignment = textAlignment
|
||||||
|
|
||||||
let menuController = UIMenuController.shared
|
|
||||||
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
|
|
||||||
let muteItem = UIMenuItem(title: "Mute", action: #selector(view.muteText(_:)))
|
|
||||||
menuController.menuItems = self.enableHighlighting ? [highlightItem, muteItem] : []
|
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,6 @@ struct UserStatusSheet: View {
|
|||||||
|
|
||||||
struct UserStatusSheet_Previews: PreviewProvider {
|
struct UserStatusSheet_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.postbox, keypair: test_keypair, status: .init())
|
UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.nostrNetwork.postbox, keypair: test_keypair, status: .init())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-45
@@ -199,7 +199,7 @@ struct ContentView: View {
|
|||||||
func MaybeReportView(target: ReportTarget) -> some View {
|
func MaybeReportView(target: ReportTarget) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if let keypair = damus_state.keypair.to_full() {
|
if let keypair = damus_state.keypair.to_full() {
|
||||||
ReportView(postbox: damus_state.postbox, target: target, keypair: keypair)
|
ReportView(postbox: damus_state.nostrNetwork.postbox, target: target, keypair: keypair)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
@@ -317,7 +317,7 @@ struct ContentView: View {
|
|||||||
case .post(let action):
|
case .post(let action):
|
||||||
PostView(action: action, damus_state: damus_state!)
|
PostView(action: action, damus_state: damus_state!)
|
||||||
case .user_status:
|
case .user_status:
|
||||||
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.nostrNetwork.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
case .event:
|
case .event:
|
||||||
EventDetailView()
|
EventDetailView()
|
||||||
@@ -356,7 +356,7 @@ struct ContentView: View {
|
|||||||
self.hide_bar = !show
|
self.hide_bar = !show
|
||||||
}
|
}
|
||||||
.onReceive(timer) { n in
|
.onReceive(timer) { n in
|
||||||
self.damus_state?.postbox.try_flushing_events()
|
self.damus_state?.nostrNetwork.postbox.try_flushing_events()
|
||||||
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.report)) { target in
|
.onReceive(handle_notify(.report)) { target in
|
||||||
@@ -367,8 +367,6 @@ struct ContentView: View {
|
|||||||
self.confirm_mute = true
|
self.confirm_mute = true
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
||||||
try? damus_state.pool.add_relay(.nwc(url: nwc.relay))
|
|
||||||
|
|
||||||
// update the lightning address on our profile when we attach a
|
// update the lightning address on our profile when we attach a
|
||||||
// wallet with an associated
|
// wallet with an associated
|
||||||
guard let ds = self.damus_state,
|
guard let ds = self.damus_state,
|
||||||
@@ -389,12 +387,12 @@ struct ContentView: View {
|
|||||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
|
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
|
||||||
|
|
||||||
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||||
ds.postbox.send(ev)
|
ds.nostrNetwork.postbox.send(ev)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.broadcast)) { ev in
|
.onReceive(handle_notify(.broadcast)) { ev in
|
||||||
guard let ds = self.damus_state else { return }
|
guard let ds = self.damus_state else { return }
|
||||||
|
|
||||||
ds.postbox.send(ev)
|
ds.nostrNetwork.postbox.send(ev)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unfollow)) { target in
|
.onReceive(handle_notify(.unfollow)) { target in
|
||||||
guard let state = self.damus_state else { return }
|
guard let state = self.damus_state else { return }
|
||||||
@@ -416,7 +414,7 @@ struct ContentView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) {
|
if !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) {
|
||||||
self.active_sheet = nil
|
self.active_sheet = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,7 +458,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.disconnect_relays)) { () in
|
.onReceive(handle_notify(.disconnect_relays)) { () in
|
||||||
damus_state.pool.disconnect()
|
damus_state.nostrNetwork.pool.disconnect()
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
||||||
print("txn: 📙 DAMUS ACTIVE NOTIFY")
|
print("txn: 📙 DAMUS ACTIVE NOTIFY")
|
||||||
@@ -506,7 +504,7 @@ struct ContentView: View {
|
|||||||
break
|
break
|
||||||
case .active:
|
case .active:
|
||||||
print("txn: 📙 DAMUS ACTIVE")
|
print("txn: 📙 DAMUS ACTIVE")
|
||||||
damus_state.pool.ping()
|
damus_state.nostrNetwork.pool.ping()
|
||||||
@unknown default:
|
@unknown default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -525,7 +523,7 @@ struct ContentView: View {
|
|||||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
|
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
|
||||||
|
|
||||||
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||||
ds.postbox.send(profile_ev)
|
ds.nostrNetwork.postbox.send(profile_ev)
|
||||||
}
|
}
|
||||||
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
||||||
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
||||||
@@ -557,7 +555,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ds.mutelist_manager.set_mutelist(mutelist)
|
ds.mutelist_manager.set_mutelist(mutelist)
|
||||||
ds.postbox.send(mutelist)
|
ds.nostrNetwork.postbox.send(mutelist)
|
||||||
|
|
||||||
confirm_overwrite_mutelist = false
|
confirm_overwrite_mutelist = false
|
||||||
confirm_mute = false
|
confirm_mute = false
|
||||||
@@ -589,7 +587,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ds.mutelist_manager.set_mutelist(ev)
|
ds.mutelist_manager.set_mutelist(ev)
|
||||||
ds.postbox.send(ev)
|
ds.nostrNetwork.postbox.send(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, message: {
|
}, message: {
|
||||||
@@ -630,7 +628,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func handleNotification(notification: LossyLocalNotification) {
|
func handleNotification(notification: LossyLocalNotification) {
|
||||||
Log.info("ContentView is handling a notification", for: .push_notifications)
|
Log.info("ContentView is handling a notification", for: .push_notifications)
|
||||||
guard let damus_state else {
|
guard damus_state != nil else {
|
||||||
// This should never happen because `listenAndHandleLocalNotifications` is called after damus state is initialized in `onAppear`
|
// This should never happen because `listenAndHandleLocalNotifications` is called after damus state is initialized in `onAppear`
|
||||||
assertionFailure("DamusState not loaded when ContentView (new handler) was handling a notification")
|
assertionFailure("DamusState not loaded when ContentView (new handler) was handling a notification")
|
||||||
Log.error("DamusState not loaded when ContentView (new handler) was handling a notification", for: .push_notifications)
|
Log.error("DamusState not loaded when ContentView (new handler) was handling a notification", for: .push_notifications)
|
||||||
@@ -658,28 +656,14 @@ struct ContentView: View {
|
|||||||
|
|
||||||
guard let ndb = mndb else { return }
|
guard let ndb = mndb else { return }
|
||||||
|
|
||||||
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
|
||||||
let model_cache = RelayModelCache()
|
let model_cache = RelayModelCache()
|
||||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
|
||||||
|
|
||||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||||
|
|
||||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||||
for relay in bootstrap_relays {
|
|
||||||
let descriptor = RelayDescriptor(url: relay, info: .rw)
|
|
||||||
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
self.damus_state = DamusState(keypair: keypair,
|
||||||
|
|
||||||
if let nwc_str = settings.nostr_wallet_connect,
|
|
||||||
let nwc = WalletConnectURL(str: nwc_str) {
|
|
||||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
|
||||||
}
|
|
||||||
|
|
||||||
self.damus_state = DamusState(pool: pool,
|
|
||||||
keypair: keypair,
|
|
||||||
likes: EventCounter(our_pubkey: pubkey),
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
boosts: EventCounter(our_pubkey: pubkey),
|
boosts: EventCounter(our_pubkey: pubkey),
|
||||||
contacts: Contacts(our_pubkey: pubkey),
|
contacts: Contacts(our_pubkey: pubkey),
|
||||||
@@ -695,8 +679,6 @@ struct ContentView: View {
|
|||||||
drafts: Drafts(),
|
drafts: Drafts(),
|
||||||
events: EventCache(ndb: ndb),
|
events: EventCache(ndb: ndb),
|
||||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||||
postbox: PostBox(pool: pool),
|
|
||||||
bootstrap_relays: bootstrap_relays,
|
|
||||||
replies: ReplyCounter(our_pubkey: pubkey),
|
replies: ReplyCounter(our_pubkey: pubkey),
|
||||||
wallet: WalletModel(settings: settings),
|
wallet: WalletModel(settings: settings),
|
||||||
nav: self.navigationCoordinator,
|
nav: self.navigationCoordinator,
|
||||||
@@ -720,7 +702,8 @@ struct ContentView: View {
|
|||||||
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
|
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
|
||||||
}
|
}
|
||||||
|
|
||||||
pool.connect()
|
damus_state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||||
|
damus_state.nostrNetwork.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func music_changed(_ state: MusicState) {
|
func music_changed(_ state: MusicState) {
|
||||||
@@ -743,7 +726,7 @@ struct ContentView: View {
|
|||||||
pdata.status.music = music
|
pdata.status.music = music
|
||||||
|
|
||||||
guard let ev = music.to_note(keypair: kp) else { return }
|
guard let ev = music.to_note(keypair: kp) else { return }
|
||||||
damus_state.postbox.send(ev)
|
damus_state.nostrNetwork.postbox.send(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,7 +975,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
|||||||
var has_event = false
|
var has_event = false
|
||||||
guard let filter else { return }
|
guard let filter else { return }
|
||||||
|
|
||||||
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
||||||
guard case .nostr_event(let ev) = res else {
|
guard case .nostr_event(let ev) = res else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1006,7 +989,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
|||||||
break
|
break
|
||||||
case .event(_, let ev):
|
case .event(_, let ev):
|
||||||
has_event = true
|
has_event = true
|
||||||
state.pool.unsubscribe(sub_id: subid)
|
state.nostrNetwork.pool.unsubscribe(sub_id: subid)
|
||||||
|
|
||||||
switch query {
|
switch query {
|
||||||
case .profile:
|
case .profile:
|
||||||
@@ -1019,11 +1002,11 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
|||||||
case .eose:
|
case .eose:
|
||||||
if !has_event {
|
if !has_event {
|
||||||
attempts += 1
|
attempts += 1
|
||||||
if attempts >= state.pool.our_descriptors.count {
|
if attempts >= state.nostrNetwork.pool.our_descriptors.count {
|
||||||
callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil
|
callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
|
state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
|
||||||
case .notice:
|
case .notice:
|
||||||
break
|
break
|
||||||
case .auth:
|
case .auth:
|
||||||
@@ -1042,15 +1025,15 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
|||||||
/// - naddr: the `naddr` address
|
/// - naddr: the `naddr` address
|
||||||
/// - callback: A function to handle the found event
|
/// - callback: A function to handle the found event
|
||||||
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
|
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
|
||||||
var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
let nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
||||||
|
|
||||||
let filter = NostrFilter(kinds: nostrKinds, authors: [naddr.author])
|
let filter = NostrFilter(kinds: nostrKinds, authors: [naddr.author])
|
||||||
|
|
||||||
let subid = UUID().description
|
let subid = UUID().description
|
||||||
|
|
||||||
damus_state.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
|
damus_state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
|
||||||
guard case .nostr_event(let ev) = res else {
|
guard case .nostr_event(let ev) = res else {
|
||||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1058,14 +1041,14 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos
|
|||||||
for tag in ev.tags {
|
for tag in ev.tags {
|
||||||
if(tag.count >= 2 && tag[0].string() == "d"){
|
if(tag.count >= 2 && tag[0].string() == "d"){
|
||||||
if (tag[1].string() == naddr.identifier){
|
if (tag[1].string() == naddr.identifier){
|
||||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||||
callback(ev)
|
callback(ev)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1113,7 +1096,7 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
|
|||||||
|
|
||||||
let old_contacts = state.contacts.event
|
let old_contacts = state.contacts.event
|
||||||
|
|
||||||
guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
guard let ev = unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
||||||
else {
|
else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1139,7 +1122,7 @@ func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
guard let ev = follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
||||||
else {
|
else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1214,7 +1197,7 @@ extension LossyLocalNotification {
|
|||||||
case .nprofile(let nProfile):
|
case .nprofile(let nProfile):
|
||||||
// TODO: Improve this by implementing a profile route that handles nprofiles with their relay hints.
|
// TODO: Improve this by implementing a profile route that handles nprofiles with their relay hints.
|
||||||
return .route(.ProfileByKey(pubkey: nProfile.author))
|
return .route(.ProfileByKey(pubkey: nProfile.author))
|
||||||
case .nrelay(let string):
|
case .nrelay:
|
||||||
// We do not need to implement `nrelay` support, it has been deprecated.
|
// We do not need to implement `nrelay` support, it has been deprecated.
|
||||||
// See https://github.com/nostr-protocol/nips/blob/6e7a618e7f873bb91e743caacc3b09edab7796a0/BREAKING.md?plain=1#L21
|
// See https://github.com/nostr-protocol/nips/blob/6e7a618e7f873bb91e743caacc3b09edab7796a0/BREAKING.md?plain=1#L21
|
||||||
return .sheet(.error(ErrorView.UserPresentableError(
|
return .sheet(.error(ErrorView.UserPresentableError(
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>INSendMessageIntent</string>
|
||||||
|
</array>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
//
|
|
||||||
// CameraService+Extensions.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Suhail Saqan on 8/5/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
extension AVCaptureVideoOrientation {
|
|
||||||
init?(deviceOrientation: UIDeviceOrientation) {
|
|
||||||
switch deviceOrientation {
|
|
||||||
case .portrait: self = .portrait
|
|
||||||
case .portraitUpsideDown: self = .portraitUpsideDown
|
|
||||||
case .landscapeLeft: self = .landscapeRight
|
|
||||||
case .landscapeRight: self = .landscapeLeft
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init?(interfaceOrientation: UIInterfaceOrientation) {
|
|
||||||
switch interfaceOrientation {
|
|
||||||
case .portrait: self = .portrait
|
|
||||||
case .portraitUpsideDown: self = .portraitUpsideDown
|
|
||||||
case .landscapeLeft: self = .landscapeLeft
|
|
||||||
case .landscapeRight: self = .landscapeRight
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,44 +63,10 @@ func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func decode_json_relays(_ content: String) -> [RelayURL: RelayInfo]? {
|
func decode_json_relays(_ content: String) -> [RelayURL: LegacyKind3RelayRWConfiguration]? {
|
||||||
return decode_json(content)
|
return decode_json(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: RelayURL) -> NostrEvent?{
|
|
||||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
|
||||||
|
|
||||||
relays.removeValue(forKey: relay)
|
|
||||||
|
|
||||||
guard let content = encode_json(relays) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
|
||||||
}
|
|
||||||
|
|
||||||
func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: RelayURL, info: RelayInfo) -> NostrEvent? {
|
|
||||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
|
||||||
|
|
||||||
// If kind:3 content is empty, or if the relay doesn't exist in the list,
|
|
||||||
// we want to create a kind:3 event with the new relay
|
|
||||||
guard ev.content.isEmpty || relays.index(forKey: relay) == nil else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
relays[relay] = info
|
|
||||||
|
|
||||||
guard let content = encode_json(relays) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [RelayURL: RelayInfo] {
|
|
||||||
return decode_json_relays(content) ?? make_contact_relays(relays)
|
|
||||||
}
|
|
||||||
|
|
||||||
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
||||||
return contacts.references.contains { ref in
|
return contacts.references.contains { ref in
|
||||||
switch (ref, follow) {
|
switch (ref, follow) {
|
||||||
@@ -128,22 +94,3 @@ func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEven
|
|||||||
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
|
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func make_contact_relays(_ relays: [RelayDescriptor]) -> [RelayURL: RelayInfo] {
|
|
||||||
return relays.reduce(into: [:]) { acc, relay in
|
|
||||||
acc[relay.url] = relay.info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func make_relay_metadata(relays: [RelayDescriptor], keypair: FullKeypair) -> NostrEvent? {
|
|
||||||
let tags = relays.compactMap { r -> [String]? in
|
|
||||||
var tag = ["r", r.url.absoluteString]
|
|
||||||
if (r.info.read ?? true) != (r.info.write ?? true) {
|
|
||||||
tag += r.info.read == true ? ["read"] : ["write"]
|
|
||||||
}
|
|
||||||
if ((r.info.read ?? true) || (r.info.write ?? true)) && r.variant == .regular {
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 10_002, tags: tags)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import Foundation
|
|||||||
|
|
||||||
/// Simple filter to determine whether to show posts or all posts and replies.
|
/// Simple filter to determine whether to show posts or all posts and replies.
|
||||||
enum FilterState : Int {
|
enum FilterState : Int {
|
||||||
case posts_and_replies = 1
|
|
||||||
case posts = 0
|
case posts = 0
|
||||||
|
case posts_and_replies = 1
|
||||||
|
case conversations = 2
|
||||||
|
|
||||||
func filter(ev: NostrEvent) -> Bool {
|
func filter(ev: NostrEvent) -> Bool {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -19,6 +20,8 @@ enum FilterState : Int {
|
|||||||
return ev.known_kind == .boost || ev.known_kind == .highlight || !ev.is_reply()
|
return ev.known_kind == .boost || ev.known_kind == .highlight || !ev.is_reply()
|
||||||
case .posts_and_replies:
|
case .posts_and_replies:
|
||||||
return true
|
return true
|
||||||
|
case .conversations:
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,12 @@ func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func timestamp_filter(ev: NostrEvent) -> Bool {
|
||||||
|
// Allow notes that are created no more than 3 seconds in the future
|
||||||
|
// to account for natural clock skew between sender and receiver.
|
||||||
|
ev.age >= -3
|
||||||
|
}
|
||||||
|
|
||||||
/// Generic filter with various tweakable settings
|
/// Generic filter with various tweakable settings
|
||||||
struct ContentFilters {
|
struct ContentFilters {
|
||||||
var filters: [(NostrEvent) -> Bool]
|
var filters: [(NostrEvent) -> Bool]
|
||||||
@@ -63,6 +72,7 @@ extension ContentFilters {
|
|||||||
filters.append(nsfw_tag_filter)
|
filters.append(nsfw_tag_filter)
|
||||||
}
|
}
|
||||||
filters.append(get_repost_of_muted_user_filter(damus_state: damus_state))
|
filters.append(get_repost_of_muted_user_filter(damus_state: damus_state))
|
||||||
|
filters.append(timestamp_filter)
|
||||||
return filters
|
return filters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class CreateAccountModel: ObservableObject {
|
|||||||
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var full_keypair: FullKeypair {
|
||||||
|
return FullKeypair(pubkey: self.pubkey, privkey: self.privkey)
|
||||||
|
}
|
||||||
|
|
||||||
init(display_name: String = "", name: String = "", about: String = "") {
|
init(display_name: String = "", name: String = "", about: String = "") {
|
||||||
let keypair = generate_new_keypair()
|
let keypair = generate_new_keypair()
|
||||||
self.pubkey = keypair.pubkey
|
self.pubkey = keypair.pubkey
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import LinkPresentation
|
|||||||
import EmojiPicker
|
import EmojiPicker
|
||||||
|
|
||||||
class DamusState: HeadlessDamusState {
|
class DamusState: HeadlessDamusState {
|
||||||
let pool: RelayPool
|
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
let likes: EventCounter
|
let likes: EventCounter
|
||||||
let boosts: EventCounter
|
let boosts: EventCounter
|
||||||
@@ -28,8 +27,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
let drafts: Drafts
|
let drafts: Drafts
|
||||||
let events: EventCache
|
let events: EventCache
|
||||||
let bookmarks: BookmarksManager
|
let bookmarks: BookmarksManager
|
||||||
let postbox: PostBox
|
|
||||||
let bootstrap_relays: [RelayURL]
|
|
||||||
let replies: ReplyCounter
|
let replies: ReplyCounter
|
||||||
let wallet: WalletModel
|
let wallet: WalletModel
|
||||||
let nav: NavigationCoordinator
|
let nav: NavigationCoordinator
|
||||||
@@ -39,9 +36,9 @@ class DamusState: HeadlessDamusState {
|
|||||||
var purple: DamusPurple
|
var purple: DamusPurple
|
||||||
var push_notification_client: PushNotificationClient
|
var push_notification_client: PushNotificationClient
|
||||||
let emoji_provider: EmojiProvider
|
let emoji_provider: EmojiProvider
|
||||||
|
private(set) var nostrNetwork: NostrNetworkManager
|
||||||
|
|
||||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
|
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
|
||||||
self.pool = pool
|
|
||||||
self.keypair = keypair
|
self.keypair = keypair
|
||||||
self.likes = likes
|
self.likes = likes
|
||||||
self.boosts = boosts
|
self.boosts = boosts
|
||||||
@@ -58,8 +55,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
self.drafts = drafts
|
self.drafts = drafts
|
||||||
self.events = events
|
self.events = events
|
||||||
self.bookmarks = bookmarks
|
self.bookmarks = bookmarks
|
||||||
self.postbox = postbox
|
|
||||||
self.bootstrap_relays = bootstrap_relays
|
|
||||||
self.replies = replies
|
self.replies = replies
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
self.nav = nav
|
self.nav = nav
|
||||||
@@ -73,6 +68,9 @@ class DamusState: HeadlessDamusState {
|
|||||||
self.quote_reposts = quote_reposts
|
self.quote_reposts = quote_reposts
|
||||||
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
||||||
self.emoji_provider = emoji_provider
|
self.emoji_provider = emoji_provider
|
||||||
|
|
||||||
|
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
|
||||||
|
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -98,27 +96,13 @@ class DamusState: HeadlessDamusState {
|
|||||||
guard let ndb = mndb else { return nil }
|
guard let ndb = mndb else { return nil }
|
||||||
let pubkey = keypair.pubkey
|
let pubkey = keypair.pubkey
|
||||||
|
|
||||||
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
|
||||||
let model_cache = RelayModelCache()
|
let model_cache = RelayModelCache()
|
||||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||||
|
|
||||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||||
|
|
||||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
|
||||||
for relay in bootstrap_relays {
|
|
||||||
let descriptor = RelayDescriptor(url: relay, info: .rw)
|
|
||||||
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
|
||||||
|
|
||||||
if let nwc_str = settings.nostr_wallet_connect,
|
|
||||||
let nwc = WalletConnectURL(str: nwc_str) {
|
|
||||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
|
||||||
}
|
|
||||||
self.init(
|
self.init(
|
||||||
pool: pool,
|
|
||||||
keypair: keypair,
|
keypair: keypair,
|
||||||
likes: EventCounter(our_pubkey: pubkey),
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
boosts: EventCounter(our_pubkey: pubkey),
|
boosts: EventCounter(our_pubkey: pubkey),
|
||||||
@@ -135,8 +119,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
drafts: Drafts(),
|
drafts: Drafts(),
|
||||||
events: EventCache(ndb: ndb),
|
events: EventCache(ndb: ndb),
|
||||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||||
postbox: PostBox(pool: pool),
|
|
||||||
bootstrap_relays: bootstrap_relays,
|
|
||||||
replies: ReplyCounter(our_pubkey: pubkey),
|
replies: ReplyCounter(our_pubkey: pubkey),
|
||||||
wallet: WalletModel(settings: settings),
|
wallet: WalletModel(settings: settings),
|
||||||
nav: navigationCoordinator,
|
nav: navigationCoordinator,
|
||||||
@@ -179,7 +161,7 @@ class DamusState: HeadlessDamusState {
|
|||||||
try await self.push_notification_client.revoke_token()
|
try await self.push_notification_client.revoke_token()
|
||||||
}
|
}
|
||||||
wallet.disconnect()
|
wallet.disconnect()
|
||||||
pool.close()
|
nostrNetwork.pool.close()
|
||||||
ndb.close()
|
ndb.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +171,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
let kp = Keypair(pubkey: empty_pub, privkey: nil)
|
let kp = Keypair(pubkey: empty_pub, privkey: nil)
|
||||||
|
|
||||||
return DamusState.init(
|
return DamusState.init(
|
||||||
pool: RelayPool(ndb: .empty),
|
|
||||||
keypair: Keypair(pubkey: empty_pub, privkey: empty_sec),
|
keypair: Keypair(pubkey: empty_pub, privkey: empty_sec),
|
||||||
likes: EventCounter(our_pubkey: empty_pub),
|
likes: EventCounter(our_pubkey: empty_pub),
|
||||||
boosts: EventCounter(our_pubkey: empty_pub),
|
boosts: EventCounter(our_pubkey: empty_pub),
|
||||||
@@ -206,8 +187,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
drafts: Drafts(),
|
drafts: Drafts(),
|
||||||
events: EventCache(ndb: .empty),
|
events: EventCache(ndb: .empty),
|
||||||
bookmarks: BookmarksManager(pubkey: empty_pub),
|
bookmarks: BookmarksManager(pubkey: empty_pub),
|
||||||
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
|
||||||
bootstrap_relays: [],
|
|
||||||
replies: ReplyCounter(our_pubkey: empty_pub),
|
replies: ReplyCounter(our_pubkey: empty_pub),
|
||||||
wallet: WalletModel(settings: UserSettingsStore()),
|
wallet: WalletModel(settings: UserSettingsStore()),
|
||||||
nav: NavigationCoordinator(),
|
nav: NavigationCoordinator(),
|
||||||
@@ -219,3 +198,29 @@ class DamusState: HeadlessDamusState {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate extension DamusState {
|
||||||
|
struct NostrNetworkManagerDelegate: NostrNetworkManager.Delegate {
|
||||||
|
let settings: UserSettingsStore
|
||||||
|
let contacts: Contacts
|
||||||
|
|
||||||
|
var ndb: Ndb
|
||||||
|
var keypair: Keypair
|
||||||
|
|
||||||
|
var latestRelayListEventIdHex: String? {
|
||||||
|
get { self.settings.latestRelayListEventIdHex }
|
||||||
|
set { self.settings.latestRelayListEventIdHex = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestContactListEvent: NostrEvent? { self.contacts.event }
|
||||||
|
var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() }
|
||||||
|
var developerMode: Bool { self.settings.developer_mode }
|
||||||
|
var relayModelCache: RelayModelCache
|
||||||
|
var relayFilters: RelayFilters
|
||||||
|
|
||||||
|
var nwcWallet: WalletConnectURL? {
|
||||||
|
guard let nwcString = self.settings.nostr_wallet_connect else { return nil }
|
||||||
|
return WalletConnectURL(str: nwcString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class DraftArtifacts: Equatable {
|
|||||||
if case .pubkey(let pubkey) = mention.ref {
|
if case .pubkey(let pubkey) = mention.ref {
|
||||||
// A profile reference, format things properly.
|
// A profile reference, format things properly.
|
||||||
let profile = damus_state.ndb.lookup_profile(pubkey)?.unsafeUnownedValue?.profile
|
let profile = damus_state.ndb.lookup_profile(pubkey)?.unsafeUnownedValue?.profile
|
||||||
let profile_name = parse_display_name(profile: profile, pubkey: pubkey).username
|
let profile_name = DisplayName(profile: profile, pubkey: pubkey).username
|
||||||
guard let url_address = URL(string: block.asString) else {
|
guard let url_address = URL(string: block.asString) else {
|
||||||
rich_text_content.append(.init(string: block.asString))
|
rich_text_content.append(.init(string: block.asString))
|
||||||
continue
|
continue
|
||||||
@@ -251,7 +251,7 @@ class Drafts: ObservableObject {
|
|||||||
// TODO: Once it is time to implement draft syncing with relays, please consider the following:
|
// TODO: Once it is time to implement draft syncing with relays, please consider the following:
|
||||||
// - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations
|
// - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations
|
||||||
// - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again)
|
// - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again)
|
||||||
damus_state.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
|
damus_state.nostrNetwork.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.settings.draft_event_ids = draft_events.map({ $0.id.hex() })
|
damus_state.settings.draft_event_ids = draft_events.map({ $0.id.hex() })
|
||||||
|
|||||||
@@ -68,13 +68,13 @@ class EventsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func subscribe() {
|
func subscribe() {
|
||||||
state.pool.subscribe(sub_id: sub_id,
|
state.nostrNetwork.pool.subscribe(sub_id: sub_id,
|
||||||
filters: [get_filter()],
|
filters: [get_filter()],
|
||||||
handler: handle_nostr_event)
|
handler: handle_nostr_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
state.pool.unsubscribe(sub_id: sub_id)
|
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handle_event(relay_id: RelayURL, ev: NostrEvent) {
|
private func handle_event(relay_id: RelayURL, ev: NostrEvent) {
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ class FollowersModel: ObservableObject {
|
|||||||
let filter = get_filter()
|
let filter = get_filter()
|
||||||
let filters = [filter]
|
let filters = [filter]
|
||||||
//print_filters(relay_id: "following", filters: [filters])
|
//print_filters(relay_id: "following", filters: [filters])
|
||||||
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
self.damus_state.pool.unsubscribe(sub_id: sub_id)
|
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_contact_event(_ ev: NostrEvent) {
|
func handle_contact_event(_ ev: NostrEvent) {
|
||||||
@@ -61,7 +61,7 @@ class FollowersModel: ObservableObject {
|
|||||||
|
|
||||||
let filter = NostrFilter(kinds: [.metadata],
|
let filter = NostrFilter(kinds: [.metadata],
|
||||||
authors: authors)
|
authors: authors)
|
||||||
damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
|
damus_state.nostrNetwork.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||||
@@ -86,7 +86,7 @@ class FollowersModel: ObservableObject {
|
|||||||
guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return }
|
guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return }
|
||||||
load_profiles(relay_id: relay_id, txn: txn)
|
load_profiles(relay_id: relay_id, txn: txn)
|
||||||
} else if sub_id == self.profiles_id {
|
} else if sub_id == self.profiles_id {
|
||||||
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
||||||
}
|
}
|
||||||
|
|
||||||
case .ok:
|
case .ok:
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class FollowingModel {
|
|||||||
}
|
}
|
||||||
let filters = [filter]
|
let filters = [filter]
|
||||||
//print_filters(relay_id: "following", filters: [filters])
|
//print_filters(relay_id: "following", filters: [filters])
|
||||||
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
@@ -50,7 +50,7 @@ class FollowingModel {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
print("unsubscribing from following \(sub_id)")
|
print("unsubscribing from following \(sub_id)")
|
||||||
self.damus_state.pool.unsubscribe(sub_id: sub_id)
|
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||||
|
|||||||
@@ -95,13 +95,13 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pool: RelayPool {
|
var pool: RelayPool {
|
||||||
return damus_state.pool
|
self.damus_state.nostrNetwork.pool
|
||||||
}
|
}
|
||||||
|
|
||||||
var dms: DirectMessagesModel {
|
var dms: DirectMessagesModel {
|
||||||
return damus_state.dms
|
return damus_state.dms
|
||||||
}
|
}
|
||||||
|
|
||||||
func has_sub_id_event(sub_id: String, ev_id: NoteId) -> Bool {
|
func has_sub_id_event(sub_id: String, ev_id: NoteId) -> Bool {
|
||||||
if !has_event.keys.contains(sub_id) {
|
if !has_event.keys.contains(sub_id) {
|
||||||
has_event[sub_id] = Set()
|
has_event[sub_id] = Set()
|
||||||
@@ -225,6 +225,8 @@ class HomeModel: ContactsDelegate {
|
|||||||
// TODO: Implement draft syncing with relays. We intentionally do not support that as of writing. See `DraftsModel.swift` for other details
|
// TODO: Implement draft syncing with relays. We intentionally do not support that as of writing. See `DraftsModel.swift` for other details
|
||||||
// try? damus_state.drafts.load(wrapped_draft_note: ev, with: damus_state)
|
// try? damus_state.drafts.load(wrapped_draft_note: ev, with: damus_state)
|
||||||
break
|
break
|
||||||
|
case .relay_list:
|
||||||
|
break // This will be handled by `UserRelayListManager`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,13 +262,13 @@ class HomeModel: ContactsDelegate {
|
|||||||
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
|
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
|
||||||
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
|
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
|
||||||
let nwc = WalletConnectURL(str: nwc_str),
|
let nwc = WalletConnectURL(str: nwc_str),
|
||||||
let resp = await FullWalletResponse(from: ev, nwc: nwc) else {
|
let resp = await WalletConnect.FullWalletResponse(from: ev, nwc: nwc) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// since command results are not returned for ephemeral events,
|
// since command results are not returned for ephemeral events,
|
||||||
// remove the request from the postbox which is likely failing over and over
|
// remove the request from the postbox which is likely failing over and over
|
||||||
if damus_state.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) {
|
if damus_state.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) {
|
||||||
print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
|
print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
|
||||||
} else {
|
} else {
|
||||||
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
|
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
|
||||||
@@ -274,12 +276,24 @@ class HomeModel: ContactsDelegate {
|
|||||||
|
|
||||||
guard resp.response.error == nil else {
|
guard resp.response.error == nil else {
|
||||||
print("nwc error: \(resp.response)")
|
print("nwc error: \(resp.response)")
|
||||||
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
WalletConnect.handle_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.response.result_type == .list_transactions {
|
||||||
|
Log.info("Received NWC transaction list from %s", for: .nwc, relay.absoluteString)
|
||||||
|
damus_state.wallet.handle_nwc_response(response: resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.response.result_type == .get_balance {
|
||||||
|
Log.info("Received NWC balance information from %s", for: .nwc, relay.absoluteString)
|
||||||
|
damus_state.wallet.handle_nwc_response(response: resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
|
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
|
||||||
nwc_success(state: self.damus_state, resp: resp)
|
WalletConnect.handle_zap_success(state: self.damus_state, resp: resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +467,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
let nwc = WalletConnectURL(str: nwc_str),
|
let nwc = WalletConnectURL(str: nwc_str),
|
||||||
nwc.relay == relay_id
|
nwc.relay == relay_id
|
||||||
{
|
{
|
||||||
subscribe_to_nwc(url: nwc, pool: pool)
|
WalletConnect.subscribe(url: nwc, pool: pool)
|
||||||
}
|
}
|
||||||
case .error(let merr):
|
case .error(let merr):
|
||||||
let desc = String(describing: merr)
|
let desc = String(describing: merr)
|
||||||
@@ -466,7 +480,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
|
update_signal_from_pool(signal: self.signal, pool: damus_state.nostrNetwork.pool)
|
||||||
case .nostr_event(let ev):
|
case .nostr_event(let ev):
|
||||||
switch ev {
|
switch ev {
|
||||||
case .event(let sub_id, let ev):
|
case .event(let sub_id, let ev):
|
||||||
@@ -936,7 +950,6 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) {
|
|||||||
state.contacts.event = ev
|
state.contacts.event = ev
|
||||||
|
|
||||||
load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
|
load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||||
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func process_contact_event(state: DamusState, ev: NostrEvent) {
|
func process_contact_event(state: DamusState, ev: NostrEvent) {
|
||||||
@@ -944,78 +957,6 @@ func process_contact_event(state: DamusState, ev: NostrEvent) {
|
|||||||
add_contact_if_friend(contacts: state.contacts, ev: ev)
|
add_contact_if_friend(contacts: state.contacts, ev: ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
|
||||||
let bootstrap_dict: [RelayURL: RelayInfo] = [:]
|
|
||||||
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in
|
|
||||||
d[r] = .rw
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let decoded: [RelayURL: RelayInfo] = decode_json_relays(ev.content) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var changed = false
|
|
||||||
|
|
||||||
var new = Set<RelayURL>()
|
|
||||||
for key in decoded.keys {
|
|
||||||
new.insert(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
var old = Set<RelayURL>()
|
|
||||||
for key in old_decoded.keys {
|
|
||||||
old.insert(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
let diff = old.symmetricDifference(new)
|
|
||||||
|
|
||||||
let new_relay_filters = load_relay_filters(state.pubkey) == nil
|
|
||||||
for d in diff {
|
|
||||||
changed = true
|
|
||||||
if new.contains(d) {
|
|
||||||
let descriptor = RelayDescriptor(url: d, info: decoded[d] ?? .rw)
|
|
||||||
add_new_relay(model_cache: state.relay_model_cache, relay_filters: state.relay_filters, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: state.settings.developer_mode)
|
|
||||||
} else {
|
|
||||||
state.pool.remove_relay(d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
|
|
||||||
state.pool.connect()
|
|
||||||
notify(.relays_changed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) {
|
|
||||||
try? pool.add_relay(descriptor)
|
|
||||||
let url = descriptor.url
|
|
||||||
|
|
||||||
let relay_id = url
|
|
||||||
guard model_cache.model(withURL: url) == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Task.detached(priority: .background) {
|
|
||||||
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
let model = RelayModel(url, metadata: meta)
|
|
||||||
model_cache.insert(model: model)
|
|
||||||
|
|
||||||
if logging_enabled {
|
|
||||||
pool.setLog(model.log, for: relay_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if this is the first time adding filters, we should filter non-paid relays
|
|
||||||
if new_relay_filters && !meta.is_paid {
|
|
||||||
relay_filters.insert(timeline: .search, relay_id: relay_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetch_relay_metadata(relay_id: RelayURL) async throws -> RelayMetadata? {
|
func fetch_relay_metadata(relay_id: RelayURL) async throws -> RelayMetadata? {
|
||||||
var urlString = relay_id.absoluteString.replacingOccurrences(of: "wss://", with: "https://")
|
var urlString = relay_id.absoluteString.replacingOccurrences(of: "wss://", with: "https://")
|
||||||
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
|
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
|
||||||
@@ -1238,3 +1179,4 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -47,16 +47,16 @@ enum MuteItem: Hashable, Equatable {
|
|||||||
// rhs is the item we want to check against (ie. the item in the mute list)
|
// rhs is the item we want to check against (ie. the item in the mute list)
|
||||||
|
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, let rhs_expiration_date)):
|
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, _)):
|
||||||
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
|
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
|
||||||
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, let rhs_expiration_date)):
|
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, _)):
|
||||||
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
|
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
|
||||||
case (.word(let lhs_word, _), .word(let rhs_word, let rhs_expiration_date)):
|
case (.word(let lhs_word, _), .word(let rhs_word, _)):
|
||||||
return lhs_word == rhs_word && !rhs.is_expired()
|
return lhs_word == rhs_word && !rhs.is_expired()
|
||||||
case (.thread(let lhs_thread, _), .thread(let rhs_thread, let rhs_expiration_date)):
|
case (.thread(let lhs_thread, _), .thread(let rhs_thread, _)):
|
||||||
return lhs_thread == rhs_thread && !rhs.is_expired()
|
return lhs_thread == rhs_thread && !rhs.is_expired()
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: Da
|
|||||||
let previous_mute_list_event = damus_state.mutelist_manager.event
|
let previous_mute_list_event = damus_state.mutelist_manager.event
|
||||||
guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return }
|
guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return }
|
||||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
|
damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
|
||||||
damus_state.postbox.send(new_mutelist_event)
|
damus_state.nostrNetwork.postbox.send(new_mutelist_event)
|
||||||
// Set existing muted threads to an empty array
|
// Set existing muted threads to an empty array
|
||||||
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
|
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
//
|
||||||
|
// NostrNetworkManager.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-02-26.
|
||||||
|
//
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Manages interactions with the Nostr Network.
|
||||||
|
///
|
||||||
|
/// This delineates a layer that is responsible for doing mid-level management of interactions with the Nostr network, controlling lower-level classes that perform more network/DB specific code, and providing an easier to use and more semantic interfaces for the rest of the app.
|
||||||
|
///
|
||||||
|
/// This is responsible for:
|
||||||
|
/// - Managing the user's relay list
|
||||||
|
/// - Establishing a `RelayPool` and maintaining it in sync with the user's relay list as it changes
|
||||||
|
/// - Abstracting away complexities of interacting with the nostr network, providing an easier-to-use interface to fetch and send content related to the Nostr network
|
||||||
|
///
|
||||||
|
/// This is **NOT** responsible for:
|
||||||
|
/// - Doing actual storage of relay list (delegated via the delegate
|
||||||
|
/// - Handling low-level relay logic (this will be delegated to lower level classes used in RelayPool/RelayConnection)
|
||||||
|
class NostrNetworkManager {
|
||||||
|
/// The relay pool that we manage
|
||||||
|
///
|
||||||
|
/// ## Implementation notes
|
||||||
|
///
|
||||||
|
/// - This will be marked `private` in the future to prevent other code from accessing the relay pool directly. Code outside this layer should use a higher level interface
|
||||||
|
let pool: RelayPool // TODO: Make this private and make higher level interface for classes outside the NostrNetworkManager
|
||||||
|
/// A delegate that allows us to interact with the rest of app without introducing hard or circular dependencies
|
||||||
|
private var delegate: Delegate
|
||||||
|
/// Manages the user's relay list, controls RelayPool's connected relays
|
||||||
|
let userRelayList: UserRelayListManager
|
||||||
|
/// Handles sending out notes to the network
|
||||||
|
let postbox: PostBox
|
||||||
|
/// Handles subscriptions and functions to read or consume data from the Nostr network
|
||||||
|
let reader: SubscriptionManager
|
||||||
|
|
||||||
|
init(delegate: Delegate) {
|
||||||
|
self.delegate = delegate
|
||||||
|
let pool = RelayPool(ndb: delegate.ndb, keypair: delegate.keypair)
|
||||||
|
self.pool = pool
|
||||||
|
let reader = SubscriptionManager(pool: pool, ndb: delegate.ndb)
|
||||||
|
let userRelayList = UserRelayListManager(delegate: delegate, pool: pool, reader: reader)
|
||||||
|
self.reader = reader
|
||||||
|
self.userRelayList = userRelayList
|
||||||
|
self.postbox = PostBox(pool: pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Control functions
|
||||||
|
|
||||||
|
/// Connects the app to the Nostr network
|
||||||
|
func connect() {
|
||||||
|
self.userRelayList.connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Helper types
|
||||||
|
|
||||||
|
extension NostrNetworkManager {
|
||||||
|
/// The delegate that provides information and structure for the `NostrNetworkManager` to function.
|
||||||
|
///
|
||||||
|
/// ## Implementation notes
|
||||||
|
///
|
||||||
|
/// This is needed to prevent a circular reference between `DamusState` and `NostrNetworkManager`, and reduce coupling.
|
||||||
|
protocol Delegate: Sendable {
|
||||||
|
/// NostrDB instance, used with `RelayPool` to send events for ingestion.
|
||||||
|
var ndb: Ndb { get }
|
||||||
|
|
||||||
|
/// The keypair to use for relay authentication and updating relay lists
|
||||||
|
var keypair: Keypair { get }
|
||||||
|
|
||||||
|
/// The latest relay list event id hex
|
||||||
|
var latestRelayListEventIdHex: String? { get set } // TODO: Update this once we have full NostrDB query support
|
||||||
|
|
||||||
|
/// The latest contact list `NostrEvent`
|
||||||
|
///
|
||||||
|
/// Note: Read-only access, because `NostrNetworkManager` does not manage contact lists.
|
||||||
|
var latestContactListEvent: NostrEvent? { get }
|
||||||
|
|
||||||
|
/// Default bootstrap relays to start with when a user relay list is not present
|
||||||
|
var bootstrapRelays: [RelayURL] { get }
|
||||||
|
|
||||||
|
/// Whether the app is in developer mode
|
||||||
|
var developerMode: Bool { get }
|
||||||
|
|
||||||
|
/// The cache of relay model information
|
||||||
|
var relayModelCache: RelayModelCache { get }
|
||||||
|
|
||||||
|
/// Relay filters
|
||||||
|
var relayFilters: RelayFilters { get }
|
||||||
|
|
||||||
|
/// The user's connected NWC wallet
|
||||||
|
var nwcWallet: WalletConnectURL? { get }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
//
|
||||||
|
// SubscriptionManager.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-03-25.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension NostrNetworkManager {
|
||||||
|
/// Reads or fetches information from RelayPool and NostrDB, and provides an easier and unified higher-level interface.
|
||||||
|
///
|
||||||
|
/// ## Implementation notes
|
||||||
|
///
|
||||||
|
/// - This class will be a key part of the local relay model migration. Most higher-level code should fetch content from this class, which will properly setup the correct relay pool subscriptions, and provide a stream from NostrDB for higher performance and reliability.
|
||||||
|
class SubscriptionManager {
|
||||||
|
private let pool: RelayPool
|
||||||
|
private var ndb: Ndb
|
||||||
|
|
||||||
|
init(pool: RelayPool, ndb: Ndb) {
|
||||||
|
self.pool = pool
|
||||||
|
self.ndb = ndb
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Reading data from Nostr
|
||||||
|
|
||||||
|
/// Subscribes to data from the user's relays
|
||||||
|
///
|
||||||
|
/// ## Implementation notes
|
||||||
|
///
|
||||||
|
/// - When we migrate to the local relay model, we should modify this function to stream directly from NostrDB
|
||||||
|
///
|
||||||
|
/// - Parameter filters: The nostr filters to specify what kind of data to subscribe to
|
||||||
|
/// - Returns: An async stream of nostr data
|
||||||
|
func subscribe(filters: [NostrFilter]) -> AsyncStream<StreamItem> {
|
||||||
|
return AsyncStream<StreamItem> { continuation in
|
||||||
|
let streamTask = Task {
|
||||||
|
for await item in self.pool.subscribe(filters: filters) {
|
||||||
|
switch item {
|
||||||
|
case .eose: continuation.yield(.eose)
|
||||||
|
case .event(let nostrEvent):
|
||||||
|
// At this point of the pipeline, if the note is valid it should have been processed and verified by NostrDB,
|
||||||
|
// in which case we should pull the note from NostrDB to ensure validity.
|
||||||
|
// However, NdbNotes are unowned, so we return a function where our callers can temporarily borrow the NostrDB note
|
||||||
|
let noteId = nostrEvent.id
|
||||||
|
let lender: NdbNoteLender = { lend in
|
||||||
|
guard let ndbNoteTxn = self.ndb.lookup_note(noteId) else {
|
||||||
|
throw NdbNoteLenderError.errorLoadingNote
|
||||||
|
}
|
||||||
|
guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else {
|
||||||
|
throw NdbNoteLenderError.errorLoadingNote
|
||||||
|
}
|
||||||
|
lend(unownedNote)
|
||||||
|
}
|
||||||
|
continuation.yield(.event(borrow: lender))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
streamTask.cancel() // Close the RelayPool stream when caller stops streaming
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StreamItem {
|
||||||
|
/// An event which can be borrowed from NostrDB
|
||||||
|
case event(borrow: NdbNoteLender)
|
||||||
|
/// The end of stored events
|
||||||
|
case eose
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//
|
||||||
|
// UserRelayListErrors.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-02-27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension NostrNetworkManager.UserRelayListManager {
|
||||||
|
/// Models an error that may occur when performing operations that change the user's relay list.
|
||||||
|
///
|
||||||
|
/// Callers to functions that throw this error SHOULD handle them in order to provide a better user experience.
|
||||||
|
enum UpdateError: Error {
|
||||||
|
/// The user is not authorized to change relay list, usually because the private key is missing.
|
||||||
|
case notAuthorizedToChangeRelayList
|
||||||
|
/// An error occurred when forming the relay list Nostr event.
|
||||||
|
case cannotFormRelayListEvent
|
||||||
|
/// Cannot add item to the relay list because the relay is already present in the list.
|
||||||
|
case relayAlreadyExists
|
||||||
|
/// Cannot update the relay list because we do not have the user's previous relay list.
|
||||||
|
///
|
||||||
|
/// Implementers must be careful not to overwrite the user's existing relay list if it exists somewhere else.
|
||||||
|
case noInitialRelayList
|
||||||
|
/// Cannot remove or update a specific relay because it is not on the relay list
|
||||||
|
case noSuchRelay
|
||||||
|
|
||||||
|
/// Convert `RelayPool.RelayError` into `UserRelayListUpdateError`
|
||||||
|
static func from(_ relayPoolError: RelayPool.RelayError) -> Self {
|
||||||
|
switch relayPoolError {
|
||||||
|
case .RelayAlreadyExists: return .relayAlreadyExists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var humanReadableError: ErrorView.UserPresentableError {
|
||||||
|
switch self {
|
||||||
|
case .notAuthorizedToChangeRelayList:
|
||||||
|
ErrorView.UserPresentableError(
|
||||||
|
user_visible_description: NSLocalizedString("You do not have permission to alter this relay list.", comment: "Human readable error description"),
|
||||||
|
tip: NSLocalizedString("Please make sure you have logged-in with your private key.", comment: "Human readable tip for error"),
|
||||||
|
technical_info: nil
|
||||||
|
)
|
||||||
|
case .cannotFormRelayListEvent:
|
||||||
|
ErrorView.UserPresentableError(
|
||||||
|
user_visible_description: NSLocalizedString("There was a problem creating the relay list event.", comment: "Human readable error description"),
|
||||||
|
tip: NSLocalizedString("Please try again later or contact support if the issue persists.", comment: "Human readable tip for error"),
|
||||||
|
technical_info: "Failed forming Nostr event for the relay list update."
|
||||||
|
)
|
||||||
|
case .relayAlreadyExists:
|
||||||
|
ErrorView.UserPresentableError(
|
||||||
|
user_visible_description: NSLocalizedString("This relay is already in your list.", comment: "Human readable tip for error"),
|
||||||
|
tip: NSLocalizedString("Check the address and/or the relay list.", comment: "Human readable tip for error"),
|
||||||
|
technical_info: nil
|
||||||
|
)
|
||||||
|
case .noInitialRelayList:
|
||||||
|
ErrorView.UserPresentableError(
|
||||||
|
user_visible_description: NSLocalizedString("No initial relay list available to update.", comment: "Human readable error description"),
|
||||||
|
tip: NSLocalizedString("Please go to Settings > First Aid > Repair relay list, or contact support.", comment: "Human readable tip for error"),
|
||||||
|
technical_info: "Missing initial relay list data for reference during update."
|
||||||
|
)
|
||||||
|
case .noSuchRelay:
|
||||||
|
ErrorView.UserPresentableError(
|
||||||
|
user_visible_description: NSLocalizedString("The specified relay that you are trying to udpate was not found in your relay list.", comment: "Human readable error description"),
|
||||||
|
tip: NSLocalizedString("This is an unexpected error, please contact support.", comment: "Human readable tip for error"),
|
||||||
|
technical_info: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoadingError: Error {
|
||||||
|
case relayListParseError
|
||||||
|
|
||||||
|
var humanReadableError: ErrorView.UserPresentableError {
|
||||||
|
switch self {
|
||||||
|
case .relayListParseError:
|
||||||
|
return ErrorView.UserPresentableError(
|
||||||
|
user_visible_description: NSLocalizedString("Your relay list appears to be broken, so we cannot connect you to your Nostr network.", comment: "Human readable error description for a failure to parse the relay list due to a bad relay list"),
|
||||||
|
tip: NSLocalizedString("Please contact support for further help.", comment: "Human readable tips for what to do for a failure to find the relay list"),
|
||||||
|
technical_info: "Relay list could not be parsed."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
//
|
||||||
|
// UserRelayListManager.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-02-27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension NostrNetworkManager {
|
||||||
|
/// Manages the user's relay list
|
||||||
|
///
|
||||||
|
/// - It can compute the user's current relay list
|
||||||
|
/// - It can compute the best relay list to connect to
|
||||||
|
/// - It can edit the user's relay list
|
||||||
|
class UserRelayListManager {
|
||||||
|
private var delegate: Delegate
|
||||||
|
private let pool: RelayPool
|
||||||
|
private let reader: SubscriptionManager
|
||||||
|
|
||||||
|
private var relayListObserverTask: Task<Void, Never>? = nil
|
||||||
|
private var walletUpdatesObserverTask: AnyCancellable? = nil
|
||||||
|
|
||||||
|
init(delegate: Delegate, pool: RelayPool, reader: SubscriptionManager) {
|
||||||
|
self.delegate = delegate
|
||||||
|
self.pool = pool
|
||||||
|
self.reader = reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Computing the relays to connect to
|
||||||
|
|
||||||
|
private func relaysToConnectTo() -> [RelayPool.RelayDescriptor] {
|
||||||
|
return self.computeRelaysToConnectTo(with: self.getBestEffortRelayList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func computeRelaysToConnectTo(with relayList: NIP65.RelayList) -> [RelayPool.RelayDescriptor] {
|
||||||
|
let regularRelayDescriptorList = relayList.toRelayDescriptors()
|
||||||
|
if let nwcWallet = delegate.nwcWallet {
|
||||||
|
return regularRelayDescriptorList + [.nwc(url: nwcWallet.relay)]
|
||||||
|
}
|
||||||
|
return regularRelayDescriptorList
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Getting the user's relay list
|
||||||
|
|
||||||
|
/// Gets the "best effort" relay list.
|
||||||
|
///
|
||||||
|
/// It attempts to get a relay list from the user. If one is not available, it uses the default bootstrap list.
|
||||||
|
///
|
||||||
|
/// This is always guaranteed to return a relay list.
|
||||||
|
func getBestEffortRelayList() -> NIP65.RelayList {
|
||||||
|
guard let userCurrentRelayList = self.getUserCurrentRelayList() else {
|
||||||
|
return NIP65.RelayList(relays: delegate.bootstrapRelays)
|
||||||
|
}
|
||||||
|
return userCurrentRelayList
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the user's current relay list.
|
||||||
|
///
|
||||||
|
/// It attempts to get a NIP-65 relay list from the local database, or falls back to a legacy list.
|
||||||
|
func getUserCurrentRelayList() -> NIP65.RelayList? {
|
||||||
|
if let latestRelayListEvent = try? self.getLatestNIP65RelayList() { return latestRelayListEvent }
|
||||||
|
if let latestRelayListEvent = try? self.getLatestKind3RelayList() { return latestRelayListEvent }
|
||||||
|
if let latestRelayListEvent = try? self.getLatestUserDefaultsRelayList() { return latestRelayListEvent }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the latest NIP-65 relay list from NostrDB.
|
||||||
|
///
|
||||||
|
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
|
||||||
|
///
|
||||||
|
/// - Returns: The latest NIP-65 relay list object
|
||||||
|
private func getLatestNIP65RelayList() throws(LoadingError) -> NIP65.RelayList? {
|
||||||
|
guard let latestRelayListEvent = self.getLatestNIP65RelayListEvent() else { return nil }
|
||||||
|
guard let list = try? NIP65.RelayList(event: latestRelayListEvent) else { throw .relayListParseError }
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the latest NIP-65 relay list event from NostrDB.
|
||||||
|
///
|
||||||
|
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
|
||||||
|
///
|
||||||
|
/// It is recommended to use this function only if the NostrEvent metadata is needed. For cases where only the relay list info is needed, use `getLatestNIP65RelayList` instead.
|
||||||
|
///
|
||||||
|
/// - Returns: The latest NIP-65 relay list NdbNote
|
||||||
|
private func getLatestNIP65RelayListEvent() -> NdbNote? {
|
||||||
|
guard let latestRelayListEventId = delegate.latestRelayListEventIdHex else { return nil }
|
||||||
|
guard let latestRelayListEventId = NoteId(hex: latestRelayListEventId) else { return nil }
|
||||||
|
return delegate.ndb.lookup_note(latestRelayListEventId)?.unsafeUnownedValue?.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the latest `kind:3` relay list from NostrDB.
|
||||||
|
///
|
||||||
|
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
|
||||||
|
private func getLatestKind3RelayList() throws(LoadingError) -> NIP65.RelayList? {
|
||||||
|
guard let latestContactListEvent = delegate.latestContactListEvent else { return nil }
|
||||||
|
guard let legacyContactList = try? NIP65.RelayList.fromLegacyContactList(latestContactListEvent) else { throw .relayListParseError }
|
||||||
|
return legacyContactList
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the latest relay list from `UserDefaults`
|
||||||
|
///
|
||||||
|
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
|
||||||
|
private func getLatestUserDefaultsRelayList() throws(LoadingError) -> NIP65.RelayList? {
|
||||||
|
let key = bootstrap_relays_setting_key(pubkey: delegate.keypair.pubkey)
|
||||||
|
guard let relays = UserDefaults.standard.stringArray(forKey: key) else { return nil }
|
||||||
|
let relayUrls = relays.compactMap({ RelayURL($0) })
|
||||||
|
if relayUrls.count == 0 { return nil }
|
||||||
|
return NIP65.RelayList(relays: relayUrls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Getting metadata from the user's relay list
|
||||||
|
|
||||||
|
/// Gets the creation date of the user's current relay list, with preference to NIP-65 relay lists
|
||||||
|
/// - Returns: The current relay list's creation date
|
||||||
|
private func getUserCurrentRelayListCreationDate() -> UInt32? {
|
||||||
|
if let latestNIP65RelayListEvent = self.getLatestNIP65RelayListEvent() { return latestNIP65RelayListEvent.created_at }
|
||||||
|
if let latestKind3RelayListEvent = delegate.latestContactListEvent { return latestKind3RelayListEvent.created_at }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Listening to and handling relay updates from the network
|
||||||
|
|
||||||
|
func connect() {
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
self.relayListObserverTask?.cancel()
|
||||||
|
self.relayListObserverTask = Task { await self.listenAndHandleRelayUpdates() }
|
||||||
|
self.walletUpdatesObserverTask?.cancel()
|
||||||
|
self.walletUpdatesObserverTask = handle_notify(.attached_wallet).sink { _ in self.load() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenAndHandleRelayUpdates() async {
|
||||||
|
let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey])
|
||||||
|
for await item in self.reader.subscribe(filters: [filter]) {
|
||||||
|
switch item {
|
||||||
|
case .event(borrow: let borrow): // Signature validity already ensured at this point
|
||||||
|
let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate()
|
||||||
|
try? borrow { note in
|
||||||
|
guard note.pubkey == self.delegate.keypair.pubkey else { return } // Ensure this new list was ours
|
||||||
|
guard note.createdAt > (currentRelayListCreationDate ?? 0) else { return } // Ensure this is a newer list
|
||||||
|
guard let relayList = try? NIP65.RelayList(event: note) else { return } // Ensure it is a valid NIP-65 list
|
||||||
|
|
||||||
|
try? self.set(userRelayList: relayList) // Set the validated list
|
||||||
|
}
|
||||||
|
case .eose: continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Editing the user's relay list
|
||||||
|
|
||||||
|
func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) throws(UpdateError) {
|
||||||
|
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
||||||
|
guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists }
|
||||||
|
var newList = currentUserRelayList.relays
|
||||||
|
newList[relay.url] = relay
|
||||||
|
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) throws(UpdateError) {
|
||||||
|
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
||||||
|
guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists }
|
||||||
|
try self.upsert(relay: relay, force: force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(relayURL: RelayURL, force: Bool = false) throws(UpdateError) {
|
||||||
|
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
||||||
|
guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay }
|
||||||
|
var newList = currentUserRelayList.relays
|
||||||
|
newList[relayURL] = nil
|
||||||
|
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(userRelayList: NIP65.RelayList) throws(UpdateError) {
|
||||||
|
guard let fullKeypair = delegate.keypair.to_full() else { throw .notAuthorizedToChangeRelayList }
|
||||||
|
guard let relayListEvent = userRelayList.toNostrEvent(keypair: fullKeypair) else { throw .cannotFormRelayListEvent }
|
||||||
|
|
||||||
|
self.apply(newRelayList: self.computeRelaysToConnectTo(with: userRelayList))
|
||||||
|
|
||||||
|
self.pool.send(.event(relayListEvent)) // This will send to NostrDB as well, which will locally save that NIP-65 event
|
||||||
|
self.delegate.latestRelayListEventIdHex = relayListEvent.id.hex() // Make sure we are able to recall this event from NostrDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Syncing our saved user relay list with the active `RelayPool`
|
||||||
|
|
||||||
|
/// Loads the current user relay list
|
||||||
|
func load() {
|
||||||
|
self.apply(newRelayList: self.relaysToConnectTo())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a new relay list into the active relay pool, making sure it matches the specified relay list.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - state: The state of the app
|
||||||
|
/// - newRelayList: The new relay list to be applied
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// ## Implementation notes
|
||||||
|
///
|
||||||
|
/// - This is `private` because syncing the user's saved relay list with the relay pool is `NostrNetworkManager`'s responsibility,
|
||||||
|
/// so we do not want other classes to forcibly load this.
|
||||||
|
private func apply(newRelayList: [RelayPool.RelayDescriptor]) {
|
||||||
|
let currentRelayList = self.pool.relays.map({ $0.descriptor })
|
||||||
|
|
||||||
|
var changed = false
|
||||||
|
let new_relay_filters = load_relay_filters(delegate.keypair.pubkey) == nil
|
||||||
|
|
||||||
|
for index in self.pool.relays.indices {
|
||||||
|
guard let newDescriptor = newRelayList.first(where: { $0.url == self.pool.relays[index].descriptor.url }) else { continue }
|
||||||
|
self.pool.relays[index].descriptor.info = newDescriptor.info
|
||||||
|
// Relay read-write configuration change does not need reconnection to the relay, so we do not set the `changed` flag.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Working with URL Sets for difference analysis
|
||||||
|
let currentRelayURLs = Set(currentRelayList.map { $0.url })
|
||||||
|
let newRelayURLs = Set(newRelayList.map { $0.url })
|
||||||
|
|
||||||
|
// Analyzing which relays to add or remove
|
||||||
|
let relaysToRemove = currentRelayURLs.subtracting(newRelayURLs)
|
||||||
|
let relaysToAdd = newRelayURLs.subtracting(currentRelayURLs)
|
||||||
|
|
||||||
|
// Remove relays not in the new list
|
||||||
|
relaysToRemove.forEach { url in
|
||||||
|
pool.remove_relay(url)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new relays from the new list
|
||||||
|
relaysToAdd.forEach { url in
|
||||||
|
guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return }
|
||||||
|
add_new_relay(
|
||||||
|
model_cache: delegate.relayModelCache,
|
||||||
|
relay_filters: delegate.relayFilters,
|
||||||
|
pool: pool,
|
||||||
|
descriptor: descriptor,
|
||||||
|
new_relay_filters: new_relay_filters,
|
||||||
|
logging_enabled: delegate.developerMode
|
||||||
|
)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
pool.connect()
|
||||||
|
notify(.relays_changed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper extensions
|
||||||
|
|
||||||
|
fileprivate extension NIP65.RelayList.RelayItem {
|
||||||
|
func toRelayDescriptor() -> RelayPool.RelayDescriptor {
|
||||||
|
return RelayPool.RelayDescriptor(url: self.url, info: self.rwConfiguration, variant: .regular) // NIP-65 relays are regular by definition.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension NIP65.RelayList {
|
||||||
|
func toRelayDescriptors() -> [RelayPool.RelayDescriptor] {
|
||||||
|
return self.relays.values.map({ $0.toRelayDescriptor() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper functions
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds a new relay, taking care of other tangential concerns, such as updating the relay model cache, configuring logging, etc
|
||||||
|
///
|
||||||
|
/// ## Implementation notes
|
||||||
|
///
|
||||||
|
/// 1. This function used to be in `HomeModel.swift` and moved here when `UserRelayListManager` was first implemented
|
||||||
|
/// 2. This is `fileprivate` because only `UserRelayListManager` should be able to manage the user's relay list and apply them to the `RelayPool`
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - model_cache: The relay model cache, that keeps metadata cached
|
||||||
|
/// - relay_filters: Relay filters
|
||||||
|
/// - pool: The relay pool to add this in
|
||||||
|
/// - descriptor: The description of the relay being added
|
||||||
|
/// - new_relay_filters: Whether to insert new relay filters
|
||||||
|
/// - logging_enabled: Whether logging is enabled
|
||||||
|
fileprivate func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayPool.RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) {
|
||||||
|
try? pool.add_relay(descriptor)
|
||||||
|
let url = descriptor.url
|
||||||
|
|
||||||
|
let relay_id = url
|
||||||
|
guard model_cache.model(withURL: url) == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.detached(priority: .background) {
|
||||||
|
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
let model = RelayModel(url, metadata: meta)
|
||||||
|
model_cache.insert(model: model)
|
||||||
|
|
||||||
|
if logging_enabled {
|
||||||
|
pool.setLog(model.log, for: relay_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is the first time adding filters, we should filter non-paid relays
|
||||||
|
if new_relay_filters && !meta.is_paid {
|
||||||
|
relay_filters.insert(timeline: .search, relay_id: relay_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,10 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state.settings.hellthread_notifications_disabled && ev.is_hellthread(max_pubkeys: state.settings.hellthread_notification_max_pubkeys) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Don't show notifications that match mute list.
|
// Don't show notifications that match mute list.
|
||||||
if state.mutelist_manager.is_event_muted(ev) {
|
if state.mutelist_manager.is_event_muted(ev) {
|
||||||
return false
|
return false
|
||||||
@@ -50,7 +54,14 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
|
|||||||
guard ev.age < EVENT_MAX_AGE_FOR_NOTIFICATION else {
|
guard ev.age < EVENT_MAX_AGE_FOR_NOTIFICATION else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't show notifications for future events.
|
||||||
|
// Allow notes that are created no more than 3 seconds in the future
|
||||||
|
// to account for natural clock skew between sender and receiver.
|
||||||
|
guard ev.age >= -3 else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,18 @@ import Foundation
|
|||||||
class ProfileModel: ObservableObject, Equatable {
|
class ProfileModel: ObservableObject, Equatable {
|
||||||
@Published var contacts: NostrEvent? = nil
|
@Published var contacts: NostrEvent? = nil
|
||||||
@Published var following: Int = 0
|
@Published var following: Int = 0
|
||||||
@Published var relays: [RelayURL: RelayInfo]? = nil
|
@Published var relay_list: NIP65.RelayList? = nil
|
||||||
|
@Published var legacy_relay_list: [RelayURL: LegacyKind3RelayRWConfiguration]? = nil
|
||||||
@Published var progress: Int = 0
|
@Published var progress: Int = 0
|
||||||
|
var relay_urls: [RelayURL]? {
|
||||||
|
if let relay_list {
|
||||||
|
return relay_list.relays.values.map({ $0.url })
|
||||||
|
}
|
||||||
|
if let legacy_relay_list {
|
||||||
|
return Array(legacy_relay_list.keys)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
private let MAX_SHARE_RELAYS = 4
|
private let MAX_SHARE_RELAYS = 4
|
||||||
|
|
||||||
@@ -22,8 +32,10 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
var seen_event: Set<NoteId> = Set()
|
var seen_event: Set<NoteId> = Set()
|
||||||
var sub_id = UUID().description
|
var sub_id = UUID().description
|
||||||
var prof_subid = UUID().description
|
var prof_subid = UUID().description
|
||||||
|
var conversations_subid = UUID().description
|
||||||
var findRelay_subid = UUID().description
|
var findRelay_subid = UUID().description
|
||||||
|
var conversation_events: Set<NoteId> = Set()
|
||||||
|
|
||||||
init(pubkey: Pubkey, damus: DamusState) {
|
init(pubkey: Pubkey, damus: DamusState) {
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.damus = damus
|
self.damus = damus
|
||||||
@@ -57,25 +69,45 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
|
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
|
||||||
damus.pool.unsubscribe(sub_id: sub_id)
|
damus.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||||
damus.pool.unsubscribe(sub_id: prof_subid)
|
damus.nostrNetwork.pool.unsubscribe(sub_id: prof_subid)
|
||||||
|
if pubkey != damus.pubkey {
|
||||||
|
damus.nostrNetwork.pool.unsubscribe(sub_id: conversations_subid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe() {
|
func subscribe() {
|
||||||
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
|
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
|
||||||
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
||||||
|
var relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey])
|
||||||
|
|
||||||
profile_filter.authors = [pubkey]
|
profile_filter.authors = [pubkey]
|
||||||
|
|
||||||
text_filter.authors = [pubkey]
|
text_filter.authors = [pubkey]
|
||||||
text_filter.limit = 500
|
text_filter.limit = 500
|
||||||
|
|
||||||
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
|
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
|
||||||
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
||||||
damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
damus.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
||||||
damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event)
|
damus.nostrNetwork.pool.subscribe(sub_id: prof_subid, filters: [profile_filter, relay_list_filter], handler: handle_event)
|
||||||
|
|
||||||
|
subscribe_to_conversations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func subscribe_to_conversations() {
|
||||||
|
// Only subscribe to conversation events if the profile is not us.
|
||||||
|
guard pubkey != damus.pubkey else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let conversation_kinds: [NostrKind] = [.text, .longform, .highlight]
|
||||||
|
let limit: UInt32 = 500
|
||||||
|
let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey])
|
||||||
|
let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey])
|
||||||
|
print("subscribing to conversation events from and to profile \(pubkey) with sub_id \(conversations_subid)")
|
||||||
|
damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
|
||||||
|
}
|
||||||
|
|
||||||
func handle_profile_contact_event(_ ev: NostrEvent) {
|
func handle_profile_contact_event(_ ev: NostrEvent) {
|
||||||
process_contact_event(state: damus, ev: ev)
|
process_contact_event(state: damus, ev: ev)
|
||||||
|
|
||||||
@@ -88,17 +120,10 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
|
|
||||||
self.contacts = ev
|
self.contacts = ev
|
||||||
self.following = count_pubkeys(ev.tags)
|
self.following = count_pubkeys(ev.tags)
|
||||||
self.relays = decode_json_relays(ev.content)
|
self.legacy_relay_list = decode_json_relays(ev.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_event(_ ev: NostrEvent) {
|
|
||||||
guard ev.should_show_event else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if seen_event.contains(ev.id) {
|
private func add_event(_ ev: NostrEvent) {
|
||||||
return
|
|
||||||
}
|
|
||||||
if ev.is_textlike || ev.known_kind == .boost {
|
if ev.is_textlike || ev.known_kind == .boost {
|
||||||
if self.events.insert(ev) {
|
if self.events.insert(ev) {
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
@@ -106,27 +131,63 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
} else if ev.known_kind == .contacts {
|
} else if ev.known_kind == .contacts {
|
||||||
handle_profile_contact_event(ev)
|
handle_profile_contact_event(ev)
|
||||||
}
|
}
|
||||||
|
else if ev.known_kind == .relay_list {
|
||||||
|
self.relay_list = try? NIP65.RelayList(event: ev) // Whether another user's list is malformatted is something beyond our control. Probably best to suppress errors
|
||||||
|
}
|
||||||
seen_event.insert(ev.id)
|
seen_event.insert(ev.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the event public key matches the public key(s) we are querying.
|
||||||
|
// This is done to protect against a relay not properly filtering events by the pubkey
|
||||||
|
// See https://github.com/damus-io/damus/issues/1846 for more information
|
||||||
|
private func relay_filtered_correctly(_ ev: NostrEvent, subid: String?) -> Bool {
|
||||||
|
if subid == self.conversations_subid {
|
||||||
|
switch ev.pubkey {
|
||||||
|
case self.pubkey:
|
||||||
|
return ev.referenced_pubkeys.contains(damus.pubkey)
|
||||||
|
case damus.pubkey:
|
||||||
|
return ev.referenced_pubkeys.contains(self.pubkey)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.pubkey == ev.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||||
switch ev {
|
switch ev {
|
||||||
case .ws_event:
|
case .ws_event:
|
||||||
return
|
return
|
||||||
case .nostr_event(let resp):
|
case .nostr_event(let resp):
|
||||||
guard resp.subid == self.sub_id || resp.subid == self.prof_subid else {
|
guard resp.subid == self.sub_id || resp.subid == self.prof_subid || resp.subid == self.conversations_subid else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch resp {
|
switch resp {
|
||||||
case .ok:
|
case .ok:
|
||||||
break
|
break
|
||||||
case .event(_, let ev):
|
case .event(_, let ev):
|
||||||
// Ensure the event public key matches this profiles public key
|
guard ev.should_show_event else {
|
||||||
// This is done to protect against a relay not properly filtering events by the pubkey
|
break
|
||||||
// See https://github.com/damus-io/damus/issues/1846 for more information
|
}
|
||||||
guard self.pubkey == ev.pubkey else { break }
|
|
||||||
|
|
||||||
add_event(ev)
|
if !seen_event.contains(ev.id) {
|
||||||
|
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
add_event(ev)
|
||||||
|
|
||||||
|
if resp.subid == self.conversations_subid {
|
||||||
|
conversation_events.insert(ev.id)
|
||||||
|
}
|
||||||
|
} else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) {
|
||||||
|
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation_events.insert(ev.id)
|
||||||
|
}
|
||||||
case .notice:
|
case .notice:
|
||||||
break
|
break
|
||||||
//notify(.notice, notice)
|
//notify(.notice, notice)
|
||||||
@@ -145,7 +206,7 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
|
|
||||||
private func findRelaysHandler(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
private func findRelaysHandler(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||||
if case .nostr_event(let resp) = ev, case .event(_, let event) = resp, case .contacts = event.known_kind {
|
if case .nostr_event(let resp) = ev, case .event(_, let event) = resp, case .contacts = event.known_kind {
|
||||||
self.relays = decode_json_relays(event.content)
|
self.legacy_relay_list = decode_json_relays(event.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,15 +214,15 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
var profile_filter = NostrFilter(kinds: [.contacts])
|
var profile_filter = NostrFilter(kinds: [.contacts])
|
||||||
profile_filter.authors = [pubkey]
|
profile_filter.authors = [pubkey]
|
||||||
|
|
||||||
damus.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler)
|
damus.nostrNetwork.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribeFindRelays() {
|
func unsubscribeFindRelays() {
|
||||||
damus.pool.unsubscribe(sub_id: findRelay_subid)
|
damus.nostrNetwork.pool.unsubscribe(sub_id: findRelay_subid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCappedRelayStrings() -> [String] {
|
func getCappedRelayStrings() -> [String] {
|
||||||
return relays?.keys.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
|
return self.relay_urls?.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
// Minimum threshold the hellthread pubkey tag count setting can go down to.
|
||||||
|
let HELLTHREAD_MIN_PUBKEYS: Int = 6
|
||||||
|
|
||||||
|
// Maximum threshold the hellthread pubkey tag count setting can go up to.
|
||||||
|
let HELLTHREAD_MAX_PUBKEYS: Int = 24
|
||||||
|
|
||||||
struct PushNotificationClient {
|
struct PushNotificationClient {
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
let settings: UserSettingsStore
|
let settings: UserSettingsStore
|
||||||
@@ -175,15 +181,33 @@ extension PushNotificationClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct NotificationSettings: Codable, Equatable {
|
struct NotificationSettings: Codable, Equatable {
|
||||||
let zap_notifications_enabled: Bool
|
let zap_notifications_enabled: Bool?
|
||||||
let mention_notifications_enabled: Bool
|
let mention_notifications_enabled: Bool?
|
||||||
let repost_notifications_enabled: Bool
|
let repost_notifications_enabled: Bool?
|
||||||
let reaction_notifications_enabled: Bool
|
let reaction_notifications_enabled: Bool?
|
||||||
let dm_notifications_enabled: Bool
|
let dm_notifications_enabled: Bool?
|
||||||
let only_notifications_from_following_enabled: Bool
|
let only_notifications_from_following_enabled: Bool?
|
||||||
|
let hellthread_notifications_disabled: Bool?
|
||||||
|
let hellthread_notifications_max_pubkeys: Int?
|
||||||
|
|
||||||
static func from(json_data: Data) -> Self? {
|
static func from(json_data: Data) -> Self? {
|
||||||
guard let decoded = try? JSONDecoder().decode(Self.self, from: json_data) else { return nil }
|
guard let decoded = try? JSONDecoder().decode(Self.self, from: json_data) else { return nil }
|
||||||
|
|
||||||
|
// Normalize hellthread_notifications_max_pubkeys in case
|
||||||
|
// it goes beyond the expected range supported on the client.
|
||||||
|
if let max_pubkeys = decoded.hellthread_notifications_max_pubkeys, max_pubkeys < HELLTHREAD_MIN_PUBKEYS || max_pubkeys > HELLTHREAD_MAX_PUBKEYS {
|
||||||
|
return NotificationSettings(
|
||||||
|
zap_notifications_enabled: decoded.zap_notifications_enabled,
|
||||||
|
mention_notifications_enabled: decoded.mention_notifications_enabled,
|
||||||
|
repost_notifications_enabled: decoded.repost_notifications_enabled,
|
||||||
|
reaction_notifications_enabled: decoded.reaction_notifications_enabled,
|
||||||
|
dm_notifications_enabled: decoded.dm_notifications_enabled,
|
||||||
|
only_notifications_from_following_enabled: decoded.only_notifications_from_following_enabled,
|
||||||
|
hellthread_notifications_disabled: decoded.hellthread_notifications_disabled,
|
||||||
|
hellthread_notifications_max_pubkeys: max(min(HELLTHREAD_MAX_PUBKEYS, max_pubkeys), HELLTHREAD_MIN_PUBKEYS)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return decoded
|
return decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +218,9 @@ extension PushNotificationClient {
|
|||||||
repost_notifications_enabled: settings.repost_notification,
|
repost_notifications_enabled: settings.repost_notification,
|
||||||
reaction_notifications_enabled: settings.like_notification,
|
reaction_notifications_enabled: settings.like_notification,
|
||||||
dm_notifications_enabled: settings.dm_notification,
|
dm_notifications_enabled: settings.dm_notification,
|
||||||
only_notifications_from_following_enabled: settings.notification_only_from_following
|
only_notifications_from_following_enabled: settings.notification_only_from_following,
|
||||||
|
hellthread_notifications_disabled: settings.hellthread_notifications_disabled,
|
||||||
|
hellthread_notifications_max_pubkeys: settings.hellthread_notification_max_pubkeys
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ class SearchHomeModel: ObservableObject {
|
|||||||
|
|
||||||
func subscribe() {
|
func subscribe() {
|
||||||
loading = true
|
loading = true
|
||||||
let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters)
|
let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters)
|
||||||
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
|
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe(to: RelayURL? = nil) {
|
func unsubscribe(to: RelayURL? = nil) {
|
||||||
loading = false
|
loading = false
|
||||||
damus_state.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
|
damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
|
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
|
||||||
@@ -140,7 +140,7 @@ func load_profiles<Y>(context: String, profiles_subid: String, relay_id: RelayUR
|
|||||||
|
|
||||||
let filter = NostrFilter(kinds: [.metadata], authors: authors)
|
let filter = NostrFilter(kinds: [.metadata], authors: authors)
|
||||||
|
|
||||||
damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in
|
damus_state.nostrNetwork.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in
|
||||||
|
|
||||||
let now = UInt64(Date.now.timeIntervalSince1970)
|
let now = UInt64(Date.now.timeIntervalSince1970)
|
||||||
switch conn_ev {
|
switch conn_ev {
|
||||||
@@ -156,7 +156,7 @@ func load_profiles<Y>(context: String, profiles_subid: String, relay_id: RelayUR
|
|||||||
}
|
}
|
||||||
case .eose:
|
case .eose:
|
||||||
print("load_profiles[\(context)]: done loading \(authors.count) profiles from \(relay_id)")
|
print("load_profiles[\(context)]: done loading \(authors.count) profiles from \(relay_id)")
|
||||||
damus_state.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id])
|
damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id])
|
||||||
case .ok:
|
case .ok:
|
||||||
break
|
break
|
||||||
case .notice:
|
case .notice:
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ class SearchModel: ObservableObject {
|
|||||||
//likes_filter.ids = ref_events.referenced_ids!
|
//likes_filter.ids = ref_events.referenced_ids!
|
||||||
|
|
||||||
print("subscribing to search '\(search)' with sub_id \(sub_id)")
|
print("subscribing to search '\(search)' with sub_id \(sub_id)")
|
||||||
state.pool.register_handler(sub_id: sub_id, handler: handle_event)
|
state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
|
||||||
loading = true
|
loading = true
|
||||||
state.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
|
state.nostrNetwork.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
state.pool.unsubscribe(sub_id: sub_id)
|
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||||
loading = false
|
loading = false
|
||||||
print("unsubscribing from search '\(search)' with sub_id \(sub_id)")
|
print("unsubscribing from search '\(search)' with sub_id \(sub_id)")
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ class SearchModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||||
let (sub_id, done) = handle_subid_event(pool: state.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
||||||
if ev.is_textlike && ev.should_show_event {
|
if ev.is_textlike && ev.should_show_event {
|
||||||
self.add_event(ev)
|
self.add_event(ev)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,18 @@ class ThreadModel: ObservableObject {
|
|||||||
///
|
///
|
||||||
/// This is a computed property because we then don't need to worry about keeping things in sync
|
/// This is a computed property because we then don't need to worry about keeping things in sync
|
||||||
var parent_events: [NostrEvent] {
|
var parent_events: [NostrEvent] {
|
||||||
return event_map.parent_events(of: selected_event)
|
// This block of code helps ensure `ThreadEventMap` stays in sync with `EventCache`
|
||||||
|
let parent_events_from_cache = damus_state.events.parent_events(event: selected_event, keypair: damus_state.keypair)
|
||||||
|
for parent_event in parent_events_from_cache {
|
||||||
|
add_event(
|
||||||
|
parent_event,
|
||||||
|
keypair: damus_state.keypair,
|
||||||
|
look_for_parent_events: false, // We have all parents we need for now
|
||||||
|
publish_changes: false // Publishing changes during a view render is problematic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent_events_from_cache
|
||||||
}
|
}
|
||||||
/// All of the direct and indirect replies of `selected_event` in the thread. sorted chronologically
|
/// All of the direct and indirect replies of `selected_event` in the thread. sorted chronologically
|
||||||
///
|
///
|
||||||
@@ -77,12 +88,12 @@ class ThreadModel: ObservableObject {
|
|||||||
|
|
||||||
/// Unsubscribe from events in the relay pool. Call this when unloading the view
|
/// Unsubscribe from events in the relay pool. Call this when unloading the view
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
self.damus_state.pool.remove_handler(sub_id: base_subid)
|
self.damus_state.nostrNetwork.pool.remove_handler(sub_id: base_subid)
|
||||||
self.damus_state.pool.remove_handler(sub_id: meta_subid)
|
self.damus_state.nostrNetwork.pool.remove_handler(sub_id: meta_subid)
|
||||||
self.damus_state.pool.remove_handler(sub_id: profiles_subid)
|
self.damus_state.nostrNetwork.pool.remove_handler(sub_id: profiles_subid)
|
||||||
self.damus_state.pool.unsubscribe(sub_id: base_subid)
|
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid)
|
||||||
self.damus_state.pool.unsubscribe(sub_id: meta_subid)
|
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: meta_subid)
|
||||||
self.damus_state.pool.unsubscribe(sub_id: profiles_subid)
|
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid)
|
||||||
Log.info("unsubscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
Log.info("unsubscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,14 +129,20 @@ class ThreadModel: ObservableObject {
|
|||||||
let meta_filters = [meta_events, quote_events]
|
let meta_filters = [meta_events, quote_events]
|
||||||
|
|
||||||
Log.info("subscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
Log.info("subscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
||||||
damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
|
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
|
||||||
damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
|
damus_state.nostrNetwork.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds an event to this thread.
|
/// Adds an event to this thread.
|
||||||
/// Normally this does not need to be called externally because it is the responsibility of this class to load the events, not the view's.
|
/// Normally this does not need to be called externally because it is the responsibility of this class to load the events, not the view's.
|
||||||
/// However, this can be called externally for testing purposes (e.g. injecting events for testing)
|
/// However, this can be called externally for testing purposes (e.g. injecting events for testing)
|
||||||
func add_event(_ ev: NostrEvent, keypair: Keypair) {
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - ev: The event to add into the thread event map
|
||||||
|
/// - keypair: The user's keypair
|
||||||
|
/// - look_for_parent_events: Whether to search for parent events of the input event in NostrDB
|
||||||
|
/// - publish_changes: Whether to publish changes at the end
|
||||||
|
func add_event(_ ev: NostrEvent, keypair: Keypair, look_for_parent_events: Bool = true, publish_changes: Bool = true) {
|
||||||
if event_map.contains(id: ev.id) {
|
if event_map.contains(id: ev.id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -136,8 +153,22 @@ class ThreadModel: ObservableObject {
|
|||||||
|
|
||||||
event_map.add(event: ev)
|
event_map.add(event: ev)
|
||||||
|
|
||||||
// Publish changes
|
if look_for_parent_events {
|
||||||
objectWillChange.send()
|
// Add all parent events that we have on EventCache (and subsequently on NostrDB)
|
||||||
|
// This helps ensure we include as many locally-stored notes as possible — even on poor networking conditions
|
||||||
|
damus_state.events.parent_events(event: ev, keypair: damus_state.keypair).forEach {
|
||||||
|
add_event(
|
||||||
|
$0, // The `lookup` function in `parent_events` turns the event into an "owned" object, so we do not need to clone here
|
||||||
|
keypair: damus_state.keypair,
|
||||||
|
look_for_parent_events: false, // We do not need deep recursion
|
||||||
|
publish_changes: false // Do not publish changes multiple times
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publish_changes {
|
||||||
|
objectWillChange.send()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles an incoming event from a relay pool
|
/// Handles an incoming event from a relay pool
|
||||||
@@ -145,7 +176,7 @@ class ThreadModel: ObservableObject {
|
|||||||
/// Marked as private because it is this class' responsibility to load events, not the view's. Simplify the interface
|
/// Marked as private because it is this class' responsibility to load events, not the view's. Simplify the interface
|
||||||
@MainActor
|
@MainActor
|
||||||
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||||
let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in
|
let (sub_id, done) = handle_subid_event(pool: damus_state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sid, ev in
|
||||||
guard subids.contains(sid) else {
|
guard subids.contains(sid) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,13 @@ class UserSettingsStore: ObservableObject {
|
|||||||
|
|
||||||
@Setting(key: "notification_only_from_following", default_value: false)
|
@Setting(key: "notification_only_from_following", default_value: false)
|
||||||
var notification_only_from_following: Bool
|
var notification_only_from_following: Bool
|
||||||
|
|
||||||
|
@Setting(key: "hellthread_notifications_disabled", default_value: false)
|
||||||
|
var hellthread_notifications_disabled: Bool
|
||||||
|
|
||||||
|
@Setting(key: "hellthread_notification_max_pubkeys", default_value: DEFAULT_HELLTHREAD_MAX_PUBKEYS)
|
||||||
|
var hellthread_notification_max_pubkeys: Int
|
||||||
|
|
||||||
@Setting(key: "translate_dms", default_value: false)
|
@Setting(key: "translate_dms", default_value: false)
|
||||||
var translate_dms: Bool
|
var translate_dms: Bool
|
||||||
|
|
||||||
@@ -336,6 +342,10 @@ class UserSettingsStore: ObservableObject {
|
|||||||
@Setting(key: "draft_event_ids", default_value: nil)
|
@Setting(key: "draft_event_ids", default_value: nil)
|
||||||
var draft_event_ids: [String]?
|
var draft_event_ids: [String]?
|
||||||
|
|
||||||
|
// TODO: Get rid of this once we have NostrDB query capabilities integrated
|
||||||
|
@Setting(key: "latest_relay_list_event_id", default_value: nil)
|
||||||
|
var latestRelayListEventIdHex: String?
|
||||||
|
|
||||||
// MARK: Helper types
|
// MARK: Helper types
|
||||||
|
|
||||||
enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
|
enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
|
||||||
|
|||||||
@@ -13,10 +13,17 @@ enum WalletConnectState {
|
|||||||
case none
|
case none
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Models and manages the user's NWC wallet based on the app's settings
|
||||||
class WalletModel: ObservableObject {
|
class WalletModel: ObservableObject {
|
||||||
var settings: UserSettingsStore
|
var settings: UserSettingsStore
|
||||||
private(set) var previous_state: WalletConnectState
|
private(set) var previous_state: WalletConnectState
|
||||||
var initial_percent: Int
|
var initial_percent: Int
|
||||||
|
/// The wallet's balance, in sats.
|
||||||
|
/// Starts with `nil` to signify it is not loaded yet
|
||||||
|
@Published private(set) var balance: Int64? = nil
|
||||||
|
/// The list of NWC transactions made in the wallet
|
||||||
|
/// Starts with `nil` to signify it is not loaded yet
|
||||||
|
@Published private(set) var transactions: [WalletConnect.Transaction]? = nil
|
||||||
|
|
||||||
@Published private(set) var connect_state: WalletConnectState
|
@Published private(set) var connect_state: WalletConnectState
|
||||||
|
|
||||||
@@ -61,4 +68,27 @@ class WalletModel: ObservableObject {
|
|||||||
self.connect_state = .existing(nwc)
|
self.connect_state = .existing(nwc)
|
||||||
self.previous_state = .existing(nwc)
|
self.previous_state = .existing(nwc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles an NWC response event and updates the model.
|
||||||
|
///
|
||||||
|
/// This takes a response received from the NWC relay and updates the internal state of this model.
|
||||||
|
///
|
||||||
|
/// - Parameter response: The NWC response received from the network
|
||||||
|
func handle_nwc_response(response: WalletConnect.FullWalletResponse) {
|
||||||
|
switch response.response.result {
|
||||||
|
case .get_balance(let balanceResp):
|
||||||
|
self.balance = balanceResp.balance / 1000
|
||||||
|
case .none:
|
||||||
|
return
|
||||||
|
case .some(.pay_invoice(_)):
|
||||||
|
return
|
||||||
|
case .list_transactions(let transactionsResp):
|
||||||
|
self.transactions = transactionsResp.transactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetWalletStateInformation() {
|
||||||
|
self.transactions = nil
|
||||||
|
self.balance = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ class ZapsModel: ObservableObject {
|
|||||||
case .note(let note_target):
|
case .note(let note_target):
|
||||||
filter.referenced_ids = [note_target.note_id]
|
filter.referenced_ids = [note_target.note_id]
|
||||||
}
|
}
|
||||||
state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
|
state.nostrNetwork.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
state.pool.unsubscribe(sub_id: zaps_subid)
|
state.nostrNetwork.pool.unsubscribe(sub_id: zaps_subid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// NIP04.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-03-10.
|
||||||
|
//
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Functions and utilities for the NIP-04 spec
|
||||||
|
struct NIP04 {}
|
||||||
|
|
||||||
|
extension NIP04 {
|
||||||
|
/// Encrypts a message using NIP-04.
|
||||||
|
static func encrypt_message(message: String, privkey: Privkey, to_pk: Pubkey, encoding: EncEncoding = .base64) -> String? {
|
||||||
|
let iv = random_bytes(count: 16).bytes
|
||||||
|
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let utf8_message = Data(message.utf8).bytes
|
||||||
|
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch encoding {
|
||||||
|
case .base64:
|
||||||
|
return encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||||
|
case .bech32:
|
||||||
|
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an event with encrypted `contents` field, using NIP-04
|
||||||
|
static func create_encrypted_event(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: FullKeypair, created_at: UInt32, kind: UInt32) -> NostrEvent? {
|
||||||
|
let privkey = keypair.privkey
|
||||||
|
|
||||||
|
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return NostrEvent(content: enc_content, keypair: keypair.to_keypair(), kind: kind, tags: tags, createdAt: created_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a NIP-04 style direct message event
|
||||||
|
static func create_dm(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent?
|
||||||
|
{
|
||||||
|
let created = created_at ?? UInt32(Date().timeIntervalSince1970)
|
||||||
|
|
||||||
|
guard let keypair = keypair.to_full() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return create_encrypted_event(message, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created, kind: 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
//
|
||||||
|
// NIP65.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-02-21.
|
||||||
|
//
|
||||||
|
// Some text excerpts taken from the Nostr Protocol itself (which are public domain)
|
||||||
|
|
||||||
|
import OrderedCollections
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Includes models and functions for working with NIP-65
|
||||||
|
struct NIP65: Sendable {}
|
||||||
|
|
||||||
|
extension NIP65 {
|
||||||
|
/// Models a NIP-65 relay list
|
||||||
|
struct RelayList: NostrEventConvertible, Sendable {
|
||||||
|
let relays: OrderedDictionary<RelayURL, RelayItem>
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
init(event: NdbNote) throws(NIP65DecodingError) {
|
||||||
|
try self.init(event: UnownedNdbNote(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
init(event: borrowing UnownedNdbNote) throws(NIP65DecodingError) {
|
||||||
|
guard event.known_kind == .relay_list else { throw .notRelayList }
|
||||||
|
var relays: [RelayItem] = []
|
||||||
|
for tag in event.tags {
|
||||||
|
guard let relay = try RelayItem.fromTag(tag: tag) else { continue }
|
||||||
|
relays.append(relay)
|
||||||
|
}
|
||||||
|
self.relays = Self.relayOrderedDictionary(from: relays)
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(event: NdbNote?) throws(NIP65DecodingError) {
|
||||||
|
guard let event else { return nil }
|
||||||
|
try self.init(event: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(relays: [RelayItem]) {
|
||||||
|
self.relays = Self.relayOrderedDictionary(from: relays)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(relays: [RelayURL]) {
|
||||||
|
let relayItemList = relays.map({ RelayItem(url: $0, rwConfiguration: .readWrite) })
|
||||||
|
self.relays = Self.relayOrderedDictionary(from: relayItemList)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func relayOrderedDictionary(from relayList: [RelayItem]) -> OrderedDictionary<RelayURL, RelayItem> {
|
||||||
|
var seenUrls: Set<RelayURL> = []
|
||||||
|
return OrderedDictionary(uniqueKeysWithValues: relayList.compactMap({
|
||||||
|
// We need to ensure the keys are unique to avoid assertion errors from OrderedDictionary
|
||||||
|
guard !seenUrls.contains($0.url) else { return nil }
|
||||||
|
seenUrls.insert($0.url)
|
||||||
|
return ($0.url, $0)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Conversion to a Nostr Event
|
||||||
|
|
||||||
|
func toNostrEvent(keypair: FullKeypair, timestamp: UInt32? = nil) -> NostrEvent? {
|
||||||
|
return NdbNote(
|
||||||
|
content: "",
|
||||||
|
keypair: keypair.to_keypair(),
|
||||||
|
kind: NostrKind.relay_list.rawValue,
|
||||||
|
tags: self.relays.values.map({ $0.tag }),
|
||||||
|
createdAt: timestamp ?? UInt32(Date.now.timeIntervalSince1970)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NIP65 {
|
||||||
|
/// An error thrown when decoding an item into a NIP-65 relay list
|
||||||
|
enum NIP65DecodingError: Error {
|
||||||
|
/// The Nostr event being converted is not a NIP-65 relay list
|
||||||
|
case notRelayList
|
||||||
|
/// The relay URL is invalid
|
||||||
|
case invalidRelayURL
|
||||||
|
///The relay RW marker is invalid
|
||||||
|
case invalidRelayMarker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NIP65.RelayList {
|
||||||
|
/// An item referencing a relay and its configuration inside a relay list
|
||||||
|
struct RelayItem: ThrowingTagConvertible, Sendable {
|
||||||
|
typealias E = NIP65.NIP65DecodingError
|
||||||
|
|
||||||
|
let url: RelayURL
|
||||||
|
let rwConfiguration: RWConfiguration
|
||||||
|
|
||||||
|
/// The raw tag sequence in a Nostr event
|
||||||
|
var tag: [String] {
|
||||||
|
var tag = ["r", url.absoluteString]
|
||||||
|
if let rwMarker = rwConfiguration.tagItem { tag.append(rwMarker) }
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a new relay item from a Nostr event's tag sequence
|
||||||
|
static func fromTag(tag: TagSequence) throws(E) -> NIP65.RelayList.RelayItem? {
|
||||||
|
var i = tag.makeIterator()
|
||||||
|
|
||||||
|
guard tag.count >= 2,
|
||||||
|
let t0 = i.next(),
|
||||||
|
let key = t0.single_char,
|
||||||
|
let rkey = RefId.RefKey(rawValue: key),
|
||||||
|
let t1 = i.next()
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
let t2 = i.next()
|
||||||
|
|
||||||
|
switch rkey {
|
||||||
|
case .r: return try self.fromRawInfo(urlString: t1.string(), rwMarker: t2?.string())
|
||||||
|
// Keep options explicit to make compiler prompt developer on whether to ignore or handle new future options
|
||||||
|
case .e, .p, .q, .t, .d, .a: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes a Relay Item based on raw information
|
||||||
|
static func fromRawInfo(urlString: String, rwMarker: String?) throws(NIP65.NIP65DecodingError) -> NIP65.RelayList.RelayItem? {
|
||||||
|
guard let relayUrl = RelayURL(urlString) else { throw .invalidRelayURL }
|
||||||
|
guard let rwConfiguration = RWConfiguration.fromTagItem(rwMarker) else { throw .invalidRelayMarker }
|
||||||
|
return NIP65.RelayList.RelayItem(url: relayUrl, rwConfiguration: rwConfiguration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NIP65.RelayList.RelayItem {
|
||||||
|
/// The read/write configuration for a relay item
|
||||||
|
enum RWConfiguration: TagItemConvertible {
|
||||||
|
case read
|
||||||
|
case write
|
||||||
|
case readWrite
|
||||||
|
|
||||||
|
static let READ_MARKER: String = "read"
|
||||||
|
static let WRITE_MARKER: String = "write"
|
||||||
|
|
||||||
|
var canRead: Bool {
|
||||||
|
switch self {
|
||||||
|
case .read, .readWrite: return true
|
||||||
|
case .write: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var canWrite: Bool {
|
||||||
|
switch self {
|
||||||
|
case .write, .readWrite: return true
|
||||||
|
case .read: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A raw Nostr Event tag item
|
||||||
|
var tagItem: String? {
|
||||||
|
switch self {
|
||||||
|
case .read: Self.READ_MARKER
|
||||||
|
case .write: Self.WRITE_MARKER
|
||||||
|
case .readWrite: nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize this from a raw Nostr Event tag item
|
||||||
|
static func fromTagItem(_ item: String?) -> Self? {
|
||||||
|
if item == READ_MARKER { return .read }
|
||||||
|
if item == WRITE_MARKER { return .write }
|
||||||
|
return .readWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,19 @@ protocol TagConvertible {
|
|||||||
static func from_tag(tag: TagSequence) -> Self?
|
static func from_tag(tag: TagSequence) -> Self?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Protocol for types that can be converted from/to a tag sequence with the possibilty of an error
|
||||||
|
protocol ThrowingTagConvertible {
|
||||||
|
associatedtype E: Error
|
||||||
|
var tag: [String] { get }
|
||||||
|
static func fromTag(tag: TagSequence) throws(E) -> Self?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Protocol for types that can be converted from/to a tag item
|
||||||
|
protocol TagItemConvertible {
|
||||||
|
var tagItem: String? { get }
|
||||||
|
static func fromTagItem(_ item: String?) -> Self?
|
||||||
|
}
|
||||||
|
|
||||||
struct QuoteId: IdType, TagKey, TagConvertible {
|
struct QuoteId: IdType, TagKey, TagConvertible {
|
||||||
let id: Data
|
let id: Data
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ extension NdbProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func displayName(profile: Profile?, pubkey: Pubkey) -> DisplayName {
|
static func displayName(profile: Profile?, pubkey: Pubkey) -> DisplayName {
|
||||||
return parse_display_name(profile: profile, pubkey: pubkey)
|
return DisplayName(name: profile?.name, display_name: profile?.display_name, pubkey: pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
var damus_donation: Int? {
|
var damus_donation: Int? {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: Relay) -> NostrEvent? {
|
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: RelayPool.Relay) -> NostrEvent? {
|
||||||
let tags: [[String]] = [["relay", relay.descriptor.url.absoluteString],["challenge", challenge_string]]
|
let tags: [[String]] = [["relay", relay.descriptor.url.absoluteString],["challenge", challenge_string]]
|
||||||
let event = NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 22242, tags: tags)
|
let event = NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 22242, tags: tags)
|
||||||
return event
|
return event
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
|
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayPool.RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
|
||||||
var tags = zap_target_to_tags(target)
|
var tags = zap_target_to_tags(target)
|
||||||
var relay_tag = ["relays"]
|
var relay_tag = ["relays"]
|
||||||
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
||||||
@@ -68,7 +68,7 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
|
|||||||
|
|
||||||
guard let note = NostrEvent(content: message, keypair: identity.to_keypair(), kind: 9733, tags: tags),
|
guard let note = NostrEvent(content: message, keypair: identity.to_keypair(), kind: 9733, tags: tags),
|
||||||
let note_json = encode_json(note),
|
let note_json = encode_json(note),
|
||||||
let enc = encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
let enc = NIP04.encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -78,8 +78,8 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
|
|||||||
|
|
||||||
func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
||||||
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
|
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
|
||||||
let rw_relay_info = RelayInfo(read: true, write: true)
|
let rw_relay_info = LegacyKind3RelayRWConfiguration(read: true, write: true)
|
||||||
var relays: [RelayURL: RelayInfo] = [:]
|
var relays: [RelayURL: LegacyKind3RelayRWConfiguration] = [:]
|
||||||
|
|
||||||
for relay in bootstrap_relays {
|
for relay in bootstrap_relays {
|
||||||
relays[relay] = rw_relay_info
|
relays[relay] = rw_relay_info
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ import CryptoKit
|
|||||||
import NaturalLanguage
|
import NaturalLanguage
|
||||||
|
|
||||||
|
|
||||||
|
/// A protocol for structs and classes that can convert themselves from/to a NostrEvent
|
||||||
|
protocol NostrEventConvertible {
|
||||||
|
associatedtype E: Error
|
||||||
|
|
||||||
|
/// Iniitialize this type from a NostrEvent
|
||||||
|
init(event: NostrEvent) throws(E)
|
||||||
|
|
||||||
|
/// Convert this type into a Nostr Event, using a keypair for signing and a specific timestamp
|
||||||
|
func toNostrEvent(keypair: FullKeypair, timestamp: UInt32?) -> NostrEvent?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
enum ValidationResult: Decodable {
|
enum ValidationResult: Decodable {
|
||||||
case unknown
|
case unknown
|
||||||
case ok
|
case ok
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
/// A known Nostr event kind, addressable by name, with the actual number assigned by the protocol as the value
|
||||||
enum NostrKind: UInt32, Codable {
|
enum NostrKind: UInt32, Codable {
|
||||||
case metadata = 0
|
case metadata = 0
|
||||||
case text = 1
|
case text = 1
|
||||||
@@ -18,6 +19,7 @@ enum NostrKind: UInt32, Codable {
|
|||||||
case like = 7
|
case like = 7
|
||||||
case chat = 42
|
case chat = 42
|
||||||
case mute_list = 10000
|
case mute_list = 10000
|
||||||
|
case relay_list = 10002
|
||||||
case list_deprecated = 30000
|
case list_deprecated = 30000
|
||||||
case draft = 31234
|
case draft = 31234
|
||||||
case longform = 30023
|
case longform = 30023
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ struct NostrSubscribe {
|
|||||||
let sub_id: String
|
let sub_id: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Models a request/message that is sent to a Nostr relay
|
||||||
enum NostrRequestType {
|
enum NostrRequestType {
|
||||||
|
/// A standard nostr request
|
||||||
case typical(NostrRequest)
|
case typical(NostrRequest)
|
||||||
|
/// A customized nostr request. Generally used in the context of a nostrscript.
|
||||||
case custom(String)
|
case custom(String)
|
||||||
|
|
||||||
|
/// Whether this request is meant to write data to a relay
|
||||||
var is_write: Bool {
|
var is_write: Bool {
|
||||||
guard case .typical(let req) = self else {
|
guard case .typical(let req) = self else {
|
||||||
return true
|
return true
|
||||||
@@ -25,6 +28,7 @@ enum NostrRequestType {
|
|||||||
return req.is_write
|
return req.is_write
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this request is meant to read data from a relay
|
||||||
var is_read: Bool {
|
var is_read: Bool {
|
||||||
guard case .typical(let req) = self else {
|
guard case .typical(let req) = self else {
|
||||||
return true
|
return true
|
||||||
@@ -34,12 +38,18 @@ enum NostrRequestType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Models a standard request/message that is sent to a Nostr relay.
|
||||||
enum NostrRequest {
|
enum NostrRequest {
|
||||||
|
/// Subscribes to receive information from the relay
|
||||||
case subscribe(NostrSubscribe)
|
case subscribe(NostrSubscribe)
|
||||||
|
/// Unsubscribes from an existing subscription, addressed by its id
|
||||||
case unsubscribe(String)
|
case unsubscribe(String)
|
||||||
|
/// Posts an event
|
||||||
case event(NostrEvent)
|
case event(NostrEvent)
|
||||||
|
/// Authenticate with the relay
|
||||||
case auth(NostrEvent)
|
case auth(NostrEvent)
|
||||||
|
|
||||||
|
/// Whether this request is meant to write data to a relay
|
||||||
var is_write: Bool {
|
var is_write: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .subscribe:
|
case .subscribe:
|
||||||
@@ -53,6 +63,7 @@ enum NostrRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this request is meant to read data from a relay
|
||||||
var is_read: Bool {
|
var is_read: Bool {
|
||||||
return !is_write
|
return !is_write
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ enum NostrResponse {
|
|||||||
|
|
||||||
static func owned_from_json(json: String) -> NostrResponse? {
|
static func owned_from_json(json: String) -> NostrResponse? {
|
||||||
return json.withCString{ cstr in
|
return json.withCString{ cstr in
|
||||||
let bufsize: Int = max(Int(Double(json.utf8.count) * 4.0), Int(getpagesize()))
|
let bufsize: Int = max(Int(Double(json.utf8.count) * 8.0), Int(getpagesize()))
|
||||||
let data = malloc(bufsize)
|
let data = malloc(bufsize)
|
||||||
|
|
||||||
if data == nil {
|
if data == nil {
|
||||||
|
|||||||
@@ -115,6 +115,19 @@ enum FollowRef: TagKeys, Hashable, TagConvertible, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Models common tag references defined by the Nostr protocol, and their associated values.
|
||||||
|
///
|
||||||
|
/// For example, this raw JSON tag sequence:
|
||||||
|
/// ```json
|
||||||
|
/// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// would be parsed into something equivalent to `.pubkey(Pubkey(hex: "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"))`
|
||||||
|
///
|
||||||
|
/// ## Notes
|
||||||
|
///
|
||||||
|
/// - Not all tag information from all NIPs can be modelled using this alone, as some NIPs may define extra associated values for specific event types. You may need to use a specialized type for some event kinds
|
||||||
|
///
|
||||||
enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||||
case event(NoteId)
|
case event(NoteId)
|
||||||
case pubkey(Pubkey)
|
case pubkey(Pubkey)
|
||||||
@@ -124,6 +137,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case naddr(NAddr)
|
case naddr(NAddr)
|
||||||
case reference(String)
|
case reference(String)
|
||||||
|
|
||||||
|
/// The key that defines the type of reference being made
|
||||||
var key: RefKey {
|
var key: RefKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .event: return .e
|
case .event: return .e
|
||||||
@@ -136,6 +150,14 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Defines the type of reference being made on a Nostr event tag
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```json
|
||||||
|
/// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The `RefKey` is "p"
|
||||||
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
|
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
|
||||||
case e, p, t, d, q, a, r
|
case e, p, t, d, q, a, r
|
||||||
|
|
||||||
@@ -148,10 +170,12 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A raw nostr-style tag sequence representation of this object
|
||||||
var tag: [String] {
|
var tag: [String] {
|
||||||
[self.key.description, self.description]
|
[self.key.description, self.description]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Describes what is being referenced, as a `String`
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .event(let noteId): return noteId.hex()
|
case .event(let noteId): return noteId.hex()
|
||||||
@@ -166,6 +190,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a raw tag sequence
|
||||||
static func from_tag(tag: TagSequence) -> RefId? {
|
static func from_tag(tag: TagSequence) -> RefId? {
|
||||||
var i = tag.makeIterator()
|
var i = tag.makeIterator()
|
||||||
|
|
||||||
|
|||||||
+88
-50
@@ -7,16 +7,25 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct RelayInfo: Codable {
|
public struct LegacyKind3RelayRWConfiguration: Codable, Sendable {
|
||||||
let read: Bool?
|
public let read: Bool?
|
||||||
let write: Bool?
|
public let write: Bool?
|
||||||
|
|
||||||
init(read: Bool, write: Bool) {
|
init(read: Bool, write: Bool) {
|
||||||
self.read = read
|
self.read = read
|
||||||
self.write = write
|
self.write = write
|
||||||
}
|
}
|
||||||
|
|
||||||
static let rw = RelayInfo(read: true, write: true)
|
static let rw = LegacyKind3RelayRWConfiguration(read: true, write: true)
|
||||||
|
|
||||||
|
func toNIP65RWConfiguration() -> NIP65.RelayList.RelayItem.RWConfiguration? {
|
||||||
|
switch (self.read, self.write) {
|
||||||
|
case (false, true): return .write
|
||||||
|
case (true, false): return .read
|
||||||
|
case (true, true): return .readWrite
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RelayVariant {
|
enum RelayVariant {
|
||||||
@@ -25,30 +34,33 @@ enum RelayVariant {
|
|||||||
case nwc
|
case nwc
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RelayDescriptor {
|
extension RelayPool {
|
||||||
let url: RelayURL
|
/// Describes a relay for use in `RelayPool`
|
||||||
let info: RelayInfo
|
public struct RelayDescriptor {
|
||||||
let variant: RelayVariant
|
let url: RelayURL
|
||||||
|
var info: NIP65.RelayList.RelayItem.RWConfiguration
|
||||||
init(url: RelayURL, info: RelayInfo, variant: RelayVariant = .regular) {
|
let variant: RelayVariant
|
||||||
self.url = url
|
|
||||||
self.info = info
|
init(url: RelayURL, info: NIP65.RelayList.RelayItem.RWConfiguration, variant: RelayVariant = .regular) {
|
||||||
self.variant = variant
|
self.url = url
|
||||||
}
|
self.info = info
|
||||||
|
self.variant = variant
|
||||||
var ephemeral: Bool {
|
}
|
||||||
switch variant {
|
|
||||||
case .regular:
|
var ephemeral: Bool {
|
||||||
return false
|
switch variant {
|
||||||
case .ephemeral:
|
case .regular:
|
||||||
return true
|
return false
|
||||||
case .nwc:
|
case .ephemeral:
|
||||||
return true
|
return true
|
||||||
|
case .nwc:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func nwc(url: RelayURL) -> RelayDescriptor {
|
||||||
|
return RelayDescriptor(url: url, info: .readWrite, variant: .nwc)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static func nwc(url: RelayURL) -> RelayDescriptor {
|
|
||||||
return RelayDescriptor(url: url, info: .rw, variant: .nwc)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,30 +141,56 @@ struct RelayMetadata: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Relay: Identifiable {
|
extension RelayPool {
|
||||||
let descriptor: RelayDescriptor
|
class Relay: Identifiable {
|
||||||
let connection: RelayConnection
|
var descriptor: RelayDescriptor
|
||||||
var authentication_state: RelayAuthenticationState
|
let connection: RelayConnection
|
||||||
|
var authentication_state: RelayAuthenticationState
|
||||||
var flags: Int
|
|
||||||
|
var flags: Int
|
||||||
init(descriptor: RelayDescriptor, connection: RelayConnection) {
|
|
||||||
self.flags = 0
|
init(descriptor: RelayDescriptor, connection: RelayConnection) {
|
||||||
self.descriptor = descriptor
|
self.flags = 0
|
||||||
self.connection = connection
|
self.descriptor = descriptor
|
||||||
self.authentication_state = RelayAuthenticationState.none
|
self.connection = connection
|
||||||
|
self.authentication_state = RelayAuthenticationState.none
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_broken: Bool {
|
||||||
|
return (flags & RelayFlags.broken.rawValue) == RelayFlags.broken.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: RelayURL {
|
||||||
|
return descriptor.url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var is_broken: Bool {
|
|
||||||
return (flags & RelayFlags.broken.rawValue) == RelayFlags.broken.rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
var id: RelayURL {
|
|
||||||
return descriptor.url
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RelayError: Error {
|
extension RelayPool {
|
||||||
case RelayAlreadyExists
|
enum RelayError: Error {
|
||||||
|
case RelayAlreadyExists
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Extension to bridge NIP-65 relay list structs with app-native objects
|
||||||
|
|
||||||
|
extension NIP65.RelayList {
|
||||||
|
static func fromLegacyContactList(_ contactList: NdbNote) throws(BridgeError) -> Self {
|
||||||
|
guard let relayListInfo = decode_json_relays(contactList.content) else { throw .couldNotDecodeRelayListInfo }
|
||||||
|
let relayItems = relayListInfo.map({ url, rwConfiguration in
|
||||||
|
return RelayItem(url: url, rwConfiguration: rwConfiguration.toNIP65RWConfiguration() ?? .readWrite)
|
||||||
|
})
|
||||||
|
return NIP65.RelayList(relays: relayItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fromLegacyContactList(_ contactList: NdbNote?) throws(BridgeError) -> Self? {
|
||||||
|
guard let contactList = contactList else { return nil }
|
||||||
|
return try fromLegacyContactList(contactList)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BridgeError: Error {
|
||||||
|
case couldNotDecodeRelayListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ struct SeenEvent: Hashable {
|
|||||||
let evid: NoteId
|
let evid: NoteId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Establishes and manages connections and subscriptions to a list of relays.
|
||||||
class RelayPool {
|
class RelayPool {
|
||||||
var relays: [Relay] = []
|
private(set) var relays: [Relay] = []
|
||||||
var handlers: [RelayHandler] = []
|
var handlers: [RelayHandler] = []
|
||||||
var request_queue: [QueuedRequest] = []
|
var request_queue: [QueuedRequest] = []
|
||||||
var seen: Set<SeenEvent> = Set()
|
var seen: Set<SeenEvent> = Set()
|
||||||
var counts: [RelayURL: UInt64] = [:]
|
var counts: [RelayURL: UInt64] = [:]
|
||||||
var ndb: Ndb
|
var ndb: Ndb
|
||||||
|
/// The keypair used to authenticate with relays
|
||||||
var keypair: Keypair?
|
var keypair: Keypair?
|
||||||
var message_received_function: (((String, RelayDescriptor)) -> Void)?
|
var message_received_function: (((String, RelayDescriptor)) -> Void)?
|
||||||
var message_sent_function: (((String, Relay)) -> Void)?
|
var message_sent_function: (((String, Relay)) -> Void)?
|
||||||
@@ -122,7 +124,7 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_relay(_ desc: RelayDescriptor) throws {
|
func add_relay(_ desc: RelayDescriptor) throws(RelayError) {
|
||||||
let relay_id = desc.url
|
let relay_id = desc.url
|
||||||
if get_relay(relay_id) != nil {
|
if get_relay(relay_id) != nil {
|
||||||
throw RelayError.RelayAlreadyExists
|
throw RelayError.RelayAlreadyExists
|
||||||
@@ -200,6 +202,64 @@ class RelayPool {
|
|||||||
register_handler(sub_id: sub_id, handler: handler)
|
register_handler(sub_id: sub_id, handler: handler)
|
||||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subscribes to data from the `RelayPool` based on a filter and a list of desired relays.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - filters: The filters specifying the desired content.
|
||||||
|
/// - desiredRelays: The desired relays which to subsctibe to. If `nil`, it defaults to the `RelayPool`'s default list
|
||||||
|
/// - eoseTimeout: The maximum timeout which to give up waiting for the eoseSignal, in seconds
|
||||||
|
/// - Returns: Returns an async stream that callers can easily consume via a for-loop
|
||||||
|
func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: TimeInterval = 10) -> AsyncStream<StreamItem> {
|
||||||
|
let desiredRelays = desiredRelays ?? self.relays.map({ $0.descriptor.url })
|
||||||
|
return AsyncStream<StreamItem> { continuation in
|
||||||
|
let sub_id = UUID().uuidString
|
||||||
|
var seenEvents: Set<NoteId> = []
|
||||||
|
var relaysWhoFinishedInitialResults: Set<RelayURL> = []
|
||||||
|
var eoseSent = false
|
||||||
|
self.subscribe(sub_id: sub_id, filters: filters, handler: { (relayUrl, connectionEvent) in
|
||||||
|
switch connectionEvent {
|
||||||
|
case .ws_event(let ev):
|
||||||
|
// Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here.
|
||||||
|
// For the future, perhaps we should abstract away `.ws_event` in `RelayPool`? Seems like something to be handled on the `RelayConnection` layer.
|
||||||
|
break
|
||||||
|
case .nostr_event(let nostrResponse):
|
||||||
|
guard nostrResponse.subid == sub_id else { return } // Do not stream items that do not belong in this subscription
|
||||||
|
switch nostrResponse {
|
||||||
|
case .event(_, let nostrEvent):
|
||||||
|
if seenEvents.contains(nostrEvent.id) { break } // Don't send two of the same events.
|
||||||
|
continuation.yield(with: .success(.event(nostrEvent)))
|
||||||
|
seenEvents.insert(nostrEvent.id)
|
||||||
|
case .notice(let note):
|
||||||
|
break // We do not support handling these yet
|
||||||
|
case .eose(_):
|
||||||
|
relaysWhoFinishedInitialResults.insert(relayUrl)
|
||||||
|
if relaysWhoFinishedInitialResults == Set(desiredRelays) {
|
||||||
|
continuation.yield(with: .success(.eose))
|
||||||
|
eoseSent = true
|
||||||
|
}
|
||||||
|
case .ok(_): break // No need to handle this, we are not sending an event to the relay
|
||||||
|
case .auth(_): break // Handled in a separate function in RelayPool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, to: desiredRelays)
|
||||||
|
Task {
|
||||||
|
try? await Task.sleep(nanoseconds: 1_000_000_000 * UInt64(eoseTimeout))
|
||||||
|
if !eoseSent { continuation.yield(with: .success(.eose)) }
|
||||||
|
}
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
self.unsubscribe(sub_id: sub_id, to: desiredRelays)
|
||||||
|
self.remove_handler(sub_id: sub_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StreamItem {
|
||||||
|
/// A Nostr event
|
||||||
|
case event(NostrEvent)
|
||||||
|
/// The "end of stored events" signal
|
||||||
|
case eose
|
||||||
|
}
|
||||||
|
|
||||||
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) {
|
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) {
|
||||||
register_handler(sub_id: sub_id, handler: handler)
|
register_handler(sub_id: sub_id, handler: handler)
|
||||||
@@ -243,19 +303,19 @@ class RelayPool {
|
|||||||
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
|
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
|
||||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||||
|
|
||||||
self.send_raw_to_local_ndb(req)
|
self.send_raw_to_local_ndb(req) // Always send Nostr events and data to NostrDB for a local copy
|
||||||
|
|
||||||
for relay in relays {
|
for relay in relays {
|
||||||
if req.is_read && !(relay.descriptor.info.read ?? true) {
|
if req.is_read && !(relay.descriptor.info.canRead) {
|
||||||
continue
|
continue // Do not send read requests to relays that are not READ relays
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.is_write && !(relay.descriptor.info.write ?? true) {
|
if req.is_write && !(relay.descriptor.info.canWrite) {
|
||||||
continue
|
continue // Do not send write requests to relays that are not WRITE relays
|
||||||
}
|
}
|
||||||
|
|
||||||
if relay.descriptor.ephemeral && skip_ephemeral {
|
if relay.descriptor.ephemeral && skip_ephemeral {
|
||||||
continue
|
continue // Do not send requests to ephemeral relays if we want to skip them
|
||||||
}
|
}
|
||||||
|
|
||||||
guard relay.connection.isConnected else {
|
guard relay.connection.isConnected else {
|
||||||
@@ -354,7 +414,7 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) {
|
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) {
|
||||||
try? pool.add_relay(RelayDescriptor(url: url, info: .rw))
|
try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -83,8 +83,7 @@ var test_damus_state: DamusState = ({
|
|||||||
let our_pubkey = test_pubkey
|
let our_pubkey = test_pubkey
|
||||||
let pool = RelayPool(ndb: ndb)
|
let pool = RelayPool(ndb: ndb)
|
||||||
let settings = UserSettingsStore()
|
let settings = UserSettingsStore()
|
||||||
let damus = DamusState(pool: pool,
|
let damus = DamusState(keypair: test_keypair,
|
||||||
keypair: test_keypair,
|
|
||||||
likes: .init(our_pubkey: our_pubkey),
|
likes: .init(our_pubkey: our_pubkey),
|
||||||
boosts: .init(our_pubkey: our_pubkey),
|
boosts: .init(our_pubkey: our_pubkey),
|
||||||
contacts: .init(our_pubkey: our_pubkey),
|
contacts: .init(our_pubkey: our_pubkey),
|
||||||
@@ -100,8 +99,6 @@ var test_damus_state: DamusState = ({
|
|||||||
drafts: .init(),
|
drafts: .init(),
|
||||||
events: .init(ndb: ndb),
|
events: .init(ndb: ndb),
|
||||||
bookmarks: .init(pubkey: our_pubkey),
|
bookmarks: .init(pubkey: our_pubkey),
|
||||||
postbox: .init(pool: pool),
|
|
||||||
bootstrap_relays: .init(),
|
|
||||||
replies: .init(our_pubkey: our_pubkey),
|
replies: .init(our_pubkey: our_pubkey),
|
||||||
wallet: .init(settings: settings),
|
wallet: .init(settings: settings),
|
||||||
nav: .init(),
|
nav: .init(),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import Foundation
|
|||||||
class Constants {
|
class Constants {
|
||||||
//static let EXAMPLE_DEMOS: DamusState = .empty
|
//static let EXAMPLE_DEMOS: DamusState = .empty
|
||||||
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
|
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
|
||||||
|
static let IMAGE_CACHE_DIRNAME: String = "ImageCache"
|
||||||
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"
|
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"
|
||||||
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
|
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,15 @@ import Foundation
|
|||||||
enum DisplayName: Equatable {
|
enum DisplayName: Equatable {
|
||||||
case both(username: String, displayName: String)
|
case both(username: String, displayName: String)
|
||||||
case one(String)
|
case one(String)
|
||||||
|
|
||||||
|
init (profile: Profile?, pubkey: Pubkey) {
|
||||||
|
self = parse_display_name(name: profile?.name, display_name: profile?.display_name, pubkey: pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
init (name: String?, display_name: String?, pubkey: Pubkey) {
|
||||||
|
self = parse_display_name(name: name, display_name: display_name, pubkey: pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .one(let one):
|
case .one(let one):
|
||||||
@@ -28,20 +36,37 @@ enum DisplayName: Equatable {
|
|||||||
return username
|
return username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nameComponents() -> PersonNameComponents {
|
||||||
|
var components = PersonNameComponents()
|
||||||
|
switch self {
|
||||||
|
case .one(let one):
|
||||||
|
components.nickname = one
|
||||||
|
return components
|
||||||
|
case .both(username: let username, displayName: let displayName):
|
||||||
|
components.nickname = username
|
||||||
|
let names = displayName.split(separator: " ")
|
||||||
|
if let name = names.first {
|
||||||
|
components.givenName = String(name)
|
||||||
|
components.familyName = names.dropFirst().joined(separator: " ")
|
||||||
|
}
|
||||||
|
return components
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func parse_display_name(profile: Profile?, pubkey: Pubkey) -> DisplayName {
|
func parse_display_name(name: String?, display_name: String?, pubkey: Pubkey) -> DisplayName {
|
||||||
if pubkey == ANON_PUBKEY {
|
if pubkey == ANON_PUBKEY {
|
||||||
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
|
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let profile else {
|
if name == nil && display_name == nil {
|
||||||
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
|
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = profile.name?.isEmpty == false ? profile.name : nil
|
let name = name?.isEmpty == false ? name : nil
|
||||||
let disp_name = profile.display_name?.isEmpty == false ? profile.display_name : nil
|
let disp_name = display_name?.isEmpty == false ? display_name : nil
|
||||||
|
|
||||||
if let name, let disp_name, name != disp_name {
|
if let name, let disp_name, name != disp_name {
|
||||||
return .both(username: name, displayName: disp_name)
|
return .both(username: name, displayName: disp_name)
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ func preload_image(url: URL) {
|
|||||||
|
|
||||||
//print("Preloading image \(url.absoluteString)")
|
//print("Preloading image \(url.absoluteString)")
|
||||||
|
|
||||||
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: url)) { val in
|
KingfisherManager.shared.retrieveImage(with: Kingfisher.KF.ImageResource(downloadURL: url)) { val in
|
||||||
//print("Preloaded image \(url.absoluteString)")
|
//print("Preloaded image \(url.absoluteString)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ extension KFOptionSetter {
|
|||||||
|
|
||||||
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
|
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
|
||||||
guard let url = fallbackUrl, let key = cacheKey else { return self }
|
guard let url = fallbackUrl, let key = cacheKey else { return self }
|
||||||
let imageResource = Kingfisher.ImageResource(downloadURL: url, cacheKey: key)
|
let imageResource = Kingfisher.KF.ImageResource(downloadURL: url, cacheKey: key)
|
||||||
let source = imageResource.convertToSource()
|
let source = imageResource.convertToSource()
|
||||||
options.alternativeSources = [source]
|
options.alternativeSources = [source]
|
||||||
|
|
||||||
@@ -159,20 +159,25 @@ struct CustomCacheSerializer: CacheSerializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomSessionDelegate: SessionDelegate {
|
class CustomSessionDelegate: SessionDelegate, @unchecked Sendable {
|
||||||
override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
override func urlSession(
|
||||||
|
_ session: URLSession,
|
||||||
|
dataTask: URLSessionDataTask,
|
||||||
|
didReceive response: URLResponse
|
||||||
|
) async -> URLSession.ResponseDisposition {
|
||||||
let contentLength = response.expectedContentLength
|
let contentLength = response.expectedContentLength
|
||||||
|
|
||||||
// Content-Length header is optional (-1 when missing)
|
// Content-Length header is optional (-1 when missing)
|
||||||
if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
|
if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
|
||||||
return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
|
return await super.urlSession(session, dataTask: dataTask, didReceive: URLResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
|
return await super.urlSession(session, dataTask: dataTask, didReceive: response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomImageDownloader: ImageDownloader {
|
|
||||||
|
class CustomImageDownloader: ImageDownloader, @unchecked Sendable {
|
||||||
|
|
||||||
static let shared = CustomImageDownloader(name: "shared")
|
static let shared = CustomImageDownloader(name: "shared")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// ExtraFonts.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-03-13.
|
||||||
|
//
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension Font {
|
||||||
|
// Note: When changing the font size accessibility setting, these styles only update after an app restart. It's a current limitation of this.
|
||||||
|
|
||||||
|
static let veryLargeTitle: Font = .system(size: UIFont.preferredFont(forTextStyle: .largeTitle).pointSize * 1.5, weight: .bold) // Makes a bigger title while allowing for iOS dynamic font sizing to take effect
|
||||||
|
static let veryVeryLargeTitle: Font = .system(size: UIFont.preferredFont(forTextStyle: .largeTitle).pointSize * 2.1, weight: .bold) // Makes a bigger title while allowing for iOS dynamic font sizing to take effect
|
||||||
|
}
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ enum LogCategory: String {
|
|||||||
case storage
|
case storage
|
||||||
case networking
|
case networking
|
||||||
case timeline
|
case timeline
|
||||||
|
/// Logs related to Nostr Wallet Connect components
|
||||||
|
case nwc
|
||||||
case push_notifications
|
case push_notifications
|
||||||
case damus_purple
|
case damus_purple
|
||||||
case image_uploading
|
case image_uploading
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ enum CancelSendErr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PostBox {
|
class PostBox {
|
||||||
let pool: RelayPool
|
private let pool: RelayPool
|
||||||
var events: [NoteId: PostedEvent]
|
var events: [NoteId: PostedEvent]
|
||||||
|
|
||||||
init(pool: RelayPool) {
|
init(pool: RelayPool) {
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// Stores information, metadata, and logs about different relays. Generally used as a singleton.
|
||||||
|
///
|
||||||
|
/// # Discussion
|
||||||
|
///
|
||||||
|
/// This class is primarily used as a shared singleton in `DamusState`, to allow other parts of the app to access information, metadata, and logs about relays without having to fetch it themselves.
|
||||||
|
///
|
||||||
|
/// For example, it is used by `RelayView` to supplement information about the relay without having to fetch those again from the network, as well as to display logs collected throughout the use of the app.
|
||||||
final class RelayModelCache: ObservableObject {
|
final class RelayModelCache: ObservableObject {
|
||||||
private var models = [RelayURL: RelayModel]()
|
private var models = [RelayURL: RelayModel]()
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ enum Route: Hashable {
|
|||||||
case .FollowersYouKnow(let friendedFollowers, let followers):
|
case .FollowersYouKnow(let friendedFollowers, let followers):
|
||||||
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
|
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
|
||||||
case .Script(let load_model):
|
case .Script(let load_model):
|
||||||
LoadScript(pool: damusState.pool, model: load_model)
|
LoadScript(pool: damusState.nostrNetwork.pool, model: load_model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ enum Route: Hashable {
|
|||||||
case .Search(let search):
|
case .Search(let search):
|
||||||
hasher.combine("search")
|
hasher.combine("search")
|
||||||
hasher.combine(search.search)
|
hasher.combine(search.search)
|
||||||
case .NDBSearch(let results):
|
case .NDBSearch:
|
||||||
hasher.combine("results")
|
hasher.combine("results")
|
||||||
case .EULA:
|
case .EULA:
|
||||||
hasher.combine("eula")
|
hasher.combine("eula")
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
//
|
|
||||||
// WalletConnect+.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-11-27.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
func make_wallet_pay_invoice_request(invoice: String) -> WalletRequest<PayInvoiceRequest> {
|
|
||||||
let data = PayInvoiceRequest(invoice: invoice)
|
|
||||||
return WalletRequest(method: "pay_invoice", params: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func make_wallet_balance_request() -> WalletRequest<EmptyRequest> {
|
|
||||||
return WalletRequest(method: "get_balance", params: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EmptyRequest: Codable {
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PayInvoiceRequest: Codable {
|
|
||||||
let invoice: String
|
|
||||||
}
|
|
||||||
|
|
||||||
func make_wallet_connect_request<T>(req: WalletRequest<T>, to_pk: Pubkey, keypair: FullKeypair) -> NostrEvent? {
|
|
||||||
let tags = [to_pk.tag]
|
|
||||||
let created_at = UInt32(Date().timeIntervalSince1970)
|
|
||||||
guard let content = encode_json(req) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return create_encrypted_event(content, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created_at, kind: 23194)
|
|
||||||
}
|
|
||||||
|
|
||||||
func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
|
|
||||||
var filter = NostrFilter(kinds: [.nwc_response])
|
|
||||||
filter.authors = [url.pubkey]
|
|
||||||
filter.limit = 0
|
|
||||||
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
|
||||||
|
|
||||||
pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
|
||||||
let req = make_wallet_pay_invoice_request(invoice: invoice)
|
|
||||||
guard let ev = make_wallet_connect_request(req: req, to_pk: url.pubkey, keypair: url.keypair) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
try? pool.add_relay(.nwc(url: url.relay))
|
|
||||||
subscribe_to_nwc(url: url, pool: pool)
|
|
||||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
|
||||||
return ev
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func nwc_success(state: DamusState, resp: FullWalletResponse) {
|
|
||||||
// find the pending zap and mark it as pending-confirmed
|
|
||||||
for kv in state.zaps.our_zaps {
|
|
||||||
let zaps = kv.value
|
|
||||||
|
|
||||||
for zap in zaps {
|
|
||||||
guard case .pending(let pzap) = zap,
|
|
||||||
case .nwc(let nwc_state) = pzap.state,
|
|
||||||
case .postbox_pending(let nwc_req) = nwc_state.state,
|
|
||||||
nwc_req.id == resp.req_id
|
|
||||||
else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if nwc_state.update_state(state: .confirmed) {
|
|
||||||
// notify the zaps model of an update so it can mark them as paid
|
|
||||||
state.events.get_cache_data(NoteId(pzap.target.id)).zaps_model.objectWillChange.send()
|
|
||||||
print("NWC success confirmed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
|
|
||||||
let percent_f = Double(percent) / 100.0
|
|
||||||
let donations_msats = Int64(percent_f * Double(base_msats))
|
|
||||||
|
|
||||||
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
|
|
||||||
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
|
|
||||||
// we failed... oh well. no donation for us.
|
|
||||||
print("damus-donation failed to fetch invoice")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("damus-donation donating...")
|
|
||||||
nwc_pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nwc_error(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) {
|
|
||||||
// find a pending zap with the nwc request id associated with this response and remove it
|
|
||||||
for kv in zapcache.our_zaps {
|
|
||||||
let zaps = kv.value
|
|
||||||
|
|
||||||
for zap in zaps {
|
|
||||||
guard case .pending(let pzap) = zap,
|
|
||||||
case .nwc(let nwc_state) = pzap.state,
|
|
||||||
case .postbox_pending(let req) = nwc_state.state,
|
|
||||||
req.id == resp.req_id
|
|
||||||
else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the pending zap if there was an error
|
|
||||||
let reqid = ZapRequestId(from_pending: pzap)
|
|
||||||
remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
//
|
|
||||||
// WalletConnect.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by William Casarin on 2023-03-22.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct WalletConnectURL: Equatable {
|
|
||||||
static func == (lhs: WalletConnectURL, rhs: WalletConnectURL) -> Bool {
|
|
||||||
return lhs.keypair == rhs.keypair &&
|
|
||||||
lhs.pubkey == rhs.pubkey &&
|
|
||||||
lhs.relay == rhs.relay
|
|
||||||
}
|
|
||||||
|
|
||||||
let relay: RelayURL
|
|
||||||
let keypair: FullKeypair
|
|
||||||
let pubkey: Pubkey
|
|
||||||
let lud16: String?
|
|
||||||
|
|
||||||
func to_url() -> URL {
|
|
||||||
var urlComponents = URLComponents()
|
|
||||||
urlComponents.scheme = "nostrwalletconnect"
|
|
||||||
urlComponents.host = pubkey.hex()
|
|
||||||
urlComponents.queryItems = [
|
|
||||||
URLQueryItem(name: "relay", value: relay.absoluteString),
|
|
||||||
URLQueryItem(name: "secret", value: keypair.privkey.hex())
|
|
||||||
]
|
|
||||||
|
|
||||||
if let lud16 {
|
|
||||||
urlComponents.queryItems?.append(URLQueryItem(name: "lud16", value: lud16))
|
|
||||||
}
|
|
||||||
|
|
||||||
return urlComponents.url!
|
|
||||||
}
|
|
||||||
|
|
||||||
init?(str: String) {
|
|
||||||
guard let components = URLComponents(string: str),
|
|
||||||
components.scheme == "nostrwalletconnect" || components.scheme == "nostr+walletconnect",
|
|
||||||
// The line below provides flexibility for both `nostrwalletconnect://` (non-compliant, but commonly used) and `nostrwalletconnect:` (NIP-47 compliant) formats
|
|
||||||
let encoded_pubkey = components.path == "" ? components.host : components.path,
|
|
||||||
let pubkey = hex_decode_pubkey(encoded_pubkey),
|
|
||||||
let items = components.queryItems,
|
|
||||||
let relay = items.first(where: { qi in qi.name == "relay" })?.value,
|
|
||||||
let relay_url = RelayURL(relay),
|
|
||||||
let secret = items.first(where: { qi in qi.name == "secret" })?.value,
|
|
||||||
secret.utf8.count == 64,
|
|
||||||
let decoded = hex_decode(secret)
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let privkey = Privkey(Data(decoded))
|
|
||||||
guard let our_pk = privkey_to_pubkey(privkey: privkey) else { return nil }
|
|
||||||
|
|
||||||
let lud16 = items.first(where: { qi in qi.name == "lud16" })?.value
|
|
||||||
let keypair = FullKeypair(pubkey: our_pk, privkey: privkey)
|
|
||||||
self = WalletConnectURL(pubkey: pubkey, relay: relay_url, keypair: keypair, lud16: lud16)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(pubkey: Pubkey, relay: RelayURL, keypair: FullKeypair, lud16: String?) {
|
|
||||||
self.pubkey = pubkey
|
|
||||||
self.relay = relay
|
|
||||||
self.keypair = keypair
|
|
||||||
self.lud16 = lud16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WalletRequest<T: Codable>: Codable {
|
|
||||||
let method: String
|
|
||||||
let params: T?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WalletResponseErr: Codable {
|
|
||||||
let code: String?
|
|
||||||
let message: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PayInvoiceResponse: Decodable {
|
|
||||||
let preimage: String
|
|
||||||
}
|
|
||||||
|
|
||||||
enum WalletResponseResultType: String {
|
|
||||||
case pay_invoice
|
|
||||||
}
|
|
||||||
|
|
||||||
enum WalletResponseResult {
|
|
||||||
case pay_invoice(PayInvoiceResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FullWalletResponse {
|
|
||||||
let req_id: NoteId
|
|
||||||
let response: WalletResponse
|
|
||||||
|
|
||||||
init?(from: NostrEvent, nwc: WalletConnectURL) async {
|
|
||||||
guard let note_id = from.referenced_ids.first else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
self.req_id = note_id
|
|
||||||
|
|
||||||
let ares = Task {
|
|
||||||
guard let json = decrypt_dm(nwc.keypair.privkey, pubkey: nwc.pubkey, content: from.content, encoding: .base64),
|
|
||||||
let resp: WalletResponse = decode_json(json)
|
|
||||||
else {
|
|
||||||
let resp: WalletResponse? = nil
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let res = await ares.value else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
self.response = res
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WalletResponse: Decodable {
|
|
||||||
let result_type: WalletResponseResultType
|
|
||||||
let error: WalletResponseErr?
|
|
||||||
let result: WalletResponseResult?
|
|
||||||
|
|
||||||
private enum CodingKeys: CodingKey {
|
|
||||||
case result_type, error, result
|
|
||||||
}
|
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let result_type_str = try container.decode(String.self, forKey: .result_type)
|
|
||||||
|
|
||||||
guard let result_type = WalletResponseResultType(rawValue: result_type_str) else {
|
|
||||||
throw DecodingError.typeMismatch(WalletResponseResultType.self, .init(codingPath: decoder.codingPath, debugDescription: "result_type \(result_type_str) is unknown"))
|
|
||||||
}
|
|
||||||
|
|
||||||
self.result_type = result_type
|
|
||||||
self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error)
|
|
||||||
|
|
||||||
guard self.error == nil else {
|
|
||||||
self.result = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result_type {
|
|
||||||
case .pay_invoice:
|
|
||||||
let res = try container.decode(PayInvoiceResponse.self, forKey: .result)
|
|
||||||
self.result = .pay_invoice(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
//
|
||||||
|
// Request.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-03-10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension WalletConnect {
|
||||||
|
/// Models a request to an NWC wallet provider
|
||||||
|
enum Request: Codable {
|
||||||
|
/// Pay an invoice
|
||||||
|
case payInvoice(
|
||||||
|
/// bolt-11 invoice string
|
||||||
|
invoice: String
|
||||||
|
)
|
||||||
|
/// Get the current wallet balance
|
||||||
|
case getBalance
|
||||||
|
/// Get the current wallet transaction history
|
||||||
|
case getTransactionList(
|
||||||
|
/// Starting timestamp in seconds since epoch (inclusive), optional.
|
||||||
|
from: UInt64?,
|
||||||
|
/// Ending timestamp in seconds since epoch (inclusive), optional.
|
||||||
|
until: UInt64?,
|
||||||
|
/// Maximum number of invoices to return, optional.
|
||||||
|
limit: Int?,
|
||||||
|
/// Offset of the first invoice to return, optional.
|
||||||
|
offset: Int?,
|
||||||
|
/// Include unpaid invoices, optional, default false.
|
||||||
|
unpaid: Bool?,
|
||||||
|
/// "incoming" for invoices, "outgoing" for payments, undefined for both.
|
||||||
|
type: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Interface
|
||||||
|
|
||||||
|
/// Converts the NWC request into a raw Nostr event to be sent in the network
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - to_pk: The destination pubkey (used for encryption)
|
||||||
|
/// - keypair: The requester's pubkey (used for encryption and signing)
|
||||||
|
/// - Returns: The NWC request in a raw Nostr Event format, or nil if it cannot be encoded
|
||||||
|
func to_nostr_event(to_pk: Pubkey, keypair: FullKeypair) -> NostrEvent? {
|
||||||
|
let tags = [to_pk.tag]
|
||||||
|
let created_at = UInt32(Date().timeIntervalSince1970)
|
||||||
|
guard let content = encode_json(self) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return NIP04.create_encrypted_event(content, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created_at, kind: NostrKind.nwc_request.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Encoding and decoding
|
||||||
|
|
||||||
|
/// Keys for top-level JSON
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case method
|
||||||
|
case params
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keys for the JSON inside the "params" object
|
||||||
|
private enum ParamKeys: String, CodingKey {
|
||||||
|
case invoice
|
||||||
|
case from, until, limit, offset, unpaid, type
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constants for possible request "method" verbs
|
||||||
|
private enum Method: String {
|
||||||
|
case payInvoice = "pay_invoice"
|
||||||
|
case getBalance = "get_balance"
|
||||||
|
case listTransactions = "list_transactions"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes a payload into this request structure
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let method = try container.decode(String.self, forKey: .method)
|
||||||
|
|
||||||
|
|
||||||
|
switch method {
|
||||||
|
case Method.payInvoice.rawValue:
|
||||||
|
let paramsContainer = try container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||||
|
let invoice = try paramsContainer.decode(String.self, forKey: .invoice)
|
||||||
|
self = .payInvoice(invoice: invoice)
|
||||||
|
|
||||||
|
case Method.getBalance.rawValue:
|
||||||
|
// No params to decode
|
||||||
|
self = .getBalance
|
||||||
|
|
||||||
|
case Method.listTransactions.rawValue:
|
||||||
|
let paramsContainer = try container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||||
|
let from = try paramsContainer.decodeIfPresent(UInt64.self, forKey: .from)
|
||||||
|
let until = try paramsContainer.decodeIfPresent(UInt64.self, forKey: .until)
|
||||||
|
let limit = try paramsContainer.decodeIfPresent(Int.self, forKey: .limit)
|
||||||
|
let offset = try paramsContainer.decodeIfPresent(Int.self, forKey: .offset)
|
||||||
|
let unpaid = try paramsContainer.decodeIfPresent(Bool.self, forKey: .unpaid)
|
||||||
|
let type = try paramsContainer.decodeIfPresent(String.self, forKey: .type)
|
||||||
|
self = .getTransactionList(from: from, until: until, limit: limit, offset: offset, unpaid: unpaid, type: type)
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw DecodingError.dataCorruptedError(
|
||||||
|
forKey: .method,
|
||||||
|
in: container,
|
||||||
|
debugDescription: "Unknown wallet method \"\(method)\""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes this request structure into a payload
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
switch self {
|
||||||
|
case .payInvoice(let invoice):
|
||||||
|
try container.encode(Method.payInvoice.rawValue, forKey: .method)
|
||||||
|
var paramsContainer = container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||||
|
try paramsContainer.encode(invoice, forKey: .invoice)
|
||||||
|
|
||||||
|
case .getBalance:
|
||||||
|
try container.encode(Method.getBalance.rawValue, forKey: .method)
|
||||||
|
// "params": null
|
||||||
|
try container.encodeNil(forKey: .params)
|
||||||
|
|
||||||
|
case .getTransactionList(let from, let until, let limit, let offset, let unpaid, let type):
|
||||||
|
try container.encode(Method.listTransactions.rawValue, forKey: .method)
|
||||||
|
var paramsContainer = container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||||
|
try paramsContainer.encodeIfPresent(from, forKey: .from)
|
||||||
|
try paramsContainer.encodeIfPresent(until, forKey: .until)
|
||||||
|
try paramsContainer.encodeIfPresent(limit, forKey: .limit)
|
||||||
|
try paramsContainer.encodeIfPresent(offset, forKey: .offset)
|
||||||
|
try paramsContainer.encodeIfPresent(unpaid, forKey: .unpaid)
|
||||||
|
try paramsContainer.encodeIfPresent(type, forKey: .type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
//
|
||||||
|
// Response.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-03-10.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension WalletConnect {
|
||||||
|
/// Models a response from the NWC provider
|
||||||
|
struct Response: Decodable {
|
||||||
|
let result_type: Response.Result.ResultType
|
||||||
|
let error: WalletResponseErr?
|
||||||
|
let result: Response.Result?
|
||||||
|
|
||||||
|
private enum CodingKeys: CodingKey {
|
||||||
|
case result_type, error, result
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let result_type_str = try container.decode(String.self, forKey: .result_type)
|
||||||
|
|
||||||
|
guard let result_type = Response.Result.ResultType(rawValue: result_type_str) else {
|
||||||
|
throw DecodingError.typeMismatch(Response.Result.ResultType.self, .init(codingPath: decoder.codingPath, debugDescription: "result_type \(result_type_str) is unknown"))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.result_type = result_type
|
||||||
|
self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error)
|
||||||
|
|
||||||
|
guard self.error == nil else {
|
||||||
|
self.result = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result_type {
|
||||||
|
case .pay_invoice:
|
||||||
|
let res = try container.decode(Result.PayInvoiceResponse.self, forKey: .result)
|
||||||
|
self.result = .pay_invoice(res)
|
||||||
|
case .get_balance:
|
||||||
|
let res = try container.decode(Result.GetBalanceResponse.self, forKey: .result)
|
||||||
|
self.result = .get_balance(res)
|
||||||
|
case .list_transactions:
|
||||||
|
let res = try container.decode(Result.ListTransactionsResponse.self, forKey: .result)
|
||||||
|
self.result = .list_transactions(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FullWalletResponse {
|
||||||
|
let req_id: NoteId
|
||||||
|
let response: Response
|
||||||
|
|
||||||
|
init?(from: NostrEvent, nwc: WalletConnect.ConnectURL) async {
|
||||||
|
guard let note_id = from.referenced_ids.first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.req_id = note_id
|
||||||
|
|
||||||
|
let ares = Task {
|
||||||
|
guard let json = decrypt_dm(nwc.keypair.privkey, pubkey: nwc.pubkey, content: from.content, encoding: .base64),
|
||||||
|
let resp: WalletConnect.Response = decode_json(json)
|
||||||
|
else {
|
||||||
|
let resp: WalletConnect.Response? = nil
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let res = await ares.value else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.response = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WalletResponseErr: Codable {
|
||||||
|
let code: String?
|
||||||
|
let message: String?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WalletConnect.Response {
|
||||||
|
/// The response data resulting from an NWC request
|
||||||
|
enum Result {
|
||||||
|
case pay_invoice(PayInvoiceResponse)
|
||||||
|
case get_balance(GetBalanceResponse)
|
||||||
|
case list_transactions(ListTransactionsResponse)
|
||||||
|
|
||||||
|
enum ResultType: String {
|
||||||
|
case pay_invoice
|
||||||
|
case get_balance
|
||||||
|
case list_transactions
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PayInvoiceResponse: Decodable {
|
||||||
|
let preimage: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GetBalanceResponse: Decodable {
|
||||||
|
let balance: Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ListTransactionsResponse: Decodable {
|
||||||
|
let transactions: [WalletConnect.Transaction]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
//
|
||||||
|
// WalletConnect+.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2023-11-27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// TODO: Eventually we should move these convenience functions into structured classes responsible for managing this type of functionality, such as `WalletModel`
|
||||||
|
|
||||||
|
extension WalletConnect {
|
||||||
|
/// Creates and sends a subscription to an NWC relay requesting NWC responses to be sent back.
|
||||||
|
///
|
||||||
|
/// Notes: This assumes there is already a listener somewhere else
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - url: The Nostr Wallet Connect URL containing connection info to the NWC wallet
|
||||||
|
/// - pool: The RelayPool to send the subscription request through
|
||||||
|
static func subscribe(url: WalletConnectURL, pool: RelayPool) {
|
||||||
|
var filter = NostrFilter(kinds: [.nwc_response])
|
||||||
|
filter.authors = [url.pubkey]
|
||||||
|
filter.limit = 0
|
||||||
|
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
||||||
|
|
||||||
|
pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends out a request to pay an invoice to the NWC relay, and ensures that:
|
||||||
|
/// 1. the NWC relay is connected and we are listening to NWC events
|
||||||
|
/// 2. the NWC relay is connected and we are listening to NWC
|
||||||
|
///
|
||||||
|
/// Note: This does not return information about whether the payment is succesful or not. The actual confirmation is handled elsewhere around `HomeModel` and `WalletModel`
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - url: The NWC wallet connection URL
|
||||||
|
/// - pool: The relay pool to connect to
|
||||||
|
/// - post: The postbox to send events in
|
||||||
|
/// - delay: The delay before actually sending the request to the network _(this makes it possible to cancel a zap)_
|
||||||
|
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||||
|
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||||
|
@discardableResult
|
||||||
|
static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||||
|
let req = WalletConnect.Request.payInvoice(invoice: invoice)
|
||||||
|
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
||||||
|
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
||||||
|
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends out a wallet balance request to the NWC relay, and ensures that:
|
||||||
|
/// 1. the NWC relay is connected and we are listening to NWC events
|
||||||
|
/// 2. the NWC relay is connected and we are listening to NWC
|
||||||
|
///
|
||||||
|
/// Note: This does not return the actual balance information. The actual balance is handled elsewhere around `HomeModel` and `WalletModel`
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - url: The NWC wallet connection URL
|
||||||
|
/// - pool: The relay pool to connect to
|
||||||
|
/// - post: The postbox to send events in
|
||||||
|
/// - delay: The delay before actually sending the request to the network
|
||||||
|
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||||
|
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||||
|
@discardableResult
|
||||||
|
static func request_balance_information(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||||
|
let req = WalletConnect.Request.getBalance
|
||||||
|
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
||||||
|
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
||||||
|
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends out a wallet transaction list request to the NWC relay, and ensures that:
|
||||||
|
/// 1. the NWC relay is connected and we are listening to NWC events
|
||||||
|
/// 2. the NWC relay is connected and we are listening to NWC
|
||||||
|
///
|
||||||
|
/// Note: This does not return the actual transaction list. The actual transaction list is handled elsewhere around `HomeModel` and `WalletModel`
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - url: The NWC wallet connection URL
|
||||||
|
/// - pool: The relay pool to connect to
|
||||||
|
/// - post: The postbox to send events in
|
||||||
|
/// - delay: The delay before actually sending the request to the network
|
||||||
|
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||||
|
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||||
|
@discardableResult
|
||||||
|
static func request_transaction_list(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||||
|
let req = WalletConnect.Request.getTransactionList(from: nil, until: nil, limit: 10, offset: 0, unpaid: false, type: "")
|
||||||
|
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
||||||
|
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
||||||
|
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
static func handle_zap_success(state: DamusState, resp: WalletConnect.FullWalletResponse) {
|
||||||
|
// find the pending zap and mark it as pending-confirmed
|
||||||
|
for kv in state.zaps.our_zaps {
|
||||||
|
let zaps = kv.value
|
||||||
|
|
||||||
|
for zap in zaps {
|
||||||
|
guard case .pending(let pzap) = zap,
|
||||||
|
case .nwc(let nwc_state) = pzap.state,
|
||||||
|
case .postbox_pending(let nwc_req) = nwc_state.state,
|
||||||
|
nwc_req.id == resp.req_id
|
||||||
|
else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if nwc_state.update_state(state: .confirmed) {
|
||||||
|
// notify the zaps model of an update so it can mark them as paid
|
||||||
|
state.events.get_cache_data(NoteId(pzap.target.id)).zaps_model.objectWillChange.send()
|
||||||
|
print("NWC success confirmed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a donation zap to the Damus team
|
||||||
|
static func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
|
||||||
|
let percent_f = Double(percent) / 100.0
|
||||||
|
let donations_msats = Int64(percent_f * Double(base_msats))
|
||||||
|
|
||||||
|
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
|
||||||
|
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
|
||||||
|
// we failed... oh well. no donation for us.
|
||||||
|
print("damus-donation failed to fetch invoice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("damus-donation donating...")
|
||||||
|
WalletConnect.pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles a received Nostr Wallet Connect error
|
||||||
|
static func handle_error(zapcache: Zaps, evcache: EventCache, resp: WalletConnect.FullWalletResponse) {
|
||||||
|
// find a pending zap with the nwc request id associated with this response and remove it
|
||||||
|
for kv in zapcache.our_zaps {
|
||||||
|
let zaps = kv.value
|
||||||
|
|
||||||
|
for zap in zaps {
|
||||||
|
guard case .pending(let pzap) = zap,
|
||||||
|
case .nwc(let nwc_state) = pzap.state,
|
||||||
|
case .postbox_pending(let req) = nwc_state.state,
|
||||||
|
req.id == resp.req_id
|
||||||
|
else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the pending zap if there was an error
|
||||||
|
let reqid = ZapRequestId(from_pending: pzap)
|
||||||
|
remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//
|
||||||
|
// WalletConnect.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-03-22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct WalletConnect {}
|
||||||
|
|
||||||
|
typealias WalletConnectURL = WalletConnect.ConnectURL // Declared to facilitate refactor
|
||||||
|
|
||||||
|
extension WalletConnect {
|
||||||
|
/// Models a decoded NWC URL, containing information to connect to an NWC wallet.
|
||||||
|
struct ConnectURL: Equatable {
|
||||||
|
let relay: RelayURL
|
||||||
|
let keypair: FullKeypair
|
||||||
|
let pubkey: Pubkey
|
||||||
|
let lud16: String?
|
||||||
|
|
||||||
|
static func == (lhs: ConnectURL, rhs: ConnectURL) -> Bool {
|
||||||
|
return lhs.keypair == rhs.keypair &&
|
||||||
|
lhs.pubkey == rhs.pubkey &&
|
||||||
|
lhs.relay == rhs.relay
|
||||||
|
}
|
||||||
|
|
||||||
|
func to_url() -> URL {
|
||||||
|
var urlComponents = URLComponents()
|
||||||
|
urlComponents.scheme = "nostrwalletconnect"
|
||||||
|
urlComponents.host = pubkey.hex()
|
||||||
|
urlComponents.queryItems = [
|
||||||
|
URLQueryItem(name: "relay", value: relay.absoluteString),
|
||||||
|
URLQueryItem(name: "secret", value: keypair.privkey.hex())
|
||||||
|
]
|
||||||
|
|
||||||
|
if let lud16 {
|
||||||
|
urlComponents.queryItems?.append(URLQueryItem(name: "lud16", value: lud16))
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlComponents.url!
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(str: String) {
|
||||||
|
guard let components = URLComponents(string: str),
|
||||||
|
components.scheme == "nostrwalletconnect" || components.scheme == "nostr+walletconnect",
|
||||||
|
// The line below provides flexibility for both `nostrwalletconnect://` (non-compliant, but commonly used) and `nostrwalletconnect:` (NIP-47 compliant) formats
|
||||||
|
let encoded_pubkey = components.path == "" ? components.host : components.path,
|
||||||
|
let pubkey = hex_decode_pubkey(encoded_pubkey),
|
||||||
|
let items = components.queryItems,
|
||||||
|
let relay = items.first(where: { qi in qi.name == "relay" })?.value,
|
||||||
|
let relay_url = RelayURL(relay),
|
||||||
|
let secret = items.first(where: { qi in qi.name == "secret" })?.value,
|
||||||
|
secret.utf8.count == 64,
|
||||||
|
let decoded = hex_decode(secret)
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let privkey = Privkey(Data(decoded))
|
||||||
|
guard let our_pk = privkey_to_pubkey(privkey: privkey) else { return nil }
|
||||||
|
|
||||||
|
let lud16 = items.first(where: { qi in qi.name == "lud16" })?.value
|
||||||
|
let keypair = FullKeypair(pubkey: our_pk, privkey: privkey)
|
||||||
|
self = ConnectURL(pubkey: pubkey, relay: relay_url, keypair: keypair, lud16: lud16)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(pubkey: Pubkey, relay: RelayURL, keypair: FullKeypair, lud16: String?) {
|
||||||
|
self.pubkey = pubkey
|
||||||
|
self.relay = relay
|
||||||
|
self.keypair = keypair
|
||||||
|
self.lud16 = lud16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Models an NWC wallet transaction
|
||||||
|
struct Transaction: Decodable, Equatable, Hashable {
|
||||||
|
let type: String
|
||||||
|
let invoice: String?
|
||||||
|
let description: String?
|
||||||
|
let description_hash: String?
|
||||||
|
let preimage: String?
|
||||||
|
let payment_hash: String?
|
||||||
|
let amount: Int64
|
||||||
|
let fees_paid: Int64?
|
||||||
|
let created_at: UInt64 // unixtimestamp, // invoice/payment creation time
|
||||||
|
let expires_at: UInt64? // unixtimestamp, // invoice expiration time, optional if not applicable
|
||||||
|
let settled_at: UInt64? // unixtimestamp, // invoice/payment settlement time, optional if unpaid
|
||||||
|
//"metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ struct EventActionBar: View {
|
|||||||
|
|
||||||
generator.impactOccurred()
|
generator.impactOccurred()
|
||||||
|
|
||||||
damus_state.postbox.send(like_ev)
|
damus_state.nostrNetwork.postbox.send(like_ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Helper structures
|
// MARK: Helper structures
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct RepostAction: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.postbox.send(boost)
|
damus_state.nostrNetwork.postbox.send(boost)
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
|
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
|
||||||
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
|
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ struct AddRelayView: View {
|
|||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
typealias UpdateError = NostrNetworkManager.UserRelayListManager.UpdateError
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text("Add relay", comment: "Title text to indicate user to an add a relay.")
|
Text("Add relay", comment: "Title text to indicate user to an add a relay.")
|
||||||
@@ -82,38 +84,21 @@ struct AddRelayView: View {
|
|||||||
new_relay = "wss://" + new_relay
|
new_relay = "wss://" + new_relay
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = RelayURL(new_relay),
|
guard let url = RelayURL(new_relay) else {
|
||||||
let ev = state.contacts.event,
|
relayAddErrorTitle = NSLocalizedString("Invalid relay address", comment: "Heading for an error when adding a relay")
|
||||||
let keypair = state.keypair.to_full() else {
|
relayAddErrorMessage = NSLocalizedString("Please check the address and try again", comment: "Tip for an error where the relay address being added is invalid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = RelayInfo.rw
|
|
||||||
let descriptor = RelayDescriptor(url: url, info: info)
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try state.pool.add_relay(descriptor)
|
try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite))
|
||||||
relayAddErrorTitle = nil // Clear error title
|
relayAddErrorTitle = nil // Clear error title
|
||||||
relayAddErrorMessage = nil // Clear error message
|
relayAddErrorMessage = nil // Clear error message
|
||||||
} catch RelayError.RelayAlreadyExists {
|
}
|
||||||
relayAddErrorTitle = NSLocalizedString("Duplicate relay", comment: "Title of the duplicate relay error message.")
|
catch {
|
||||||
relayAddErrorMessage = NSLocalizedString("The relay you are trying to add is already added.\nYou're all set!", comment: "An error message that appears when the user attempts to add a relay that has already been added.")
|
present_sheet(.error(self.humanReadableError(for: error)))
|
||||||
return
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.pool.connect(to: [url])
|
|
||||||
|
|
||||||
if let new_ev = add_relay(ev: ev, keypair: keypair, current_relays: state.pool.our_descriptors, relay: url, info: info) {
|
|
||||||
process_contact_event(state: state, ev: ev)
|
|
||||||
|
|
||||||
state.pool.send(.event(new_ev))
|
|
||||||
}
|
|
||||||
|
|
||||||
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
|
|
||||||
state.postbox.send(relay_metadata)
|
|
||||||
}
|
|
||||||
new_relay = ""
|
new_relay = ""
|
||||||
|
|
||||||
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
@@ -134,6 +119,17 @@ struct AddRelayView: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func humanReadableError(for error: any Error) -> ErrorView.UserPresentableError {
|
||||||
|
guard let error = error as? UpdateError else {
|
||||||
|
return .init(
|
||||||
|
user_visible_description: NSLocalizedString("An unknown error occurred while adding a relay.", comment: "Title of an unknown relay error message."),
|
||||||
|
tip: NSLocalizedString("Please contact support.", comment: "Tip for an unknown relay error message."),
|
||||||
|
technical_info: error.localizedDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return error.humanReadableError
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ struct ChatEventView: View {
|
|||||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
generator.impactOccurred()
|
generator.impactOccurred()
|
||||||
|
|
||||||
damus_state.postbox.send(like_ev)
|
damus_state.nostrNetwork.postbox.send(like_ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
var action_bar: some View {
|
var action_bar: some View {
|
||||||
|
|||||||
+116
-71
@@ -18,11 +18,26 @@ struct ConfigView: View {
|
|||||||
@State var delete_account_warning: Bool = false
|
@State var delete_account_warning: Bool = false
|
||||||
@State var confirm_delete_account: Bool = false
|
@State var confirm_delete_account: Bool = false
|
||||||
@State var delete_text: String = ""
|
@State var delete_text: String = ""
|
||||||
|
@State private var searchText: String = ""
|
||||||
|
|
||||||
@ObservedObject var settings: UserSettingsStore
|
@ObservedObject var settings: UserSettingsStore
|
||||||
|
|
||||||
|
// String constants
|
||||||
private let DELETE_KEYWORD = "DELETE"
|
private let DELETE_KEYWORD = "DELETE"
|
||||||
|
private let keysTitle = NSLocalizedString("Keys", comment: "Settings section for managing keys")
|
||||||
|
private let appearanceTitle = NSLocalizedString("Appearance and filters", comment: "Section header for text, appearance, and content filter settings")
|
||||||
|
private let searchUniverseTitle = NSLocalizedString("Search / Universe", comment: "Section header for search/universe settings")
|
||||||
|
private let notificationsTitle = NSLocalizedString("Notifications", comment: "Section header for Damus notifications")
|
||||||
|
private let zapsTitle = NSLocalizedString("Zaps", comment: "Section header for zap settings")
|
||||||
|
private let translationTitle = NSLocalizedString("Translation", comment: "Section header for text and appearance settings")
|
||||||
|
private let reactionsTitle = NSLocalizedString("Reactions", comment: "Section header for reactions settings")
|
||||||
|
private let developerTitle = NSLocalizedString("Developer", comment: "Section header for developer settings")
|
||||||
|
private let firstAidTitle = NSLocalizedString("First Aid", comment: "Section header for first aid tools and settings")
|
||||||
|
private let signOutTitle = NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account.")
|
||||||
|
private let deleteAccountTitle = NSLocalizedString("Delete Account", comment: "Button to delete the user's account.")
|
||||||
|
private let versionTitle = NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")
|
||||||
|
private let copyString = NSLocalizedString("Copy", comment: "Context menu option for copying the version of damus.")
|
||||||
|
|
||||||
init(state: DamusState) {
|
init(state: DamusState) {
|
||||||
self.state = state
|
self.state = state
|
||||||
_settings = ObservedObject(initialValue: state.settings)
|
_settings = ObservedObject(initialValue: state.settings)
|
||||||
@@ -31,91 +46,122 @@ struct ConfigView: View {
|
|||||||
func textColor() -> Color {
|
func textColor() -> Color {
|
||||||
colorScheme == .light ? DamusColors.black : DamusColors.white
|
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showSettingsButton(title : String)->Bool{
|
||||||
|
return searchText.isEmpty || title.lowercased().contains(searchText.lowercased())
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(value: Route.KeySettings(keypair: state.keypair)) {
|
// Keys
|
||||||
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "Key", color: .purple)
|
if showSettingsButton(title: keysTitle){
|
||||||
}
|
NavigationLink(value:Route.KeySettings(keypair: state.keypair)){
|
||||||
|
IconLabel(keysTitle,img_name:"Key",color:.purple)
|
||||||
NavigationLink(value: Route.AppearanceSettings(settings: settings)) {
|
|
||||||
IconLabel(NSLocalizedString("Appearance and filters", comment: "Section header for text, appearance, and content filter settings"), img_name: "eye", color: .red)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(value: Route.SearchSettings(settings: settings)) {
|
|
||||||
IconLabel(NSLocalizedString("Search/Universe", comment: "Section header for search/universe settings"), img_name: "search", color: .red)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(value: Route.NotificationSettings(settings: settings)) {
|
|
||||||
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "notification-bell-on", color: .blue)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(value: Route.ZapSettings(settings: settings)) {
|
|
||||||
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "zap.fill", color: .orange)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(value: Route.TranslationSettings(settings: settings)) {
|
|
||||||
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe", color: .green)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(value: Route.ReactionsSettings(settings: settings)) {
|
|
||||||
IconLabel(NSLocalizedString("Reactions", comment: "Section header for reactions settings"), img_name: "shaka.fill", color: .purple)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(value: Route.DeveloperSettings(settings: settings)) {
|
|
||||||
IconLabel(NSLocalizedString("Developer", comment: "Section header for developer settings"), img_name: "magic-stick2.fill", color: DamusColors.adaptableBlack)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(value: Route.FirstAidSettings(settings: settings)) {
|
|
||||||
IconLabel(NSLocalizedString("First Aid", comment: "Section header for first aid tools and settings"), img_name: "help2", color: .red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
|
|
||||||
Button(action: {
|
|
||||||
if state.keypair.privkey == nil {
|
|
||||||
logout(state)
|
|
||||||
} else {
|
|
||||||
confirm_logout = true
|
|
||||||
}
|
}
|
||||||
}, label: {
|
}
|
||||||
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), image: "logout")
|
// Appearance and filters
|
||||||
.foregroundColor(textColor())
|
if showSettingsButton(title: appearanceTitle){
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
NavigationLink(value:Route.AppearanceSettings(settings: settings)){
|
||||||
})
|
IconLabel(appearanceTitle,img_name:"eye",color:.red)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Search/Universe
|
||||||
|
if showSettingsButton(title: searchUniverseTitle){
|
||||||
|
NavigationLink(value: Route.SearchSettings(settings: settings)){
|
||||||
|
IconLabel(searchUniverseTitle,img_name:"search",color:.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if state.is_privkey_user {
|
//Notifications
|
||||||
Section(header: Text("Permanently Delete Account", comment: "Section title for deleting the user")) {
|
if showSettingsButton(title: notificationsTitle){
|
||||||
|
NavigationLink(value: Route.NotificationSettings(settings: settings)){
|
||||||
|
IconLabel(notificationsTitle,img_name:"notification-bell-on",color:.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Zaps
|
||||||
|
if showSettingsButton(title: zapsTitle){
|
||||||
|
NavigationLink(value: Route.ZapSettings(settings: settings)){
|
||||||
|
IconLabel(zapsTitle,img_name:"zap.fill",color:.orange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Translation
|
||||||
|
if showSettingsButton(title: translationTitle){
|
||||||
|
NavigationLink(value: Route.TranslationSettings(settings: settings)){
|
||||||
|
IconLabel(translationTitle,img_name:"globe",color:.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Reactions
|
||||||
|
if showSettingsButton(title: reactionsTitle){
|
||||||
|
NavigationLink(value: Route.ReactionsSettings(settings: settings)){
|
||||||
|
IconLabel(reactionsTitle,img_name:"shaka.fill",color:.purple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Developer
|
||||||
|
if showSettingsButton(title: developerTitle){
|
||||||
|
NavigationLink(value: Route.DeveloperSettings(settings: settings)){
|
||||||
|
IconLabel(developerTitle,img_name:"magic-stick2.fill",color:DamusColors.adaptableBlack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//First Aid
|
||||||
|
if showSettingsButton(title: firstAidTitle){
|
||||||
|
NavigationLink(value: Route.FirstAidSettings(settings: settings)){
|
||||||
|
IconLabel(firstAidTitle,img_name:"help2",color: .red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Sign out Section
|
||||||
|
if showSettingsButton(title: signOutTitle){
|
||||||
|
Section(signOutTitle){
|
||||||
Button(action: {
|
Button(action: {
|
||||||
delete_account_warning = true
|
if state.keypair.privkey == nil {
|
||||||
|
logout(state)
|
||||||
|
} else {
|
||||||
|
confirm_logout = true
|
||||||
|
}
|
||||||
}, label: {
|
}, label: {
|
||||||
Label(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), image: "delete")
|
Label(signOutTitle, image: "logout")
|
||||||
|
.foregroundColor(textColor())
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.foregroundColor(.red)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Delete Account
|
||||||
Section(
|
if showSettingsButton(title: deleteAccountTitle){
|
||||||
header: Text(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")),
|
if state.is_privkey_user {
|
||||||
footer: Text("").padding(.bottom, tabHeight + getSafeAreaBottom())
|
Section(header: Text("Permanently Delete Account", comment: "Section title for deleting the user")) {
|
||||||
) {
|
Button(action: {
|
||||||
Text(verbatim: VersionInfo.version)
|
delete_account_warning = true
|
||||||
.contextMenu {
|
}, label: {
|
||||||
Button {
|
Label(deleteAccountTitle, image: "delete")
|
||||||
UIPasteboard.general.string = VersionInfo.version
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
} label: {
|
.foregroundColor(.red)
|
||||||
Label(NSLocalizedString("Copy", comment: "Context menu option for copying the version of damus."), image: "copy2")
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Version info
|
||||||
|
if showSettingsButton(title: versionTitle) {
|
||||||
|
Section(
|
||||||
|
header: Text(versionTitle),
|
||||||
|
footer: Text("").padding(.bottom, tabHeight + getSafeAreaBottom())
|
||||||
|
) {
|
||||||
|
Text(verbatim: VersionInfo.version)
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = VersionInfo.version
|
||||||
|
} label: {
|
||||||
|
Label(copyString, image: "copy2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
|
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.searchable(text: $searchText, prompt: NSLocalizedString("Search within settings", comment: "Text to prompt the user to search settings."))
|
||||||
.alert(NSLocalizedString("WARNING:\n\nTHIS WILL SIGN AN EVENT THAT DELETES THIS ACCOUNT.\n\nYOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.\n\n ARE YOU SURE YOU WANT TO CONTINUE?", comment: "Alert for deleting the users account."), isPresented: $delete_account_warning) {
|
.alert(NSLocalizedString("WARNING:\n\nTHIS WILL SIGN AN EVENT THAT DELETES THIS ACCOUNT.\n\nYOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.\n\n ARE YOU SURE YOU WANT TO CONTINUE?", comment: "Alert for deleting the users account."), isPresented: $delete_account_warning) {
|
||||||
|
|
||||||
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
|
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
|
||||||
@@ -136,7 +182,7 @@ struct ConfigView: View {
|
|||||||
let ev = created_deleted_account_profile(keypair: keypair) else {
|
let ev = created_deleted_account_profile(keypair: keypair) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.postbox.send(ev)
|
state.nostrNetwork.postbox.send(ev)
|
||||||
logout(state)
|
logout(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +200,6 @@ struct ConfigView: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ConfigView_Previews: PreviewProvider {
|
struct ConfigView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -131,14 +131,14 @@ struct DMChatView: View, KeyboardReadable {
|
|||||||
.map(\.asString)
|
.map(\.asString)
|
||||||
.joined(separator: "")
|
.joined(separator: "")
|
||||||
|
|
||||||
guard let dm = create_dm(content, to_pk: pubkey, tags: tags, keypair: damus_state.keypair) else {
|
guard let dm = NIP04.create_dm(content, to_pk: pubkey, tags: tags, keypair: damus_state.keypair) else {
|
||||||
print("error creating dm")
|
print("error creating dm")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dms.draft = ""
|
dms.draft = ""
|
||||||
|
|
||||||
damus_state.postbox.send(dm)
|
damus_state.nostrNetwork.postbox.send(dm)
|
||||||
|
|
||||||
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
|
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
|
||||||
|
|
||||||
@@ -176,46 +176,6 @@ struct DMChatView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func encrypt_message(message: String, privkey: Privkey, to_pk: Pubkey, encoding: EncEncoding = .base64) -> String? {
|
|
||||||
let iv = random_bytes(count: 16).bytes
|
|
||||||
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let utf8_message = Data(message.utf8).bytes
|
|
||||||
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch encoding {
|
|
||||||
case .base64:
|
|
||||||
return encode_dm_base64(content: enc_message.bytes, iv: iv)
|
|
||||||
case .bech32:
|
|
||||||
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func create_encrypted_event(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: FullKeypair, created_at: UInt32, kind: UInt32) -> NostrEvent? {
|
|
||||||
let privkey = keypair.privkey
|
|
||||||
|
|
||||||
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return NostrEvent(content: enc_content, keypair: keypair.to_keypair(), kind: kind, tags: tags, createdAt: created_at)
|
|
||||||
}
|
|
||||||
|
|
||||||
func create_dm(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent?
|
|
||||||
{
|
|
||||||
let created = created_at ?? UInt32(Date().timeIntervalSince1970)
|
|
||||||
|
|
||||||
guard let keypair = keypair.to_full() else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return create_encrypted_event(message, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created, kind: 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
/// Layers the given views behind this ``TextEditor``.
|
/// Layers the given views behind this ``TextEditor``.
|
||||||
func textEditorBackground<V>(@ViewBuilder _ content: () -> V) -> some View where V : View {
|
func textEditorBackground<V>(@ViewBuilder _ content: () -> V) -> some View where V : View {
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ struct EventLoaderView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
damus_state.pool.unsubscribe(sub_id: subscription_uuid)
|
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subscription_uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe(filters: [NostrFilter]) {
|
func subscribe(filters: [NostrFilter]) {
|
||||||
damus_state.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
|
damus_state.nostrNetwork.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
|
||||||
damus_state.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
|
damus_state.nostrNetwork.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ struct MenuItems: View {
|
|||||||
if let full_keypair = self.damus_state.keypair.to_full(),
|
if let full_keypair = self.damus_state.keypair.to_full(),
|
||||||
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) {
|
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) {
|
||||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
|
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
|
||||||
damus_state.postbox.send(new_mutelist_ev)
|
damus_state.nostrNetwork.postbox.send(new_mutelist_ev)
|
||||||
}
|
}
|
||||||
let muted = damus_state.mutelist_manager.is_event_muted(event)
|
let muted = damus_state.mutelist_manager.is_event_muted(event)
|
||||||
isMutedThread = muted
|
isMutedThread = muted
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject {
|
|||||||
case .zap, .zap_request:
|
case .zap, .zap_request:
|
||||||
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
|
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
|
||||||
return .loaded(route: Route.Zaps(target: zap.target))
|
return .loaded(route: Route.Zaps(target: zap.target))
|
||||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .zap, .zap_request, .nwc_request, .nwc_response, .http_auth, .status:
|
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list:
|
||||||
return .unknown_or_unsupported_kind
|
return .unknown_or_unsupported_kind
|
||||||
}
|
}
|
||||||
case .naddr(let naddr):
|
case .naddr(let naddr):
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ struct AddMuteItemView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.mutelist_manager.set_mutelist(mutelist)
|
state.mutelist_manager.set_mutelist(mutelist)
|
||||||
state.postbox.send(mutelist)
|
state.nostrNetwork.postbox.send(mutelist)
|
||||||
}
|
}
|
||||||
|
|
||||||
new_text = ""
|
new_text = ""
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ struct MutelistView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
damus_state.mutelist_manager.set_mutelist(new_ev)
|
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||||
damus_state.postbox.send(new_ev)
|
damus_state.nostrNetwork.postbox.send(new_ev)
|
||||||
updateMuteItems()
|
updateMuteItems()
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
|
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
|
||||||
|
|||||||
@@ -86,10 +86,10 @@ struct DamusAppNotificationView: View {
|
|||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let url = try await damus_state.purple.generate_verified_ln_checkout_link(product_template_name: product_template_name)
|
let url = try await damus_state.purple.generate_verified_ln_checkout_link(product_template_name: product_template_name)
|
||||||
await self.open_url(url: url)
|
self.open_url(url: url)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
await self.open_url(url: damus_state.purple.environment.purple_landing_page_url().appendingPathComponent("checkout"))
|
self.open_url(url: damus_state.purple.environment.purple_landing_page_url().appendingPathComponent("checkout"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,27 @@ import SwiftUI
|
|||||||
|
|
||||||
class NotificationFilter: ObservableObject, Equatable {
|
class NotificationFilter: ObservableObject, Equatable {
|
||||||
@Published var state: NotificationFilterState
|
@Published var state: NotificationFilterState
|
||||||
@Published var fine_filter: FriendFilter
|
@Published var friend_filter: FriendFilter
|
||||||
|
@Published var hellthread_notifications_disabled: Bool
|
||||||
|
@Published var hellthread_notification_max_pubkeys: Int
|
||||||
|
|
||||||
static func == (lhs: NotificationFilter, rhs: NotificationFilter) -> Bool {
|
static func == (lhs: NotificationFilter, rhs: NotificationFilter) -> Bool {
|
||||||
return lhs.state == rhs.state && lhs.fine_filter == rhs.fine_filter
|
return lhs.state == rhs.state
|
||||||
|
&& lhs.friend_filter == rhs.friend_filter
|
||||||
|
&& lhs.hellthread_notifications_disabled == rhs.hellthread_notifications_disabled
|
||||||
|
&& lhs.hellthread_notification_max_pubkeys == rhs.hellthread_notification_max_pubkeys
|
||||||
}
|
}
|
||||||
|
|
||||||
init(state: NotificationFilterState = .all, fine_filter: FriendFilter = .all) {
|
init(
|
||||||
|
state: NotificationFilterState = .all,
|
||||||
|
friend_filter: FriendFilter = .all,
|
||||||
|
hellthread_notifications_disabled: Bool = false,
|
||||||
|
hellthread_notification_max_pubkeys: Int = DEFAULT_HELLTHREAD_MAX_PUBKEYS
|
||||||
|
) {
|
||||||
self.state = state
|
self.state = state
|
||||||
self.fine_filter = fine_filter
|
self.friend_filter = friend_filter
|
||||||
|
self.hellthread_notifications_disabled = hellthread_notifications_disabled
|
||||||
|
self.hellthread_notification_max_pubkeys = hellthread_notification_max_pubkeys
|
||||||
}
|
}
|
||||||
|
|
||||||
func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] {
|
func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] {
|
||||||
@@ -26,8 +38,14 @@ class NotificationFilter: ObservableObject, Equatable {
|
|||||||
if !self.state.filter(item) {
|
if !self.state.filter(item) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let item = item.filter({ self.fine_filter.filter(contacts: contacts, pubkey: $0.pubkey) }) {
|
if let item = item.filter({ ev in
|
||||||
|
self.friend_filter.filter(contacts: contacts, pubkey: ev.pubkey) &&
|
||||||
|
(!hellthread_notifications_disabled || !ev.is_hellthread(max_pubkeys: hellthread_notification_max_pubkeys)) &&
|
||||||
|
// Allow notes that are created no more than 3 seconds in the future
|
||||||
|
// to account for natural clock skew between sender and receiver.
|
||||||
|
ev.age >= -3
|
||||||
|
}) {
|
||||||
acc.append(item)
|
acc.append(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +83,9 @@ struct NotificationsView: View {
|
|||||||
NotificationTab(
|
NotificationTab(
|
||||||
NotificationFilter(
|
NotificationFilter(
|
||||||
state: .all,
|
state: .all,
|
||||||
fine_filter: filter.fine_filter
|
friend_filter: filter.friend_filter,
|
||||||
|
hellthread_notifications_disabled: state.settings.hellthread_notifications_disabled,
|
||||||
|
hellthread_notification_max_pubkeys: state.settings.hellthread_notification_max_pubkeys
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.tag(NotificationFilterState.all)
|
.tag(NotificationFilterState.all)
|
||||||
@@ -73,7 +93,9 @@ struct NotificationsView: View {
|
|||||||
NotificationTab(
|
NotificationTab(
|
||||||
NotificationFilter(
|
NotificationFilter(
|
||||||
state: .zaps,
|
state: .zaps,
|
||||||
fine_filter: filter.fine_filter
|
friend_filter: filter.friend_filter,
|
||||||
|
hellthread_notifications_disabled: state.settings.hellthread_notifications_disabled,
|
||||||
|
hellthread_notification_max_pubkeys: state.settings.hellthread_notification_max_pubkeys
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.tag(NotificationFilterState.zaps)
|
.tag(NotificationFilterState.zaps)
|
||||||
@@ -81,7 +103,9 @@ struct NotificationsView: View {
|
|||||||
NotificationTab(
|
NotificationTab(
|
||||||
NotificationFilter(
|
NotificationFilter(
|
||||||
state: .replies,
|
state: .replies,
|
||||||
fine_filter: filter.fine_filter
|
friend_filter: filter.friend_filter,
|
||||||
|
hellthread_notifications_disabled: state.settings.hellthread_notifications_disabled,
|
||||||
|
hellthread_notification_max_pubkeys: state.settings.hellthread_notification_max_pubkeys
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.tag(NotificationFilterState.replies)
|
.tag(NotificationFilterState.replies)
|
||||||
@@ -98,20 +122,20 @@ struct NotificationsView: View {
|
|||||||
}
|
}
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
|
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
|
||||||
FriendsButton(filter: $filter.fine_filter)
|
FriendsButton(filter: $filter.friend_filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: filter.fine_filter) { val in
|
.onChange(of: filter.friend_filter) { val in
|
||||||
state.settings.friend_filter = val
|
state.settings.friend_filter = val
|
||||||
self.subtitle = filter.fine_filter.description()
|
self.subtitle = filter.friend_filter.description()
|
||||||
}
|
}
|
||||||
.onChange(of: filter_state) { val in
|
.onChange(of: filter_state) { val in
|
||||||
filter.state = val
|
filter.state = val
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
self.filter.fine_filter = state.settings.friend_filter
|
self.filter.friend_filter = state.settings.friend_filter
|
||||||
self.subtitle = filter.fine_filter.description()
|
self.subtitle = filter.friend_filter.description()
|
||||||
filter.state = filter_state
|
filter.state = filter_state
|
||||||
}
|
}
|
||||||
.safeAreaInset(edge: .top, spacing: 0) {
|
.safeAreaInset(edge: .top, spacing: 0) {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class SuggestedUsersViewModel: ObservableObject {
|
|||||||
|
|
||||||
private func subscribeToSuggestedProfiles(pubkeys: [Pubkey]) {
|
private func subscribeToSuggestedProfiles(pubkeys: [Pubkey]) {
|
||||||
let filter = NostrFilter(kinds: [.metadata], authors: pubkeys)
|
let filter = NostrFilter(kinds: [.metadata], authors: pubkeys)
|
||||||
damus_state.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
|
damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||||
|
|||||||
+17
-19
@@ -763,7 +763,7 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts?
|
|||||||
}
|
}
|
||||||
// If there are no exact matches to the highlight, try to load a draft for the same highlight source
|
// If there are no exact matches to the highlight, try to load a draft for the same highlight source
|
||||||
// We do this to improve UX, because we don't want to leave the post view blank if they only selected a slightly different piece of text from before.
|
// We do this to improve UX, because we don't want to leave the post view blank if they only selected a slightly different piece of text from before.
|
||||||
var other_matches = drafts.highlights
|
let other_matches = drafts.highlights
|
||||||
.filter { $0.key.source == highlight.source }
|
.filter { $0.key.source == highlight.source }
|
||||||
// It's not an exact match, so there is no way of telling which one is the preferred draft. So just load the first one we found.
|
// It's not an exact match, so there is no way of telling which one is the preferred draft. So just load the first one we found.
|
||||||
return other_matches.first?.value
|
return other_matches.first?.value
|
||||||
@@ -865,33 +865,31 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
|
|||||||
|
|
||||||
|
|
||||||
var content = post.string
|
var content = post.string
|
||||||
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
|
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: "\n")
|
||||||
|
|
||||||
if !imagesString.isEmpty {
|
if !imagesString.isEmpty {
|
||||||
content.append(" " + imagesString + " ")
|
content.append("\n\n" + imagesString)
|
||||||
}
|
}
|
||||||
|
|
||||||
var tags: [[String]] = []
|
var tags: [[String]] = []
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .replying_to(let replying_to):
|
case .replying_to(let replying_to):
|
||||||
// start off with the reply tags
|
// start off with the reply tags
|
||||||
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
||||||
|
|
||||||
case .quoting(let ev):
|
case .quoting(let ev):
|
||||||
content.append(" nostr:" + bech32_note_id(ev.id))
|
content.append("\n\nnostr:" + bech32_note_id(ev.id))
|
||||||
|
|
||||||
if let quoted_ev = state.events.lookup(ev.id) {
|
tags.append(["q", ev.id.hex()]);
|
||||||
tags.append(["p", quoted_ev.pubkey.hex()])
|
|
||||||
}
|
if let quoted_ev = state.events.lookup(ev.id) {
|
||||||
case .posting(let postTarget):
|
tags.append(["p", quoted_ev.pubkey.hex()])
|
||||||
break
|
}
|
||||||
case .highlighting(let draft):
|
case .posting, .highlighting, .sharing:
|
||||||
break
|
break
|
||||||
case .sharing(_):
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// append additional tags
|
// append additional tags
|
||||||
@@ -913,7 +911,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NostrPost(content: content, kind: .text, tags: tags)
|
return NostrPost(content: content.trimmingCharacters(in: .whitespacesAndNewlines), kind: .text, tags: tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSupportedVideo(url: URL?) -> Bool {
|
func isSupportedVideo(url: URL?) -> Bool {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ struct EditMetadataView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.postbox.send(metadata_ev)
|
damus_state.nostrNetwork.postbox.send(metadata_ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_ln_valid(ln: String) -> Bool {
|
func is_ln_valid(ln: String) -> Bool {
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ struct EditPictureControl: View {
|
|||||||
|
|
||||||
var accessibility_value: String? {
|
var accessibility_value: String? {
|
||||||
if style.first_time_setup {
|
if style.first_time_setup {
|
||||||
if let current_image_url = model.current_image_url {
|
if model.current_image_url != nil {
|
||||||
switch self.model.context {
|
switch self.model.context {
|
||||||
case .normal:
|
case .normal:
|
||||||
return NSLocalizedString("Image is setup", comment: "Accessibility value on image control")
|
return NSLocalizedString("Image is setup", comment: "Accessibility value on image control")
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ struct ProfileName: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func name_choice(profile: Profile?) -> String {
|
func name_choice(profile: Profile?) -> String {
|
||||||
return prefix == "@" ? current_display_name(profile: profile).username.truncate(maxLength: 50) : current_display_name(profile: profile).displayName.truncate(maxLength: 50)
|
let displayName = current_display_name(profile: profile)
|
||||||
|
let untruncatedName = prefix == "@" ? displayName.username : displayName.displayName
|
||||||
|
return untruncatedName.truncate(maxLength: 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
func onlyzapper(profile: Profile?) -> Bool {
|
func onlyzapper(profile: Profile?) -> Bool {
|
||||||
|
|||||||
@@ -122,6 +122,12 @@ struct ProfileView: View {
|
|||||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||||
var filters = ContentFilters.defaults(damus_state: damus_state)
|
var filters = ContentFilters.defaults(damus_state: damus_state)
|
||||||
filters.append(fstate.filter)
|
filters.append(fstate.filter)
|
||||||
|
switch fstate {
|
||||||
|
case .posts, .posts_and_replies:
|
||||||
|
filters.append({ profile.pubkey == $0.pubkey })
|
||||||
|
case .conversations:
|
||||||
|
filters.append({ profile.conversation_events.contains($0.id) } )
|
||||||
|
}
|
||||||
return ContentFilters(filters: filters).filter
|
return ContentFilters(filters: filters).filter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +219,7 @@ struct ProfileView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
damus_state.mutelist_manager.set_mutelist(new_ev)
|
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||||
damus_state.postbox.send(new_ev)
|
damus_state.nostrNetwork.postbox.send(new_ev)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
|
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
|
||||||
@@ -390,18 +396,18 @@ struct ProfileView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let relays = profile.relays {
|
if let relays = profile.relay_urls {
|
||||||
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
|
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
|
||||||
let noun_string = pluralizedString(key: "relays_count", count: relays.keys.count)
|
let noun_string = pluralizedString(key: "relays_count", count: relays.count)
|
||||||
let noun_text = Text(noun_string).font(.subheadline).foregroundColor(.gray)
|
let noun_text = Text(noun_string).font(.subheadline).foregroundColor(.gray)
|
||||||
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'.")
|
let relay_text = Text("\(Text(verbatim: relays.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'.")
|
||||||
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
|
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
|
||||||
NavigationLink(value: Route.RelayConfig) {
|
NavigationLink(value: Route.RelayConfig) {
|
||||||
relay_text
|
relay_text
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
} else {
|
} else {
|
||||||
NavigationLink(value: Route.UserRelays(relays: Array(relays.keys).sorted())) {
|
NavigationLink(value: Route.UserRelays(relays: relays.sorted())) {
|
||||||
relay_text
|
relay_text
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
@@ -429,6 +435,17 @@ struct ProfileView: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tabs: [(String, FilterState)] {
|
||||||
|
var tabs = [
|
||||||
|
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
|
||||||
|
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
|
||||||
|
]
|
||||||
|
if profile.pubkey != damus_state.pubkey && !profile.conversation_events.isEmpty {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
return tabs
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
@@ -440,10 +457,7 @@ struct ProfileView: View {
|
|||||||
aboutSection
|
aboutSection
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
CustomPicker(tabs: [
|
CustomPicker(tabs: tabs, selection: $filter_state)
|
||||||
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
|
|
||||||
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
|
|
||||||
], selection: $filter_state)
|
|
||||||
Divider()
|
Divider()
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
}
|
}
|
||||||
@@ -455,6 +469,9 @@ struct ProfileView: View {
|
|||||||
if filter_state == FilterState.posts_and_replies {
|
if filter_state == FilterState.posts_and_replies {
|
||||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies))
|
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies))
|
||||||
}
|
}
|
||||||
|
if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty {
|
||||||
|
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.conversations))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Theme.safeAreaInsets?.left)
|
.padding(.horizontal, Theme.safeAreaInsets?.left)
|
||||||
.zIndex(-yOffset > navbarHeight ? 0 : 1)
|
.zIndex(-yOffset > navbarHeight ? 0 : 1)
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ fileprivate struct ProfileActionSheetZapButton: View {
|
|||||||
.foregroundColor(Color.primary)
|
.foregroundColor(Color.primary)
|
||||||
.profile_button_style(scheme: colorScheme)
|
.profile_button_style(scheme: colorScheme)
|
||||||
case .zap_success:
|
case .zap_success:
|
||||||
Image("checkmark")
|
Image("checkmark-damus")
|
||||||
.foregroundColor(Color.green)
|
.foregroundColor(Color.green)
|
||||||
.profile_button_style(scheme: colorScheme)
|
.profile_button_style(scheme: colorScheme)
|
||||||
case .zap_failure:
|
case .zap_failure:
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ struct DamusPurpleAccountView: View {
|
|||||||
func profile_display_name() -> String {
|
func profile_display_name() -> String {
|
||||||
let profile_txn: NdbTxn<ProfileRecord?>? = damus_state.profiles.lookup_with_timestamp(account.pubkey)
|
let profile_txn: NdbTxn<ProfileRecord?>? = damus_state.profiles.lookup_with_timestamp(account.pubkey)
|
||||||
let profile: NdbProfile? = profile_txn?.unsafeUnownedValue?.profile
|
let profile: NdbProfile? = profile_txn?.unsafeUnownedValue?.profile
|
||||||
let display_name = parse_display_name(profile: profile, pubkey: account.pubkey).displayName
|
let display_name = DisplayName(profile: profile, pubkey: account.pubkey).displayName
|
||||||
return display_name
|
return display_name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,11 +57,14 @@ struct QRScanNSECView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
struct QRScanNSECView_Previews: PreviewProvider {
|
||||||
@State var showQR = true
|
@State static var showQR = true
|
||||||
@State var privKeyFound = false
|
@State static var privKeyFound = false
|
||||||
@State var shouldSaveKey = true
|
@State static var shouldSaveKey = true
|
||||||
return QRScanNSECView(showQR: $showQR,
|
|
||||||
privKeyFound: $privKeyFound,
|
static var previews: some View {
|
||||||
codeScannerCompletion: { _ in })
|
QRScanNSECView(showQR: $showQR,
|
||||||
|
privKeyFound: $privKeyFound,
|
||||||
|
codeScannerCompletion: { _ in })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ struct RelayFilterView: View {
|
|||||||
self.state = state
|
self.state = state
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
|
|
||||||
//_relays = State(initialValue: state.pool.descriptors)
|
//_relays = State(initialValue: state.networkManager.pool.descriptors)
|
||||||
}
|
}
|
||||||
|
|
||||||
var relays: [RelayDescriptor] {
|
var relays: [RelayPool.RelayDescriptor] {
|
||||||
return state.pool.our_descriptors
|
return state.nostrNetwork.pool.our_descriptors
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ enum RelayTab: Int, CaseIterable{
|
|||||||
|
|
||||||
struct RelayConfigView: View {
|
struct RelayConfigView: View {
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
@State var relays: [RelayDescriptor]
|
@State var relays: [RelayPool.RelayDescriptor]
|
||||||
@State private var showActionButtons = false
|
@State private var showActionButtons = false
|
||||||
@State var show_add_relay: Bool = false
|
@State var show_add_relay: Bool = false
|
||||||
@State var selectedTab = 0
|
@State var selectedTab = 0
|
||||||
@@ -32,15 +32,15 @@ struct RelayConfigView: View {
|
|||||||
|
|
||||||
init(state: DamusState) {
|
init(state: DamusState) {
|
||||||
self.state = state
|
self.state = state
|
||||||
_relays = State(initialValue: state.pool.our_descriptors)
|
_relays = State(initialValue: state.nostrNetwork.pool.our_descriptors)
|
||||||
UITabBar.appearance().isHidden = true
|
UITabBar.appearance().isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var recommended: [RelayDescriptor] {
|
var recommended: [RelayPool.RelayDescriptor] {
|
||||||
let rs: [RelayDescriptor] = []
|
let rs: [RelayPool.RelayDescriptor] = []
|
||||||
let recommended_relay_addresses = get_default_bootstrap_relays()
|
let recommended_relay_addresses = get_default_bootstrap_relays()
|
||||||
return recommended_relay_addresses.reduce(into: rs) { xs, x in
|
return recommended_relay_addresses.reduce(into: rs) { xs, x in
|
||||||
xs.append(RelayDescriptor(url: x, info: .rw))
|
xs.append(RelayPool.RelayDescriptor(url: x, info: .readWrite))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ struct RelayConfigView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.relays_changed)) { _ in
|
.onReceive(handle_notify(.relays_changed)) { _ in
|
||||||
self.relays = state.pool.our_descriptors
|
self.relays = state.nostrNetwork.pool.our_descriptors
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
notify(.display_tabbar(false))
|
notify(.display_tabbar(false))
|
||||||
@@ -109,7 +109,7 @@ struct RelayConfigView: View {
|
|||||||
.ignoresSafeArea(.all)
|
.ignoresSafeArea(.all)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RelayList(title: String, relayList: [RelayDescriptor], recommended: Bool) -> some View {
|
func RelayList(title: String, relayList: [RelayPool.RelayDescriptor], recommended: Bool) -> some View {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(title)
|
Text(title)
|
||||||
|
|||||||
@@ -25,32 +25,12 @@ struct RelayDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func check_connection() -> Bool {
|
func check_connection() -> Bool {
|
||||||
for relay in state.pool.relays {
|
return state.nostrNetwork.userRelayList.getUserCurrentRelayList()?.relays.keys.contains(self.relay) == true
|
||||||
if relay.id == self.relay {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveRelayButton(_ keypair: FullKeypair) -> some View {
|
func RemoveRelayButton(_ keypair: FullKeypair) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
guard let ev = state.contacts.event else {
|
self.removeRelay()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let descriptors = state.pool.our_descriptors
|
|
||||||
guard let new_ev = remove_relay( ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
process_contact_event(state: state, ev: new_ev)
|
|
||||||
state.postbox.send(new_ev)
|
|
||||||
|
|
||||||
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
|
|
||||||
state.postbox.send(relay_metadata)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Disconnect", comment: "Button to disconnect from the relay.")
|
Text("Disconnect", comment: "Button to disconnect from the relay.")
|
||||||
@@ -63,19 +43,7 @@ struct RelayDetailView: View {
|
|||||||
|
|
||||||
func ConnectRelayButton(_ keypair: FullKeypair) -> some View {
|
func ConnectRelayButton(_ keypair: FullKeypair) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
guard let ev_before_add = state.contacts.event else {
|
self.connectRelay()
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay, info: .rw) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
process_contact_event(state: state, ev: ev_after_add)
|
|
||||||
state.postbox.send(ev_after_add)
|
|
||||||
|
|
||||||
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
|
|
||||||
state.postbox.send(relay_metadata)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Connect", comment: "Button to connect to the relay.")
|
Text("Connect", comment: "Button to connect to the relay.")
|
||||||
@@ -208,13 +176,33 @@ struct RelayDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var relay_object: Relay? {
|
private var relay_object: RelayPool.Relay? {
|
||||||
state.pool.get_relay(relay)
|
state.nostrNetwork.pool.get_relay(relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var relay_connection: RelayConnection? {
|
private var relay_connection: RelayConnection? {
|
||||||
relay_object?.connection
|
relay_object?.connection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeRelay() {
|
||||||
|
do {
|
||||||
|
try state.nostrNetwork.userRelayList.remove(relayURL: self.relay)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
present_sheet(.error(error.humanReadableError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectRelay() {
|
||||||
|
do {
|
||||||
|
try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite))
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
present_sheet(.error(error.humanReadableError))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RelayDetailView_Previews: PreviewProvider {
|
struct RelayDetailView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ struct RelayStatusView: View {
|
|||||||
|
|
||||||
struct RelayStatusView_Previews: PreviewProvider {
|
struct RelayStatusView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let connection = test_damus_state.pool.get_relay(RelayURL("wss://relay.damus.io")!)!.connection
|
let connection = test_damus_state.nostrNetwork.pool.get_relay(RelayURL("wss://relay.damus.io")!)!.connection
|
||||||
RelayStatusView(connection: connection)
|
RelayStatusView(connection: connection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ struct RelayToggle: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var relay_connection: RelayConnection? {
|
private var relay_connection: RelayConnection? {
|
||||||
state.pool.get_relay(relay_id)?.connection
|
state.nostrNetwork.pool.get_relay(relay_id)?.connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct RelayView: View {
|
|||||||
self.recommended = recommended
|
self.recommended = recommended
|
||||||
self.model_cache = state.relay_model_cache
|
self.model_cache = state.relay_model_cache
|
||||||
_showActionButtons = showActionButtons
|
_showActionButtons = showActionButtons
|
||||||
let relay_state = RelayView.get_relay_state(pool: state.pool, relay: relay)
|
let relay_state = RelayView.get_relay_state(pool: state.nostrNetwork.pool, relay: relay)
|
||||||
self._relay_state = State(initialValue: relay_state)
|
self._relay_state = State(initialValue: relay_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ struct RelayView: View {
|
|||||||
AddButton(keypair: keypair)
|
AddButton(keypair: keypair)
|
||||||
} else {
|
} else {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
remove_action(privkey: keypair.privkey)
|
Task { await remove_action(privkey: keypair.privkey) }
|
||||||
}) {
|
}) {
|
||||||
Text("Added", comment: "Button to show relay server is already added to list.")
|
Text("Added", comment: "Button to show relay server is already added to list.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -105,7 +105,7 @@ struct RelayView: View {
|
|||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.relays_changed)) { _ in
|
.onReceive(handle_notify(.relays_changed)) { _ in
|
||||||
self.relay_state = RelayView.get_relay_state(pool: state.pool, relay: self.relay)
|
self.relay_state = RelayView.get_relay_state(pool: state.nostrNetwork.pool, relay: self.relay)
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
state.nav.push(route: Route.RelayDetail(relay: relay, metadata: model_cache.model(with_relay_id: relay)?.metadata))
|
state.nav.push(route: Route.RelayDetail(relay: relay, metadata: model_cache.model(with_relay_id: relay)?.metadata))
|
||||||
@@ -113,46 +113,30 @@ struct RelayView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var relay_connection: RelayConnection? {
|
private var relay_connection: RelayConnection? {
|
||||||
state.pool.get_relay(relay)?.connection
|
state.nostrNetwork.pool.get_relay(relay)?.connection
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_action(keypair: FullKeypair) {
|
func add_action(keypair: FullKeypair) async {
|
||||||
guard let ev_before_add = state.contacts.event else {
|
do {
|
||||||
return
|
try await state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite))
|
||||||
}
|
}
|
||||||
guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay, info: .rw) else {
|
catch {
|
||||||
return
|
present_sheet(.error(error.humanReadableError))
|
||||||
}
|
|
||||||
process_contact_event(state: state, ev: ev_after_add)
|
|
||||||
state.postbox.send(ev_after_add)
|
|
||||||
|
|
||||||
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
|
|
||||||
state.postbox.send(relay_metadata)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove_action(privkey: Privkey) {
|
func remove_action(privkey: Privkey) async {
|
||||||
guard let ev = state.contacts.event else {
|
do {
|
||||||
return
|
try await state.nostrNetwork.userRelayList.remove(relayURL: relay)
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
let descriptors = state.pool.our_descriptors
|
present_sheet(.error(error.humanReadableError))
|
||||||
guard let keypair = state.keypair.to_full(),
|
|
||||||
let new_ev = remove_relay(ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
process_contact_event(state: state, ev: new_ev)
|
|
||||||
state.postbox.send(new_ev)
|
|
||||||
|
|
||||||
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
|
|
||||||
state.postbox.send(relay_metadata)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddButton(keypair: FullKeypair) -> some View {
|
func AddButton(keypair: FullKeypair) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
add_action(keypair: keypair)
|
Task { await add_action(keypair: keypair) }
|
||||||
}) {
|
}) {
|
||||||
Text("Add", comment: "Button to add relay server to list.")
|
Text("Add", comment: "Button to add relay server to list.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -170,7 +154,7 @@ struct RelayView: View {
|
|||||||
|
|
||||||
func RemoveButton(privkey: Privkey, showText: Bool) -> some View {
|
func RemoveButton(privkey: Privkey, showText: Bool) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
remove_action(privkey: privkey)
|
Task { await remove_action(privkey: privkey) }
|
||||||
}) {
|
}) {
|
||||||
if showText {
|
if showText {
|
||||||
Text("Disconnect", comment: "Button to disconnect from a relay server.")
|
Text("Disconnect", comment: "Button to disconnect from a relay server.")
|
||||||
|
|||||||
@@ -132,9 +132,9 @@ struct ReportView_Previews: PreviewProvider {
|
|||||||
let ds = test_damus_state
|
let ds = test_damus_state
|
||||||
VStack {
|
VStack {
|
||||||
|
|
||||||
ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!)
|
ReportView(postbox: ds.nostrNetwork.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!)
|
||||||
|
|
||||||
ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
|
ReportView(postbox: ds.nostrNetwork.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user