Compare commits

..

45 Commits

Author SHA1 Message Date
d26804d665 Add Unicode 16 emoji reactions for iOS 18.4+ by upgrading EmojiPicker
Changelog-Added: Added Unicode 16 emoji reactions for iOS 18.4+ by upgrading EmojiPicker
Closes: https://github.com/damus-io/damus/issues/2915
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-03-13 18:38:30 +08:00
William Casarin
0c148c8a1f Merge Communication Notifications 2025-03-03 14:22:11 -08:00
William Casarin
3cccb2eb6b project: fix version yet again
we should always be using the inherited project version so that
everything is consistent

Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-03 14:21:53 -08:00
William Casarin
af4949e26a Communication notifications
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-03 14:21:53 -08:00
Claude
5bb7e95624 chore: update Package.swift to Swift tools version 6.0 2025-03-03 12:52:56 -08:00
814bcf694f Change spaces to newlines in new posts to provide cleaner separation between text, uploaded media, and quoted notes
Changelog-Changed: Changed spaces to newlines in new posts to provide cleaner separation between text, uploaded media, and quoted notes
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-03-03 11:51:08 -08:00
Daniel D’Aquino
b0382c61b1 Merge pull request #2889 from damus-io/translations
Translations
2025-03-03 11:33:28 -08:00
e2650a8bfc Fix bug where profile view was showing more than just the notes and replies on the notes / notes & replies tabs
Changelog-Fixed: Fix bug where profile view was showing more than just the notes and replies on the notes / notes & replies tabs
Fixes: caa4bfe864 ("Fix bug where profile view was showing more than just the notes and replies on the notes / notes & replies tabs")
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-03-03 11:20:34 -08:00
transifex-integration[bot]
ac39a53b33 Translate Localizable.stringsdict in pt_PT
100% translated source file: 'Localizable.stringsdict'
on 'pt_PT'.
2025-03-02 15:18:22 +00:00
transifex-integration[bot]
fb356cdf0b Translate InfoPlist.strings in pt_PT
100% translated source file: 'InfoPlist.strings'
on 'pt_PT'.
2025-03-02 15:16:26 +00:00
transifex-integration[bot]
238e89ce16 Translate InfoPlist.strings in pt_PT
100% translated source file: 'InfoPlist.strings'
on 'pt_PT'.
2025-03-02 15:16:15 +00:00
transifex-integration[bot]
6e041c79f7 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2025-03-02 15:14:24 +00:00
William Casarin
6ef4b60d14 test: disable broken tests
Signed-off-by: William Casarin <jb55@jb55.com>
2025-03-01 17:07:19 -08:00
transifex-integration[bot]
054bec2d9a Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2025-03-01 14:15:26 -05:00
transifex-integration[bot]
943a46a343 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-03-01 14:15:26 -05:00
transifex-integration[bot]
17381f6b94 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-03-01 14:15:26 -05:00
transifex-integration[bot]
18c88de407 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-03-01 14:15:25 -05:00
transifex-integration[bot]
99d21fc89b Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-03-01 14:15:25 -05:00
transifex-integration[bot]
db5c86a0d1 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-03-01 14:15:25 -05:00
transifex-integration[bot]
736ec6fb9e Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-03-01 14:15:25 -05:00
transifex-integration[bot]
fa2327325a Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-03-01 14:15:25 -05:00
transifex-integration[bot]
4fdf048040 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-03-01 14:15:25 -05:00
transifex-integration[bot]
273538bd36 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-03-01 14:15:25 -05:00
transifex-integration[bot]
0980c8c040 Translate Localizable.stringsdict in nl
100% translated source file: 'Localizable.stringsdict'
on 'nl'.
2025-03-01 14:15:25 -05:00
transifex-integration[bot]
f0bfdeaa5a Translate Localizable.stringsdict in th
100% translated source file: 'Localizable.stringsdict'
on 'th'.
2025-03-01 14:15:25 -05:00
transifex-integration[bot]
ab7c5c18e3 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-01 14:15:24 -05:00
transifex-integration[bot]
6ae95ab5ec Translate Localizable.stringsdict in ja
100% translated source file: 'Localizable.stringsdict'
on 'ja'.
2025-03-01 14:15:24 -05:00
eec630b2b0 Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-03-01 14:15:23 -05:00
William Casarin
2b3d86968d add todo for fixing q tags
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-28 09:44:29 -08:00
William Casarin
935a6cae7a Merge conversation tab and other updates from Terry
I've tested these and they seem to be working!

Terry Yiu (3):
      Fix reposts banner to be localizable
      Add Conversations tab to profiles
      Remove mystery tabs meant to fix tab switching bug that no longer exists
2025-02-25 12:48:34 -08:00
William Casarin
d4940d8386 prs: ensure PR always have a linked issue
This makes project management a bit nicer in linear

Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-25 10:28:37 -08:00
71ec18f6c6 Remove mystery tabs meant to fix tab switching bug that no longer exists
Changelog-Removed: Removed mystery tabs meant to fix tab switching bug that no longer exists
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-02-25 09:21:36 -05:00
caa4bfe864 Add Conversations tab to profiles
Changelog-Added: Added Conversations tab to profiles
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-02-24 21:34:16 -05:00
a87ba73160 Fix reposts banner to be localizable
Changelog-Fixed: Fixed reposts banner to be localizable
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-02-24 14:52:53 -05:00
Daniel D’Aquino
4324b185fe Improve open action handling for notifications
Push notifications were not opened reliably. To improve robustness, the
following changes were introduced:
1. The notification opening logic was updated to become more similar to
   URL handling, in a way that uses better defined interfaces and
   functions that provide better result guarantees, by separating
   complex handling logic, and the side-effects/mutations that
   are made after computing the open action — instead of relying on a
   complex logic function that produces side-effects as a result, which
   obfuscates the actual behavior of the function.
2. The LoadableThreadView was expanded and renamed to
   LoadableNostrEventView, to reflect that it can also handle non-thread
   nostr events, such as DMs, which is a necessity for handling push
   notifications.
3. A new type of Notify object, the `QueueableNotify` was introduced, to
   address issues where the listener/handler is not instantiated at the
   time the app notifies that there is a push notification to be opened.
   This was implemented using async streams, which simplifies the usage
   of this down to a simple "for-in" loop.

Closes: https://github.com/damus-io/damus/issues/2825
Changelog-Fixed: Fixed issue where some push notifications would not open in the app and leave users confused
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-21 11:28:26 -08:00
Daniel D’Aquino
1ab9b30b85 Add development and testing tips
These were included to help other developers with testing or
development.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-21 11:28:26 -08:00
William Casarin
81cf6ad297 Merge remote-tracking branches 'pr/2863', 'pr/2864', 'pr/2865' and 'pr/2866'
Daniel D’Aquino (4):
      Reduce swipe sensitivity on thread chat view
      Fix issue where a NWC connection would not work unless restarting the app
      Implement developer feature to avoid distractions
      Fix issue where note persisted after note publication
2025-02-21 11:10:55 -08:00
William Casarin
1b3be3a13b Revert "Update EventMenu.swift"
should have tested this first lol

This reverts commit 3a2ce04d6b.
2025-02-21 11:07:47 -08:00
alltheseas
3a2ce04d6b Update EventMenu.swift
replaced deprecated noteID with neventID in EventMenu.swift. NoteID currently appears in bubble/context menu of each note (top right three dots ellipsis).
2025-02-21 08:45:55 -06:00
Daniel D’Aquino
981821a6bc Fix issue where note persisted after note publication
There is no changelog entry needed because drafts are still an
unreleased feature.

Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Closes: https://github.com/damus-io/damus/issues/2836
2025-02-19 20:21:49 -08:00
Daniel D’Aquino
98f83769bd Fix issue where a NWC connection would not work unless restarting the app
Changelog-Fixed: Fixed issue where app would need a restart for new NWC wallets to work
Closes: https://github.com/damus-io/damus/issues/2859
Closes: https://github.com/damus-io/damus/issues/1135
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-19 17:58:14 -08:00
Daniel D’Aquino
7684f53281 Implement developer feature to avoid distractions
This commit implements an optional developer feature to scramble text
and blur images to prevent distractions during development and testing.

It is not perfect (It breaks some mentions and rich text objects, and
does not scramble non-alphanumeric languages such as Japanese), but
good enough to avoid distractions while working on most features.

No changelog entry is needed because this is not meant for the final
user.

Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-19 17:39:21 -08:00
Daniel D’Aquino
15af686a58 Fix issue where a NWC connection would not work unless restarting the app
Changelog-Fixed: Fixed issue where app would need a restart for new NWC wallets to work
Closes: https://github.com/damus-io/damus/issues/2859
Closes: https://github.com/damus-io/damus/issues/1135
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-19 17:22:32 -08:00
Daniel D’Aquino
aad8f9e8d4 Reduce swipe sensitivity on thread chat view
Value determined experimentally.

Closes: https://github.com/damus-io/damus/issues/2743
Changelog-Fixed: Fixed overly sensitive horizontal swipe on thread chat view
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-17 17:22:02 -08:00
b2ee44c0ab Trim whitespaces from Lightning addresses
Changelog-Fixed: Trim whitespaces from Lightning addresses
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-02-17 16:43:32 -08:00
55 changed files with 1363 additions and 509 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -5,15 +5,32 @@
// Created by Daniel DAquino on 2023-11-10.
//
import Kingfisher
import ImageIO
import UserNotifications
import Foundation
import UniformTypeIdentifiers
import Intents
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
private func configureKingfisherCache() {
guard let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER) else {
return
}
let cachePath = groupURL.appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME)
if let cache = try? ImageCache(name: "sharedCache", cacheDirectoryURL: cachePath) {
KingfisherManager.shared.cache = cache
}
}
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
configureKingfisherCache()
self.contentHandler = contentHandler
guard let nostr_event_json = request.content.userInfo["nostr_event"] as? String,
@@ -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()
}

View File

@@ -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"),
]
)

1
TODO
View File

@@ -0,0 +1 @@
Fix q tags

View File

@@ -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 */; };
@@ -1048,6 +1050,9 @@
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 */; };
@@ -1451,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 */; };
@@ -1617,6 +1622,9 @@
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 */; };
@@ -1799,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>"; };
@@ -2417,6 +2426,7 @@
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>"; };
@@ -2450,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>"; };
@@ -2495,6 +2505,7 @@
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>"; };
@@ -2600,6 +2611,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4C5726BA2D72C6FA00E7FF82 /* Kingfisher in Frameworks */,
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */,
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
@@ -3173,7 +3185,7 @@
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */,
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */,
D74EA0922D2E77B9002290DD /* LoadableThreadView.swift */,
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -3235,6 +3247,7 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */,
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */,
E04A37C52B544F090029650D /* URIParsing.swift */,
4C1D4FB02A7958E60024F453 /* VersionInfo.swift */,
@@ -3350,6 +3363,7 @@
4CA3529C2A76AE47003BB08B /* Notify */ = {
isa = PBXGroup;
children = (
D706C5B62D602A050027C627 /* QueueableNotify.swift */,
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */,
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */,
4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */,
@@ -3679,6 +3693,7 @@
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */,
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -4161,6 +4176,7 @@
D789D11F2AFEFBF20083A7AB /* secp256k1 */,
D7EDED302B1290B80018B19C /* MarkdownUI */,
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */,
4C5726B92D72C6FA00E7FF82 /* Kingfisher */,
);
productName = DamusNotificationService;
productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */;
@@ -4543,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 */,
@@ -4618,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 */,
@@ -4712,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 */,
@@ -4870,6 +4888,7 @@
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 */,
@@ -4983,7 +5002,7 @@
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 */,
@@ -5012,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 */,
@@ -5343,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 */,
@@ -5430,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 */,
@@ -5518,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 */,
@@ -5545,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 */,
@@ -6205,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;
@@ -6228,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;
@@ -6274,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;
@@ -6293,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;
@@ -6312,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;
@@ -6340,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;
@@ -6364,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;
@@ -6392,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;
@@ -6480,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;
@@ -6499,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;
@@ -6518,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;
@@ -6533,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;
@@ -6552,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;
@@ -6567,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;
@@ -6587,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;
@@ -6602,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;
@@ -6621,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;
@@ -6636,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;
@@ -6656,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;
@@ -6671,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;
@@ -6757,7 +6768,7 @@
repositoryURL = "https://github.com/tyiu/EmojiPicker.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.1.1;
minimumVersion = 0.2.0;
};
};
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
@@ -6850,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" */;

View File

@@ -1,5 +1,5 @@
{
"originHash" : "085cf0f645323bf77edb52886489bf77b309a0a2d2b78a54beaf8520b540d596",
"originHash" : "06318d35ee2e6bd681b95591e67da33a9461b48a3c652e58bd9d1a6f0d82bdac",
"pins" : [
{
"identity" : "codescanner",
@@ -22,8 +22,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiKit",
"state" : {
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
"version" : "0.1.2"
"revision" : "47a4b1402de26be0299dcb4d667c1faaf21a7874",
"version" : "0.2.0"
}
},
{
@@ -31,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"
}
},
{

View File

@@ -10,36 +10,42 @@ import SwiftUI
struct Reposted: View {
let damus: DamusState
let pubkey: Pubkey
let target: NoteId
let target: NostrEvent
@State var reposts: Int
init(damus: DamusState, pubkey: Pubkey, target: NoteId) {
init(damus: DamusState, pubkey: Pubkey, target: NostrEvent) {
self.damus = damus
self.pubkey = pubkey
self.target = target
self.reposts = damus.boosts.counts[target] ?? 1
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)
NavigationLink(value: Route.Reposts(reposts: .reposts(state: damus, target: target))) {
let other_reposts = reposts - 1
if other_reposts > 0 {
Text(" and \(other_reposts) others reposted", comment: "Text indicating that the note was reposted (i.e. re-shared) by multiple people")
.foregroundColor(Color.gray)
} else {
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 else { return }
let repost_count = damus.boosts.counts[target]
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
}
@@ -47,9 +53,25 @@ struct Reposted: View {
}
}
func people_reposted_text(profiles: Profiles, pubkey: Pubkey, reposts: Int, locale: Locale = Locale.current) -> String {
guard reposts > 0 else {
return ""
}
let bundle = bundleForLocale(locale: locale)
let other_reposts = reposts - 1
let display_name = event_author_name(profiles: profiles, pubkey: pubkey)
if other_reposts == 0 {
return String(format: NSLocalizedString("%@ reposted", bundle: bundle, comment: "Text indicating that the note was reposted (i.e. re-shared)."), locale: locale, display_name)
} else {
return String(format: localizedStringFormat(key: "people_reposted_count", locale: locale), locale: locale, other_reposts, display_name)
}
}
struct Reposted_Previews: PreviewProvider {
static var previews: some View {
let test_state = test_damus_state
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note.id)
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note)
}
}

View File

@@ -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?)
{

View File

@@ -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>

View File

@@ -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
}
}
}

View File

@@ -115,7 +115,7 @@ class DraftArtifacts: Equatable {
if case .pubkey(let pubkey) = mention.ref {
// A profile reference, format things properly.
let profile = damus_state.ndb.lookup_profile(pubkey)?.unsafeUnownedValue?.profile
let profile_name = 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

View File

@@ -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)

View File

@@ -34,7 +34,7 @@ struct DamusURLHandler {
let thread = await ThreadModel(event: nostrEvent, damus_state: damus_state)
return .route(.Thread(thread: thread))
case .event_reference(let event_reference):
return .route(.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)

View File

@@ -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

View File

@@ -58,7 +58,7 @@ extension NdbProfile {
}
static func displayName(profile: Profile?, pubkey: Pubkey) -> DisplayName {
return parse_display_name(profile: profile, pubkey: pubkey)
return DisplayName(name: profile?.name, display_name: profile?.display_name, pubkey: pubkey)
}
var damus_donation: Int? {

View File

@@ -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)
}

View File

@@ -0,0 +1,90 @@
//
// QueueableNotify.swift
// damus
//
// Created by Daniel DAquino on 2025-02-14.
//
/// This notifies another object about some payload,
/// with automatic "queueing" of messages if there are no listeners.
///
/// When used as a singleton, this can be used to easily send notifications to be handled at the app-level.
///
/// This serves the same purpose as `Notify`, except this implements the queueing of messages,
/// which means that messages can be handled even if the listener is not instantiated yet.
///
/// **Example:** The app delegate can send some events that need handling from `ContentView` but some can occur before `ContentView` is even instantiated.
///
///
/// ## Usage notes
///
/// - This code was mainly written to have one listener at a time. Have more than one listener may be possible, but this class has not been tested/optimized for that purpose.
///
///
/// ## Implementation notes
///
/// - This makes heavy use of `AsyncStream` and continuations, because that allows complexities here to be handled elegantly with a simple "for-in" loop
/// - Without this, it would take a couple of callbacks and manual handling of queued items to achieve the same effect
/// - Modeled as an `actor` for extra thread-safety
actor QueueableNotify<T: Sendable> {
/// The continuation, which allows us to publish new items to the listener
/// If `nil`, that means there is no listeners to the stream, which is used for determining whether to queue new incoming items.
private var continuation: AsyncStream<T>.Continuation?
/// Holds queue items
private var queue: [T] = []
/// The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
var maxQueueItems: Int
/// Initializes the object
/// - Parameter maxQueueItems: The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
init(maxQueueItems: Int) {
self.maxQueueItems = maxQueueItems
}
/// The async stream, used for listening for notifications
///
/// This will first stream the queued "inbox" items that the listener may have missed, and then it will do a real-time stream of new items as they come in.
///
/// Example:
///
/// ```swift
/// for await notification in queueableNotify.stream {
/// // Do something with the notification
/// }
/// ```
var stream: AsyncStream<T> {
return AsyncStream { continuation in
// Stream queued "inbox" items that the listener may have missed
for item in queue {
continuation.yield(item)
}
// Clean up if the stream closes
continuation.onTermination = { continuation in
Task { await self.cleanup() }
}
// Point to this stream, so that it can receive new updates
self.continuation = continuation
}
}
/// Cleans up after a stream is closed by the listener
private func cleanup() {
self.continuation = nil // This will cause new items to be queued for when another listener is attached
}
/// Adds a new notification item to be handled by a listener.
///
/// This will automatically stream the new item to the listener, or queue the item if no one is listening
func add(item: T) {
while queue.count >= maxQueueItems { queue.removeFirst() } // Ensures queue stays within the desired size
guard let continuation else {
// No one is listening, queue it (send it to an inbox for later handling)
queue.append(item)
return
}
// Send directly to the active listener stream
continuation.yield(item)
}
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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")

View File

@@ -0,0 +1,30 @@
//
// Undistractor.swift
// damus
//
// Created by Daniel DAquino on 2025-02-19.
//
/// Keeping the minds of developers safe from the occupational hazard of social media distractions when testing Damus since 2025
struct Undistractor {
static func makeGibberish(text: String) -> String {
let lowercaseLetters = "abcdefghijklmnopqrstuvwxyz"
let uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
var transformedText = ""
for char in text {
if lowercaseLetters.contains(char) {
if let randomLetter = lowercaseLetters.randomElement() {
transformedText.append(randomLetter)
}
} else if uppercaseLetters.contains(char) {
if let randomLetter = uppercaseLetters.randomElement() {
transformedText.append(randomLetter)
}
} else {
transformedText.append(char)
}
}
return transformedText
}
}

View File

@@ -298,7 +298,7 @@ struct ChatEventView: View {
}
.swipeSpacing(-20)
.swipeActionsStyle(.mask)
.swipeMinimumDistance(20)
.swipeMinimumDistance(40)
.swipeDragGesturePriority(.normal)
}
}

View File

@@ -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
}

View 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("Cant display note", comment: "User-visible heading for an error message indicating a note has an unknown kind or is unsupported for viewing."),
description: NSLocalizedString("We do not yet support viewing this type of content.", comment: "User-visible description of an error indicating a note has an unknown kind or is unsupported for viewing."),
advice: NSLocalizedString("Please try opening this content on another Nostr app that supports this type of content.", comment: "User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing.")
)
}
// MARK: Skeleton views
// Implementation notes
// - No localization is needed because the text will be redacted
// - No accessibility label is needed because these will be summarized into a single accessibility label at the top-level view. See `body` in this struct
var skeleton: some View {
VStack(alignment: .leading, spacing: 40) {
Self.skeleton_selected_event
Self.skeleton_chat_event(message: "Nice! Have you tried Damus?", right: false)
Self.skeleton_chat_event(message: "Yes, it's awesome.", right: true)
Spacer()
}
.padding()
}
static func skeleton_chat_event(message: String, right: Bool) -> some View {
HStack(alignment: .center) {
if !right {
self.skeleton_chat_user_avatar
}
else {
Spacer()
}
ChatBubble(
direction: right ? .right : .left,
stroke_content: Color.accentColor.opacity(0),
stroke_style: .init(lineWidth: 4),
background_style: Color.secondary.opacity(0.5),
content: {
Text(verbatim: message)
.padding()
}
)
if right {
self.skeleton_chat_user_avatar
}
else {
Spacer()
}
}
}
static var skeleton_selected_event: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Circle()
.frame(width: 50, height: 50)
.foregroundStyle(.secondary.opacity(0.5))
Text(verbatim: "Satoshi Nakamoto")
.bold()
}
Text(verbatim: "Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive.")
HStack {
self.skeleton_action_item
Spacer()
self.skeleton_action_item
Spacer()
self.skeleton_action_item
Spacer()
self.skeleton_action_item
}
}
}
static var skeleton_chat_user_avatar: some View {
Circle()
.fill(.secondary.opacity(0.5))
.frame(width: 35, height: 35)
.padding(.bottom, -21)
}
static var skeleton_action_item: some View {
Circle()
.fill(Color.secondary.opacity(0.5))
.frame(width: 25, height: 25)
}
}
extension LoadableNostrEventView {
struct SomethingWrong: View {
let imageSystemName: String
let heading: String
let description: String
let advice: String
var body: some View {
VStack(spacing: 6) {
Image(systemName: imageSystemName)
.resizable()
.frame(width: 30, height: 30)
.accessibilityHidden(true)
Text(heading)
.font(.title)
.bold()
.padding(.bottom, 10)
Text(description)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 5) {
Image(systemName: "sparkles")
.accessibilityHidden(true)
Text("Advice", comment: "Heading for some advice text to help the user with an error")
.font(.headline)
}
Text(advice)
}
.padding()
.background(Color.secondary.opacity(0.2))
.cornerRadius(10)
.padding(.vertical, 30)
}
.padding()
}
}
}
#Preview("Loadable") {
LoadableNostrEventView(state: test_damus_state, note_reference: .note_id(test_thread_note_1.id))
}

View File

@@ -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 its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive.")
HStack {
self.skeleton_action_item
Spacer()
self.skeleton_action_item
Spacer()
self.skeleton_action_item
Spacer()
self.skeleton_action_item
}
}
}
var skeleton_chat_user_avatar: some View {
Circle()
.fill(.secondary.opacity(0.5))
.frame(width: 35, height: 35)
.padding(.bottom, -21)
}
var skeleton_action_item: some View {
Circle()
.fill(Color.secondary.opacity(0.5))
.frame(width: 25, height: 25)
}
}
#Preview("Loadable") {
LoadableThreadView(state: test_damus_state, note_reference: .note_id(test_thread_note_1.id))
}

View File

@@ -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)))
}

View File

@@ -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,

View File

@@ -230,6 +230,7 @@ struct PostView: View {
damus_state.drafts.post = nil
}
damus_state.drafts.save(damus_state: damus_state)
}
func load_draft() -> Bool {
@@ -864,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
@@ -912,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 {

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -16,7 +16,7 @@ struct RepostedEvent: View {
var body: some View {
VStack(alignment: .leading) {
NavigationLink(value: Route.ProfileByKey(pubkey: event.pubkey)) {
Reposted(damus: damus, pubkey: event.pubkey, target: inner_ev.id)
Reposted(damus: damus, pubkey: event.pubkey, target: inner_ev)
.padding(.horizontal)
}
.buttonStyle(PlainButtonStyle())

View File

@@ -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(

View File

@@ -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)

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
@@ -326,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>
@@ -471,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="Cant display note" xml:space="preserve">
<source>Cant display note</source>
<target>Cant 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>
@@ -582,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>
@@ -1571,11 +1581,6 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
<target>Nostr Address</target>
<note>Label for the Nostr Address section of user profile form.</note>
</trans-unit>
<trans-unit id="Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive." xml:space="preserve">
<source>Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive.</source>
<target>Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="NostrScript" xml:space="preserve">
<source>NostrScript</source>
<target>NostrScript</target>
@@ -1604,7 +1609,7 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
<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>
<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>
@@ -1771,6 +1776,11 @@ 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 → &quot;Purple&quot; 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>
@@ -1781,6 +1791,11 @@ Section title for deleting the user</note>
<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>
@@ -2048,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>
@@ -2139,7 +2149,7 @@ Button to save key, complete account creation, and start using the app.</note>
<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>
<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>
@@ -2626,9 +2636,9 @@ 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, whether you need to connect to a specific relay to access this content." xml:space="preserve">
<source>Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.</source>
<target>Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.</target>
<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">
@@ -2653,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>
@@ -2799,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>
@@ -2827,6 +2837,11 @@ 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>
@@ -2898,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>
@@ -2928,6 +2938,11 @@ User confirm Yes</note>
<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 &quot;nrelay&quot;, 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>
@@ -2953,10 +2968,10 @@ 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 only for visually impaired users</note>
<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>
@@ -3305,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>
@@ -3701,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>
@@ -3933,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>
@@ -4078,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="Cant display note" xml:space="preserve">
<source>Cant display note</source>
<target state="new">Cant 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>
@@ -4192,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>
@@ -5176,11 +5216,6 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
<target state="new">Nostr Address</target>
<note>Label for the Nostr Address section of user profile form.</note>
</trans-unit>
<trans-unit id="Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive." xml:space="preserve">
<source>Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive.</source>
<target state="new">Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive.</target>
<note/>
</trans-unit>
<trans-unit id="NostrScript" xml:space="preserve">
<source>NostrScript</source>
<target state="new">NostrScript</target>
@@ -5209,7 +5244,7 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
<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>
<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>
@@ -5376,6 +5411,11 @@ 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 → &quot;Purple&quot; 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>
@@ -5386,6 +5426,11 @@ Section title for deleting the user</note>
<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>
@@ -5643,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>
@@ -5734,7 +5774,7 @@ Button to save key, complete account creation, and start using the app.</note>
<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>
<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>
@@ -6231,9 +6271,9 @@ 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, whether you need to connect to a specific relay to access this content." xml:space="preserve">
<source>Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.</source>
<target state="new">Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.</target>
<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">
@@ -6258,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>
@@ -6404,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>
@@ -6432,6 +6472,11 @@ 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>
@@ -6503,11 +6548,6 @@ 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>
@@ -6528,6 +6568,11 @@ User confirm Yes</note>
<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 &quot;nrelay&quot;, 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>
@@ -6558,10 +6603,10 @@ 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 only for visually impaired users</note>
<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>

View File

@@ -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."
},
@@ -207,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."
},
@@ -273,6 +273,9 @@
"Camera's permission was denied. You can change this in iOS settings." : {
"comment" : "Camera's permission denied error label"
},
"Cant display note" : {
"comment" : "User-visible heading for an error message indicating a note has an unknown kind or is unsupported for viewing."
},
"Cancel" : {
"comment" : "Alert button to cancel out of alert for muting a user.\nButton to cancel a repost.\nButton to cancel any interaction with the QRCode link.\nButton to cancel out of alert that creates a new mutelist.\nButton to cancel out of posting a note.\nButton to cancel out of search text entry mode.\nButton to cancel the upload.\nCancel button text for dismissing profile status settings view.\nCancel button text for dismissing updating image url.\nCancel deleting bookmarks.\nCancel deleting the user.\nCancel out of logging out the user.\nCancel out of search view.\nCancel resetting the contact list.\nText for button to cancel out of connecting Nostr Wallet Connect lightning wallet."
},
@@ -345,6 +348,9 @@
"Continue" : {
"comment" : "Button to dismiss suggested users view and continue to the main app\nContinue with bookmarks.\nContinue with deleting the user.\nContinue with resetting the contact list.\nPrompt to user to continue"
},
"Conversations" : {
"comment" : "Label for filter for seeing notes and replies that involve conversations between the signed in user and the current profile."
},
"Copied" : {
"comment" : "Label indicating that a user's key was copied."
},
@@ -939,9 +945,6 @@
},
"Nostr Address" : {
"comment" : "Label for the Nostr Address section of user profile form."
},
"Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive." : {
},
"NostrScript" : {
"comment" : "Navigation title for the view showing NostrScript."
@@ -959,7 +962,7 @@
"comment" : "Text to indicate that what is being shown is a note which has been muted."
},
"Note not found" : {
"comment" : "Heading for the thread view in a not found error state"
"comment" : "Heading for the thread view in a not found error state."
},
"Note you've muted" : {
"comment" : "Label indicating note has been muted\nText to indicate that what is being shown is a note which has been muted."
@@ -1069,12 +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"
},
@@ -1242,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"
},
@@ -1294,7 +1300,7 @@
"comment" : "Ask user if they want to save their account information."
},
"Saved" : {
"comment" : "Small label indicating that the user's draft has been saved to storage"
"comment" : "Small label indicating that the user's draft has been saved to storage."
},
"Scan a user's pubkey" : {
"comment" : "Text to prompt scanning a QR code of a user's pubkey to open their profile."
@@ -1587,7 +1593,7 @@
"Truncate timeline text" : {
"comment" : "Setting to truncate text in timeline"
},
"Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content." : {
"Try checking the link again, your internet connection, or contact the person who provided you the link for help." : {
"comment" : "Tips on what to do if a note cannot be found."
},
"Type %@ to delete" : {
@@ -1599,6 +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."
},
@@ -1674,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."
},
@@ -1695,6 +1701,9 @@
"We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)" : {
"comment" : "Message indicating that no First Aid actions are available."
},
"We do not yet support viewing this type of content." : {
"comment" : "User-visible description of an error indicating a note has an unknown kind or is unsupported for viewing."
},
"We were unable to find the note you were looking for." : {
"comment" : "Text for the thread view when it is unable to find the note the user is looking for"
},
@@ -1743,9 +1752,6 @@
"you" : {
"comment" : "You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself."
},
"You are dreaming..." : {
"comment" : "Text telling the user that they are dreaming."
},
"You cannot share content because you are not logged in. Please close this view, log in to your account, and try again." : {
"comment" : "Label explaining that sharing cannot proceed because the user is not logged in."
},
@@ -1758,14 +1764,17 @@
"You have no bookmarks yet, add them in the context menu" : {
"comment" : "Text indicating that there are no bookmarks to be viewed"
},
"You opened an invalid link. The link you tried to open refers to \"nrelay\", which has been deprecated and is not supported." : {
"comment" : "User-visible error description for a user who tries to open a deprecated \"nrelay\" link."
},
"You unlocked" : {
"comment" : "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple"
},
"Your content is being broadcasted to the network. Please wait." : {
"comment" : "Label explaining that their content sharing action is in progress"
},
"Your draft has been saved to storage" : {
"comment" : "Accessibility label indicating that a user's post draft has been saved, meant only for visually impaired users"
"Your draft has been saved to storage." : {
"comment" : "Accessibility label indicating that a user's post draft has been saved, meant to be read by screen reading technology."
},
"Your Name" : {
"comment" : "Label for Your Name section of user profile form."

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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>

View File

@@ -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

View 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))
}
}
}
}

16
docs/DEV_TIPS.md Normal file
View 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`.