Compare commits
81 Commits
rust-nostr
...
unicode-16
| Author | SHA1 | Date | |
|---|---|---|---|
|
d26804d665
|
|||
|
|
0c148c8a1f | ||
|
|
3cccb2eb6b | ||
|
|
af4949e26a | ||
|
|
5bb7e95624 | ||
| 814bcf694f | |||
|
|
b0382c61b1 | ||
| e2650a8bfc | |||
|
|
ac39a53b33 | ||
|
|
fb356cdf0b | ||
|
|
238e89ce16 | ||
|
|
6e041c79f7 | ||
|
|
6ef4b60d14 | ||
|
|
054bec2d9a
|
||
|
|
943a46a343
|
||
|
|
17381f6b94
|
||
|
|
18c88de407
|
||
|
|
99d21fc89b
|
||
|
|
db5c86a0d1
|
||
|
|
736ec6fb9e
|
||
|
|
fa2327325a
|
||
|
|
4fdf048040
|
||
|
|
273538bd36
|
||
|
|
0980c8c040
|
||
|
|
f0bfdeaa5a
|
||
|
|
ab7c5c18e3
|
||
|
|
6ae95ab5ec
|
||
|
eec630b2b0
|
|||
|
|
2b3d86968d | ||
|
|
935a6cae7a | ||
|
|
d4940d8386 | ||
|
71ec18f6c6
|
|||
|
caa4bfe864
|
|||
|
a87ba73160
|
|||
|
|
4324b185fe | ||
|
|
1ab9b30b85 | ||
|
|
81cf6ad297 | ||
|
|
1b3be3a13b | ||
|
|
3a2ce04d6b | ||
|
|
981821a6bc | ||
|
|
98f83769bd | ||
|
|
7684f53281 | ||
|
|
15af686a58 | ||
|
|
aad8f9e8d4 | ||
| b2ee44c0ab | |||
|
|
a696ac5084 | ||
|
|
28237c3a63 | ||
|
|
1cae4640c0 | ||
|
|
21a07d54cb | ||
|
|
1efd07b852 | ||
|
|
e5eb7d44a2 | ||
|
|
ec9a89ee4d | ||
|
|
4741c2a3e8 | ||
|
|
0111c5e2dc | ||
|
|
bed4e00b53 | ||
|
|
bf14d7138a | ||
|
|
0c5da08a42 | ||
|
|
a6e123e928 | ||
|
|
69b1173e08 | ||
|
|
c3326213e9 | ||
|
325109d7b8
|
|||
|
|
f16d76605b
|
||
|
|
3eee1b205a
|
||
|
|
9545c6446d
|
||
|
|
40a75f65ab
|
||
|
|
98f42c9896
|
||
|
|
5c22989675
|
||
|
999f16f6a4
|
|||
|
|
3f5fd6eee8
|
||
|
|
7c195aa75c
|
||
|
|
2071efc129
|
||
|
|
9db2e9b464
|
||
|
|
5f6cb568ff
|
||
|
|
045399a065
|
||
|
|
1b526143d0
|
||
|
|
8a046c0d1b
|
||
|
|
2893e4234d
|
||
|
|
973a5ce2cb
|
||
|
|
1e81e90341
|
||
| 9e7943e0e9 | |||
|
|
d4d17fcbad |
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -6,6 +6,7 @@ _[Please provide a summary of the changes in this PR.]_
|
||||
|
||||
- [ ] I have read (or I am familiar with) the [Contribution Guidelines](../docs/CONTRIBUTING.md)
|
||||
- [ ] I have tested the changes in this PR
|
||||
- [ ] I have opened or referred to an existing github issue related to this change.
|
||||
- [ ] My PR is either small, or I have split it into smaller logical commits that are easier to review
|
||||
- [ ] I have added the signoff line to all my commits. See [Signing off your work](../docs/CONTRIBUTING.md#sign-your-work---the-developers-certificate-of-origin)
|
||||
- [ ] I have added appropriate changelog entries for the changes in this PR. See [Adding changelog entries](../docs/CONTRIBUTING.md#add-changelog-changed-changelog-fixed-etc)
|
||||
|
||||
5
ACKNOWLEDGEMENTS.md
Normal file
5
ACKNOWLEDGEMENTS.md
Normal file
@@ -0,0 +1,5 @@
|
||||
### Acknowledgements and licenses
|
||||
|
||||
1. This product contains code derived from [Nostr SDK iOS](https://github.com/nostr-sdk/nostr-sdk-ios). [License](https://github.com/nostr-sdk/nostr-sdk-ios/blob/40df800c6749d7ce0b6fd7328e76cbc0dc71c87b/LICENSE)
|
||||
2. This product includes software developed by the "Marcin Krzyzanowski" (http://krzyzanowskim.com/). [License](https://github.com/krzyzanowskim/CryptoSwift/blob/e74bbbfbef939224b242ae7c342a90e60b88b5ce/LICENSE)
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ A twitter-like [nostr][nostr] client for iPhone, iPad and MacOS.
|
||||
|
||||
[nostr]: https://github.com/fiatjaf/nostr
|
||||
|
||||
## How is Damus better than twitter?
|
||||
## How is Damus better than X/Twitter?
|
||||
There are no toxic algorithms.\
|
||||
You can send or receive zaps (satoshis) without asking for permission.\
|
||||
[There is no central database](https://fiatjaf.com/nostr.html). Therefore, Damus is censorship resistant.\
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
|
||||
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
|
||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
|
||||
3A96E3FE2D6BCE3800AE1630 /* RepostedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
|
||||
@@ -188,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 */; };
|
||||
@@ -1045,6 +1047,12 @@
|
||||
D703D7B62C67118200A400EA /* String+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9472A9AD44700DC3548 /* String+extension.swift */; };
|
||||
D703D7B72C67118F00A400EA /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
|
||||
D703D7B82C6711A000A400EA /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; };
|
||||
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */; };
|
||||
D706C5B02D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */; };
|
||||
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */; };
|
||||
D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5B62D602A050027C627 /* QueueableNotify.swift */; };
|
||||
D706C5B82D602A110027C627 /* QueueableNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5B62D602A050027C627 /* QueueableNotify.swift */; };
|
||||
D706C5B92D602A110027C627 /* QueueableNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5B62D602A050027C627 /* QueueableNotify.swift */; };
|
||||
D70A3B172B02DCE5008BD568 /* NotificationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */; };
|
||||
D70D90982CDED61800CD0534 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D70D90972CDED61800CD0534 /* CodeScanner */; };
|
||||
D70D909C2CDED7B200CD0534 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D70D909B2CDED7B200CD0534 /* CodeScanner */; };
|
||||
@@ -1448,9 +1456,9 @@
|
||||
D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
|
||||
D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
|
||||
D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
|
||||
D74EA0932D2E77B9002290DD /* LoadableThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */; };
|
||||
D74EA0942D2E77B9002290DD /* LoadableThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */; };
|
||||
D74EA0952D2E77B9002290DD /* LoadableThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */; };
|
||||
D74EA0932D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||
D74EA0952D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
|
||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
||||
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
|
||||
@@ -1500,9 +1508,6 @@
|
||||
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */; };
|
||||
D7B76C902C825042003A16CB /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
|
||||
D7B76C912C82507F003A16CB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; };
|
||||
D7BEE6F32D37AE1B00CF659F /* NostrSDK in Frameworks */ = {isa = PBXBuildFile; productRef = D7BEE6F22D37AE1B00CF659F /* NostrSDK */; };
|
||||
D7BEE6F52D37B20400CF659F /* NostrSDK in Frameworks */ = {isa = PBXBuildFile; productRef = D7BEE6F42D37B20400CF659F /* NostrSDK */; };
|
||||
D7BEE6F72D37B21400CF659F /* NostrSDK in Frameworks */ = {isa = PBXBuildFile; productRef = D7BEE6F62D37B21400CF659F /* NostrSDK */; };
|
||||
D7BEE6F92D37B37400CF659F /* DraftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BEE6F82D37B37400CF659F /* DraftTests.swift */; };
|
||||
D7C48C0B2D12DE0C00A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */; };
|
||||
D7C48C0D2D12E34900A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0C2D12E34900A3BACF /* SwiftyCrop */; };
|
||||
@@ -1607,6 +1612,19 @@
|
||||
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
|
||||
D7D68FF92C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
|
||||
D7D68FFA2C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
|
||||
D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */; };
|
||||
D7DB1FDF2D5A78CE00CF06DA /* NIP44.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */; };
|
||||
D7DB1FE02D5A78CE00CF06DA /* NIP44.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */; };
|
||||
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */; };
|
||||
D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */; };
|
||||
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */; };
|
||||
D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */; };
|
||||
D7DB1FEE2D5AC51B00CF06DA /* NIP44v2EncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */; };
|
||||
D7DB1FF12D5AC5D700CF06DA /* nip44.vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */; };
|
||||
D7DB1FF32D5AC5EA00CF06DA /* LICENSES in Resources */ = {isa = PBXBuildFile; fileRef = D7DB1FF22D5AC5E400CF06DA /* LICENSES */; };
|
||||
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
|
||||
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
||||
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
||||
@@ -1789,6 +1807,7 @@
|
||||
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedTests.swift; sourceTree = "<group>"; };
|
||||
3A994C4C2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A994C4D2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A994C4E2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -2406,6 +2425,8 @@
|
||||
D703D7222C66E47100A400EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D703D7262C66E47100A400EA /* highlighter action extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "highlighter action extension.entitlements"; sourceTree = "<group>"; };
|
||||
D703D72A2C66F29500A400EA /* getSelection.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = getSelection.js; sourceTree = "<group>"; };
|
||||
D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoSaveIndicatorView.swift; sourceTree = "<group>"; };
|
||||
D706C5B62D602A050027C627 /* QueueableNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueableNotify.swift; sourceTree = "<group>"; };
|
||||
D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFormatter.swift; sourceTree = "<group>"; };
|
||||
D7100C552B76F8E600C59298 /* PurpleViewPrimitives.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleViewPrimitives.swift; sourceTree = "<group>"; };
|
||||
D7100C572B76FC8400C59298 /* MarketingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketingContentView.swift; sourceTree = "<group>"; };
|
||||
@@ -2439,7 +2460,7 @@
|
||||
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Zaps+.swift"; sourceTree = "<group>"; };
|
||||
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
|
||||
D74EA08D2D2E271E002290DD /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableThreadView.swift; sourceTree = "<group>"; };
|
||||
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableNostrEventView.swift; sourceTree = "<group>"; };
|
||||
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
|
||||
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
|
||||
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
|
||||
@@ -2480,6 +2501,11 @@
|
||||
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
|
||||
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; };
|
||||
D7D68FF82C9E01B60015A515 /* KFClickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFClickable.swift; sourceTree = "<group>"; };
|
||||
D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44.swift; sourceTree = "<group>"; };
|
||||
D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44v2EncryptionTests.swift; sourceTree = "<group>"; };
|
||||
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = nip44.vectors.json; sourceTree = "<group>"; };
|
||||
D7DB1FF22D5AC5E400CF06DA /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSES; sourceTree = "<group>"; };
|
||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Undistractor.swift; sourceTree = "<group>"; };
|
||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
||||
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
|
||||
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
|
||||
@@ -2523,8 +2549,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D7BEE6F32D37AE1B00CF659F /* NostrSDK in Frameworks */,
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */,
|
||||
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
|
||||
D70D90982CDED61800CD0534 /* CodeScanner in Frameworks */,
|
||||
D7C48C0B2D12DE0C00A3BACF /* SwiftyCrop in Frameworks */,
|
||||
@@ -2554,8 +2580,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D7BEE6F52D37B20400CF659F /* NostrSDK in Frameworks */,
|
||||
82D6FC862CD9A4A600C925F4 /* MarkdownUI in Frameworks */,
|
||||
D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */,
|
||||
82D6FC8A2CD9A54600C925F4 /* SwipeActions in Frameworks */,
|
||||
D7F360292CEBBE34009D34DA /* CodeScanner in Frameworks */,
|
||||
D7C48C0D2D12E34900A3BACF /* SwiftyCrop in Frameworks */,
|
||||
@@ -2571,7 +2597,7 @@
|
||||
files = (
|
||||
D703D7AF2C670FB700A400EA /* MarkdownUI in Frameworks */,
|
||||
D73E5F9D2C6AA8E3007EB227 /* SwipeActions in Frameworks */,
|
||||
D7BEE6F72D37B21400CF659F /* NostrSDK in Frameworks */,
|
||||
D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */,
|
||||
D73E5F762C6A997E007EB227 /* EmojiPicker in Frameworks */,
|
||||
D703D7192C66E47100A400EA /* UniformTypeIdentifiers.framework in Frameworks */,
|
||||
D7C48C0F2D12E35600A3BACF /* SwiftyCrop in Frameworks */,
|
||||
@@ -2585,8 +2611,10 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C5726BA2D72C6FA00E7FF82 /* Kingfisher in Frameworks */,
|
||||
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */,
|
||||
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
|
||||
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -2753,6 +2781,8 @@
|
||||
children = (
|
||||
4C0C03982A61E27B0098B3B8 /* bool_setting.wasm */,
|
||||
4C0C03972A61E27B0098B3B8 /* primal.wasm */,
|
||||
D7DB1FF22D5AC5E400CF06DA /* LICENSES */,
|
||||
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */,
|
||||
);
|
||||
name = Fixtures;
|
||||
sourceTree = "<group>";
|
||||
@@ -3155,7 +3185,7 @@
|
||||
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */,
|
||||
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
|
||||
D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */,
|
||||
D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */,
|
||||
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -3217,6 +3247,7 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */,
|
||||
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */,
|
||||
E04A37C52B544F090029650D /* URIParsing.swift */,
|
||||
4C1D4FB02A7958E60024F453 /* VersionInfo.swift */,
|
||||
@@ -3332,6 +3363,7 @@
|
||||
4CA3529C2A76AE47003BB08B /* Notify */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D706C5B62D602A050027C627 /* QueueableNotify.swift */,
|
||||
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */,
|
||||
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */,
|
||||
4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */,
|
||||
@@ -3579,6 +3611,7 @@
|
||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
|
||||
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
|
||||
4C45E5002BED4CE10025A428 /* NIP10 */,
|
||||
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
|
||||
@@ -3616,6 +3649,7 @@
|
||||
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */,
|
||||
D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */,
|
||||
E06336A72B7582D600A88E6B /* Assets */,
|
||||
D72A2D032AD9C165002AFF62 /* Mocking */,
|
||||
@@ -3659,6 +3693,7 @@
|
||||
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */,
|
||||
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
|
||||
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
|
||||
3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -3746,6 +3781,7 @@
|
||||
4CF0ABF42985CD4200D66079 /* Posting */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */,
|
||||
4CF0ABF52985CD5500D66079 /* UserSearch.swift */,
|
||||
);
|
||||
path = Posting;
|
||||
@@ -3946,6 +3982,14 @@
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7DB1FDC2D5A77E500CF06DA /* NIP44 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */,
|
||||
);
|
||||
path = NIP44;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E06336A72B7582D600A88E6B /* Assets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -4012,7 +4056,7 @@
|
||||
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
|
||||
D70D90972CDED61800CD0534 /* CodeScanner */,
|
||||
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */,
|
||||
D7BEE6F22D37AE1B00CF659F /* NostrSDK */,
|
||||
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */,
|
||||
);
|
||||
productName = damus;
|
||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||
@@ -4079,7 +4123,7 @@
|
||||
82D6FC892CD9A54600C925F4 /* SwipeActions */,
|
||||
D7F360282CEBBE34009D34DA /* CodeScanner */,
|
||||
D7C48C0C2D12E34900A3BACF /* SwiftyCrop */,
|
||||
D7BEE6F42D37B20400CF659F /* NostrSDK */,
|
||||
D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */,
|
||||
);
|
||||
productName = "share extension";
|
||||
productReference = 82D6FA972CD9820500C925F4 /* ShareExtension.appex */;
|
||||
@@ -4108,7 +4152,7 @@
|
||||
D73E5F9C2C6AA8E3007EB227 /* SwipeActions */,
|
||||
D70D909B2CDED7B200CD0534 /* CodeScanner */,
|
||||
D7C48C0E2D12E35600A3BACF /* SwiftyCrop */,
|
||||
D7BEE6F62D37B21400CF659F /* NostrSDK */,
|
||||
D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */,
|
||||
);
|
||||
productName = "highlighter action extension";
|
||||
productReference = D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */;
|
||||
@@ -4131,6 +4175,8 @@
|
||||
packageProductDependencies = (
|
||||
D789D11F2AFEFBF20083A7AB /* secp256k1 */,
|
||||
D7EDED302B1290B80018B19C /* MarkdownUI */,
|
||||
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */,
|
||||
4C5726B92D72C6FA00E7FF82 /* Kingfisher */,
|
||||
);
|
||||
productName = DamusNotificationService;
|
||||
productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */;
|
||||
@@ -4218,7 +4264,7 @@
|
||||
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */,
|
||||
D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */,
|
||||
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */,
|
||||
D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */,
|
||||
D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */,
|
||||
);
|
||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -4258,7 +4304,9 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E06336AB2B75850100A88E6B /* img_with_location.jpeg in Resources */,
|
||||
D7DB1FF12D5AC5D700CF06DA /* nip44.vectors.json in Resources */,
|
||||
4C0C039A2A61E27B0098B3B8 /* bool_setting.wasm in Resources */,
|
||||
D7DB1FF32D5AC5EA00CF06DA /* LICENSES in Resources */,
|
||||
4C0C03992A61E27B0098B3B8 /* primal.wasm in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -4511,6 +4559,7 @@
|
||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
|
||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
||||
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
|
||||
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */,
|
||||
D72E12782BEED22500F4F781 /* Array.swift in Sources */,
|
||||
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */,
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
|
||||
@@ -4586,6 +4635,7 @@
|
||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
|
||||
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
|
||||
4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */,
|
||||
D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */,
|
||||
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */,
|
||||
4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */,
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
|
||||
@@ -4670,6 +4720,7 @@
|
||||
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
|
||||
D7EFBA372CC322F300F45588 /* DamusVideoControlsView.swift in Sources */,
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
|
||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
||||
4C1253682A76D2470004F4B8 /* MuteNotify.swift in Sources */,
|
||||
@@ -4679,7 +4730,7 @@
|
||||
4CA927612A290E340098A105 /* EventShell.swift in Sources */,
|
||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
|
||||
D74EA0942D2E77B9002290DD /* LoadableThreadView.swift in Sources */,
|
||||
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
||||
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
|
||||
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
|
||||
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */,
|
||||
@@ -4718,6 +4769,7 @@
|
||||
50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */,
|
||||
D7EDED212B117DCA0018B19C /* SequenceUtils.swift in Sources */,
|
||||
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */,
|
||||
D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
||||
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
|
||||
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
||||
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
|
||||
@@ -4831,10 +4883,12 @@
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
||||
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
|
||||
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */,
|
||||
D7DB1FEE2D5AC51B00CF06DA /* NIP44v2EncryptionTests.swift in Sources */,
|
||||
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
|
||||
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
|
||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
||||
3A96E3FE2D6BCE3800AE1630 /* RepostedTests.swift in Sources */,
|
||||
4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */,
|
||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */,
|
||||
@@ -4941,13 +4995,14 @@
|
||||
82D6FAE62CD99F7900C925F4 /* LocalNotificationNotify.swift in Sources */,
|
||||
82D6FAE72CD99F7900C925F4 /* LoginNotify.swift in Sources */,
|
||||
82D6FAE82CD99F7900C925F4 /* LogoutNotify.swift in Sources */,
|
||||
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||
82D6FAE92CD99F7900C925F4 /* NewMutesNotify.swift in Sources */,
|
||||
82D6FAEA2CD99F7900C925F4 /* NewUnmutesNotify.swift in Sources */,
|
||||
82D6FAEB2CD99F7900C925F4 /* Notify.swift in Sources */,
|
||||
82D6FAEC2CD99F7900C925F4 /* OnlyZapsNotify.swift in Sources */,
|
||||
82D6FAED2CD99F7900C925F4 /* PostNotify.swift in Sources */,
|
||||
82D6FAEE2CD99F7900C925F4 /* PresentSheetNotify.swift in Sources */,
|
||||
D74EA0932D2E77B9002290DD /* LoadableThreadView.swift in Sources */,
|
||||
D74EA0932D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
||||
82D6FAEF2CD99F7900C925F4 /* ProfileUpdatedNotify.swift in Sources */,
|
||||
82D6FAF02CD99F7900C925F4 /* ReportNotify.swift in Sources */,
|
||||
82D6FAF12CD99F7900C925F4 /* ScrollToTopNotify.swift in Sources */,
|
||||
@@ -4976,6 +5031,7 @@
|
||||
82D6FB082CD99F7900C925F4 /* UserStatusSheet.swift in Sources */,
|
||||
82D6FB092CD99F7900C925F4 /* SearchHeaderView.swift in Sources */,
|
||||
82D6FB0A2CD99F7900C925F4 /* DamusGradient.swift in Sources */,
|
||||
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */,
|
||||
82D6FB0B2CD99F7900C925F4 /* AlbyGradient.swift in Sources */,
|
||||
82D6FB0C2CD99F7900C925F4 /* GoldSupportGradient.swift in Sources */,
|
||||
82D6FB0D2CD99F7900C925F4 /* PinkGradient.swift in Sources */,
|
||||
@@ -5036,6 +5092,7 @@
|
||||
82D6FB452CD99F7900C925F4 /* InputDismissKeyboard.swift in Sources */,
|
||||
82D6FB462CD99F7900C925F4 /* Constants.swift in Sources */,
|
||||
82D6FB472CD99F7900C925F4 /* LinkView.swift in Sources */,
|
||||
D7DB1FDF2D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
||||
82D6FB482CD99F7900C925F4 /* PreviewCache.swift in Sources */,
|
||||
82D6FB492CD99F7900C925F4 /* Theme.swift in Sources */,
|
||||
82D6FB4A2CD99F7900C925F4 /* NIP05.swift in Sources */,
|
||||
@@ -5306,6 +5363,7 @@
|
||||
82D6FC522CD99F7900C925F4 /* DMView.swift in Sources */,
|
||||
82D6FC532CD99F7900C925F4 /* EmptyTimelineView.swift in Sources */,
|
||||
82D6FC542CD99F7900C925F4 /* EmptyUserSearchView.swift in Sources */,
|
||||
D706C5B82D602A110027C627 /* QueueableNotify.swift in Sources */,
|
||||
82D6FC552CD99F7900C925F4 /* EventView.swift in Sources */,
|
||||
82D6FC562CD99F7900C925F4 /* EventDetailView.swift in Sources */,
|
||||
82D6FC572CD99F7900C925F4 /* FollowButtonView.swift in Sources */,
|
||||
@@ -5393,6 +5451,7 @@
|
||||
D73E5E412C6A97F4007EB227 /* GoldSupportGradient.swift in Sources */,
|
||||
D73E5E422C6A97F4007EB227 /* PinkGradient.swift in Sources */,
|
||||
D73E5E432C6A97F4007EB227 /* GrayGradient.swift in Sources */,
|
||||
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */,
|
||||
D73E5E442C6A97F4007EB227 /* DamusLogoGradient.swift in Sources */,
|
||||
D73E5E452C6A97F4007EB227 /* DamusBackground.swift in Sources */,
|
||||
D73E5E462C6A97F4007EB227 /* DamusLightGradient.swift in Sources */,
|
||||
@@ -5481,7 +5540,7 @@
|
||||
D73E5E9C2C6A97F4007EB227 /* Reply.swift in Sources */,
|
||||
D73E5E9D2C6A97F4007EB227 /* SearchModel.swift in Sources */,
|
||||
D73E5E9E2C6A97F4007EB227 /* NostrFilter+Hashable.swift in Sources */,
|
||||
D74EA0952D2E77B9002290DD /* LoadableThreadView.swift in Sources */,
|
||||
D74EA0952D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
||||
D73E5F912C6AA71B007EB227 /* InputDismissKeyboard.swift in Sources */,
|
||||
D73E5E9F2C6A97F4007EB227 /* CreateAccountModel.swift in Sources */,
|
||||
D73E5EA12C6A97F4007EB227 /* SignalModel.swift in Sources */,
|
||||
@@ -5508,6 +5567,7 @@
|
||||
D73E5EB42C6A97F4007EB227 /* NoteContent.swift in Sources */,
|
||||
D73E5EB52C6A97F4007EB227 /* LongformEvent.swift in Sources */,
|
||||
D73E5EB62C6A97F4007EB227 /* PushNotificationClient.swift in Sources */,
|
||||
D706C5B92D602A110027C627 /* QueueableNotify.swift in Sources */,
|
||||
D71AD8FD2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
|
||||
D73E5EB72C6A97F4007EB227 /* HighlightEvent.swift in Sources */,
|
||||
D73E5EB82C6A97F4007EB227 /* RelayConnection.swift in Sources */,
|
||||
@@ -5767,6 +5827,8 @@
|
||||
D703D7712C670B6D00A400EA /* NdbProfile.swift in Sources */,
|
||||
D703D7A22C670E1A00A400EA /* list.c in Sources */,
|
||||
D703D7A42C670E3C00A400EA /* midl.c in Sources */,
|
||||
D7DB1FE02D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
||||
D706C5B02D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||
D703D7982C670DF200A400EA /* utf8.c in Sources */,
|
||||
D703D78B2C670C9500A400EA /* MakeZapRequest.swift in Sources */,
|
||||
D703D7862C670C6500A400EA /* NewUnmutesNotify.swift in Sources */,
|
||||
@@ -6166,7 +6228,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -6189,7 +6251,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||
MARKETING_VERSION = 1.10;
|
||||
MARKETING_VERSION = 1.13;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -6235,7 +6297,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -6254,7 +6316,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||
MARKETING_VERSION = 1.10;
|
||||
MARKETING_VERSION = 1.13;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -6273,8 +6335,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;
|
||||
@@ -6301,9 +6363,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;
|
||||
@@ -6325,8 +6387,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;
|
||||
@@ -6353,9 +6415,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;
|
||||
@@ -6441,7 +6503,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;
|
||||
@@ -6460,7 +6521,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;
|
||||
@@ -6479,7 +6539,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;
|
||||
@@ -6494,7 +6553,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;
|
||||
@@ -6513,7 +6571,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;
|
||||
@@ -6528,7 +6585,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;
|
||||
@@ -6548,7 +6604,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;
|
||||
@@ -6563,7 +6618,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;
|
||||
@@ -6582,7 +6636,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;
|
||||
@@ -6597,7 +6650,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;
|
||||
@@ -6617,7 +6669,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;
|
||||
@@ -6632,7 +6683,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;
|
||||
@@ -6718,7 +6768,7 @@
|
||||
repositoryURL = "https://github.com/tyiu/EmojiPicker.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.1.1;
|
||||
minimumVersion = 0.2.0;
|
||||
};
|
||||
};
|
||||
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||
@@ -6777,14 +6827,6 @@
|
||||
minimumVersion = 1.14.1;
|
||||
};
|
||||
};
|
||||
D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/rust-nostr/nostr-sdk-swift";
|
||||
requirement = {
|
||||
kind = revision;
|
||||
revision = 27711a03ea7d977162595eea1d9b2d5a45f0b628;
|
||||
};
|
||||
};
|
||||
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/benedom/SwiftyCrop";
|
||||
@@ -6793,6 +6835,14 @@
|
||||
revision = 454d0a0d4faf6f3a19c8d817ab9d7d27524bd79f;
|
||||
};
|
||||
};
|
||||
D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git";
|
||||
requirement = {
|
||||
kind = revision;
|
||||
revision = e74bbbfbef939224b242ae7c342a90e60b88b5ce;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -6811,6 +6861,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" */;
|
||||
@@ -6906,21 +6961,6 @@
|
||||
package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;
|
||||
productName = SnapshotTesting;
|
||||
};
|
||||
D7BEE6F22D37AE1B00CF659F /* NostrSDK */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */;
|
||||
productName = NostrSDK;
|
||||
};
|
||||
D7BEE6F42D37B20400CF659F /* NostrSDK */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */;
|
||||
productName = NostrSDK;
|
||||
};
|
||||
D7BEE6F62D37B21400CF659F /* NostrSDK */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7BEE6F12D37AE1B00CF659F /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */;
|
||||
productName = NostrSDK;
|
||||
};
|
||||
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */;
|
||||
@@ -6936,6 +6976,26 @@
|
||||
package = D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */;
|
||||
productName = SwiftyCrop;
|
||||
};
|
||||
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
|
||||
productName = CryptoSwift;
|
||||
};
|
||||
D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
|
||||
productName = CryptoSwift;
|
||||
};
|
||||
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
|
||||
productName = CryptoSwift;
|
||||
};
|
||||
D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
|
||||
productName = CryptoSwift;
|
||||
};
|
||||
D7EDED242B117F7C0018B19C /* MarkdownUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "fa2b0ad84b4bd1a962ffbe49810548db7c9d7131f4a1fd4b4af06ff4c6de0a44",
|
||||
"originHash" : "06318d35ee2e6bd681b95591e67da33a9461b48a3c652e58bd9d1a6f0d82bdac",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "codescanner",
|
||||
@@ -9,13 +9,21 @@
|
||||
"revision" : "9fa582f4b36c69c2a55bff5fb3377eb170ae273c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cryptoswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
|
||||
"state" : {
|
||||
"revision" : "e74bbbfbef939224b242ae7c342a90e60b88b5ce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "emojikit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiKit",
|
||||
"state" : {
|
||||
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
|
||||
"version" : "0.1.2"
|
||||
"revision" : "47a4b1402de26be0299dcb4d667c1faaf21a7874",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23,8 +31,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiPicker.git",
|
||||
"state" : {
|
||||
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
|
||||
"version" : "0.1.1"
|
||||
"revision" : "3f48903721eae223238ff0af17c22d6373d33813",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -45,14 +53,6 @@
|
||||
"version" : "7.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nostr-sdk-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/rust-nostr/nostr-sdk-swift",
|
||||
"state" : {
|
||||
"revision" : "27711a03ea7d977162595eea1d9b2d5a45f0b628"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "secp256k1.swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -10,22 +10,68 @@ import SwiftUI
|
||||
struct Reposted: View {
|
||||
let damus: DamusState
|
||||
let pubkey: Pubkey
|
||||
let target: NostrEvent
|
||||
@State var reposts: Int
|
||||
|
||||
init(damus: DamusState, pubkey: Pubkey, target: NostrEvent) {
|
||||
self.damus = damus
|
||||
self.pubkey = pubkey
|
||||
self.target = target
|
||||
self.reposts = damus.boosts.counts[target.id] ?? 1
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Image("repost")
|
||||
.foregroundColor(Color.gray)
|
||||
ProfileName(pubkey: pubkey, damus: damus, show_nip5_domain: false)
|
||||
.foregroundColor(Color.gray)
|
||||
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
||||
.foregroundColor(Color.gray)
|
||||
|
||||
// Show profile picture of the reposter only if the reposter is not the author of the reposted note.
|
||||
if pubkey != target.pubkey {
|
||||
ProfilePicView(pubkey: pubkey, size: eventview_pfp_size(.small), highlight: .none, profiles: damus.profiles, disable_animation: damus.settings.disable_animation)
|
||||
.onTapGesture {
|
||||
show_profile_action_sheet_if_enabled(damus_state: damus, pubkey: pubkey)
|
||||
}
|
||||
.onLongPressGesture(minimumDuration: 0.1) {
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
damus.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.Reposts(reposts: .reposts(state: damus, target: target.id))) {
|
||||
Text(people_reposted_text(profiles: damus.profiles, pubkey: pubkey, reposts: reposts))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.update_stats), perform: { note_id in
|
||||
guard note_id == target.id else { return }
|
||||
let repost_count = damus.boosts.counts[target.id]
|
||||
if let repost_count, reposts != repost_count {
|
||||
reposts = repost_count
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func people_reposted_text(profiles: Profiles, pubkey: Pubkey, reposts: Int, locale: Locale = Locale.current) -> String {
|
||||
guard reposts > 0 else {
|
||||
return ""
|
||||
}
|
||||
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
let other_reposts = reposts - 1
|
||||
let display_name = event_author_name(profiles: profiles, pubkey: pubkey)
|
||||
|
||||
if other_reposts == 0 {
|
||||
return String(format: NSLocalizedString("%@ reposted", bundle: bundle, comment: "Text indicating that the note was reposted (i.e. re-shared)."), locale: locale, display_name)
|
||||
} else {
|
||||
return String(format: localizedStringFormat(key: "people_reposted_count", locale: locale), locale: locale, other_reposts, display_name)
|
||||
}
|
||||
}
|
||||
|
||||
struct Reposted_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state
|
||||
Reposted(damus: test_state, pubkey: test_state.pubkey)
|
||||
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ struct SupporterBadge_Previews: PreviewProvider {
|
||||
.frame(width: 100)
|
||||
}
|
||||
|
||||
Text("Double star (just shape itself, with alt background color, to show it adapts to background well)")
|
||||
Text(verbatim: "Double star (just shape itself, with alt background color, to show it adapts to background well)")
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if #available(iOS 17.0, *) {
|
||||
@@ -257,13 +257,13 @@ struct SupporterBadge_Previews: PreviewProvider {
|
||||
.background(Color.blue)
|
||||
}
|
||||
|
||||
Text("Double star (fallback for iOS 16 and below)")
|
||||
|
||||
Text(verbatim: "Double star (fallback for iOS 16 and below)")
|
||||
|
||||
HStack(alignment: .center) {
|
||||
DoubleStar.Fallback(size: 17, starOffset: 5)
|
||||
}
|
||||
|
||||
Text("Double star (fallback for iOS 16 and below, with alt color limitation shown)")
|
||||
Text(verbatim: "Double star (fallback for iOS 16 and below, with alt color limitation shown)")
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
|
||||
@@ -222,12 +222,6 @@ struct ContentView: View {
|
||||
navigationCoordinator.push(route: Route.Script(script: model))
|
||||
}
|
||||
|
||||
func open_profile(pubkey: Pubkey) {
|
||||
let profile_model = ProfileModel(pubkey: pubkey, damus: damus_state!)
|
||||
let followers = FollowersModel(damus_state: damus_state!, target: pubkey)
|
||||
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
|
||||
}
|
||||
|
||||
func open_search(filt: NostrFilter) {
|
||||
let search = SearchModel(state: damus_state!, search: filt)
|
||||
navigationCoordinator.push(route: Route.Search(search: search))
|
||||
@@ -312,6 +306,9 @@ struct ContentView: View {
|
||||
hasSeenOnboardingSuggestions = true
|
||||
}
|
||||
self.appDelegate?.state = damus_state
|
||||
Task { // We probably don't need this to be a detached task. According to https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Defining-and-Calling-Asynchronous-Functions, awaits are only suspension points that do not block the thread.
|
||||
await self.listenAndHandleLocalNotifications()
|
||||
}
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
switch item {
|
||||
@@ -370,6 +367,8 @@ struct ContentView: View {
|
||||
self.confirm_mute = true
|
||||
}
|
||||
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
||||
try? damus_state.pool.add_relay(.nwc(url: nwc.relay))
|
||||
|
||||
// update the lightning address on our profile when we attach a
|
||||
// wallet with an associated
|
||||
guard let ds = self.damus_state,
|
||||
@@ -511,27 +510,6 @@ struct ContentView: View {
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.local_notification)) { local in
|
||||
guard let damus_state else { return }
|
||||
|
||||
switch local.mention {
|
||||
case .pubkey(let pubkey):
|
||||
open_profile(pubkey: pubkey)
|
||||
|
||||
case .note(let noteId):
|
||||
openEvent(noteId: noteId, notificationType: local.type)
|
||||
case .nevent(let nevent):
|
||||
openEvent(noteId: nevent.noteid, notificationType: local.type)
|
||||
case .nprofile(let nprofile):
|
||||
open_profile(pubkey: nprofile.author)
|
||||
case .nrelay(_):
|
||||
break
|
||||
case .naddr(let naddr):
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
|
||||
home.filter_events()
|
||||
@@ -641,6 +619,28 @@ struct ContentView: View {
|
||||
self.selected_timeline = timeline
|
||||
}
|
||||
|
||||
/// Listens to requests to open a push/local user notification
|
||||
///
|
||||
/// This function never returns, it just keeps streaming
|
||||
func listenAndHandleLocalNotifications() async {
|
||||
for await notification in await QueueableNotify<LossyLocalNotification>.shared.stream {
|
||||
self.handleNotification(notification: notification)
|
||||
}
|
||||
}
|
||||
|
||||
func handleNotification(notification: LossyLocalNotification) {
|
||||
Log.info("ContentView is handling a notification", for: .push_notifications)
|
||||
guard let damus_state else {
|
||||
// This should never happen because `listenAndHandleLocalNotifications` is called after damus state is initialized in `onAppear`
|
||||
assertionFailure("DamusState not loaded when ContentView (new handler) was handling a notification")
|
||||
Log.error("DamusState not loaded when ContentView (new handler) was handling a notification", for: .push_notifications)
|
||||
return
|
||||
}
|
||||
let local = notification
|
||||
let openAction = local.toViewOpenAction()
|
||||
self.execute_open_action(openAction)
|
||||
}
|
||||
|
||||
func connect() {
|
||||
// nostrdb
|
||||
var mndb = Ndb()
|
||||
@@ -746,23 +746,6 @@ struct ContentView: View {
|
||||
damus_state.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func openEvent(noteId: NoteId, notificationType: LocalNotificationType) {
|
||||
guard let target = damus_state.events.lookup(noteId) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch notificationType {
|
||||
case .dm:
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.set_active_dm(target.pubkey)
|
||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
||||
case .like, .zap, .mention, .repost, .reply, .tagged:
|
||||
open_event(ev: target)
|
||||
case .profile_zap:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// An open action within the app
|
||||
/// This is used to model, store, and communicate a desired view action to be taken as a result of opening an object,
|
||||
@@ -1216,6 +1199,35 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
||||
}
|
||||
}
|
||||
|
||||
extension LossyLocalNotification {
|
||||
/// Computes a view open action from a mention reference.
|
||||
/// Use this when opening a user-presentable interface to a specific mention reference.
|
||||
func toViewOpenAction() -> ContentView.ViewOpenAction {
|
||||
switch self.mention {
|
||||
case .pubkey(let pubkey):
|
||||
return .route(.ProfileByKey(pubkey: pubkey))
|
||||
case .note(let noteId):
|
||||
return .route(.LoadableNostrEvent(note_reference: .note_id(noteId)))
|
||||
case .nevent(let nEvent):
|
||||
// TODO: Improve this by implementing a route that handles nevents with their relay hints.
|
||||
return .route(.LoadableNostrEvent(note_reference: .note_id(nEvent.noteid)))
|
||||
case .nprofile(let nProfile):
|
||||
// TODO: Improve this by implementing a profile route that handles nprofiles with their relay hints.
|
||||
return .route(.ProfileByKey(pubkey: nProfile.author))
|
||||
case .nrelay(let string):
|
||||
// We do not need to implement `nrelay` support, it has been deprecated.
|
||||
// See https://github.com/nostr-protocol/nips/blob/6e7a618e7f873bb91e743caacc3b09edab7796a0/BREAKING.md?plain=1#L21
|
||||
return .sheet(.error(ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("You opened an invalid link. The link you tried to open refers to \"nrelay\", which has been deprecated and is not supported.", comment: "User-visible error description for a user who tries to open a deprecated \"nrelay\" link."),
|
||||
tip: NSLocalizedString("Please contact the person who provided the link, and ask for another link.", comment: "User-visible tip on what to do if a link contains a deprecated \"nrelay\" reference."),
|
||||
technical_info: "`MentionRef.toViewOpenAction` detected deprecated `nrelay` contents"
|
||||
)))
|
||||
case .naddr(let nAddr):
|
||||
return .route(.LoadableNostrEvent(note_reference: .naddr(nAddr)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func logout(_ state: DamusState?)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,8 +10,9 @@ import Foundation
|
||||
|
||||
/// Simple filter to determine whether to show posts or all posts and replies.
|
||||
enum FilterState : Int {
|
||||
case posts_and_replies = 1
|
||||
case posts = 0
|
||||
case posts_and_replies = 1
|
||||
case conversations = 2
|
||||
|
||||
func filter(ev: NostrEvent) -> Bool {
|
||||
switch self {
|
||||
@@ -19,6 +20,8 @@ enum FilterState : Int {
|
||||
return ev.known_kind == .boost || ev.known_kind == .highlight || !ev.is_reply()
|
||||
case .posts_and_replies:
|
||||
return true
|
||||
case .conversations:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -79,6 +79,7 @@ class HomeModel: ContactsDelegate {
|
||||
var notifications = NotificationsModel()
|
||||
var notification_status = NotificationStatusModel()
|
||||
var events: EventHolder = EventHolder()
|
||||
var already_reposted: Set<NoteId> = Set()
|
||||
var zap_button: ZapButtonModel = ZapButtonModel()
|
||||
|
||||
init() {
|
||||
@@ -375,6 +376,8 @@ class HomeModel: ContactsDelegate {
|
||||
boost_ev_id = inner_ev.id
|
||||
|
||||
Task {
|
||||
// NOTE (jb55): remove this after nostrdb update, since nostrdb
|
||||
// processess reposts when note is ingested
|
||||
guard validate_event(ev: inner_ev) == .ok else {
|
||||
return
|
||||
}
|
||||
@@ -394,7 +397,7 @@ class HomeModel: ContactsDelegate {
|
||||
switch self.damus_state.boosts.add_event(ev, target: e) {
|
||||
case .already_counted:
|
||||
break
|
||||
case .success(let n):
|
||||
case .success(_):
|
||||
notify(.update_stats(note_id: e))
|
||||
}
|
||||
}
|
||||
@@ -403,7 +406,7 @@ class HomeModel: ContactsDelegate {
|
||||
switch damus_state.quote_reposts.add_event(ev, target: target) {
|
||||
case .already_counted:
|
||||
break
|
||||
case .success(let n):
|
||||
case .success(_):
|
||||
notify(.update_stats(note_id: target))
|
||||
}
|
||||
}
|
||||
@@ -732,6 +735,16 @@ class HomeModel: ContactsDelegate {
|
||||
handle_quote_repost_event(ev, target: quoted_event.note_id)
|
||||
}
|
||||
|
||||
// don't add duplicate reposts to home
|
||||
if ev.known_kind == .boost, let target = ev.get_inner_event()?.id {
|
||||
if already_reposted.contains(target) {
|
||||
Log.info("Skipping duplicate repost for event %s", for: .timeline, target.hex())
|
||||
return
|
||||
} else {
|
||||
already_reposted.insert(target)
|
||||
}
|
||||
}
|
||||
|
||||
if sub_id == home_subid {
|
||||
insert_home_event(ev)
|
||||
} else if sub_id == notifications_subid {
|
||||
|
||||
@@ -22,8 +22,10 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
var seen_event: Set<NoteId> = Set()
|
||||
var sub_id = UUID().description
|
||||
var prof_subid = UUID().description
|
||||
var conversations_subid = UUID().description
|
||||
var findRelay_subid = UUID().description
|
||||
|
||||
var conversation_events: Set<NoteId> = Set()
|
||||
|
||||
init(pubkey: Pubkey, damus: DamusState) {
|
||||
self.pubkey = pubkey
|
||||
self.damus = damus
|
||||
@@ -59,6 +61,9 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
|
||||
damus.pool.unsubscribe(sub_id: sub_id)
|
||||
damus.pool.unsubscribe(sub_id: prof_subid)
|
||||
if pubkey != damus.pubkey {
|
||||
damus.pool.unsubscribe(sub_id: conversations_subid)
|
||||
}
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
@@ -69,13 +74,29 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
|
||||
text_filter.authors = [pubkey]
|
||||
text_filter.limit = 500
|
||||
|
||||
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
|
||||
|
||||
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
|
||||
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
||||
damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
||||
damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event)
|
||||
|
||||
subscribe_to_conversations()
|
||||
}
|
||||
|
||||
|
||||
private func subscribe_to_conversations() {
|
||||
// Only subscribe to conversation events if the profile is not us.
|
||||
guard pubkey != damus.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
let conversation_kinds: [NostrKind] = [.text, .longform, .highlight]
|
||||
let limit: UInt32 = 500
|
||||
let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey])
|
||||
let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey])
|
||||
print("subscribing to conversation events from and to profile \(pubkey) with sub_id \(conversations_subid)")
|
||||
damus.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
|
||||
}
|
||||
|
||||
func handle_profile_contact_event(_ ev: NostrEvent) {
|
||||
process_contact_event(state: damus, ev: ev)
|
||||
|
||||
@@ -90,15 +111,8 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
self.following = count_pubkeys(ev.tags)
|
||||
self.relays = decode_json_relays(ev.content)
|
||||
}
|
||||
|
||||
func add_event(_ ev: NostrEvent) {
|
||||
guard ev.should_show_event else {
|
||||
return
|
||||
}
|
||||
|
||||
if seen_event.contains(ev.id) {
|
||||
return
|
||||
}
|
||||
private func add_event(_ ev: NostrEvent) {
|
||||
if ev.is_textlike || ev.known_kind == .boost {
|
||||
if self.events.insert(ev) {
|
||||
self.objectWillChange.send()
|
||||
@@ -109,24 +123,57 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
seen_event.insert(ev.id)
|
||||
}
|
||||
|
||||
// Ensure the event public key matches the public key(s) we are querying.
|
||||
// This is done to protect against a relay not properly filtering events by the pubkey
|
||||
// See https://github.com/damus-io/damus/issues/1846 for more information
|
||||
private func relay_filtered_correctly(_ ev: NostrEvent, subid: String?) -> Bool {
|
||||
if subid == self.conversations_subid {
|
||||
switch ev.pubkey {
|
||||
case self.pubkey:
|
||||
return ev.referenced_pubkeys.contains(damus.pubkey)
|
||||
case damus.pubkey:
|
||||
return ev.referenced_pubkeys.contains(self.pubkey)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return self.pubkey == ev.pubkey
|
||||
}
|
||||
|
||||
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
switch ev {
|
||||
case .ws_event:
|
||||
return
|
||||
case .nostr_event(let resp):
|
||||
guard resp.subid == self.sub_id || resp.subid == self.prof_subid else {
|
||||
guard resp.subid == self.sub_id || resp.subid == self.prof_subid || resp.subid == self.conversations_subid else {
|
||||
return
|
||||
}
|
||||
switch resp {
|
||||
case .ok:
|
||||
break
|
||||
case .event(_, let ev):
|
||||
// Ensure the event public key matches this profiles public key
|
||||
// This is done to protect against a relay not properly filtering events by the pubkey
|
||||
// See https://github.com/damus-io/damus/issues/1846 for more information
|
||||
guard self.pubkey == ev.pubkey else { break }
|
||||
guard ev.should_show_event else {
|
||||
break
|
||||
}
|
||||
|
||||
add_event(ev)
|
||||
if !seen_event.contains(ev.id) {
|
||||
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
||||
break
|
||||
}
|
||||
|
||||
add_event(ev)
|
||||
|
||||
if resp.subid == self.conversations_subid {
|
||||
conversation_events.insert(ev.id)
|
||||
}
|
||||
} else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) {
|
||||
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
||||
break
|
||||
}
|
||||
|
||||
conversation_events.insert(ev.id)
|
||||
}
|
||||
case .notice:
|
||||
break
|
||||
//notify(.notice, notice)
|
||||
|
||||
@@ -388,7 +388,7 @@ class DamusPurple: StoreObserverDelegate {
|
||||
case .none:
|
||||
return .sheet(.error(.init(
|
||||
user_visible_description: NSLocalizedString("You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.", comment: "Error label upon continuing in the app from a Damus Purple purchase"),
|
||||
tip: NSLocalizedString("Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue", comment: "User-facing tips on what to do if a Purple welcome link doesn't work"),
|
||||
tip: NSLocalizedString("Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.", comment: "User-facing tips on what to do if a Purple welcome link doesn't work"),
|
||||
technical_info: "Handling Purple URL \"\(purple_url)\" failed, the `is_good_to_go` result was `\(String(describing: is_good_to_go))`"
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ struct DamusURLHandler {
|
||||
let thread = await ThreadModel(event: nostrEvent, damus_state: damus_state)
|
||||
return .route(.Thread(thread: thread))
|
||||
case .event_reference(let event_reference):
|
||||
return .route(.ThreadFromReference(note_reference: event_reference))
|
||||
return .route(.LoadableNostrEvent(note_reference: event_reference))
|
||||
case .wallet_connect(let walletConnectURL):
|
||||
damus_state.wallet.new(walletConnectURL)
|
||||
return .route(.Wallet(wallet: damus_state.wallet))
|
||||
@@ -99,7 +99,7 @@ struct DamusURLHandler {
|
||||
case profile(Pubkey)
|
||||
case filter(NostrFilter)
|
||||
case event(NostrEvent)
|
||||
case event_reference(LoadableThreadModel.NoteReference)
|
||||
case event_reference(LoadableNostrEventViewModel.NoteReference)
|
||||
case wallet_connect(WalletConnectURL)
|
||||
case script([UInt8])
|
||||
case purple(DamusPurpleURL)
|
||||
|
||||
@@ -201,6 +201,10 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "developer_mode", default_value: false)
|
||||
var developer_mode: Bool
|
||||
|
||||
/// Makes all post content gibberish and blurhashes images, to avoid distractions when developers are working.
|
||||
@Setting(key: "undistract_mode", default_value: false)
|
||||
var undistractMode: Bool
|
||||
|
||||
@Setting(key: "always_show_onboarding_suggestions", default_value: false)
|
||||
var always_show_onboarding_suggestions: Bool
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-01-20.
|
||||
//
|
||||
import NostrSDK
|
||||
import Foundation
|
||||
|
||||
/// This models a NIP-37 draft.
|
||||
@@ -77,13 +76,7 @@ struct NIP37Draft {
|
||||
guard let note_json_string = String(data: note_json_data, encoding: .utf8) else {
|
||||
throw NIP37DraftEventError.encoding_error
|
||||
}
|
||||
guard let secret_key = SecretKey.from(privkey: keypair.privkey) else {
|
||||
throw NIP37DraftEventError.invalid_keypair
|
||||
}
|
||||
guard let pubkey = PublicKey.from(pubkey: keypair.pubkey) else {
|
||||
throw NIP37DraftEventError.invalid_keypair
|
||||
}
|
||||
guard let contents = try? nip44Encrypt(secretKey: secret_key, publicKey: pubkey, content: note_json_string, version: Nip44Version.v2) else {
|
||||
guard let contents = try? NIP44v2Encryption.encrypt(plaintext: note_json_string, privateKeyA: keypair.privkey, publicKeyB: keypair.pubkey) else {
|
||||
return nil
|
||||
}
|
||||
var tags = [
|
||||
@@ -111,16 +104,10 @@ struct NIP37Draft {
|
||||
static func unwrap(note: NdbNote, keypair: FullKeypair) throws -> NdbNote? {
|
||||
let wrapped_note = note
|
||||
guard wrapped_note.known_kind == .draft else { return nil }
|
||||
guard let private_key = SecretKey.from(privkey: keypair.privkey) else {
|
||||
throw NIP37DraftEventError.invalid_keypair
|
||||
}
|
||||
guard let pubkey = PublicKey.from(pubkey: keypair.pubkey) else {
|
||||
throw NIP37DraftEventError.invalid_keypair
|
||||
}
|
||||
guard let draft_event_json = try? nip44Decrypt(
|
||||
secretKey: private_key,
|
||||
publicKey: pubkey,
|
||||
payload: wrapped_note.content
|
||||
guard let draft_event_json = try? NIP44v2Encryption.decrypt(
|
||||
payload: wrapped_note.content,
|
||||
privateKeyA: keypair.privkey,
|
||||
publicKeyB: keypair.pubkey
|
||||
) else { return nil }
|
||||
return NdbNote.owned_from_json(json: draft_event_json)
|
||||
}
|
||||
@@ -130,17 +117,3 @@ struct NIP37Draft {
|
||||
case encoding_error
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience extensions
|
||||
|
||||
fileprivate extension PublicKey {
|
||||
static func from(pubkey: Pubkey) -> PublicKey? {
|
||||
return try? PublicKey.parse(publicKey: pubkey.hex())
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension SecretKey {
|
||||
static func from(privkey: Privkey) -> SecretKey? {
|
||||
return try? SecretKey.parse(secretKey: privkey.hex())
|
||||
}
|
||||
}
|
||||
|
||||
357
damus/NIP44/NIP44.swift
Normal file
357
damus/NIP44/NIP44.swift
Normal file
@@ -0,0 +1,357 @@
|
||||
//
|
||||
// NIP44.swift
|
||||
// damus
|
||||
//
|
||||
// Based on NIP44v2Encrypting.swift created by Terry Yiu on 3/16/24, from https://github.com/nostr-sdk/nostr-sdk-ios, which is MIT licensed.
|
||||
//
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2023 Nostr SDK
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
//
|
||||
// Adapted by Daniel D’Aquino on 2025-02-10.
|
||||
//
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import CryptoSwift
|
||||
import secp256k1
|
||||
|
||||
struct NIP44v2Encryption {
|
||||
|
||||
/// Produces a `String` containing `plaintext` that has been encrypted using the `privateKey` of user A and the `publicKey` of user B.
|
||||
///
|
||||
/// The result is non-deterministic because a cryptographically secure pseudorandom generated nonce is used each time,
|
||||
/// but can be decrypted deterministically with a call to ``NIP44v2Encryption/decrypt(payload:privateKeyA:publicKeyB:)``,
|
||||
/// where user A and user B are interchangeable.
|
||||
///
|
||||
/// This function can `throw` an error from ``EncryptionError`` if it fails to encrypt the plaintext.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - plaintext: The plaintext to encrypt.
|
||||
/// - privateKeyA: The private key of user A.
|
||||
/// - publicKeyB: The public key of user B.
|
||||
/// - Returns: The encrypted ciphertext.
|
||||
static func encrypt(plaintext: String, privateKeyA: Privkey, publicKeyB: Pubkey) throws -> String {
|
||||
let conversationKey = try conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB)
|
||||
|
||||
return try encrypt(plaintext: plaintext, conversationKey: conversationKey)
|
||||
}
|
||||
|
||||
/// Produces a `String` containing `payload` that has been decrypted using the `privateKey` of user A and the `publicKey` of user B,
|
||||
/// and the result is identical to if the `privateKey` of user B and `publicKey` of user A were used to decrypt `payload` instead.
|
||||
///
|
||||
/// Any ciphertext returned from the call to ``NIP44v2Encryption/encrypt(plaintext:privateKeyA:publicKeyB:)``
|
||||
/// can be decrypted, where user A and B are interchangeable.
|
||||
///
|
||||
/// This function can `throw` an error from ``EncryptionError`` if it fails to decrypt the payload.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - payload: The payload to decrypt.
|
||||
/// - privateKeyA: The private key of user A.
|
||||
/// - publicKeyB: The public key of user B.
|
||||
/// - Returns: The decrypted plaintext message.
|
||||
static func decrypt(payload: String, privateKeyA: Privkey, publicKeyB: Pubkey) throws -> String {
|
||||
let conversationKey = try conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB)
|
||||
|
||||
return try decrypt(payload: payload, conversationKey: conversationKey)
|
||||
}
|
||||
|
||||
/// Calculates length of the padded byte array.
|
||||
static func calculatePaddedLength(_ unpaddedLength: Int) throws -> Int {
|
||||
guard unpaddedLength > 0 else {
|
||||
throw EncryptionError.unpaddedLengthInvalid(unpaddedLength)
|
||||
}
|
||||
if unpaddedLength <= 32 {
|
||||
return 32
|
||||
}
|
||||
|
||||
let nextPower = 1 << (Int(floor(log2(Double(unpaddedLength) - 1))) + 1)
|
||||
let chunk: Int
|
||||
|
||||
if nextPower <= 256 {
|
||||
chunk = 32
|
||||
} else {
|
||||
chunk = nextPower / 8
|
||||
}
|
||||
|
||||
return chunk * (Int(floor((Double(unpaddedLength) - 1) / Double(chunk))) + 1)
|
||||
}
|
||||
|
||||
/// Converts unpadded plaintext to padded bytes.
|
||||
static func pad(_ plaintext: String) throws -> Data {
|
||||
guard let unpadded = plaintext.data(using: .utf8) else {
|
||||
throw EncryptionError.utf8EncodingFailed
|
||||
}
|
||||
|
||||
let unpaddedLength = unpadded.count
|
||||
|
||||
guard 1...65535 ~= unpaddedLength else {
|
||||
throw EncryptionError.plaintextLengthInvalid(unpaddedLength)
|
||||
}
|
||||
|
||||
var prefix = Data(count: 2)
|
||||
prefix.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in
|
||||
ptr.storeBytes(of: UInt16(unpaddedLength).bigEndian, as: UInt16.self)
|
||||
}
|
||||
|
||||
let suffix = Data(count: try calculatePaddedLength(unpaddedLength) - unpaddedLength)
|
||||
|
||||
return prefix + unpadded + suffix
|
||||
}
|
||||
|
||||
/// Converts padded bytes to unpadded plaintext.
|
||||
static func unpad(_ padded: Data) throws -> String {
|
||||
guard padded.count >= 2 else {
|
||||
throw EncryptionError.paddingInvalid
|
||||
}
|
||||
|
||||
let unpaddedLength = (Int(padded[0]) << 8) | Int(padded[1])
|
||||
|
||||
guard 2+unpaddedLength <= padded.count else {
|
||||
throw EncryptionError.paddingInvalid
|
||||
}
|
||||
|
||||
let unpadded = toBytes(from: padded)[2..<2+unpaddedLength]
|
||||
let paddedLength = try calculatePaddedLength(unpaddedLength)
|
||||
|
||||
guard unpaddedLength > 0,
|
||||
unpadded.count == unpaddedLength,
|
||||
padded.count == 2 + paddedLength,
|
||||
let result = String(data: Data(unpadded), encoding: .utf8) else {
|
||||
throw EncryptionError.paddingInvalid
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func decodePayload(_ payload: String) throws -> DecodedPayload {
|
||||
let payloadLength = payload.count
|
||||
|
||||
guard payloadLength > 0 && payload.first != "#" else {
|
||||
throw EncryptionError.unknownVersion()
|
||||
}
|
||||
guard 132...87472 ~= payloadLength else {
|
||||
throw EncryptionError.payloadSizeInvalid(payloadLength)
|
||||
}
|
||||
|
||||
guard let data = Data(base64Encoded: payload) else {
|
||||
throw EncryptionError.base64EncodingFailed
|
||||
}
|
||||
|
||||
let dataLength = data.count
|
||||
|
||||
guard 99...65603 ~= dataLength else {
|
||||
throw EncryptionError.dataSizeInvalid(dataLength)
|
||||
}
|
||||
|
||||
guard let version = data.first else {
|
||||
throw EncryptionError.unknownVersion()
|
||||
}
|
||||
|
||||
guard version == 2 else {
|
||||
throw EncryptionError.unknownVersion(Int(version))
|
||||
}
|
||||
|
||||
let nonce = data[data.index(data.startIndex, offsetBy: 1)..<data.index(data.startIndex, offsetBy: 33)]
|
||||
let ciphertext = data[data.index(data.startIndex, offsetBy: 33)..<data.index(data.startIndex, offsetBy: dataLength - 32)]
|
||||
let mac = data[data.index(data.startIndex, offsetBy: dataLength - 32)..<data.index(data.startIndex, offsetBy: dataLength)]
|
||||
|
||||
return DecodedPayload(nonce: nonce, ciphertext: ciphertext, mac: mac)
|
||||
}
|
||||
|
||||
static func hmacAad(key: Data, message: Data, aad: Data) throws -> Data {
|
||||
guard aad.count == 32 else {
|
||||
throw EncryptionError.aadLengthInvalid(aad.count)
|
||||
}
|
||||
|
||||
let combined = aad + message
|
||||
|
||||
return Data(CryptoKit.HMAC<CryptoKit.SHA256>.authenticationCode(for: combined, using: SymmetricKey(data: key)))
|
||||
}
|
||||
|
||||
static func toBytes(from data: Data) -> [UInt8] {
|
||||
data.withUnsafeBytes { bytesPointer in Array(bytesPointer) }
|
||||
}
|
||||
|
||||
static func preparePublicKeyBytes(from publicKey: Pubkey) throws -> [UInt8] {
|
||||
let publicKeyBytes = publicKey.bytes
|
||||
|
||||
let prefix = Data([2])
|
||||
let prefixBytes = toBytes(from: prefix)
|
||||
|
||||
return prefixBytes + publicKeyBytes
|
||||
}
|
||||
|
||||
static func parsePublicKey(from bytes: [UInt8]) throws -> secp256k1_pubkey {
|
||||
var publicKey = secp256k1_pubkey()
|
||||
guard secp256k1_ec_pubkey_parse(secp256k1.Context.raw, &publicKey, bytes, bytes.count) == 1 else {
|
||||
throw EncryptionError.publicKeyInvalid
|
||||
}
|
||||
return publicKey
|
||||
}
|
||||
|
||||
static func computeSharedSecret(using publicKey: secp256k1_pubkey, and privateKeyBytes: [UInt8]) throws -> [UInt8] {
|
||||
var sharedSecret = [UInt8](repeating: 0, count: 32)
|
||||
var mutablePublicKey = publicKey
|
||||
|
||||
// Multiplication of point B by scalar a (a ⋅ B), defined in [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki).
|
||||
// The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method bytes(P) from BIP340.
|
||||
// Private and public keys must be validated as per BIP340: pubkey must be a valid, on-curve point, and private key must be a scalar in range [1, secp256k1_order - 1]
|
||||
guard secp256k1_ecdh(secp256k1.Context.raw, &sharedSecret, &mutablePublicKey, privateKeyBytes, { (output, x32, _, _) in
|
||||
memcpy(output, x32, 32)
|
||||
return 1
|
||||
}, nil) != 0 else {
|
||||
throw EncryptionError.sharedSecretComputationFailed
|
||||
}
|
||||
return sharedSecret
|
||||
}
|
||||
|
||||
/// Calculates long-term key between users A and B.
|
||||
/// The conversation key of A's private key and B's public key is equal to the conversation key of B's private key and A's public key.
|
||||
static func conversationKey(privateKeyA: Privkey, publicKeyB: Pubkey) throws -> ContiguousBytes {
|
||||
let privateKeyABytes = privateKeyA.bytes
|
||||
let publicKeyBBytes = try preparePublicKeyBytes(from: publicKeyB)
|
||||
let parsedPublicKeyB = try parsePublicKey(from: publicKeyBBytes)
|
||||
let sharedSecret = try computeSharedSecret(using: parsedPublicKeyB, and: privateKeyABytes)
|
||||
|
||||
return CryptoKit.HKDF<CryptoKit.SHA256>.extract(inputKeyMaterial: SymmetricKey(data: sharedSecret), salt: Data("nip44-v2".utf8))
|
||||
}
|
||||
|
||||
/// Calculates unique per-message key.
|
||||
static func messageKeys(conversationKey: ContiguousBytes, nonce: Data) throws -> MessageKeys {
|
||||
let conversationKeyByteCount = conversationKey.bytes.count
|
||||
guard conversationKeyByteCount == 32 else {
|
||||
throw EncryptionError.conversationKeyLengthInvalid(conversationKeyByteCount)
|
||||
}
|
||||
|
||||
guard nonce.count == 32 else {
|
||||
throw EncryptionError.nonceLengthInvalid(nonce.count)
|
||||
}
|
||||
|
||||
let keys = CryptoKit.HKDF<CryptoKit.SHA256>.expand(pseudoRandomKey: conversationKey, info: nonce, outputByteCount: 76)
|
||||
let keysBytes = keys.bytes
|
||||
|
||||
let chaChaKey = Data(keysBytes[0..<32])
|
||||
let chaChaNonce = Data(keysBytes[32..<44])
|
||||
let hmacKey = Data(keysBytes[44..<76])
|
||||
|
||||
return MessageKeys(chaChaKey: chaChaKey, chaChaNonce: chaChaNonce, hmacKey: hmacKey)
|
||||
}
|
||||
|
||||
static func encrypt(plaintext: String, conversationKey: ContiguousBytes, nonce: Data? = nil) throws -> String {
|
||||
let nonceData: Data
|
||||
if let nonce {
|
||||
nonceData = nonce
|
||||
} else {
|
||||
// Fetches randomness from CSPRNG.
|
||||
nonceData = Data.secureRandomBytes(count: 32)
|
||||
}
|
||||
|
||||
let messageKeys = try messageKeys(conversationKey: conversationKey, nonce: nonceData)
|
||||
let padded = try pad(plaintext)
|
||||
let paddedBytes = toBytes(from: padded)
|
||||
|
||||
let chaChaKey = toBytes(from: messageKeys.chaChaKey)
|
||||
let chaChaNonce = toBytes(from: messageKeys.chaChaNonce)
|
||||
|
||||
let ciphertext = try ChaCha20(key: chaChaKey, iv: chaChaNonce).encrypt(paddedBytes)
|
||||
let ciphertextData = Data(ciphertext)
|
||||
|
||||
let mac = try hmacAad(key: messageKeys.hmacKey, message: ciphertextData, aad: nonceData)
|
||||
|
||||
let data = Data([2]) + nonceData + ciphertextData + mac
|
||||
return data.base64EncodedString()
|
||||
}
|
||||
|
||||
static func decrypt(payload: String, conversationKey: ContiguousBytes) throws -> String {
|
||||
let decodedPayload = try decodePayload(payload)
|
||||
let nonce = decodedPayload.nonce
|
||||
let ciphertext = decodedPayload.ciphertext
|
||||
let ciphertextBytes = toBytes(from: ciphertext)
|
||||
let mac = decodedPayload.mac
|
||||
|
||||
let messageKeys = try messageKeys(conversationKey: conversationKey, nonce: nonce)
|
||||
|
||||
let calculatedMac = try hmacAad(key: messageKeys.hmacKey, message: ciphertext, aad: nonce)
|
||||
|
||||
guard calculatedMac == mac else {
|
||||
throw EncryptionError.macInvalid
|
||||
}
|
||||
|
||||
let chaChaNonce = toBytes(from: messageKeys.chaChaNonce)
|
||||
let chaChaKey = toBytes(from: messageKeys.chaChaKey)
|
||||
|
||||
let paddedPlaintext = try ChaCha20(key: chaChaKey, iv: chaChaNonce).decrypt(ciphertextBytes)
|
||||
let paddedPlaintextData = Data(paddedPlaintext.bytes)
|
||||
|
||||
return try unpad(paddedPlaintextData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper structures and extensions
|
||||
|
||||
extension Data {
|
||||
/// Random data of a given size, from CSPRNG
|
||||
/// - Parameter count: The size of the data, in bytes
|
||||
/// - Returns: Bytes randomly generated from CSPRNG
|
||||
static func secureRandomBytes(count: Int) -> Data {
|
||||
var bytes = [Int8](repeating: 0, count: count)
|
||||
guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else {
|
||||
fatalError("can't copy secure random data")
|
||||
}
|
||||
return Data(bytes: bytes, count: count)
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP44v2Encryption {
|
||||
struct DecodedPayload {
|
||||
let nonce: Data
|
||||
let ciphertext: Data
|
||||
let mac: Data
|
||||
}
|
||||
|
||||
struct MessageKeys {
|
||||
let chaChaKey: Data
|
||||
let chaChaNonce: Data
|
||||
let hmacKey: Data
|
||||
}
|
||||
|
||||
public enum EncryptionError: Error {
|
||||
case aadLengthInvalid(Int)
|
||||
case base64EncodingFailed
|
||||
case chaCha20DecryptionFailed
|
||||
case chaCha20EncryptionFailed
|
||||
case conversationKeyLengthInvalid(Int)
|
||||
case dataSizeInvalid(Int)
|
||||
case macInvalid
|
||||
case nonceLengthInvalid(Int)
|
||||
case paddingInvalid
|
||||
case payloadSizeInvalid(Int)
|
||||
case plaintextLengthInvalid(Int)
|
||||
case privateKeyInvalid
|
||||
case publicKeyInvalid
|
||||
case sharedSecretComputationFailed
|
||||
case unknownVersion(Int? = nil)
|
||||
case unpaddedLengthInvalid(Int)
|
||||
case utf8EncodingFailed
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@ class ProfileRecord {
|
||||
}
|
||||
|
||||
guard let profile = data.profile,
|
||||
let addr = profile.lud16 ?? profile.lud06 else {
|
||||
let addr = (profile.lud16 ?? profile.lud06)?.trimmingCharacters(in: .whitespaces)
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -57,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? {
|
||||
@@ -301,7 +302,7 @@ class Profile: Codable {
|
||||
*/
|
||||
|
||||
func make_test_profile() -> Profile {
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com", damus_donation: 1)
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: nil, lud16: "jb55@jb55.com", nip05: "jb55@jb55.com", damus_donation: 1)
|
||||
}
|
||||
|
||||
func make_ln_url(_ str: String?) -> URL? {
|
||||
|
||||
@@ -7,19 +7,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LocalNotificationNotify: Notify {
|
||||
typealias Payload = LossyLocalNotification
|
||||
var payload: Payload
|
||||
}
|
||||
|
||||
extension NotifyHandler {
|
||||
static var local_notification: NotifyHandler<LocalNotificationNotify> {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension Notifications {
|
||||
static func local_notification(_ payload: LossyLocalNotification) -> Notifications<LocalNotificationNotify> {
|
||||
.init(.init(payload: payload))
|
||||
}
|
||||
extension QueueableNotify<LossyLocalNotification> {
|
||||
/// A shared singleton for opening local and push user notifications
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - The queue can only hold one element. This is done because if the user hypothetically opened 10 push notifications and there was a lag, we wouldn't want the app to suddenly open 10 different things.
|
||||
static let shared = QueueableNotify(maxQueueItems: 1)
|
||||
}
|
||||
|
||||
90
damus/Notify/QueueableNotify.swift
Normal file
90
damus/Notify/QueueableNotify.swift
Normal file
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// QueueableNotify.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-14.
|
||||
//
|
||||
|
||||
/// This notifies another object about some payload,
|
||||
/// with automatic "queueing" of messages if there are no listeners.
|
||||
///
|
||||
/// When used as a singleton, this can be used to easily send notifications to be handled at the app-level.
|
||||
///
|
||||
/// This serves the same purpose as `Notify`, except this implements the queueing of messages,
|
||||
/// which means that messages can be handled even if the listener is not instantiated yet.
|
||||
///
|
||||
/// **Example:** The app delegate can send some events that need handling from `ContentView` — but some can occur before `ContentView` is even instantiated.
|
||||
///
|
||||
///
|
||||
/// ## Usage notes
|
||||
///
|
||||
/// - This code was mainly written to have one listener at a time. Have more than one listener may be possible, but this class has not been tested/optimized for that purpose.
|
||||
///
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This makes heavy use of `AsyncStream` and continuations, because that allows complexities here to be handled elegantly with a simple "for-in" loop
|
||||
/// - Without this, it would take a couple of callbacks and manual handling of queued items to achieve the same effect
|
||||
/// - Modeled as an `actor` for extra thread-safety
|
||||
actor QueueableNotify<T: Sendable> {
|
||||
/// The continuation, which allows us to publish new items to the listener
|
||||
/// If `nil`, that means there is no listeners to the stream, which is used for determining whether to queue new incoming items.
|
||||
private var continuation: AsyncStream<T>.Continuation?
|
||||
/// Holds queue items
|
||||
private var queue: [T] = []
|
||||
/// The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
|
||||
var maxQueueItems: Int
|
||||
|
||||
/// Initializes the object
|
||||
/// - Parameter maxQueueItems: The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
|
||||
init(maxQueueItems: Int) {
|
||||
self.maxQueueItems = maxQueueItems
|
||||
}
|
||||
|
||||
/// The async stream, used for listening for notifications
|
||||
///
|
||||
/// This will first stream the queued "inbox" items that the listener may have missed, and then it will do a real-time stream of new items as they come in.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```swift
|
||||
/// for await notification in queueableNotify.stream {
|
||||
/// // Do something with the notification
|
||||
/// }
|
||||
/// ```
|
||||
var stream: AsyncStream<T> {
|
||||
return AsyncStream { continuation in
|
||||
// Stream queued "inbox" items that the listener may have missed
|
||||
for item in queue {
|
||||
continuation.yield(item)
|
||||
}
|
||||
|
||||
// Clean up if the stream closes
|
||||
continuation.onTermination = { continuation in
|
||||
Task { await self.cleanup() }
|
||||
}
|
||||
|
||||
// Point to this stream, so that it can receive new updates
|
||||
self.continuation = continuation
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up after a stream is closed by the listener
|
||||
private func cleanup() {
|
||||
self.continuation = nil // This will cause new items to be queued for when another listener is attached
|
||||
}
|
||||
|
||||
/// Adds a new notification item to be handled by a listener.
|
||||
///
|
||||
/// This will automatically stream the new item to the listener, or queue the item if no one is listening
|
||||
func add(item: T) {
|
||||
while queue.count >= maxQueueItems { queue.removeFirst() } // Ensures queue stays within the desired size
|
||||
guard let continuation else {
|
||||
// No one is listening, queue it (send it to an inbox for later handling)
|
||||
queue.append(item)
|
||||
return
|
||||
}
|
||||
// Send directly to the active listener stream
|
||||
continuation.yield(item)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -21,6 +21,17 @@ let ANON_PUBKEY = Pubkey(Data([
|
||||
struct FullKeypair: Equatable {
|
||||
let pubkey: Pubkey
|
||||
let privkey: Privkey
|
||||
|
||||
init(pubkey: Pubkey, privkey: Privkey) {
|
||||
self.pubkey = pubkey
|
||||
self.privkey = privkey
|
||||
}
|
||||
|
||||
init?(privkey: Privkey) {
|
||||
self.privkey = privkey
|
||||
guard let pubkey = privkey_to_pubkey_raw(sec: privkey.bytes) else { return nil }
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
func to_keypair() -> Keypair {
|
||||
return Keypair(pubkey: pubkey, privkey: privkey)
|
||||
|
||||
@@ -14,6 +14,7 @@ enum LogCategory: String {
|
||||
case render
|
||||
case storage
|
||||
case networking
|
||||
case timeline
|
||||
case push_notifications
|
||||
case damus_purple
|
||||
case image_uploading
|
||||
|
||||
@@ -32,7 +32,7 @@ enum Route: Hashable {
|
||||
case DeveloperSettings(settings: UserSettingsStore)
|
||||
case FirstAidSettings(settings: UserSettingsStore)
|
||||
case Thread(thread: ThreadModel)
|
||||
case ThreadFromReference(note_reference: LoadableThreadModel.NoteReference)
|
||||
case LoadableNostrEvent(note_reference: LoadableNostrEventViewModel.NoteReference)
|
||||
case Reposts(reposts: EventsModel)
|
||||
case QuoteReposts(quotes: EventsModel)
|
||||
case Reactions(reactions: EventsModel)
|
||||
@@ -97,8 +97,8 @@ enum Route: Hashable {
|
||||
case .Thread(let thread):
|
||||
ChatroomThreadView(damus: damusState, thread: thread)
|
||||
//ThreadView(state: damusState, thread: thread)
|
||||
case .ThreadFromReference(let note_reference):
|
||||
LoadableThreadView(state: damusState, note_reference: note_reference)
|
||||
case .LoadableNostrEvent(let note_reference):
|
||||
LoadableNostrEventView(state: damusState, note_reference: note_reference)
|
||||
case .Reposts(let reposts):
|
||||
RepostsView(damus_state: damusState, model: reposts)
|
||||
case .QuoteReposts(let quote_reposts):
|
||||
@@ -190,8 +190,8 @@ enum Route: Hashable {
|
||||
case .Thread(let threadModel):
|
||||
hasher.combine("thread")
|
||||
hasher.combine(threadModel.original_event.id)
|
||||
case .ThreadFromReference(note_reference: let note_reference):
|
||||
hasher.combine("thread_from_reference")
|
||||
case .LoadableNostrEvent(note_reference: let note_reference):
|
||||
hasher.combine("loadable_nostr_event")
|
||||
hasher.combine(note_reference)
|
||||
case .Reposts(let reposts):
|
||||
hasher.combine("reposts")
|
||||
|
||||
30
damus/Util/Undistractor.swift
Normal file
30
damus/Util/Undistractor.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Undistractor.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-19.
|
||||
//
|
||||
|
||||
/// Keeping the minds of developers safe from the occupational hazard of social media distractions when testing Damus since 2025
|
||||
struct Undistractor {
|
||||
static func makeGibberish(text: String) -> String {
|
||||
let lowercaseLetters = "abcdefghijklmnopqrstuvwxyz"
|
||||
let uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
var transformedText = ""
|
||||
|
||||
for char in text {
|
||||
if lowercaseLetters.contains(char) {
|
||||
if let randomLetter = lowercaseLetters.randomElement() {
|
||||
transformedText.append(randomLetter)
|
||||
}
|
||||
} else if uppercaseLetters.contains(char) {
|
||||
if let randomLetter = uppercaseLetters.randomElement() {
|
||||
transformedText.append(randomLetter)
|
||||
}
|
||||
} else {
|
||||
transformedText.append(char)
|
||||
}
|
||||
}
|
||||
return transformedText
|
||||
}
|
||||
}
|
||||
@@ -298,7 +298,7 @@ struct ChatEventView: View {
|
||||
}
|
||||
.swipeSpacing(-20)
|
||||
.swipeActionsStyle(.mask)
|
||||
.swipeMinimumDistance(20)
|
||||
.swipeMinimumDistance(40)
|
||||
.swipeDragGesturePriority(.normal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ struct EventView: View {
|
||||
|
||||
// blame the porn bots for this code
|
||||
func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool {
|
||||
if settings.undistractMode {
|
||||
return true
|
||||
}
|
||||
|
||||
if !settings.blur_images {
|
||||
return false
|
||||
}
|
||||
|
||||
275
damus/Views/LoadableNostrEventView.swift
Normal file
275
damus/Views/LoadableNostrEventView.swift
Normal file
@@ -0,0 +1,275 @@
|
||||
//
|
||||
// LoadableNostrEventView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D'Aquino on 2025-01-08.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
/// A view model for `LoadableNostrEventView`
|
||||
///
|
||||
/// This takes a nostr event reference, automatically tries to load it, and updates itself to reflect its current state
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This is on the main actor because `ObservableObjects` with `Published` properties should be on the main actor for thread-safety.
|
||||
///
|
||||
@MainActor
|
||||
class LoadableNostrEventViewModel: ObservableObject {
|
||||
let damus_state: DamusState
|
||||
let note_reference: NoteReference
|
||||
@Published var state: ThreadModelLoadingState = .loading
|
||||
/// The time period after which it will give up loading the view.
|
||||
/// Written in nanoseconds
|
||||
let TIMEOUT: UInt64 = 10 * 1_000_000_000 // 10 seconds
|
||||
|
||||
init(damus_state: DamusState, note_reference: NoteReference) {
|
||||
self.damus_state = damus_state
|
||||
self.note_reference = note_reference
|
||||
Task { await self.load() }
|
||||
}
|
||||
|
||||
func load() async {
|
||||
// Start the loading process in a separate task to manage the timeout independently.
|
||||
let loadTask = Task { @MainActor in
|
||||
self.state = await executeLoadingLogic(note_reference: self.note_reference)
|
||||
}
|
||||
|
||||
// Setup a timer to cancel the load after the timeout period
|
||||
let timeoutTask = Task { @MainActor in
|
||||
try await Task.sleep(nanoseconds: TIMEOUT)
|
||||
loadTask.cancel() // This sends a cancellation signal to the load task.
|
||||
self.state = .not_found
|
||||
}
|
||||
|
||||
await loadTask.value
|
||||
timeoutTask.cancel() // Cancel the timeout task if loading finishes earlier.
|
||||
}
|
||||
|
||||
/// Asynchronously find an event from NostrDB or from the network (if not available on NostrDB)
|
||||
private func loadEvent(noteId: NoteId) async -> NostrEvent? {
|
||||
let res = await find_event(state: damus_state, query: .event(evid: noteId))
|
||||
guard let res, case .event(let ev) = res else { return nil }
|
||||
return ev
|
||||
}
|
||||
|
||||
/// Gets the note reference and tries to load it, outputting a new state for this view model.
|
||||
private func executeLoadingLogic(note_reference: NoteReference) async -> ThreadModelLoadingState {
|
||||
switch note_reference {
|
||||
case .note_id(let note_id):
|
||||
guard let ev = await self.loadEvent(noteId: note_id) else { return .not_found }
|
||||
guard let known_kind = ev.known_kind else { return .unknown_or_unsupported_kind }
|
||||
switch known_kind {
|
||||
case .text, .highlight:
|
||||
return .loaded(route: Route.Thread(thread: ThreadModel(event: ev, damus_state: damus_state)))
|
||||
case .dm:
|
||||
let dm_model = damus_state.dms.lookup_or_create(ev.pubkey)
|
||||
return .loaded(route: Route.DMChat(dms: dm_model))
|
||||
case .like:
|
||||
// Load the event that this reaction refers to.
|
||||
guard let first_referenced_note_id = ev.referenced_ids.first else { return .not_found }
|
||||
return await self.executeLoadingLogic(note_reference: .note_id(first_referenced_note_id))
|
||||
case .zap, .zap_request:
|
||||
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
|
||||
return .loaded(route: Route.Zaps(target: zap.target))
|
||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .zap, .zap_request, .nwc_request, .nwc_response, .http_auth, .status:
|
||||
return .unknown_or_unsupported_kind
|
||||
}
|
||||
case .naddr(let naddr):
|
||||
guard let event = await naddrLookup(damus_state: damus_state, naddr: naddr) else { return .not_found }
|
||||
return .loaded(route: Route.Thread(thread: ThreadModel(event: event, damus_state: damus_state)))
|
||||
}
|
||||
}
|
||||
|
||||
enum ThreadModelLoadingState {
|
||||
case loading
|
||||
case loaded(route: Route)
|
||||
case not_found
|
||||
case unknown_or_unsupported_kind
|
||||
}
|
||||
|
||||
enum NoteReference: Hashable {
|
||||
case note_id(NoteId)
|
||||
case naddr(NAddr)
|
||||
}
|
||||
}
|
||||
|
||||
/// A view for a Nostr event that has not been loaded yet.
|
||||
/// This takes a Nostr event reference and loads it, while providing nice loading UX and graceful error handling.
|
||||
struct LoadableNostrEventView: View {
|
||||
let state: DamusState
|
||||
@StateObject var loadableModel: LoadableNostrEventViewModel
|
||||
var loading: Bool {
|
||||
switch loadableModel.state {
|
||||
case .loading:
|
||||
return true
|
||||
case .loaded, .not_found, .unknown_or_unsupported_kind:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init(state: DamusState, note_reference: LoadableNostrEventViewModel.NoteReference) {
|
||||
self.state = state
|
||||
self._loadableModel = StateObject.init(wrappedValue: LoadableNostrEventViewModel(damus_state: state, note_reference: note_reference))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
switch self.loadableModel.state {
|
||||
case .loading:
|
||||
ScrollView(.vertical) {
|
||||
self.skeleton
|
||||
.redacted(reason: loading ? .placeholder : [])
|
||||
.shimmer(loading)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(NSLocalizedString("Loading thread", comment: "Accessibility label for the thread view when it is loading"))
|
||||
}
|
||||
case .loaded(route: let route):
|
||||
route.view(navigationCoordinator: state.nav, damusState: state)
|
||||
case .not_found:
|
||||
self.not_found
|
||||
case .unknown_or_unsupported_kind:
|
||||
self.unknown_or_unsupported_kind
|
||||
}
|
||||
}
|
||||
|
||||
var not_found: some View {
|
||||
SomethingWrong(
|
||||
imageSystemName: "questionmark.app",
|
||||
heading: NSLocalizedString("Note not found", comment: "Heading for the thread view in a not found error state."),
|
||||
description: NSLocalizedString("We were unable to find the note you were looking for.", comment: "Text for the thread view when it is unable to find the note the user is looking for"),
|
||||
advice: NSLocalizedString("Try checking the link again, your internet connection, or contact the person who provided you the link for help.", comment: "Tips on what to do if a note cannot be found.")
|
||||
)
|
||||
}
|
||||
|
||||
var unknown_or_unsupported_kind: some View {
|
||||
SomethingWrong(
|
||||
imageSystemName: "questionmark.app",
|
||||
heading: NSLocalizedString("Can’t display note", comment: "User-visible heading for an error message indicating a note has an unknown kind or is unsupported for viewing."),
|
||||
description: NSLocalizedString("We do not yet support viewing this type of content.", comment: "User-visible description of an error indicating a note has an unknown kind or is unsupported for viewing."),
|
||||
advice: NSLocalizedString("Please try opening this content on another Nostr app that supports this type of content.", comment: "User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing.")
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Skeleton views
|
||||
// Implementation notes
|
||||
// - No localization is needed because the text will be redacted
|
||||
// - No accessibility label is needed because these will be summarized into a single accessibility label at the top-level view. See `body` in this struct
|
||||
|
||||
var skeleton: some View {
|
||||
VStack(alignment: .leading, spacing: 40) {
|
||||
Self.skeleton_selected_event
|
||||
Self.skeleton_chat_event(message: "Nice! Have you tried Damus?", right: false)
|
||||
Self.skeleton_chat_event(message: "Yes, it's awesome.", right: true)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
static func skeleton_chat_event(message: String, right: Bool) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
if !right {
|
||||
self.skeleton_chat_user_avatar
|
||||
}
|
||||
else {
|
||||
Spacer()
|
||||
}
|
||||
ChatBubble(
|
||||
direction: right ? .right : .left,
|
||||
stroke_content: Color.accentColor.opacity(0),
|
||||
stroke_style: .init(lineWidth: 4),
|
||||
background_style: Color.secondary.opacity(0.5),
|
||||
content: {
|
||||
Text(verbatim: message)
|
||||
.padding()
|
||||
}
|
||||
)
|
||||
if right {
|
||||
self.skeleton_chat_user_avatar
|
||||
}
|
||||
else {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var skeleton_selected_event: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 50, height: 50)
|
||||
.foregroundStyle(.secondary.opacity(0.5))
|
||||
Text(verbatim: "Satoshi Nakamoto")
|
||||
.bold()
|
||||
}
|
||||
Text(verbatim: "Nostr is the super app. Because 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.")
|
||||
HStack {
|
||||
self.skeleton_action_item
|
||||
Spacer()
|
||||
self.skeleton_action_item
|
||||
Spacer()
|
||||
self.skeleton_action_item
|
||||
Spacer()
|
||||
self.skeleton_action_item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var skeleton_chat_user_avatar: some View {
|
||||
Circle()
|
||||
.fill(.secondary.opacity(0.5))
|
||||
.frame(width: 35, height: 35)
|
||||
.padding(.bottom, -21)
|
||||
}
|
||||
|
||||
static var skeleton_action_item: some View {
|
||||
Circle()
|
||||
.fill(Color.secondary.opacity(0.5))
|
||||
.frame(width: 25, height: 25)
|
||||
}
|
||||
}
|
||||
|
||||
extension LoadableNostrEventView {
|
||||
struct SomethingWrong: View {
|
||||
let imageSystemName: String
|
||||
let heading: String
|
||||
let description: String
|
||||
let advice: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 6) {
|
||||
Image(systemName: imageSystemName)
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.accessibilityHidden(true)
|
||||
Text(heading)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding(.bottom, 10)
|
||||
Text(description)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "sparkles")
|
||||
.accessibilityHidden(true)
|
||||
Text("Advice", comment: "Heading for some advice text to help the user with an error")
|
||||
.font(.headline)
|
||||
}
|
||||
Text(advice)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.secondary.opacity(0.2))
|
||||
.cornerRadius(10)
|
||||
.padding(.vertical, 30)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Loadable") {
|
||||
LoadableNostrEventView(state: test_damus_state, note_reference: .note_id(test_thread_note_1.id))
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
//
|
||||
// LoadableThreadView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D'Aquino on 2025-01-08.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
/// A view model for `LoadableThreadView`
|
||||
///
|
||||
/// This takes a note reference, automatically tries to load it, and updates itself to reflect its current state
|
||||
///
|
||||
///
|
||||
class LoadableThreadModel: ObservableObject {
|
||||
let damus_state: DamusState
|
||||
let note_reference: NoteReference
|
||||
@Published var state: ThreadModelLoadingState = .loading
|
||||
/// The time period after which it will give up loading the view.
|
||||
/// Written in nanoseconds
|
||||
let TIMEOUT: UInt64 = 10 * 1_000_000_000 // 10 seconds
|
||||
|
||||
init(damus_state: DamusState, note_reference: NoteReference) {
|
||||
self.damus_state = damus_state
|
||||
self.note_reference = note_reference
|
||||
Task { await self.load() }
|
||||
}
|
||||
|
||||
func load() async {
|
||||
// Start the loading process in a separate task to manage the timeout independently.
|
||||
let loadTask = Task { @MainActor in
|
||||
self.state = await executeLoadingLogic()
|
||||
}
|
||||
|
||||
// Setup a timer to cancel the load after the timeout period
|
||||
let timeoutTask = Task { @MainActor in
|
||||
try await Task.sleep(nanoseconds: TIMEOUT)
|
||||
loadTask.cancel() // This sends a cancellation signal to the load task.
|
||||
self.state = .not_found
|
||||
}
|
||||
|
||||
await loadTask.value
|
||||
timeoutTask.cancel() // Cancel the timeout task if loading finishes earlier.
|
||||
}
|
||||
|
||||
private func executeLoadingLogic() async -> ThreadModelLoadingState {
|
||||
switch note_reference {
|
||||
case .note_id(let note_id):
|
||||
let res = await find_event(state: damus_state, query: .event(evid: note_id))
|
||||
guard let res, case .event(let ev) = res else { return .not_found }
|
||||
return .loaded(model: await ThreadModel(event: ev, damus_state: damus_state))
|
||||
case .naddr(let naddr):
|
||||
guard let event = await naddrLookup(damus_state: damus_state, naddr: naddr) else { return .not_found }
|
||||
return .loaded(model: await ThreadModel(event: event, damus_state: damus_state))
|
||||
}
|
||||
}
|
||||
|
||||
enum ThreadModelLoadingState {
|
||||
case loading
|
||||
case loaded(model: ThreadModel)
|
||||
case not_found
|
||||
}
|
||||
|
||||
enum NoteReference: Hashable {
|
||||
case note_id(NoteId)
|
||||
case naddr(NAddr)
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadableThreadView: View {
|
||||
let state: DamusState
|
||||
@StateObject var loadable_thread: LoadableThreadModel
|
||||
var loading: Bool {
|
||||
switch loadable_thread.state {
|
||||
case .loading:
|
||||
return true
|
||||
case .loaded, .not_found:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init(state: DamusState, note_reference: LoadableThreadModel.NoteReference) {
|
||||
self.state = state
|
||||
self._loadable_thread = StateObject.init(wrappedValue: LoadableThreadModel(damus_state: state, note_reference: note_reference))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
switch self.loadable_thread.state {
|
||||
case .loading:
|
||||
ScrollView(.vertical) {
|
||||
self.skeleton
|
||||
.redacted(reason: loading ? .placeholder : [])
|
||||
.shimmer(loading)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(NSLocalizedString("Loading thread", comment: "Accessibility label for the thread view when it is loading"))
|
||||
}
|
||||
case .loaded(model: let thread_model):
|
||||
ChatroomThreadView(damus: state, thread: thread_model)
|
||||
case .not_found:
|
||||
self.not_found
|
||||
}
|
||||
}
|
||||
|
||||
var not_found: some View {
|
||||
VStack(spacing: 6) {
|
||||
Image(systemName: "questionmark.app")
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.accessibilityHidden(true)
|
||||
Text("Note not found", comment: "Heading for the thread view in a not found error state")
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding(.bottom, 10)
|
||||
Text("We were unable to find the note you were looking for.", comment: "Text for the thread view when it is unable to find the note the user is looking for")
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "sparkles")
|
||||
.accessibilityHidden(true)
|
||||
Text("Advice", comment: "Heading for some advice text to help the user with an error")
|
||||
.font(.headline)
|
||||
}
|
||||
Text("Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.", comment: "Tips on what to do if a note cannot be found.")
|
||||
}
|
||||
.padding()
|
||||
.background(Color.secondary.opacity(0.2))
|
||||
.cornerRadius(10)
|
||||
.padding(.vertical, 30)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
// MARK: Skeleton views
|
||||
// Implementation notes
|
||||
// - No localization is needed because the text will be redacted
|
||||
// - No accessibility label is needed because these will be summarized into a single accessibility label at the top-level view. See `body` in this struct
|
||||
|
||||
var skeleton: some View {
|
||||
VStack(alignment: .leading, spacing: 40) {
|
||||
self.skeleton_selected_event
|
||||
self.skeleton_chat_event(message: "Nice! Have you tried Damus?", right: false)
|
||||
self.skeleton_chat_event(message: "Yes, it's awesome.", right: true)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
func skeleton_chat_event(message: String, right: Bool) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
if !right {
|
||||
self.skeleton_chat_user_avatar
|
||||
}
|
||||
else {
|
||||
Spacer()
|
||||
}
|
||||
ChatBubble(
|
||||
direction: right ? .right : .left,
|
||||
stroke_content: Color.accentColor.opacity(0),
|
||||
stroke_style: .init(lineWidth: 4),
|
||||
background_style: Color.secondary.opacity(0.5),
|
||||
content: {
|
||||
Text(message)
|
||||
.padding()
|
||||
}
|
||||
)
|
||||
if right {
|
||||
self.skeleton_chat_user_avatar
|
||||
}
|
||||
else {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var skeleton_selected_event: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 50, height: 50)
|
||||
.foregroundStyle(.secondary.opacity(0.5))
|
||||
Text("Satoshi Nakamoto")
|
||||
.bold()
|
||||
}
|
||||
Text("Nostr is the super app. Because 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.")
|
||||
HStack {
|
||||
self.skeleton_action_item
|
||||
Spacer()
|
||||
self.skeleton_action_item
|
||||
Spacer()
|
||||
self.skeleton_action_item
|
||||
Spacer()
|
||||
self.skeleton_action_item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var skeleton_chat_user_avatar: some View {
|
||||
Circle()
|
||||
.fill(.secondary.opacity(0.5))
|
||||
.frame(width: 35, height: 35)
|
||||
.padding(.bottom, -21)
|
||||
}
|
||||
|
||||
var skeleton_action_item: some View {
|
||||
Circle()
|
||||
.fill(Color.secondary.opacity(0.5))
|
||||
.frame(width: 25, height: 25)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Loadable") {
|
||||
LoadableThreadView(state: test_damus_state, note_reference: .note_id(test_thread_note_1.id))
|
||||
}
|
||||
@@ -40,6 +40,9 @@ struct NoteContentView: View {
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
var note_artifacts: NoteArtifacts {
|
||||
if damus_state.settings.undistractMode {
|
||||
return .separated(.just_content(Undistractor.makeGibberish(text: event.get_content(damus_state.keypair))))
|
||||
}
|
||||
return self.artifacts_model.state.artifacts ?? .separated(.just_content(event.get_content(damus_state.keypair)))
|
||||
}
|
||||
|
||||
|
||||
@@ -60,21 +60,8 @@ struct NotificationsView: View {
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var mystery: some View {
|
||||
let profile_txn = state.profiles.lookup(id: state.pubkey)
|
||||
let profile = profile_txn?.unsafeUnownedValue
|
||||
return VStack(spacing: 20) {
|
||||
Text("Wake up, \(Profile.displayName(profile: profile, pubkey: state.pubkey).displayName.truncate(maxLength: 50))", comment: "Text telling the user to wake up, where the argument is their display name.")
|
||||
Text("You are dreaming...", comment: "Text telling the user that they are dreaming.")
|
||||
}
|
||||
.id("what")
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $filter_state) {
|
||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||
mystery
|
||||
|
||||
NotificationTab(
|
||||
NotificationFilter(
|
||||
state: .all,
|
||||
|
||||
@@ -71,9 +71,12 @@ struct PostView: View {
|
||||
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
|
||||
@State var newCursorIndex: Int?
|
||||
@State var textHeight: CGFloat? = nil
|
||||
@State var saved_state: SaveState = .needs_saving()
|
||||
/// A timer that helps us add a delay between when changes occur and when they are saved persistently (to avoid too many disk writes and a jittery save indicator)
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
/// Manages the auto-save logic for drafts.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This intentionally does _not_ use `@ObservedObject` or `@StateObject` because observing changes causes unwanted automatic scrolling to the text cursor on each save state update.
|
||||
var autoSaveModel: AutoSaveIndicatorView.AutoSaveViewModel
|
||||
|
||||
@State var preUploadedMedia: [PreUploadedMedia] = []
|
||||
|
||||
@@ -89,16 +92,6 @@ struct PostView: View {
|
||||
let placeholder_messages: [String]
|
||||
let initial_text_suffix: String?
|
||||
|
||||
enum SaveState: Equatable {
|
||||
/// The draft has been modified and needs saving.
|
||||
/// Saving should occur in N seconds
|
||||
case needs_saving(seconds_remaining: Int = 3)
|
||||
/// A saving operation is in progress
|
||||
case saving
|
||||
/// The draft has been saved to disk.
|
||||
case saved
|
||||
}
|
||||
|
||||
init(
|
||||
action: PostAction,
|
||||
damus_state: DamusState,
|
||||
@@ -111,6 +104,7 @@ struct PostView: View {
|
||||
self.prompt_view = prompt_view
|
||||
self.placeholder_messages = placeholder_messages ?? [POST_PLACEHOLDER]
|
||||
self.initial_text_suffix = initial_text_suffix
|
||||
self.autoSaveModel = AutoSaveIndicatorView.AutoSaveViewModel(save: { damus_state.drafts.save(damus_state: damus_state) })
|
||||
}
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@@ -183,33 +177,12 @@ struct PostView: View {
|
||||
})
|
||||
}
|
||||
|
||||
var save_state_indicator: some View {
|
||||
HStack {
|
||||
switch saved_state {
|
||||
case .needs_saving:
|
||||
EmptyView()
|
||||
.accessibilityHidden(true) // Probably no need to show this to visually impaired users, might be too noisy
|
||||
case .saving:
|
||||
ProgressView()
|
||||
.accessibilityHidden(true) // Probably no need to show this to visually impaired users, might be too noisy
|
||||
case .saved:
|
||||
Image(systemName: "checkmark")
|
||||
.accessibilityHidden(true)
|
||||
Text("Saved", comment: "Small label indicating that the user's draft has been saved to storage")
|
||||
.accessibilityLabel(NSLocalizedString("Your draft has been saved to storage", comment: "Accessibility label indicating that a user's post draft has been saved, meant only for visually impaired users"))
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.padding(6)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
var AttachmentBar: some View {
|
||||
HStack(alignment: .center, spacing: 15) {
|
||||
ImageButton
|
||||
CameraButton
|
||||
Spacer()
|
||||
self.save_state_indicator
|
||||
AutoSaveIndicatorView(saveViewModel: self.autoSaveModel)
|
||||
}
|
||||
.disabled(uploading_disabled)
|
||||
}
|
||||
@@ -257,19 +230,20 @@ struct PostView: View {
|
||||
damus_state.drafts.post = nil
|
||||
}
|
||||
|
||||
damus_state.drafts.save(damus_state: damus_state)
|
||||
}
|
||||
|
||||
func load_draft() -> Bool {
|
||||
guard let draft = load_draft_for_post(drafts: self.damus_state.drafts, action: self.action) else {
|
||||
self.post = NSMutableAttributedString("")
|
||||
self.uploadedMedias = []
|
||||
self.saved_state = .needs_saving()
|
||||
self.autoSaveModel.markNothingToSave() // We should not save empty drafts.
|
||||
return false
|
||||
}
|
||||
|
||||
self.uploadedMedias = draft.media
|
||||
self.post = draft.content
|
||||
self.saved_state = .saved
|
||||
self.autoSaveModel.markSaved() // The draft we just loaded is saved to memory. Mark it as such.
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -287,7 +261,7 @@ struct PostView: View {
|
||||
let artifacts = DraftArtifacts(content: post, media: uploadedMedias, references: references, id: UUID().uuidString)
|
||||
set_draft_for_post(drafts: damus_state.drafts, action: action, artifacts: artifacts)
|
||||
}
|
||||
self.saved_state = .needs_saving()
|
||||
self.autoSaveModel.needsSaving()
|
||||
}
|
||||
|
||||
var TextEntry: some View {
|
||||
@@ -602,21 +576,6 @@ struct PostView: View {
|
||||
preUploadedMedia.removeAll()
|
||||
}
|
||||
}
|
||||
.onReceive(timer) { time in
|
||||
switch self.saved_state {
|
||||
case .needs_saving(seconds_remaining: let seconds_remaining):
|
||||
if seconds_remaining <= 0 {
|
||||
self.saved_state = .saving
|
||||
damus_state.drafts.save(damus_state: damus_state)
|
||||
self.saved_state = .saved
|
||||
}
|
||||
else {
|
||||
self.saved_state = .needs_saving(seconds_remaining: seconds_remaining - 1)
|
||||
}
|
||||
case .saving, .saved:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -906,33 +865,29 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
|
||||
|
||||
|
||||
var content = post.string
|
||||
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
|
||||
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: "\n")
|
||||
|
||||
if !imagesString.isEmpty {
|
||||
content.append(" " + imagesString + " ")
|
||||
content.append("\n\n" + imagesString)
|
||||
}
|
||||
|
||||
var tags: [[String]] = []
|
||||
|
||||
switch action {
|
||||
case .replying_to(let replying_to):
|
||||
// start off with the reply tags
|
||||
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
||||
case .replying_to(let replying_to):
|
||||
// start off with the reply tags
|
||||
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
||||
|
||||
case .quoting(let ev):
|
||||
content.append(" nostr:" + bech32_note_id(ev.id))
|
||||
case .quoting(let ev):
|
||||
content.append("\n\nnostr:" + bech32_note_id(ev.id))
|
||||
|
||||
if let quoted_ev = state.events.lookup(ev.id) {
|
||||
tags.append(["p", quoted_ev.pubkey.hex()])
|
||||
}
|
||||
case .posting(let postTarget):
|
||||
break
|
||||
case .highlighting(let draft):
|
||||
break
|
||||
case .sharing(_):
|
||||
break
|
||||
if let quoted_ev = state.events.lookup(ev.id) {
|
||||
tags.append(["p", quoted_ev.pubkey.hex()])
|
||||
}
|
||||
case .posting, .highlighting, .sharing:
|
||||
break
|
||||
}
|
||||
|
||||
// append additional tags
|
||||
@@ -954,7 +909,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
|
||||
}
|
||||
}
|
||||
|
||||
return NostrPost(content: content, kind: .text, tags: tags)
|
||||
return NostrPost(content: content.trimmingCharacters(in: .whitespacesAndNewlines), kind: .text, tags: tags)
|
||||
}
|
||||
|
||||
func isSupportedVideo(url: URL?) -> Bool {
|
||||
|
||||
136
damus/Views/Posting/AutoSaveIndicatorView.swift
Normal file
136
damus/Views/Posting/AutoSaveIndicatorView.swift
Normal file
@@ -0,0 +1,136 @@
|
||||
//
|
||||
// AutoSaveIndicatorView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-12.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
/// A small indicator view to indicate whether an item has been saved or not.
|
||||
///
|
||||
/// This view uses and observes an `AutoSaveViewModel`.
|
||||
struct AutoSaveIndicatorView: View {
|
||||
@ObservedObject var saveViewModel: AutoSaveViewModel
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
switch saveViewModel.savedState {
|
||||
case .needsSaving, .nothingToSave:
|
||||
EmptyView()
|
||||
.accessibilityHidden(true) // Probably no need to show this to users with visual impairment, might be too noisy.
|
||||
case .saving:
|
||||
ProgressView()
|
||||
.accessibilityHidden(true) // Probably no need to show this to users with visual impairment, might be too noisy.
|
||||
case .saved:
|
||||
Image(systemName: "checkmark")
|
||||
.accessibilityHidden(true)
|
||||
Text("Saved", comment: "Small label indicating that the user's draft has been saved to storage.")
|
||||
.accessibilityLabel(NSLocalizedString("Your draft has been saved to storage.", comment: "Accessibility label indicating that a user's post draft has been saved, meant to be read by screen reading technology."))
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.padding(6)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension AutoSaveIndicatorView {
|
||||
/// A simple data structure to model the saving state of an item that can be auto-saved every few seconds.
|
||||
enum SaveState: Equatable {
|
||||
/// There is nothing to save (e.g. A new empty item was just created, an item was just loaded)
|
||||
case nothingToSave
|
||||
/// The item has been modified and needs saving.
|
||||
/// Saving should occur in N seconds.
|
||||
case needsSaving(secondsRemaining: Int)
|
||||
/// A saving operation is in progress.
|
||||
case saving
|
||||
/// The item has been saved to disk.
|
||||
case saved
|
||||
}
|
||||
|
||||
/// Models an auto-save mechanism, which automatically saves an item after N seconds.
|
||||
///
|
||||
/// # Implementation notes
|
||||
///
|
||||
/// - This runs on the main actor because running this on other actors causes issues with published properties.
|
||||
/// - Running on one actor helps ensure thread safety.
|
||||
@MainActor
|
||||
class AutoSaveViewModel: ObservableObject {
|
||||
/// The delay between the time something is marked as needing to save, and the actual saving operation.
|
||||
///
|
||||
/// Should be low enough that the user does not lose significant progress, and should be high enough to avoid unnecessary disk writes and jittery, stress-inducing behavior
|
||||
let saveDelay: Int
|
||||
/// The current state of this model
|
||||
@Published private(set) var savedState: SaveState
|
||||
/// A timer which counts down the time to save the item
|
||||
private var timer: Timer?
|
||||
/// The function that performs the actual save operation
|
||||
var save: () async -> Void
|
||||
|
||||
|
||||
// MARK: Init/de-init
|
||||
|
||||
/// Initializes a new auto-save model
|
||||
/// - Parameters:
|
||||
/// - save: The function that performs the save operation
|
||||
/// - initialState: Optional initial state
|
||||
/// - saveDelay: The time delay between the item is marked as needing to be saved, and the actual save operation — denoted in seconds.
|
||||
init(save: @escaping () async -> Void, initialState: SaveState = .nothingToSave, saveDelay: Int = 3) {
|
||||
self.saveDelay = saveDelay
|
||||
self.savedState = initialState
|
||||
self.save = save
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { timer in
|
||||
Task { await self.tick() } // Task { await ... } ensures the function is properly run on the main actor and avoids thread-safety issues
|
||||
})
|
||||
self.timer = timer
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let timer = self.timer {
|
||||
timer.isValid ? timer.invalidate() : ()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Internal logic
|
||||
|
||||
/// Runs internal countdown-to-save logic
|
||||
private func tick() async {
|
||||
switch self.savedState {
|
||||
case .needsSaving(secondsRemaining: let secondsRemaining):
|
||||
if secondsRemaining <= 0 {
|
||||
self.savedState = .saving
|
||||
await save()
|
||||
self.savedState = .saved
|
||||
}
|
||||
else {
|
||||
self.savedState = .needsSaving(secondsRemaining: secondsRemaining - 1)
|
||||
}
|
||||
case .saving, .saved, .nothingToSave:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: External interface
|
||||
|
||||
/// Marks item as needing to be saved.
|
||||
/// Call this whenever your item is modified.
|
||||
func needsSaving() {
|
||||
self.savedState = .needsSaving(secondsRemaining: self.saveDelay)
|
||||
}
|
||||
|
||||
/// Marks item as saved.
|
||||
/// Call this when you know the item is already saved (e.g. when loading a saved item from memory).
|
||||
func markSaved() {
|
||||
self.savedState = .saved
|
||||
}
|
||||
|
||||
/// Tells the auto-save logic that there is nothing to be saved.
|
||||
/// Call this when there is nothing to be saved (e.g. when opening a new empty item).
|
||||
func markNothingToSave() {
|
||||
self.savedState = .nothingToSave
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,9 @@ struct EditMetadataView: View {
|
||||
TextField(NSLocalizedString("Lightning Address or LNURL", comment: "Placeholder text for entry of Lightning Address or LNURL."), text: $ln)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
.onReceive(Just(ln)) { newValue in
|
||||
self.ln = newValue.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}
|
||||
|
||||
Section(content: {
|
||||
|
||||
@@ -122,6 +122,12 @@ struct ProfileView: View {
|
||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||
var filters = ContentFilters.defaults(damus_state: damus_state)
|
||||
filters.append(fstate.filter)
|
||||
switch fstate {
|
||||
case .posts, .posts_and_replies:
|
||||
filters.append({ profile.pubkey == $0.pubkey })
|
||||
case .conversations:
|
||||
filters.append({ profile.conversation_events.contains($0.id) } )
|
||||
}
|
||||
return ContentFilters(filters: filters).filter
|
||||
}
|
||||
|
||||
@@ -429,6 +435,17 @@ struct ProfileView: View {
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
var tabs: [(String, FilterState)] {
|
||||
var tabs = [
|
||||
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
|
||||
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
|
||||
]
|
||||
if profile.pubkey != damus_state.pubkey && !profile.conversation_events.isEmpty {
|
||||
tabs.append((NSLocalizedString("Conversations", comment: "Label for filter for seeing notes and replies that involve conversations between the signed in user and the current profile."), FilterState.conversations))
|
||||
}
|
||||
return tabs
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ScrollView(.vertical) {
|
||||
@@ -440,10 +457,7 @@ struct ProfileView: View {
|
||||
aboutSection
|
||||
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(tabs: [
|
||||
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
|
||||
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
|
||||
], selection: $filter_state)
|
||||
CustomPicker(tabs: tabs, selection: $filter_state)
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
@@ -455,6 +469,9 @@ struct ProfileView: View {
|
||||
if filter_state == FilterState.posts_and_replies {
|
||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies))
|
||||
}
|
||||
if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty {
|
||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.conversations))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Theme.safeAreaInsets?.left)
|
||||
.zIndex(-yOffset > navbarHeight ? 0 : 1)
|
||||
|
||||
@@ -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 RepostedEvent: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
NavigationLink(value: Route.ProfileByKey(pubkey: event.pubkey)) {
|
||||
Reposted(damus: damus, pubkey: event.pubkey)
|
||||
Reposted(damus: damus, pubkey: event.pubkey, target: inner_ev)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
@@ -17,6 +17,7 @@ struct DeveloperSettingsView: View {
|
||||
Toggle(NSLocalizedString("Developer Mode", comment: "Setting to enable developer mode"), isOn: $settings.developer_mode)
|
||||
.toggleStyle(.switch)
|
||||
if settings.developer_mode {
|
||||
Toggle(NSLocalizedString("Undistract mode", comment: "Developer mode setting to scramble text and images to avoid distractions during development."), isOn: $settings.undistractMode)
|
||||
Toggle(NSLocalizedString("Always show onboarding", comment: "Developer mode setting to always show onboarding suggestions."), isOn: $settings.always_show_onboarding_suggestions)
|
||||
Picker(NSLocalizedString("Push notification environment", comment: "Prompt selection of the Push notification environment (Developer feature to switch between real/production mode to test modes)."),
|
||||
selection: Binding(
|
||||
|
||||
@@ -25,11 +25,6 @@ struct PostingTimelineView: View {
|
||||
@State var headerHeight: CGFloat = 0
|
||||
@Binding var headerOffset: CGFloat
|
||||
@SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||
|
||||
var mystery: some View {
|
||||
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
||||
.id("what")
|
||||
}
|
||||
|
||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||
var filters = ContentFilters.defaults(damus_state: damus_state)
|
||||
@@ -95,9 +90,6 @@ struct PostingTimelineView: View {
|
||||
VStack {
|
||||
ZStack {
|
||||
TabView(selection: $filter_state) {
|
||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||
mystery
|
||||
|
||||
contentTimelineView(filter: content_filter(.posts))
|
||||
.tag(FilterState.posts)
|
||||
.id(FilterState.posts)
|
||||
|
||||
@@ -24,8 +24,8 @@ struct ProfileZapLinkView<Content: View>: View {
|
||||
self.label = label
|
||||
self.action = action
|
||||
self.reactions_enabled = reactions_enabled
|
||||
self.lud16 = lud16
|
||||
self.lnurl = lnurl
|
||||
self.lud16 = lud16?.trimmingCharacters(in: .whitespaces)
|
||||
self.lnurl = lnurl?.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
init(damus_state: DamusState, pubkey: Pubkey, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
|
||||
@@ -36,8 +36,8 @@ struct ProfileZapLinkView<Content: View>: View {
|
||||
let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey)
|
||||
let record = profile_txn?.unsafeUnownedValue
|
||||
self.reactions_enabled = record?.profile?.reactions ?? true
|
||||
self.lud16 = record?.profile?.lud06
|
||||
self.lnurl = record?.lnurl
|
||||
self.lud16 = record?.profile?.lud06?.trimmingCharacters(in: .whitespaces)
|
||||
self.lnurl = record?.lnurl?.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
|
||||
@@ -46,8 +46,8 @@ struct ProfileZapLinkView<Content: View>: View {
|
||||
self.action = action
|
||||
|
||||
self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true
|
||||
self.lud16 = unownedProfileRecord?.profile?.lud16
|
||||
self.lnurl = unownedProfileRecord?.lnurl
|
||||
self.lud16 = unownedProfileRecord?.profile?.lud16?.trimmingCharacters(in: .whitespaces)
|
||||
self.lnurl = unownedProfileRecord?.lnurl?.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -81,12 +81,13 @@ struct ProfileZapLinkView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(lnurl == nil)
|
||||
.disabled(lnurl == nil && lud16 == nil)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ProfileZapLinkView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in
|
||||
let profile = make_test_profile()
|
||||
ProfileZapLinkView(pubkey: test_pubkey, reactions_enabled: true, lud16: profile.lud16, lnurl: profile.lud06, label: { reactions_enabled, lud16, lnurl in
|
||||
Image("zap.fill")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -86,13 +102,65 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
Log.info("App delegate is handling a push notification", for: .push_notifications)
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
guard let notification = LossyLocalNotification.from_user_info(user_info: userInfo) else {
|
||||
Log.error("App delegate could not decode notification information", for: .push_notifications)
|
||||
return
|
||||
}
|
||||
notify(.local_notification(notification))
|
||||
Log.info("App delegate notifying the app about the received push notification", for: .push_notifications)
|
||||
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.
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 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
<note>Privacy - Face ID Usage Description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
|
||||
<source>Damus needs access to your microphone for creating video recording posts</source>
|
||||
<target>Damus needs access to your microphone for creating video recording posts</target>
|
||||
<source>Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network</source>
|
||||
<target>Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network</target>
|
||||
<note>Privacy - Microphone Usage Description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
|
||||
@@ -94,6 +94,11 @@ Sentence composed of 2 variables to describe how many zap payments there are on
|
||||
<target>%@ replied to your note</target>
|
||||
<note>Heading for local notification indicating a new reply</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ reposted" xml:space="preserve">
|
||||
<source>%@ reposted</source>
|
||||
<target>%@ reposted</target>
|
||||
<note>Text indicating that the note was reposted (i.e. re-shared).</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." xml:space="preserve">
|
||||
<source>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</source>
|
||||
<target>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</target>
|
||||
@@ -252,6 +257,11 @@ Title text to indicate user to an add a relay.</note>
|
||||
<target>Additional information</target>
|
||||
<note>Header text to prompt user to optionally provide additional information when reporting a user or note.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advice" xml:space="preserve">
|
||||
<source>Advice</source>
|
||||
<target>Advice</target>
|
||||
<note>Heading for some advice text to help the user with an error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All" xml:space="preserve">
|
||||
<source>All</source>
|
||||
<target>All</target>
|
||||
@@ -321,11 +331,6 @@ Section header for text and appearance settings</note>
|
||||
<target>Appearance and filters</target>
|
||||
<note>Section header for text, appearance, and content filter settings</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Are you lost?" xml:space="preserve">
|
||||
<source>Are you lost?</source>
|
||||
<target>Are you lost?</target>
|
||||
<note>Text asking the user if they are lost in the app.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Are you sure you want to clear the cache? This will free space, but images may take longer to load again." xml:space="preserve">
|
||||
<source>Are you sure you want to clear the cache? This will free space, but images may take longer to load again.</source>
|
||||
<target>Are you sure you want to clear the cache? This will free space, but images may take longer to load again.</target>
|
||||
@@ -466,6 +471,11 @@ Text for button to cancel out of connecting Nostr Wallet Connect lightning walle
|
||||
<target>Cancelled</target>
|
||||
<note>Title indicating that the user has cancelled.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can’t display note" xml:space="preserve">
|
||||
<source>Can’t display note</source>
|
||||
<target>Can’t display note</target>
|
||||
<note>User-visible heading for an error message indicating a note has an unknown kind or is unsupported for viewing.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?" xml:space="preserve">
|
||||
<source>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</source>
|
||||
<target>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</target>
|
||||
@@ -553,6 +563,16 @@ Text for button to conect to Nostr Wallet Connect lightning wallet.</note>
|
||||
<target>Contact list has been reset</target>
|
||||
<note>Message indicating that the contact list was successfully reset.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact support via DMs" xml:space="preserve">
|
||||
<source>Contact support via DMs</source>
|
||||
<target>Contact support via DMs</target>
|
||||
<note>Button label to contact support from an error screen</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact support via email at [support@damus.io](mailto:support@damus.io)" xml:space="preserve">
|
||||
<source>Contact support via email at [support@damus.io](mailto:support@damus.io)</source>
|
||||
<target>Contact support via email at [support@damus.io](mailto:support@damus.io)</target>
|
||||
<note>Text to contact support via email</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Content filters" xml:space="preserve">
|
||||
<source>Content filters</source>
|
||||
<target>Content filters</target>
|
||||
@@ -567,6 +587,11 @@ Continue with deleting the user.
|
||||
Continue with resetting the contact list.
|
||||
Prompt to user to continue</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conversations" xml:space="preserve">
|
||||
<source>Conversations</source>
|
||||
<target>Conversations</target>
|
||||
<note>Label for filter for seeing notes and replies that involve conversations between the signed in user and the current profile.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copied" xml:space="preserve">
|
||||
<source>Copied</source>
|
||||
<target>Copied</target>
|
||||
@@ -654,6 +679,11 @@ Context menu option for copying the version of damus.</note>
|
||||
<target>Could not find user to mute...</target>
|
||||
<note>Alert message to indicate that the muted user could not be found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Could not parse the URL you are trying to open." xml:space="preserve">
|
||||
<source>Could not parse the URL you are trying to open.</source>
|
||||
<target>Could not parse the URL you are trying to open.</target>
|
||||
<note>User visible error description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create Account" xml:space="preserve">
|
||||
<source>Create Account</source>
|
||||
<target>Create Account</target>
|
||||
@@ -1293,6 +1323,11 @@ Settings section for managing keys</note>
|
||||
<target>Load media</target>
|
||||
<note>Button to show media in note.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Loading thread" xml:space="preserve">
|
||||
<source>Loading thread</source>
|
||||
<target>Loading thread</target>
|
||||
<note>Accessibility label for the thread view when it is loading</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Local" xml:space="preserve">
|
||||
<source>Local</source>
|
||||
<target>Local</target>
|
||||
@@ -1400,20 +1435,20 @@ Title for confirmation dialog to mute a profile.</note>
|
||||
<target>Mute Hashtag</target>
|
||||
<note>Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute User" xml:space="preserve">
|
||||
<source>Mute User</source>
|
||||
<target>Mute User</target>
|
||||
<note>Title of alert for muting a user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute conversation" xml:space="preserve">
|
||||
<source>Mute conversation</source>
|
||||
<target>Mute conversation</target>
|
||||
<note>Context menu option for muting a conversation.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute user" xml:space="preserve">
|
||||
<source>Mute user</source>
|
||||
<target>Mute user</target>
|
||||
<note>Context menu option for muting users.</note>
|
||||
<trans-unit id="Mute/Block User" xml:space="preserve">
|
||||
<source>Mute/Block User</source>
|
||||
<target>Mute/Block User</target>
|
||||
<note>Title of alert for muting/blocking a user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute/Block user" xml:space="preserve">
|
||||
<source>Mute/Block user</source>
|
||||
<target>Mute/Block user</target>
|
||||
<note>Context menu option for muting/blocking users.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Muted" xml:space="preserve">
|
||||
<source>Muted</source>
|
||||
@@ -1571,6 +1606,11 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
|
||||
<target>Note from a %@ you've muted</target>
|
||||
<note>Text to indicate that what is being shown is a note which has been muted.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Note not found" xml:space="preserve">
|
||||
<source>Note not found</source>
|
||||
<target>Note not found</target>
|
||||
<note>Heading for the thread view in a not found error state.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Note you've muted" xml:space="preserve">
|
||||
<source>Note you've muted</source>
|
||||
<target>Note you've muted</target>
|
||||
@@ -1660,6 +1700,11 @@ Button label to dismiss an error dialog</note>
|
||||
<target>OnlyZaps mode</target>
|
||||
<note>Setting toggle to hide reactions.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Oops!" xml:space="preserve">
|
||||
<source>Oops!</source>
|
||||
<target>Oops!</target>
|
||||
<note>Heading for an error screen</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open in browser" xml:space="preserve">
|
||||
<source>Open in browser</source>
|
||||
<target>Open in browser</target>
|
||||
@@ -1731,6 +1776,26 @@ Section title for deleting the user</note>
|
||||
<target>Please choose relays from the list below to filter the current feed:</target>
|
||||
<note>Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please contact the person who provided the link, and ask for another link." xml:space="preserve">
|
||||
<source>Please contact the person who provided the link, and ask for another link.</source>
|
||||
<target>Please contact the person who provided the link, and ask for another link.</target>
|
||||
<note>User-visible tip on what to do if a link contains a deprecated "nrelay" reference.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="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." xml:space="preserve">
|
||||
<source>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.</source>
|
||||
<target>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.</target>
|
||||
<note>User-facing tips on what to do if a Purple welcome link doesn't work</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please try again, check the URL for typos, or contact support for further help." xml:space="preserve">
|
||||
<source>Please try again, check the URL for typos, or contact support for further help.</source>
|
||||
<target>Please try again, check the URL for typos, or contact support for further help.</target>
|
||||
<note>User visible error tips</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please try opening this content on another Nostr app that supports this type of content." xml:space="preserve">
|
||||
<source>Please try opening this content on another Nostr app that supports this type of content.</source>
|
||||
<target>Please try opening this content on another Nostr app that supports this type of content.</target>
|
||||
<note>User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Point your camera to a QR code…" xml:space="preserve">
|
||||
<source>Point your camera to a QR code…</source>
|
||||
<target>Point your camera to a QR code…</target>
|
||||
@@ -1998,11 +2063,6 @@ Label indicating that the current view is for the user to report content.</note>
|
||||
<target>Repost or quote this note</target>
|
||||
<note>Accessibility label for repost/quote button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reposted" xml:space="preserve">
|
||||
<source>Reposted</source>
|
||||
<target>Reposted</target>
|
||||
<note>Text indicating that the note was reposted (i.e. re-shared).</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reposted by %@" xml:space="preserve">
|
||||
<source>Reposted by %@</source>
|
||||
<target>Reposted by %@</target>
|
||||
@@ -2086,6 +2146,11 @@ Button to save key, complete account creation, and start using the app.</note>
|
||||
<target>Save your login info?</target>
|
||||
<note>Ask user if they want to save their account information.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved" xml:space="preserve">
|
||||
<source>Saved</source>
|
||||
<target>Saved</target>
|
||||
<note>Small label indicating that the user's draft has been saved to storage.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan Code" xml:space="preserve">
|
||||
<source>Scan Code</source>
|
||||
<target>Scan Code</target>
|
||||
@@ -2254,11 +2319,6 @@ Button to show more of a long profile description.</note>
|
||||
<target>Show only from users you follow</target>
|
||||
<note>Setting to Show notifications only associated to users your follow</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show only preferred languages on Universe feed" xml:space="preserve">
|
||||
<source>Show only preferred languages on Universe feed</source>
|
||||
<target>Show only preferred languages on Universe feed</target>
|
||||
<note>Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show profile action sheets" xml:space="preserve">
|
||||
<source>Show profile action sheets</source>
|
||||
<target>Show profile action sheets</target>
|
||||
@@ -2515,11 +2575,6 @@ Nice to meet you all! #introductions #plebchain </target>
|
||||
<target>This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?</target>
|
||||
<note>Comment explaining why a user cannot be zapped.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Thread" xml:space="preserve">
|
||||
<source>Thread</source>
|
||||
<target>Thread</target>
|
||||
<note>Navigation bar title for note thread.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Threads" xml:space="preserve">
|
||||
<source>Threads</source>
|
||||
<target>Threads</target>
|
||||
@@ -2581,6 +2636,11 @@ Section header for text and appearance settings</note>
|
||||
<target>Truncate timeline text</target>
|
||||
<note>Setting to truncate text in timeline</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Try checking the link again, your internet connection, or contact the person who provided you the link for help." xml:space="preserve">
|
||||
<source>Try checking the link again, your internet connection, or contact the person who provided you the link for help.</source>
|
||||
<target>Try checking the link again, your internet connection, or contact the person who provided you the link for help.</target>
|
||||
<note>Tips on what to do if a note cannot be found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Type %@ to delete" xml:space="preserve">
|
||||
<source>Type %@ to delete</source>
|
||||
<target>Type %@ to delete</target>
|
||||
@@ -2603,6 +2663,11 @@ Example URL to LibreTranslate server</note>
|
||||
<target>Unable to find a QR Code</target>
|
||||
<note>Alert message letting user know a QR Code was not found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Undistract mode" xml:space="preserve">
|
||||
<source>Undistract mode</source>
|
||||
<target>Undistract mode</target>
|
||||
<note>Developer mode setting to scramble text and images to avoid distractions during development.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unfollow" xml:space="preserve">
|
||||
<source>Unfollow</source>
|
||||
<target>Unfollow</target>
|
||||
@@ -2749,11 +2814,6 @@ This will reset your contact list, including the list of everyone you follow and
|
||||
This 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.</target>
|
||||
<note>Alert for resetting the user's contact list.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wake up, %@" xml:space="preserve">
|
||||
<source>Wake up, %@</source>
|
||||
<target>Wake up, %@</target>
|
||||
<note>Text telling the user to wake up, where the argument is their display name.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wallet" xml:space="preserve">
|
||||
<source>Wallet</source>
|
||||
<target>Wallet</target>
|
||||
@@ -2777,6 +2837,16 @@ Title for section in zap settings that controls the Lightning wallet selection.<
|
||||
<target>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)</target>
|
||||
<note>Message indicating that no First Aid actions are available.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We do not yet support viewing this type of content." xml:space="preserve">
|
||||
<source>We do not yet support viewing this type of content.</source>
|
||||
<target>We do not yet support viewing this type of content.</target>
|
||||
<note>User-visible description of an error indicating a note has an unknown kind or is unsupported for viewing.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We were unable to find the note you were looking for." xml:space="preserve">
|
||||
<source>We were unable to find the note you were looking for.</source>
|
||||
<target>We were unable to find the note you were looking for.</target>
|
||||
<note>Text for the thread view when it is unable to find the note the user is looking for</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We'll save your account key, so you won't need to enter it manually next time you log in." xml:space="preserve">
|
||||
<source>We'll save your account key, so you won't need to enter it manually next time you log in.</source>
|
||||
<target>We'll save your account key, so you won't need to enter it manually next time you log in.</target>
|
||||
@@ -2843,11 +2913,6 @@ User confirm Yes</note>
|
||||
<target>Yes, Overwrite</target>
|
||||
<note>Text of button that confirms to overwrite the existing mutelist.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You are dreaming..." xml:space="preserve">
|
||||
<source>You are dreaming...</source>
|
||||
<target>You are dreaming...</target>
|
||||
<note>Text telling the user that they are dreaming.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again." xml:space="preserve">
|
||||
<source>You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again.</source>
|
||||
<target>You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again.</target>
|
||||
@@ -2858,11 +2923,26 @@ User confirm Yes</note>
|
||||
<target>You cannot post a highlight because you have selected no text on the page! Please close this, select some text, and try again.</target>
|
||||
<note>Label explaining a highlight cannot be made because there was no selected text, and some instructions on how to resolve the issue</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug." xml:space="preserve">
|
||||
<source>You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.</source>
|
||||
<target>You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.</target>
|
||||
<note>Error label upon continuing in the app from a Damus Purple purchase</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug." xml:space="preserve">
|
||||
<source>You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.</source>
|
||||
<target>You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.</target>
|
||||
<note>Error label upon continuing in the app from a Damus Purple purchase</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You have no bookmarks yet, add them in the context menu" xml:space="preserve">
|
||||
<source>You have no bookmarks yet, add them in the context menu</source>
|
||||
<target>You have no bookmarks yet, add them in the context menu</target>
|
||||
<note>Text indicating that there are no bookmarks to be viewed</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You opened an invalid link. The link you tried to open refers to "nrelay", which has been deprecated and is not supported." xml:space="preserve">
|
||||
<source>You opened an invalid link. The link you tried to open refers to "nrelay", which has been deprecated and is not supported.</source>
|
||||
<target>You opened an invalid link. The link you tried to open refers to "nrelay", which has been deprecated and is not supported.</target>
|
||||
<note>User-visible error description for a user who tries to open a deprecated "nrelay" link.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You unlocked" xml:space="preserve">
|
||||
<source>You unlocked</source>
|
||||
<target>You unlocked</target>
|
||||
@@ -2888,6 +2968,11 @@ User confirm Yes</note>
|
||||
<target>Your Purple subscription has expired. Renew?</target>
|
||||
<note>A notification message explaining to the user that their Damus Purple Subscription has expired, prompting them to renew.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your draft has been saved to storage." xml:space="preserve">
|
||||
<source>Your draft has been saved to storage.</source>
|
||||
<target>Your draft has been saved to storage.</target>
|
||||
<note>Accessibility label indicating that a user's post draft has been saved, meant to be read by screen reading technology.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your highlight is being broadcasted to the network. Please wait." xml:space="preserve">
|
||||
<source>Your highlight is being broadcasted to the network. Please wait.</source>
|
||||
<target>Your highlight is being broadcasted to the network. Please wait.</target>
|
||||
@@ -3235,6 +3320,21 @@ String indicating that a given timestamp just occurred</note>
|
||||
<target>%#@IMPORTS@</target>
|
||||
<note/>
|
||||
</trans-unit>
|
||||
<trans-unit id="/people_reposted_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
|
||||
<source>%#@REPOSTED@</source>
|
||||
<target>%#@REPOSTED@</target>
|
||||
<note/>
|
||||
</trans-unit>
|
||||
<trans-unit id="/people_reposted_count:dict/REPOSTED:dict/one:dict/:string" xml:space="preserve">
|
||||
<source>%2$@ and %1$d other reposted</source>
|
||||
<target>%2$@ and %1$d other reposted</target>
|
||||
<note/>
|
||||
</trans-unit>
|
||||
<trans-unit id="/people_reposted_count:dict/REPOSTED:dict/other:dict/:string" xml:space="preserve">
|
||||
<source>%2$@ and %1$d others reposted</source>
|
||||
<target>%2$@ and %1$d others reposted</target>
|
||||
<note/>
|
||||
</trans-unit>
|
||||
<trans-unit id="/quoted_reposts_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
|
||||
<source>%#@QUOTE_REPOSTS@</source>
|
||||
<target>%#@QUOTE_REPOSTS@</target>
|
||||
@@ -3631,6 +3731,11 @@ Sentence composed of 2 variables to describe how many zap payments there are on
|
||||
<target state="new">%@ replied to your note</target>
|
||||
<note>Heading for local notification indicating a new reply</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ reposted" xml:space="preserve">
|
||||
<source>%@ reposted</source>
|
||||
<target state="new">%@ reposted</target>
|
||||
<note>Text indicating that the note was reposted (i.e. re-shared).</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." xml:space="preserve">
|
||||
<source>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</source>
|
||||
<target state="new">%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</target>
|
||||
@@ -3789,6 +3894,11 @@ Title text to indicate user to an add a relay.</note>
|
||||
<target state="new">Additional information</target>
|
||||
<note>Header text to prompt user to optionally provide additional information when reporting a user or note.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advice" xml:space="preserve">
|
||||
<source>Advice</source>
|
||||
<target state="new">Advice</target>
|
||||
<note>Heading for some advice text to help the user with an error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All" xml:space="preserve">
|
||||
<source>All</source>
|
||||
<target state="new">All</target>
|
||||
@@ -3858,11 +3968,6 @@ Section header for text and appearance settings</note>
|
||||
<target state="new">Appearance and filters</target>
|
||||
<note>Section header for text, appearance, and content filter settings</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Are you lost?" xml:space="preserve">
|
||||
<source>Are you lost?</source>
|
||||
<target state="new">Are you lost?</target>
|
||||
<note>Text asking the user if they are lost in the app.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Are you sure you want to clear the cache? This will free space, but images may take longer to load again." xml:space="preserve">
|
||||
<source>Are you sure you want to clear the cache? This will free space, but images may take longer to load again.</source>
|
||||
<target state="new">Are you sure you want to clear the cache? This will free space, but images may take longer to load again.</target>
|
||||
@@ -4003,6 +4108,11 @@ Text for button to cancel out of connecting Nostr Wallet Connect lightning walle
|
||||
<target state="new">Cancelled</target>
|
||||
<note>Title indicating that the user has cancelled.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can’t display note" xml:space="preserve">
|
||||
<source>Can’t display note</source>
|
||||
<target state="new">Can’t display note</target>
|
||||
<note>User-visible heading for an error message indicating a note has an unknown kind or is unsupported for viewing.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?" xml:space="preserve">
|
||||
<source>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</source>
|
||||
<target state="new">Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</target>
|
||||
@@ -4093,6 +4203,16 @@ Text for button to conect to Nostr Wallet Connect lightning wallet.</note>
|
||||
<target state="new">Contact list has been reset</target>
|
||||
<note>Message indicating that the contact list was successfully reset.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact support via DMs" xml:space="preserve">
|
||||
<source>Contact support via DMs</source>
|
||||
<target state="new">Contact support via DMs</target>
|
||||
<note>Button label to contact support from an error screen</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact support via email at [support@damus.io](mailto:support@damus.io)" xml:space="preserve">
|
||||
<source>Contact support via email at [support@damus.io](mailto:support@damus.io)</source>
|
||||
<target state="new">Contact support via email at [support@damus.io](mailto:support@damus.io)</target>
|
||||
<note>Text to contact support via email</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Content filters" xml:space="preserve">
|
||||
<source>Content filters</source>
|
||||
<target state="new">Content filters</target>
|
||||
@@ -4107,6 +4227,11 @@ Continue with deleting the user.
|
||||
Continue with resetting the contact list.
|
||||
Prompt to user to continue</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Conversations" xml:space="preserve">
|
||||
<source>Conversations</source>
|
||||
<target state="new">Conversations</target>
|
||||
<note>Label for filter for seeing notes and replies that involve conversations between the signed in user and the current profile.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copied" xml:space="preserve">
|
||||
<source>Copied</source>
|
||||
<target state="new">Copied</target>
|
||||
@@ -4194,6 +4319,11 @@ Context menu option for copying the version of damus.</note>
|
||||
<target state="new">Could not find user to mute...</target>
|
||||
<note>Alert message to indicate that the muted user could not be found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Could not parse the URL you are trying to open." xml:space="preserve">
|
||||
<source>Could not parse the URL you are trying to open.</source>
|
||||
<target state="new">Could not parse the URL you are trying to open.</target>
|
||||
<note>User visible error description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create Account" xml:space="preserve">
|
||||
<source>Create Account</source>
|
||||
<target state="new">Create Account</target>
|
||||
@@ -4833,6 +4963,11 @@ Settings section for managing keys</note>
|
||||
<target state="new">Load media</target>
|
||||
<note>Button to show media in note.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Loading thread" xml:space="preserve">
|
||||
<source>Loading thread</source>
|
||||
<target state="new">Loading thread</target>
|
||||
<note>Accessibility label for the thread view when it is loading</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Local" xml:space="preserve">
|
||||
<source>Local</source>
|
||||
<target state="new">Local</target>
|
||||
@@ -4940,20 +5075,20 @@ Title for confirmation dialog to mute a profile.</note>
|
||||
<target state="new">Mute Hashtag</target>
|
||||
<note>Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute User" xml:space="preserve">
|
||||
<source>Mute User</source>
|
||||
<target state="new">Mute User</target>
|
||||
<note>Title of alert for muting a user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute conversation" xml:space="preserve">
|
||||
<source>Mute conversation</source>
|
||||
<target state="new">Mute conversation</target>
|
||||
<note>Context menu option for muting a conversation.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute user" xml:space="preserve">
|
||||
<source>Mute user</source>
|
||||
<target state="new">Mute user</target>
|
||||
<note>Context menu option for muting users.</note>
|
||||
<trans-unit id="Mute/Block User" xml:space="preserve">
|
||||
<source>Mute/Block User</source>
|
||||
<target state="new">Mute/Block User</target>
|
||||
<note>Title of alert for muting/blocking a user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute/Block user" xml:space="preserve">
|
||||
<source>Mute/Block user</source>
|
||||
<target state="new">Mute/Block user</target>
|
||||
<note>Context menu option for muting/blocking users.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Muted" xml:space="preserve">
|
||||
<source>Muted</source>
|
||||
@@ -5106,6 +5241,11 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
|
||||
<target state="new">Note from a %@ you've muted</target>
|
||||
<note>Text to indicate that what is being shown is a note which has been muted.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Note not found" xml:space="preserve">
|
||||
<source>Note not found</source>
|
||||
<target state="new">Note not found</target>
|
||||
<note>Heading for the thread view in a not found error state.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Note you've muted" xml:space="preserve">
|
||||
<source>Note you've muted</source>
|
||||
<target state="new">Note you've muted</target>
|
||||
@@ -5195,6 +5335,11 @@ Button label to dismiss an error dialog</note>
|
||||
<target state="new">OnlyZaps mode</target>
|
||||
<note>Setting toggle to hide reactions.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Oops!" xml:space="preserve">
|
||||
<source>Oops!</source>
|
||||
<target state="new">Oops!</target>
|
||||
<note>Heading for an error screen</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open in browser" xml:space="preserve">
|
||||
<source>Open in browser</source>
|
||||
<target state="new">Open in browser</target>
|
||||
@@ -5266,6 +5411,26 @@ Section title for deleting the user</note>
|
||||
<target state="new">Please choose relays from the list below to filter the current feed:</target>
|
||||
<note>Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please contact the person who provided the link, and ask for another link." xml:space="preserve">
|
||||
<source>Please contact the person who provided the link, and ask for another link.</source>
|
||||
<target state="new">Please contact the person who provided the link, and ask for another link.</target>
|
||||
<note>User-visible tip on what to do if a link contains a deprecated "nrelay" reference.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="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." xml:space="preserve">
|
||||
<source>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.</source>
|
||||
<target state="new">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.</target>
|
||||
<note>User-facing tips on what to do if a Purple welcome link doesn't work</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please try again, check the URL for typos, or contact support for further help." xml:space="preserve">
|
||||
<source>Please try again, check the URL for typos, or contact support for further help.</source>
|
||||
<target state="new">Please try again, check the URL for typos, or contact support for further help.</target>
|
||||
<note>User visible error tips</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please try opening this content on another Nostr app that supports this type of content." xml:space="preserve">
|
||||
<source>Please try opening this content on another Nostr app that supports this type of content.</source>
|
||||
<target state="new">Please try opening this content on another Nostr app that supports this type of content.</target>
|
||||
<note>User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Point your camera to a QR code…" xml:space="preserve">
|
||||
<source>Point your camera to a QR code…</source>
|
||||
<target state="new">Point your camera to a QR code…</target>
|
||||
@@ -5523,11 +5688,6 @@ Label indicating that the current view is for the user to report content.</note>
|
||||
<target state="new">Repost or quote this note</target>
|
||||
<note>Accessibility label for repost/quote button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reposted" xml:space="preserve">
|
||||
<source>Reposted</source>
|
||||
<target state="new">Reposted</target>
|
||||
<note>Text indicating that the note was reposted (i.e. re-shared).</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reposted by %@" xml:space="preserve">
|
||||
<source>Reposted by %@</source>
|
||||
<target state="new">Reposted by %@</target>
|
||||
@@ -5611,6 +5771,11 @@ Button to save key, complete account creation, and start using the app.</note>
|
||||
<target state="new">Save your login info?</target>
|
||||
<note>Ask user if they want to save their account information.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved" xml:space="preserve">
|
||||
<source>Saved</source>
|
||||
<target state="new">Saved</target>
|
||||
<note>Small label indicating that the user's draft has been saved to storage.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan Code" xml:space="preserve">
|
||||
<source>Scan Code</source>
|
||||
<target state="new">Scan Code</target>
|
||||
@@ -5789,11 +5954,6 @@ Button to show more of a long profile description.</note>
|
||||
<target state="new">Show only from users you follow</target>
|
||||
<note>Setting to Show notifications only associated to users your follow</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show only preferred languages on Universe feed" xml:space="preserve">
|
||||
<source>Show only preferred languages on Universe feed</source>
|
||||
<target state="new">Show only preferred languages on Universe feed</target>
|
||||
<note>Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show profile action sheets" xml:space="preserve">
|
||||
<source>Show profile action sheets</source>
|
||||
<target state="new">Show profile action sheets</target>
|
||||
@@ -6050,11 +6210,6 @@ Nice to meet you all! #introductions #plebchain </target>
|
||||
<target state="new">This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?</target>
|
||||
<note>Comment explaining why a user cannot be zapped.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Thread" xml:space="preserve">
|
||||
<source>Thread</source>
|
||||
<target state="new">Thread</target>
|
||||
<note>Navigation bar title for note thread.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Threads" xml:space="preserve">
|
||||
<source>Threads</source>
|
||||
<target state="new">Threads</target>
|
||||
@@ -6116,6 +6271,11 @@ Section header for text and appearance settings</note>
|
||||
<target state="new">Truncate timeline text</target>
|
||||
<note>Setting to truncate text in timeline</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Try checking the link again, your internet connection, or contact the person who provided you the link for help." xml:space="preserve">
|
||||
<source>Try checking the link again, your internet connection, or contact the person who provided you the link for help.</source>
|
||||
<target state="new">Try checking the link again, your internet connection, or contact the person who provided you the link for help.</target>
|
||||
<note>Tips on what to do if a note cannot be found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Type %@ to delete" xml:space="preserve">
|
||||
<source>Type %@ to delete</source>
|
||||
<target state="new">Type %@ to delete</target>
|
||||
@@ -6138,6 +6298,11 @@ Example URL to LibreTranslate server</note>
|
||||
<target state="new">Unable to find a QR Code</target>
|
||||
<note>Alert message letting user know a QR Code was not found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Undistract mode" xml:space="preserve">
|
||||
<source>Undistract mode</source>
|
||||
<target state="new">Undistract mode</target>
|
||||
<note>Developer mode setting to scramble text and images to avoid distractions during development.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unfollow" xml:space="preserve">
|
||||
<source>Unfollow</source>
|
||||
<target state="new">Unfollow</target>
|
||||
@@ -6284,11 +6449,6 @@ This will reset your contact list, including the list of everyone you follow and
|
||||
This 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.</target>
|
||||
<note>Alert for resetting the user's contact list.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wake up, %@" xml:space="preserve">
|
||||
<source>Wake up, %@</source>
|
||||
<target state="new">Wake up, %@</target>
|
||||
<note>Text telling the user to wake up, where the argument is their display name.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wallet" xml:space="preserve">
|
||||
<source>Wallet</source>
|
||||
<target state="new">Wallet</target>
|
||||
@@ -6312,6 +6472,16 @@ Title for section in zap settings that controls the Lightning wallet selection.<
|
||||
<target state="new">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)</target>
|
||||
<note>Message indicating that no First Aid actions are available.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We do not yet support viewing this type of content." xml:space="preserve">
|
||||
<source>We do not yet support viewing this type of content.</source>
|
||||
<target state="new">We do not yet support viewing this type of content.</target>
|
||||
<note>User-visible description of an error indicating a note has an unknown kind or is unsupported for viewing.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We were unable to find the note you were looking for." xml:space="preserve">
|
||||
<source>We were unable to find the note you were looking for.</source>
|
||||
<target state="new">We were unable to find the note you were looking for.</target>
|
||||
<note>Text for the thread view when it is unable to find the note the user is looking for</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We'll save your account key, so you won't need to enter it manually next time you log in." xml:space="preserve">
|
||||
<source>We'll save your account key, so you won't need to enter it manually next time you log in.</source>
|
||||
<target state="new">We'll save your account key, so you won't need to enter it manually next time you log in.</target>
|
||||
@@ -6378,21 +6548,31 @@ User confirm Yes</note>
|
||||
<target state="new">Yes, Overwrite</target>
|
||||
<note>Text of button that confirms to overwrite the existing mutelist.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You are dreaming..." xml:space="preserve">
|
||||
<source>You are dreaming...</source>
|
||||
<target state="new">You are dreaming...</target>
|
||||
<note>Text telling the user that they are dreaming.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You cannot share content because you are not logged in. Please close this view, log in to your account, and try again." xml:space="preserve">
|
||||
<source>You cannot share content because you are not logged in. Please close this view, log in to your account, and try again.</source>
|
||||
<target state="new">You cannot share content because you are not logged in. Please close this view, log in to your account, and try again.</target>
|
||||
<note>Label explaining that sharing cannot proceed because the user is not logged in.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug." xml:space="preserve">
|
||||
<source>You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.</source>
|
||||
<target state="new">You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.</target>
|
||||
<note>Error label upon continuing in the app from a Damus Purple purchase</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug." xml:space="preserve">
|
||||
<source>You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.</source>
|
||||
<target state="new">You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.</target>
|
||||
<note>Error label upon continuing in the app from a Damus Purple purchase</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You have no bookmarks yet, add them in the context menu" xml:space="preserve">
|
||||
<source>You have no bookmarks yet, add them in the context menu</source>
|
||||
<target state="new">You have no bookmarks yet, add them in the context menu</target>
|
||||
<note>Text indicating that there are no bookmarks to be viewed</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You opened an invalid link. The link you tried to open refers to "nrelay", which has been deprecated and is not supported." xml:space="preserve">
|
||||
<source>You opened an invalid link. The link you tried to open refers to "nrelay", which has been deprecated and is not supported.</source>
|
||||
<target state="new">You opened an invalid link. The link you tried to open refers to "nrelay", which has been deprecated and is not supported.</target>
|
||||
<note>User-visible error description for a user who tries to open a deprecated "nrelay" link.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You unlocked" xml:space="preserve">
|
||||
<source>You unlocked</source>
|
||||
<target state="new">You unlocked</target>
|
||||
@@ -6423,6 +6603,11 @@ User confirm Yes</note>
|
||||
<target state="new">Your content is being broadcasted to the network. Please wait.</target>
|
||||
<note>Label explaining that their content sharing action is in progress</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your draft has been saved to storage." xml:space="preserve">
|
||||
<source>Your draft has been saved to storage.</source>
|
||||
<target state="new">Your draft has been saved to storage.</target>
|
||||
<note>Accessibility label indicating that a user's post draft has been saved, meant to be read by screen reading technology.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your report will be sent to the relays you are connected to" xml:space="preserve">
|
||||
<source>Your report will be sent to the relays you are connected to</source>
|
||||
<target state="new">Your report will be sent to the relays you are connected to</target>
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
@@ -147,6 +150,9 @@
|
||||
"ADMIN" : {
|
||||
"comment" : "Text label indicating the profile picture underneath it is the admin of the Nostr relay."
|
||||
},
|
||||
"Advice" : {
|
||||
"comment" : "Heading for some advice text to help the user with an error"
|
||||
},
|
||||
"All" : {
|
||||
"comment" : "Human-readable short description of the 'friends filter' when it is set to 'all'\nLabel for filter for all notifications."
|
||||
},
|
||||
@@ -204,9 +210,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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."
|
||||
},
|
||||
@@ -270,6 +273,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."
|
||||
},
|
||||
@@ -330,12 +336,21 @@
|
||||
"Contact list has been reset" : {
|
||||
"comment" : "Message indicating that the contact list was successfully reset."
|
||||
},
|
||||
"Contact support via DMs" : {
|
||||
"comment" : "Button label to contact support from an error screen"
|
||||
},
|
||||
"Contact support via email at [support@damus.io](mailto:support@damus.io)" : {
|
||||
"comment" : "Text to contact support via email"
|
||||
},
|
||||
"Content filters" : {
|
||||
"comment" : "Section title for content filtering/moderation configuration."
|
||||
},
|
||||
"Continue" : {
|
||||
"comment" : "Button to dismiss suggested users view and continue to the main app\nContinue with bookmarks.\nContinue with deleting the user.\nContinue with resetting the contact list.\nPrompt to user to continue"
|
||||
},
|
||||
"Conversations" : {
|
||||
"comment" : "Label for filter for seeing notes and replies that involve conversations between the signed in user and the current profile."
|
||||
},
|
||||
"Copied" : {
|
||||
"comment" : "Label indicating that a user's key was copied."
|
||||
},
|
||||
@@ -387,6 +402,9 @@
|
||||
"Could not find user to mute..." : {
|
||||
"comment" : "Alert message to indicate that the muted user could not be found."
|
||||
},
|
||||
"Could not parse the URL you are trying to open." : {
|
||||
"comment" : "User visible error description"
|
||||
},
|
||||
"Create account" : {
|
||||
"comment" : "Button to navigate to create account view."
|
||||
},
|
||||
@@ -769,6 +787,9 @@
|
||||
"Load media" : {
|
||||
"comment" : "Button to show media in note."
|
||||
},
|
||||
"Loading thread" : {
|
||||
"comment" : "Accessibility label for the thread view when it is loading"
|
||||
},
|
||||
"Local" : {
|
||||
"comment" : "Option for notification mode setting: Local notification mode"
|
||||
},
|
||||
@@ -832,11 +853,11 @@
|
||||
"Mute Hashtag" : {
|
||||
"comment" : "Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore."
|
||||
},
|
||||
"Mute user" : {
|
||||
"comment" : "Context menu option for muting users."
|
||||
"Mute/Block user" : {
|
||||
"comment" : "Context menu option for muting/blocking users."
|
||||
},
|
||||
"Mute User" : {
|
||||
"comment" : "Title of alert for muting a user."
|
||||
"Mute/Block User" : {
|
||||
"comment" : "Title of alert for muting/blocking a user."
|
||||
},
|
||||
"Muted" : {
|
||||
"comment" : "Navigation title of view to see list of muted users & phrases.\nSidebar menu label for muted users view."
|
||||
@@ -940,6 +961,9 @@
|
||||
"Note from a %@ you've muted" : {
|
||||
"comment" : "Text to indicate that what is being shown is a note which has been muted."
|
||||
},
|
||||
"Note not found" : {
|
||||
"comment" : "Heading for the thread view in a not found error state."
|
||||
},
|
||||
"Note you've muted" : {
|
||||
"comment" : "Label indicating note has been muted\nText to indicate that what is being shown is a note which has been muted."
|
||||
},
|
||||
@@ -1000,6 +1024,9 @@
|
||||
"OnlyZaps mode" : {
|
||||
"comment" : "Setting toggle to hide reactions."
|
||||
},
|
||||
"Oops!" : {
|
||||
"comment" : "Heading for an error screen"
|
||||
},
|
||||
"Open in browser" : {
|
||||
"comment" : "Button to open the value found in browser."
|
||||
},
|
||||
@@ -1045,6 +1072,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"
|
||||
},
|
||||
@@ -1212,9 +1251,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 +1299,9 @@
|
||||
"Save your login info?" : {
|
||||
"comment" : "Ask user if they want to save their account information."
|
||||
},
|
||||
"Saved" : {
|
||||
"comment" : "Small label indicating that the user's draft has been saved to storage."
|
||||
},
|
||||
"Scan a user's pubkey" : {
|
||||
"comment" : "Text to prompt scanning a QR code of a user's pubkey to open their profile."
|
||||
},
|
||||
@@ -1368,9 +1407,6 @@
|
||||
"Show only from users you follow" : {
|
||||
"comment" : "Setting to Show notifications only associated to users your follow"
|
||||
},
|
||||
"Show only preferred languages on Universe feed" : {
|
||||
"comment" : "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."
|
||||
},
|
||||
"Show profile action sheets" : {
|
||||
"comment" : "Setting to show profile action sheets when clicking on a user's profile picture"
|
||||
},
|
||||
@@ -1518,9 +1554,6 @@
|
||||
"This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?" : {
|
||||
"comment" : "Comment explaining why a user cannot be zapped."
|
||||
},
|
||||
"Thread" : {
|
||||
"comment" : "Navigation bar title for note thread."
|
||||
},
|
||||
"Threads" : {
|
||||
"comment" : "Section header title for a list of threads that are muted."
|
||||
},
|
||||
@@ -1560,6 +1593,9 @@
|
||||
"Truncate timeline text" : {
|
||||
"comment" : "Setting to truncate text in timeline"
|
||||
},
|
||||
"Try checking the link again, your internet connection, or contact the person who provided you the link for help." : {
|
||||
"comment" : "Tips on what to do if a note cannot be found."
|
||||
},
|
||||
"Type %@ to delete" : {
|
||||
"comment" : "Text field prompt asking user to type DELETE in all caps to confirm that they want to proceed with deleting their account."
|
||||
},
|
||||
@@ -1569,6 +1605,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."
|
||||
},
|
||||
@@ -1644,9 +1683,6 @@
|
||||
"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."
|
||||
},
|
||||
@@ -1665,6 +1701,12 @@
|
||||
"We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)" : {
|
||||
"comment" : "Message indicating that no First Aid actions are available."
|
||||
},
|
||||
"We do not yet support viewing this type of content." : {
|
||||
"comment" : "User-visible description of an error indicating a note has an unknown kind or is unsupported for viewing."
|
||||
},
|
||||
"We were unable to find the note you were looking for." : {
|
||||
"comment" : "Text for the thread view when it is unable to find the note the user is looking for"
|
||||
},
|
||||
"We'll save your account key, so you won't need to enter it manually next time you log in." : {
|
||||
"comment" : "Reminder to user that they should save their account information."
|
||||
},
|
||||
@@ -1710,21 +1752,30 @@
|
||||
"you" : {
|
||||
"comment" : "You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself."
|
||||
},
|
||||
"You are dreaming..." : {
|
||||
"comment" : "Text telling the user that they are dreaming."
|
||||
},
|
||||
"You cannot share content because you are not logged in. Please close this view, log in to your account, and try again." : {
|
||||
"comment" : "Label explaining that sharing cannot proceed because the user is not logged in."
|
||||
},
|
||||
"You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug." : {
|
||||
"comment" : "Error label upon continuing in the app from a Damus Purple purchase"
|
||||
},
|
||||
"You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug." : {
|
||||
"comment" : "Error label upon continuing in the app from a Damus Purple purchase"
|
||||
},
|
||||
"You have no bookmarks yet, add them in the context menu" : {
|
||||
"comment" : "Text indicating that there are no bookmarks to be viewed"
|
||||
},
|
||||
"You opened an invalid link. The link you tried to open refers to \"nrelay\", which has been deprecated and is not supported." : {
|
||||
"comment" : "User-visible error description for a user who tries to open a deprecated \"nrelay\" link."
|
||||
},
|
||||
"You unlocked" : {
|
||||
"comment" : "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple"
|
||||
},
|
||||
"Your content is being broadcasted to the network. Please wait." : {
|
||||
"comment" : "Label explaining that their content sharing action is in progress"
|
||||
},
|
||||
"Your draft has been saved to storage." : {
|
||||
"comment" : "Accessibility label indicating that a user's post draft has been saved, meant to be read by screen reading technology."
|
||||
},
|
||||
"Your Name" : {
|
||||
"comment" : "Label for Your Name section of user profile form."
|
||||
},
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
/* Privacy - Face ID Usage Description */
|
||||
"NSFaceIDUsageDescription" = "Local authentication to access private key";
|
||||
/* Privacy - Microphone Usage Description */
|
||||
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone for creating video recording posts";
|
||||
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network";
|
||||
/* Privacy - Photo Library Additions Usage Description */
|
||||
"NSPhotoLibraryAddUsageDescription" = "Granting Damus access to your photos allows you to save images.";
|
||||
|
||||
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.
@@ -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.
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.
Binary file not shown.
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.
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>
|
||||
@@ -209,7 +223,7 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>other</key>
|
||||
<string>คำพูด</string>
|
||||
<string>อ้างอิง</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>sats</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
|
||||
|
||||
|
||||
23
damusTests/LICENSES
Normal file
23
damusTests/LICENSES
Normal file
@@ -0,0 +1,23 @@
|
||||
Some of the fixtures in this folder are taken from https://github.com/nostr-sdk/nostr-sdk-ios under the MIT license:
|
||||
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2023 Nostr SDK
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
432
damusTests/NIP44v2EncryptionTests.swift
Normal file
432
damusTests/NIP44v2EncryptionTests.swift
Normal file
@@ -0,0 +1,432 @@
|
||||
//
|
||||
// NIP44v2EncryptionTests.swift
|
||||
// damus
|
||||
//
|
||||
// Based on NIP44v2EncryptingTests.swift, taken from https://github.com/nostr-sdk/nostr-sdk-ios under the MIT license:
|
||||
//
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2023 Nostr SDK
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
//
|
||||
// Adapted by Daniel D’Aquino for damus on 2025-02-10.
|
||||
//
|
||||
import XCTest
|
||||
import CryptoKit
|
||||
@testable import damus
|
||||
|
||||
final class NIP44v2EncryptingTests: XCTestCase {
|
||||
|
||||
private lazy var vectors: NIP44Vectors = try! decodeFixture(filename: "nip44.vectors") // swiftlint:disable:this force_try
|
||||
|
||||
/// Calculate the conversation key from secret key, sec1, and public key, pub2.
|
||||
func testValidConversationKey() throws {
|
||||
let conversationKeyVectors = try XCTUnwrap(vectors.v2.valid.getConversationKey)
|
||||
|
||||
try conversationKeyVectors.forEach { vector in
|
||||
let expectedConversationKey = try XCTUnwrap(vector.conversationKey)
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let publicKeyB = try XCTUnwrap(Pubkey(hex: vector.pub2))
|
||||
let conversationKeyBytes = try NIP44v2Encryption.conversationKey(
|
||||
privateKeyA: privateKeyA,
|
||||
publicKeyB: publicKeyB
|
||||
).bytes
|
||||
let conversationKey = Data(conversationKeyBytes).hexString
|
||||
XCTAssertEqual(conversationKey, expectedConversationKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate ChaCha key, ChaCha nonce, and HMAC key from conversation key and nonce.
|
||||
func testValidMessageKeys() throws {
|
||||
let messageKeyVectors = try XCTUnwrap(vectors.v2.valid.getMessageKeys)
|
||||
let conversationKey = messageKeyVectors.conversationKey
|
||||
let conversationKeyBytes = try XCTUnwrap(conversationKey.hexDecoded?.bytes)
|
||||
let keys = messageKeyVectors.keys
|
||||
|
||||
try keys.forEach { vector in
|
||||
let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
|
||||
let messageKeys = try NIP44v2Encryption.messageKeys(conversationKey: conversationKeyBytes, nonce: nonce)
|
||||
XCTAssertEqual(messageKeys.chaChaKey.hexString, vector.chaChaKey)
|
||||
XCTAssertEqual(messageKeys.chaChaNonce.hexString, vector.chaChaNonce)
|
||||
XCTAssertEqual(messageKeys.hmacKey.hexString, vector.hmacKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Take unpadded length (first value), calculate padded length (second value).
|
||||
func testValidCalculatePaddedLength() throws {
|
||||
let calculatePaddedLengthVectors = try XCTUnwrap(vectors.v2.valid.calculatePaddedLength)
|
||||
try calculatePaddedLengthVectors.forEach { vector in
|
||||
XCTAssertEqual(vector.count, 2)
|
||||
let paddedLength = try NIP44v2Encryption.calculatePaddedLength(vector[0])
|
||||
XCTAssertEqual(paddedLength, vector[1])
|
||||
}
|
||||
}
|
||||
|
||||
/// Emulate real conversation with a hardcoded nonce.
|
||||
/// Calculate pub2 from sec2, verify conversation key from (sec1, pub2), encrypt, verify payload.
|
||||
/// Then calculate pub1 from sec1, verify conversation key from (sec2, pub1), decrypt, verify plaintext.
|
||||
func testValidEncryptDecrypt() throws {
|
||||
let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecrypt)
|
||||
try encryptDecryptVectors.forEach { vector in
|
||||
let sec1 = vector.sec1
|
||||
let sec2 = vector.sec2
|
||||
let expectedConversationKey = vector.conversationKey
|
||||
let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
|
||||
let plaintext = vector.plaintext
|
||||
let payload = vector.payload
|
||||
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let privateKeyB = try XCTUnwrap(Privkey(hex: vector.sec2))
|
||||
let keypair1 = try XCTUnwrap(FullKeypair(privkey: privateKeyA))
|
||||
let keypair2 = try XCTUnwrap(FullKeypair(privkey: privateKeyB))
|
||||
|
||||
// Conversation key from sec1 and pub2.
|
||||
let conversationKey1Bytes = try NIP44v2Encryption.conversationKey(
|
||||
privateKeyA: keypair1.privkey,
|
||||
publicKeyB: keypair2.pubkey
|
||||
).bytes
|
||||
XCTAssertEqual(expectedConversationKey, Data(conversationKey1Bytes).hexString)
|
||||
|
||||
// Verify payload.
|
||||
let ciphertext = try NIP44v2Encryption.encrypt(
|
||||
plaintext: plaintext,
|
||||
conversationKey: conversationKey1Bytes,
|
||||
nonce: nonce
|
||||
)
|
||||
XCTAssertEqual(payload, ciphertext)
|
||||
|
||||
// Conversation key from sec2 and pub1.
|
||||
let conversationKey2Bytes = try NIP44v2Encryption.conversationKey(
|
||||
privateKeyA: keypair2.privkey,
|
||||
publicKeyB: keypair1.pubkey
|
||||
).bytes
|
||||
XCTAssertEqual(expectedConversationKey, Data(conversationKey2Bytes).hexString)
|
||||
|
||||
// Verify that decrypted data equals the plaintext that we started off with.
|
||||
let decrypted = try NIP44v2Encryption.decrypt(payload: payload, conversationKey: conversationKey2Bytes)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as previous step, but instead of a full plaintext and payload, their checksum is provided.
|
||||
func testValidEncryptDecryptLongMessage() throws {
|
||||
let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecryptLongMessage)
|
||||
try encryptDecryptVectors.forEach { vector in
|
||||
let conversationKey = vector.conversationKey
|
||||
let conversationKeyData = try XCTUnwrap(conversationKey.hexDecoded)
|
||||
let conversationKeyBytes = conversationKeyData.bytes
|
||||
|
||||
let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
|
||||
let expectedPlaintextSHA256 = vector.plaintextSHA256
|
||||
|
||||
let plaintext = String(repeating: vector.pattern, count: vector.repeatCount)
|
||||
let plaintextData = try XCTUnwrap(plaintext.data(using: .utf8))
|
||||
let plaintextSHA256 = plaintextData.sha256()
|
||||
|
||||
XCTAssertEqual(plaintextSHA256.hexString, expectedPlaintextSHA256)
|
||||
|
||||
let payloadSHA256 = vector.payloadSHA256
|
||||
|
||||
let ciphertext = try NIP44v2Encryption.encrypt(
|
||||
plaintext: plaintext,
|
||||
conversationKey: conversationKeyBytes,
|
||||
nonce: nonce
|
||||
)
|
||||
let ciphertextData = try XCTUnwrap(ciphertext.data(using: .utf8))
|
||||
let ciphertextSHA256 = ciphertextData.sha256().hexString
|
||||
XCTAssertEqual(ciphertextSHA256, payloadSHA256)
|
||||
|
||||
let decrypted = try NIP44v2Encryption.decrypt(payload: ciphertext, conversationKey: conversationKeyBytes)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emulate real conversation with only the public encrypt and decrypt functions,
|
||||
/// where the nonce used for encryption is a cryptographically secure pseudorandom generated series of bytes.
|
||||
func testValidEncryptDecryptRandomNonce() throws {
|
||||
let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecrypt)
|
||||
try encryptDecryptVectors.forEach { vector in
|
||||
let sec1 = vector.sec1
|
||||
let sec2 = vector.sec2
|
||||
let plaintext = vector.plaintext
|
||||
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let privateKeyB = try XCTUnwrap(Privkey(hex: vector.sec2))
|
||||
|
||||
let keypair1 = try XCTUnwrap(FullKeypair(privkey: privateKeyA))
|
||||
let keypair2 = try XCTUnwrap(FullKeypair(privkey: privateKeyB))
|
||||
|
||||
// Encrypt plaintext with user A's private key and user B's public key.
|
||||
let ciphertext = try NIP44v2Encryption.encrypt(
|
||||
plaintext: plaintext,
|
||||
privateKeyA: keypair1.privkey,
|
||||
publicKeyB: keypair2.pubkey
|
||||
)
|
||||
|
||||
// Decrypt ciphertext with user B's private key and user A's public key.
|
||||
let decrypted = try NIP44v2Encryption.decrypt(payload: ciphertext, privateKeyA: keypair2.privkey, publicKeyB: keypair1.pubkey)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypting a plaintext message that is not at a minimum of 1 byte and maximum of 65535 bytes must throw an error.
|
||||
func testInvalidEncryptMessageLengths() throws {
|
||||
let encryptMessageLengthsVectors = try XCTUnwrap(vectors.v2.invalid.encryptMessageLengths)
|
||||
try encryptMessageLengthsVectors.forEach { length in
|
||||
let randomBytes = Data.secureRandomBytes(count: 32)
|
||||
XCTAssertThrowsError(try NIP44v2Encryption.encrypt(plaintext: String(repeating: "a", count: length), conversationKey: randomBytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculating conversation key must throw an error.
|
||||
func testInvalidConversationKey() throws {
|
||||
let conversationKeyVectors = try XCTUnwrap(vectors.v2.invalid.getConversationKey)
|
||||
|
||||
try conversationKeyVectors.forEach { vector in
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let publicKeyB = try XCTUnwrap(Pubkey(hex: vector.pub2))
|
||||
XCTAssertThrowsError(try NIP44v2Encryption.conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB), vector.note ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypting message content must throw an error
|
||||
func testInvalidDecrypt() throws {
|
||||
let decryptVectors = try XCTUnwrap(vectors.v2.invalid.decrypt)
|
||||
try decryptVectors.forEach { vector in
|
||||
let conversationKey = try XCTUnwrap(vector.conversationKey.hexDecoded).bytes
|
||||
let payload = vector.payload
|
||||
XCTAssertThrowsError(try NIP44v2Encryption.decrypt(payload: payload, conversationKey: conversationKey), vector.note)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct NIP44Vectors: Decodable {
|
||||
let v2: NIP44VectorsV2
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case v2
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2: Decodable {
|
||||
let valid: NIP44VectorsV2Valid
|
||||
let invalid: NIP44VectorsV2Invalid
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case valid
|
||||
case invalid
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2Valid: Decodable {
|
||||
let getConversationKey: [NIP44VectorsV2GetConversationKey]
|
||||
let getMessageKeys: NIP44VectorsV2GetMessageKeys
|
||||
let calculatePaddedLength: [[Int]]
|
||||
let encryptDecrypt: [NIP44VectorsV2EncryptDecrypt]
|
||||
let encryptDecryptLongMessage: [NIP44VectorsV2EncryptDecryptLongMessage]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case getConversationKey = "get_conversation_key"
|
||||
case getMessageKeys = "get_message_keys"
|
||||
case calculatePaddedLength = "calc_padded_len"
|
||||
case encryptDecrypt = "encrypt_decrypt"
|
||||
case encryptDecryptLongMessage = "encrypt_decrypt_long_msg"
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2Invalid: Decodable {
|
||||
let encryptMessageLengths: [Int]
|
||||
let getConversationKey: [NIP44VectorsV2GetConversationKey]
|
||||
let decrypt: [NIP44VectorsDecrypt]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case encryptMessageLengths = "encrypt_msg_lengths"
|
||||
case getConversationKey = "get_conversation_key"
|
||||
case decrypt
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsDecrypt: Decodable {
|
||||
let conversationKey: String
|
||||
let nonce: String
|
||||
let plaintext: String
|
||||
let payload: String
|
||||
let note: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case conversationKey = "conversation_key"
|
||||
case nonce
|
||||
case plaintext
|
||||
case payload
|
||||
case note
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2GetConversationKey: Decodable {
|
||||
let sec1: String
|
||||
let pub2: String
|
||||
let conversationKey: String?
|
||||
let note: String?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sec1
|
||||
case pub2
|
||||
case conversationKey = "conversation_key"
|
||||
case note
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2GetMessageKeys: Decodable {
|
||||
let conversationKey: String
|
||||
let keys: [NIP44VectorsV2MessageKeys]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case conversationKey = "conversation_key"
|
||||
case keys
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2MessageKeys: Decodable {
|
||||
let nonce: String
|
||||
let chaChaKey: String
|
||||
let chaChaNonce: String
|
||||
let hmacKey: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case nonce
|
||||
case chaChaKey = "chacha_key"
|
||||
case chaChaNonce = "chacha_nonce"
|
||||
case hmacKey = "hmac_key"
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2EncryptDecrypt: Decodable {
|
||||
let sec1: String
|
||||
let sec2: String
|
||||
let conversationKey: String
|
||||
let nonce: String
|
||||
let plaintext: String
|
||||
let payload: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sec1
|
||||
case sec2
|
||||
case conversationKey = "conversation_key"
|
||||
case nonce
|
||||
case plaintext
|
||||
case payload
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2EncryptDecryptLongMessage: Decodable {
|
||||
let conversationKey: String
|
||||
let nonce: String
|
||||
let pattern: String
|
||||
let repeatCount: Int
|
||||
let plaintextSHA256: String
|
||||
let payloadSHA256: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case conversationKey = "conversation_key"
|
||||
case nonce
|
||||
case pattern
|
||||
case repeatCount = "repeat"
|
||||
case plaintextSHA256 = "plaintext_sha256"
|
||||
case payloadSHA256 = "payload_sha256"
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Data {
|
||||
var hexString: String {
|
||||
let hexDigits = Array("0123456789abcdef".utf16)
|
||||
var hexChars = [UTF16.CodeUnit]()
|
||||
hexChars.reserveCapacity(bytes.count * 2)
|
||||
|
||||
for byte in self {
|
||||
let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
|
||||
hexChars.append(hexDigits[index1])
|
||||
hexChars.append(hexDigits[index2])
|
||||
}
|
||||
|
||||
return String(utf16CodeUnits: hexChars, count: hexChars.count)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var hexDecoded: Data? {
|
||||
guard self.count.isMultiple(of: 2) else { return nil }
|
||||
|
||||
// https://stackoverflow.com/a/62517446/982195
|
||||
let stringArray = Array(self)
|
||||
var data = Data()
|
||||
for i in stride(from: 0, to: count, by: 2) {
|
||||
let pair = String(stringArray[i]) + String(stringArray[i + 1])
|
||||
if let byteNum = UInt8(pair, radix: 16) {
|
||||
let byte = Data([byteNum])
|
||||
data.append(byte)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP44v2EncryptingTests {
|
||||
func loadFixtureString(_ filename: String) throws -> String? {
|
||||
let data = try self.loadFixtureData(filename)
|
||||
|
||||
guard let originalString = String(data: data, encoding: .utf8) else {
|
||||
throw FixtureLoadingError.decodingError
|
||||
}
|
||||
|
||||
let trimmedString = originalString.filter { !"\n\t\r".contains($0) }
|
||||
return trimmedString
|
||||
}
|
||||
|
||||
func loadFixtureData(_ filename: String) throws -> Data {
|
||||
guard let bundleData = try? readBundleFile(name: filename, ext: "json") else {
|
||||
throw FixtureLoadingError.missingFile
|
||||
}
|
||||
return bundleData
|
||||
}
|
||||
|
||||
func decodeFixture<T: Decodable>(filename: String) throws -> T {
|
||||
let data = try self.loadFixtureData(filename)
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
|
||||
func readBundleFile(name: String, ext: String) throws -> Data {
|
||||
let bundle = Bundle(for: type(of: self))
|
||||
guard let fileURL = bundle.url(forResource: name, withExtension: ext) else {
|
||||
throw CocoaError(.fileReadNoSuchFile)
|
||||
}
|
||||
|
||||
return try Data(contentsOf: fileURL)
|
||||
}
|
||||
|
||||
enum FixtureLoadingError: Error {
|
||||
case missingFile
|
||||
case decodingError
|
||||
}
|
||||
}
|
||||
37
damusTests/RepostedTests.swift
Normal file
37
damusTests/RepostedTests.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// RepostedTests.swift
|
||||
// damusTests
|
||||
//
|
||||
// Created by Terry Yiu on 2/23/25.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import damus
|
||||
|
||||
final class RepostedTests: XCTestCase {
|
||||
|
||||
func testPeopleRepostedText() throws {
|
||||
let enUsLocale = Locale(identifier: "en-US")
|
||||
let damusState = test_damus_state
|
||||
let pubkey = test_pubkey
|
||||
|
||||
// reposts must be greater than 0. Empty string is returned as a fallback if not.
|
||||
XCTAssertEqual(people_reposted_text(profiles: damusState.profiles, pubkey: pubkey, reposts: -1, locale: enUsLocale), "")
|
||||
XCTAssertEqual(people_reposted_text(profiles: damusState.profiles, pubkey: pubkey, reposts: 0, locale: enUsLocale), "")
|
||||
|
||||
// Verify the English pluralization variations.
|
||||
XCTAssertEqual(people_reposted_text(profiles: damusState.profiles, pubkey: pubkey, reposts: 1, locale: enUsLocale), "17ldvg64:nq5mhr77 reposted")
|
||||
XCTAssertEqual(people_reposted_text(profiles: damusState.profiles, pubkey: pubkey, reposts: 2, locale: enUsLocale), "17ldvg64:nq5mhr77 and 1 other reposted")
|
||||
XCTAssertEqual(people_reposted_text(profiles: damusState.profiles, pubkey: pubkey, reposts: 3, locale: enUsLocale), "17ldvg64:nq5mhr77 and 2 others reposted")
|
||||
|
||||
// Sanity check that the non-English translations are likely not malformed.
|
||||
Bundle.main.localizations.map { Locale(identifier: $0) }.forEach {
|
||||
// -1...11 covers a lot (but not all) pluralization rules for different languages.
|
||||
// However, it is good enough for a sanity check.
|
||||
for reposts in -1...11 {
|
||||
XCTAssertNoThrow(people_reposted_text(profiles: damusState.profiles, pubkey: pubkey, reposts: reposts, locale: $0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
648
damusTests/nip44.vectors.json
Normal file
648
damusTests/nip44.vectors.json
Normal file
@@ -0,0 +1,648 @@
|
||||
{
|
||||
"v2": {
|
||||
"valid": {
|
||||
"get_conversation_key": [
|
||||
{
|
||||
"sec1": "315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268",
|
||||
"pub2": "c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133",
|
||||
"conversation_key": "3dfef0ce2a4d80a25e7a328accf73448ef67096f65f79588e358d9a0eb9013f1"
|
||||
},
|
||||
{
|
||||
"sec1": "a1e37752c9fdc1273be53f68c5f74be7c8905728e8de75800b94262f9497c86e",
|
||||
"pub2": "03bb7947065dde12ba991ea045132581d0954f042c84e06d8c00066e23c1a800",
|
||||
"conversation_key": "4d14f36e81b8452128da64fe6f1eae873baae2f444b02c950b90e43553f2178b"
|
||||
},
|
||||
{
|
||||
"sec1": "98a5902fd67518a0c900f0fb62158f278f94a21d6f9d33d30cd3091195500311",
|
||||
"pub2": "aae65c15f98e5e677b5050de82e3aba47a6fe49b3dab7863cf35d9478ba9f7d1",
|
||||
"conversation_key": "9c00b769d5f54d02bf175b7284a1cbd28b6911b06cda6666b2243561ac96bad7"
|
||||
},
|
||||
{
|
||||
"sec1": "86ae5ac8034eb2542ce23ec2f84375655dab7f836836bbd3c54cefe9fdc9c19f",
|
||||
"pub2": "59f90272378089d73f1339710c02e2be6db584e9cdbe86eed3578f0c67c23585",
|
||||
"conversation_key": "19f934aafd3324e8415299b64df42049afaa051c71c98d0aa10e1081f2e3e2ba"
|
||||
},
|
||||
{
|
||||
"sec1": "2528c287fe822421bc0dc4c3615878eb98e8a8c31657616d08b29c00ce209e34",
|
||||
"pub2": "f66ea16104c01a1c532e03f166c5370a22a5505753005a566366097150c6df60",
|
||||
"conversation_key": "c833bbb292956c43366145326d53b955ffb5da4e4998a2d853611841903f5442"
|
||||
},
|
||||
{
|
||||
"sec1": "49808637b2d21129478041813aceb6f2c9d4929cd1303cdaf4fbdbd690905ff2",
|
||||
"pub2": "74d2aab13e97827ea21baf253ad7e39b974bb2498cc747cdb168582a11847b65",
|
||||
"conversation_key": "4bf304d3c8c4608864c0fe03890b90279328cd24a018ffa9eb8f8ccec06b505d"
|
||||
},
|
||||
{
|
||||
"sec1": "af67c382106242c5baabf856efdc0629cc1c5b4061f85b8ceaba52aa7e4b4082",
|
||||
"pub2": "bdaf0001d63e7ec994fad736eab178ee3c2d7cfc925ae29f37d19224486db57b",
|
||||
"conversation_key": "a3a575dd66d45e9379904047ebfb9a7873c471687d0535db00ef2daa24b391db"
|
||||
},
|
||||
{
|
||||
"sec1": "0e44e2d1db3c1717b05ffa0f08d102a09c554a1cbbf678ab158b259a44e682f1",
|
||||
"pub2": "1ffa76c5cc7a836af6914b840483726207cb750889753d7499fb8b76aa8fe0de",
|
||||
"conversation_key": "a39970a667b7f861f100e3827f4adbf6f464e2697686fe1a81aeda817d6b8bdf"
|
||||
},
|
||||
{
|
||||
"sec1": "5fc0070dbd0666dbddc21d788db04050b86ed8b456b080794c2a0c8e33287bb6",
|
||||
"pub2": "31990752f296dd22e146c9e6f152a269d84b241cc95bb3ff8ec341628a54caf0",
|
||||
"conversation_key": "72c21075f4b2349ce01a3e604e02a9ab9f07e35dd07eff746de348b4f3c6365e"
|
||||
},
|
||||
{
|
||||
"sec1": "1b7de0d64d9b12ddbb52ef217a3a7c47c4362ce7ea837d760dad58ab313cba64",
|
||||
"pub2": "24383541dd8083b93d144b431679d70ef4eec10c98fceef1eff08b1d81d4b065",
|
||||
"conversation_key": "dd152a76b44e63d1afd4dfff0785fa07b3e494a9e8401aba31ff925caeb8f5b1"
|
||||
},
|
||||
{
|
||||
"sec1": "df2f560e213ca5fb33b9ecde771c7c0cbd30f1cf43c2c24de54480069d9ab0af",
|
||||
"pub2": "eeea26e552fc8b5e377acaa03e47daa2d7b0c787fac1e0774c9504d9094c430e",
|
||||
"conversation_key": "770519e803b80f411c34aef59c3ca018608842ebf53909c48d35250bd9323af6"
|
||||
},
|
||||
{
|
||||
"sec1": "cffff919fcc07b8003fdc63bc8a00c0f5dc81022c1c927c62c597352190d95b9",
|
||||
"pub2": "eb5c3cca1a968e26684e5b0eb733aecfc844f95a09ac4e126a9e58a4e4902f92",
|
||||
"conversation_key": "46a14ee7e80e439ec75c66f04ad824b53a632b8409a29bbb7c192e43c00bb795"
|
||||
},
|
||||
{
|
||||
"sec1": "64ba5a685e443e881e9094647ddd32db14444bb21aa7986beeba3d1c4673ba0a",
|
||||
"pub2": "50e6a4339fac1f3bf86f2401dd797af43ad45bbf58e0801a7877a3984c77c3c4",
|
||||
"conversation_key": "968b9dbbfcede1664a4ca35a5d3379c064736e87aafbf0b5d114dff710b8a946"
|
||||
},
|
||||
{
|
||||
"sec1": "dd0c31ccce4ec8083f9b75dbf23cc2878e6d1b6baa17713841a2428f69dee91a",
|
||||
"pub2": "b483e84c1339812bed25be55cff959778dfc6edde97ccd9e3649f442472c091b",
|
||||
"conversation_key": "09024503c7bde07eb7865505891c1ea672bf2d9e25e18dd7a7cea6c69bf44b5d"
|
||||
},
|
||||
{
|
||||
"sec1": "af71313b0d95c41e968a172b33ba5ebd19d06cdf8a7a98df80ecf7af4f6f0358",
|
||||
"pub2": "2a5c25266695b461ee2af927a6c44a3c598b8095b0557e9bd7f787067435bc7c",
|
||||
"conversation_key": "fe5155b27c1c4b4e92a933edae23726a04802a7cc354a77ac273c85aa3c97a92"
|
||||
},
|
||||
{
|
||||
"sec1": "6636e8a389f75fe068a03b3edb3ea4a785e2768e3f73f48ffb1fc5e7cb7289dc",
|
||||
"pub2": "514eb2064224b6a5829ea21b6e8f7d3ea15ff8e70e8555010f649eb6e09aec70",
|
||||
"conversation_key": "ff7afacd4d1a6856d37ca5b546890e46e922b508639214991cf8048ddbe9745c"
|
||||
},
|
||||
{
|
||||
"sec1": "94b212f02a3cfb8ad147d52941d3f1dbe1753804458e6645af92c7b2ea791caa",
|
||||
"pub2": "f0cac333231367a04b652a77ab4f8d658b94e86b5a8a0c472c5c7b0d4c6a40cc",
|
||||
"conversation_key": "e292eaf873addfed0a457c6bd16c8effde33d6664265697f69f420ab16f6669b"
|
||||
},
|
||||
{
|
||||
"sec1": "aa61f9734e69ae88e5d4ced5aae881c96f0d7f16cca603d3bed9eec391136da6",
|
||||
"pub2": "4303e5360a884c360221de8606b72dd316da49a37fe51e17ada4f35f671620a6",
|
||||
"conversation_key": "8e7d44fd4767456df1fb61f134092a52fcd6836ebab3b00766e16732683ed848"
|
||||
},
|
||||
{
|
||||
"sec1": "5e914bdac54f3f8e2cba94ee898b33240019297b69e96e70c8a495943a72fc98",
|
||||
"pub2": "5bd097924f606695c59f18ff8fd53c174adbafaaa71b3c0b4144a3e0a474b198",
|
||||
"conversation_key": "f5a0aecf2984bf923c8cd5e7bb8be262d1a8353cb93959434b943a07cf5644bc"
|
||||
},
|
||||
{
|
||||
"sec1": "8b275067add6312ddee064bcdbeb9d17e88aa1df36f430b2cea5cc0413d8278a",
|
||||
"pub2": "65bbbfca819c90c7579f7a82b750a18c858db1afbec8f35b3c1e0e7b5588e9b8",
|
||||
"conversation_key": "2c565e7027eb46038c2263563d7af681697107e975e9914b799d425effd248d6"
|
||||
},
|
||||
{
|
||||
"sec1": "1ac848de312285f85e0f7ec208aac20142a1f453402af9b34ec2ec7a1f9c96fc",
|
||||
"pub2": "45f7318fe96034d23ee3ddc25b77f275cc1dd329664dd51b89f89c4963868e41",
|
||||
"conversation_key": "b56e970e5057a8fd929f8aad9248176b9af87819a708d9ddd56e41d1aec74088"
|
||||
},
|
||||
{
|
||||
"sec1": "295a1cf621de401783d29d0e89036aa1c62d13d9ad307161b4ceb535ba1b40e6",
|
||||
"pub2": "840115ddc7f1034d3b21d8e2103f6cb5ab0b63cf613f4ea6e61ae3d016715cdd",
|
||||
"conversation_key": "b4ee9c0b9b9fef88975773394f0a6f981ca016076143a1bb575b9ff46e804753"
|
||||
},
|
||||
{
|
||||
"sec1": "a28eed0fe977893856ab9667e06ace39f03abbcdb845c329a1981be438ba565d",
|
||||
"pub2": "b0f38b950a5013eba5ab4237f9ed29204a59f3625c71b7e210fec565edfa288c",
|
||||
"conversation_key": "9d3a802b45bc5aeeb3b303e8e18a92ddd353375710a31600d7f5fff8f3a7285b"
|
||||
},
|
||||
{
|
||||
"sec1": "7ab65af72a478c05f5c651bdc4876c74b63d20d04cdbf71741e46978797cd5a4",
|
||||
"pub2": "f1112159161b568a9cb8c9dd6430b526c4204bcc8ce07464b0845b04c041beda",
|
||||
"conversation_key": "943884cddaca5a3fef355e9e7f08a3019b0b66aa63ec90278b0f9fdb64821e79"
|
||||
},
|
||||
{
|
||||
"sec1": "95c79a7b75ba40f2229e85756884c138916f9d103fc8f18acc0877a7cceac9fe",
|
||||
"pub2": "cad76bcbd31ca7bbda184d20cc42f725ed0bb105b13580c41330e03023f0ffb3",
|
||||
"conversation_key": "81c0832a669eea13b4247c40be51ccfd15bb63fcd1bba5b4530ce0e2632f301b"
|
||||
},
|
||||
{
|
||||
"sec1": "baf55cc2febd4d980b4b393972dfc1acf49541e336b56d33d429bce44fa12ec9",
|
||||
"pub2": "0c31cf87fe565766089b64b39460ebbfdedd4a2bc8379be73ad3c0718c912e18",
|
||||
"conversation_key": "37e2344da9ecdf60ae2205d81e89d34b280b0a3f111171af7e4391ded93b8ea6"
|
||||
},
|
||||
{
|
||||
"sec1": "6eeec45acd2ed31693c5256026abf9f072f01c4abb61f51cf64e6956b6dc8907",
|
||||
"pub2": "e501b34ed11f13d816748c0369b0c728e540df3755bab59ed3327339e16ff828",
|
||||
"conversation_key": "afaa141b522ddb27bb880d768903a7f618bb8b6357728cae7fb03af639b946e6"
|
||||
},
|
||||
{
|
||||
"sec1": "261a076a9702af1647fb343c55b3f9a4f1096273002287df0015ba81ce5294df",
|
||||
"pub2": "b2777c863878893ae100fb740c8fab4bebd2bf7be78c761a75593670380a6112",
|
||||
"conversation_key": "76f8d2853de0734e51189ced523c09427c3e46338b9522cd6f74ef5e5b475c74"
|
||||
},
|
||||
{
|
||||
"sec1": "ed3ec71ca406552ea41faec53e19f44b8f90575eda4b7e96380f9cc73c26d6f3",
|
||||
"pub2": "86425951e61f94b62e20cae24184b42e8e17afcf55bafa58645efd0172624fae",
|
||||
"conversation_key": "f7ffc520a3a0e9e9b3c0967325c9bf12707f8e7a03f28b6cd69ae92cf33f7036"
|
||||
},
|
||||
{
|
||||
"sec1": "5a788fc43378d1303ac78639c59a58cb88b08b3859df33193e63a5a3801c722e",
|
||||
"pub2": "a8cba2f87657d229db69bee07850fd6f7a2ed070171a06d006ec3a8ac562cf70",
|
||||
"conversation_key": "7d705a27feeedf78b5c07283362f8e361760d3e9f78adab83e3ae5ce7aeb6409"
|
||||
},
|
||||
{
|
||||
"sec1": "63bffa986e382b0ac8ccc1aa93d18a7aa445116478be6f2453bad1f2d3af2344",
|
||||
"pub2": "b895c70a83e782c1cf84af558d1038e6b211c6f84ede60408f519a293201031d",
|
||||
"conversation_key": "3a3b8f00d4987fc6711d9be64d9c59cf9a709c6c6481c2cde404bcc7a28f174e"
|
||||
},
|
||||
{
|
||||
"sec1": "e4a8bcacbf445fd3721792b939ff58e691cdcba6a8ba67ac3467b45567a03e5c",
|
||||
"pub2": "b54053189e8c9252c6950059c783edb10675d06d20c7b342f73ec9fa6ed39c9d",
|
||||
"conversation_key": "7b3933b4ef8189d347169c7955589fc1cfc01da5239591a08a183ff6694c44ad"
|
||||
},
|
||||
{
|
||||
"sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139",
|
||||
"pub2": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"conversation_key": "8b6392dbf2ec6a2b2d5b1477fc2be84d63ef254b667cadd31bd3f444c44ae6ba",
|
||||
"note": "sec1 = n-2, pub2: random, 0x02"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb",
|
||||
"conversation_key": "be234f46f60a250bef52a5ee34c758800c4ca8e5030bf4cc1a31d37ba2104d43",
|
||||
"note": "sec1 = 2, pub2: rand"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"pub2": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"conversation_key": "3b4610cb7189beb9cc29eb3716ecc6102f1247e8f3101a03a1787d8908aeb54e",
|
||||
"note": "sec1 == pub2"
|
||||
}
|
||||
],
|
||||
"get_message_keys": {
|
||||
"conversation_key": "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54",
|
||||
"keys": [
|
||||
{
|
||||
"nonce": "e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72",
|
||||
"chacha_key": "f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76",
|
||||
"chacha_nonce": "c4ad129bb01180c0933a160c",
|
||||
"hmac_key": "027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4"
|
||||
},
|
||||
{
|
||||
"nonce": "e1d6d28c46de60168b43d79dacc519698512ec35e8ccb12640fc8e9f26121101",
|
||||
"chacha_key": "e35b88f8d4a8f1606c5082f7a64b100e5d85fcdb2e62aeafbec03fb9e860ad92",
|
||||
"chacha_nonce": "22925e920cee4a50a478be90",
|
||||
"hmac_key": "46a7c55d4283cb0df1d5e29540be67abfe709e3b2e14b7bf9976e6df994ded30"
|
||||
},
|
||||
{
|
||||
"nonce": "cfc13bef512ac9c15951ab00030dfaf2626fdca638dedb35f2993a9eeb85d650",
|
||||
"chacha_key": "020783eb35fdf5b80ef8c75377f4e937efb26bcbad0e61b4190e39939860c4bf",
|
||||
"chacha_nonce": "d3594987af769a52904656ac",
|
||||
"hmac_key": "237ec0ccb6ebd53d179fa8fd319e092acff599ef174c1fdafd499ef2b8dee745"
|
||||
},
|
||||
{
|
||||
"nonce": "ea6eb84cac23c5c1607c334e8bdf66f7977a7e374052327ec28c6906cbe25967",
|
||||
"chacha_key": "ff68db24b34fa62c78ac5ffeeaf19533afaedf651fb6a08384e46787f6ce94be",
|
||||
"chacha_nonce": "50bb859aa2dde938cc49ec7a",
|
||||
"hmac_key": "06ff32e1f7b29753a727d7927b25c2dd175aca47751462d37a2039023ec6b5a6"
|
||||
},
|
||||
{
|
||||
"nonce": "8c2e1dd3792802f1f9f7842e0323e5d52ad7472daf360f26e15f97290173605d",
|
||||
"chacha_key": "2f9daeda8683fdeede81adac247c63cc7671fa817a1fd47352e95d9487989d8b",
|
||||
"chacha_nonce": "400224ba67fc2f1b76736916",
|
||||
"hmac_key": "465c05302aeeb514e41c13ed6405297e261048cfb75a6f851ffa5b445b746e4b"
|
||||
},
|
||||
{
|
||||
"nonce": "05c28bf3d834fa4af8143bf5201a856fa5fac1a3aee58f4c93a764fc2f722367",
|
||||
"chacha_key": "1e3d45777025a035be566d80fd580def73ed6f7c043faec2c8c1c690ad31c110",
|
||||
"chacha_nonce": "021905b1ea3afc17cb9bf96f",
|
||||
"hmac_key": "74a6e481a89dcd130aaeb21060d7ec97ad30f0007d2cae7b1b11256cc70dfb81"
|
||||
},
|
||||
{
|
||||
"nonce": "5e043fb153227866e75a06d60185851bc90273bfb93342f6632a728e18a07a17",
|
||||
"chacha_key": "1ea72c9293841e7737c71567d8120145a58991aaa1c436ef77bf7adb83f882f1",
|
||||
"chacha_nonce": "72f69a5a5f795465cee59da8",
|
||||
"hmac_key": "e9daa1a1e9a266ecaa14e970a84bce3fbbf329079bbccda626582b4e66a0d4c9"
|
||||
},
|
||||
{
|
||||
"nonce": "7be7338eaf06a87e274244847fe7a97f5c6a91f44adc18fcc3e411ad6f786dbf",
|
||||
"chacha_key": "881e7968a1f0c2c80742ee03cd49ea587e13f22699730f1075ade01931582bf6",
|
||||
"chacha_nonce": "6e69be92d61c04a276021565",
|
||||
"hmac_key": "901afe79e74b19967c8829af23617d7d0ffbf1b57190c096855c6a03523a971b"
|
||||
},
|
||||
{
|
||||
"nonce": "94571c8d590905bad7becd892832b472f2aa5212894b6ce96e5ba719c178d976",
|
||||
"chacha_key": "f80873dd48466cb12d46364a97b8705c01b9b4230cb3ec3415a6b9551dc42eef",
|
||||
"chacha_nonce": "3dda53569cfcb7fac1805c35",
|
||||
"hmac_key": "e9fc264345e2839a181affebc27d2f528756e66a5f87b04bf6c5f1997047051e"
|
||||
},
|
||||
{
|
||||
"nonce": "13a6ee974b1fd759135a2c2010e3cdda47081c78e771125e4f0c382f0284a8cb",
|
||||
"chacha_key": "bc5fb403b0bed0d84cf1db872b6522072aece00363178c98ad52178d805fca85",
|
||||
"chacha_nonce": "65064239186e50304cc0f156",
|
||||
"hmac_key": "e872d320dde4ed3487958a8e43b48aabd3ced92bc24bb8ff1ccb57b590d9701a"
|
||||
},
|
||||
{
|
||||
"nonce": "082fecdb85f358367b049b08be0e82627ae1d8edb0f27327ccb593aa2613b814",
|
||||
"chacha_key": "1fbdb1cf6f6ea816349baf697932b36107803de98fcd805ebe9849b8ad0e6a45",
|
||||
"chacha_nonce": "2e605e1d825a3eaeb613db9c",
|
||||
"hmac_key": "fae910f591cf3c7eb538c598583abad33bc0a03085a96ca4ea3a08baf17c0eec"
|
||||
},
|
||||
{
|
||||
"nonce": "4c19020c74932c30ec6b2d8cd0d5bb80bd0fc87da3d8b4859d2fb003810afd03",
|
||||
"chacha_key": "1ab9905a0189e01cda82f843d226a82a03c4f5b6dbea9b22eb9bc953ba1370d4",
|
||||
"chacha_nonce": "cbb2530ea653766e5a37a83a",
|
||||
"hmac_key": "267f68acac01ac7b34b675e36c2cef5e7b7a6b697214add62a491bedd6efc178"
|
||||
},
|
||||
{
|
||||
"nonce": "67723a3381497b149ce24814eddd10c4c41a1e37e75af161930e6b9601afd0ff",
|
||||
"chacha_key": "9ecbd25e7e2e6c97b8c27d376dcc8c5679da96578557e4e21dba3a7ef4e4ac07",
|
||||
"chacha_nonce": "ef649fcf335583e8d45e3c2e",
|
||||
"hmac_key": "04dbbd812fa8226fdb45924c521a62e3d40a9e2b5806c1501efdeba75b006bf1"
|
||||
},
|
||||
{
|
||||
"nonce": "42063fe80b093e8619b1610972b4c3ab9e76c14fd908e642cd4997cafb30f36c",
|
||||
"chacha_key": "211c66531bbcc0efcdd0130f9f1ebc12a769105eb39608994bcb188fa6a73a4a",
|
||||
"chacha_nonce": "67803605a7e5010d0f63f8c8",
|
||||
"hmac_key": "e840e4e8921b57647369d121c5a19310648105dbdd008200ebf0d3b668704ff8"
|
||||
},
|
||||
{
|
||||
"nonce": "b5ac382a4be7ac03b554fe5f3043577b47ea2cd7cfc7e9ca010b1ffbb5cf1a58",
|
||||
"chacha_key": "b3b5f14f10074244ee42a3837a54309f33981c7232a8b16921e815e1f7d1bb77",
|
||||
"chacha_nonce": "4e62a0073087ed808be62469",
|
||||
"hmac_key": "c8efa10230b5ea11633816c1230ca05fa602ace80a7598916d83bae3d3d2ccd7"
|
||||
},
|
||||
{
|
||||
"nonce": "e9d1eba47dd7e6c1532dc782ff63125db83042bb32841db7eeafd528f3ea7af9",
|
||||
"chacha_key": "54241f68dc2e50e1db79e892c7c7a471856beeb8d51b7f4d16f16ab0645d2f1a",
|
||||
"chacha_nonce": "a963ed7dc29b7b1046820a1d",
|
||||
"hmac_key": "aba215c8634530dc21c70ddb3b3ee4291e0fa5fa79be0f85863747bde281c8b2"
|
||||
},
|
||||
{
|
||||
"nonce": "a94ecf8efeee9d7068de730fad8daf96694acb70901d762de39fa8a5039c3c49",
|
||||
"chacha_key": "c0565e9e201d2381a2368d7ffe60f555223874610d3d91fbbdf3076f7b1374dd",
|
||||
"chacha_nonce": "329bb3024461e84b2e1c489b",
|
||||
"hmac_key": "ac42445491f092481ce4fa33b1f2274700032db64e3a15014fbe8c28550f2fec"
|
||||
},
|
||||
{
|
||||
"nonce": "533605ea214e70c25e9a22f792f4b78b9f83a18ab2103687c8a0075919eaaa53",
|
||||
"chacha_key": "ab35a5e1e54d693ff023db8500d8d4e79ad8878c744e0eaec691e96e141d2325",
|
||||
"chacha_nonce": "653d759042b85194d4d8c0a7",
|
||||
"hmac_key": "b43628e37ba3c31ce80576f0a1f26d3a7c9361d29bb227433b66f49d44f167ba"
|
||||
},
|
||||
{
|
||||
"nonce": "7f38df30ceea1577cb60b355b4f5567ff4130c49e84fed34d779b764a9cc184c",
|
||||
"chacha_key": "a37d7f211b84a551a127ff40908974eb78415395d4f6f40324428e850e8c42a3",
|
||||
"chacha_nonce": "b822e2c959df32b3cb772a7c",
|
||||
"hmac_key": "1ba31764f01f69b5c89ded2d7c95828e8052c55f5d36f1cd535510d61ba77420"
|
||||
},
|
||||
{
|
||||
"nonce": "11b37f9dbc4d0185d1c26d5f4ed98637d7c9701fffa65a65839fa4126573a4e5",
|
||||
"chacha_key": "964f38d3a31158a5bfd28481247b18dd6e44d69f30ba2a40f6120c6d21d8a6ba",
|
||||
"chacha_nonce": "5f72c5b87c590bcd0f93b305",
|
||||
"hmac_key": "2fc4553e7cedc47f29690439890f9f19c1077ef3e9eaeef473d0711e04448918"
|
||||
},
|
||||
{
|
||||
"nonce": "8be790aa483d4cdd843189f71f135b3ec7e31f381312c8fe9f177aab2a48eafa",
|
||||
"chacha_key": "95c8c74d633721a131316309cf6daf0804d59eaa90ea998fc35bac3d2fbb7a94",
|
||||
"chacha_nonce": "409a7654c0e4bf8c2c6489be",
|
||||
"hmac_key": "21bb0b06eb2b460f8ab075f497efa9a01c9cf9146f1e3986c3bf9da5689b6dc4"
|
||||
},
|
||||
{
|
||||
"nonce": "19fd2a718ea084827d6bd73f509229ddf856732108b59fc01819f611419fd140",
|
||||
"chacha_key": "cc6714b9f5616c66143424e1413d520dae03b1a4bd202b82b0a89b0727f5cdc8",
|
||||
"chacha_nonce": "1b7fd2534f015a8f795d8f32",
|
||||
"hmac_key": "2bef39c4ce5c3c59b817e86351373d1554c98bc131c7e461ed19d96cfd6399a0"
|
||||
},
|
||||
{
|
||||
"nonce": "3c2acd893952b2f6d07d8aea76f545ca45961a93fe5757f6a5a80811d5e0255d",
|
||||
"chacha_key": "c8de6c878cb469278d0af894bc181deb6194053f73da5014c2b5d2c8db6f2056",
|
||||
"chacha_nonce": "6ffe4f1971b904a1b1a81b99",
|
||||
"hmac_key": "df1cd69dd3646fca15594284744d4211d70e7d8472e545d276421fbb79559fd4"
|
||||
},
|
||||
{
|
||||
"nonce": "7dbea4cead9ac91d4137f1c0a6eebb6ba0d1fb2cc46d829fbc75f8d86aca6301",
|
||||
"chacha_key": "c8e030f6aa680c3d0b597da9c92bb77c21c4285dd620c5889f9beba7446446b0",
|
||||
"chacha_nonce": "a9b5a67d081d3b42e737d16f",
|
||||
"hmac_key": "355a85f551bc3cce9a14461aa60994742c9bbb1c81a59ca102dc64e61726ab8e"
|
||||
},
|
||||
{
|
||||
"nonce": "45422e676cdae5f1071d3647d7a5f1f5adafb832668a578228aa1155a491f2f3",
|
||||
"chacha_key": "758437245f03a88e2c6a32807edfabff51a91c81ca2f389b0b46f2c97119ea90",
|
||||
"chacha_nonce": "263830a065af33d9c6c5aa1f",
|
||||
"hmac_key": "7c581cf3489e2de203a95106bfc0de3d4032e9d5b92b2b61fb444acd99037e17"
|
||||
},
|
||||
{
|
||||
"nonce": "babc0c03fad24107ad60678751f5db2678041ff0d28671ede8d65bdf7aa407e9",
|
||||
"chacha_key": "bd68a28bd48d9ffa3602db72c75662ac2848a0047a313d2ae2d6bc1ac153d7e9",
|
||||
"chacha_nonce": "d0f9d2a1ace6c758f594ffdd",
|
||||
"hmac_key": "eb435e3a642adfc9d59813051606fc21f81641afd58ea6641e2f5a9f123bb50a"
|
||||
},
|
||||
{
|
||||
"nonce": "7a1b8aac37d0d20b160291fad124ab697cfca53f82e326d78fef89b4b0ea8f83",
|
||||
"chacha_key": "9e97875b651a1d30d17d086d1e846778b7faad6fcbc12e08b3365d700f62e4fe",
|
||||
"chacha_nonce": "ccdaad5b3b7645be430992eb",
|
||||
"hmac_key": "6f2f55cf35174d75752f63c06cc7cbc8441759b142999ed2d5a6d09d263e1fc4"
|
||||
},
|
||||
{
|
||||
"nonce": "8370e4e32d7e680a83862cab0da6136ef607014d043e64cdf5ecc0c4e20b3d9a",
|
||||
"chacha_key": "1472bed5d19db9c546106de946e0649cd83cc9d4a66b087a65906e348dcf92e2",
|
||||
"chacha_nonce": "ed02dece5fc3a186f123420b",
|
||||
"hmac_key": "7b3f7739f49d30c6205a46b174f984bb6a9fc38e5ccfacef2dac04fcbd3b184e"
|
||||
},
|
||||
{
|
||||
"nonce": "9f1c5e8a29cd5677513c2e3a816551d6833ee54991eb3f00d5b68096fc8f0183",
|
||||
"chacha_key": "5e1a7544e4d4dafe55941fcbdf326f19b0ca37fc49c4d47e9eec7fb68cde4975",
|
||||
"chacha_nonce": "7d9acb0fdc174e3c220f40de",
|
||||
"hmac_key": "e265ab116fbbb86b2aefc089a0986a0f5b77eda50c7410404ad3b4f3f385c7a7"
|
||||
},
|
||||
{
|
||||
"nonce": "c385aa1c37c2bfd5cc35fcdbdf601034d39195e1cabff664ceb2b787c15d0225",
|
||||
"chacha_key": "06bf4e60677a13e54c4a38ab824d2ef79da22b690da2b82d0aa3e39a14ca7bdd",
|
||||
"chacha_nonce": "26b450612ca5e905b937e147",
|
||||
"hmac_key": "22208152be2b1f5f75e6bfcc1f87763d48bb7a74da1be3d102096f257207f8b3"
|
||||
},
|
||||
{
|
||||
"nonce": "3ff73528f88a50f9d35c0ddba4560bacee5b0462d0f4cb6e91caf41847040ce4",
|
||||
"chacha_key": "850c8a17a23aa761d279d9901015b2bbdfdff00adbf6bc5cf22bd44d24ecabc9",
|
||||
"chacha_nonce": "4a296a1fb0048e5020d3b129",
|
||||
"hmac_key": "b1bf49a533c4da9b1d629b7ff30882e12d37d49c19abd7b01b7807d75ee13806"
|
||||
},
|
||||
{
|
||||
"nonce": "2dcf39b9d4c52f1cb9db2d516c43a7c6c3b8c401f6a4ac8f131a9e1059957036",
|
||||
"chacha_key": "17f8057e6156ba7cc5310d01eda8c40f9aa388f9fd1712deb9511f13ecc37d27",
|
||||
"chacha_nonce": "a8188daff807a1182200b39d",
|
||||
"hmac_key": "47b89da97f68d389867b5d8a2d7ba55715a30e3d88a3cc11f3646bc2af5580ef"
|
||||
}
|
||||
]
|
||||
},
|
||||
"calc_padded_len": [
|
||||
[16, 32],
|
||||
[32, 32],
|
||||
[33, 64],
|
||||
[37, 64],
|
||||
[45, 64],
|
||||
[49, 64],
|
||||
[64, 64],
|
||||
[65, 96],
|
||||
[100, 128],
|
||||
[111, 128],
|
||||
[200, 224],
|
||||
[250, 256],
|
||||
[320, 320],
|
||||
[383, 384],
|
||||
[384, 384],
|
||||
[400, 448],
|
||||
[500, 512],
|
||||
[512, 512],
|
||||
[515, 640],
|
||||
[700, 768],
|
||||
[800, 896],
|
||||
[900, 1024],
|
||||
[1020, 1024],
|
||||
[65536, 65536]
|
||||
],
|
||||
"encrypt_decrypt": [
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"sec2": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",
|
||||
"nonce": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"plaintext": "a",
|
||||
"payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"sec2": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",
|
||||
"nonce": "f00000000000000000000000000000f00000000000000000000000000000000f",
|
||||
"plaintext": "🍕🫃",
|
||||
"payload": "AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPSKSK6is9ngkX2+cSq85Th16oRTISAOfhStnixqZziKMDvB0QQzgFZdjLTPicCJaV8nDITO+QfaQ61+KbWQIOO2Yj"
|
||||
},
|
||||
{
|
||||
"sec1": "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a",
|
||||
"sec2": "4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d",
|
||||
"conversation_key": "3e2b52a63be47d34fe0a80e34e73d436d6963bc8f39827f327057a9986c20a45",
|
||||
"nonce": "b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b",
|
||||
"plaintext": "表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀",
|
||||
"payload": "ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7s7JqlCMJBAIIjfkpHReBPXeoMCyuClwgbT419jUWU1PwaNl4FEQYKCDKVJz+97Mp3K+Q2YGa77B6gpxB/lr1QgoqpDf7wDVrDmOqGoiPjWDqy8KzLueKDcm9BVP8xeTJIxs="
|
||||
},
|
||||
{
|
||||
"sec1": "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c",
|
||||
"sec2": "b9b0a1e9cc20100c5faa3bbe2777303d25950616c4c6a3fa2e3e046f936ec2ba",
|
||||
"conversation_key": "d5a2f879123145a4b291d767428870f5a8d9e5007193321795b40183d4ab8c2b",
|
||||
"nonce": "b20989adc3ddc41cd2c435952c0d59a91315d8c5218d5040573fc3749543acaf",
|
||||
"plaintext": "ability🤝的 ȺȾ",
|
||||
"payload": "ArIJia3D3cQc0sQ1lSwNWakTFdjFIY1QQFc/w3SVQ6yvbG2S0x4Yu86QGwPTy7mP3961I1XqB6SFFTzqDZZavhxoWMj7mEVGMQIsh2RLWI5EYQaQDIePSnXPlzf7CIt+voTD"
|
||||
},
|
||||
{
|
||||
"sec1": "875adb475056aec0b4809bd2db9aa00cff53a649e7b59d8edcbf4e6330b0995c",
|
||||
"sec2": "9c05781112d5b0a2a7148a222e50e0bd891d6b60c5483f03456e982185944aae",
|
||||
"conversation_key": "3b15c977e20bfe4b8482991274635edd94f366595b1a3d2993515705ca3cedb8",
|
||||
"nonce": "8d4442713eb9d4791175cb040d98d6fc5be8864d6ec2f89cf0895a2b2b72d1b1",
|
||||
"plaintext": "pepper👀їжак",
|
||||
"payload": "Ao1EQnE+udR5EXXLBA2Y1vxb6IZNbsL4nPCJWisrctGxY3AduCS+jTUgAAnfvKafkmpy15+i9YMwCdccisRa8SvzW671T2JO4LFSPX31K4kYUKelSAdSPwe9NwO6LhOsnoJ+"
|
||||
},
|
||||
{
|
||||
"sec1": "eba1687cab6a3101bfc68fd70f214aa4cc059e9ec1b79fdb9ad0a0a4e259829f",
|
||||
"sec2": "dff20d262bef9dfd94666548f556393085e6ea421c8af86e9d333fa8747e94b3",
|
||||
"conversation_key": "4f1538411098cf11c8af216836444787c462d47f97287f46cf7edb2c4915b8a5",
|
||||
"nonce": "2180b52ae645fcf9f5080d81b1f0b5d6f2cd77ff3c986882bb549158462f3407",
|
||||
"plaintext": "( ͡° ͜ʖ ͡°)",
|
||||
"payload": "AiGAtSrmRfz59QgNgbHwtdbyzXf/PJhogrtUkVhGLzQHv4qhKQwnFQ54OjVMgqCea/Vj0YqBSdhqNR777TJ4zIUk7R0fnizp6l1zwgzWv7+ee6u+0/89KIjY5q1wu6inyuiv"
|
||||
},
|
||||
{
|
||||
"sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",
|
||||
"sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",
|
||||
"conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",
|
||||
"nonce": "e4cd5f7ce4eea024bc71b17ad456a986a74ac426c2c62b0a15eb5c5c8f888b68",
|
||||
"plaintext": "مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ،",
|
||||
"payload": "AuTNX3zk7qAkvHGxetRWqYanSsQmwsYrChXrXFyPiItoIBsWu1CB+sStla2M4VeANASHxM78i1CfHQQH1YbBy24Tng7emYW44ol6QkFD6D8Zq7QPl+8L1c47lx8RoODEQMvNCbOk5ffUV3/AhONHBXnffrI+0025c+uRGzfqpYki4lBqm9iYU+k3Tvjczq9wU0mkVDEaM34WiQi30MfkJdRbeeYaq6kNvGPunLb3xdjjs5DL720d61Flc5ZfoZm+CBhADy9D9XiVZYLKAlkijALJur9dATYKci6OBOoc2SJS2Clai5hOVzR0yVeyHRgRfH9aLSlWW5dXcUxTo7qqRjNf8W5+J4jF4gNQp5f5d0YA4vPAzjBwSP/5bGzNDslKfcAH"
|
||||
},
|
||||
{
|
||||
"sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",
|
||||
"sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",
|
||||
"conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",
|
||||
"nonce": "38d1ca0abef9e5f564e89761a86cee04574b6825d3ef2063b10ad75899e4b023",
|
||||
"plaintext": "الكل في المجمو عة (5)",
|
||||
"payload": "AjjRygq++eX1ZOiXYahs7gRXS2gl0+8gY7EK11iZ5LAjbOTrlfrxak5Lki42v2jMPpLSicy8eHjsWkkMtF0i925vOaKG/ZkMHh9ccQBdfTvgEGKzztedqDCAWb5TP1YwU1PsWaiiqG3+WgVvJiO4lUdMHXL7+zKKx8bgDtowzz4QAwI="
|
||||
},
|
||||
{
|
||||
"sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",
|
||||
"sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",
|
||||
"conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",
|
||||
"nonce": "4f1a31909f3483a9e69c8549a55bbc9af25fa5bbecf7bd32d9896f83ef2e12e0",
|
||||
"plaintext": "𝖑𝖆𝖟𝖞 社會科學院語學研究所",
|
||||
"payload": "Ak8aMZCfNIOp5pyFSaVbvJryX6W77Pe9MtmJb4PvLhLgh/TsxPLFSANcT67EC1t/qxjru5ZoADjKVEt2ejdx+xGvH49mcdfbc+l+L7gJtkH7GLKpE9pQNQWNHMAmj043PAXJZ++fiJObMRR2mye5VHEANzZWkZXMrXF7YjuG10S1pOU="
|
||||
},
|
||||
{
|
||||
"sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",
|
||||
"sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",
|
||||
"conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",
|
||||
"nonce": "a3e219242d85465e70adcd640b564b3feff57d2ef8745d5e7a0663b2dccceb54",
|
||||
"plaintext": "🙈 🙉 🙊 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗",
|
||||
"payload": "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtUf05aMF89q1FLwJvaFJYICZoMYgRJHFLwPiOHce7fuAc40kX0wXJvipyBJ9HzCOj7CgtnC1/cmPCHR3s5AIORmroBWglm1LiFMohv1FSPEbaBD51VXxJa4JyWpYhreSOEjn1wd0lMKC9b+osV2N2tpbs+rbpQem2tRen3sWflmCqjkG5VOVwRErCuXuPb5+hYwd8BoZbfCrsiAVLd7YT44dRtKNBx6rkabWfddKSLtreHLDysOhQUVOp/XkE7OzSkWl6sky0Hva6qJJ/V726hMlomvcLHjE41iKmW2CpcZfOedg=="
|
||||
}
|
||||
],
|
||||
"encrypt_decrypt_long_msg": [
|
||||
{
|
||||
"conversation_key": "8fc262099ce0d0bb9b89bac05bb9e04f9bc0090acc181fef6840ccee470371ed",
|
||||
"nonce": "326bcb2c943cd6bb717588c9e5a7e738edf6ed14ec5f5344caa6ef56f0b9cff7",
|
||||
"pattern": "x",
|
||||
"repeat": 65535,
|
||||
"plaintext_sha256": "09ab7495d3e61a76f0deb12cb0306f0696cbb17ffc12131368c7a939f12f56d3",
|
||||
"payload_sha256": "90714492225faba06310bff2f249ebdc2a5e609d65a629f1c87f2d4ffc55330a"
|
||||
},
|
||||
{
|
||||
"conversation_key": "56adbe3720339363ab9c3b8526ffce9fd77600927488bfc4b59f7a68ffe5eae0",
|
||||
"nonce": "ad68da81833c2a8ff609c3d2c0335fd44fe5954f85bb580c6a8d467aa9fc5dd0",
|
||||
"pattern": "!",
|
||||
"repeat": 65535,
|
||||
"plaintext_sha256": "6af297793b72ae092c422e552c3bb3cbc310da274bd1cf9e31023a7fe4a2d75e",
|
||||
"payload_sha256": "8013e45a109fad3362133132b460a2d5bce235fe71c8b8f4014793fb52a49844"
|
||||
},
|
||||
{
|
||||
"conversation_key": "7fc540779979e472bb8d12480b443d1e5eb1098eae546ef2390bee499bbf46be",
|
||||
"nonce": "34905e82105c20de9a2f6cd385a0d541e6bcc10601d12481ff3a7575dc622033",
|
||||
"pattern": "🦄",
|
||||
"repeat": 16383,
|
||||
"plaintext_sha256": "a249558d161b77297bc0cb311dde7d77190f6571b25c7e4429cd19044634a61f",
|
||||
"payload_sha256": "b3348422471da1f3c59d79acfe2fe103f3cd24488109e5b18734cdb5953afd15"
|
||||
}
|
||||
]
|
||||
},
|
||||
"invalid": {
|
||||
"encrypt_msg_lengths": [0, 65536, 100000, 10000000],
|
||||
"get_conversation_key": [
|
||||
{
|
||||
"sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"note": "sec1 higher than curve.n"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"note": "sec1 is 0"
|
||||
},
|
||||
{
|
||||
"sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139",
|
||||
"pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"note": "pub2 is invalid, no sqrt, all-ff"
|
||||
},
|
||||
{
|
||||
"sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"note": "sec1 == curve.n"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"note": "pub2 is invalid, no sqrt"
|
||||
},
|
||||
{
|
||||
"sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
|
||||
"pub2": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"note": "pub2 is point of order 3 on twist"
|
||||
},
|
||||
{
|
||||
"sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
|
||||
"pub2": "eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d",
|
||||
"note": "pub2 is point of order 13 on twist"
|
||||
},
|
||||
{
|
||||
"sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
|
||||
"pub2": "709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f",
|
||||
"note": "pub2 is point of order 3319 on twist"
|
||||
}
|
||||
],
|
||||
"decrypt": [
|
||||
{
|
||||
"conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642",
|
||||
"nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db",
|
||||
"plaintext": "n o b l e",
|
||||
"payload": "#Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJdU0MIDf06CUvEvdnr1cp1fiMtlM/GrE92xAc1K5odTpCzUB+mjXgbaqtntBUbTToSUoT0ovrlPwzGjyp",
|
||||
"note": "unknown encryption version"
|
||||
},
|
||||
{
|
||||
"conversation_key": "36f04e558af246352dcf73b692fbd3646a2207bd8abd4b1cd26b234db84d9481",
|
||||
"nonce": "ad408d4be8616dc84bb0bf046454a2a102edac937c35209c43cd7964c5feb781",
|
||||
"plaintext": "⚠️",
|
||||
"payload": "AK1AjUvoYW3IS7C/BGRUoqEC7ayTfDUgnEPNeWTF/reBZFaha6EAIRueE9D1B1RuoiuFScC0Q94yjIuxZD3JStQtE8JMNacWFs9rlYP+ZydtHhRucp+lxfdvFlaGV/sQlqZz",
|
||||
"note": "unknown encryption version 0"
|
||||
},
|
||||
{
|
||||
"conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642",
|
||||
"nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db",
|
||||
"plaintext": "n o s t r",
|
||||
"payload": "Atфupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJZE0UICD06CUvEvdnr1cp1fiMtlM/GrE92xAc1EwsVCQEgWEu2gsHUVf4JAa3TpgkmFc3TWsax0v6n/Wq",
|
||||
"note": "invalid base64"
|
||||
},
|
||||
{
|
||||
"conversation_key": "cff7bd6a3e29a450fd27f6c125d5edeb0987c475fd1e8d97591e0d4d8a89763c",
|
||||
"nonce": "09ff97750b084012e15ecb84614ce88180d7b8ec0d468508a86b6d70c0361a25",
|
||||
"plaintext": "¯\\_(ツ)_/¯",
|
||||
"payload": "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholyySBfeh+EN8wNB9gaLlg4j6wdBYh+3oK+mnxWu3NKRbSvQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"note": "invalid MAC"
|
||||
},
|
||||
{
|
||||
"conversation_key": "cfcc9cf682dfb00b11357f65bdc45e29156b69db424d20b3596919074f5bf957",
|
||||
"nonce": "65b14b0b949aaa7d52c417eb753b390e8ad6d84b23af4bec6d9bfa3e03a08af4",
|
||||
"plaintext": "🥎",
|
||||
"payload": "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0oRWQ8avl4OLqWZiTJ10vIgKrNqjoaX+fNhE9RqmR5g0f6BtUg1ijFMz71MO1D4lQLQfW7+UHva8PGYgQ1QpHlKgR",
|
||||
"note": "invalid MAC"
|
||||
},
|
||||
{
|
||||
"conversation_key": "5254827d29177622d40a7b67cad014fe7137700c3c523903ebbe3e1b74d40214",
|
||||
"nonce": "7ab65dbb8bbc2b8e35cafb5745314e1f050325a864d11d0475ef75b3660d91c1",
|
||||
"plaintext": "elliptic-curve cryptography",
|
||||
"payload": "Anq2XbuLvCuONcr7V0UxTh8FAyWoZNEdBHXvdbNmDZHB573MI7R7rrTYftpqmvUpahmBC2sngmI14/L0HjOZ7lWGJlzdh6luiOnGPc46cGxf08MRC4CIuxx3i2Lm0KqgJ7vA",
|
||||
"note": "invalid padding"
|
||||
},
|
||||
{
|
||||
"conversation_key": "fea39aca9aa8340c3a78ae1f0902aa7e726946e4efcd7783379df8096029c496",
|
||||
"nonce": "7d4283e3b54c885d6afee881f48e62f0a3f5d7a9e1cb71ccab594a7882c39330",
|
||||
"plaintext": "noble",
|
||||
"payload": "An1Cg+O1TIhdav7ogfSOYvCj9dep4ctxzKtZSniCw5MwRrrPJFyAQYZh5VpjC2QYzny5LIQ9v9lhqmZR4WBYRNJ0ognHVNMwiFV1SHpvUFT8HHZN/m/QarflbvDHAtO6pY16",
|
||||
"note": "invalid padding"
|
||||
},
|
||||
{
|
||||
"conversation_key": "0c4cffb7a6f7e706ec94b2e879f1fc54ff8de38d8db87e11787694d5392d5b3f",
|
||||
"nonce": "6f9fd72667c273acd23ca6653711a708434474dd9eb15c3edb01ce9a95743e9b",
|
||||
"plaintext": "censorship-resistant and global social network",
|
||||
"payload": "Am+f1yZnwnOs0jymZTcRpwhDRHTdnrFcPtsBzpqVdD6b2NZDaNm/TPkZGr75kbB6tCSoq7YRcbPiNfJXNch3Tf+o9+zZTMxwjgX/nm3yDKR2kHQMBhVleCB9uPuljl40AJ8kXRD0gjw+aYRJFUMK9gCETZAjjmrsCM+nGRZ1FfNsHr6Z",
|
||||
"note": "invalid padding"
|
||||
},
|
||||
{
|
||||
"conversation_key": "5cd2d13b9e355aeb2452afbd3786870dbeecb9d355b12cb0a3b6e9da5744cd35",
|
||||
"nonce": "b60036976a1ada277b948fd4caa065304b96964742b89d26f26a25263a5060bd",
|
||||
"plaintext": "0",
|
||||
"payload": "",
|
||||
"note": "invalid payload length: 0"
|
||||
},
|
||||
{
|
||||
"conversation_key": "d61d3f09c7dfe1c0be91af7109b60a7d9d498920c90cbba1e137320fdd938853",
|
||||
"nonce": "1a29d02c8b4527745a2ccb38bfa45655deb37bc338ab9289d756354cea1fd07c",
|
||||
"plaintext": "1",
|
||||
"payload": "Ag==",
|
||||
"note": "invalid payload length: 4"
|
||||
},
|
||||
{
|
||||
"conversation_key": "873bb0fc665eb950a8e7d5971965539f6ebd645c83c08cd6a85aafbad0f0bc47",
|
||||
"nonce": "c826d3c38e765ab8cc42060116cd1464b2a6ce01d33deba5dedfb48615306d4a",
|
||||
"plaintext": "2",
|
||||
"payload": "AqxgToSh3H7iLYRJjoWAM+vSv/Y1mgNlm6OWWjOYUClrFF8=",
|
||||
"note": "invalid payload length: 48"
|
||||
},
|
||||
{
|
||||
"conversation_key": "9f2fef8f5401ac33f74641b568a7a30bb19409c76ffdc5eae2db6b39d2617fbe",
|
||||
"nonce": "9ff6484642545221624eaac7b9ea27133a4cc2356682a6033aceeef043549861",
|
||||
"plaintext": "3",
|
||||
"payload": "Ap/2SEZCVFIhYk6qx7nqJxM6TMI1ZoKmAzrO7vBDVJhhuZXWiM20i/tIsbjT0KxkJs2MZjh1oXNYMO9ggfk7i47WQA==",
|
||||
"note": "invalid payload length: 92"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
16
docs/DEV_TIPS.md
Normal file
16
docs/DEV_TIPS.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Dev tips
|
||||
|
||||
A collection of tips when developing or testing Damus.
|
||||
|
||||
|
||||
## Logging
|
||||
|
||||
- Info and debug messages must be activated in the macOS Console to become visible, they are not visible by default. To activate, go to Console > Action > Include Info Messages.
|
||||
|
||||
|
||||
## Testing push notifications
|
||||
|
||||
- Dev builds (i.e. anything that isn't an official build from TestFlight or AppStore) only work with the development/sandbox APNS environment. If testing push notifications on a local damus build, ensure that:
|
||||
- Damus is configured to use the "staging" push notifications environment, under Settings > Developer settings.
|
||||
- Ensure that Nostr events are sent to `wss://notify-staging.damus.io`.
|
||||
|
||||
Reference in New Issue
Block a user