Compare commits

..

1 Commits

Author SHA1 Message Date
tyiu f7a7e7ed8a Fix translation export script by upgrading nostr-sdk-swift dependency to support Mac Catalyst
Changelog-Fixed: Fixed translation export script by upgrading nostr-sdk-swift dependency to support Mac Catalyst
Closes: https://github.com/damus-io/damus/issues/2841
Fixes: 24c3e61a4b ("Fix translation export script")
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-02-10 08:36:04 -05:00
115 changed files with 1484 additions and 5645 deletions
-1
View File
@@ -6,7 +6,6 @@ _[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 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
- [ ] 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)
-5
View File
@@ -1,5 +0,0 @@
### Acknowledgements and licenses
1. This product contains code derived from [Nostr SDK iOS](https://github.com/nostr-sdk/nostr-sdk-ios). [License](https://github.com/nostr-sdk/nostr-sdk-ios/blob/40df800c6749d7ce0b6fd7328e76cbc0dc71c87b/LICENSE)
2. This product includes software developed by the "Marcin Krzyzanowski" (http://krzyzanowskim.com/). [License](https://github.com/krzyzanowskim/CryptoSwift/blob/e74bbbfbef939224b242ae7c342a90e60b88b5ce/LICENSE)
-59
View File
@@ -1,62 +1,3 @@
## [1.13.1] - 2025-03-21
### Fixed
- Fixed an issue where threads would not load properly (Daniel DAquino)
[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 DAquino)
- Added user-friendly error view for errors around the app that would not fit in other places (Daniel DAquino)
- 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 DAquino)
- Profile image cropping tools (Daniel DAquino)
- 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 DAquino)
- Improved reliability of picture selector (Daniel DAquino)
- 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 DAquino)
- Fixed issue where app would need a restart for new NWC wallets to work (Daniel DAquino)
- Fixed overly sensitive horizontal swipe on thread chat view (Daniel DAquino)
- 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 DAquino)
- Fixed an issue where events on a thread view would occasionally disappear (Daniel DAquino)
- Improved robustness of the URL handler (Daniel DAquino)
- 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
### Added
@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
<true/>
<key>com.apple.security.app-sandbox</key>
@@ -18,7 +18,7 @@ struct NotificationExtensionState: HeadlessDamusState {
let lnurls: LNUrls
init?() {
guard let ndb = Ndb(owns_db_file: false) else { return nil }
guard let ndb = try? Ndb(owns_db_file: false) else { return nil }
self.ndb = ndb
guard let keypair = get_saved_keypair() else { return nil }
@@ -5,32 +5,15 @@
// Created by Daniel DAquino on 2023-11-10.
//
import Kingfisher
import ImageIO
import UserNotifications
import Foundation
import UniformTypeIdentifiers
import Intents
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
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) {
configureKingfisherCache()
self.contentHandler = contentHandler
guard let nostr_event_json = request.content.userInfo["nostr_event"] as? String,
@@ -57,16 +40,9 @@ class NotificationService: UNNotificationServiceExtension {
return
}
let sender_profile = {
let txn = state.ndb.lookup_profile(nostr_event.pubkey)
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
let txn = state.ndb.lookup_profile(nostr_event.pubkey)
let profile = txn?.unsafeUnownedValue?.profile
let name = Profile.displayName(profile: profile, pubkey: nostr_event.pubkey).displayName
// 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
@@ -80,7 +56,7 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(content)
return
}
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
Log.debug("should_display_notification failed", for: .push_notifications)
// We should not display notification for this event. Suppress notification.
@@ -89,7 +65,7 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(request.content)
return
}
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
Log.debug("generate_local_notification_object failed", for: .push_notifications)
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
@@ -98,58 +74,15 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(request.content)
return
}
Task {
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 {
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: name, notify: notification_object, state: state) else {
Log.debug("NotificationFormatter.format_message failed", for: .push_notifications)
return
}
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)
}
contentHandler(improvedContent)
}
}
@@ -162,162 +95,3 @@ 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()
}
+3 -32
View File
@@ -1,32 +1,3 @@
// swift-tools-version: 6.0
// 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"),
]
)
dependencies: [
.Package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
]
+1 -1
View File
@@ -8,7 +8,7 @@ A twitter-like [nostr][nostr] client for iPhone, iPad and MacOS.
[nostr]: https://github.com/fiatjaf/nostr
## How is Damus better than X/Twitter?
## How is Damus better than twitter?
There are no toxic algorithms.\
You can send or receive zaps (satoshis) without asking for permission.\
[There is no central database](https://fiatjaf.com/nostr.html). Therefore, Damus is censorship resistant.\
-1
View File
@@ -1 +0,0 @@
Fix q tags
+72 -210
View File
@@ -22,7 +22,6 @@
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.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 */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
@@ -189,7 +188,6 @@
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; };
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.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 */; };
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
@@ -408,22 +406,10 @@
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAE2A194075008FC15A /* PinkGradient.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 */; };
5CB017212D2D985E00A9ED05 /* 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 */; };
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 */; };
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */; };
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */; };
@@ -642,6 +628,7 @@
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.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 */; };
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
@@ -932,6 +919,7 @@
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.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 */; };
BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; };
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; };
@@ -1057,12 +1045,6 @@
D703D7B62C67118200A400EA /* String+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9472A9AD44700DC3548 /* String+extension.swift */; };
D703D7B72C67118F00A400EA /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
D703D7B82C6711A000A400EA /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; };
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */; };
D706C5B02D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */; };
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */; };
D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5B62D602A050027C627 /* QueueableNotify.swift */; };
D706C5B82D602A110027C627 /* QueueableNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5B62D602A050027C627 /* QueueableNotify.swift */; };
D706C5B92D602A110027C627 /* QueueableNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5B62D602A050027C627 /* QueueableNotify.swift */; };
D70A3B172B02DCE5008BD568 /* NotificationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */; };
D70D90982CDED61800CD0534 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D70D90972CDED61800CD0534 /* CodeScanner */; };
D70D909C2CDED7B200CD0534 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D70D909B2CDED7B200CD0534 /* CodeScanner */; };
@@ -1095,9 +1077,6 @@
D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */; };
D7373BA82B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.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 */; };
D73E5E162C6A9619007EB227 /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
@@ -1207,6 +1186,7 @@
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.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 */; };
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
D73E5E902C6A97F4007EB227 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
@@ -1468,9 +1448,9 @@
D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
D74EA0932D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
D74EA0952D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
D74EA0932D2E77B9002290DD /* LoadableThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */; };
D74EA0942D2E77B9002290DD /* LoadableThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */; };
D74EA0952D2E77B9002290DD /* LoadableThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */; };
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
@@ -1493,18 +1473,6 @@
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; };
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.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 */; };
D798D21B2B0856F200234419 /* NdbTagsIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDD1AE12A6B3074001CD4DF /* NdbTagsIterator.swift */; };
D798D21C2B0857E400234419 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
@@ -1532,6 +1500,9 @@
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */; };
D7B76C902C825042003A16CB /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
D7B76C912C82507F003A16CB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; };
D7BEE6F32D37AE1B00CF659F /* NostrSDK in Frameworks */ = {isa = PBXBuildFile; productRef = D7BEE6F22D37AE1B00CF659F /* NostrSDK */; };
D7BEE6F52D37B20400CF659F /* NostrSDK in Frameworks */ = {isa = PBXBuildFile; productRef = D7BEE6F42D37B20400CF659F /* NostrSDK */; };
D7BEE6F72D37B21400CF659F /* NostrSDK in Frameworks */ = {isa = PBXBuildFile; productRef = D7BEE6F62D37B21400CF659F /* NostrSDK */; };
D7BEE6F92D37B37400CF659F /* DraftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BEE6F82D37B37400CF659F /* DraftTests.swift */; };
D7C48C0B2D12DE0C00A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */; };
D7C48C0D2D12E34900A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0C2D12E34900A3BACF /* SwiftyCrop */; };
@@ -1636,19 +1607,6 @@
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
D7D68FF92C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
D7D68FFA2C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */; };
D7DB1FDF2D5A78CE00CF06DA /* NIP44.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */; };
D7DB1FE02D5A78CE00CF06DA /* NIP44.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */; };
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */; };
D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */; };
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */; };
D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */; };
D7DB1FEE2D5AC51B00CF06DA /* NIP44v2EncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */; };
D7DB1FF12D5AC5D700CF06DA /* nip44.vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */; };
D7DB1FF32D5AC5EA00CF06DA /* LICENSES in Resources */ = {isa = PBXBuildFile; fileRef = D7DB1FF22D5AC5E400CF06DA /* LICENSES */; };
D7DB93052D66A44100DA1EE5 /* 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 */; };
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
@@ -1831,7 +1789,6 @@
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>"; };
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>"; };
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>"; };
@@ -2392,12 +2349,8 @@
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>"; };
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>"; };
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>"; };
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>"; };
@@ -2439,6 +2392,7 @@
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>"; };
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>"; };
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>"; };
@@ -2452,8 +2406,6 @@
D703D7222C66E47100A400EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D703D7262C66E47100A400EA /* highlighter action extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "highlighter action extension.entitlements"; sourceTree = "<group>"; };
D703D72A2C66F29500A400EA /* getSelection.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = getSelection.js; sourceTree = "<group>"; };
D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoSaveIndicatorView.swift; sourceTree = "<group>"; };
D706C5B62D602A050027C627 /* QueueableNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueableNotify.swift; sourceTree = "<group>"; };
D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFormatter.swift; sourceTree = "<group>"; };
D7100C552B76F8E600C59298 /* PurpleViewPrimitives.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleViewPrimitives.swift; sourceTree = "<group>"; };
D7100C572B76FC8400C59298 /* MarketingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketingContentView.swift; sourceTree = "<group>"; };
@@ -2478,7 +2430,6 @@
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>"; };
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>"; };
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>"; };
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessDamusState.swift; sourceTree = "<group>"; };
@@ -2488,7 +2439,7 @@
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Zaps+.swift"; sourceTree = "<group>"; };
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
D74EA08D2D2E271E002290DD /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableNostrEventView.swift; sourceTree = "<group>"; };
D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableThreadView.swift; sourceTree = "<group>"; };
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
@@ -2505,9 +2456,6 @@
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>"; };
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>"; };
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>"; };
@@ -2532,11 +2480,6 @@
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; };
D7D68FF82C9E01B60015A515 /* KFClickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFClickable.swift; sourceTree = "<group>"; };
D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44.swift; sourceTree = "<group>"; };
D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44v2EncryptionTests.swift; 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>"; };
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Undistractor.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>"; };
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
@@ -2580,8 +2523,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D7BEE6F32D37AE1B00CF659F /* NostrSDK in Frameworks */,
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */,
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
D70D90982CDED61800CD0534 /* CodeScanner in Frameworks */,
D7C48C0B2D12DE0C00A3BACF /* SwiftyCrop in Frameworks */,
@@ -2611,8 +2554,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D7BEE6F52D37B20400CF659F /* NostrSDK in Frameworks */,
82D6FC862CD9A4A600C925F4 /* MarkdownUI in Frameworks */,
D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */,
82D6FC8A2CD9A54600C925F4 /* SwipeActions in Frameworks */,
D7F360292CEBBE34009D34DA /* CodeScanner in Frameworks */,
D7C48C0D2D12E34900A3BACF /* SwiftyCrop in Frameworks */,
@@ -2628,7 +2571,7 @@
files = (
D703D7AF2C670FB700A400EA /* MarkdownUI in Frameworks */,
D73E5F9D2C6AA8E3007EB227 /* SwipeActions in Frameworks */,
D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */,
D7BEE6F72D37B21400CF659F /* NostrSDK in Frameworks */,
D73E5F762C6A997E007EB227 /* EmojiPicker in Frameworks */,
D703D7192C66E47100A400EA /* UniformTypeIdentifiers.framework in Frameworks */,
D7C48C0F2D12E35600A3BACF /* SwiftyCrop in Frameworks */,
@@ -2642,10 +2585,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4C5726BA2D72C6FA00E7FF82 /* Kingfisher in Frameworks */,
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */,
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2812,8 +2753,6 @@
children = (
4C0C03982A61E27B0098B3B8 /* bool_setting.wasm */,
4C0C03972A61E27B0098B3B8 /* primal.wasm */,
D7DB1FF22D5AC5E400CF06DA /* LICENSES */,
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */,
);
name = Fixtures;
sourceTree = "<group>";
@@ -3216,7 +3155,7 @@
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */,
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */,
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */,
D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -3253,10 +3192,6 @@
4C7D095A2A098C5C00943473 /* Wallet */ = {
isa = PBXGroup;
children = (
5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */,
5CB017302D4422D600A9ED05 /* NWCSettings.swift */,
5CB0172C2D42C76600A9ED05 /* BalanceView.swift */,
5CB017242D42C5BD00A9ED05 /* TransactionsView.swift */,
4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */,
4C7D095D2A098C5D00943473 /* WalletView.swift */,
4C7D09672A0AE9B200943473 /* NWCScannerView.swift */,
@@ -3282,12 +3217,10 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
D73B74E02D8365B40067BDBC /* ExtraFonts.swift */,
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */,
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */,
E04A37C52B544F090029650D /* URIParsing.swift */,
4C1D4FB02A7958E60024F453 /* VersionInfo.swift */,
D78F080A2D7F78B000FC6C75 /* WalletConnect */,
4C7D09612A098D0E00943473 /* WalletConnect.swift */,
4C198DF329F88D23004C165C /* Images */,
4C198DEA29F88C6B004C165C /* BlurHash */,
4CE4F0F329D779B5005914DB /* PostBox.swift */,
@@ -3337,6 +3270,7 @@
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */,
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -3398,7 +3332,6 @@
4CA3529C2A76AE47003BB08B /* Notify */ = {
isa = PBXGroup;
children = (
D706C5B62D602A050027C627 /* QueueableNotify.swift */,
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */,
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */,
4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */,
@@ -3646,9 +3579,7 @@
4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup;
children = (
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
D78F08152D7F7F5F00FC6C75 /* NIP04 */,
4C45E5002BED4CE10025A428 /* NIP10 */,
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
4CA3529C2A76AE47003BB08B /* Notify */,
@@ -3685,7 +3616,6 @@
4CE6DEF627F7A08200C66700 /* damusTests */ = {
isa = PBXGroup;
children = (
D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */,
D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */,
E06336A72B7582D600A88E6B /* Assets */,
D72A2D032AD9C165002AFF62 /* Mocking */,
@@ -3729,7 +3659,6 @@
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */,
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -3817,7 +3746,6 @@
4CF0ABF42985CD4200D66079 /* Posting */ = {
isa = PBXGroup;
children = (
D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */,
4CF0ABF52985CD5500D66079 /* UserSearch.swift */,
);
path = Posting;
@@ -3889,6 +3817,7 @@
children = (
BA3759902ABCCEBA0018D73B /* CameraModel.swift */,
BA3759912ABCCEBA0018D73B /* CameraService.swift */,
BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */,
BA3759892ABCCDE30018D73B /* ImageResizer.swift */,
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */,
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */,
@@ -3988,25 +3917,6 @@
path = Chat;
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 */ = {
isa = PBXGroup;
children = (
@@ -4036,14 +3946,6 @@
path = Utils;
sourceTree = "<group>";
};
D7DB1FDC2D5A77E500CF06DA /* NIP44 */ = {
isa = PBXGroup;
children = (
D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */,
);
path = NIP44;
sourceTree = "<group>";
};
E06336A72B7582D600A88E6B /* Assets */ = {
isa = PBXGroup;
children = (
@@ -4110,7 +4012,7 @@
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
D70D90972CDED61800CD0534 /* CodeScanner */,
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */,
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */,
D7BEE6F22D37AE1B00CF659F /* NostrSDK */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -4177,7 +4079,7 @@
82D6FC892CD9A54600C925F4 /* SwipeActions */,
D7F360282CEBBE34009D34DA /* CodeScanner */,
D7C48C0C2D12E34900A3BACF /* SwiftyCrop */,
D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */,
D7BEE6F42D37B20400CF659F /* NostrSDK */,
);
productName = "share extension";
productReference = 82D6FA972CD9820500C925F4 /* ShareExtension.appex */;
@@ -4206,7 +4108,7 @@
D73E5F9C2C6AA8E3007EB227 /* SwipeActions */,
D70D909B2CDED7B200CD0534 /* CodeScanner */,
D7C48C0E2D12E35600A3BACF /* SwiftyCrop */,
D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */,
D7BEE6F62D37B21400CF659F /* NostrSDK */,
);
productName = "highlighter action extension";
productReference = D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */;
@@ -4229,8 +4131,6 @@
packageProductDependencies = (
D789D11F2AFEFBF20083A7AB /* secp256k1 */,
D7EDED302B1290B80018B19C /* MarkdownUI */,
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */,
4C5726B92D72C6FA00E7FF82 /* Kingfisher */,
);
productName = DamusNotificationService;
productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */;
@@ -4318,7 +4218,7 @@
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */,
D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */,
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */,
D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */,
D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -4358,9 +4258,7 @@
buildActionMask = 2147483647;
files = (
E06336AB2B75850100A88E6B /* img_with_location.jpeg in Resources */,
D7DB1FF12D5AC5D700CF06DA /* nip44.vectors.json in Resources */,
4C0C039A2A61E27B0098B3B8 /* bool_setting.wasm in Resources */,
D7DB1FF32D5AC5EA00CF06DA /* LICENSES in Resources */,
4C0C03992A61E27B0098B3B8 /* primal.wasm in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -4458,7 +4356,6 @@
D7CBD1D42B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift in Sources */,
4C32B9572A9AD44700DC3548 /* Root.swift in Sources */,
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
5C8498032D5D150000F74FEB /* ZapExplainer.swift in Sources */,
504323A72A34915F006AE6DC /* RelayModel.swift in Sources */,
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */,
@@ -4571,6 +4468,7 @@
4C363A8428233689006E126D /* Parser.swift in Sources */,
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */,
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
@@ -4613,7 +4511,6 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */,
D72E12782BEED22500F4F781 /* Array.swift in Sources */,
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */,
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
@@ -4656,7 +4553,6 @@
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
D78F08142D7F78F900FC6C75 /* Response.swift in Sources */,
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
@@ -4682,7 +4578,6 @@
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */,
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */,
D73B74E12D8365BA0067BDBC /* ExtraFonts.swift in Sources */,
4C7D09622A098D0E00943473 /* WalletConnect.swift in Sources */,
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
@@ -4691,7 +4586,6 @@
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */,
D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */,
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */,
4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
@@ -4719,7 +4613,6 @@
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */,
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */,
5CB017252D42C5C400A9ED05 /* TransactionsView.swift in Sources */,
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
@@ -4769,8 +4662,6 @@
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */,
5CB017312D4422DB00A9ED05 /* NWCSettings.swift in Sources */,
D78F080D2D7F78EF00FC6C75 /* Request.swift in Sources */,
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */,
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */,
@@ -4779,7 +4670,6 @@
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
D7EFBA372CC322F300F45588 /* DamusVideoControlsView.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4C1253682A76D2470004F4B8 /* MuteNotify.swift in Sources */,
@@ -4789,7 +4679,7 @@
4CA927612A290E340098A105 /* EventShell.swift in Sources */,
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
D74EA0942D2E77B9002290DD /* LoadableThreadView.swift in Sources */,
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */,
@@ -4828,7 +4718,6 @@
50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */,
D7EDED212B117DCA0018B19C /* SequenceUtils.swift in Sources */,
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */,
D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */,
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
@@ -4850,7 +4739,6 @@
4C9146FE2A2A87C200DDEA40 /* nostrscript.c in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
D78F08182D7F7F7500FC6C75 /* NIP04.swift in Sources */,
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
F71694EC2A662292001F4053 /* SuggestedUsersViewModel.swift in Sources */,
@@ -4869,7 +4757,6 @@
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C9147002A2A891E00DDEA40 /* error.c in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
5CB0172F2D42C76A00A9ED05 /* BalanceView.swift in Sources */,
4C1253602A76CF890004F4B8 /* ScrollToTopNotify.swift in Sources */,
4CA3529E2A76AE67003BB08B /* FollowNotify.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
@@ -4944,12 +4831,10 @@
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */,
D7DB1FEE2D5AC51B00CF06DA /* NIP44v2EncryptionTests.swift in Sources */,
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
3A96E3FE2D6BCE3800AE1630 /* RepostedTests.swift in Sources */,
4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */,
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */,
@@ -5014,7 +4899,6 @@
82D6FABC2CD99F7900C925F4 /* refmap.c in Sources */,
82D6FABD2CD99F7900C925F4 /* verifier.c in Sources */,
82D6FABE2CD99F7900C925F4 /* NdbProfile.swift in Sources */,
D78F08112D7F78F900FC6C75 /* Response.swift in Sources */,
82D6FABF2CD99F7900C925F4 /* NdbTagIterator.swift in Sources */,
82D6FAC02CD99F7900C925F4 /* NdbNote.swift in Sources */,
82D6FAC12CD99F7900C925F4 /* AsciiCharacter.swift in Sources */,
@@ -5051,21 +4935,19 @@
82D6FAE02CD99F7900C925F4 /* DisplayTabBarNotify.swift in Sources */,
82D6FAE12CD99F7900C925F4 /* BroadcastNotify.swift in Sources */,
82D6FAE22CD99F7900C925F4 /* ComposeNotify.swift in Sources */,
D73B74E22D8365BA0067BDBC /* ExtraFonts.swift in Sources */,
82D6FAE32CD99F7900C925F4 /* FollowedNotify.swift in Sources */,
82D6FAE42CD99F7900C925F4 /* FollowNotify.swift in Sources */,
82D6FAE52CD99F7900C925F4 /* LikedNotify.swift in Sources */,
82D6FAE62CD99F7900C925F4 /* LocalNotificationNotify.swift in Sources */,
82D6FAE72CD99F7900C925F4 /* LoginNotify.swift in Sources */,
82D6FAE82CD99F7900C925F4 /* LogoutNotify.swift in Sources */,
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
82D6FAE92CD99F7900C925F4 /* NewMutesNotify.swift in Sources */,
82D6FAEA2CD99F7900C925F4 /* NewUnmutesNotify.swift in Sources */,
82D6FAEB2CD99F7900C925F4 /* Notify.swift in Sources */,
82D6FAEC2CD99F7900C925F4 /* OnlyZapsNotify.swift in Sources */,
82D6FAED2CD99F7900C925F4 /* PostNotify.swift in Sources */,
82D6FAEE2CD99F7900C925F4 /* PresentSheetNotify.swift in Sources */,
D74EA0932D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
D74EA0932D2E77B9002290DD /* LoadableThreadView.swift in Sources */,
82D6FAEF2CD99F7900C925F4 /* ProfileUpdatedNotify.swift in Sources */,
82D6FAF02CD99F7900C925F4 /* ReportNotify.swift in Sources */,
82D6FAF12CD99F7900C925F4 /* ScrollToTopNotify.swift in Sources */,
@@ -5084,7 +4966,6 @@
82D6FAFE2CD99F7900C925F4 /* Pubkey.swift in Sources */,
82D6FAFF2CD99F7900C925F4 /* NoteId.swift in Sources */,
82D6FB002CD99F7900C925F4 /* Referenced.swift in Sources */,
5CB0172D2D42C76A00A9ED05 /* BalanceView.swift in Sources */,
82D6FB012CD99F7900C925F4 /* Block.swift in Sources */,
82D6FB022CD99F7900C925F4 /* MigratedTypes.swift in Sources */,
82D6FB032CD99F7900C925F4 /* DamusDuration.swift in Sources */,
@@ -5092,11 +4973,9 @@
82D6FB052CD99F7900C925F4 /* MusicController.swift in Sources */,
82D6FB062CD99F7900C925F4 /* UserStatusView.swift in Sources */,
82D6FB072CD99F7900C925F4 /* UserStatus.swift in Sources */,
5CB017262D42C5C400A9ED05 /* TransactionsView.swift in Sources */,
82D6FB082CD99F7900C925F4 /* UserStatusSheet.swift in Sources */,
82D6FB092CD99F7900C925F4 /* SearchHeaderView.swift in Sources */,
82D6FB0A2CD99F7900C925F4 /* DamusGradient.swift in Sources */,
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */,
82D6FB0B2CD99F7900C925F4 /* AlbyGradient.swift in Sources */,
82D6FB0C2CD99F7900C925F4 /* GoldSupportGradient.swift in Sources */,
82D6FB0D2CD99F7900C925F4 /* PinkGradient.swift in Sources */,
@@ -5138,7 +5017,6 @@
82D6FB322CD99F7900C925F4 /* FillAndStroke.swift in Sources */,
82D6FB332CD99F7900C925F4 /* Array.swift in Sources */,
82D6FB342CD99F7900C925F4 /* VectorMath.swift in Sources */,
5C8498022D5D150000F74FEB /* ZapExplainer.swift in Sources */,
82D6FB352CD99F7900C925F4 /* OffsetExtension.swift in Sources */,
82D6FB362CD99F7900C925F4 /* RelayFilters.swift in Sources */,
82D6FB372CD99F7900C925F4 /* RelayModelCache.swift in Sources */,
@@ -5158,7 +5036,6 @@
82D6FB452CD99F7900C925F4 /* InputDismissKeyboard.swift in Sources */,
82D6FB462CD99F7900C925F4 /* Constants.swift in Sources */,
82D6FB472CD99F7900C925F4 /* LinkView.swift in Sources */,
D7DB1FDF2D5A78CE00CF06DA /* NIP44.swift in Sources */,
82D6FB482CD99F7900C925F4 /* PreviewCache.swift in Sources */,
82D6FB492CD99F7900C925F4 /* Theme.swift in Sources */,
82D6FB4A2CD99F7900C925F4 /* NIP05.swift in Sources */,
@@ -5184,7 +5061,6 @@
82D6FB5E2CD99F7900C925F4 /* CredentialHandler.swift in Sources */,
82D6FB5F2CD99F7900C925F4 /* KeyboardVisible.swift in Sources */,
82D6FB602CD99F7900C925F4 /* StringUtil.swift in Sources */,
D78F08172D7F7F7500FC6C75 /* NIP04.swift in Sources */,
82D6FB612CD99F7900C925F4 /* Router.swift in Sources */,
82D6FB622CD99F7900C925F4 /* Log.swift in Sources */,
82D6FB632CD99F7900C925F4 /* AVPlayer+Additions.swift in Sources */,
@@ -5199,6 +5075,7 @@
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */,
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */,
82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */,
82D6FB6F2CD99F7900C925F4 /* CameraService+Extensions.swift in Sources */,
82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */,
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */,
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */,
@@ -5413,7 +5290,6 @@
82D6FC432CD99F7900C925F4 /* ReactionView.swift in Sources */,
82D6FC442CD99F7900C925F4 /* EventActionBar.swift in Sources */,
82D6FC452CD99F7900C925F4 /* EventDetailBar.swift in Sources */,
D78F080C2D7F78EF00FC6C75 /* Request.swift in Sources */,
82D6FC462CD99F7900C925F4 /* ShareAction.swift in Sources */,
82D6FC472CD99F7900C925F4 /* RepostAction.swift in Sources */,
82D6FC482CD99F7900C925F4 /* ShareActionButton.swift in Sources */,
@@ -5430,7 +5306,6 @@
82D6FC522CD99F7900C925F4 /* DMView.swift in Sources */,
82D6FC532CD99F7900C925F4 /* EmptyTimelineView.swift in Sources */,
82D6FC542CD99F7900C925F4 /* EmptyUserSearchView.swift in Sources */,
D706C5B82D602A110027C627 /* QueueableNotify.swift in Sources */,
82D6FC552CD99F7900C925F4 /* EventView.swift in Sources */,
82D6FC562CD99F7900C925F4 /* EventDetailView.swift in Sources */,
82D6FC572CD99F7900C925F4 /* FollowButtonView.swift in Sources */,
@@ -5439,7 +5314,6 @@
82D6FC5A2CD99F7900C925F4 /* QRScanNSECView.swift in Sources */,
82D6FC5B2CD99F7900C925F4 /* NoteContentView.swift in Sources */,
82D6FC5C2CD99F7900C925F4 /* PostButton.swift in Sources */,
5CB017322D4422DB00A9ED05 /* NWCSettings.swift in Sources */,
82D6FC5D2CD99F7900C925F4 /* PostView.swift in Sources */,
82D6FC5E2CD99F7900C925F4 /* AttachMediaUtility.swift in Sources */,
82D6FC5F2CD99F7900C925F4 /* MediaPicker.swift in Sources */,
@@ -5519,7 +5393,6 @@
D73E5E412C6A97F4007EB227 /* GoldSupportGradient.swift in Sources */,
D73E5E422C6A97F4007EB227 /* PinkGradient.swift in Sources */,
D73E5E432C6A97F4007EB227 /* GrayGradient.swift in Sources */,
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */,
D73E5E442C6A97F4007EB227 /* DamusLogoGradient.swift in Sources */,
D73E5E452C6A97F4007EB227 /* DamusBackground.swift in Sources */,
D73E5E462C6A97F4007EB227 /* DamusLightGradient.swift in Sources */,
@@ -5589,8 +5462,8 @@
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */,
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */,
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */,
D73E5E8D2C6A97F4007EB227 /* CameraService+Extensions.swift in Sources */,
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */,
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */,
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */,
D773BC602C6D538500349F0A /* CommentItem.swift in Sources */,
D73E5E902C6A97F4007EB227 /* VideoCaptureProcessor.swift in Sources */,
@@ -5605,15 +5478,13 @@
D73E5E992C6A97F4007EB227 /* Liked.swift in Sources */,
D73E5E9A2C6A97F4007EB227 /* ProfileUpdate.swift in Sources */,
D73E5E9B2C6A97F4007EB227 /* PostBlock.swift in Sources */,
5CB017332D4422DB00A9ED05 /* NWCSettings.swift in Sources */,
D73E5E9C2C6A97F4007EB227 /* Reply.swift in Sources */,
D73E5E9D2C6A97F4007EB227 /* SearchModel.swift in Sources */,
D73E5E9E2C6A97F4007EB227 /* NostrFilter+Hashable.swift in Sources */,
D74EA0952D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
D74EA0952D2E77B9002290DD /* LoadableThreadView.swift in Sources */,
D73E5F912C6AA71B007EB227 /* InputDismissKeyboard.swift in Sources */,
D73E5E9F2C6A97F4007EB227 /* CreateAccountModel.swift in Sources */,
D73E5EA12C6A97F4007EB227 /* SignalModel.swift in Sources */,
5CB017272D42C5C400A9ED05 /* TransactionsView.swift in Sources */,
D73E5EA22C6A97F4007EB227 /* FollowTarget.swift in Sources */,
D73E5EA32C6A97F4007EB227 /* BookmarksManager.swift in Sources */,
D73E5EA42C6A97F4007EB227 /* EventsModel.swift in Sources */,
@@ -5621,7 +5492,6 @@
D73E5EA62C6A97F4007EB227 /* FollowersModel.swift in Sources */,
D73E5EA72C6A97F4007EB227 /* SearchHomeModel.swift in Sources */,
D73E5EA82C6A97F4007EB227 /* DirectMessageModel.swift in Sources */,
D78F08132D7F78F900FC6C75 /* Response.swift in Sources */,
D73E5EA92C6A97F4007EB227 /* Report.swift in Sources */,
D73E5EAA2C6A97F4007EB227 /* ZapsModel.swift in Sources */,
D73E5EAB2C6A97F4007EB227 /* DraftsModel.swift in Sources */,
@@ -5638,7 +5508,6 @@
D73E5EB42C6A97F4007EB227 /* NoteContent.swift in Sources */,
D73E5EB52C6A97F4007EB227 /* LongformEvent.swift in Sources */,
D73E5EB62C6A97F4007EB227 /* PushNotificationClient.swift in Sources */,
D706C5B92D602A110027C627 /* QueueableNotify.swift in Sources */,
D71AD8FD2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
D73E5EB72C6A97F4007EB227 /* HighlightEvent.swift in Sources */,
D73E5EB82C6A97F4007EB227 /* RelayConnection.swift in Sources */,
@@ -5697,12 +5566,10 @@
D73E5EF22C6A97F4007EB227 /* DamusPurpleURLSheetView.swift in Sources */,
D73E5EF32C6A97F4007EB227 /* DamusPurpleVerifyNpubView.swift in Sources */,
D73E5EF42C6A97F4007EB227 /* DamusPurpleAccountView.swift in Sources */,
5CB0172E2D42C76A00A9ED05 /* BalanceView.swift in Sources */,
D73E5EF52C6A97F4007EB227 /* DamusPurpleNewUserOnboardingView.swift in Sources */,
D73E5EF62C6A97F4007EB227 /* SearchingEventView.swift in Sources */,
D73E5EF72C6A97F4007EB227 /* PullDownSearch.swift in Sources */,
D73E5EF82C6A97F4007EB227 /* NotificationsView.swift in Sources */,
D73B74E32D8365BA0067BDBC /* ExtraFonts.swift in Sources */,
D73E5EF92C6A97F4007EB227 /* EventGroupView.swift in Sources */,
D73E5EFA2C6A97F4007EB227 /* NotificationItemView.swift in Sources */,
D73E5EFB2C6A97F4007EB227 /* ProfilePicturesView.swift in Sources */,
@@ -5757,7 +5624,6 @@
D73E5F292C6A97F4007EB227 /* ReplyDescription.swift in Sources */,
D73E5F2A2C6A97F4007EB227 /* RelativeTime.swift in Sources */,
D73E5F732C6A9885007EB227 /* TestData.swift in Sources */,
D78F08192D7F7F7500FC6C75 /* NIP04.swift in Sources */,
D73E5F2B2C6A97F4007EB227 /* ReplyPart.swift in Sources */,
D73E5F2C2C6A97F4007EB227 /* ProxyView.swift in Sources */,
D73E5F2D2C6A97F4007EB227 /* SelectedEventView.swift in Sources */,
@@ -5847,7 +5713,6 @@
D703D7A32C670E1D00A400EA /* nostr_bech32.c in Sources */,
D703D7992C670DF900A400EA /* sha256.c in Sources */,
D703D7972C670DED00A400EA /* wasm.c in Sources */,
5C8498042D5D150000F74FEB /* ZapExplainer.swift in Sources */,
D703D7842C670C4700A400EA /* SequenceUtils.swift in Sources */,
D703D7912C670D1E00A400EA /* DisplayName.swift in Sources */,
D703D7B02C6710A500A400EA /* Root.swift in Sources */,
@@ -5902,8 +5767,6 @@
D703D7712C670B6D00A400EA /* NdbProfile.swift in Sources */,
D703D7A22C670E1A00A400EA /* list.c in Sources */,
D703D7A42C670E3C00A400EA /* midl.c in Sources */,
D7DB1FE02D5A78CE00CF06DA /* NIP44.swift in Sources */,
D706C5B02D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
D703D7982C670DF200A400EA /* utf8.c in Sources */,
D703D78B2C670C9500A400EA /* MakeZapRequest.swift in Sources */,
D703D7862C670C6500A400EA /* NewUnmutesNotify.swift in Sources */,
@@ -6060,7 +5923,6 @@
D798D2232B0859B700234419 /* KeychainStorage.swift in Sources */,
D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
D7CE1B272B0BE224002EDAD4 /* bech32_util.c in Sources */,
D78F08122D7F78F900FC6C75 /* Response.swift in Sources */,
D7CCFC102B05880F00323D86 /* Id.swift in Sources */,
D7CB5D532B1174E900AD4105 /* DeepLPlan.swift in Sources */,
D7EDED282B1180940018B19C /* ImageUploadModel.swift in Sources */,
@@ -6070,11 +5932,9 @@
D7CE1B332B0BE6DE002EDAD4 /* Nostr.swift in Sources */,
D7CE1B3D2B0BE719002EDAD4 /* Verifiable.swift in Sources */,
D7CE1B382B0BE719002EDAD4 /* VeriferOptions.swift in Sources */,
D78F080F2D7F78EF00FC6C75 /* Request.swift in Sources */,
D7CCFC152B05891000323D86 /* Referenced.swift in Sources */,
D7CE1B2B2B0BE243002EDAD4 /* hex.c in Sources */,
D798D2222B08598A00234419 /* ReferencedId.swift in Sources */,
D78F081A2D7F803100FC6C75 /* NIP04.swift in Sources */,
D7B76C912C82507F003A16CB /* NIP98AuthenticatedRequest.swift in Sources */,
D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */,
D7CE1B192B0BE132002EDAD4 /* builder.c in Sources */,
@@ -6306,7 +6166,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 5;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -6329,7 +6189,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.14;
MARKETING_VERSION = 1.10;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -6375,7 +6235,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 5;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -6394,7 +6254,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.14;
MARKETING_VERSION = 1.10;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@@ -6413,8 +6273,8 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -6441,9 +6301,9 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
@@ -6465,8 +6325,8 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -6493,9 +6353,9 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
@@ -6581,6 +6441,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "share extension/share extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -6599,6 +6460,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6617,6 +6479,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "share extension/share extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -6631,6 +6494,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6649,6 +6513,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "highlighter action extension/highlighter action extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -6663,6 +6528,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6682,6 +6548,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "highlighter action extension/highlighter action extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -6696,6 +6563,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6714,6 +6582,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -6728,6 +6597,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6747,6 +6617,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -6761,6 +6632,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6846,7 +6718,7 @@
repositoryURL = "https://github.com/tyiu/EmojiPicker.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.2.0;
minimumVersion = 0.1.1;
};
};
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
@@ -6905,6 +6777,14 @@
minimumVersion = 1.14.1;
};
};
D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/rust-nostr/nostr-sdk-swift";
requirement = {
kind = revision;
revision = 27711a03ea7d977162595eea1d9b2d5a45f0b628;
};
};
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/benedom/SwiftyCrop";
@@ -6913,14 +6793,6 @@
revision = 454d0a0d4faf6f3a19c8d817ab9d7d27524bd79f;
};
};
D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git";
requirement = {
kind = revision;
revision = e74bbbfbef939224b242ae7c342a90e60b88b5ce;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -6939,11 +6811,6 @@
package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
productName = MarkdownUI;
};
4C5726B92D72C6FA00E7FF82 /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
4C649880286E0EE300EAE2B3 /* secp256k1 */ = {
isa = XCSwiftPackageProductDependency;
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
@@ -7039,6 +6906,21 @@
package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;
productName = SnapshotTesting;
};
D7BEE6F22D37AE1B00CF659F /* NostrSDK */ = {
isa = XCSwiftPackageProductDependency;
package = D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */;
productName = NostrSDK;
};
D7BEE6F42D37B20400CF659F /* NostrSDK */ = {
isa = XCSwiftPackageProductDependency;
package = D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */;
productName = NostrSDK;
};
D7BEE6F62D37B21400CF659F /* NostrSDK */ = {
isa = XCSwiftPackageProductDependency;
package = D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */;
productName = NostrSDK;
};
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */ = {
isa = XCSwiftPackageProductDependency;
package = D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */;
@@ -7054,26 +6936,6 @@
package = D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */;
productName = SwiftyCrop;
};
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */ = {
isa = XCSwiftPackageProductDependency;
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
productName = CryptoSwift;
};
D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */ = {
isa = XCSwiftPackageProductDependency;
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
productName = CryptoSwift;
};
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */ = {
isa = XCSwiftPackageProductDependency;
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
productName = CryptoSwift;
};
D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */ = {
isa = XCSwiftPackageProductDependency;
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
productName = CryptoSwift;
};
D7EDED242B117F7C0018B19C /* MarkdownUI */ = {
isa = XCSwiftPackageProductDependency;
package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
@@ -1,5 +1,5 @@
{
"originHash" : "06318d35ee2e6bd681b95591e67da33a9461b48a3c652e58bd9d1a6f0d82bdac",
"originHash" : "fa2b0ad84b4bd1a962ffbe49810548db7c9d7131f4a1fd4b4af06ff4c6de0a44",
"pins" : [
{
"identity" : "codescanner",
@@ -9,21 +9,13 @@
"revision" : "9fa582f4b36c69c2a55bff5fb3377eb170ae273c"
}
},
{
"identity" : "cryptoswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
"state" : {
"revision" : "e74bbbfbef939224b242ae7c342a90e60b88b5ce"
}
},
{
"identity" : "emojikit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiKit",
"state" : {
"revision" : "47a4b1402de26be0299dcb4d667c1faaf21a7874",
"version" : "0.2.0"
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
"version" : "0.1.2"
}
},
{
@@ -31,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiPicker.git",
"state" : {
"revision" : "3f48903721eae223238ff0af17c22d6373d33813",
"version" : "0.2.0"
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
"version" : "0.1.1"
}
},
{
@@ -53,6 +45,14 @@
"version" : "7.6.1"
}
},
{
"identity" : "nostr-sdk-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/rust-nostr/nostr-sdk-swift",
"state" : {
"revision" : "27711a03ea7d977162595eea1d9b2d5a45f0b628"
}
},
{
"identity" : "secp256k1.swift",
"kind" : "remoteSourceControl",

Before

Width:  |  Height:  |  Size: 216 B

After

Width:  |  Height:  |  Size: 216 B

+2 -2
View File
@@ -232,7 +232,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
flusher = .once({ pe in
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
Task { @MainActor in
await WalletConnect.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 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)
}
})
}
@@ -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)
let delay = damus_state.settings.nozaps ? nil : 5.0
let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
+5 -51
View File
@@ -10,68 +10,22 @@ import SwiftUI
struct Reposted: View {
let damus: DamusState
let pubkey: Pubkey
let target: NostrEvent
@State var reposts: Int
init(damus: DamusState, pubkey: Pubkey, target: NostrEvent) {
self.damus = damus
self.pubkey = pubkey
self.target = target
self.reposts = damus.boosts.counts[target.id] ?? 1
}
var body: some View {
HStack(alignment: .center) {
Image("repost")
.foregroundColor(Color.gray)
// Show profile picture of the reposter only if the reposter is not the author of the reposted note.
if pubkey != target.pubkey {
ProfilePicView(pubkey: pubkey, size: eventview_pfp_size(.small), highlight: .none, profiles: damus.profiles, disable_animation: damus.settings.disable_animation)
.onTapGesture {
show_profile_action_sheet_if_enabled(damus_state: damus, pubkey: pubkey)
}
.onLongPressGesture(minimumDuration: 0.1) {
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
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)
}
ProfileName(pubkey: pubkey, damus: damus, show_nip5_domain: false)
.foregroundColor(Color.gray)
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
.foregroundColor(Color.gray)
}
.onReceive(handle_notify(.update_stats), perform: { note_id in
guard note_id == target.id else { return }
let repost_count = damus.boosts.counts[target.id]
if let repost_count, reposts != repost_count {
reposts = repost_count
}
})
}
}
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 {
static var previews: some View {
let test_state = test_damus_state
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note)
Reposted(damus: test_state, pubkey: test_state.pubkey)
}
}
+14 -37
View File
@@ -94,12 +94,12 @@ struct SelectableText: View {
case show_mute_word_view(highlighted_text: String)
func should_show_highlight_post_view() -> Bool {
guard case .show_highlight_post_view = self else { return false }
guard case .show_highlight_post_view(let highlighted_text) = self else { return false }
return true
}
func should_show_mute_word_view() -> Bool {
guard case .show_mute_word_view = self else { return false }
guard case .show_mute_word_view(let highlighted_text) = self else { return false }
return true
}
@@ -119,23 +119,16 @@ struct SelectableText: View {
fileprivate class TextView: UITextView {
var postHighlight: (String) -> Void
var muteWord: (String) -> Void
private let enableHighlighting: Bool
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void, enableHighlighting: Bool) {
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void) {
self.postHighlight = postHighlight
self.muteWord = muteWord
self.enableHighlighting = enableHighlighting
super.init(frame: frame, textContainer: textContainer)
if enableHighlighting {
self.delegate = self
}
}
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 {
if action == #selector(highlightText(_:)) {
@@ -149,44 +142,23 @@ fileprivate class TextView: UITextView {
return super.canPerformAction(action, withSender: sender)
}
private func getSelectedText() -> String? {
func getSelectedText() -> String? {
guard let selectedRange = self.selectedTextRange else { return nil }
return self.text(in: selectedRange)
}
@objc private func highlightText(_ sender: Any?) {
@objc public func highlightText(_ sender: Any?) {
guard let selectedText = self.getSelectedText() else { return }
self.postHighlight(selectedText)
}
@objc private func muteText(_ sender: Any?) {
@objc public func muteText(_ sender: Any?) {
guard let selectedText = self.getSelectedText() else { return }
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 {
let attributedString: AttributedString
@@ -200,7 +172,7 @@ fileprivate struct TextViewRepresentable: UIViewRepresentable {
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord, enableHighlighting: enableHighlighting)
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord)
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
@@ -211,6 +183,11 @@ fileprivate struct TextViewRepresentable: UIViewRepresentable {
view.textContainerInset.right = 1.0
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
}
+4 -4
View File
@@ -245,7 +245,7 @@ struct SupporterBadge_Previews: PreviewProvider {
.frame(width: 100)
}
Text(verbatim: "Double star (just shape itself, with alt background color, to show it adapts to background well)")
Text("Double star (just shape itself, with alt background color, to show it adapts to background well)")
.multilineTextAlignment(.center)
if #available(iOS 17.0, *) {
@@ -257,13 +257,13 @@ struct SupporterBadge_Previews: PreviewProvider {
.background(Color.blue)
}
Text(verbatim: "Double star (fallback for iOS 16 and below)")
Text("Double star (fallback for iOS 16 and below)")
HStack(alignment: .center) {
DoubleStar.Fallback(size: 17, starOffset: 5)
}
Text(verbatim: "Double star (fallback for iOS 16 and below, with alt color limitation shown)")
Text("Double star (fallback for iOS 16 and below, with alt color limitation shown)")
.multilineTextAlignment(.center)
HStack(alignment: .center) {
+45 -59
View File
@@ -222,6 +222,12 @@ struct ContentView: View {
navigationCoordinator.push(route: Route.Script(script: model))
}
func open_profile(pubkey: Pubkey) {
let profile_model = ProfileModel(pubkey: pubkey, damus: damus_state!)
let followers = FollowersModel(damus_state: damus_state!, target: pubkey)
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
}
func open_search(filt: NostrFilter) {
let search = SearchModel(state: damus_state!, search: filt)
navigationCoordinator.push(route: Route.Search(search: search))
@@ -306,9 +312,6 @@ struct ContentView: View {
hasSeenOnboardingSuggestions = true
}
self.appDelegate?.state = damus_state
Task { // We probably don't need this to be a detached task. According to https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Defining-and-Calling-Asynchronous-Functions, awaits are only suspension points that do not block the thread.
await self.listenAndHandleLocalNotifications()
}
}
.sheet(item: $active_sheet) { item in
switch item {
@@ -367,10 +370,6 @@ struct ContentView: View {
self.confirm_mute = true
}
.onReceive(handle_notify(.attached_wallet)) { nwc in
// Ensure to add NWC relay to the pool and connect it.
try? damus_state.pool.add_relay(.nwc(url: nwc.relay))
damus_state.pool.connect(to: [nwc.relay])
// update the lightning address on our profile when we attach a
// wallet with an associated
guard let ds = self.damus_state,
@@ -512,6 +511,27 @@ struct ContentView: View {
@unknown default:
break
}
}
.onReceive(handle_notify(.local_notification)) { local in
guard let damus_state else { return }
switch local.mention {
case .pubkey(let pubkey):
open_profile(pubkey: pubkey)
case .note(let noteId):
openEvent(noteId: noteId, notificationType: local.type)
case .nevent(let nevent):
openEvent(noteId: nevent.noteid, notificationType: local.type)
case .nprofile(let nprofile):
open_profile(pubkey: nprofile.author)
case .nrelay(_):
break
case .naddr(let naddr):
break
}
}
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
home.filter_events()
@@ -621,28 +641,6 @@ struct ContentView: View {
self.selected_timeline = timeline
}
/// Listens to requests to open a push/local user notification
///
/// This function never returns, it just keeps streaming
func listenAndHandleLocalNotifications() async {
for await notification in await QueueableNotify<LossyLocalNotification>.shared.stream {
self.handleNotification(notification: notification)
}
}
func handleNotification(notification: LossyLocalNotification) {
Log.info("ContentView is handling a notification", for: .push_notifications)
guard damus_state != nil else {
// 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")
Log.error("DamusState not loaded when ContentView (new handler) was handling a notification", for: .push_notifications)
return
}
let local = notification
let openAction = local.toViewOpenAction()
self.execute_open_action(openAction)
}
func connect() {
// nostrdb
var mndb = Ndb()
@@ -748,6 +746,23 @@ struct ContentView: View {
damus_state.postbox.send(ev)
}
}
private func openEvent(noteId: NoteId, notificationType: LocalNotificationType) {
guard let target = damus_state.events.lookup(noteId) else {
return
}
switch notificationType {
case .dm:
selected_timeline = .dms
damus_state.dms.set_active_dm(target.pubkey)
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
case .like, .zap, .mention, .repost, .reply, .tagged:
open_event(ev: target)
case .profile_zap:
break
}
}
/// An open action within the app
/// This is used to model, store, and communicate a desired view action to be taken as a result of opening an object,
@@ -1044,7 +1059,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
/// - naddr: the `naddr` address
/// - callback: A function to handle the found event
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
let nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
let filter = NostrFilter(kinds: nostrKinds, authors: [naddr.author])
@@ -1201,35 +1216,6 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
}
}
extension LossyLocalNotification {
/// Computes a view open action from a mention reference.
/// Use this when opening a user-presentable interface to a specific mention reference.
func toViewOpenAction() -> ContentView.ViewOpenAction {
switch self.mention {
case .pubkey(let pubkey):
return .route(.ProfileByKey(pubkey: pubkey))
case .note(let noteId):
return .route(.LoadableNostrEvent(note_reference: .note_id(noteId)))
case .nevent(let nEvent):
// TODO: Improve this by implementing a route that handles nevents with their relay hints.
return .route(.LoadableNostrEvent(note_reference: .note_id(nEvent.noteid)))
case .nprofile(let nProfile):
// TODO: Improve this by implementing a profile route that handles nprofiles with their relay hints.
return .route(.ProfileByKey(pubkey: nProfile.author))
case .nrelay:
// 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
return .sheet(.error(ErrorView.UserPresentableError(
user_visible_description: NSLocalizedString("You opened an invalid link. The link you tried to open refers to \"nrelay\", which has been deprecated and is not supported.", comment: "User-visible error description for a user who tries to open a deprecated \"nrelay\" link."),
tip: NSLocalizedString("Please contact the person who provided the link, and ask for another link.", comment: "User-visible tip on what to do if a link contains a deprecated \"nrelay\" reference."),
technical_info: "`MentionRef.toViewOpenAction` detected deprecated `nrelay` contents"
)))
case .naddr(let nAddr):
return .route(.LoadableNostrEvent(note_reference: .naddr(nAddr)))
}
}
}
func logout(_ state: DamusState?)
{
-4
View File
@@ -2,10 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
@@ -0,0 +1,32 @@
//
// 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
}
}
}
+1 -4
View File
@@ -10,9 +10,8 @@ import Foundation
/// Simple filter to determine whether to show posts or all posts and replies.
enum FilterState : Int {
case posts = 0
case posts_and_replies = 1
case conversations = 2
case posts = 0
func filter(ev: NostrEvent) -> Bool {
switch self {
@@ -20,8 +19,6 @@ enum FilterState : Int {
return ev.known_kind == .boost || ev.known_kind == .highlight || !ev.is_reply()
case .posts_and_replies:
return true
case .conversations:
return true
}
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ class DraftArtifacts: Equatable {
if case .pubkey(let pubkey) = mention.ref {
// A profile reference, format things properly.
let profile = damus_state.ndb.lookup_profile(pubkey)?.unsafeUnownedValue?.profile
let profile_name = DisplayName(profile: profile, pubkey: pubkey).username
let profile_name = parse_display_name(profile: profile, pubkey: pubkey).username
guard let url_address = URL(string: block.asString) else {
rich_text_content.append(.init(string: block.asString))
continue
+6 -31
View File
@@ -79,7 +79,6 @@ class HomeModel: ContactsDelegate {
var notifications = NotificationsModel()
var notification_status = NotificationStatusModel()
var events: EventHolder = EventHolder()
var already_reposted: Set<NoteId> = Set()
var zap_button: ZapButtonModel = ZapButtonModel()
init() {
@@ -260,7 +259,7 @@ class HomeModel: ContactsDelegate {
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: nwc_str),
let resp = await WalletConnect.FullWalletResponse(from: ev, nwc: nwc) else {
let resp = await FullWalletResponse(from: ev, nwc: nwc) else {
return
}
@@ -274,24 +273,12 @@ class HomeModel: ContactsDelegate {
guard resp.response.error == nil else {
print("nwc error: \(resp.response)")
WalletConnect.handle_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
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)]")
WalletConnect.handle_zap_success(state: self.damus_state, resp: resp)
nwc_success(state: self.damus_state, resp: resp)
}
}
@@ -388,8 +375,6 @@ class HomeModel: ContactsDelegate {
boost_ev_id = inner_ev.id
Task {
// NOTE (jb55): remove this after nostrdb update, since nostrdb
// processess reposts when note is ingested
guard validate_event(ev: inner_ev) == .ok else {
return
}
@@ -409,7 +394,7 @@ class HomeModel: ContactsDelegate {
switch self.damus_state.boosts.add_event(ev, target: e) {
case .already_counted:
break
case .success(_):
case .success(let n):
notify(.update_stats(note_id: e))
}
}
@@ -418,7 +403,7 @@ class HomeModel: ContactsDelegate {
switch damus_state.quote_reposts.add_event(ev, target: target) {
case .already_counted:
break
case .success(_):
case .success(let n):
notify(.update_stats(note_id: target))
}
}
@@ -465,7 +450,7 @@ class HomeModel: ContactsDelegate {
let nwc = WalletConnectURL(str: nwc_str),
nwc.relay == relay_id
{
WalletConnect.subscribe(url: nwc, pool: pool)
subscribe_to_nwc(url: nwc, pool: pool)
}
case .error(let merr):
let desc = String(describing: merr)
@@ -747,16 +732,6 @@ class HomeModel: ContactsDelegate {
handle_quote_repost_event(ev, target: quoted_event.note_id)
}
// don't add duplicate reposts to home
if ev.known_kind == .boost, let target = ev.get_inner_event()?.id {
if already_reposted.contains(target) {
Log.info("Skipping duplicate repost for event %s", for: .timeline, target.hex())
return
} else {
already_reposted.insert(target)
}
}
if sub_id == home_subid {
insert_home_event(ev)
} else if sub_id == notifications_subid {
+10 -10
View File
@@ -47,16 +47,16 @@ enum MuteItem: Hashable, Equatable {
// rhs is the item we want to check against (ie. the item in the mute list)
switch (lhs, rhs) {
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, _)):
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, _)):
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
case (.word(let lhs_word, _), .word(let rhs_word, _)):
return lhs_word == rhs_word && !rhs.is_expired()
case (.thread(let lhs_thread, _), .thread(let rhs_thread, _)):
return lhs_thread == rhs_thread && !rhs.is_expired()
default:
return false
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, let rhs_expiration_date)):
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, let rhs_expiration_date)):
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
case (.word(let lhs_word, _), .word(let rhs_word, let rhs_expiration_date)):
return lhs_word == rhs_word && !rhs.is_expired()
case (.thread(let lhs_thread, _), .thread(let rhs_thread, let rhs_expiration_date)):
return lhs_thread == rhs_thread && !rhs.is_expired()
default:
return false
}
}
+18 -65
View File
@@ -22,10 +22,8 @@ class ProfileModel: ObservableObject, Equatable {
var seen_event: Set<NoteId> = Set()
var sub_id = UUID().description
var prof_subid = UUID().description
var conversations_subid = UUID().description
var findRelay_subid = UUID().description
var conversation_events: Set<NoteId> = Set()
init(pubkey: Pubkey, damus: DamusState) {
self.pubkey = pubkey
self.damus = damus
@@ -61,9 +59,6 @@ class ProfileModel: ObservableObject, Equatable {
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
damus.pool.unsubscribe(sub_id: sub_id)
damus.pool.unsubscribe(sub_id: prof_subid)
if pubkey != damus.pubkey {
damus.pool.unsubscribe(sub_id: conversations_subid)
}
}
func subscribe() {
@@ -74,29 +69,13 @@ class ProfileModel: ObservableObject, Equatable {
text_filter.authors = [pubkey]
text_filter.limit = 500
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
damus.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)
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.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
}
func handle_profile_contact_event(_ ev: NostrEvent) {
process_contact_event(state: damus, ev: ev)
@@ -111,8 +90,15 @@ class ProfileModel: ObservableObject, Equatable {
self.following = count_pubkeys(ev.tags)
self.relays = decode_json_relays(ev.content)
}
func add_event(_ ev: NostrEvent) {
guard ev.should_show_event else {
return
}
private func add_event(_ ev: NostrEvent) {
if seen_event.contains(ev.id) {
return
}
if ev.is_textlike || ev.known_kind == .boost {
if self.events.insert(ev) {
self.objectWillChange.send()
@@ -123,57 +109,24 @@ class ProfileModel: ObservableObject, Equatable {
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) {
switch ev {
case .ws_event:
return
case .nostr_event(let resp):
guard resp.subid == self.sub_id || resp.subid == self.prof_subid || resp.subid == self.conversations_subid else {
guard resp.subid == self.sub_id || resp.subid == self.prof_subid else {
return
}
switch resp {
case .ok:
break
case .event(_, let ev):
guard ev.should_show_event else {
break
}
// Ensure the event public key matches this profiles public key
// 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
guard self.pubkey == ev.pubkey else { break }
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)
}
add_event(ev)
case .notice:
break
//notify(.notice, notice)
+1 -1
View File
@@ -388,7 +388,7 @@ class DamusPurple: StoreObserverDelegate {
case .none:
return .sheet(.error(.init(
user_visible_description: NSLocalizedString("You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.", comment: "Error label upon continuing in the app from a Damus Purple purchase"),
tip: NSLocalizedString("Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.", comment: "User-facing tips on what to do if a Purple welcome link doesn't work"),
tip: NSLocalizedString("Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue", comment: "User-facing tips on what to do if a Purple welcome link doesn't work"),
technical_info: "Handling Purple URL \"\(purple_url)\" failed, the `is_good_to_go` result was `\(String(describing: is_good_to_go))`"
)))
}
+4 -35
View File
@@ -29,18 +29,7 @@ class ThreadModel: ObservableObject {
///
/// This is a computed property because we then don't need to worry about keeping things in sync
var parent_events: [NostrEvent] {
// 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
return event_map.parent_events(of: selected_event)
}
/// All of the direct and indirect replies of `selected_event` in the thread. sorted chronologically
///
@@ -136,13 +125,7 @@ class ThreadModel: ObservableObject {
/// 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.
/// However, this can be called externally for testing purposes (e.g. injecting events for testing)
///
/// - 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) {
func add_event(_ ev: NostrEvent, keypair: Keypair) {
if event_map.contains(id: ev.id) {
return
}
@@ -153,22 +136,8 @@ class ThreadModel: ObservableObject {
event_map.add(event: ev)
if look_for_parent_events {
// 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()
}
// Publish changes
objectWillChange.send()
}
/// Handles an incoming event from a relay pool
+2 -2
View File
@@ -34,7 +34,7 @@ struct DamusURLHandler {
let thread = await ThreadModel(event: nostrEvent, damus_state: damus_state)
return .route(.Thread(thread: thread))
case .event_reference(let event_reference):
return .route(.LoadableNostrEvent(note_reference: event_reference))
return .route(.ThreadFromReference(note_reference: event_reference))
case .wallet_connect(let walletConnectURL):
damus_state.wallet.new(walletConnectURL)
return .route(.Wallet(wallet: damus_state.wallet))
@@ -99,7 +99,7 @@ struct DamusURLHandler {
case profile(Pubkey)
case filter(NostrFilter)
case event(NostrEvent)
case event_reference(LoadableNostrEventViewModel.NoteReference)
case event_reference(LoadableThreadModel.NoteReference)
case wallet_connect(WalletConnectURL)
case script([UInt8])
case purple(DamusPurpleURL)
-4
View File
@@ -201,10 +201,6 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "developer_mode", default_value: false)
var developer_mode: Bool
/// Makes all post content gibberish and blurhashes images, to avoid distractions when developers are working.
@Setting(key: "undistract_mode", default_value: false)
var undistractMode: Bool
@Setting(key: "always_show_onboarding_suggestions", default_value: false)
var always_show_onboarding_suggestions: Bool
-30
View File
@@ -13,17 +13,10 @@ enum WalletConnectState {
case none
}
/// Models and manages the user's NWC wallet based on the app's settings
class WalletModel: ObservableObject {
var settings: UserSettingsStore
private(set) var previous_state: WalletConnectState
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
@@ -68,27 +61,4 @@ class WalletModel: ObservableObject {
self.connect_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
}
}
-55
View File
@@ -1,55 +0,0 @@
//
// NIP04.swift
// damus
//
// Created by Daniel DAquino 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)
}
}
+32 -5
View File
@@ -4,6 +4,7 @@
//
// Created by Daniel DAquino on 2025-01-20.
//
import NostrSDK
import Foundation
/// This models a NIP-37 draft.
@@ -76,7 +77,13 @@ struct NIP37Draft {
guard let note_json_string = String(data: note_json_data, encoding: .utf8) else {
throw NIP37DraftEventError.encoding_error
}
guard let contents = try? NIP44v2Encryption.encrypt(plaintext: note_json_string, privateKeyA: keypair.privkey, publicKeyB: keypair.pubkey) else {
guard let secret_key = SecretKey.from(privkey: keypair.privkey) else {
throw NIP37DraftEventError.invalid_keypair
}
guard let pubkey = PublicKey.from(pubkey: keypair.pubkey) else {
throw NIP37DraftEventError.invalid_keypair
}
guard let contents = try? nip44Encrypt(secretKey: secret_key, publicKey: pubkey, content: note_json_string, version: Nip44Version.v2) else {
return nil
}
var tags = [
@@ -104,10 +111,16 @@ struct NIP37Draft {
static func unwrap(note: NdbNote, keypair: FullKeypair) throws -> NdbNote? {
let wrapped_note = note
guard wrapped_note.known_kind == .draft else { return nil }
guard let draft_event_json = try? NIP44v2Encryption.decrypt(
payload: wrapped_note.content,
privateKeyA: keypair.privkey,
publicKeyB: keypair.pubkey
guard let private_key = SecretKey.from(privkey: keypair.privkey) else {
throw NIP37DraftEventError.invalid_keypair
}
guard let pubkey = PublicKey.from(pubkey: keypair.pubkey) else {
throw NIP37DraftEventError.invalid_keypair
}
guard let draft_event_json = try? nip44Decrypt(
secretKey: private_key,
publicKey: pubkey,
payload: wrapped_note.content
) else { return nil }
return NdbNote.owned_from_json(json: draft_event_json)
}
@@ -117,3 +130,17 @@ struct NIP37Draft {
case encoding_error
}
}
// MARK: - Convenience extensions
fileprivate extension PublicKey {
static func from(pubkey: Pubkey) -> PublicKey? {
return try? PublicKey.parse(publicKey: pubkey.hex())
}
}
fileprivate extension SecretKey {
static func from(privkey: Privkey) -> SecretKey? {
return try? SecretKey.parse(secretKey: privkey.hex())
}
}
-357
View File
@@ -1,357 +0,0 @@
//
// NIP44.swift
// damus
//
// Based on NIP44v2Encrypting.swift created by Terry Yiu on 3/16/24, from https://github.com/nostr-sdk/nostr-sdk-ios, which is MIT licensed.
//
// MIT License
//
// Copyright (c) 2023 Nostr SDK
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
//
// Adapted by Daniel DAquino on 2025-02-10.
//
import Foundation
import CryptoKit
import CryptoSwift
import secp256k1
struct NIP44v2Encryption {
/// Produces a `String` containing `plaintext` that has been encrypted using the `privateKey` of user A and the `publicKey` of user B.
///
/// The result is non-deterministic because a cryptographically secure pseudorandom generated nonce is used each time,
/// but can be decrypted deterministically with a call to ``NIP44v2Encryption/decrypt(payload:privateKeyA:publicKeyB:)``,
/// where user A and user B are interchangeable.
///
/// This function can `throw` an error from ``EncryptionError`` if it fails to encrypt the plaintext.
///
/// - Parameters:
/// - plaintext: The plaintext to encrypt.
/// - privateKeyA: The private key of user A.
/// - publicKeyB: The public key of user B.
/// - Returns: The encrypted ciphertext.
static func encrypt(plaintext: String, privateKeyA: Privkey, publicKeyB: Pubkey) throws -> String {
let conversationKey = try conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB)
return try encrypt(plaintext: plaintext, conversationKey: conversationKey)
}
/// Produces a `String` containing `payload` that has been decrypted using the `privateKey` of user A and the `publicKey` of user B,
/// and the result is identical to if the `privateKey` of user B and `publicKey` of user A were used to decrypt `payload` instead.
///
/// Any ciphertext returned from the call to ``NIP44v2Encryption/encrypt(plaintext:privateKeyA:publicKeyB:)``
/// can be decrypted, where user A and B are interchangeable.
///
/// This function can `throw` an error from ``EncryptionError`` if it fails to decrypt the payload.
///
/// - Parameters:
/// - payload: The payload to decrypt.
/// - privateKeyA: The private key of user A.
/// - publicKeyB: The public key of user B.
/// - Returns: The decrypted plaintext message.
static func decrypt(payload: String, privateKeyA: Privkey, publicKeyB: Pubkey) throws -> String {
let conversationKey = try conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB)
return try decrypt(payload: payload, conversationKey: conversationKey)
}
/// Calculates length of the padded byte array.
static func calculatePaddedLength(_ unpaddedLength: Int) throws -> Int {
guard unpaddedLength > 0 else {
throw EncryptionError.unpaddedLengthInvalid(unpaddedLength)
}
if unpaddedLength <= 32 {
return 32
}
let nextPower = 1 << (Int(floor(log2(Double(unpaddedLength) - 1))) + 1)
let chunk: Int
if nextPower <= 256 {
chunk = 32
} else {
chunk = nextPower / 8
}
return chunk * (Int(floor((Double(unpaddedLength) - 1) / Double(chunk))) + 1)
}
/// Converts unpadded plaintext to padded bytes.
static func pad(_ plaintext: String) throws -> Data {
guard let unpadded = plaintext.data(using: .utf8) else {
throw EncryptionError.utf8EncodingFailed
}
let unpaddedLength = unpadded.count
guard 1...65535 ~= unpaddedLength else {
throw EncryptionError.plaintextLengthInvalid(unpaddedLength)
}
var prefix = Data(count: 2)
prefix.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in
ptr.storeBytes(of: UInt16(unpaddedLength).bigEndian, as: UInt16.self)
}
let suffix = Data(count: try calculatePaddedLength(unpaddedLength) - unpaddedLength)
return prefix + unpadded + suffix
}
/// Converts padded bytes to unpadded plaintext.
static func unpad(_ padded: Data) throws -> String {
guard padded.count >= 2 else {
throw EncryptionError.paddingInvalid
}
let unpaddedLength = (Int(padded[0]) << 8) | Int(padded[1])
guard 2+unpaddedLength <= padded.count else {
throw EncryptionError.paddingInvalid
}
let unpadded = toBytes(from: padded)[2..<2+unpaddedLength]
let paddedLength = try calculatePaddedLength(unpaddedLength)
guard unpaddedLength > 0,
unpadded.count == unpaddedLength,
padded.count == 2 + paddedLength,
let result = String(data: Data(unpadded), encoding: .utf8) else {
throw EncryptionError.paddingInvalid
}
return result
}
static func decodePayload(_ payload: String) throws -> DecodedPayload {
let payloadLength = payload.count
guard payloadLength > 0 && payload.first != "#" else {
throw EncryptionError.unknownVersion()
}
guard 132...87472 ~= payloadLength else {
throw EncryptionError.payloadSizeInvalid(payloadLength)
}
guard let data = Data(base64Encoded: payload) else {
throw EncryptionError.base64EncodingFailed
}
let dataLength = data.count
guard 99...65603 ~= dataLength else {
throw EncryptionError.dataSizeInvalid(dataLength)
}
guard let version = data.first else {
throw EncryptionError.unknownVersion()
}
guard version == 2 else {
throw EncryptionError.unknownVersion(Int(version))
}
let nonce = data[data.index(data.startIndex, offsetBy: 1)..<data.index(data.startIndex, offsetBy: 33)]
let ciphertext = data[data.index(data.startIndex, offsetBy: 33)..<data.index(data.startIndex, offsetBy: dataLength - 32)]
let mac = data[data.index(data.startIndex, offsetBy: dataLength - 32)..<data.index(data.startIndex, offsetBy: dataLength)]
return DecodedPayload(nonce: nonce, ciphertext: ciphertext, mac: mac)
}
static func hmacAad(key: Data, message: Data, aad: Data) throws -> Data {
guard aad.count == 32 else {
throw EncryptionError.aadLengthInvalid(aad.count)
}
let combined = aad + message
return Data(CryptoKit.HMAC<CryptoKit.SHA256>.authenticationCode(for: combined, using: SymmetricKey(data: key)))
}
static func toBytes(from data: Data) -> [UInt8] {
data.withUnsafeBytes { bytesPointer in Array(bytesPointer) }
}
static func preparePublicKeyBytes(from publicKey: Pubkey) throws -> [UInt8] {
let publicKeyBytes = publicKey.bytes
let prefix = Data([2])
let prefixBytes = toBytes(from: prefix)
return prefixBytes + publicKeyBytes
}
static func parsePublicKey(from bytes: [UInt8]) throws -> secp256k1_pubkey {
var publicKey = secp256k1_pubkey()
guard secp256k1_ec_pubkey_parse(secp256k1.Context.raw, &publicKey, bytes, bytes.count) == 1 else {
throw EncryptionError.publicKeyInvalid
}
return publicKey
}
static func computeSharedSecret(using publicKey: secp256k1_pubkey, and privateKeyBytes: [UInt8]) throws -> [UInt8] {
var sharedSecret = [UInt8](repeating: 0, count: 32)
var mutablePublicKey = publicKey
// Multiplication of point B by scalar a (a B), defined in [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki).
// The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method bytes(P) from BIP340.
// Private and public keys must be validated as per BIP340: pubkey must be a valid, on-curve point, and private key must be a scalar in range [1, secp256k1_order - 1]
guard secp256k1_ecdh(secp256k1.Context.raw, &sharedSecret, &mutablePublicKey, privateKeyBytes, { (output, x32, _, _) in
memcpy(output, x32, 32)
return 1
}, nil) != 0 else {
throw EncryptionError.sharedSecretComputationFailed
}
return sharedSecret
}
/// Calculates long-term key between users A and B.
/// The conversation key of A's private key and B's public key is equal to the conversation key of B's private key and A's public key.
static func conversationKey(privateKeyA: Privkey, publicKeyB: Pubkey) throws -> ContiguousBytes {
let privateKeyABytes = privateKeyA.bytes
let publicKeyBBytes = try preparePublicKeyBytes(from: publicKeyB)
let parsedPublicKeyB = try parsePublicKey(from: publicKeyBBytes)
let sharedSecret = try computeSharedSecret(using: parsedPublicKeyB, and: privateKeyABytes)
return CryptoKit.HKDF<CryptoKit.SHA256>.extract(inputKeyMaterial: SymmetricKey(data: sharedSecret), salt: Data("nip44-v2".utf8))
}
/// Calculates unique per-message key.
static func messageKeys(conversationKey: ContiguousBytes, nonce: Data) throws -> MessageKeys {
let conversationKeyByteCount = conversationKey.bytes.count
guard conversationKeyByteCount == 32 else {
throw EncryptionError.conversationKeyLengthInvalid(conversationKeyByteCount)
}
guard nonce.count == 32 else {
throw EncryptionError.nonceLengthInvalid(nonce.count)
}
let keys = CryptoKit.HKDF<CryptoKit.SHA256>.expand(pseudoRandomKey: conversationKey, info: nonce, outputByteCount: 76)
let keysBytes = keys.bytes
let chaChaKey = Data(keysBytes[0..<32])
let chaChaNonce = Data(keysBytes[32..<44])
let hmacKey = Data(keysBytes[44..<76])
return MessageKeys(chaChaKey: chaChaKey, chaChaNonce: chaChaNonce, hmacKey: hmacKey)
}
static func encrypt(plaintext: String, conversationKey: ContiguousBytes, nonce: Data? = nil) throws -> String {
let nonceData: Data
if let nonce {
nonceData = nonce
} else {
// Fetches randomness from CSPRNG.
nonceData = Data.secureRandomBytes(count: 32)
}
let messageKeys = try messageKeys(conversationKey: conversationKey, nonce: nonceData)
let padded = try pad(plaintext)
let paddedBytes = toBytes(from: padded)
let chaChaKey = toBytes(from: messageKeys.chaChaKey)
let chaChaNonce = toBytes(from: messageKeys.chaChaNonce)
let ciphertext = try ChaCha20(key: chaChaKey, iv: chaChaNonce).encrypt(paddedBytes)
let ciphertextData = Data(ciphertext)
let mac = try hmacAad(key: messageKeys.hmacKey, message: ciphertextData, aad: nonceData)
let data = Data([2]) + nonceData + ciphertextData + mac
return data.base64EncodedString()
}
static func decrypt(payload: String, conversationKey: ContiguousBytes) throws -> String {
let decodedPayload = try decodePayload(payload)
let nonce = decodedPayload.nonce
let ciphertext = decodedPayload.ciphertext
let ciphertextBytes = toBytes(from: ciphertext)
let mac = decodedPayload.mac
let messageKeys = try messageKeys(conversationKey: conversationKey, nonce: nonce)
let calculatedMac = try hmacAad(key: messageKeys.hmacKey, message: ciphertext, aad: nonce)
guard calculatedMac == mac else {
throw EncryptionError.macInvalid
}
let chaChaNonce = toBytes(from: messageKeys.chaChaNonce)
let chaChaKey = toBytes(from: messageKeys.chaChaKey)
let paddedPlaintext = try ChaCha20(key: chaChaKey, iv: chaChaNonce).decrypt(ciphertextBytes)
let paddedPlaintextData = Data(paddedPlaintext.bytes)
return try unpad(paddedPlaintextData)
}
}
// MARK: - Helper structures and extensions
extension Data {
/// Random data of a given size, from CSPRNG
/// - Parameter count: The size of the data, in bytes
/// - Returns: Bytes randomly generated from CSPRNG
static func secureRandomBytes(count: Int) -> Data {
var bytes = [Int8](repeating: 0, count: count)
guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else {
fatalError("can't copy secure random data")
}
return Data(bytes: bytes, count: count)
}
}
extension NIP44v2Encryption {
struct DecodedPayload {
let nonce: Data
let ciphertext: Data
let mac: Data
}
struct MessageKeys {
let chaChaKey: Data
let chaChaNonce: Data
let hmacKey: Data
}
public enum EncryptionError: Error {
case aadLengthInvalid(Int)
case base64EncodingFailed
case chaCha20DecryptionFailed
case chaCha20EncryptionFailed
case conversationKeyLengthInvalid(Int)
case dataSizeInvalid(Int)
case macInvalid
case nonceLengthInvalid(Int)
case paddingInvalid
case payloadSizeInvalid(Int)
case plaintextLengthInvalid(Int)
case privateKeyInvalid
case publicKeyInvalid
case sharedSecretComputationFailed
case unknownVersion(Int? = nil)
case unpaddedLengthInvalid(Int)
case utf8EncodingFailed
}
}
+3 -4
View File
@@ -31,8 +31,7 @@ class ProfileRecord {
}
guard let profile = data.profile,
let addr = (profile.lud16 ?? profile.lud06)?.trimmingCharacters(in: .whitespaces)
else {
let addr = profile.lud16 ?? profile.lud06 else {
return nil;
}
@@ -58,7 +57,7 @@ extension NdbProfile {
}
static func displayName(profile: Profile?, pubkey: Pubkey) -> DisplayName {
return DisplayName(name: profile?.name, display_name: profile?.display_name, pubkey: pubkey)
return parse_display_name(profile: profile, pubkey: pubkey)
}
var damus_donation: Int? {
@@ -302,7 +301,7 @@ class Profile: Codable {
*/
func make_test_profile() -> Profile {
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: nil, lud16: "jb55@jb55.com", nip05: "jb55@jb55.com", damus_donation: 1)
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com", damus_donation: 1)
}
func make_ln_url(_ str: String?) -> URL? {
+1 -1
View File
@@ -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),
let note_json = encode_json(note),
let enc = NIP04.encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
let enc = encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
else {
return nil
}
+1 -1
View File
@@ -45,7 +45,7 @@ enum NostrResponse {
static func owned_from_json(json: String) -> NostrResponse? {
return json.withCString{ cstr in
let bufsize: Int = max(Int(Double(json.utf8.count) * 8.0), Int(getpagesize()))
let bufsize: Int = max(Int(Double(json.utf8.count) * 4.0), Int(getpagesize()))
let data = malloc(bufsize)
if data == nil {
+15 -7
View File
@@ -7,11 +7,19 @@
import Foundation
extension QueueableNotify<LossyLocalNotification> {
/// A shared singleton for opening local and push user notifications
///
/// ## Implementation notes
///
/// - The queue can only hold one element. This is done because if the user hypothetically opened 10 push notifications and there was a lag, we wouldn't want the app to suddenly open 10 different things.
static let shared = QueueableNotify(maxQueueItems: 1)
struct LocalNotificationNotify: Notify {
typealias Payload = LossyLocalNotification
var payload: Payload
}
extension NotifyHandler {
static var local_notification: NotifyHandler<LocalNotificationNotify> {
.init()
}
}
extension Notifications {
static func local_notification(_ payload: LossyLocalNotification) -> Notifications<LocalNotificationNotify> {
.init(.init(payload: payload))
}
}
-90
View File
@@ -1,90 +0,0 @@
//
// QueueableNotify.swift
// damus
//
// Created by Daniel DAquino on 2025-02-14.
//
/// This notifies another object about some payload,
/// with automatic "queueing" of messages if there are no listeners.
///
/// When used as a singleton, this can be used to easily send notifications to be handled at the app-level.
///
/// This serves the same purpose as `Notify`, except this implements the queueing of messages,
/// which means that messages can be handled even if the listener is not instantiated yet.
///
/// **Example:** The app delegate can send some events that need handling from `ContentView` but some can occur before `ContentView` is even instantiated.
///
///
/// ## Usage notes
///
/// - This code was mainly written to have one listener at a time. Have more than one listener may be possible, but this class has not been tested/optimized for that purpose.
///
///
/// ## Implementation notes
///
/// - This makes heavy use of `AsyncStream` and continuations, because that allows complexities here to be handled elegantly with a simple "for-in" loop
/// - Without this, it would take a couple of callbacks and manual handling of queued items to achieve the same effect
/// - Modeled as an `actor` for extra thread-safety
actor QueueableNotify<T: Sendable> {
/// The continuation, which allows us to publish new items to the listener
/// If `nil`, that means there is no listeners to the stream, which is used for determining whether to queue new incoming items.
private var continuation: AsyncStream<T>.Continuation?
/// Holds queue items
private var queue: [T] = []
/// The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
var maxQueueItems: Int
/// Initializes the object
/// - Parameter maxQueueItems: The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
init(maxQueueItems: Int) {
self.maxQueueItems = maxQueueItems
}
/// The async stream, used for listening for notifications
///
/// This will first stream the queued "inbox" items that the listener may have missed, and then it will do a real-time stream of new items as they come in.
///
/// Example:
///
/// ```swift
/// for await notification in queueableNotify.stream {
/// // Do something with the notification
/// }
/// ```
var stream: AsyncStream<T> {
return AsyncStream { continuation in
// Stream queued "inbox" items that the listener may have missed
for item in queue {
continuation.yield(item)
}
// Clean up if the stream closes
continuation.onTermination = { continuation in
Task { await self.cleanup() }
}
// Point to this stream, so that it can receive new updates
self.continuation = continuation
}
}
/// Cleans up after a stream is closed by the listener
private func cleanup() {
self.continuation = nil // This will cause new items to be queued for when another listener is attached
}
/// Adds a new notification item to be handled by a listener.
///
/// This will automatically stream the new item to the listener, or queue the item if no one is listening
func add(item: T) {
while queue.count >= maxQueueItems { queue.removeFirst() } // Ensures queue stays within the desired size
guard let continuation else {
// No one is listening, queue it (send it to an inbox for later handling)
queue.append(item)
return
}
// Send directly to the active listener stream
continuation.yield(item)
}
}
-1
View File
@@ -14,7 +14,6 @@ import Foundation
class Constants {
//static let EXAMPLE_DEMOS: DamusState = .empty
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 NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
+7 -32
View File
@@ -10,15 +10,7 @@ import Foundation
enum DisplayName: Equatable {
case both(username: String, displayName: 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 {
switch self {
case .one(let one):
@@ -36,37 +28,20 @@ enum DisplayName: Equatable {
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(name: String?, display_name: String?, pubkey: Pubkey) -> DisplayName {
func parse_display_name(profile: Profile?, pubkey: Pubkey) -> DisplayName {
if pubkey == ANON_PUBKEY {
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
}
if name == nil && display_name == nil {
guard let profile else {
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
}
let name = name?.isEmpty == false ? name : nil
let disp_name = display_name?.isEmpty == false ? display_name : nil
let name = profile.name?.isEmpty == false ? profile.name : nil
let disp_name = profile.display_name?.isEmpty == false ? profile.display_name : nil
if let name, let disp_name, name != disp_name {
return .both(username: name, displayName: disp_name)
-15
View File
@@ -1,15 +0,0 @@
//
// ExtraFonts.swift
// damus
//
// Created by Daniel DAquino 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
}
-11
View File
@@ -21,17 +21,6 @@ let ANON_PUBKEY = Pubkey(Data([
struct FullKeypair: Equatable {
let pubkey: Pubkey
let privkey: Privkey
init(pubkey: Pubkey, privkey: Privkey) {
self.pubkey = pubkey
self.privkey = privkey
}
init?(privkey: Privkey) {
self.privkey = privkey
guard let pubkey = privkey_to_pubkey_raw(sec: privkey.bytes) else { return nil }
self.pubkey = pubkey
}
func to_keypair() -> Keypair {
return Keypair(pubkey: pubkey, privkey: privkey)
-3
View File
@@ -14,9 +14,6 @@ enum LogCategory: String {
case render
case storage
case networking
case timeline
/// Logs related to Nostr Wallet Connect components
case nwc
case push_notifications
case damus_purple
case image_uploading
+6 -6
View File
@@ -32,7 +32,7 @@ enum Route: Hashable {
case DeveloperSettings(settings: UserSettingsStore)
case FirstAidSettings(settings: UserSettingsStore)
case Thread(thread: ThreadModel)
case LoadableNostrEvent(note_reference: LoadableNostrEventViewModel.NoteReference)
case ThreadFromReference(note_reference: LoadableThreadModel.NoteReference)
case Reposts(reposts: EventsModel)
case QuoteReposts(quotes: EventsModel)
case Reactions(reactions: EventsModel)
@@ -97,8 +97,8 @@ enum Route: Hashable {
case .Thread(let thread):
ChatroomThreadView(damus: damusState, thread: thread)
//ThreadView(state: damusState, thread: thread)
case .LoadableNostrEvent(let note_reference):
LoadableNostrEventView(state: damusState, note_reference: note_reference)
case .ThreadFromReference(let note_reference):
LoadableThreadView(state: damusState, note_reference: note_reference)
case .Reposts(let reposts):
RepostsView(damus_state: damusState, model: reposts)
case .QuoteReposts(let quote_reposts):
@@ -190,8 +190,8 @@ enum Route: Hashable {
case .Thread(let threadModel):
hasher.combine("thread")
hasher.combine(threadModel.original_event.id)
case .LoadableNostrEvent(note_reference: let note_reference):
hasher.combine("loadable_nostr_event")
case .ThreadFromReference(note_reference: let note_reference):
hasher.combine("thread_from_reference")
hasher.combine(note_reference)
case .Reposts(let reposts):
hasher.combine("reposts")
@@ -209,7 +209,7 @@ enum Route: Hashable {
case .Search(let search):
hasher.combine("search")
hasher.combine(search.search)
case .NDBSearch:
case .NDBSearch(let results):
hasher.combine("results")
case .EULA:
hasher.combine("eula")
-30
View File
@@ -1,30 +0,0 @@
//
// Undistractor.swift
// damus
//
// Created by Daniel DAquino on 2025-02-19.
//
/// Keeping the minds of developers safe from the occupational hazard of social media distractions when testing Damus since 2025
struct Undistractor {
static func makeGibberish(text: String) -> String {
let lowercaseLetters = "abcdefghijklmnopqrstuvwxyz"
let uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
var transformedText = ""
for char in text {
if lowercaseLetters.contains(char) {
if let randomLetter = lowercaseLetters.randomElement() {
transformedText.append(randomLetter)
}
} else if uppercaseLetters.contains(char) {
if let randomLetter = uppercaseLetters.randomElement() {
transformedText.append(randomLetter)
}
} else {
transformedText.append(char)
}
}
return transformedText
}
}
+118
View File
@@ -0,0 +1,118 @@
//
// WalletConnect+.swift
// damus
//
// Created by Daniel DAquino 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
}
}
}
+155
View File
@@ -0,0 +1,155 @@
//
// 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)
}
}
}
-137
View File
@@ -1,137 +0,0 @@
//
// Request.swift
// damus
//
// Created by Daniel DAquino 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)
}
}
}
}
-110
View File
@@ -1,110 +0,0 @@
//
// Response.swift
// damus
//
// Created by Daniel DAquino 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]
}
}
}
@@ -1,170 +0,0 @@
//
// WalletConnect+.swift
// damus
//
// Created by Daniel DAquino 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
}
}
}
}
@@ -1,92 +0,0 @@
//
// 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.
}
}
+1 -1
View File
@@ -298,7 +298,7 @@ struct ChatEventView: View {
}
.swipeSpacing(-20)
.swipeActionsStyle(.mask)
.swipeMinimumDistance(40)
.swipeMinimumDistance(20)
.swipeDragGesturePriority(.normal)
}
}
+63 -108
View File
@@ -18,26 +18,11 @@ struct ConfigView: View {
@State var delete_account_warning: Bool = false
@State var confirm_delete_account: Bool = false
@State var delete_text: String = ""
@State private var searchText: String = ""
@ObservedObject var settings: UserSettingsStore
// String constants
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) {
self.state = state
_settings = ObservedObject(initialValue: state.settings)
@@ -46,122 +31,91 @@ struct ConfigView: View {
func textColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
func showSettingsButton(title : String)->Bool{
return searchText.isEmpty || title.lowercased().contains(searchText.lowercased())
}
var body: some View {
ZStack(alignment: .leading) {
Form {
Section {
// Keys
if showSettingsButton(title: keysTitle){
NavigationLink(value:Route.KeySettings(keypair: state.keypair)){
IconLabel(keysTitle,img_name:"Key",color:.purple)
}
}
// Appearance and filters
if showSettingsButton(title: appearanceTitle){
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)
}
NavigationLink(value: Route.KeySettings(keypair: state.keypair)) {
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "Key", color: .purple)
}
//Notifications
if showSettingsButton(title: notificationsTitle){
NavigationLink(value: Route.NotificationSettings(settings: settings)){
IconLabel(notificationsTitle,img_name:"notification-bell-on",color:.blue)
}
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)
}
//Zaps
if showSettingsButton(title: zapsTitle){
NavigationLink(value: Route.ZapSettings(settings: settings)){
IconLabel(zapsTitle,img_name:"zap.fill",color:.orange)
}
NavigationLink(value: Route.SearchSettings(settings: settings)) {
IconLabel(NSLocalizedString("Search/Universe", comment: "Section header for search/universe settings"), img_name: "search", color: .red)
}
//Translation
if showSettingsButton(title: translationTitle){
NavigationLink(value: Route.TranslationSettings(settings: settings)){
IconLabel(translationTitle,img_name:"globe",color:.green)
}
NavigationLink(value: Route.NotificationSettings(settings: settings)) {
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "notification-bell-on", color: .blue)
}
//Reactions
if showSettingsButton(title: reactionsTitle){
NavigationLink(value: Route.ReactionsSettings(settings: settings)){
IconLabel(reactionsTitle,img_name:"shaka.fill",color:.purple)
}
NavigationLink(value: Route.ZapSettings(settings: settings)) {
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "zap.fill", color: .orange)
}
//Developer
if showSettingsButton(title: developerTitle){
NavigationLink(value: Route.DeveloperSettings(settings: settings)){
IconLabel(developerTitle,img_name:"magic-stick2.fill",color:DamusColors.adaptableBlack)
}
NavigationLink(value: Route.TranslationSettings(settings: settings)) {
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe", color: .green)
}
//First Aid
if showSettingsButton(title: firstAidTitle){
NavigationLink(value: Route.FirstAidSettings(settings: settings)){
IconLabel(firstAidTitle,img_name:"help2",color: .red)
}
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)
}
}
//Sign out Section
if showSettingsButton(title: signOutTitle){
Section(signOutTitle){
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")
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
})
}
if state.is_privkey_user {
Section(header: Text("Permanently Delete Account", comment: "Section title for deleting the user")) {
Button(action: {
if state.keypair.privkey == nil {
logout(state)
} else {
confirm_logout = true
}
delete_account_warning = true
}, label: {
Label(signOutTitle, image: "logout")
.foregroundColor(textColor())
Label(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), image: "delete")
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.red)
})
}
}
// Delete Account
if showSettingsButton(title: deleteAccountTitle){
if state.is_privkey_user {
Section(header: Text("Permanently Delete Account", comment: "Section title for deleting the user")) {
Button(action: {
delete_account_warning = true
}, label: {
Label(deleteAccountTitle, image: "delete")
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.red)
})
}
}
}
// 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")
}
Section(
header: Text(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")),
footer: Text("").padding(.bottom, tabHeight + getSafeAreaBottom())
) {
Text(verbatim: VersionInfo.version)
.contextMenu {
Button {
UIPasteboard.general.string = VersionInfo.version
} label: {
Label(NSLocalizedString("Copy", comment: "Context menu option for copying the version of damus."), image: "copy2")
}
}
}
}
}
}
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
.navigationBarTitleDisplayMode(.large)
.searchable(text: $searchText,prompt: "Search within 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) {
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
@@ -200,6 +154,7 @@ struct ConfigView: View {
dismiss()
}
}
}
struct ConfigView_Previews: PreviewProvider {
+41 -1
View File
@@ -131,7 +131,7 @@ struct DMChatView: View, KeyboardReadable {
.map(\.asString)
.joined(separator: "")
guard let dm = NIP04.create_dm(content, to_pk: pubkey, tags: tags, keypair: damus_state.keypair) else {
guard let dm = create_dm(content, to_pk: pubkey, tags: tags, keypair: damus_state.keypair) else {
print("error creating dm")
return
}
@@ -176,6 +176,46 @@ 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 {
/// Layers the given views behind this ``TextEditor``.
func textEditorBackground<V>(@ViewBuilder _ content: () -> V) -> some View where V : View {
-4
View File
@@ -57,10 +57,6 @@ struct EventView: View {
// blame the porn bots for this code
func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool {
if settings.undistractMode {
return true
}
if !settings.blur_images {
return false
}
-275
View File
@@ -1,275 +0,0 @@
//
// LoadableNostrEventView.swift
// damus
//
// Created by Daniel D'Aquino on 2025-01-08.
//
import SwiftUI
/// A view model for `LoadableNostrEventView`
///
/// This takes a nostr event reference, automatically tries to load it, and updates itself to reflect its current state
///
/// ## Implementation notes
///
/// - This is on the main actor because `ObservableObjects` with `Published` properties should be on the main actor for thread-safety.
///
@MainActor
class LoadableNostrEventViewModel: ObservableObject {
let damus_state: DamusState
let note_reference: NoteReference
@Published var state: ThreadModelLoadingState = .loading
/// The time period after which it will give up loading the view.
/// Written in nanoseconds
let TIMEOUT: UInt64 = 10 * 1_000_000_000 // 10 seconds
init(damus_state: DamusState, note_reference: NoteReference) {
self.damus_state = damus_state
self.note_reference = note_reference
Task { await self.load() }
}
func load() async {
// Start the loading process in a separate task to manage the timeout independently.
let loadTask = Task { @MainActor in
self.state = await executeLoadingLogic(note_reference: self.note_reference)
}
// Setup a timer to cancel the load after the timeout period
let timeoutTask = Task { @MainActor in
try await Task.sleep(nanoseconds: TIMEOUT)
loadTask.cancel() // This sends a cancellation signal to the load task.
self.state = .not_found
}
await loadTask.value
timeoutTask.cancel() // Cancel the timeout task if loading finishes earlier.
}
/// Asynchronously find an event from NostrDB or from the network (if not available on NostrDB)
private func loadEvent(noteId: NoteId) async -> NostrEvent? {
let res = await find_event(state: damus_state, query: .event(evid: noteId))
guard let res, case .event(let ev) = res else { return nil }
return ev
}
/// Gets the note reference and tries to load it, outputting a new state for this view model.
private func executeLoadingLogic(note_reference: NoteReference) async -> ThreadModelLoadingState {
switch note_reference {
case .note_id(let note_id):
guard let ev = await self.loadEvent(noteId: note_id) else { return .not_found }
guard let known_kind = ev.known_kind else { return .unknown_or_unsupported_kind }
switch known_kind {
case .text, .highlight:
return .loaded(route: Route.Thread(thread: ThreadModel(event: ev, damus_state: damus_state)))
case .dm:
let dm_model = damus_state.dms.lookup_or_create(ev.pubkey)
return .loaded(route: Route.DMChat(dms: dm_model))
case .like:
// Load the event that this reaction refers to.
guard let first_referenced_note_id = ev.referenced_ids.first else { return .not_found }
return await self.executeLoadingLogic(note_reference: .note_id(first_referenced_note_id))
case .zap, .zap_request:
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
return .loaded(route: Route.Zaps(target: zap.target))
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status:
return .unknown_or_unsupported_kind
}
case .naddr(let naddr):
guard let event = await naddrLookup(damus_state: damus_state, naddr: naddr) else { return .not_found }
return .loaded(route: Route.Thread(thread: ThreadModel(event: event, damus_state: damus_state)))
}
}
enum ThreadModelLoadingState {
case loading
case loaded(route: Route)
case not_found
case unknown_or_unsupported_kind
}
enum NoteReference: Hashable {
case note_id(NoteId)
case naddr(NAddr)
}
}
/// A view for a Nostr event that has not been loaded yet.
/// This takes a Nostr event reference and loads it, while providing nice loading UX and graceful error handling.
struct LoadableNostrEventView: View {
let state: DamusState
@StateObject var loadableModel: LoadableNostrEventViewModel
var loading: Bool {
switch loadableModel.state {
case .loading:
return true
case .loaded, .not_found, .unknown_or_unsupported_kind:
return false
}
}
init(state: DamusState, note_reference: LoadableNostrEventViewModel.NoteReference) {
self.state = state
self._loadableModel = StateObject.init(wrappedValue: LoadableNostrEventViewModel(damus_state: state, note_reference: note_reference))
}
var body: some View {
switch self.loadableModel.state {
case .loading:
ScrollView(.vertical) {
self.skeleton
.redacted(reason: loading ? .placeholder : [])
.shimmer(loading)
.accessibilityElement(children: .ignore)
.accessibilityLabel(NSLocalizedString("Loading thread", comment: "Accessibility label for the thread view when it is loading"))
}
case .loaded(route: let route):
route.view(navigationCoordinator: state.nav, damusState: state)
case .not_found:
self.not_found
case .unknown_or_unsupported_kind:
self.unknown_or_unsupported_kind
}
}
var not_found: some View {
SomethingWrong(
imageSystemName: "questionmark.app",
heading: NSLocalizedString("Note not found", comment: "Heading for the thread view in a not found error state."),
description: NSLocalizedString("We were unable to find the note you were looking for.", comment: "Text for the thread view when it is unable to find the note the user is looking for"),
advice: NSLocalizedString("Try checking the link again, your internet connection, or contact the person who provided you the link for help.", comment: "Tips on what to do if a note cannot be found.")
)
}
var unknown_or_unsupported_kind: some View {
SomethingWrong(
imageSystemName: "questionmark.app",
heading: NSLocalizedString("Cant display note", comment: "User-visible heading for an error message indicating a note has an unknown kind or is unsupported for viewing."),
description: NSLocalizedString("We do not yet support viewing this type of content.", comment: "User-visible description of an error indicating a note has an unknown kind or is unsupported for viewing."),
advice: NSLocalizedString("Please try opening this content on another Nostr app that supports this type of content.", comment: "User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing.")
)
}
// MARK: Skeleton views
// Implementation notes
// - No localization is needed because the text will be redacted
// - No accessibility label is needed because these will be summarized into a single accessibility label at the top-level view. See `body` in this struct
var skeleton: some View {
VStack(alignment: .leading, spacing: 40) {
Self.skeleton_selected_event
Self.skeleton_chat_event(message: "Nice! Have you tried Damus?", right: false)
Self.skeleton_chat_event(message: "Yes, it's awesome.", right: true)
Spacer()
}
.padding()
}
static func skeleton_chat_event(message: String, right: Bool) -> some View {
HStack(alignment: .center) {
if !right {
self.skeleton_chat_user_avatar
}
else {
Spacer()
}
ChatBubble(
direction: right ? .right : .left,
stroke_content: Color.accentColor.opacity(0),
stroke_style: .init(lineWidth: 4),
background_style: Color.secondary.opacity(0.5),
content: {
Text(verbatim: message)
.padding()
}
)
if right {
self.skeleton_chat_user_avatar
}
else {
Spacer()
}
}
}
static var skeleton_selected_event: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Circle()
.frame(width: 50, height: 50)
.foregroundStyle(.secondary.opacity(0.5))
Text(verbatim: "Satoshi Nakamoto")
.bold()
}
Text(verbatim: "Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive.")
HStack {
self.skeleton_action_item
Spacer()
self.skeleton_action_item
Spacer()
self.skeleton_action_item
Spacer()
self.skeleton_action_item
}
}
}
static var skeleton_chat_user_avatar: some View {
Circle()
.fill(.secondary.opacity(0.5))
.frame(width: 35, height: 35)
.padding(.bottom, -21)
}
static var skeleton_action_item: some View {
Circle()
.fill(Color.secondary.opacity(0.5))
.frame(width: 25, height: 25)
}
}
extension LoadableNostrEventView {
struct SomethingWrong: View {
let imageSystemName: String
let heading: String
let description: String
let advice: String
var body: some View {
VStack(spacing: 6) {
Image(systemName: imageSystemName)
.resizable()
.frame(width: 30, height: 30)
.accessibilityHidden(true)
Text(heading)
.font(.title)
.bold()
.padding(.bottom, 10)
Text(description)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 5) {
Image(systemName: "sparkles")
.accessibilityHidden(true)
Text("Advice", comment: "Heading for some advice text to help the user with an error")
.font(.headline)
}
Text(advice)
}
.padding()
.background(Color.secondary.opacity(0.2))
.cornerRadius(10)
.padding(.vertical, 30)
}
.padding()
}
}
}
#Preview("Loadable") {
LoadableNostrEventView(state: test_damus_state, note_reference: .note_id(test_thread_note_1.id))
}
+216
View File
@@ -0,0 +1,216 @@
//
// LoadableThreadView.swift
// damus
//
// Created by Daniel D'Aquino on 2025-01-08.
//
import SwiftUI
/// A view model for `LoadableThreadView`
///
/// This takes a note reference, automatically tries to load it, and updates itself to reflect its current state
///
///
class LoadableThreadModel: ObservableObject {
let damus_state: DamusState
let note_reference: NoteReference
@Published var state: ThreadModelLoadingState = .loading
/// The time period after which it will give up loading the view.
/// Written in nanoseconds
let TIMEOUT: UInt64 = 10 * 1_000_000_000 // 10 seconds
init(damus_state: DamusState, note_reference: NoteReference) {
self.damus_state = damus_state
self.note_reference = note_reference
Task { await self.load() }
}
func load() async {
// Start the loading process in a separate task to manage the timeout independently.
let loadTask = Task { @MainActor in
self.state = await executeLoadingLogic()
}
// Setup a timer to cancel the load after the timeout period
let timeoutTask = Task { @MainActor in
try await Task.sleep(nanoseconds: TIMEOUT)
loadTask.cancel() // This sends a cancellation signal to the load task.
self.state = .not_found
}
await loadTask.value
timeoutTask.cancel() // Cancel the timeout task if loading finishes earlier.
}
private func executeLoadingLogic() async -> ThreadModelLoadingState {
switch note_reference {
case .note_id(let note_id):
let res = await find_event(state: damus_state, query: .event(evid: note_id))
guard let res, case .event(let ev) = res else { return .not_found }
return .loaded(model: await ThreadModel(event: ev, damus_state: damus_state))
case .naddr(let naddr):
guard let event = await naddrLookup(damus_state: damus_state, naddr: naddr) else { return .not_found }
return .loaded(model: await ThreadModel(event: event, damus_state: damus_state))
}
}
enum ThreadModelLoadingState {
case loading
case loaded(model: ThreadModel)
case not_found
}
enum NoteReference: Hashable {
case note_id(NoteId)
case naddr(NAddr)
}
}
struct LoadableThreadView: View {
let state: DamusState
@StateObject var loadable_thread: LoadableThreadModel
var loading: Bool {
switch loadable_thread.state {
case .loading:
return true
case .loaded, .not_found:
return false
}
}
init(state: DamusState, note_reference: LoadableThreadModel.NoteReference) {
self.state = state
self._loadable_thread = StateObject.init(wrappedValue: LoadableThreadModel(damus_state: state, note_reference: note_reference))
}
var body: some View {
switch self.loadable_thread.state {
case .loading:
ScrollView(.vertical) {
self.skeleton
.redacted(reason: loading ? .placeholder : [])
.shimmer(loading)
.accessibilityElement(children: .ignore)
.accessibilityLabel(NSLocalizedString("Loading thread", comment: "Accessibility label for the thread view when it is loading"))
}
case .loaded(model: let thread_model):
ChatroomThreadView(damus: state, thread: thread_model)
case .not_found:
self.not_found
}
}
var not_found: some View {
VStack(spacing: 6) {
Image(systemName: "questionmark.app")
.resizable()
.frame(width: 30, height: 30)
.accessibilityHidden(true)
Text("Note not found", comment: "Heading for the thread view in a not found error state")
.font(.title)
.bold()
.padding(.bottom, 10)
Text("We were unable to find the note you were looking for.", comment: "Text for the thread view when it is unable to find the note the user is looking for")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 5) {
Image(systemName: "sparkles")
.accessibilityHidden(true)
Text("Advice", comment: "Heading for some advice text to help the user with an error")
.font(.headline)
}
Text("Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.", comment: "Tips on what to do if a note cannot be found.")
}
.padding()
.background(Color.secondary.opacity(0.2))
.cornerRadius(10)
.padding(.vertical, 30)
}
.padding()
}
// MARK: Skeleton views
// Implementation notes
// - No localization is needed because the text will be redacted
// - No accessibility label is needed because these will be summarized into a single accessibility label at the top-level view. See `body` in this struct
var skeleton: some View {
VStack(alignment: .leading, spacing: 40) {
self.skeleton_selected_event
self.skeleton_chat_event(message: "Nice! Have you tried Damus?", right: false)
self.skeleton_chat_event(message: "Yes, it's awesome.", right: true)
Spacer()
}
.padding()
}
func skeleton_chat_event(message: String, right: Bool) -> some View {
HStack(alignment: .center) {
if !right {
self.skeleton_chat_user_avatar
}
else {
Spacer()
}
ChatBubble(
direction: right ? .right : .left,
stroke_content: Color.accentColor.opacity(0),
stroke_style: .init(lineWidth: 4),
background_style: Color.secondary.opacity(0.5),
content: {
Text(message)
.padding()
}
)
if right {
self.skeleton_chat_user_avatar
}
else {
Spacer()
}
}
}
var skeleton_selected_event: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Circle()
.frame(width: 50, height: 50)
.foregroundStyle(.secondary.opacity(0.5))
Text("Satoshi Nakamoto")
.bold()
}
Text("Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive.")
HStack {
self.skeleton_action_item
Spacer()
self.skeleton_action_item
Spacer()
self.skeleton_action_item
Spacer()
self.skeleton_action_item
}
}
}
var skeleton_chat_user_avatar: some View {
Circle()
.fill(.secondary.opacity(0.5))
.frame(width: 35, height: 35)
.padding(.bottom, -21)
}
var skeleton_action_item: some View {
Circle()
.fill(Color.secondary.opacity(0.5))
.frame(width: 25, height: 25)
}
}
#Preview("Loadable") {
LoadableThreadView(state: test_damus_state, note_reference: .note_id(test_thread_note_1.id))
}
-3
View File
@@ -40,9 +40,6 @@ struct NoteContentView: View {
@ObservedObject var settings: UserSettingsStore
var note_artifacts: NoteArtifacts {
if damus_state.settings.undistractMode {
return .separated(.just_content(Undistractor.makeGibberish(text: event.get_content(damus_state.keypair))))
}
return self.artifacts_model.state.artifacts ?? .separated(.just_content(event.get_content(damus_state.keypair)))
}
@@ -86,10 +86,10 @@ struct DamusAppNotificationView: View {
Task {
do {
let url = try await damus_state.purple.generate_verified_ln_checkout_link(product_template_name: product_template_name)
self.open_url(url: url)
await self.open_url(url: url)
}
catch {
self.open_url(url: damus_state.purple.environment.purple_landing_page_url().appendingPathComponent("checkout"))
await self.open_url(url: damus_state.purple.environment.purple_landing_page_url().appendingPathComponent("checkout"))
}
}
}
@@ -60,8 +60,21 @@ struct NotificationsView: View {
@Environment(\.colorScheme) var colorScheme
var mystery: some View {
let profile_txn = state.profiles.lookup(id: state.pubkey)
let profile = profile_txn?.unsafeUnownedValue
return VStack(spacing: 20) {
Text("Wake up, \(Profile.displayName(profile: profile, pubkey: state.pubkey).displayName.truncate(maxLength: 50))", comment: "Text telling the user to wake up, where the argument is their display name.")
Text("You are dreaming...", comment: "Text telling the user that they are dreaming.")
}
.id("what")
}
var body: some View {
TabView(selection: $filter_state) {
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
mystery
NotificationTab(
NotificationFilter(
state: .all,
+72 -27
View File
@@ -71,12 +71,9 @@ struct PostView: View {
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
@State var newCursorIndex: Int?
@State var textHeight: CGFloat? = nil
/// Manages the auto-save logic for drafts.
///
/// ## Implementation notes
///
/// - This intentionally does _not_ use `@ObservedObject` or `@StateObject` because observing changes causes unwanted automatic scrolling to the text cursor on each save state update.
var autoSaveModel: AutoSaveIndicatorView.AutoSaveViewModel
@State var saved_state: SaveState = .needs_saving()
/// A timer that helps us add a delay between when changes occur and when they are saved persistently (to avoid too many disk writes and a jittery save indicator)
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State var preUploadedMedia: [PreUploadedMedia] = []
@@ -92,6 +89,16 @@ struct PostView: View {
let placeholder_messages: [String]
let initial_text_suffix: String?
enum SaveState: Equatable {
/// The draft has been modified and needs saving.
/// Saving should occur in N seconds
case needs_saving(seconds_remaining: Int = 3)
/// A saving operation is in progress
case saving
/// The draft has been saved to disk.
case saved
}
init(
action: PostAction,
damus_state: DamusState,
@@ -104,7 +111,6 @@ struct PostView: View {
self.prompt_view = prompt_view
self.placeholder_messages = placeholder_messages ?? [POST_PLACEHOLDER]
self.initial_text_suffix = initial_text_suffix
self.autoSaveModel = AutoSaveIndicatorView.AutoSaveViewModel(save: { damus_state.drafts.save(damus_state: damus_state) })
}
@Environment(\.dismiss) var dismiss
@@ -177,12 +183,33 @@ struct PostView: View {
})
}
var save_state_indicator: some View {
HStack {
switch saved_state {
case .needs_saving:
EmptyView()
.accessibilityHidden(true) // Probably no need to show this to visually impaired users, might be too noisy
case .saving:
ProgressView()
.accessibilityHidden(true) // Probably no need to show this to visually impaired users, might be too noisy
case .saved:
Image(systemName: "checkmark")
.accessibilityHidden(true)
Text("Saved", comment: "Small label indicating that the user's draft has been saved to storage")
.accessibilityLabel(NSLocalizedString("Your draft has been saved to storage", comment: "Accessibility label indicating that a user's post draft has been saved, meant only for visually impaired users"))
.font(.caption)
}
}
.padding(6)
.foregroundStyle(.secondary)
}
var AttachmentBar: some View {
HStack(alignment: .center, spacing: 15) {
ImageButton
CameraButton
Spacer()
AutoSaveIndicatorView(saveViewModel: self.autoSaveModel)
self.save_state_indicator
}
.disabled(uploading_disabled)
}
@@ -230,20 +257,19 @@ struct PostView: View {
damus_state.drafts.post = nil
}
damus_state.drafts.save(damus_state: damus_state)
}
func load_draft() -> Bool {
guard let draft = load_draft_for_post(drafts: self.damus_state.drafts, action: self.action) else {
self.post = NSMutableAttributedString("")
self.uploadedMedias = []
self.autoSaveModel.markNothingToSave() // We should not save empty drafts.
self.saved_state = .needs_saving()
return false
}
self.uploadedMedias = draft.media
self.post = draft.content
self.autoSaveModel.markSaved() // The draft we just loaded is saved to memory. Mark it as such.
self.saved_state = .saved
return true
}
@@ -261,7 +287,7 @@ struct PostView: View {
let artifacts = DraftArtifacts(content: post, media: uploadedMedias, references: references, id: UUID().uuidString)
set_draft_for_post(drafts: damus_state.drafts, action: action, artifacts: artifacts)
}
self.autoSaveModel.needsSaving()
self.saved_state = .needs_saving()
}
var TextEntry: some View {
@@ -576,6 +602,21 @@ struct PostView: View {
preUploadedMedia.removeAll()
}
}
.onReceive(timer) { time in
switch self.saved_state {
case .needs_saving(seconds_remaining: let seconds_remaining):
if seconds_remaining <= 0 {
self.saved_state = .saving
damus_state.drafts.save(damus_state: damus_state)
self.saved_state = .saved
}
else {
self.saved_state = .needs_saving(seconds_remaining: seconds_remaining - 1)
}
case .saving, .saved:
break
}
}
}
}
@@ -763,7 +804,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
// 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.
let other_matches = drafts.highlights
var other_matches = drafts.highlights
.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.
return other_matches.first?.value
@@ -865,29 +906,33 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
var content = post.string
.trimmingCharacters(in: .whitespacesAndNewlines)
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: "\n")
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
if !imagesString.isEmpty {
content.append("\n\n" + imagesString)
content.append(" " + imagesString + " ")
}
var tags: [[String]] = []
switch action {
case .replying_to(let replying_to):
// start off with the reply tags
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
case .replying_to(let replying_to):
// start off with the reply tags
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
case .quoting(let ev):
content.append("\n\nnostr:" + bech32_note_id(ev.id))
case .quoting(let ev):
content.append(" nostr:" + bech32_note_id(ev.id))
if let quoted_ev = state.events.lookup(ev.id) {
tags.append(["p", quoted_ev.pubkey.hex()])
}
case .posting, .highlighting, .sharing:
break
if let quoted_ev = state.events.lookup(ev.id) {
tags.append(["p", quoted_ev.pubkey.hex()])
}
case .posting(let postTarget):
break
case .highlighting(let draft):
break
case .sharing(_):
break
}
// append additional tags
@@ -909,7 +954,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
}
}
return NostrPost(content: content.trimmingCharacters(in: .whitespacesAndNewlines), kind: .text, tags: tags)
return NostrPost(content: content, kind: .text, tags: tags)
}
func isSupportedVideo(url: URL?) -> Bool {
@@ -1,136 +0,0 @@
//
// AutoSaveIndicatorView.swift
// damus
//
// Created by Daniel DAquino on 2025-02-12.
//
import SwiftUI
/// A small indicator view to indicate whether an item has been saved or not.
///
/// This view uses and observes an `AutoSaveViewModel`.
struct AutoSaveIndicatorView: View {
@ObservedObject var saveViewModel: AutoSaveViewModel
var body: some View {
HStack {
switch saveViewModel.savedState {
case .needsSaving, .nothingToSave:
EmptyView()
.accessibilityHidden(true) // Probably no need to show this to users with visual impairment, might be too noisy.
case .saving:
ProgressView()
.accessibilityHidden(true) // Probably no need to show this to users with visual impairment, might be too noisy.
case .saved:
Image(systemName: "checkmark")
.accessibilityHidden(true)
Text("Saved", comment: "Small label indicating that the user's draft has been saved to storage.")
.accessibilityLabel(NSLocalizedString("Your draft has been saved to storage.", comment: "Accessibility label indicating that a user's post draft has been saved, meant to be read by screen reading technology."))
.font(.caption)
}
}
.padding(6)
.foregroundStyle(.secondary)
}
}
extension AutoSaveIndicatorView {
/// A simple data structure to model the saving state of an item that can be auto-saved every few seconds.
enum SaveState: Equatable {
/// There is nothing to save (e.g. A new empty item was just created, an item was just loaded)
case nothingToSave
/// The item has been modified and needs saving.
/// Saving should occur in N seconds.
case needsSaving(secondsRemaining: Int)
/// A saving operation is in progress.
case saving
/// The item has been saved to disk.
case saved
}
/// Models an auto-save mechanism, which automatically saves an item after N seconds.
///
/// # Implementation notes
///
/// - This runs on the main actor because running this on other actors causes issues with published properties.
/// - Running on one actor helps ensure thread safety.
@MainActor
class AutoSaveViewModel: ObservableObject {
/// The delay between the time something is marked as needing to save, and the actual saving operation.
///
/// Should be low enough that the user does not lose significant progress, and should be high enough to avoid unnecessary disk writes and jittery, stress-inducing behavior
let saveDelay: Int
/// The current state of this model
@Published private(set) var savedState: SaveState
/// A timer which counts down the time to save the item
private var timer: Timer?
/// The function that performs the actual save operation
var save: () async -> Void
// MARK: Init/de-init
/// Initializes a new auto-save model
/// - Parameters:
/// - save: The function that performs the save operation
/// - initialState: Optional initial state
/// - saveDelay: The time delay between the item is marked as needing to be saved, and the actual save operation denoted in seconds.
init(save: @escaping () async -> Void, initialState: SaveState = .nothingToSave, saveDelay: Int = 3) {
self.saveDelay = saveDelay
self.savedState = initialState
self.save = save
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { timer in
Task { await self.tick() } // Task { await ... } ensures the function is properly run on the main actor and avoids thread-safety issues
})
self.timer = timer
}
deinit {
if let timer = self.timer {
timer.isValid ? timer.invalidate() : ()
}
}
// MARK: Internal logic
/// Runs internal countdown-to-save logic
private func tick() async {
switch self.savedState {
case .needsSaving(secondsRemaining: let secondsRemaining):
if secondsRemaining <= 0 {
self.savedState = .saving
await save()
self.savedState = .saved
}
else {
self.savedState = .needsSaving(secondsRemaining: secondsRemaining - 1)
}
case .saving, .saved, .nothingToSave:
break
}
}
// MARK: External interface
/// Marks item as needing to be saved.
/// Call this whenever your item is modified.
func needsSaving() {
self.savedState = .needsSaving(secondsRemaining: self.saveDelay)
}
/// Marks item as saved.
/// Call this when you know the item is already saved (e.g. when loading a saved item from memory).
func markSaved() {
self.savedState = .saved
}
/// Tells the auto-save logic that there is nothing to be saved.
/// Call this when there is nothing to be saved (e.g. when opening a new empty item).
func markNothingToSave() {
self.savedState = .nothingToSave
}
}
}
@@ -170,9 +170,6 @@ struct EditMetadataView: View {
TextField(NSLocalizedString("Lightning Address or LNURL", comment: "Placeholder text for entry of Lightning Address or LNURL."), text: $ln)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.onReceive(Just(ln)) { newValue in
self.ln = newValue.trimmingCharacters(in: .whitespaces)
}
}
Section(content: {
+1 -1
View File
@@ -287,7 +287,7 @@ struct EditPictureControl: View {
var accessibility_value: String? {
if style.first_time_setup {
if model.current_image_url != nil {
if let current_image_url = model.current_image_url {
switch self.model.context {
case .normal:
return NSLocalizedString("Image is setup", comment: "Accessibility value on image control")
+4 -21
View File
@@ -122,12 +122,6 @@ struct ProfileView: View {
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
var filters = ContentFilters.defaults(damus_state: damus_state)
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
}
@@ -435,17 +429,6 @@ struct ProfileView: View {
.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 {
ZStack {
ScrollView(.vertical) {
@@ -457,7 +440,10 @@ struct ProfileView: View {
aboutSection
VStack(spacing: 0) {
CustomPicker(tabs: tabs, selection: $filter_state)
CustomPicker(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)
], selection: $filter_state)
Divider()
.frame(height: 1)
}
@@ -469,9 +455,6 @@ struct ProfileView: View {
if filter_state == 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)
.zIndex(-yOffset > navbarHeight ? 0 : 1)
+1 -1
View File
@@ -297,7 +297,7 @@ fileprivate struct ProfileActionSheetZapButton: View {
.foregroundColor(Color.primary)
.profile_button_style(scheme: colorScheme)
case .zap_success:
Image("checkmark-damus")
Image("checkmark")
.foregroundColor(Color.green)
.profile_button_style(scheme: colorScheme)
case .zap_failure:
@@ -123,7 +123,7 @@ struct DamusPurpleAccountView: View {
func profile_display_name() -> String {
let profile_txn: NdbTxn<ProfileRecord?>? = damus_state.profiles.lookup_with_timestamp(account.pubkey)
let profile: NdbProfile? = profile_txn?.unsafeUnownedValue?.profile
let display_name = DisplayName(profile: profile, pubkey: account.pubkey).displayName
let display_name = parse_display_name(profile: profile, pubkey: account.pubkey).displayName
return display_name
}
}
+7 -10
View File
@@ -57,14 +57,11 @@ struct QRScanNSECView: View {
}
}
struct QRScanNSECView_Previews: PreviewProvider {
@State static var showQR = true
@State static var privKeyFound = false
@State static var shouldSaveKey = true
static var previews: some View {
QRScanNSECView(showQR: $showQR,
privKeyFound: $privKeyFound,
codeScannerCompletion: { _ in })
}
#Preview {
@State var showQR = true
@State var privKeyFound = false
@State var shouldSaveKey = true
return QRScanNSECView(showQR: $showQR,
privKeyFound: $privKeyFound,
codeScannerCompletion: { _ in })
}
+1 -1
View File
@@ -16,7 +16,7 @@ struct RepostedEvent: View {
var body: some View {
VStack(alignment: .leading) {
NavigationLink(value: Route.ProfileByKey(pubkey: event.pubkey)) {
Reposted(damus: damus, pubkey: event.pubkey, target: inner_ev)
Reposted(damus: damus, pubkey: event.pubkey)
.padding(.horizontal)
}
.buttonStyle(PlainButtonStyle())
@@ -17,7 +17,6 @@ struct DeveloperSettingsView: View {
Toggle(NSLocalizedString("Developer Mode", comment: "Setting to enable developer mode"), isOn: $settings.developer_mode)
.toggleStyle(.switch)
if settings.developer_mode {
Toggle(NSLocalizedString("Undistract mode", comment: "Developer mode setting to scramble text and images to avoid distractions during development."), isOn: $settings.undistractMode)
Toggle(NSLocalizedString("Always show onboarding", comment: "Developer mode setting to always show onboarding suggestions."), isOn: $settings.always_show_onboarding_suggestions)
Picker(NSLocalizedString("Push notification environment", comment: "Prompt selection of the Push notification environment (Developer feature to switch between real/production mode to test modes)."),
selection: Binding(
-3
View File
@@ -162,9 +162,6 @@ struct SideMenuView: View {
PubkeyView(pubkey: damus_state.pubkey, sidemenu: true)
.pubkey_context_menu(pubkey: damus_state.pubkey)
.simultaneousGesture(TapGesture().onEnded{
isSidebarVisible = true
})
}
}
}
@@ -25,6 +25,11 @@ struct PostingTimelineView: View {
@State var headerHeight: CGFloat = 0
@Binding var headerOffset: CGFloat
@SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
var mystery: some View {
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
.id("what")
}
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
var filters = ContentFilters.defaults(damus_state: damus_state)
@@ -90,6 +95,9 @@ struct PostingTimelineView: View {
VStack {
ZStack {
TabView(selection: $filter_state) {
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
mystery
contentTimelineView(filter: content_filter(.posts))
.tag(FilterState.posts)
.id(FilterState.posts)
-56
View File
@@ -1,56 +0,0 @@
//
// BalanceView.swift
// damus
//
// Created by eric on 1/23/25.
//
import SwiftUI
struct BalanceView: View {
var balance: Int64?
var body: some View {
VStack(spacing: 5) {
Text("Current balance", comment: "Label for displaying current wallet balance")
.foregroundStyle(DamusColors.neutral6)
if let balance {
self.numericalBalanceView(text: NumberFormatter.localizedString(from: NSNumber(integerLiteral: Int(balance)), number: .decimal))
}
else {
// Make sure we do not show any numeric value to the user when still loading (or when failed to load)
// This is important because if we show a numeric value like "zero" when things are not loaded properly, we risk scaring the user into thinking that they have lost funds.
self.numericalBalanceView(text: "??")
.redacted(reason: .placeholder)
.shimmer(true)
}
}
}
func numericalBalanceView(text: String) -> some View {
HStack {
Text(verbatim: text)
.lineLimit(1)
.minimumScaleFactor(0.70)
.font(.veryVeryLargeTitle)
.fontWeight(.heavy)
.foregroundStyle(PinkGradient)
HStack(alignment: .top) {
Text("SATS", comment: "Abbreviation for Satoshis, smallest bitcoin unit")
.font(.caption)
.fontWeight(.heavy)
.foregroundStyle(PinkGradient)
}
}
.padding(.bottom)
}
}
struct BalanceView_Previews: PreviewProvider {
static var previews: some View {
BalanceView(balance: 100000000)
BalanceView(balance: nil)
}
}
+87 -174
View File
@@ -15,13 +15,13 @@ struct ConnectWalletView: View {
@State private var showAlert = false
@State var error: String? = nil
@State var wallet_scan_result: WalletScanResult = .scanning
@State var show_introduction: Bool = true
var nav: NavigationCoordinator
var body: some View {
MainContent
.navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for attaching Nostr Wallet Connect lightning wallet."))
.navigationBarTitleDisplayMode(.inline)
.padding()
.onChange(of: wallet_scan_result) { res in
scanning = false
@@ -48,137 +48,57 @@ struct ConnectWalletView: View {
}
}
struct AreYouSure: View {
let nwc: WalletConnectURL
@Binding var show_introduction: Bool
@ObservedObject var model: WalletModel
var body: some View {
ScrollView {
VStack(spacing: 25) {
Text("Setup Wallet", comment: "Heading for wallet setup confirmation screen")
.font(.veryLargeTitle)
.fontWeight(.bold)
.multilineTextAlignment(.center)
Spacer()
ConnectGraphic
Spacer()
NWCSettings.AccountDetailsView(nwc: nwc)
Spacer()
Button(action: {
model.connect(nwc)
show_introduction = false
}) {
HStack {
Text("Connect", comment: "Text for button to conect to Nostr Wallet Connect lightning wallet.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
Button(action: {
model.cancel()
show_introduction = true
}) {
HStack {
Text("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning wallet.")
.padding()
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
}
.buttonStyle(NeutralButtonStyle())
func AreYouSure(nwc: WalletConnectURL) -> some View {
VStack(spacing: 25) {
Text("Are you sure you want to connect this wallet?", comment: "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.")
.fontWeight(.bold)
.multilineTextAlignment(.center)
Text(nwc.relay.absoluteString)
.font(.body)
.foregroundColor(.gray)
if let lud16 = nwc.lud16 {
Text(lud16)
.font(.body)
.foregroundColor(.gray)
}
Button(action: {
model.connect(nwc)
}) {
HStack {
Text("Connect", comment: "Text for button to conect to Nostr Wallet Connect lightning wallet.")
.fontWeight(.semibold)
}
.padding(.bottom, 50)
.padding()
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
}
var ConnectGraphic: some View {
HStack(spacing: 0) {
Button(action: {}, label: {
Image("damus-home")
.resizable()
.frame(width: 30, height: 30)
})
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 9999))
.disabled(true)
.padding(.horizontal, 30)
Image("chevron-double-right")
.resizable()
.frame(width: 25, height: 25)
Button(action: {}, label: {
Image("wallet")
.resizable()
.frame(width: 30, height: 30)
.foregroundStyle(LINEAR_GRADIENT)
})
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 9999))
.disabled(true)
.padding(.horizontal, 30)
.buttonStyle(GradientButtonStyle())
Button(action: {
model.cancel()
}) {
HStack {
Text("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning wallet.")
.padding()
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
}
.buttonStyle(NeutralButtonStyle())
}
}
var AutomaticSetup: some View {
VStack(spacing: 10) {
Text("AUTOMATIC SETUP", comment: "Heading for the section that performs an automatic wallet connection setup.")
.font(.caption)
.padding(.top)
.foregroundStyle(PinkGradient)
var ConnectWallet: some View {
VStack(spacing: 25) {
Text("Create new wallet", comment: "Button text for creating a new wallet.")
.font(.title)
.fontWeight(.bold)
Text("Easily create a new wallet and attach it to your account.", comment: "Description for the create new wallet feature.")
.font(.body)
.multilineTextAlignment(.center)
Spacer()
AlbyButton() {
openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!)
}
CoinosButton() {
show_introduction = false
openURL(URL(string:"https://coinos.io/settings/nostr")!)
}
.padding()
}
.frame(minHeight: 250)
.padding(10)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(DamusColors.neutral3, lineWidth: 2)
.padding(2) // Avoids border clipping on the sides
)
.padding(.top, 20)
}
var ManualSetup: some View {
VStack(spacing: 10) {
Text("MANUAL SETUP", comment: "Label for manual wallet setup.")
.font(.caption)
.padding(.top)
.foregroundStyle(PinkGradient)
Text("Use existing", comment: "Button text to use an existing wallet.")
.font(.title)
.fontWeight(.bold)
Text("Attach to any third party provider you already use.", comment: "Information text guiding users on attaching existing provider.")
.font(.body)
.multilineTextAlignment(.center)
Spacer()
Button(action: {
if let pasted_nwc = UIPasteboard.general.string {
@@ -195,10 +115,9 @@ struct ConnectWalletView: View {
Text("Paste NWC Address", comment: "Text for button to connect a lightning wallet.")
.fontWeight(.semibold)
}
.frame(minWidth: 250, maxWidth: .infinity, maxHeight: 15, alignment: .center)
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.horizontal)
Button(action: {
nav.push(route: Route.WalletScanner(result: $wallet_scan_result))
@@ -208,80 +127,74 @@ struct ConnectWalletView: View {
Text("Scan NWC Address", comment: "Text for button to connect a lightning wallet.")
.fontWeight(.semibold)
}
.frame(minWidth: 250, maxWidth: .infinity, maxHeight: 15, alignment: .center)
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.horizontal)
.padding(.bottom)
if let err = self.error {
Text(err)
.foregroundColor(.red)
}
}
.frame(minHeight: 300)
.padding(10)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(DamusColors.neutral3, lineWidth: 2)
.padding(2) // Avoids border clipping on the sides
)
.padding(.top, 20)
}
var ConnectWallet: some View {
ScrollView {
VStack(spacing: 25) {
Text("Setup Wallet", comment: "Heading for Nostr Wallet Connect setup screen")
.font(.veryLargeTitle)
.fontWeight(.bold)
.multilineTextAlignment(.center)
AutomaticSetup
ManualSetup
if let err = self.error {
Text(err)
.foregroundColor(.red)
}
}
.padding(.bottom, 50)
.padding()
var TopSection: some View {
HStack(spacing: 0) {
Button(action: {}, label: {
Image("damus-home")
.resizable()
.frame(width: 30, height: 30)
})
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 9999))
.disabled(true)
.padding(.horizontal, 30)
Image("chevron-double-right")
.resizable()
.frame(width: 25, height: 25)
Button(action: {}, label: {
Image("wallet")
.resizable()
.frame(width: 30, height: 30)
.foregroundStyle(LINEAR_GRADIENT)
})
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 9999))
.disabled(true)
.padding(.horizontal, 30)
}
}
var TitleSection: some View {
VStack(spacing: 25) {
Text("Damus Wallet", comment: "Title text for Damus Wallet view.")
.fontWeight(.bold)
Text("Securely connect your Damus app to your wallet using Nostr Wallet Connect", comment: "Text to prompt user to connect their wallet using 'Nostr Wallet Connect'.")
.font(.caption)
.multilineTextAlignment(.center)
}
}
var MainContent: some View {
Group {
TopSection
switch model.connect_state {
case .new(let nwc):
AreYouSure(nwc: nwc, show_introduction: $show_introduction, model: self.model)
.onAppear() {
show_introduction = false
}
AreYouSure(nwc: nwc)
case .existing:
Text(verbatim: "Shouldn't happen")
case .none:
TitleSection
ConnectWallet
}
}
.fullScreenCover(isPresented: $show_introduction, content: {
ZapExplainerView(show_introduction: $show_introduction, nav: nav)
})
}
}
struct ConnectWalletView_Previews: PreviewProvider {
static var previews: some View {
ConnectWalletView(model: WalletModel(settings: UserSettingsStore()), nav: .init())
.previewDisplayName("Main Wallet Connect View")
ConnectWalletView.AreYouSure(nwc: get_test_nwc(), show_introduction: .constant(false), model: WalletModel(settings: test_damus_state.settings))
.previewDisplayName("Are you sure screen")
}
static func get_test_nwc() -> WalletConnectURL {
let pk = "9d088f4760422443d4699b485e2ac66e565a2f5da1198c55ddc5679458e3f67a"
let sec = "ff2eefd57196d42089e1b42acc39916d7ecac52e0625bd70597bbd5be14aff18"
let relay = "wss://relay.getalby.com/v1"
let str = "nostrwalletconnect://\(pk)?relay=\(relay)&secret=\(sec)"
return WalletConnectURL(str: str)!
}
}
-227
View File
@@ -1,227 +0,0 @@
//
// NWCSettings.swift
// damus
//
// Created by eric on 1/24/25.
//
import SwiftUI
struct NWCSettings: View {
let damus_state: DamusState
let nwc: WalletConnectURL
@ObservedObject var model: WalletModel
@ObservedObject var settings: UserSettingsStore
func donation_binding() -> Binding<Double> {
return Binding(get: {
return Double(model.settings.donation_percent)
}, set: { v in
model.settings.donation_percent = Int(v)
})
}
static let min_donation: Double = 0.0
static let max_donation: Double = 100.0
var percent: Double {
Double(model.settings.donation_percent) / 100.0
}
var tip_msats: String {
let msats = Int64(percent * Double(model.settings.default_zap_amount * 1000))
let s = format_msats_abbrev(msats)
// TODO: fix formatting and remove this hack
let parts = s.split(separator: ".")
if parts.count == 1 {
return s
}
if let end = parts[safe: 1] {
if end.allSatisfy({ c in c.isNumber }) {
return String(parts[0])
} else {
return s
}
}
return s
}
var SupportDamus: some View {
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 20)
.fill(DamusGradient.gradient.opacity(0.5))
VStack(alignment: .leading, spacing: 20) {
HStack {
Image("logo-nobg")
.resizable()
.frame(width: 50, height: 50)
Text("Support Damus", comment: "Text calling for the user to support Damus through zaps")
.font(.title.bold())
.foregroundColor(.white)
}
Text("Help build the future of decentralized communication on the web.", comment: "Text indicating the goal of developing Damus which the user can help with.")
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.white)
Text("An additional percentage of each zap will be sent to support Damus development", comment: "Text indicating that they can contribute zaps to support Damus development.")
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.white)
let binding = donation_binding()
HStack {
Slider(value: binding,
in: NWCSettings.min_donation...NWCSettings.max_donation,
label: { })
Text("\(Int(binding.wrappedValue))%", comment: "Percentage of additional zap that should be sent to support Damus development.")
.font(.title.bold())
.foregroundColor(.white)
.frame(width: 80)
}
HStack{
Spacer()
VStack {
HStack {
Text("\(Image("zap.fill")) \(format_msats_abbrev(Int64(model.settings.default_zap_amount) * 1000))")
.font(.title)
.foregroundColor(percent == 0 ? .gray : .yellow)
.frame(width: 120)
}
Text("Zap", comment: "Text underneath the number of sats indicating that it's the amount used for zaps.")
.foregroundColor(.white)
}
Spacer()
Text(verbatim: "+")
.font(.title)
.foregroundColor(.white)
Spacer()
VStack {
HStack {
Text("\(Image("zap.fill")) \(tip_msats)")
.font(.title)
.foregroundColor(percent == 0 ? .gray : Color.yellow)
.frame(width: 120)
}
Text(verbatim: percent == 0 ? "🩶" : "💜")
.foregroundColor(.white)
}
Spacer()
}
EventProfile(damus_state: damus_state, pubkey: damus_state.pubkey, size: .small)
}
.padding(25)
}
.frame(height: 370)
}
var body: some View {
VStack(alignment: .leading, spacing: 20) {
SupportDamus
.padding(.bottom)
AccountDetailsView(nwc: nwc)
Button(action: {
self.model.disconnect()
}) {
HStack {
Text("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
}
.padding()
.onAppear() {
model.initial_percent = model.settings.donation_percent
}
.onChange(of: model.settings.donation_percent) { p in
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
guard let profile = profile_txn?.unsafeUnownedValue else {
return
}
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: p, reactions: profile.reactions)
notify(.profile_updated(.manual(pubkey: self.damus_state.pubkey, profile: prof)))
}
.onDisappear {
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
guard let keypair = damus_state.keypair.to_full(),
let profile = profile_txn?.unsafeUnownedValue,
model.initial_percent != profile.damus_donation
else {
return
}
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: model.settings.donation_percent, reactions: profile.reactions)
guard let meta = make_metadata_event(keypair: keypair, metadata: prof) else {
return
}
damus_state.postbox.send(meta)
}
}
struct AccountDetailsView: View {
let nwc: WalletConnect.ConnectURL
var body: some View {
VStack(alignment: .leading) {
Text("Account details", comment: "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.")
.font(.title2)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.padding(.bottom)
Text("Routing", comment: "Label indicating the routing address for Nostr Wallet Connect payments. In other words, the relay used by the NWC wallet provider")
.font(.headline)
Text(nwc.relay.absoluteString)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.gray)
.padding(.bottom)
if let lud16 = nwc.lud16 {
Text("Account", comment: "Label for the user account information with the Nostr Wallet Connect wallet provider.")
.font(.headline)
Text(lud16)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.gray)
}
}
.frame(maxWidth: .infinity, minHeight: 250, alignment: .leading)
.padding(.horizontal, 20)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(DamusColors.neutral3, lineWidth: 2)
)
}
}
}
struct NWCSettings_Previews: PreviewProvider {
static let tds = test_damus_state
static var previews: some View {
NWCSettings(damus_state: tds, nwc: test_wallet_connect_url, model: WalletModel(state: .existing(test_wallet_connect_url), settings: tds.settings), settings: tds.settings)
}
}
-148
View File
@@ -1,148 +0,0 @@
//
// TransactionsView.swift
// damus
//
// Created by eric on 1/23/25.
//
import SwiftUI
struct TransactionView: View {
let damus_state: DamusState
var transaction: WalletConnect.Transaction
var body: some View {
let isIncomingTransaction = transaction.type == "incoming"
let txType = isIncomingTransaction ? "arrow-bottom-left" : "arrow-top-right"
let txColor = isIncomingTransaction ? DamusColors.success : Color.gray
let txOp = isIncomingTransaction ? "+" : "-"
let created_at = Date.init(timeIntervalSince1970: TimeInterval(transaction.created_at))
let formatter = RelativeDateTimeFormatter()
let relativeDate = formatter.localizedString(for: created_at, relativeTo: Date.now)
let event = decode_nostr_event_json(transaction.description ?? "")
let pubkey = (event?.pubkey ?? ANON_PUBKEY)
VStack(alignment: .leading) {
HStack(alignment: .center) {
ZStack {
ProfilePicView(pubkey: pubkey, size: 45, highlight: .custom(.damusAdaptableBlack, 0.1), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
Image(txType)
.resizable()
.frame(width: 18, height: 18)
.foregroundColor(.white)
.padding(2)
.background(txColor)
.clipShape(Circle())
.overlay(Circle().stroke(Color.damusAdaptableWhite, lineWidth: 1.0))
.padding(.top, 25)
.padding(.leading, 35)
}
VStack(alignment: .leading, spacing: 10) {
Text(self.userDisplayName(pubkey: pubkey))
.font(.headline)
.bold()
.foregroundColor(DamusColors.adaptableBlack)
Text(relativeDate)
.font(.caption)
.foregroundColor(Color.gray)
}
.padding(.horizontal, 10)
Spacer()
Text(verbatim: "\(txOp) \(format_msats(transaction.amount))")
.font(.headline)
.foregroundColor(txColor)
.bold()
}
.frame(maxWidth: .infinity, minHeight: 75, alignment: .center)
.padding(.horizontal, 10)
.background(DamusColors.neutral1)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
}
}
func userDisplayName(pubkey: Pubkey) -> String {
let profile_txn = damus_state.profiles.lookup(id: pubkey, txn_name: "txview-profile")
let profile = profile_txn?.unsafeUnownedValue
if let display_name = profile?.display_name {
return display_name
} else if let name = profile?.name {
return "@" + name
} else {
return NSLocalizedString("Unknown", comment: "A name label for an unknown user")
}
}
}
struct TransactionsView: View {
let damus_state: DamusState
let transactions: [WalletConnect.Transaction]?
var sortedTransactions: [WalletConnect.Transaction]? {
transactions?.sorted(by: { $0.created_at > $1.created_at })
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Latest transactions", comment: "Heading for latest wallet transactions list")
.foregroundStyle(DamusColors.neutral6)
if let sortedTransactions {
if sortedTransactions.isEmpty {
emptyTransactions
} else {
ForEach(sortedTransactions, id: \.self) { transaction in
TransactionView(damus_state: damus_state, transaction: transaction)
}
}
}
else {
// Make sure we do not show "No transactions yet" to the user when still loading (or when failed to load)
// This is important because if we show that when things are not loaded properly, we risk scaring the user into thinking that they have lost funds.
emptyTransactions
.redacted(reason: .placeholder)
.shimmer(true)
}
}
}
var emptyTransactions: some View {
HStack {
Text("No transactions yet", comment: "Message shown when no transactions are available")
.foregroundStyle(DamusColors.neutral6)
}
.frame(maxWidth: .infinity, minHeight: 75, alignment: .center)
.padding(.horizontal, 10)
.background(DamusColors.neutral1)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
}
}
struct TransactionsView_Previews: PreviewProvider {
static let tds = test_damus_state
static let transaction1: WalletConnect.Transaction = WalletConnect.Transaction(type: "incoming", invoice: "", description: "{\"id\":\"7c0999a5870ca3ba0186a29a8650152b555cee29b53b5b8747d8a3798042d01c\",\"pubkey\":\"b8851a06dfd79d48fc325234a15e9a46a32a0982a823b54cdf82514b9b120ba1\",\"created_at\":1736383715,\"kind\":9734,\"tags\":[[\"p\",\"520830c334a3f79f88cac934580d26f91a7832c6b21fb9625690ea2ed81b5626\"],[\"amount\",\"21000\"],[\"e\",\"a25e152a4cd1b3bbc3d22e8e9315d8ea1f35c227b2f212c7cff18abff36fa208\"],[\"relays\",\"wss://nos.lol\",\"wss://nostr.wine\",\"wss://premium.primal.net\",\"wss://relay.damus.io\",\"wss://relay.nostr.band\",\"wss://relay.nostrarabia.com\"]],\"content\":\"🫡 Onward!\",\"sig\":\"e77d16822fa21b9c2e6b580b51c470588052c14aeb222f08f0e735027e366157c8742a6d5cb850780c2bf44ac63d89b048e5cc56dd47a1bfc740a3173e578f4e\"}", description_hash: "", preimage: "", payment_hash: "1234567890", amount: 21000, fees_paid: 0, created_at: 1737736866, expires_at: 0, settled_at: 0)
static let transaction2: WalletConnect.Transaction = WalletConnect.Transaction(type: "incoming", invoice: "", description: "", description_hash: "", preimage: "", payment_hash: "123456789033", amount: 100000000, fees_paid: 0, created_at: 1737690090, expires_at: 0, settled_at: 0)
static let transaction3: WalletConnect.Transaction = WalletConnect.Transaction(type: "outgoing", invoice: "", description: "", description_hash: "", preimage: "", payment_hash: "123456789042", amount: 303000, fees_paid: 0, created_at: 1737590101, expires_at: 0, settled_at: 0)
static let transaction4: WalletConnect.Transaction = WalletConnect.Transaction(type: "incoming", invoice: "", description: "", description_hash: "", preimage: "", payment_hash: "1234567890662", amount: 720000, fees_paid: 0, created_at: 1737090300, expires_at: 0, settled_at: 0)
static var test_transactions: [WalletConnect.Transaction] = [transaction1, transaction2, transaction3, transaction4]
static var previews: some View {
TransactionsView(damus_state: tds, transactions: test_transactions)
}
}
+191 -46
View File
@@ -9,7 +9,6 @@ import SwiftUI
struct WalletView: View {
let damus_state: DamusState
@State var show_settings: Bool = false
@ObservedObject var model: WalletModel
@ObservedObject var settings: UserSettingsStore
@@ -22,20 +21,178 @@ struct WalletView: View {
func MainWalletView(nwc: WalletConnectURL) -> some View {
ScrollView {
VStack(spacing: 35) {
VStack(spacing: 5) {
BalanceView(balance: model.balance)
TransactionsView(damus_state: damus_state, transactions: model.transactions)
if !damus_state.settings.nozaps {
SupportDamus
.padding(.vertical, 20)
}
VStack(spacing: 5) {
VStack(spacing: 10) {
Text("Wallet Relay", comment: "Label text indicating that below it is the information about the wallet relay.")
.fontWeight(.semibold)
.padding(.top)
Divider()
RelayView(state: damus_state, relay: nwc.relay, showActionButtons: .constant(false), recommended: false)
}
.frame(maxWidth: .infinity, minHeight: 125, alignment: .top)
.padding(.horizontal, 10)
.background(DamusColors.neutral1)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
if let lud16 = nwc.lud16 {
VStack(spacing: 10) {
Text("Wallet Address", comment: "Label text indicating that below it is the wallet address.")
.fontWeight(.semibold)
Divider()
Text(lud16)
}
.frame(maxWidth: .infinity, minHeight: 75, alignment: .center)
.padding(.horizontal, 10)
.background(DamusColors.neutral1)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
}
}
Button(action: {
self.model.disconnect()
}) {
HStack {
Text("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.bottom, 50) // Bottom padding while Scrolling
}
.navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for Wallet view"))
.navigationBarTitleDisplayMode(.inline)
.padding()
.padding(.bottom, 50)
}
}
func donation_binding() -> Binding<Double> {
return Binding(get: {
return Double(model.settings.donation_percent)
}, set: { v in
model.settings.donation_percent = Int(v)
})
}
static let min_donation: Double = 0.0
static let max_donation: Double = 100.0
var percent: Double {
Double(model.settings.donation_percent) / 100.0
}
var tip_msats: String {
let msats = Int64(percent * Double(model.settings.default_zap_amount * 1000))
let s = format_msats_abbrev(msats)
// TODO: fix formatting and remove this hack
let parts = s.split(separator: ".")
if parts.count == 1 {
return s
}
if let end = parts[safe: 1] {
if end.allSatisfy({ c in c.isNumber }) {
return String(parts[0])
} else {
return s
}
}
return s
}
var SupportDamus: some View {
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 20)
.fill(DamusGradient.gradient.opacity(0.5))
VStack(alignment: .leading, spacing: 20) {
HStack {
Image("logo-nobg")
.resizable()
.frame(width: 50, height: 50)
Text("Support Damus", comment: "Text calling for the user to support Damus through zaps")
.font(.title.bold())
.foregroundColor(.white)
}
Text("Help build the future of decentralized communication on the web.", comment: "Text indicating the goal of developing Damus which the user can help with.")
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.white)
Text("An additional percentage of each zap will be sent to support Damus development", comment: "Text indicating that they can contribute zaps to support Damus development.")
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.white)
let binding = donation_binding()
HStack {
Slider(value: binding,
in: WalletView.min_donation...WalletView.max_donation,
label: { })
Text("\(Int(binding.wrappedValue))%", comment: "Percentage of additional zap that should be sent to support Damus development.")
.font(.title.bold())
.foregroundColor(.white)
.frame(width: 80)
}
HStack{
Spacer()
VStack {
HStack {
Text("\(Image("zap.fill")) \(format_msats_abbrev(Int64(model.settings.default_zap_amount) * 1000))")
.font(.title)
.foregroundColor(percent == 0 ? .gray : .yellow)
.frame(width: 120)
}
Text("Zap", comment: "Text underneath the number of sats indicating that it's the amount used for zaps.")
.foregroundColor(.white)
}
Spacer()
Text(verbatim: "+")
.font(.title)
.foregroundColor(.white)
Spacer()
VStack {
HStack {
Text("\(Image("zap.fill")) \(tip_msats)")
.font(.title)
.foregroundColor(percent == 0 ? .gray : Color.yellow)
.frame(width: 120)
}
Text(verbatim: percent == 0 ? "🩶" : "💜")
.foregroundColor(.white)
}
Spacer()
}
EventProfile(damus_state: damus_state, pubkey: damus_state.pubkey, size: .small)
}
.padding(25)
}
.frame(height: 370)
}
var body: some View {
switch model.connect_state {
case .new:
@@ -44,50 +201,38 @@ struct WalletView: View {
ConnectWalletView(model: model, nav: damus_state.nav)
case .existing(let nwc):
MainWalletView(nwc: nwc)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(
action: { show_settings = true },
label: {
Image("settings")
.foregroundColor(.gray)
}
)
}
}
.onAppear() {
Task { await self.updateWalletInformation() }
model.initial_percent = settings.donation_percent
}
.refreshable {
model.resetWalletStateInformation()
await self.updateWalletInformation()
}
.sheet(isPresented: $show_settings, onDismiss: { self.show_settings = false }) {
ScrollView {
NWCSettings(damus_state: damus_state, nwc: nwc, model: model, settings: settings)
.padding(.top, 30)
.onChange(of: settings.donation_percent) { p in
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
guard let profile = profile_txn?.unsafeUnownedValue else {
return
}
.presentationDragIndicator(.visible)
.presentationDetents([.large])
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: p, reactions: profile.reactions)
notify(.profile_updated(.manual(pubkey: self.damus_state.pubkey, profile: prof)))
}
.onDisappear {
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
guard let keypair = damus_state.keypair.to_full(),
let profile = profile_txn?.unsafeUnownedValue,
model.initial_percent != profile.damus_donation
else {
return
}
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: settings.donation_percent, reactions: profile.reactions)
guard let meta = make_metadata_event(keypair: keypair, metadata: prof) else {
return
}
damus_state.postbox.send(meta)
}
}
}
@MainActor
func updateWalletInformation() async {
guard let url = damus_state.settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: url) else {
return
}
let flusher: OnFlush? = nil
let delay = 0.0 // We don't need a delay when fetching a transaction list or balance
WalletConnect.request_transaction_list(url: nwc, pool: damus_state.pool, post: damus_state.postbox, delay: delay, on_flush: flusher)
WalletConnect.request_balance_information(url: nwc, pool: damus_state.pool, post: damus_state.postbox, delay: delay, on_flush: flusher)
return
}
}
let test_wallet_connect_url = WalletConnectURL(pubkey: test_pubkey, relay: .init("wss://relay.damus.io")!, keypair: test_damus_state.keypair.to_full()!, lud16: "jb55@sendsats.com")
-203
View File
@@ -1,203 +0,0 @@
//
// ZapExplainer.swift
// damus
//
// Created by eric on 2/12/25.
//
import SwiftUI
struct ZapExplainerView: View {
@Binding var show_introduction: Bool
var nav: NavigationCoordinator
@Environment(\.colorScheme) var colorScheme
var body: some View {
ScrollView {
VStack {
Text("Get cash instantly from your followers", comment: "Feature description for receiving money instantly.")
.font(.veryLargeTitle)
.multilineTextAlignment(.center)
.padding(.top)
VStack(alignment: .leading) {
GetPaid
Gift
GiveThanks
}
WhyZaps
ScrollView(.horizontal) {
HStack(spacing: 20) {
FindWallet
LinkAccount
StartReceiving
}
.padding(5)
}
.scrollIndicators(.hidden)
Button(action: {
show_introduction = false
}) {
HStack {
Text("Set up wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.top, 30)
Button(action: {
nav.popToRoot()
}) {
HStack {
Text("Maybe later", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
.padding()
}
.buttonStyle(NeutralButtonStyle())
}
.padding(.bottom)
.padding(.horizontal)
}
.scrollIndicators(.never)
.background(
Image("eula-bg")
.resizable()
.blur(radius: 70)
.opacity(colorScheme == .light ? 0.6 : 1.0)
.ignoresSafeArea(),
alignment: .top
)
}
var GetPaid: some View {
self.benefitPoint(
imageName: "zap.fill",
heading: NSLocalizedString("Get paid for being you", comment: "Description for monetizing one's presence."),
description: NSLocalizedString("Setting up Zaps lets people know you're ready to start receiving money.", comment: "Information about enabling payments.")
)
}
var Gift: some View {
self.benefitPoint(
imageName: "gift",
heading: NSLocalizedString("Let your fans show their support", comment: "Heading pointing out a benefit of connecting a lightning wallet."),
description: NSLocalizedString("You drive the conversation and we want to make it easier for people to support your work beyond follows, reposts, and likes.", comment: "Text explaining the benefit of connecting a lightning wallet for content creators.")
)
}
var GiveThanks: some View {
self.benefitPoint(
imageName: "gift",
heading: NSLocalizedString("Give thanks", comment: "Heading explaining a benefit of connecting a lightning wallet."),
description: NSLocalizedString("When supporters tip with Zaps, they can add a note and we can make it easy for you to instantly reply to show your gratitude.", comment: "Description explaining a benefit of connecting a lightning wallet.")
)
}
func benefitPoint(imageName: String, heading: String, description: String) -> some View {
VStack(alignment: .leading) {
HStack(alignment: .top, spacing: 10) {
Button(action: {}, label: {
Image(imageName)
.resizable()
.frame(width: 25, height: 25)
})
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10), cornerRadius: 9999))
.disabled(true)
VStack(alignment: .leading, spacing: 10) {
Text(heading)
.font(.title2)
.fontWeight(.bold)
Text(description)
.font(.body)
}
.padding(.top, 9)
}
}
.padding(.top)
}
var WhyZaps: some View {
VStack(alignment: .leading, spacing: 15) {
Text("Why add Zaps?", comment: "Heading to explain the benefits of zaps.")
.font(.title)
.fontWeight(.bold)
Text("Zaps are an easy way to support the incredible\nvoices that make up the conversation on nostr.\nHere's how it works", comment: "Describing the functional benefits of Zaps.")
.lineLimit(4)
.font(.body)
}
.padding(.top, 30)
}
var FindWallet: some View {
self.WhyAddZapsBox(
iconName: "wallet.fill",
heading: NSLocalizedString("Find a Wallet", comment: "The heading for one of the \"Why add Zaps?\" boxes"),
description: NSLocalizedString("Choose the third-party payment provider you'd like to use.", comment: "The description for one of the \"Why add Zaps?\" boxes")
)
}
var LinkAccount: some View {
self.WhyAddZapsBox(
iconName: "link",
heading: NSLocalizedString("Link your account", comment: "The heading for one of the \"Why add Zaps?\" boxes"),
description: NSLocalizedString("Link to services that support Nostr Wallet Connect like Alby, Coinos and more.", comment: "The description for one of the \"Why add Zaps?\" boxes")
)
}
var StartReceiving: some View {
self.WhyAddZapsBox(
iconName: "bitcoin",
heading: NSLocalizedString("Start receiving money", comment: "The heading for one of the \"Why add Zaps?\" boxes"),
description: NSLocalizedString("People will be able to send you cash from your profile. No money goes to Damus.", comment: "The description for one of the \"Why add Zaps?\" boxes")
)
}
func WhyAddZapsBox(iconName: String, heading: String, description: String) -> some View {
VStack(alignment: .leading, spacing: 2) {
Button(action: {}, label: {
Image(iconName)
.resizable()
.frame(width: 25, height: 25)
})
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10), cornerRadius: 9999))
.disabled(true)
Text(heading)
.font(.title2)
.fontWeight(.bold)
.padding(.bottom, 2)
Text(description)
.font(.caption)
Spacer()
}
.frame(maxWidth: 175, minHeight: 175)
.padding(10)
.background(DamusColors.neutral1)
.cornerRadius(15)
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(DamusColors.neutral1, lineWidth: 2)
)
.padding(.top, 20)
}
}
struct ZapExplainerView_Previews: PreviewProvider {
static var previews: some View {
ZapExplainerView(show_introduction: .constant(true), nav: .init())
}
}
+8 -9
View File
@@ -24,8 +24,8 @@ struct ProfileZapLinkView<Content: View>: View {
self.label = label
self.action = action
self.reactions_enabled = reactions_enabled
self.lud16 = lud16?.trimmingCharacters(in: .whitespaces)
self.lnurl = lnurl?.trimmingCharacters(in: .whitespaces)
self.lud16 = lud16
self.lnurl = lnurl
}
init(damus_state: DamusState, pubkey: Pubkey, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
@@ -36,8 +36,8 @@ struct ProfileZapLinkView<Content: View>: View {
let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey)
let record = profile_txn?.unsafeUnownedValue
self.reactions_enabled = record?.profile?.reactions ?? true
self.lud16 = record?.profile?.lud06?.trimmingCharacters(in: .whitespaces)
self.lnurl = record?.lnurl?.trimmingCharacters(in: .whitespaces)
self.lud16 = record?.profile?.lud06
self.lnurl = record?.lnurl
}
init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
@@ -46,8 +46,8 @@ struct ProfileZapLinkView<Content: View>: View {
self.action = action
self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true
self.lud16 = unownedProfileRecord?.profile?.lud16?.trimmingCharacters(in: .whitespaces)
self.lnurl = unownedProfileRecord?.lnurl?.trimmingCharacters(in: .whitespaces)
self.lud16 = unownedProfileRecord?.profile?.lud16
self.lnurl = unownedProfileRecord?.lnurl
}
var body: some View {
@@ -81,13 +81,12 @@ struct ProfileZapLinkView<Content: View>: View {
}
}
}
.disabled(lnurl == nil && lud16 == nil)
.disabled(lnurl == nil)
}
}
#Preview {
let profile = make_test_profile()
ProfileZapLinkView(pubkey: test_pubkey, reactions_enabled: true, lud16: profile.lud16, lnurl: profile.lud06, label: { reactions_enabled, lud16, lnurl in
ProfileZapLinkView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in
Image("zap.fill")
})
}
-2
View File
@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
+2 -70
View File
@@ -5,7 +5,6 @@
// Created by William Casarin on 2022-04-01.
//
import Kingfisher
import SwiftUI
import StoreKit
@@ -60,28 +59,13 @@ struct MainView: View {
}
}
func registerNotificationCategories() {
// Define the communication category
let communicationCategory = UNNotificationCategory(
identifier: "COMMUNICATION",
actions: [],
intentIdentifiers: ["INSendMessageIntent"],
options: []
)
// Register the category with the notification center
UNUserNotificationCenter.current().setNotificationCategories([communicationCategory])
}
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var state: DamusState? = nil
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
SKPaymentQueue.default().add(StoreObserver.standard)
registerNotificationCategories()
migrateKingfisherCacheIfNeeded()
configureKingfisherCache()
return true
}
@@ -102,65 +86,13 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
Log.info("App delegate is handling a push notification", for: .push_notifications)
let userInfo = response.notification.request.content.userInfo
guard let notification = LossyLocalNotification.from_user_info(user_info: userInfo) else {
Log.error("App delegate could not decode notification information", for: .push_notifications)
return
}
Log.info("App delegate notifying the app about the received push notification", for: .push_notifications)
Task { await QueueableNotify<LossyLocalNotification>.shared.add(item: notification) }
notify(.local_notification(notification))
completionHandler()
}
private func migrateKingfisherCacheIfNeeded() {
let fileManager = FileManager.default
let defaults = UserDefaults.standard
let migrationKey = "KingfisherCacheMigrated"
// Check if migration has already been done
guard !defaults.bool(forKey: migrationKey) else { return }
// Get the default Kingfisher cache (before we override it)
let defaultCache = ImageCache.default
let oldCachePath = defaultCache.diskStorage.directoryURL.path
// New shared cache location
guard let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER) else { return }
let newCachePath = groupURL.appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME).path
// Check if the old cache exists
if fileManager.fileExists(atPath: oldCachePath) {
do {
// Move the old cache to the new location
try fileManager.moveItem(atPath: oldCachePath, toPath: newCachePath)
print("Successfully migrated Kingfisher cache to \(newCachePath)")
} catch {
print("Failed to migrate cache: \(error)")
// Optionally, copy instead of move if you want to preserve the old cache as a fallback
do {
try fileManager.copyItem(atPath: oldCachePath, toPath: newCachePath)
print("Copied cache instead due to error")
} catch {
print("Failed to copy cache: \(error)")
}
}
}
// Mark migration as complete
defaults.set(true, forKey: migrationKey)
}
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
}
}
}
class OrientationTracker: ObservableObject {
Binary file not shown.
Binary file not shown.
+1 -17
View File
@@ -66,22 +66,6 @@
<string>Imporieren</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ und %1$d weiteres Profil teilten</string>
<key>other</key>
<string>%2$@ und %1$d weitere teilten</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -191,7 +175,7 @@
<key>one</key>
<string>%2$@ und %1$d weiteres Profil teilten eine Notiz, in der du markiert warst</string>
<key>other</key>
<string>%2$@ und %1$d weitere teilten eine Notiz, in der du markiert wurdest</string>
<string>%2$@ und %1$d weitere teiten eine Notiz, in der du markiert warst</string>
</dict>
</dict>
<key>reposted_your_note_3</key>
-16
View File
@@ -66,22 +66,6 @@
<string>Imports</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ and %1$d other reposted</string>
<key>other</key>
<string>%2$@ and %1$d others reposted</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
File diff suppressed because it is too large Load Diff
@@ -58,9 +58,6 @@
"%@ replied to your note" : {
"comment" : "Heading for local notification indicating a new reply"
},
"%@ reposted" : {
"comment" : "Text indicating that the note was reposted (i.e. re-shared)."
},
"%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." : {
"comment" : "Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string."
},
@@ -105,15 +102,9 @@
"Accessibility" : {
"comment" : "Section header for accessibility settings"
},
"Account" : {
"comment" : "Label for the user account information with the Nostr Wallet Connect wallet provider."
},
"Account creation" : {
"comment" : "Label for Purple account creation date"
},
"Account details" : {
"comment" : "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet."
},
"Account private key" : {
"comment" : "Accessibility label for the private key input field"
},
@@ -156,9 +147,6 @@
"ADMIN" : {
"comment" : "Text label indicating the profile picture underneath it is the admin of the Nostr relay."
},
"Advice" : {
"comment" : "Heading for some advice text to help the user with an error"
},
"All" : {
"comment" : "Human-readable short description of the 'friends filter' when it is set to 'all'\nLabel for filter for all notifications."
},
@@ -216,9 +204,15 @@
}
}
},
"Are you lost?" : {
"comment" : "Text asking the user if they are lost in the app."
},
"Are you sure you want to clear the cache? This will free space, but images may take longer to load again." : {
"comment" : "Message explaining what it means to clear the cache, asking if user wants to proceed."
},
"Are you sure you want to connect this wallet?" : {
"comment" : "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet."
},
"Are you sure you want to delete all of your bookmarks?" : {
"comment" : "Alert for deleting all of the bookmarks."
},
@@ -234,15 +228,9 @@
"As part of your Damus Purple membership, you get complimentary and automated translations. Would you like to enable Damus Purple translations?\n\nTip: You can always change this later in Settings → Translations" : {
"comment" : "Message notifying the user that they get auto-translations as part of their service"
},
"Attach to any third party provider you already use." : {
"comment" : "Information text guiding users on attaching existing provider."
},
"Authenticated" : {
"comment" : "Label to display that authentication to a server has succeeded."
},
"AUTOMATIC SETUP" : {
"comment" : "Heading for the section that performs an automatic wallet connection setup."
},
"Automatic translations" : {
"comment" : "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple"
},
@@ -282,9 +270,6 @@
"Camera's permission was denied. You can change this in iOS settings." : {
"comment" : "Camera's permission denied error label"
},
"Cant display note" : {
"comment" : "User-visible heading for an error message indicating a note has an unknown kind or is unsupported for viewing."
},
"Cancel" : {
"comment" : "Alert button to cancel out of alert for muting a user.\nButton to cancel a repost.\nButton to cancel any interaction with the QRCode link.\nButton to cancel out of alert that creates a new mutelist.\nButton to cancel out of posting a note.\nButton to cancel out of search text entry mode.\nButton to cancel the upload.\nCancel button text for dismissing profile status settings view.\nCancel button text for dismissing updating image url.\nCancel deleting bookmarks.\nCancel deleting the user.\nCancel out of logging out the user.\nCancel out of search view.\nCancel resetting the contact list.\nText for button to cancel out of connecting Nostr Wallet Connect lightning wallet."
},
@@ -297,9 +282,6 @@
"Choose from Library" : {
"comment" : "Option to select photo from library"
},
"Choose the third-party payment provider you'd like to use." : {
"comment" : "The description for one of the \"Why add Zaps?\" boxes"
},
"Clear All" : {
"comment" : "Button for clearing bookmarks data."
},
@@ -348,21 +330,12 @@
"Contact list has been reset" : {
"comment" : "Message indicating that the contact list was successfully reset."
},
"Contact support via DMs" : {
"comment" : "Button label to contact support from an error screen"
},
"Contact support via email at [support@damus.io](mailto:support@damus.io)" : {
"comment" : "Text to contact support via email"
},
"Content filters" : {
"comment" : "Section title for content filtering/moderation configuration."
},
"Continue" : {
"comment" : "Button to dismiss suggested users view and continue to the main app\nContinue with bookmarks.\nContinue with deleting the user.\nContinue with resetting the contact list.\nPrompt to user to continue"
},
"Conversations" : {
"comment" : "Label for filter for seeing notes and replies that involve conversations between the signed in user and the current profile."
},
"Copied" : {
"comment" : "Label indicating that a user's key was copied."
},
@@ -414,9 +387,6 @@
"Could not find user to mute..." : {
"comment" : "Alert message to indicate that the muted user could not be found."
},
"Could not parse the URL you are trying to open." : {
"comment" : "User visible error description"
},
"Create account" : {
"comment" : "Button to navigate to create account view."
},
@@ -426,12 +396,6 @@
"Create new mutelist" : {
"comment" : "Title of alert prompting the user to create a new mutelist."
},
"Create new wallet" : {
"comment" : "Button text for creating a new wallet."
},
"Current balance" : {
"comment" : "Label for displaying current wallet balance"
},
"Custom" : {
"comment" : "Dropdown option for selecting a custom translation server."
},
@@ -450,6 +414,9 @@
"Damus Purple environment" : {
"comment" : "Prompt selection of the Damus purple environment (Developer feature to switch between real/production mode to test modes)."
},
"Damus Wallet" : {
"comment" : "Title text for Damus Wallet view."
},
"DeepL (Proprietary, Higher Accuracy)" : {
"comment" : "Dropdown option for selecting DeepL as the translation service."
},
@@ -504,9 +471,6 @@
"Earn Money" : {
"comment" : "Heading indicating that this application allows users to earn money."
},
"Easily create a new wallet and attach it to your account." : {
"comment" : "Description for the create new wallet feature."
},
"Edit" : {
"comment" : "Button to edit user's profile.\nButton to enter edit mode for modifying the list of relays.\nEdit Button for editing profile"
},
@@ -594,9 +558,6 @@
"Failed to parse" : {
"comment" : "NostrScript error message when it fails to parse a script."
},
"Find a Wallet" : {
"comment" : "The heading for one of the \"Why add Zaps?\" boxes"
},
"First Aid" : {
"comment" : "Navigation title for first aid settings and tools\nSection header for first aid tools and settings"
},
@@ -688,15 +649,6 @@
"Get API Key with BTC/Lightning" : {
"comment" : "Button to navigate to nokyctranslate website to get a translation API key.\nButton to navigate to translate.nostr.wine to get a translation API key."
},
"Get cash instantly from your followers" : {
"comment" : "Feature description for receiving money instantly."
},
"Get paid for being you" : {
"comment" : "Description for monetizing one's presence."
},
"Give thanks" : {
"comment" : "Heading explaining a benefit of connecting a lightning wallet."
},
"Go to the app" : {
"comment" : "Button label giving the user the option to go to the app after sharing content"
},
@@ -790,18 +742,12 @@
"Keys" : {
"comment" : "Navigation title for managing keys.\nSettings section for managing keys"
},
"Latest transactions" : {
"comment" : "Heading for latest wallet transactions list"
},
"Learn more about the features" : {
"comment" : "Label for a link to the Damus website, to allow the user to learn more about the features of Purple"
},
"Left Handed" : {
"comment" : "Moves the post button to the left side of the screen"
},
"Let your fans show their support" : {
"comment" : "Heading pointing out a benefit of connecting a lightning wallet."
},
"LibreTranslate (Open Source)" : {
"comment" : "Dropdown option for selecting LibreTranslate as the translation service."
},
@@ -817,21 +763,12 @@
"Likes" : {
"comment" : "Setting to enable Like Local Notification"
},
"Link to services that support Nostr Wallet Connect like Alby, Coinos and more." : {
"comment" : "The description for one of the \"Why add Zaps?\" boxes"
},
"Link your account" : {
"comment" : "The heading for one of the \"Why add Zaps?\" boxes"
},
"LIVE" : {
"comment" : "Text indicator that the video is a livestream."
},
"Load media" : {
"comment" : "Button to show media in note."
},
"Loading thread" : {
"comment" : "Accessibility label for the thread view when it is loading"
},
"Local" : {
"comment" : "Option for notification mode setting: Local notification mode"
},
@@ -865,12 +802,6 @@
"Manage subscription" : {
"comment" : "Button to take user to manage Damus Purple subscription"
},
"MANUAL SETUP" : {
"comment" : "Label for manual wallet setup."
},
"Maybe later" : {
"comment" : "Text for button to disconnect from Nostr Wallet Connect lightning wallet."
},
"Media previews" : {
"comment" : "Setting to show media"
},
@@ -901,11 +832,11 @@
"Mute Hashtag" : {
"comment" : "Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore."
},
"Mute/Block user" : {
"comment" : "Context menu option for muting/blocking users."
"Mute user" : {
"comment" : "Context menu option for muting users."
},
"Mute/Block User" : {
"comment" : "Title of alert for muting/blocking a user."
"Mute User" : {
"comment" : "Title of alert for muting a user."
},
"Muted" : {
"comment" : "Navigation title of view to see list of muted users & phrases.\nSidebar menu label for muted users view."
@@ -964,9 +895,6 @@
"No results" : {
"comment" : "A label indicating that note search resulted in no results"
},
"No transactions yet" : {
"comment" : "Message shown when no transactions are available"
},
"No zaps will be sent, only a lightning payment." : {
"comment" : "Description of non-zap type where sats are sent to the user's wallet as a regular Lightning payment, not as a zap."
},
@@ -1012,9 +940,6 @@
"Note from a %@ you've muted" : {
"comment" : "Text to indicate that what is being shown is a note which has been muted."
},
"Note not found" : {
"comment" : "Heading for the thread view in a not found error state."
},
"Note you've muted" : {
"comment" : "Label indicating note has been muted\nText to indicate that what is being shown is a note which has been muted."
},
@@ -1075,9 +1000,6 @@
"OnlyZaps mode" : {
"comment" : "Setting toggle to hide reactions."
},
"Oops!" : {
"comment" : "Heading for an error screen"
},
"Open in browser" : {
"comment" : "Button to open the value found in browser."
},
@@ -1114,9 +1036,6 @@
"People" : {
"comment" : "Label for filter for seeing only people follows."
},
"People will be able to send you cash from your profile. No money goes to Damus." : {
"comment" : "The description for one of the \"Why add Zaps?\" boxes"
},
"Permanently Delete Account" : {
"comment" : "Alert for deleting the users account.\nSection title for deleting the user"
},
@@ -1126,18 +1045,6 @@
"Please choose relays from the list below to filter the current feed:" : {
"comment" : "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on."
},
"Please contact the person who provided the link, and ask for another link." : {
"comment" : "User-visible tip on what to do if a link contains a deprecated \"nrelay\" reference."
},
"Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue." : {
"comment" : "User-facing tips on what to do if a Purple welcome link doesn't work"
},
"Please try again, check the URL for typos, or contact support for further help." : {
"comment" : "User visible error tips"
},
"Please try opening this content on another Nostr app that supports this type of content." : {
"comment" : "User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing."
},
"Point your camera to a QR code…" : {
"comment" : "Text on QR code camera view instructing user to point to QR code"
},
@@ -1305,6 +1212,9 @@
"Repost or quote this note" : {
"comment" : "Accessibility label for repost/quote button"
},
"Reposted" : {
"comment" : "Text indicating that the note was reposted (i.e. re-shared)."
},
"Reposted by %@" : {
"comment" : "Reposted by heading in local notification"
},
@@ -1323,9 +1233,6 @@
"Retry" : {
"comment" : "Button to retry completing account creation after an error occurred."
},
"Routing" : {
"comment" : "Label indicating the routing address for Nostr Wallet Connect payments. In other words, the relay used by the NWC wallet provider"
},
"Run" : {
"comment" : "Button that runs a NostrScript."
},
@@ -1338,9 +1245,6 @@
"Satoshi Nakamoto" : {
"comment" : "Name of Bitcoin creator(s)."
},
"SATS" : {
"comment" : "Abbreviation for Satoshis, smallest bitcoin unit"
},
"Save" : {
"comment" : "Button for saving profile.\nButton to save key, complete account creation, and start using the app."
},
@@ -1359,9 +1263,6 @@
"Save your login info?" : {
"comment" : "Ask user if they want to save their account information."
},
"Saved" : {
"comment" : "Small label indicating that the user's draft has been saved to storage."
},
"Scan a user's pubkey" : {
"comment" : "Text to prompt scanning a QR code of a user's pubkey to open their profile."
},
@@ -1398,6 +1299,9 @@
"Secret Account Login Key" : {
"comment" : "Section title for user's secret account login key."
},
"Securely connect your Damus app to your wallet using Nostr Wallet Connect" : {
"comment" : "Text to prompt user to connect their wallet using 'Nostr Wallet Connect'."
},
"Select a Lightning wallet" : {
"comment" : "Title of section for selecting a Lightning wallet to pay a Lightning invoice."
},
@@ -1422,18 +1326,9 @@
"Service" : {
"comment" : "Prompt selection of translation service provider."
},
"Set up wallet" : {
"comment" : "Text for button to disconnect from Nostr Wallet Connect lightning wallet."
},
"Setting up Zaps lets people know you're ready to start receiving money." : {
"comment" : "Information about enabling payments."
},
"Settings" : {
"comment" : "Navigation title for Settings view.\nSidebar menu label for accessing the app settings"
},
"Setup Wallet" : {
"comment" : "Heading for Nostr Wallet Connect setup screen\nHeading for wallet setup confirmation screen"
},
"Share" : {
"comment" : "Button to share a note\nButton to share an image.\nButton to share the link to a profile.\nSave button text for saving profile status settings."
},
@@ -1473,6 +1368,9 @@
"Show only from users you follow" : {
"comment" : "Setting to Show notifications only associated to users your follow"
},
"Show only preferred languages on Universe feed" : {
"comment" : "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."
},
"Show profile action sheets" : {
"comment" : "Setting to show profile action sheets when clicking on a user's profile picture"
},
@@ -1530,9 +1428,6 @@
"Staging (for dev builds)" : {
"comment" : "Label indicating the staging environment for Push notification functionality"
},
"Start receiving money" : {
"comment" : "The heading for one of the \"Why add Zaps?\" boxes"
},
"Staying humble..." : {
"comment" : "Placeholder as an example of what the user could set as their profile status."
},
@@ -1623,6 +1518,9 @@
"This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?" : {
"comment" : "Comment explaining why a user cannot be zapped."
},
"Thread" : {
"comment" : "Navigation bar title for note thread."
},
"Threads" : {
"comment" : "Section header title for a list of threads that are muted."
},
@@ -1662,9 +1560,6 @@
"Truncate timeline text" : {
"comment" : "Setting to truncate text in timeline"
},
"Try checking the link again, your internet connection, or contact the person who provided you the link for help." : {
"comment" : "Tips on what to do if a note cannot be found."
},
"Type %@ to delete" : {
"comment" : "Text field prompt asking user to type DELETE in all caps to confirm that they want to proceed with deleting their account."
},
@@ -1674,9 +1569,6 @@
"Unable to find a QR Code" : {
"comment" : "Alert message letting user know a QR Code was not found."
},
"Undistract mode" : {
"comment" : "Developer mode setting to scramble text and images to avoid distractions during development."
},
"Unfollow" : {
"comment" : "Button to unfollow a user."
},
@@ -1689,9 +1581,6 @@
"Universe 🛸" : {
"comment" : "Toolbar label for the universal view where notes from all connected relay servers appear."
},
"Unknown" : {
"comment" : "A name label for an unknown user"
},
"Unmute" : {
"comment" : "Button to unmute a profile."
},
@@ -1713,9 +1602,6 @@
"URL" : {
"comment" : "Custom URL host for Damus Purple testing\nCustom URL host for Damus push notification testing\nExample URL to LibreTranslate server"
},
"Use existing" : {
"comment" : "Button text to use an existing wallet."
},
"User has been muted" : {
"comment" : "Alert message that informs a user was muted."
},
@@ -1758,9 +1644,18 @@
"Visit the Damus website on a web browser to manage billing" : {
"comment" : "Instruction on how to manage billing externally"
},
"Wake up, %@" : {
"comment" : "Text telling the user to wake up, where the argument is their display name."
},
"Wallet" : {
"comment" : "Navigation title for Wallet view\nNavigation title for attaching Nostr Wallet Connect lightning wallet.\nSidebar menu label for Wallet view.\nTitle for section in zap settings that controls the Lightning wallet selection."
},
"Wallet Address" : {
"comment" : "Label text indicating that below it is the wallet address."
},
"Wallet Relay" : {
"comment" : "Label text indicating that below it is the information about the wallet relay."
},
"WARNING:\n\nThis will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY." : {
"comment" : "Alert for resetting the user's contact list."
},
@@ -1770,12 +1665,6 @@
"We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)" : {
"comment" : "Message indicating that no First Aid actions are available."
},
"We do not yet support viewing this type of content." : {
"comment" : "User-visible description of an error indicating a note has an unknown kind or is unsupported for viewing."
},
"We were unable to find the note you were looking for." : {
"comment" : "Text for the thread view when it is unable to find the note the user is looking for"
},
"We'll save your account key, so you won't need to enter it manually next time you log in." : {
"comment" : "Reminder to user that they should save their account information."
},
@@ -1803,15 +1692,9 @@
"What do you want to report?" : {
"comment" : "Header text to prompt user what issue they want to report."
},
"When supporters tip with Zaps, they can add a note and we can make it easy for you to instantly reply to show your gratitude." : {
"comment" : "Description explaining a benefit of connecting a lightning wallet."
},
"Who to Follow" : {
"comment" : "Title for a screen displaying suggestions of who to follow"
},
"Why add Zaps?" : {
"comment" : "Heading to explain the benefits of zaps."
},
"Words" : {
"comment" : "Section header title for a list of words that are muted."
},
@@ -1827,33 +1710,21 @@
"you" : {
"comment" : "You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself."
},
"You are dreaming..." : {
"comment" : "Text telling the user that they are dreaming."
},
"You cannot share content because you are not logged in. Please close this view, log in to your account, and try again." : {
"comment" : "Label explaining that sharing cannot proceed because the user is not logged in."
},
"You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug." : {
"comment" : "Error label upon continuing in the app from a Damus Purple purchase"
},
"You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug." : {
"comment" : "Error label upon continuing in the app from a Damus Purple purchase"
},
"You drive the conversation and we want to make it easier for people to support your work beyond follows, reposts, and likes." : {
"comment" : "Text explaining the benefit of connecting a lightning wallet for content creators."
},
"You have no bookmarks yet, add them in the context menu" : {
"comment" : "Text indicating that there are no bookmarks to be viewed"
},
"You opened an invalid link. The link you tried to open refers to \"nrelay\", which has been deprecated and is not supported." : {
"comment" : "User-visible error description for a user who tries to open a deprecated \"nrelay\" link."
},
"You unlocked" : {
"comment" : "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple"
},
"Your content is being broadcasted to the network. Please wait." : {
"comment" : "Label explaining that their content sharing action is in progress"
},
"Your draft has been saved to storage." : {
"comment" : "Accessibility label indicating that a user's post draft has been saved, meant to be read by screen reading technology."
},
"Your Name" : {
"comment" : "Label for Your Name section of user profile form."
},
@@ -1901,9 +1772,6 @@
},
"Zaps" : {
"comment" : "Label for filter for zap notifications.\nNavigation bar title for the Zaps view.\nNavigation title for zap settings.\nSection header for zap settings\nSetting to enable Zap Local Notification\nTitle for section in zap settings that controls general zap preferences."
},
"Zaps are an easy way to support the incredible\nvoices that make up the conversation on nostr.\nHere's how it works" : {
"comment" : "Describing the functional benefits of Zaps."
}
},
"version" : "1.0"
@@ -9,6 +9,6 @@
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "Local authentication to access private key";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network";
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone for creating video recording posts";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Granting Damus access to your photos allows you to save images.";
@@ -66,22 +66,6 @@
<string>Imports</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ and %1$d other reposted</string>
<key>other</key>
<string>%2$@ and %1$d others reposted</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
-16
View File
@@ -66,22 +66,6 @@
<string>Importok</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ és %1$d egyéb újraosztva</string>
<key>other</key>
<string>%2$@ és %1$d egyebek újraosztva</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
-14
View File
@@ -58,20 +58,6 @@
<string>インポート</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@と他%1$d人がリポストしました</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
-16
View File
@@ -66,22 +66,6 @@
<string>Importeringen</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ en %1$d ander hebben herplaatst</string>
<key>other</key>
<string>%2$@ en %1$d anderen hebben herplaatst</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More