Compare commits
86 Commits
create-pos
...
hide-hellt
| Author | SHA1 | Date | |
|---|---|---|---|
|
dddb95e0b3
|
|||
|
bf333e55b8
|
|||
|
|
9510290c29 | ||
|
|
3b1238b9c7 | ||
|
|
3bec23ecac | ||
|
|
7b678228b6 | ||
|
|
b1292d4562 | ||
|
|
a62d782fe5 | ||
|
|
81b07eb339 | ||
|
|
02f88398b9 | ||
|
|
e80961cc09 | ||
|
|
bd7721dc26 | ||
|
|
6d974bf71c | ||
|
|
aeeb817735 | ||
|
|
b5e7033958 | ||
|
|
bdc843f30f | ||
|
|
a823fa8e14 | ||
|
|
9232386c15 | ||
|
|
3c1547718c | ||
|
|
b67a7f3e9e | ||
|
|
92850d4f64 | ||
|
|
6323eafd7e | ||
|
|
b8fe826b58 | ||
|
|
841c49238f | ||
|
|
7ab612e3d9 | ||
|
|
6d8a27688f | ||
|
|
765385319a | ||
|
|
342c49a3e5 | ||
|
|
25860e7bb2 | ||
|
|
6962f2b462 | ||
|
e48ce4c6c5
|
|||
|
1cb311cc2c
|
|||
|
|
401846abe4
|
||
|
|
16ef393350
|
||
|
|
d5742f8e4c
|
||
|
|
319063f823
|
||
|
|
5b13cf5634
|
||
|
|
da10b908b3
|
||
|
|
4568935bc5
|
||
|
|
467404a55e
|
||
|
|
fcfe1e4558
|
||
|
|
c9d87a1b9a
|
||
|
|
35ebf4dfc2
|
||
|
|
bc3c256d22
|
||
|
|
0e10e74496
|
||
|
|
ebe9097f73
|
||
|
|
d61a11b647
|
||
|
|
d980cc1f8e
|
||
|
|
bd6056ce2e
|
||
|
|
cf48fda8d0
|
||
|
|
dc344cd28c
|
||
|
|
358610575f
|
||
|
|
a7869fccbb
|
||
|
|
a50903f90a
|
||
|
|
9243705995
|
||
|
|
db4dd9eee9
|
||
|
|
22f2aba969 | ||
|
|
98f2777fda | ||
|
|
102ce43216 | ||
|
|
0c148c8a1f | ||
|
|
3cccb2eb6b | ||
|
|
af4949e26a | ||
|
|
5bb7e95624 | ||
| 814bcf694f | |||
|
|
b0382c61b1 | ||
| e2650a8bfc | |||
|
|
ac39a53b33 | ||
|
|
fb356cdf0b | ||
|
|
238e89ce16 | ||
|
|
6e041c79f7 | ||
|
|
6ef4b60d14 | ||
|
|
054bec2d9a
|
||
|
|
943a46a343
|
||
|
|
17381f6b94
|
||
|
|
18c88de407
|
||
|
|
99d21fc89b
|
||
|
|
db5c86a0d1
|
||
|
|
736ec6fb9e
|
||
|
|
fa2327325a
|
||
|
|
4fdf048040
|
||
|
|
273538bd36
|
||
|
|
0980c8c040
|
||
|
|
f0bfdeaa5a
|
||
|
|
ab7c5c18e3
|
||
|
|
6ae95ab5ec
|
||
|
eec630b2b0
|
59
CHANGELOG.md
59
CHANGELOG.md
@@ -1,3 +1,62 @@
|
||||
## [1.13.1] - 2025-03-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where threads would not load properly (Daniel D’Aquino)
|
||||
|
||||
|
||||
[1.13.1]: https://github.com/damus-io/damus/releases/tag/v1.13.1
|
||||
|
||||
|
||||
## [1.13] - 2025-03-14
|
||||
|
||||
### Added
|
||||
|
||||
- Added local persistence of note drafts (Daniel D’Aquino)
|
||||
- Added user-friendly error view for errors around the app that would not fit in other places (Daniel D’Aquino)
|
||||
- Coinos connection button in Wallet view (ericholguin)
|
||||
- Added Alby Go to mobile wallets selection menu (Tomek ⚡ K)
|
||||
- Minor accessibility improvements around picture editing and onboarding (Daniel D’Aquino)
|
||||
- Profile image cropping tools (Daniel D’Aquino)
|
||||
- Added Conversations tab to profiles (Terry Yiu)
|
||||
- Added profile pictures to push notifications (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Don't show reposts for the same note more than once in your home feed (William Casarin)
|
||||
- Improved profile image bandwidth optimization (Daniel D’Aquino)
|
||||
- Improved reliability of picture selector (Daniel D’Aquino)
|
||||
- Changed spaces to newlines in new posts to provide cleaner separation between text, uploaded media, and quoted notes (Terry Yiu)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where some push notifications would not open in the app and leave users confused (Daniel D’Aquino)
|
||||
- Fixed issue where app would need a restart for new NWC wallets to work (Daniel D’Aquino)
|
||||
- Fixed overly sensitive horizontal swipe on thread chat view (Daniel D’Aquino)
|
||||
- Trim whitespaces from Lightning addresses (Terry Yiu)
|
||||
- Fixed translation export script by upgrading nostr-sdk-swift dependency to support Mac Catalyst (Terry Yiu)
|
||||
- Fixed issue where users continue to receive push notifications after logout (Daniel D’Aquino)
|
||||
- Fixed an issue where events on a thread view would occasionally disappear (Daniel D’Aquino)
|
||||
- Improved robustness of the URL handler (Daniel D’Aquino)
|
||||
- Translate notes even if they are in a preferred language but not the current language as that is what users expect (Terry Yiu)
|
||||
- Cancel ongoing uploading operations after the user cancels the post (Swift Coder)
|
||||
- Fixed link and photo sharing support on macOS (Swift Coder)
|
||||
- Fix bug where profile view was showing more than just the notes and replies on the notes / notes & replies tabs (Terry Yiu)
|
||||
- Fixed reposts banner to be localizable (Terry Yiu)
|
||||
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed language filtering from Universe feed because language detection can be inaccurate (Terry Yiu)
|
||||
- Removed mystery tabs meant to fix tab switching bug that no longer exists (Terry Yiu)
|
||||
|
||||
|
||||
|
||||
[1.13](https://github.com/damus-io/damus/releases/tag/v1.13): https://github.com/damus-io/damus/releases/tag/v1.13
|
||||
|
||||
|
||||
## [1.12.3] - 2025-02-06
|
||||
|
||||
### Added
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!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>
|
||||
|
||||
@@ -5,15 +5,32 @@
|
||||
// Created by Daniel D’Aquino 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,
|
||||
@@ -40,9 +57,16 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
// 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
|
||||
@@ -56,7 +80,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.
|
||||
@@ -65,7 +89,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.
|
||||
@@ -74,15 +98,58 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
Task {
|
||||
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: name, notify: notification_object, state: state) else {
|
||||
let sender_dn = DisplayName(name: sender_profile.name, display_name: sender_profile.display_name, pubkey: sender_pubkey)
|
||||
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: sender_dn.displayName, notify: notification_object, state: state) else {
|
||||
|
||||
Log.debug("NotificationFormatter.format_message failed", for: .push_notifications)
|
||||
return
|
||||
}
|
||||
|
||||
contentHandler(improvedContent)
|
||||
do {
|
||||
var options: [AnyHashable: Any] = [:]
|
||||
if let imageSource = CGImageSourceCreateWithURL(sender_profile.picture as CFURL, nil),
|
||||
let uti = CGImageSourceGetType(imageSource) {
|
||||
options[UNNotificationAttachmentOptionsTypeHintKey] = uti
|
||||
}
|
||||
|
||||
let attachment = try UNNotificationAttachment(identifier: sender_profile.picture.absoluteString, url: sender_profile.picture, options: options)
|
||||
improvedContent.attachments = [attachment]
|
||||
} catch {
|
||||
Log.error("failed to get notification attachment: %s", for: .push_notifications, error.localizedDescription)
|
||||
}
|
||||
|
||||
let kind = nostr_event.known_kind
|
||||
|
||||
// these aren't supported yet
|
||||
if !(kind == .text || kind == .dm) {
|
||||
contentHandler(improvedContent)
|
||||
return
|
||||
}
|
||||
|
||||
// rich communication notifications for kind1, dms, etc
|
||||
|
||||
let message_intent = await message_intent_from_note(ndb: state.ndb,
|
||||
sender_profile: sender_profile,
|
||||
content: improvedContent.body,
|
||||
note: nostr_event,
|
||||
our_pubkey: state.keypair.pubkey)
|
||||
|
||||
improvedContent.threadIdentifier = nostr_event.thread_id().hex()
|
||||
improvedContent.categoryIdentifier = "COMMUNICATION"
|
||||
|
||||
let interaction = INInteraction(intent: message_intent, response: nil)
|
||||
interaction.direction = .incoming
|
||||
do {
|
||||
try await interaction.donate()
|
||||
let updated = try improvedContent.updating(from: message_intent)
|
||||
contentHandler(updated)
|
||||
} catch {
|
||||
Log.error("failed to donate interaction: %s", for: .push_notifications, error.localizedDescription)
|
||||
contentHandler(improvedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,3 +162,162 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ProfileBuf {
|
||||
let picture: URL
|
||||
let name: String?
|
||||
let display_name: String?
|
||||
let nip05: String?
|
||||
}
|
||||
|
||||
func message_intent_from_note(ndb: Ndb, sender_profile: ProfileBuf, content: String, note: NdbNote, our_pubkey: Pubkey) async -> INSendMessageIntent {
|
||||
let sender_pk = note.pubkey
|
||||
let sender = await profile_to_inperson(name: sender_profile.name,
|
||||
display_name: sender_profile.display_name,
|
||||
picture: sender_profile.picture.absoluteString,
|
||||
nip05: sender_profile.nip05,
|
||||
pubkey: sender_pk,
|
||||
our_pubkey: our_pubkey)
|
||||
|
||||
let conversationIdentifier = note.thread_id().hex()
|
||||
var recipients: [INPerson] = []
|
||||
var pks: [Pubkey] = []
|
||||
let meta = INSendMessageIntentDonationMetadata()
|
||||
|
||||
// gather recipients
|
||||
if let recipient_note_id = note.direct_replies() {
|
||||
let replying_to = ndb.lookup_note(recipient_note_id)
|
||||
if let replying_to_pk = replying_to?.unsafeUnownedValue?.pubkey {
|
||||
meta.isReplyToCurrentUser = replying_to_pk == our_pubkey
|
||||
|
||||
if replying_to_pk != sender_pk {
|
||||
// we push the actual person being replied to first
|
||||
pks.append(replying_to_pk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pubkeys = Array(note.referenced_pubkeys)
|
||||
meta.recipientCount = pubkeys.count
|
||||
if pubkeys.contains(sender_pk) {
|
||||
meta.recipientCount -= 1
|
||||
}
|
||||
|
||||
for pk in pubkeys.prefix(3) {
|
||||
if pk == sender_pk || pks.contains(pk) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !meta.isReplyToCurrentUser && pk == our_pubkey {
|
||||
meta.mentionsCurrentUser = true
|
||||
}
|
||||
|
||||
pks.append(pk)
|
||||
}
|
||||
|
||||
for pk in pks {
|
||||
let recipient = await pubkey_to_inperson(ndb: ndb, pubkey: pk, our_pubkey: our_pubkey)
|
||||
recipients.append(recipient)
|
||||
}
|
||||
|
||||
// we enable default formatting this way
|
||||
var groupName = INSpeakableString(spokenPhrase: "")
|
||||
|
||||
// otherwise we just say its a DM
|
||||
if note.known_kind == .dm {
|
||||
groupName = INSpeakableString(spokenPhrase: "DM")
|
||||
}
|
||||
|
||||
let intent = INSendMessageIntent(recipients: recipients,
|
||||
outgoingMessageType: .outgoingMessageText,
|
||||
content: content,
|
||||
speakableGroupName: groupName,
|
||||
conversationIdentifier: conversationIdentifier,
|
||||
serviceName: "kind\(note.kind)",
|
||||
sender: sender,
|
||||
attachments: nil)
|
||||
intent.donationMetadata = meta
|
||||
|
||||
// this is needed for recipients > 0
|
||||
if let img = sender.image {
|
||||
intent.setImage(img, forParameterNamed: \.speakableGroupName)
|
||||
}
|
||||
|
||||
return intent
|
||||
}
|
||||
|
||||
func pubkey_to_inperson(ndb: Ndb, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
|
||||
let profile_txn = ndb.lookup_profile(pubkey)
|
||||
let profile = profile_txn?.unsafeUnownedValue?.profile
|
||||
let name = profile?.name
|
||||
let display_name = profile?.display_name
|
||||
let nip05 = profile?.nip05
|
||||
let picture = profile?.picture
|
||||
|
||||
return await profile_to_inperson(name: name,
|
||||
display_name: display_name,
|
||||
picture: picture,
|
||||
nip05: nip05,
|
||||
pubkey: pubkey,
|
||||
our_pubkey: our_pubkey)
|
||||
}
|
||||
|
||||
func fetch_pfp(picture: URL) async throws -> RetrieveImageResult {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: picture)) { result in
|
||||
switch result {
|
||||
case .success(let img):
|
||||
continuation.resume(returning: img)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func profile_to_inperson(name: String?, display_name: String?, picture: String?, nip05: String?, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
|
||||
let npub = pubkey.npub
|
||||
let handle = INPersonHandle(value: npub, type: .unknown)
|
||||
var aliases: [INPersonHandle] = []
|
||||
|
||||
if let nip05 {
|
||||
aliases.append(INPersonHandle(value: nip05, type: .emailAddress))
|
||||
}
|
||||
|
||||
let nostrName = DisplayName(name: name, display_name: display_name, pubkey: pubkey)
|
||||
let nameComponents = nostrName.nameComponents()
|
||||
let displayName = nostrName.displayName
|
||||
let contactIdentifier = npub
|
||||
let customIdentifier = npub
|
||||
let suggestionType = INPersonSuggestionType.socialProfile
|
||||
|
||||
var image: INImage? = nil
|
||||
|
||||
if let picture,
|
||||
let url = URL(string: picture),
|
||||
let img = try? await fetch_pfp(picture: url),
|
||||
let imgdata = img.data()
|
||||
{
|
||||
image = INImage(imageData: imgdata)
|
||||
} else {
|
||||
Log.error("Failed to fetch pfp (%s) for %s", for: .push_notifications, picture ?? "nil", displayName)
|
||||
}
|
||||
|
||||
let person = INPerson(personHandle: handle,
|
||||
nameComponents: nameComponents,
|
||||
displayName: displayName,
|
||||
image: image,
|
||||
contactIdentifier: contactIdentifier,
|
||||
customIdentifier: customIdentifier,
|
||||
isMe: pubkey == our_pubkey,
|
||||
suggestionType: suggestionType
|
||||
)
|
||||
|
||||
return person
|
||||
}
|
||||
|
||||
func robohash(_ pk: Pubkey) -> String {
|
||||
return "https://robohash.org/" + pk.hex()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
|
||||
]
|
||||
// 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"),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
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 */; };
|
||||
@@ -206,7 +207,7 @@
|
||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
|
||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
|
||||
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A542A7E91FE005E6031 /* LongPostTests.swift */; };
|
||||
4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A542A7E91FE005E6031 /* LargeEventTests.swift */; };
|
||||
4C684A572A7FFAE6005E6031 /* UrlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A562A7FFAE6005E6031 /* UrlTests.swift */; };
|
||||
4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; };
|
||||
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */; };
|
||||
@@ -407,10 +408,22 @@
|
||||
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 */; };
|
||||
@@ -1084,6 +1097,9 @@
|
||||
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 */; };
|
||||
@@ -1480,6 +1496,18 @@
|
||||
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 */; };
|
||||
@@ -2148,7 +2176,7 @@
|
||||
4C64305B2A945AFF00B0C0E9 /* MusicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicController.swift; sourceTree = "<group>"; };
|
||||
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
|
||||
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
|
||||
4C684A542A7E91FE005E6031 /* LongPostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPostTests.swift; sourceTree = "<group>"; };
|
||||
4C684A542A7E91FE005E6031 /* LargeEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeEventTests.swift; sourceTree = "<group>"; };
|
||||
4C684A562A7FFAE6005E6031 /* UrlTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlTests.swift; sourceTree = "<group>"; };
|
||||
4C687C202A5F7ED00092C550 /* DamusBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusBackground.swift; sourceTree = "<group>"; };
|
||||
4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeaderView.swift; sourceTree = "<group>"; };
|
||||
@@ -2367,8 +2395,12 @@
|
||||
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>"; };
|
||||
@@ -2450,6 +2482,7 @@
|
||||
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>"; };
|
||||
@@ -2476,6 +2509,9 @@
|
||||
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>"; };
|
||||
@@ -2610,6 +2646,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C5726BA2D72C6FA00E7FF82 /* Kingfisher in Frameworks */,
|
||||
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */,
|
||||
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
|
||||
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
|
||||
@@ -3220,6 +3257,10 @@
|
||||
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 */,
|
||||
@@ -3245,11 +3286,12 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D73B74E02D8365B40067BDBC /* ExtraFonts.swift */,
|
||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */,
|
||||
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */,
|
||||
E04A37C52B544F090029650D /* URIParsing.swift */,
|
||||
4C1D4FB02A7958E60024F453 /* VersionInfo.swift */,
|
||||
4C7D09612A098D0E00943473 /* WalletConnect.swift */,
|
||||
D78F080A2D7F78B000FC6C75 /* WalletConnect */,
|
||||
4C198DF329F88D23004C165C /* Images */,
|
||||
4C198DEA29F88C6B004C165C /* BlurHash */,
|
||||
4CE4F0F329D779B5005914DB /* PostBox.swift */,
|
||||
@@ -3299,7 +3341,6 @@
|
||||
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */,
|
||||
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
|
||||
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
|
||||
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -3611,6 +3652,7 @@
|
||||
children = (
|
||||
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
|
||||
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
|
||||
D78F08152D7F7F5F00FC6C75 /* NIP04 */,
|
||||
4C45E5002BED4CE10025A428 /* NIP10 */,
|
||||
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
|
||||
4CA3529C2A76AE47003BB08B /* Notify */,
|
||||
@@ -3677,7 +3719,7 @@
|
||||
4C19AE542A5D977400C90DB7 /* HashtagTests.swift */,
|
||||
3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */,
|
||||
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */,
|
||||
4C684A542A7E91FE005E6031 /* LongPostTests.swift */,
|
||||
4C684A542A7E91FE005E6031 /* LargeEventTests.swift */,
|
||||
4C684A562A7FFAE6005E6031 /* UrlTests.swift */,
|
||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */,
|
||||
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */,
|
||||
@@ -3951,6 +3993,25 @@
|
||||
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 = (
|
||||
@@ -4174,6 +4235,7 @@
|
||||
D789D11F2AFEFBF20083A7AB /* secp256k1 */,
|
||||
D7EDED302B1290B80018B19C /* MarkdownUI */,
|
||||
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */,
|
||||
4C5726B92D72C6FA00E7FF82 /* Kingfisher */,
|
||||
);
|
||||
productName = DamusNotificationService;
|
||||
productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */;
|
||||
@@ -4401,6 +4463,7 @@
|
||||
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 */,
|
||||
@@ -4599,6 +4662,7 @@
|
||||
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 */,
|
||||
@@ -4624,6 +4688,7 @@
|
||||
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 */,
|
||||
@@ -4660,6 +4725,7 @@
|
||||
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 */,
|
||||
@@ -4709,6 +4775,8 @@
|
||||
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 */,
|
||||
@@ -4788,6 +4856,7 @@
|
||||
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 */,
|
||||
@@ -4806,6 +4875,7 @@
|
||||
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 */,
|
||||
@@ -4908,7 +4978,7 @@
|
||||
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */,
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
|
||||
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
|
||||
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */,
|
||||
4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */,
|
||||
E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -4950,6 +5020,7 @@
|
||||
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 */,
|
||||
@@ -4986,6 +5057,7 @@
|
||||
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 */,
|
||||
@@ -5018,6 +5090,7 @@
|
||||
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 */,
|
||||
@@ -5025,6 +5098,7 @@
|
||||
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 */,
|
||||
@@ -5070,6 +5144,7 @@
|
||||
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 */,
|
||||
@@ -5115,6 +5190,7 @@
|
||||
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 */,
|
||||
@@ -5344,6 +5420,7 @@
|
||||
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 */,
|
||||
@@ -5369,6 +5446,7 @@
|
||||
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 */,
|
||||
@@ -5520,6 +5598,7 @@
|
||||
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 */,
|
||||
@@ -5534,6 +5613,7 @@
|
||||
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 */,
|
||||
@@ -5541,6 +5621,7 @@
|
||||
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 */,
|
||||
@@ -5548,6 +5629,7 @@
|
||||
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 */,
|
||||
@@ -5623,10 +5705,12 @@
|
||||
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 */,
|
||||
@@ -5681,6 +5765,7 @@
|
||||
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 */,
|
||||
@@ -5770,6 +5855,7 @@
|
||||
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 */,
|
||||
@@ -5982,6 +6068,7 @@
|
||||
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 */,
|
||||
@@ -5991,9 +6078,11 @@
|
||||
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 */,
|
||||
@@ -6225,7 +6314,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -6248,7 +6337,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||
MARKETING_VERSION = 1.10;
|
||||
MARKETING_VERSION = 1.14;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -6294,7 +6383,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -6313,7 +6402,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||
MARKETING_VERSION = 1.10;
|
||||
MARKETING_VERSION = 1.14;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -6332,8 +6421,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;
|
||||
@@ -6360,9 +6449,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;
|
||||
@@ -6384,8 +6473,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;
|
||||
@@ -6412,9 +6501,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;
|
||||
@@ -6500,7 +6589,6 @@
|
||||
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;
|
||||
@@ -6519,7 +6607,6 @@
|
||||
"@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;
|
||||
@@ -6538,7 +6625,6 @@
|
||||
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;
|
||||
@@ -6553,7 +6639,6 @@
|
||||
"@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;
|
||||
@@ -6572,7 +6657,6 @@
|
||||
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;
|
||||
@@ -6587,7 +6671,6 @@
|
||||
"@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;
|
||||
@@ -6607,7 +6690,6 @@
|
||||
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;
|
||||
@@ -6622,7 +6704,6 @@
|
||||
"@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;
|
||||
@@ -6641,7 +6722,6 @@
|
||||
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;
|
||||
@@ -6656,7 +6736,6 @@
|
||||
"@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;
|
||||
@@ -6676,7 +6755,6 @@
|
||||
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;
|
||||
@@ -6691,7 +6769,6 @@
|
||||
"@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;
|
||||
@@ -6870,6 +6947,11 @@
|
||||
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" */;
|
||||
|
||||
@@ -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 send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
|
||||
await WalletConnect.send_donation_zap(pool: damus_state.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 = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
|
||||
let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.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)")
|
||||
|
||||
@@ -367,7 +367,9 @@ 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
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<!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>
|
||||
|
||||
@@ -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 = parse_display_name(profile: profile, pubkey: pubkey).username
|
||||
let profile_name = DisplayName(profile: profile, pubkey: pubkey).username
|
||||
guard let url_address = URL(string: block.asString) else {
|
||||
rich_text_content.append(.init(string: block.asString))
|
||||
continue
|
||||
|
||||
@@ -260,7 +260,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 FullWalletResponse(from: ev, nwc: nwc) else {
|
||||
let resp = await WalletConnect.FullWalletResponse(from: ev, nwc: nwc) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -274,12 +274,24 @@ class HomeModel: ContactsDelegate {
|
||||
|
||||
guard resp.response.error == nil else {
|
||||
print("nwc error: \(resp.response)")
|
||||
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||
WalletConnect.handle_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||
return
|
||||
}
|
||||
|
||||
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)]")
|
||||
nwc_success(state: self.damus_state, resp: resp)
|
||||
WalletConnect.handle_zap_success(state: self.damus_state, resp: resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,7 +465,7 @@ class HomeModel: ContactsDelegate {
|
||||
let nwc = WalletConnectURL(str: nwc_str),
|
||||
nwc.relay == relay_id
|
||||
{
|
||||
subscribe_to_nwc(url: nwc, pool: pool)
|
||||
WalletConnect.subscribe(url: nwc, pool: pool)
|
||||
}
|
||||
case .error(let merr):
|
||||
let desc = String(describing: merr)
|
||||
|
||||
@@ -41,6 +41,10 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
|
||||
return false
|
||||
}
|
||||
|
||||
if state.settings.hellthread_notifications_disabled && ev.is_hellthread(max_pubkeys: state.settings.hellthread_notification_max_pubkeys) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't show notifications that match mute list.
|
||||
if state.mutelist_manager.is_event_muted(ev) {
|
||||
return false
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// Minimum threshold the hellthread pubkey tag count setting can go down to.
|
||||
let HELLTHREAD_MIN_PUBKEYS: Int = 6
|
||||
|
||||
// Maximum threshold the hellthread pubkey tag count setting can go up to.
|
||||
let HELLTHREAD_MAX_PUBKEYS: Int = 24
|
||||
|
||||
struct PushNotificationClient {
|
||||
let keypair: Keypair
|
||||
let settings: UserSettingsStore
|
||||
@@ -175,15 +181,33 @@ extension PushNotificationClient {
|
||||
}
|
||||
|
||||
struct NotificationSettings: Codable, Equatable {
|
||||
let zap_notifications_enabled: Bool
|
||||
let mention_notifications_enabled: Bool
|
||||
let repost_notifications_enabled: Bool
|
||||
let reaction_notifications_enabled: Bool
|
||||
let dm_notifications_enabled: Bool
|
||||
let only_notifications_from_following_enabled: Bool
|
||||
|
||||
let zap_notifications_enabled: Bool?
|
||||
let mention_notifications_enabled: Bool?
|
||||
let repost_notifications_enabled: Bool?
|
||||
let reaction_notifications_enabled: Bool?
|
||||
let dm_notifications_enabled: Bool?
|
||||
let only_notifications_from_following_enabled: Bool?
|
||||
let hellthread_notifications_disabled: Bool?
|
||||
let hellthread_notifications_max_pubkeys: Int?
|
||||
|
||||
static func from(json_data: Data) -> Self? {
|
||||
guard let decoded = try? JSONDecoder().decode(Self.self, from: json_data) else { return nil }
|
||||
|
||||
// Normalize hellthread_notifications_max_pubkeys in case
|
||||
// it goes beyond the expected range supported on the client.
|
||||
if let max_pubkeys = decoded.hellthread_notifications_max_pubkeys, max_pubkeys < HELLTHREAD_MIN_PUBKEYS || max_pubkeys > HELLTHREAD_MAX_PUBKEYS {
|
||||
return NotificationSettings(
|
||||
zap_notifications_enabled: decoded.zap_notifications_enabled,
|
||||
mention_notifications_enabled: decoded.mention_notifications_enabled,
|
||||
repost_notifications_enabled: decoded.repost_notifications_enabled,
|
||||
reaction_notifications_enabled: decoded.reaction_notifications_enabled,
|
||||
dm_notifications_enabled: decoded.dm_notifications_enabled,
|
||||
only_notifications_from_following_enabled: decoded.only_notifications_from_following_enabled,
|
||||
hellthread_notifications_disabled: decoded.hellthread_notifications_disabled,
|
||||
hellthread_notifications_max_pubkeys: max(min(HELLTHREAD_MAX_PUBKEYS, max_pubkeys), HELLTHREAD_MIN_PUBKEYS)
|
||||
)
|
||||
}
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
@@ -194,7 +218,9 @@ extension PushNotificationClient {
|
||||
repost_notifications_enabled: settings.repost_notification,
|
||||
reaction_notifications_enabled: settings.like_notification,
|
||||
dm_notifications_enabled: settings.dm_notification,
|
||||
only_notifications_from_following_enabled: settings.notification_only_from_following
|
||||
only_notifications_from_following_enabled: settings.notification_only_from_following,
|
||||
hellthread_notifications_disabled: settings.hellthread_notifications_disabled,
|
||||
hellthread_notifications_max_pubkeys: settings.hellthread_notification_max_pubkeys
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,18 @@ class ThreadModel: ObservableObject {
|
||||
///
|
||||
/// This is a computed property because we then don't need to worry about keeping things in sync
|
||||
var parent_events: [NostrEvent] {
|
||||
return event_map.parent_events(of: selected_event)
|
||||
// This block of code helps ensure `ThreadEventMap` stays in sync with `EventCache`
|
||||
let parent_events_from_cache = damus_state.events.parent_events(event: selected_event, keypair: damus_state.keypair)
|
||||
for parent_event in parent_events_from_cache {
|
||||
add_event(
|
||||
parent_event,
|
||||
keypair: damus_state.keypair,
|
||||
look_for_parent_events: false, // We have all parents we need for now
|
||||
publish_changes: false // Publishing changes during a view render is problematic
|
||||
)
|
||||
}
|
||||
|
||||
return parent_events_from_cache
|
||||
}
|
||||
/// All of the direct and indirect replies of `selected_event` in the thread. sorted chronologically
|
||||
///
|
||||
@@ -125,7 +136,13 @@ 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)
|
||||
func add_event(_ ev: NostrEvent, keypair: Keypair) {
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - ev: The event to add into the thread event map
|
||||
/// - keypair: The user's keypair
|
||||
/// - look_for_parent_events: Whether to search for parent events of the input event in NostrDB
|
||||
/// - publish_changes: Whether to publish changes at the end
|
||||
func add_event(_ ev: NostrEvent, keypair: Keypair, look_for_parent_events: Bool = true, publish_changes: Bool = true) {
|
||||
if event_map.contains(id: ev.id) {
|
||||
return
|
||||
}
|
||||
@@ -136,8 +153,22 @@ class ThreadModel: ObservableObject {
|
||||
|
||||
event_map.add(event: ev)
|
||||
|
||||
// Publish changes
|
||||
objectWillChange.send()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles an incoming event from a relay pool
|
||||
|
||||
@@ -160,7 +160,13 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
@Setting(key: "notification_only_from_following", default_value: false)
|
||||
var notification_only_from_following: Bool
|
||||
|
||||
|
||||
@Setting(key: "hellthread_notifications_disabled", default_value: false)
|
||||
var hellthread_notifications_disabled: Bool
|
||||
|
||||
@Setting(key: "hellthread_notification_max_pubkeys", default_value: DEFAULT_HELLTHREAD_MAX_PUBKEYS)
|
||||
var hellthread_notification_max_pubkeys: Int
|
||||
|
||||
@Setting(key: "translate_dms", default_value: false)
|
||||
var translate_dms: Bool
|
||||
|
||||
|
||||
@@ -13,10 +13,17 @@ 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
|
||||
|
||||
@@ -61,4 +68,27 @@ 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
damus/NIP04/NIP04.swift
Normal file
55
damus/NIP04/NIP04.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// NIP04.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-03-10.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
/// Functions and utilities for the NIP-04 spec
|
||||
struct NIP04 {}
|
||||
|
||||
extension NIP04 {
|
||||
/// Encrypts a message using NIP-04.
|
||||
static func encrypt_message(message: String, privkey: Privkey, to_pk: Pubkey, encoding: EncEncoding = .base64) -> String? {
|
||||
let iv = random_bytes(count: 16).bytes
|
||||
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
|
||||
return nil
|
||||
}
|
||||
let utf8_message = Data(message.utf8).bytes
|
||||
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch encoding {
|
||||
case .base64:
|
||||
return encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||
case .bech32:
|
||||
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Creates an event with encrypted `contents` field, using NIP-04
|
||||
static func create_encrypted_event(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: FullKeypair, created_at: UInt32, kind: UInt32) -> NostrEvent? {
|
||||
let privkey = keypair.privkey
|
||||
|
||||
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NostrEvent(content: enc_content, keypair: keypair.to_keypair(), kind: kind, tags: tags, createdAt: created_at)
|
||||
}
|
||||
|
||||
/// Creates a NIP-04 style direct message event
|
||||
static func create_dm(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent?
|
||||
{
|
||||
let created = created_at ?? UInt32(Date().timeIntervalSince1970)
|
||||
|
||||
guard let keypair = keypair.to_full() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return create_encrypted_event(message, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created, kind: 4)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ extension NdbProfile {
|
||||
}
|
||||
|
||||
static func displayName(profile: Profile?, pubkey: Pubkey) -> DisplayName {
|
||||
return parse_display_name(profile: profile, pubkey: pubkey)
|
||||
return DisplayName(name: profile?.name, display_name: profile?.display_name, pubkey: pubkey)
|
||||
}
|
||||
|
||||
var damus_donation: Int? {
|
||||
|
||||
@@ -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 = encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
||||
let enc = NIP04.encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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) * 4.0), Int(getpagesize()))
|
||||
let bufsize: Int = max(Int(Double(json.utf8.count) * 8.0), Int(getpagesize()))
|
||||
let data = malloc(bufsize)
|
||||
|
||||
if data == nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ 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"
|
||||
|
||||
|
||||
@@ -10,7 +10,15 @@ 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):
|
||||
@@ -28,20 +36,37 @@ 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(profile: Profile?, pubkey: Pubkey) -> DisplayName {
|
||||
func parse_display_name(name: String?, display_name: String?, pubkey: Pubkey) -> DisplayName {
|
||||
if pubkey == ANON_PUBKEY {
|
||||
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
|
||||
}
|
||||
|
||||
guard let profile else {
|
||||
|
||||
if name == nil && display_name == nil {
|
||||
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
|
||||
}
|
||||
|
||||
let name = profile.name?.isEmpty == false ? profile.name : nil
|
||||
let disp_name = profile.display_name?.isEmpty == false ? profile.display_name : nil
|
||||
|
||||
let name = name?.isEmpty == false ? name : nil
|
||||
let disp_name = display_name?.isEmpty == false ? display_name : nil
|
||||
|
||||
if let name, let disp_name, name != disp_name {
|
||||
return .both(username: name, displayName: disp_name)
|
||||
|
||||
15
damus/Util/ExtraFonts.swift
Normal file
15
damus/Util/ExtraFonts.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// ExtraFonts.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-03-13.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
extension Font {
|
||||
// Note: When changing the font size accessibility setting, these styles only update after an app restart. It's a current limitation of this.
|
||||
|
||||
static let veryLargeTitle: Font = .system(size: UIFont.preferredFont(forTextStyle: .largeTitle).pointSize * 1.5, weight: .bold) // Makes a bigger title while allowing for iOS dynamic font sizing to take effect
|
||||
static let veryVeryLargeTitle: Font = .system(size: UIFont.preferredFont(forTextStyle: .largeTitle).pointSize * 2.1, weight: .bold) // Makes a bigger title while allowing for iOS dynamic font sizing to take effect
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ enum LogCategory: String {
|
||||
case storage
|
||||
case networking
|
||||
case timeline
|
||||
/// Logs related to Nostr Wallet Connect components
|
||||
case nwc
|
||||
case push_notifications
|
||||
case damus_purple
|
||||
case image_uploading
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
//
|
||||
// WalletConnect+.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-11-27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func make_wallet_pay_invoice_request(invoice: String) -> WalletRequest<PayInvoiceRequest> {
|
||||
let data = PayInvoiceRequest(invoice: invoice)
|
||||
return WalletRequest(method: "pay_invoice", params: data)
|
||||
}
|
||||
|
||||
func make_wallet_balance_request() -> WalletRequest<EmptyRequest> {
|
||||
return WalletRequest(method: "get_balance", params: nil)
|
||||
}
|
||||
|
||||
struct EmptyRequest: Codable {
|
||||
}
|
||||
|
||||
struct PayInvoiceRequest: Codable {
|
||||
let invoice: String
|
||||
}
|
||||
|
||||
func make_wallet_connect_request<T>(req: WalletRequest<T>, to_pk: Pubkey, keypair: FullKeypair) -> NostrEvent? {
|
||||
let tags = [to_pk.tag]
|
||||
let created_at = UInt32(Date().timeIntervalSince1970)
|
||||
guard let content = encode_json(req) else {
|
||||
return nil
|
||||
}
|
||||
return create_encrypted_event(content, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created_at, kind: 23194)
|
||||
}
|
||||
|
||||
func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
|
||||
var filter = NostrFilter(kinds: [.nwc_response])
|
||||
filter.authors = [url.pubkey]
|
||||
filter.limit = 0
|
||||
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
||||
|
||||
pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||
let req = make_wallet_pay_invoice_request(invoice: invoice)
|
||||
guard let ev = make_wallet_connect_request(req: req, to_pk: url.pubkey, keypair: url.keypair) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
try? pool.add_relay(.nwc(url: url.relay))
|
||||
subscribe_to_nwc(url: url, pool: pool)
|
||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||
return ev
|
||||
}
|
||||
|
||||
|
||||
func nwc_success(state: DamusState, resp: FullWalletResponse) {
|
||||
// find the pending zap and mark it as pending-confirmed
|
||||
for kv in state.zaps.our_zaps {
|
||||
let zaps = kv.value
|
||||
|
||||
for zap in zaps {
|
||||
guard case .pending(let pzap) = zap,
|
||||
case .nwc(let nwc_state) = pzap.state,
|
||||
case .postbox_pending(let nwc_req) = nwc_state.state,
|
||||
nwc_req.id == resp.req_id
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
if nwc_state.update_state(state: .confirmed) {
|
||||
// notify the zaps model of an update so it can mark them as paid
|
||||
state.events.get_cache_data(NoteId(pzap.target.id)).zaps_model.objectWillChange.send()
|
||||
print("NWC success confirmed")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
|
||||
let percent_f = Double(percent) / 100.0
|
||||
let donations_msats = Int64(percent_f * Double(base_msats))
|
||||
|
||||
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
|
||||
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
|
||||
// we failed... oh well. no donation for us.
|
||||
print("damus-donation failed to fetch invoice")
|
||||
return
|
||||
}
|
||||
|
||||
print("damus-donation donating...")
|
||||
nwc_pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
|
||||
}
|
||||
|
||||
func nwc_error(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) {
|
||||
// find a pending zap with the nwc request id associated with this response and remove it
|
||||
for kv in zapcache.our_zaps {
|
||||
let zaps = kv.value
|
||||
|
||||
for zap in zaps {
|
||||
guard case .pending(let pzap) = zap,
|
||||
case .nwc(let nwc_state) = pzap.state,
|
||||
case .postbox_pending(let req) = nwc_state.state,
|
||||
req.id == resp.req_id
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
// remove the pending zap if there was an error
|
||||
let reqid = ZapRequestId(from_pending: pzap)
|
||||
remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
//
|
||||
// WalletConnect.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct WalletConnectURL: Equatable {
|
||||
static func == (lhs: WalletConnectURL, rhs: WalletConnectURL) -> Bool {
|
||||
return lhs.keypair == rhs.keypair &&
|
||||
lhs.pubkey == rhs.pubkey &&
|
||||
lhs.relay == rhs.relay
|
||||
}
|
||||
|
||||
let relay: RelayURL
|
||||
let keypair: FullKeypair
|
||||
let pubkey: Pubkey
|
||||
let lud16: String?
|
||||
|
||||
func to_url() -> URL {
|
||||
var urlComponents = URLComponents()
|
||||
urlComponents.scheme = "nostrwalletconnect"
|
||||
urlComponents.host = pubkey.hex()
|
||||
urlComponents.queryItems = [
|
||||
URLQueryItem(name: "relay", value: relay.absoluteString),
|
||||
URLQueryItem(name: "secret", value: keypair.privkey.hex())
|
||||
]
|
||||
|
||||
if let lud16 {
|
||||
urlComponents.queryItems?.append(URLQueryItem(name: "lud16", value: lud16))
|
||||
}
|
||||
|
||||
return urlComponents.url!
|
||||
}
|
||||
|
||||
init?(str: String) {
|
||||
guard let components = URLComponents(string: str),
|
||||
components.scheme == "nostrwalletconnect" || components.scheme == "nostr+walletconnect",
|
||||
// The line below provides flexibility for both `nostrwalletconnect://` (non-compliant, but commonly used) and `nostrwalletconnect:` (NIP-47 compliant) formats
|
||||
let encoded_pubkey = components.path == "" ? components.host : components.path,
|
||||
let pubkey = hex_decode_pubkey(encoded_pubkey),
|
||||
let items = components.queryItems,
|
||||
let relay = items.first(where: { qi in qi.name == "relay" })?.value,
|
||||
let relay_url = RelayURL(relay),
|
||||
let secret = items.first(where: { qi in qi.name == "secret" })?.value,
|
||||
secret.utf8.count == 64,
|
||||
let decoded = hex_decode(secret)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let privkey = Privkey(Data(decoded))
|
||||
guard let our_pk = privkey_to_pubkey(privkey: privkey) else { return nil }
|
||||
|
||||
let lud16 = items.first(where: { qi in qi.name == "lud16" })?.value
|
||||
let keypair = FullKeypair(pubkey: our_pk, privkey: privkey)
|
||||
self = WalletConnectURL(pubkey: pubkey, relay: relay_url, keypair: keypair, lud16: lud16)
|
||||
}
|
||||
|
||||
init(pubkey: Pubkey, relay: RelayURL, keypair: FullKeypair, lud16: String?) {
|
||||
self.pubkey = pubkey
|
||||
self.relay = relay
|
||||
self.keypair = keypair
|
||||
self.lud16 = lud16
|
||||
}
|
||||
}
|
||||
|
||||
struct WalletRequest<T: Codable>: Codable {
|
||||
let method: String
|
||||
let params: T?
|
||||
}
|
||||
|
||||
struct WalletResponseErr: Codable {
|
||||
let code: String?
|
||||
let message: String?
|
||||
}
|
||||
|
||||
struct PayInvoiceResponse: Decodable {
|
||||
let preimage: String
|
||||
}
|
||||
|
||||
enum WalletResponseResultType: String {
|
||||
case pay_invoice
|
||||
}
|
||||
|
||||
enum WalletResponseResult {
|
||||
case pay_invoice(PayInvoiceResponse)
|
||||
}
|
||||
|
||||
struct FullWalletResponse {
|
||||
let req_id: NoteId
|
||||
let response: WalletResponse
|
||||
|
||||
init?(from: NostrEvent, nwc: WalletConnectURL) async {
|
||||
guard let note_id = from.referenced_ids.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.req_id = note_id
|
||||
|
||||
let ares = Task {
|
||||
guard let json = decrypt_dm(nwc.keypair.privkey, pubkey: nwc.pubkey, content: from.content, encoding: .base64),
|
||||
let resp: WalletResponse = decode_json(json)
|
||||
else {
|
||||
let resp: WalletResponse? = nil
|
||||
return resp
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
guard let res = await ares.value else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.response = res
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct WalletResponse: Decodable {
|
||||
let result_type: WalletResponseResultType
|
||||
let error: WalletResponseErr?
|
||||
let result: WalletResponseResult?
|
||||
|
||||
private enum CodingKeys: CodingKey {
|
||||
case result_type, error, result
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let result_type_str = try container.decode(String.self, forKey: .result_type)
|
||||
|
||||
guard let result_type = WalletResponseResultType(rawValue: result_type_str) else {
|
||||
throw DecodingError.typeMismatch(WalletResponseResultType.self, .init(codingPath: decoder.codingPath, debugDescription: "result_type \(result_type_str) is unknown"))
|
||||
}
|
||||
|
||||
self.result_type = result_type
|
||||
self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error)
|
||||
|
||||
guard self.error == nil else {
|
||||
self.result = nil
|
||||
return
|
||||
}
|
||||
|
||||
switch result_type {
|
||||
case .pay_invoice:
|
||||
let res = try container.decode(PayInvoiceResponse.self, forKey: .result)
|
||||
self.result = .pay_invoice(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
137
damus/Util/WalletConnect/Request.swift
Normal file
137
damus/Util/WalletConnect/Request.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// Request.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-03-10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension WalletConnect {
|
||||
/// Models a request to an NWC wallet provider
|
||||
enum Request: Codable {
|
||||
/// Pay an invoice
|
||||
case payInvoice(
|
||||
/// bolt-11 invoice string
|
||||
invoice: String
|
||||
)
|
||||
/// Get the current wallet balance
|
||||
case getBalance
|
||||
/// Get the current wallet transaction history
|
||||
case getTransactionList(
|
||||
/// Starting timestamp in seconds since epoch (inclusive), optional.
|
||||
from: UInt64?,
|
||||
/// Ending timestamp in seconds since epoch (inclusive), optional.
|
||||
until: UInt64?,
|
||||
/// Maximum number of invoices to return, optional.
|
||||
limit: Int?,
|
||||
/// Offset of the first invoice to return, optional.
|
||||
offset: Int?,
|
||||
/// Include unpaid invoices, optional, default false.
|
||||
unpaid: Bool?,
|
||||
/// "incoming" for invoices, "outgoing" for payments, undefined for both.
|
||||
type: String?
|
||||
)
|
||||
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
/// Converts the NWC request into a raw Nostr event to be sent in the network
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - to_pk: The destination pubkey (used for encryption)
|
||||
/// - keypair: The requester's pubkey (used for encryption and signing)
|
||||
/// - Returns: The NWC request in a raw Nostr Event format, or nil if it cannot be encoded
|
||||
func to_nostr_event(to_pk: Pubkey, keypair: FullKeypair) -> NostrEvent? {
|
||||
let tags = [to_pk.tag]
|
||||
let created_at = UInt32(Date().timeIntervalSince1970)
|
||||
guard let content = encode_json(self) else {
|
||||
return nil
|
||||
}
|
||||
return NIP04.create_encrypted_event(content, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created_at, kind: NostrKind.nwc_request.rawValue)
|
||||
}
|
||||
|
||||
// MARK: - Encoding and decoding
|
||||
|
||||
/// Keys for top-level JSON
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case method
|
||||
case params
|
||||
}
|
||||
|
||||
/// Keys for the JSON inside the "params" object
|
||||
private enum ParamKeys: String, CodingKey {
|
||||
case invoice
|
||||
case from, until, limit, offset, unpaid, type
|
||||
}
|
||||
|
||||
/// Constants for possible request "method" verbs
|
||||
private enum Method: String {
|
||||
case payInvoice = "pay_invoice"
|
||||
case getBalance = "get_balance"
|
||||
case listTransactions = "list_transactions"
|
||||
}
|
||||
|
||||
/// Decodes a payload into this request structure
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let method = try container.decode(String.self, forKey: .method)
|
||||
|
||||
|
||||
switch method {
|
||||
case Method.payInvoice.rawValue:
|
||||
let paramsContainer = try container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||
let invoice = try paramsContainer.decode(String.self, forKey: .invoice)
|
||||
self = .payInvoice(invoice: invoice)
|
||||
|
||||
case Method.getBalance.rawValue:
|
||||
// No params to decode
|
||||
self = .getBalance
|
||||
|
||||
case Method.listTransactions.rawValue:
|
||||
let paramsContainer = try container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||
let from = try paramsContainer.decodeIfPresent(UInt64.self, forKey: .from)
|
||||
let until = try paramsContainer.decodeIfPresent(UInt64.self, forKey: .until)
|
||||
let limit = try paramsContainer.decodeIfPresent(Int.self, forKey: .limit)
|
||||
let offset = try paramsContainer.decodeIfPresent(Int.self, forKey: .offset)
|
||||
let unpaid = try paramsContainer.decodeIfPresent(Bool.self, forKey: .unpaid)
|
||||
let type = try paramsContainer.decodeIfPresent(String.self, forKey: .type)
|
||||
self = .getTransactionList(from: from, until: until, limit: limit, offset: offset, unpaid: unpaid, type: type)
|
||||
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .method,
|
||||
in: container,
|
||||
debugDescription: "Unknown wallet method \"\(method)\""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes this request structure into a payload
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch self {
|
||||
case .payInvoice(let invoice):
|
||||
try container.encode(Method.payInvoice.rawValue, forKey: .method)
|
||||
var paramsContainer = container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||
try paramsContainer.encode(invoice, forKey: .invoice)
|
||||
|
||||
case .getBalance:
|
||||
try container.encode(Method.getBalance.rawValue, forKey: .method)
|
||||
// "params": null
|
||||
try container.encodeNil(forKey: .params)
|
||||
|
||||
case .getTransactionList(let from, let until, let limit, let offset, let unpaid, let type):
|
||||
try container.encode(Method.listTransactions.rawValue, forKey: .method)
|
||||
var paramsContainer = container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||
try paramsContainer.encodeIfPresent(from, forKey: .from)
|
||||
try paramsContainer.encodeIfPresent(until, forKey: .until)
|
||||
try paramsContainer.encodeIfPresent(limit, forKey: .limit)
|
||||
try paramsContainer.encodeIfPresent(offset, forKey: .offset)
|
||||
try paramsContainer.encodeIfPresent(unpaid, forKey: .unpaid)
|
||||
try paramsContainer.encodeIfPresent(type, forKey: .type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
damus/Util/WalletConnect/Response.swift
Normal file
110
damus/Util/WalletConnect/Response.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// Response.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-03-10.
|
||||
//
|
||||
|
||||
extension WalletConnect {
|
||||
/// Models a response from the NWC provider
|
||||
struct Response: Decodable {
|
||||
let result_type: Response.Result.ResultType
|
||||
let error: WalletResponseErr?
|
||||
let result: Response.Result?
|
||||
|
||||
private enum CodingKeys: CodingKey {
|
||||
case result_type, error, result
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let result_type_str = try container.decode(String.self, forKey: .result_type)
|
||||
|
||||
guard let result_type = Response.Result.ResultType(rawValue: result_type_str) else {
|
||||
throw DecodingError.typeMismatch(Response.Result.ResultType.self, .init(codingPath: decoder.codingPath, debugDescription: "result_type \(result_type_str) is unknown"))
|
||||
}
|
||||
|
||||
self.result_type = result_type
|
||||
self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error)
|
||||
|
||||
guard self.error == nil else {
|
||||
self.result = nil
|
||||
return
|
||||
}
|
||||
|
||||
switch result_type {
|
||||
case .pay_invoice:
|
||||
let res = try container.decode(Result.PayInvoiceResponse.self, forKey: .result)
|
||||
self.result = .pay_invoice(res)
|
||||
case .get_balance:
|
||||
let res = try container.decode(Result.GetBalanceResponse.self, forKey: .result)
|
||||
self.result = .get_balance(res)
|
||||
case .list_transactions:
|
||||
let res = try container.decode(Result.ListTransactionsResponse.self, forKey: .result)
|
||||
self.result = .list_transactions(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FullWalletResponse {
|
||||
let req_id: NoteId
|
||||
let response: Response
|
||||
|
||||
init?(from: NostrEvent, nwc: WalletConnect.ConnectURL) async {
|
||||
guard let note_id = from.referenced_ids.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.req_id = note_id
|
||||
|
||||
let ares = Task {
|
||||
guard let json = decrypt_dm(nwc.keypair.privkey, pubkey: nwc.pubkey, content: from.content, encoding: .base64),
|
||||
let resp: WalletConnect.Response = decode_json(json)
|
||||
else {
|
||||
let resp: WalletConnect.Response? = nil
|
||||
return resp
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
guard let res = await ares.value else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.response = res
|
||||
}
|
||||
}
|
||||
|
||||
struct WalletResponseErr: Codable {
|
||||
let code: String?
|
||||
let message: String?
|
||||
}
|
||||
}
|
||||
|
||||
extension WalletConnect.Response {
|
||||
/// The response data resulting from an NWC request
|
||||
enum Result {
|
||||
case pay_invoice(PayInvoiceResponse)
|
||||
case get_balance(GetBalanceResponse)
|
||||
case list_transactions(ListTransactionsResponse)
|
||||
|
||||
enum ResultType: String {
|
||||
case pay_invoice
|
||||
case get_balance
|
||||
case list_transactions
|
||||
}
|
||||
|
||||
struct PayInvoiceResponse: Decodable {
|
||||
let preimage: String
|
||||
}
|
||||
|
||||
struct GetBalanceResponse: Decodable {
|
||||
let balance: Int64
|
||||
}
|
||||
|
||||
struct ListTransactionsResponse: Decodable {
|
||||
let transactions: [WalletConnect.Transaction]
|
||||
}
|
||||
}
|
||||
}
|
||||
170
damus/Util/WalletConnect/WalletConnect+.swift
Normal file
170
damus/Util/WalletConnect/WalletConnect+.swift
Normal file
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// WalletConnect+.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-11-27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// TODO: Eventually we should move these convenience functions into structured classes responsible for managing this type of functionality, such as `WalletModel`
|
||||
|
||||
extension WalletConnect {
|
||||
/// Creates and sends a subscription to an NWC relay requesting NWC responses to be sent back.
|
||||
///
|
||||
/// Notes: This assumes there is already a listener somewhere else
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The Nostr Wallet Connect URL containing connection info to the NWC wallet
|
||||
/// - pool: The RelayPool to send the subscription request through
|
||||
static func subscribe(url: WalletConnectURL, pool: RelayPool) {
|
||||
var filter = NostrFilter(kinds: [.nwc_response])
|
||||
filter.authors = [url.pubkey]
|
||||
filter.limit = 0
|
||||
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
||||
|
||||
pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false)
|
||||
}
|
||||
|
||||
/// Sends out a request to pay an invoice to the NWC relay, and ensures that:
|
||||
/// 1. the NWC relay is connected and we are listening to NWC events
|
||||
/// 2. the NWC relay is connected and we are listening to NWC
|
||||
///
|
||||
/// Note: This does not return information about whether the payment is succesful or not. The actual confirmation is handled elsewhere around `HomeModel` and `WalletModel`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The NWC wallet connection URL
|
||||
/// - pool: The relay pool to connect to
|
||||
/// - post: The postbox to send events in
|
||||
/// - delay: The delay before actually sending the request to the network _(this makes it possible to cancel a zap)_
|
||||
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||
@discardableResult
|
||||
static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||
let req = WalletConnect.Request.payInvoice(invoice: invoice)
|
||||
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
||||
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||
return ev
|
||||
}
|
||||
|
||||
/// Sends out a wallet balance request to the NWC relay, and ensures that:
|
||||
/// 1. the NWC relay is connected and we are listening to NWC events
|
||||
/// 2. the NWC relay is connected and we are listening to NWC
|
||||
///
|
||||
/// Note: This does not return the actual balance information. The actual balance is handled elsewhere around `HomeModel` and `WalletModel`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The NWC wallet connection URL
|
||||
/// - pool: The relay pool to connect to
|
||||
/// - post: The postbox to send events in
|
||||
/// - delay: The delay before actually sending the request to the network
|
||||
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||
@discardableResult
|
||||
static func request_balance_information(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||
let req = WalletConnect.Request.getBalance
|
||||
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
||||
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||
return ev
|
||||
}
|
||||
|
||||
/// Sends out a wallet transaction list request to the NWC relay, and ensures that:
|
||||
/// 1. the NWC relay is connected and we are listening to NWC events
|
||||
/// 2. the NWC relay is connected and we are listening to NWC
|
||||
///
|
||||
/// Note: This does not return the actual transaction list. The actual transaction list is handled elsewhere around `HomeModel` and `WalletModel`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The NWC wallet connection URL
|
||||
/// - pool: The relay pool to connect to
|
||||
/// - post: The postbox to send events in
|
||||
/// - delay: The delay before actually sending the request to the network
|
||||
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||
@discardableResult
|
||||
static func request_transaction_list(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||
let req = WalletConnect.Request.getTransactionList(from: nil, until: nil, limit: 10, offset: 0, unpaid: false, type: "")
|
||||
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
||||
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||
return ev
|
||||
}
|
||||
|
||||
static func handle_zap_success(state: DamusState, resp: WalletConnect.FullWalletResponse) {
|
||||
// find the pending zap and mark it as pending-confirmed
|
||||
for kv in state.zaps.our_zaps {
|
||||
let zaps = kv.value
|
||||
|
||||
for zap in zaps {
|
||||
guard case .pending(let pzap) = zap,
|
||||
case .nwc(let nwc_state) = pzap.state,
|
||||
case .postbox_pending(let nwc_req) = nwc_state.state,
|
||||
nwc_req.id == resp.req_id
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
if nwc_state.update_state(state: .confirmed) {
|
||||
// notify the zaps model of an update so it can mark them as paid
|
||||
state.events.get_cache_data(NoteId(pzap.target.id)).zaps_model.objectWillChange.send()
|
||||
print("NWC success confirmed")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a donation zap to the Damus team
|
||||
static func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
|
||||
let percent_f = Double(percent) / 100.0
|
||||
let donations_msats = Int64(percent_f * Double(base_msats))
|
||||
|
||||
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
|
||||
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
|
||||
// we failed... oh well. no donation for us.
|
||||
print("damus-donation failed to fetch invoice")
|
||||
return
|
||||
}
|
||||
|
||||
print("damus-donation donating...")
|
||||
WalletConnect.pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
|
||||
}
|
||||
|
||||
/// Handles a received Nostr Wallet Connect error
|
||||
static func handle_error(zapcache: Zaps, evcache: EventCache, resp: WalletConnect.FullWalletResponse) {
|
||||
// find a pending zap with the nwc request id associated with this response and remove it
|
||||
for kv in zapcache.our_zaps {
|
||||
let zaps = kv.value
|
||||
|
||||
for zap in zaps {
|
||||
guard case .pending(let pzap) = zap,
|
||||
case .nwc(let nwc_state) = pzap.state,
|
||||
case .postbox_pending(let req) = nwc_state.state,
|
||||
req.id == resp.req_id
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
// remove the pending zap if there was an error
|
||||
let reqid = ZapRequestId(from_pending: pzap)
|
||||
remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
damus/Util/WalletConnect/WalletConnect.swift
Normal file
92
damus/Util/WalletConnect/WalletConnect.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// WalletConnect.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct WalletConnect {}
|
||||
|
||||
typealias WalletConnectURL = WalletConnect.ConnectURL // Declared to facilitate refactor
|
||||
|
||||
extension WalletConnect {
|
||||
/// Models a decoded NWC URL, containing information to connect to an NWC wallet.
|
||||
struct ConnectURL: Equatable {
|
||||
let relay: RelayURL
|
||||
let keypair: FullKeypair
|
||||
let pubkey: Pubkey
|
||||
let lud16: String?
|
||||
|
||||
static func == (lhs: ConnectURL, rhs: ConnectURL) -> Bool {
|
||||
return lhs.keypair == rhs.keypair &&
|
||||
lhs.pubkey == rhs.pubkey &&
|
||||
lhs.relay == rhs.relay
|
||||
}
|
||||
|
||||
func to_url() -> URL {
|
||||
var urlComponents = URLComponents()
|
||||
urlComponents.scheme = "nostrwalletconnect"
|
||||
urlComponents.host = pubkey.hex()
|
||||
urlComponents.queryItems = [
|
||||
URLQueryItem(name: "relay", value: relay.absoluteString),
|
||||
URLQueryItem(name: "secret", value: keypair.privkey.hex())
|
||||
]
|
||||
|
||||
if let lud16 {
|
||||
urlComponents.queryItems?.append(URLQueryItem(name: "lud16", value: lud16))
|
||||
}
|
||||
|
||||
return urlComponents.url!
|
||||
}
|
||||
|
||||
init?(str: String) {
|
||||
guard let components = URLComponents(string: str),
|
||||
components.scheme == "nostrwalletconnect" || components.scheme == "nostr+walletconnect",
|
||||
// The line below provides flexibility for both `nostrwalletconnect://` (non-compliant, but commonly used) and `nostrwalletconnect:` (NIP-47 compliant) formats
|
||||
let encoded_pubkey = components.path == "" ? components.host : components.path,
|
||||
let pubkey = hex_decode_pubkey(encoded_pubkey),
|
||||
let items = components.queryItems,
|
||||
let relay = items.first(where: { qi in qi.name == "relay" })?.value,
|
||||
let relay_url = RelayURL(relay),
|
||||
let secret = items.first(where: { qi in qi.name == "secret" })?.value,
|
||||
secret.utf8.count == 64,
|
||||
let decoded = hex_decode(secret)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let privkey = Privkey(Data(decoded))
|
||||
guard let our_pk = privkey_to_pubkey(privkey: privkey) else { return nil }
|
||||
|
||||
let lud16 = items.first(where: { qi in qi.name == "lud16" })?.value
|
||||
let keypair = FullKeypair(pubkey: our_pk, privkey: privkey)
|
||||
self = ConnectURL(pubkey: pubkey, relay: relay_url, keypair: keypair, lud16: lud16)
|
||||
}
|
||||
|
||||
init(pubkey: Pubkey, relay: RelayURL, keypair: FullKeypair, lud16: String?) {
|
||||
self.pubkey = pubkey
|
||||
self.relay = relay
|
||||
self.keypair = keypair
|
||||
self.lud16 = lud16
|
||||
}
|
||||
}
|
||||
|
||||
/// Models an NWC wallet transaction
|
||||
struct Transaction: Decodable, Equatable, Hashable {
|
||||
let type: String
|
||||
let invoice: String?
|
||||
let description: String?
|
||||
let description_hash: String?
|
||||
let preimage: String?
|
||||
let payment_hash: String?
|
||||
let amount: Int64
|
||||
let fees_paid: Int64?
|
||||
let created_at: UInt64 // unixtimestamp, // invoice/payment creation time
|
||||
let expires_at: UInt64? // unixtimestamp, // invoice expiration time, optional if not applicable
|
||||
let settled_at: UInt64? // unixtimestamp, // invoice/payment settlement time, optional if unpaid
|
||||
//"metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,26 @@ 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)
|
||||
@@ -31,91 +46,122 @@ 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 {
|
||||
NavigationLink(value: Route.KeySettings(keypair: state.keypair)) {
|
||||
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "Key", color: .purple)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.AppearanceSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Appearance and filters", comment: "Section header for text, appearance, and content filter settings"), img_name: "eye", color: .red)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.SearchSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Search/Universe", comment: "Section header for search/universe settings"), img_name: "search", color: .red)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.NotificationSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "notification-bell-on", color: .blue)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.ZapSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "zap.fill", color: .orange)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.TranslationSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe", color: .green)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.ReactionsSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Reactions", comment: "Section header for reactions settings"), img_name: "shaka.fill", color: .purple)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.DeveloperSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Developer", comment: "Section header for developer settings"), img_name: "magic-stick2.fill", color: DamusColors.adaptableBlack)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.FirstAidSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("First Aid", comment: "Section header for first aid tools and settings"), img_name: "help2", color: .red)
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
|
||||
Button(action: {
|
||||
if state.keypair.privkey == nil {
|
||||
logout(state)
|
||||
} else {
|
||||
confirm_logout = true
|
||||
// Keys
|
||||
if showSettingsButton(title: keysTitle){
|
||||
NavigationLink(value:Route.KeySettings(keypair: state.keypair)){
|
||||
IconLabel(keysTitle,img_name:"Key",color:.purple)
|
||||
}
|
||||
}, label: {
|
||||
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), image: "logout")
|
||||
.foregroundColor(textColor())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
})
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
if state.is_privkey_user {
|
||||
Section(header: Text("Permanently Delete Account", comment: "Section title for deleting the user")) {
|
||||
//Notifications
|
||||
if showSettingsButton(title: notificationsTitle){
|
||||
NavigationLink(value: Route.NotificationSettings(settings: settings)){
|
||||
IconLabel(notificationsTitle,img_name:"notification-bell-on",color:.blue)
|
||||
}
|
||||
}
|
||||
//Zaps
|
||||
if showSettingsButton(title: zapsTitle){
|
||||
NavigationLink(value: Route.ZapSettings(settings: settings)){
|
||||
IconLabel(zapsTitle,img_name:"zap.fill",color:.orange)
|
||||
}
|
||||
}
|
||||
//Translation
|
||||
if showSettingsButton(title: translationTitle){
|
||||
NavigationLink(value: Route.TranslationSettings(settings: settings)){
|
||||
IconLabel(translationTitle,img_name:"globe",color:.green)
|
||||
}
|
||||
}
|
||||
//Reactions
|
||||
if showSettingsButton(title: reactionsTitle){
|
||||
NavigationLink(value: Route.ReactionsSettings(settings: settings)){
|
||||
IconLabel(reactionsTitle,img_name:"shaka.fill",color:.purple)
|
||||
}
|
||||
}
|
||||
//Developer
|
||||
if showSettingsButton(title: developerTitle){
|
||||
NavigationLink(value: Route.DeveloperSettings(settings: settings)){
|
||||
IconLabel(developerTitle,img_name:"magic-stick2.fill",color:DamusColors.adaptableBlack)
|
||||
}
|
||||
}
|
||||
//First Aid
|
||||
if showSettingsButton(title: firstAidTitle){
|
||||
NavigationLink(value: Route.FirstAidSettings(settings: settings)){
|
||||
IconLabel(firstAidTitle,img_name:"help2",color: .red)
|
||||
}
|
||||
}
|
||||
}
|
||||
//Sign out Section
|
||||
if showSettingsButton(title: signOutTitle){
|
||||
Section(signOutTitle){
|
||||
Button(action: {
|
||||
delete_account_warning = true
|
||||
if state.keypair.privkey == nil {
|
||||
logout(state)
|
||||
} else {
|
||||
confirm_logout = true
|
||||
}
|
||||
}, label: {
|
||||
Label(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), image: "delete")
|
||||
Label(signOutTitle, image: "logout")
|
||||
.foregroundColor(textColor())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.foregroundColor(.red)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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) {
|
||||
@@ -154,7 +200,6 @@ struct ConfigView: View {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ConfigView_Previews: PreviewProvider {
|
||||
|
||||
@@ -131,7 +131,7 @@ struct DMChatView: View, KeyboardReadable {
|
||||
.map(\.asString)
|
||||
.joined(separator: "")
|
||||
|
||||
guard let dm = create_dm(content, to_pk: pubkey, tags: tags, keypair: damus_state.keypair) else {
|
||||
guard let dm = NIP04.create_dm(content, to_pk: pubkey, tags: tags, keypair: damus_state.keypair) else {
|
||||
print("error creating dm")
|
||||
return
|
||||
}
|
||||
@@ -176,46 +176,6 @@ struct DMChatView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func encrypt_message(message: String, privkey: Privkey, to_pk: Pubkey, encoding: EncEncoding = .base64) -> String? {
|
||||
let iv = random_bytes(count: 16).bytes
|
||||
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
|
||||
return nil
|
||||
}
|
||||
let utf8_message = Data(message.utf8).bytes
|
||||
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch encoding {
|
||||
case .base64:
|
||||
return encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||
case .bech32:
|
||||
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func create_encrypted_event(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: FullKeypair, created_at: UInt32, kind: UInt32) -> NostrEvent? {
|
||||
let privkey = keypair.privkey
|
||||
|
||||
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NostrEvent(content: enc_content, keypair: keypair.to_keypair(), kind: kind, tags: tags, createdAt: created_at)
|
||||
}
|
||||
|
||||
func create_dm(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent?
|
||||
{
|
||||
let created = created_at ?? UInt32(Date().timeIntervalSince1970)
|
||||
|
||||
guard let keypair = keypair.to_full() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return create_encrypted_event(message, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created, kind: 4)
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Layers the given views behind this ``TextEditor``.
|
||||
func textEditorBackground<V>(@ViewBuilder _ content: () -> V) -> some View where V : View {
|
||||
|
||||
@@ -9,15 +9,27 @@ import SwiftUI
|
||||
|
||||
class NotificationFilter: ObservableObject, Equatable {
|
||||
@Published var state: NotificationFilterState
|
||||
@Published var fine_filter: FriendFilter
|
||||
|
||||
@Published var friend_filter: FriendFilter
|
||||
@Published var hellthread_notifications_disabled: Bool
|
||||
@Published var hellthread_notification_max_pubkeys: Int
|
||||
|
||||
static func == (lhs: NotificationFilter, rhs: NotificationFilter) -> Bool {
|
||||
return lhs.state == rhs.state && lhs.fine_filter == rhs.fine_filter
|
||||
return lhs.state == rhs.state
|
||||
&& lhs.friend_filter == rhs.friend_filter
|
||||
&& lhs.hellthread_notifications_disabled == rhs.hellthread_notifications_disabled
|
||||
&& lhs.hellthread_notification_max_pubkeys == rhs.hellthread_notification_max_pubkeys
|
||||
}
|
||||
|
||||
init(state: NotificationFilterState = .all, fine_filter: FriendFilter = .all) {
|
||||
init(
|
||||
state: NotificationFilterState = .all,
|
||||
friend_filter: FriendFilter = .all,
|
||||
hellthread_notifications_disabled: Bool = false,
|
||||
hellthread_notification_max_pubkeys: Int = DEFAULT_HELLTHREAD_MAX_PUBKEYS
|
||||
) {
|
||||
self.state = state
|
||||
self.fine_filter = fine_filter
|
||||
self.friend_filter = friend_filter
|
||||
self.hellthread_notifications_disabled = hellthread_notifications_disabled
|
||||
self.hellthread_notification_max_pubkeys = hellthread_notification_max_pubkeys
|
||||
}
|
||||
|
||||
func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] {
|
||||
@@ -26,8 +38,11 @@ class NotificationFilter: ObservableObject, Equatable {
|
||||
if !self.state.filter(item) {
|
||||
return
|
||||
}
|
||||
|
||||
if let item = item.filter({ self.fine_filter.filter(contacts: contacts, pubkey: $0.pubkey) }) {
|
||||
|
||||
if let item = item.filter({ ev in
|
||||
self.friend_filter.filter(contacts: contacts, pubkey: ev.pubkey) &&
|
||||
(!hellthread_notifications_disabled || !ev.is_hellthread(max_pubkeys: hellthread_notification_max_pubkeys))
|
||||
}) {
|
||||
acc.append(item)
|
||||
}
|
||||
}
|
||||
@@ -65,7 +80,9 @@ struct NotificationsView: View {
|
||||
NotificationTab(
|
||||
NotificationFilter(
|
||||
state: .all,
|
||||
fine_filter: filter.fine_filter
|
||||
friend_filter: filter.friend_filter,
|
||||
hellthread_notifications_disabled: state.settings.hellthread_notifications_disabled,
|
||||
hellthread_notification_max_pubkeys: state.settings.hellthread_notification_max_pubkeys
|
||||
)
|
||||
)
|
||||
.tag(NotificationFilterState.all)
|
||||
@@ -73,7 +90,9 @@ struct NotificationsView: View {
|
||||
NotificationTab(
|
||||
NotificationFilter(
|
||||
state: .zaps,
|
||||
fine_filter: filter.fine_filter
|
||||
friend_filter: filter.friend_filter,
|
||||
hellthread_notifications_disabled: state.settings.hellthread_notifications_disabled,
|
||||
hellthread_notification_max_pubkeys: state.settings.hellthread_notification_max_pubkeys
|
||||
)
|
||||
)
|
||||
.tag(NotificationFilterState.zaps)
|
||||
@@ -81,7 +100,9 @@ struct NotificationsView: View {
|
||||
NotificationTab(
|
||||
NotificationFilter(
|
||||
state: .replies,
|
||||
fine_filter: filter.fine_filter
|
||||
friend_filter: filter.friend_filter,
|
||||
hellthread_notifications_disabled: state.settings.hellthread_notifications_disabled,
|
||||
hellthread_notification_max_pubkeys: state.settings.hellthread_notification_max_pubkeys
|
||||
)
|
||||
)
|
||||
.tag(NotificationFilterState.replies)
|
||||
@@ -98,20 +119,20 @@ struct NotificationsView: View {
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
|
||||
FriendsButton(filter: $filter.fine_filter)
|
||||
FriendsButton(filter: $filter.friend_filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: filter.fine_filter) { val in
|
||||
.onChange(of: filter.friend_filter) { val in
|
||||
state.settings.friend_filter = val
|
||||
self.subtitle = filter.fine_filter.description()
|
||||
self.subtitle = filter.friend_filter.description()
|
||||
}
|
||||
.onChange(of: filter_state) { val in
|
||||
filter.state = val
|
||||
}
|
||||
.onAppear {
|
||||
self.filter.fine_filter = state.settings.friend_filter
|
||||
self.subtitle = filter.fine_filter.description()
|
||||
self.filter.friend_filter = state.settings.friend_filter
|
||||
self.subtitle = filter.friend_filter.description()
|
||||
filter.state = filter_state
|
||||
}
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
|
||||
@@ -122,7 +122,10 @@ struct ProfileView: View {
|
||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||
var filters = ContentFilters.defaults(damus_state: damus_state)
|
||||
filters.append(fstate.filter)
|
||||
if fstate == .conversations {
|
||||
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
|
||||
|
||||
@@ -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 = parse_display_name(profile: profile, pubkey: account.pubkey).displayName
|
||||
let display_name = DisplayName(profile: profile, pubkey: account.pubkey).displayName
|
||||
return display_name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ struct NotificationSettingsView: View {
|
||||
@State var notification_preferences_sync_state: PreferencesSyncState = .undefined
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
func indicator_binding(_ val: NewEventsBits) -> Binding<Bool> {
|
||||
return Binding.init(get: {
|
||||
(settings.notification_indicators & val.rawValue) > 0
|
||||
@@ -28,7 +28,15 @@ struct NotificationSettingsView: View {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
var hellthread_notification_max_pubkeys_binding: Binding<Double> {
|
||||
Binding<Double>(get: {
|
||||
return Double(settings.hellthread_notification_max_pubkeys)
|
||||
}, set: {
|
||||
settings.hellthread_notification_max_pubkeys = Int($0)
|
||||
})
|
||||
}
|
||||
|
||||
func try_to_set_notifications_mode(new_value: UserSettingsStore.NotificationsMode) {
|
||||
notification_mode_setting_error = nil
|
||||
if new_value == .push {
|
||||
@@ -111,7 +119,24 @@ struct NotificationSettingsView: View {
|
||||
}
|
||||
|
||||
// MARK: - View layout
|
||||
|
||||
|
||||
func hellthread_notification_settings_text() -> String {
|
||||
if !settings.hellthread_notifications_disabled {
|
||||
return NSLocalizedString("Hide notifications that tag many profiles", comment: "Label for notification settings toggle that hides notifications that tag many people.")
|
||||
}
|
||||
return pluralizedString(key: "hellthread_notifications_disabled", count: $settings.hellthread_notification_max_pubkeys.wrappedValue)
|
||||
}
|
||||
|
||||
var hellthread_notifications_max_pubkeys_view: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Slider(
|
||||
value: self.notification_preference_binding(hellthread_notification_max_pubkeys_binding),
|
||||
in: Double(HELLTHREAD_MIN_PUBKEYS)...Double(HELLTHREAD_MAX_PUBKEYS),
|
||||
step: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if settings.enable_push_notifications {
|
||||
@@ -175,6 +200,13 @@ struct NotificationSettingsView: View {
|
||||
.toggleStyle(.switch)
|
||||
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: self.notification_preference_binding($settings.notification_only_from_following))
|
||||
.toggleStyle(.switch)
|
||||
VStack {
|
||||
Toggle(hellthread_notification_settings_text(), isOn: self.notification_preference_binding($settings.hellthread_notifications_disabled))
|
||||
.toggleStyle(.switch)
|
||||
if settings.hellthread_notifications_disabled {
|
||||
hellthread_notifications_max_pubkeys_view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(
|
||||
|
||||
@@ -162,6 +162,9 @@ struct SideMenuView: View {
|
||||
|
||||
PubkeyView(pubkey: damus_state.pubkey, sidemenu: true)
|
||||
.pubkey_context_menu(pubkey: damus_state.pubkey)
|
||||
.simultaneousGesture(TapGesture().onEnded{
|
||||
isSidebarVisible = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
damus/Views/Wallet/BalanceView.swift
Normal file
56
damus/Views/Wallet/BalanceView.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,57 +48,137 @@ struct ConnectWalletView: View {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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())
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
|
||||
.padding(.bottom, 50)
|
||||
.padding()
|
||||
}
|
||||
.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)
|
||||
}
|
||||
|
||||
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(NeutralButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
var ConnectWallet: some View {
|
||||
VStack(spacing: 25) {
|
||||
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)
|
||||
|
||||
AlbyButton() {
|
||||
openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!)
|
||||
}
|
||||
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()
|
||||
|
||||
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 {
|
||||
@@ -115,9 +195,10 @@ struct ConnectWalletView: View {
|
||||
Text("Paste NWC Address", comment: "Text for button to connect a lightning wallet.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
|
||||
.frame(minWidth: 250, maxWidth: .infinity, maxHeight: 15, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(.horizontal)
|
||||
|
||||
Button(action: {
|
||||
nav.push(route: Route.WalletScanner(result: $wallet_scan_result))
|
||||
@@ -127,74 +208,80 @@ struct ConnectWalletView: View {
|
||||
Text("Scan NWC Address", comment: "Text for button to connect a lightning wallet.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
|
||||
.frame(minWidth: 250, maxWidth: .infinity, maxHeight: 15, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
|
||||
|
||||
if let err = self.error {
|
||||
Text(err)
|
||||
.foregroundColor(.red)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.padding(.bottom, 50)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
var MainContent: some View {
|
||||
Group {
|
||||
TopSection
|
||||
switch model.connect_state {
|
||||
case .new(let nwc):
|
||||
AreYouSure(nwc: nwc)
|
||||
AreYouSure(nwc: nwc, show_introduction: $show_introduction, model: self.model)
|
||||
.onAppear() {
|
||||
show_introduction = false
|
||||
}
|
||||
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
damus/Views/Wallet/NWCSettings.swift
Normal file
227
damus/Views/Wallet/NWCSettings.swift
Normal file
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// 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
damus/Views/Wallet/TransactionsView.swift
Normal file
148
damus/Views/Wallet/TransactionsView.swift
Normal file
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct WalletView: View {
|
||||
let damus_state: DamusState
|
||||
@State var show_settings: Bool = false
|
||||
@ObservedObject var model: WalletModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
@@ -21,178 +22,20 @@ struct WalletView: View {
|
||||
func MainWalletView(nwc: WalletConnectURL) -> some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 35) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
BalanceView(balance: model.balance)
|
||||
|
||||
TransactionsView(damus_state: damus_state, transactions: model.transactions)
|
||||
}
|
||||
|
||||
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:
|
||||
@@ -201,38 +44,50 @@ 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() {
|
||||
model.initial_percent = settings.donation_percent
|
||||
Task { await self.updateWalletInformation() }
|
||||
}
|
||||
.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
|
||||
}
|
||||
|
||||
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)))
|
||||
.refreshable {
|
||||
model.resetWalletStateInformation()
|
||||
await self.updateWalletInformation()
|
||||
}
|
||||
.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
|
||||
.sheet(isPresented: $show_settings, onDismiss: { self.show_settings = false }) {
|
||||
ScrollView {
|
||||
NWCSettings(damus_state: damus_state, nwc: nwc, model: model, settings: settings)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
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)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.large])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
damus/Views/Wallet/ZapExplainer.swift
Normal file
203
damus/Views/Wallet/ZapExplainer.swift
Normal file
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
<!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>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by William Casarin on 2022-04-01.
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
@@ -59,13 +60,28 @@ 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
|
||||
}
|
||||
|
||||
@@ -96,6 +112,55 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
||||
Task { await QueueableNotify<LossyLocalNotification>.shared.add(item: 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.
@@ -66,6 +66,22 @@
|
||||
<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>
|
||||
@@ -175,7 +191,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 teiten eine Notiz, in der du markiert warst</string>
|
||||
<string>%2$@ und %1$d weitere teilten eine Notiz, in der du markiert wurdest</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reposted_your_note_3</key>
|
||||
|
||||
@@ -50,6 +50,22 @@
|
||||
<string>Following</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>hellthread_notifications_disabled</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@HELLTHREAD_PROFILES@</string>
|
||||
<key>HELLTHREAD_PROFILES</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Hide notifications that tag more than %d profile</string>
|
||||
<key>other</key>
|
||||
<string>Hide notifications that tag more than %d profiles</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>imports_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
@@ -82,6 +98,22 @@
|
||||
<string>%2$@ and %1$d others reposted</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>quoted_reposts_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@QUOTE_REPOSTS@</string>
|
||||
<key>QUOTE_REPOSTS</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Quote</string>
|
||||
<key>other</key>
|
||||
<string>Quotes</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reacted_tagged_in_3</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
@@ -242,22 +274,6 @@
|
||||
<string>Reposts</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>quoted_reposts_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@QUOTE_REPOSTS@</string>
|
||||
<key>QUOTE_REPOSTS</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Quote</string>
|
||||
<key>other</key>
|
||||
<string>Quotes</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>sats</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,9 @@
|
||||
"%@ 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."
|
||||
},
|
||||
@@ -102,9 +105,15 @@
|
||||
"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"
|
||||
},
|
||||
@@ -207,15 +216,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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."
|
||||
},
|
||||
@@ -231,9 +234,15 @@
|
||||
"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"
|
||||
},
|
||||
@@ -273,6 +282,9 @@
|
||||
"Camera's permission was denied. You can change this in iOS settings." : {
|
||||
"comment" : "Camera's permission denied error label"
|
||||
},
|
||||
"Can’t 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."
|
||||
},
|
||||
@@ -285,6 +297,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -345,6 +360,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -408,6 +426,12 @@
|
||||
"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."
|
||||
},
|
||||
@@ -426,9 +450,6 @@
|
||||
"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."
|
||||
},
|
||||
@@ -483,6 +504,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -570,6 +594,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -661,6 +688,15 @@
|
||||
"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"
|
||||
},
|
||||
@@ -754,12 +790,18 @@
|
||||
"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."
|
||||
},
|
||||
@@ -775,6 +817,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."
|
||||
},
|
||||
@@ -817,6 +865,12 @@
|
||||
"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"
|
||||
},
|
||||
@@ -910,6 +964,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -939,9 +996,6 @@
|
||||
},
|
||||
"Nostr Address" : {
|
||||
"comment" : "Label for the Nostr Address section of user profile form."
|
||||
},
|
||||
"Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive." : {
|
||||
|
||||
},
|
||||
"NostrScript" : {
|
||||
"comment" : "Navigation title for the view showing NostrScript."
|
||||
@@ -959,7 +1013,7 @@
|
||||
"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"
|
||||
"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."
|
||||
@@ -1060,6 +1114,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1069,12 +1126,18 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1242,9 +1305,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1263,6 +1323,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1275,6 +1338,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1294,7 +1360,7 @@
|
||||
"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"
|
||||
"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."
|
||||
@@ -1332,9 +1398,6 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1359,9 +1422,18 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1458,6 +1530,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1587,7 +1662,7 @@
|
||||
"Truncate timeline text" : {
|
||||
"comment" : "Setting to truncate text in timeline"
|
||||
},
|
||||
"Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content." : {
|
||||
"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" : {
|
||||
@@ -1599,6 +1674,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1611,6 +1689,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1632,6 +1713,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1674,18 +1758,9 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1695,6 +1770,9 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1725,9 +1803,15 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1743,9 +1827,6 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1755,17 +1836,23 @@
|
||||
"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 only for visually impaired users"
|
||||
"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."
|
||||
@@ -1814,6 +1901,9 @@
|
||||
},
|
||||
"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"
|
||||
|
||||
Binary file not shown.
@@ -66,6 +66,22 @@
|
||||
<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.
@@ -66,6 +66,22 @@
|
||||
<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.
@@ -58,6 +58,20 @@
|
||||
<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.
@@ -66,6 +66,22 @@
|
||||
<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.
@@ -82,6 +82,26 @@
|
||||
<string>Importa</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$@ i %1$d inna osoba podała dalej</string>
|
||||
<key>few</key>
|
||||
<string>%2$@ i %1$d inne osoby podały dalej</string>
|
||||
<key>many</key>
|
||||
<string>%2$@ i %1$d innych osób podało dalej</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ i %1$d innej osoby podało dalej</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reacted_tagged_in_3</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
@@ -213,13 +233,13 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ i %1$d inna osoba opublikowała notatkę, w której Cię oznaczono</string>
|
||||
<string>%2$@ i %1$d inna osoba podała dalej notatkę, w której cię oznaczono</string>
|
||||
<key>few</key>
|
||||
<string>%2$@ i %1$d inne osoby opublikowały notatkę, w której Cię oznaczono</string>
|
||||
<string>%2$@ i %1$d inne osoby podały dalej notatkę, w której cię oznaczono</string>
|
||||
<key>many</key>
|
||||
<string>%2$@ i %1$d innych osób opublikowało notatkę, w której Cię oznaczono</string>
|
||||
<string>%2$@ i %1$d innych osób podało dalej notatkę, w której cię oznaczono</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ i %1$d innej osoby opublikowało wiadomość, w której jesteś oznaczony</string>
|
||||
<string>%2$@ i %1$d innej osoby podało dalej notatkę, w której cię oznaczono</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reposted_your_note_3</key>
|
||||
@@ -233,13 +253,13 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ i %1$d inna osoba opublikowała twoją notatkę</string>
|
||||
<string>%2$@ i %1$d inna osoba podała dalej twoją notatkę</string>
|
||||
<key>few</key>
|
||||
<string>%2$@ i %1$d inne osoby opublikowały twoją notatkę</string>
|
||||
<string>%2$@ i %1$d inne osoby podały dalej twoją notatkę</string>
|
||||
<key>many</key>
|
||||
<string>%2$@ i %1$d innych osób opublikowały twoją notatkę</string>
|
||||
<string>%2$@ i %1$d innych osób podało dalej twoją notatkę</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ i %1$d innej osoby opublikowało twoją notatkę</string>
|
||||
<string>%2$@ i %1$d innej osoby podało dalej twoją notatkę</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reposted_your_profile_3</key>
|
||||
@@ -253,13 +273,13 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%2$@ i %1$d inna osoba opublikowała twój profil</string>
|
||||
<string>%2$@ i %1$d inna osoba podała dalej twój profil</string>
|
||||
<key>few</key>
|
||||
<string>%2$@ i %1$d inne osoby opublikowały twój profil</string>
|
||||
<string>%2$@ i %1$d inne osoby podały dalej twój profil</string>
|
||||
<key>many</key>
|
||||
<string>%2$@ i %1$d innych osób opublikowało twój profil</string>
|
||||
<string>%2$@ i %1$d innych osób podało dalej twój profil</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ i %1$d innych osób opublikowało twój profil</string>
|
||||
<string>%2$@ i %1$d innych osoby podało dalej twój profil</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reposts_count</key>
|
||||
@@ -279,7 +299,7 @@
|
||||
<key>many</key>
|
||||
<string>Podanych dalej</string>
|
||||
<key>other</key>
|
||||
<string>Podanych dalej</string>
|
||||
<string>Podane dalej</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>quoted_reposts_count</key>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -74,6 +74,24 @@
|
||||
<string>importações</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$@ e mais %1$d republicaram</string>
|
||||
<key>many</key>
|
||||
<string>%2$@ e mais %1$d republicaram</string>
|
||||
<key>other</key>
|
||||
<string>%2$@ e mais %1$d republicaram</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>reacted_tagged_in_3</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
Binary file not shown.
@@ -58,6 +58,20 @@
|
||||
<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>
|
||||
|
||||
@@ -270,6 +270,7 @@ final class EditPictureControlTests: XCTestCase {
|
||||
XCTAssertEqual(view_model.state.step, SelectionState.Step.ready)
|
||||
}
|
||||
|
||||
/*
|
||||
@MainActor
|
||||
func testEditPictureControlFirstTimeSetup() async {
|
||||
var current_image_url: URL? = nil
|
||||
@@ -325,6 +326,7 @@ final class EditPictureControlTests: XCTestCase {
|
||||
sleep(2) // Wait a bit for things to load
|
||||
assertSnapshot(matching: hostView, as: .image(on: .iPhoneSe(.portrait)))
|
||||
}
|
||||
*/
|
||||
|
||||
// MARK: Mock classes
|
||||
|
||||
|
||||
50
damusTests/LargeEventTests.swift
Normal file
50
damusTests/LargeEventTests.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// LargeEventTests.swift
|
||||
// damusTests
|
||||
//
|
||||
// Created by William Casarin on 2023-08-05.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import damus
|
||||
|
||||
final class LargeEventTests: XCTestCase {
|
||||
|
||||
func testLongPost() throws {
|
||||
let json = "[\"EVENT\",\"subid\",\(test_failing_nostr_report)]"
|
||||
let resp = NostrResponse.owned_from_json(json: json)
|
||||
|
||||
XCTAssertNotNil(resp)
|
||||
guard let resp,
|
||||
case .event(let subid, let ev) = resp
|
||||
else {
|
||||
XCTAssertFalse(true)
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(subid, "subid")
|
||||
XCTAssertTrue(ev.should_show_event)
|
||||
XCTAssertTrue(!ev.too_big)
|
||||
XCTAssertTrue(should_show_event(state: test_damus_state, ev: ev))
|
||||
XCTAssertTrue(validate_event(ev: ev) == .ok)
|
||||
}
|
||||
|
||||
func testIsHellthread() throws {
|
||||
let json = "[\"EVENT\",\"subid\",\(test_failing_nostr_report)]"
|
||||
let resp = NostrResponse.owned_from_json(json: json)
|
||||
|
||||
XCTAssertNotNil(resp)
|
||||
guard let resp,
|
||||
case .event(let subid, let ev) = resp
|
||||
else {
|
||||
XCTAssertFalse(true)
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(subid, "subid")
|
||||
XCTAssertTrue(ev.should_show_event)
|
||||
XCTAssertTrue(ev.is_hellthread(max_pubkeys: 10))
|
||||
XCTAssertTrue(validate_event(ev: ev) == .ok)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,13 +17,16 @@ final class LocalizationUtilTests: XCTestCase {
|
||||
let keys = [
|
||||
["followers_count", "Followers", "Follower", "Followers"],
|
||||
["following_count", "Following", "Following", "Following"],
|
||||
["hellthread_notifications_disabled", "Hide notifications that tag more than 0 profiles", "Hide notifications that tag more than 1 profile", "Hide notifications that tag more than 2 profiles"],
|
||||
["imports_count", "Imports", "Import", "Imports"],
|
||||
["quoted_reposts_count", "Quotes", "Quote", "Quotes"],
|
||||
["reactions_count", "Reactions", "Reaction", "Reactions"],
|
||||
["relays_count", "Relays", "Relay", "Relays"],
|
||||
["reposts_count", "Reposts", "Repost", "Reposts"],
|
||||
["sats", "sats", "sat", "sats"],
|
||||
["zaps_count", "Zaps", "Zap", "Zaps"],
|
||||
["word_count", "0 Words", "1 Word", "2 Words"]
|
||||
["users_talking_about_it", "0 users talking about it", "1 user talking about it", "2 users talking about it"],
|
||||
["word_count", "0 Words", "1 Word", "2 Words"],
|
||||
["zaps_count", "Zaps", "Zap", "Zaps"]
|
||||
]
|
||||
|
||||
for key in keys {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// LongPostTests.swift
|
||||
// damusTests
|
||||
//
|
||||
// Created by William Casarin on 2023-08-05.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import damus
|
||||
|
||||
final class LongPostTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testLongPost() throws {
|
||||
let contacts = Contacts(our_pubkey: test_keypair.pubkey)
|
||||
let json = "[\"EVENT\",\"subid\",\(test_failing_nostr_report)]"
|
||||
let resp = NostrResponse.owned_from_json(json: json)
|
||||
|
||||
XCTAssertNotNil(resp)
|
||||
guard let resp,
|
||||
case .event(let subid, let ev) = resp
|
||||
else {
|
||||
XCTAssertFalse(true)
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(subid, "subid")
|
||||
XCTAssertTrue(ev.should_show_event)
|
||||
XCTAssertTrue(!ev.too_big)
|
||||
XCTAssertTrue(should_show_event(state: test_damus_state, ev: ev))
|
||||
XCTAssertTrue(validate_event(ev: ev) == .ok )
|
||||
}
|
||||
|
||||
func testPerformanceExample() throws {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -87,7 +87,7 @@ final class WalletConnectTests: XCTestCase {
|
||||
let pool = RelayPool(ndb: .empty)
|
||||
let box = PostBox(pool: pool)
|
||||
|
||||
nwc_pay(url: nwc, pool: pool, post: box, invoice: "invoice")
|
||||
WalletConnect.pay(url: nwc, pool: pool, post: box, invoice: "invoice")
|
||||
|
||||
XCTAssertEqual(pool.our_descriptors.count, 0)
|
||||
XCTAssertEqual(pool.all_descriptors.count, 1)
|
||||
|
||||
@@ -14,6 +14,9 @@ import CryptoKit
|
||||
|
||||
let MAX_NOTE_SIZE: Int = 2 << 18
|
||||
|
||||
// Default threshold of the hellthread pubkey tag count setting if it is not set.
|
||||
let DEFAULT_HELLTHREAD_MAX_PUBKEYS: Int = 10
|
||||
|
||||
struct NdbStr {
|
||||
let note: NdbNote
|
||||
let str: UnsafePointer<CChar>
|
||||
@@ -299,6 +302,15 @@ extension NdbNote {
|
||||
return !too_big
|
||||
}
|
||||
|
||||
func is_hellthread(max_pubkeys: Int) -> Bool {
|
||||
switch known_kind {
|
||||
case .text, .boost, .like, .zap:
|
||||
Set(referenced_pubkeys).count > max_pubkeys
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
func get_blocks(keypair: Keypair) -> Blocks {
|
||||
return parse_note_content(content: .init(note: self, keypair: keypair))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user