Compare commits

..

245 Commits

Author SHA1 Message Date
0b85b53eda Clean up code in ProfileName.name_choice
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-11 15:05:09 -04:00
b35cc33c32 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-29 11:40:47 -03:00
SanjaySiddharth
9510290c29 Fix bug that closes the side menu when copying npub
Changelog-Fixed: Fixed issue where the side menu would close when copying the npub
Closes: https://github.com/damus-io/damus/issues/2748
Signed-off-by: SanjaySiddharth <mjsanjaysiddharth1999@gmail.com>
2025-03-28 22:00:00 -03:00
Daniel D’Aquino
3b1238b9c7 Merge pull request #2936 from damus-io/translations
Translations
2025-03-28 21:53:33 -03:00
SanjaySiddharth
3bec23ecac Add search feature to the settings screen
Closes: https://github.com/damus-io/damus/issues/2838
Changelog-Added: Added a search interface to the settings screen
Signed-off-by: SanjaySiddharth <mjsanjaysiddharth1999@gmail.com>
2025-03-28 18:48:08 -03:00
transifex-integration[bot]
7b678228b6 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:29:51 +00:00
transifex-integration[bot]
b1292d4562 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:29:41 +00:00
transifex-integration[bot]
a62d782fe5 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:29:30 +00:00
transifex-integration[bot]
81b07eb339 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:29:17 +00:00
transifex-integration[bot]
02f88398b9 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:28:56 +00:00
transifex-integration[bot]
e80961cc09 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:28:48 +00:00
transifex-integration[bot]
bd7721dc26 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:28:36 +00:00
transifex-integration[bot]
6d974bf71c Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:28:25 +00:00
transifex-integration[bot]
aeeb817735 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:27:59 +00:00
transifex-integration[bot]
b5e7033958 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:27:39 +00:00
transifex-integration[bot]
bdc843f30f Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:27:31 +00:00
transifex-integration[bot]
a823fa8e14 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:27:16 +00:00
transifex-integration[bot]
9232386c15 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:27:09 +00:00
transifex-integration[bot]
3c1547718c Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:26:58 +00:00
transifex-integration[bot]
b67a7f3e9e Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:23:06 +00:00
transifex-integration[bot]
92850d4f64 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-25 06:22:59 +00:00
Daniel D’Aquino
6323eafd7e v1.13.1 changelog
Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-03-24 14:02:08 -03:00
Daniel D’Aquino
b8fe826b58 Improve syncing and performance of ThreadModel
This commit introduces two minor improvements:
1. It ensures better consistency between ThreadModel and EventCache
2. It avoids unnecessary recursion calls on `add_event`

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-03-24 13:52:35 -03:00
Daniel D’Aquino
841c49238f Fix thread parent event loading regression
This fixes a regression where ThreadModel would no longer look at
NostrDB saved notes for parent events, causing some instability on
thread loading — especially in poor networking conditions.

This was fixed by adding a call that searches for parent events in
EventCache/NostrDB each time an event is added to the ThreadModel.

That `add_event` function is the ideal spot to place the call because it
is the only interface used for all information updates incoming to the
ThreadModel, including:
- anytime an event is loaded from the network into the thread model.
- when the ThreadModel is first initialized, with an initial event.

Fixes: 74d5bee1f6
Changelog-Fixed: Fixed an issue where threads would not load properly
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-03-24 13:52:35 -03:00
transifex-integration[bot]
7ab612e3d9 Translate Localizable.strings in hu_HU
100% translated source file: 'Localizable.strings'
on 'hu_HU'.
2025-03-23 18:13:22 +00:00
transifex-integration[bot]
6d8a27688f Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2025-03-23 16:34:49 +00:00
transifex-integration[bot]
765385319a Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2025-03-23 16:34:31 +00:00
transifex-integration[bot]
342c49a3e5 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-03-23 10:48:13 +00:00
transifex-integration[bot]
25860e7bb2 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-03-23 05:06:23 +00:00
transifex-integration[bot]
6962f2b462 Translate InfoPlist.strings in fr
100% translated source file: 'InfoPlist.strings'
on 'fr'.
2025-03-22 21:44:17 +00:00
e48ce4c6c5 Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-03-22 15:53:21 -04:00
1cb311cc2c Fix localization issues in TransactionsView
Fixes: 22f2aba969 ("Fix localization issues in TransactionsView")
Changelog-Fixed: Fix localization issues in TransactionsView
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-03-22 15:51:31 -04:00
transifex-integration[bot]
401846abe4 Translate Localizable.stringsdict in hu_HU
100% translated source file: 'Localizable.stringsdict'
on 'hu_HU'.
2025-03-22 14:50:14 -04:00
transifex-integration[bot]
16ef393350 Translate Localizable.strings in hu_HU
100% translated source file: 'Localizable.strings'
on 'hu_HU'.
2025-03-22 14:50:14 -04:00
transifex-integration[bot]
d5742f8e4c Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-03-22 14:50:14 -04:00
transifex-integration[bot]
319063f823 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-03-22 14:50:14 -04:00
transifex-integration[bot]
5b13cf5634 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-03-22 14:50:14 -04:00
transifex-integration[bot]
da10b908b3 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:14 -04:00
transifex-integration[bot]
4568935bc5 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:14 -04:00
transifex-integration[bot]
467404a55e Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:13 -04:00
transifex-integration[bot]
fcfe1e4558 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:13 -04:00
transifex-integration[bot]
c9d87a1b9a Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:13 -04:00
transifex-integration[bot]
35ebf4dfc2 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:13 -04:00
transifex-integration[bot]
bc3c256d22 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:13 -04:00
transifex-integration[bot]
0e10e74496 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:13 -04:00
transifex-integration[bot]
ebe9097f73 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:13 -04:00
transifex-integration[bot]
d61a11b647 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:13 -04:00
transifex-integration[bot]
d980cc1f8e Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:12 -04:00
transifex-integration[bot]
bd6056ce2e Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:12 -04:00
transifex-integration[bot]
cf48fda8d0 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:12 -04:00
transifex-integration[bot]
dc344cd28c Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:12 -04:00
transifex-integration[bot]
358610575f Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:12 -04:00
transifex-integration[bot]
a7869fccbb Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:12 -04:00
transifex-integration[bot]
a50903f90a Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2025-03-22 14:50:12 -04:00
transifex-integration[bot]
9243705995 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-03-22 14:50:12 -04:00
transifex-integration[bot]
db4dd9eee9 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-03-22 14:50:10 -04:00
ericholguin
22f2aba969 nwc: Wallet Redesign
This PR redesigns the NWC wallet view. A new view is added to introduce zaps to users. The set up wallet view is simplified, with new and existing wallet setup separated.
This also adds new NWC features such as getBalance and listTransactions allowing users to see their balance and previous transactions made.

Changelog-Added: Added view introducing users to Zaps
Changelog-Added: Added new wallet view with balance and transactions list
Changelog-Changed: Improved integration with Nostr Wallet Connect wallets
Closes: https://github.com/damus-io/damus/issues/2900

Signed-off-by: ericholguin <ericholguin@apache.org>
Co-Authored-By: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-03-19 18:00:00 -03:00
Daniel D’Aquino
98f2777fda Version bump to 1.14
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-03-17 11:05:43 -03:00
Daniel D’Aquino
102ce43216 v1.13 changelog
Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-03-17 10:58:12 -03: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
William Casarin
a696ac5084 Merge translations 2025-02-13 10:20:53 -08:00
William Casarin
28237c3a63 Merge release process 2025-02-13 10:19:01 -08:00
transifex-integration[bot]
1cae4640c0 Translate InfoPlist.strings in pl_PL
100% translated source file: 'InfoPlist.strings'
on 'pl_PL'.
2025-02-13 15:00:03 +00:00
transifex-integration[bot]
21a07d54cb Translate InfoPlist.strings in pl_PL
100% translated source file: 'InfoPlist.strings'
on 'pl_PL'.
2025-02-13 14:59:49 +00:00
transifex-integration[bot]
1efd07b852 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-02-13 14:58:45 +00:00
transifex-integration[bot]
e5eb7d44a2 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-02-13 04:25:45 +00:00
William Casarin
ec9a89ee4d Merge Fix unwanted draft auto-scrolls
Daniel D’Aquino (1):
      Fix unwanted auto-scrolls related to draft saving mechanism
2025-02-12 15:47:44 -08:00
William Casarin
4741c2a3e8 reposts: add links to repost listing in timeline
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-12 15:43:19 -08:00
Daniel D’Aquino
0111c5e2dc Fix unwanted auto-scrolls related to draft saving mechanism
This commit fixes an issue where the post view would scroll to the text
cursor at seemingly random times.

This was done by detaching the save view and its logic, so that we have
3 components:
1. The `PostView`
2. An auto-save view model (which is an Observable object)
3. A separate SwiftUI view for the auto-save indicator

The auto-save view model is shared between the `PostView` and the new
indicator view to ensure proper signaling and communication across
views.

However, this view model is only observed by the indicator view,
ensuring it updates its own view, while not causing any re-renders on
the rest of the `PostView`.

This refactor had the side-effect of making the auto-save logic and view
more reusable. It is now a separate collection of elements that can be
used anywhere else.

Beyond the scroll issue, this commit also prevents empty drafts from
being saved, by introducing the new save state called `.nothingToSave`

Note: No changelog item is needed because the new drafts feature was never
publicly released.

Changelog-None
Closes: https://github.com/damus-io/damus/issues/2826
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-12 13:28:40 -08:00
William Casarin
bed4e00b53 home: dont show reposts for the same note more than once
Fixes: https://github.com/damus-io/damus/issues/859
Changelog-Changed: Don't show reposts for the same note more than once in your home feed
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-12 12:19:10 -08:00
William Casarin
bf14d7138a driveby compile warning fixes
Signed-off-by: William Casarin <jb55@jb55.com>
2025-02-12 12:19:10 -08:00
emir yorulmaz
0c5da08a42 Change 'twitter' to 'X/Twitter' on README.md 2025-02-12 11:07:47 -08:00
Daniel D’Aquino
a6e123e928 Remove rust-nostr dependency
This commit removes rust-nostr dependency, and replaces the NIP-44 logic
with a new NIP-44 module based on the Swift NostrSDK implementation.

The decision to move away from rust-nostr and the Swift NostrSDK was
made for the following reasons:
1. `rust-nostr` caused the app size to double
2. We only need NIP44 functionality, and we don't need to bring
   everything else
3. The Swift NostrSDK caused conflicts around the secp256k1 dependency
   that is hard to address
4. The way we do things in the codebase is far different from the Swift
   NostrSDK, and we optimize it for use with NostrDB. Bringing it an
   outside library causes significant complexity in integration with
   NostrDB, and would effectively cause the codebase to be split into
   two different ways of achieving the same results. Therefore it is
   cleaner if we stick to our own Nostr structures and functions and
   focus on maintaining them.

However, the library CryptoSwift was added as a dependency, to bring in
ChaCha20 which is not supported by CryptoKit (CryptoKit supports the
ChaCha20-Poly1305 cipher, but NIP-44 uses ChaCha20 with HMAC-SHA256
instead)

Closes: https://github.com/damus-io/damus/issues/2849
Changelog-Changed: Made internal changes to reduce the app binary size
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-12 11:05:58 -08:00
transifex-integration[bot]
69b1173e08 Translate InfoPlist.strings in th
100% translated source file: 'InfoPlist.strings'
on 'th'.
2025-02-12 13:03:12 +00:00
transifex-integration[bot]
c3326213e9 Translate InfoPlist.strings in ja
100% translated source file: 'InfoPlist.strings'
on 'ja'.
2025-02-12 01:49:52 +00:00
325109d7b8 Remove preview strings from translation and add missing period to duplicate string to avoid double translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-02-11 09:52:03 -05:00
transifex-integration[bot]
f16d76605b Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-02-11 09:52:03 -05:00
transifex-integration[bot]
3eee1b205a Translate InfoPlist.strings in de
100% translated source file: 'InfoPlist.strings'
on 'de'.
2025-02-11 09:52:03 -05:00
transifex-integration[bot]
9545c6446d Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-02-11 09:52:03 -05:00
transifex-integration[bot]
40a75f65ab Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-02-11 09:52:03 -05:00
transifex-integration[bot]
98f42c9896 Translate InfoPlist.strings in nl
100% translated source file: 'InfoPlist.strings'
on 'nl'.
2025-02-11 09:52:03 -05:00
transifex-integration[bot]
5c22989675 Translate InfoPlist.strings in nl
100% translated source file: 'InfoPlist.strings'
on 'nl'.
2025-02-11 09:52:03 -05:00
999f16f6a4 Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-02-11 09:51:56 -05:00
transifex-integration[bot]
3f5fd6eee8 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-02-10 18:35:32 -05:00
transifex-integration[bot]
7c195aa75c Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-02-10 18:35:32 -05:00
transifex-integration[bot]
2071efc129 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-02-10 18:35:32 -05:00
transifex-integration[bot]
9db2e9b464 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-02-10 18:35:31 -05:00
transifex-integration[bot]
5f6cb568ff Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-02-10 18:35:31 -05:00
transifex-integration[bot]
045399a065 Translate Localizable.stringsdict in th
100% translated source file: 'Localizable.stringsdict'
on 'th'.
2025-02-10 18:35:31 -05:00
transifex-integration[bot]
1b526143d0 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2025-02-10 18:35:31 -05:00
transifex-integration[bot]
8a046c0d1b Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2025-02-10 18:35:31 -05:00
transifex-integration[bot]
2893e4234d Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2025-02-10 18:35:31 -05:00
transifex-integration[bot]
973a5ce2cb Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2025-02-10 18:35:31 -05:00
transifex-integration[bot]
1e81e90341 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2025-02-10 18:35:30 -05:00
9e7943e0e9 Fix translation export script by upgrading nostr-sdk-swift dependency to support Mac Catalyst
Changelog-Fixed: Fixed translation export script by upgrading nostr-sdk-swift dependency to support Mac Catalyst
Closes: https://github.com/damus-io/damus/issues/2841
Fixes: 24c3e61a4b ("Fix translation export script")
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-02-10 15:09:35 -08:00
Daniel D’Aquino
bb7ac4fea5 Release notes for v1.12.3
Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-07 11:15:11 -08:00
Daniel D’Aquino
05d0e15359 Add release process issue template
A Github issue template to help formalizing the release process, in
order to avoid human errors in the process.

Closes: https://github.com/damus-io/damus/issues/2752
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-07 10:45:54 -08:00
Daniel D’Aquino
d4d17fcbad Add release process issue template
A Github issue template to help formalizing the release process, in
order to avoid human errors in the process.

Closes: https://github.com/damus-io/damus/issues/2752
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-05 19:35:59 +00:00
Daniel D’Aquino
c21d29a897 Improve clarity of mute button to indicate it serves as a block feature
Changelog-Changed: Improved clarity of the mute button to indicate it can be used for blocking a user
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-03 18:33:01 -08:00
Daniel D’Aquino
6e117ac39c Improve Microphone usage description
This makes the microphone access request contain a message that is more
clear to the user

Changelog-Changed: Made the microphone access request message more clear to users
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-02-03 18:33:01 -08:00
Daniel D’Aquino
79407f17e8 Add double star for Purple members that have been active for over a year
This commit adds a special badge for purple members who have been active
for more than one entire year.

Closes: https://github.com/damus-io/damus/issues/2831
Changelog-Added: Purple members who have been active for more than a year now get a special badge
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-29 21:36:36 -08:00
Daniel D’Aquino
72c19fc411 Unsubscribe from push notifications on logout
Closes: https://github.com/damus-io/damus/issues/1707
Changelog-Fixed: Fixed issue where users continue to receive push notifications after logout
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-24 10:42:29 -08:00
Daniel D’Aquino
24c3e61a4b Make drafts persistent
This commit makes drafts persistent.

It does so by:
1. Converting `DraftsArtifacts` into Nostr events
2. Wrapping those Nostr events into NIP-37 notes
3. Saving those NIP-37 notes into NostrDB
4. Loading those same notes at startup
5. Unwrapping NIP-37 notes into Nostr events
6. Parsing that into `DraftsArtifacts`, loaded into DamusState
7. PostView can then load these drafts

Furthermore, a UX indicator was added to show when a draft has been
saved.

Limitations:
1. No encoding/decoding roundtrip guarantees. That would require
   extensive and heavy refactoring which is out of the scope of this
   commit.
2. We rely on `UserSettings` to keep track of note ids, while we do not
   have Ndb query capabilities
3. No NIP-37 relay sync support has been added yet, as that adds
   important privacy and sync conflict considerations which are out of
   the scope of this ticket, which is ensuring people don't lose their
   progress while writing notes.
4. The main use cases and scenarios have been tested. Because of (1),
   there may be some small inconsistencies on the stored version of the
   draft, but care was taken to keep the substantial portions of the
   content intact.

Closes: https://github.com/damus-io/damus/issues/1862
Changelog-Added: Added local persistence of note drafts
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-24 10:36:46 -08:00
Daniel D’Aquino
74d5bee1f6 Fix disappearing events on thread view
This commit fixes an issue where events in threads would occasionally
disappear.

Previously, the computation of parent events and reply events depended
on EventCache and had to be manually computed upon event selection
change. This may lead to inconsistencies if the computation is not
re-done after a new event that leads to a change in the model, or if certain
events are not yet on the cache. Instead, these are now computed
properties inside ThreadModel, and relies exclusively on the events
already in the ThreadModel.

Several other smaller improvements were made around the affected class,
including:
- Removing unused code for simplicity
- Configuring the class external interface with more intent, avoiding
  misusage
- Adding more documentation on the usage of things, as well as
  implementation notes on why certain design decisions were taken.
- Moving things to explicit actors, to integrate more structured concurrency
- Improving code efficiency to lower computational overhead on the main
  actor
- Splitting concerns between objects with more intent and thoughful
  design.

Changelog-Fixed: Fixed an issue where events on a thread view would occasionally disappear
Closes: https://github.com/damus-io/damus/issues/2791
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-24 10:36:46 -08:00
Daniel D’Aquino
8066fa1bf8 Improve robustness of the URL handler
This commit improves reliability on the handling of
external URLs.

This was achieved through the following improvements:
1. The URL handler interface is now well-defined, with more clear inputs
   and outputs, to avoid silent failures and error paths that are hard to see
   within convoluted logic paths
2. Side effects during URL parsing were almost completely removed for
   more predictable behavior
3. Error handling logic was added to present errors to the user in a user-friendly manner,
   instead of silently failing
4. Event loading logic was moved into a special new thread view, which
   makes its own internal state evident to the user (i.e. whether
   the note is loading, loaded, or if the note could not be found)

These changes make the URL opening logic more predictable, easy to
refactor, and helps ensure the user always gets some outcome from
opening a URL, even if it means showing a "not found" or "error" screen,
to eliminate cases where nothing seems to happen.

Closes: https://github.com/damus-io/damus/issues/2429
Changelog-Fixed: Improved robustness of the URL handler
Changelog-Added: Added user-friendly error view for errors around the app that would not fit in other places
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-24 10:05:55 -08:00
26df547605 Remove language filtering from Universe feed because language detection can be inaccurate
Changelog-Removed: Removed language filtering from Universe feed because language detection can be inaccurate

Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-01-22 17:23:00 +09:00
a97532b90d Translate notes even if they are in a preferred language but not the current language as that is what users expect
Changelog-Fixed: Translate notes even if they are in a preferred language but not the current language as that is what users expect

Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-01-22 17:23:00 +09:00
Daniel D’Aquino
e8ba1ec806 Merge pull request #2812 from damus-io/translations
Translations
2025-01-22 16:56:56 +09:00
Daniel D’Aquino
e8c265a4d8 Version bump to 1.13
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-22 14:03:22 +09:00
Daniel D’Aquino
b33dc63fe4 v1.12 changelog
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-22 13:56:42 +09:00
transifex-integration[bot]
c4852f1309 Translate Localizable.strings in hu_HU
100% translated source file: 'Localizable.strings'
on 'hu_HU'.
2025-01-21 13:36:41 +00:00
transifex-integration[bot]
39a4be7076 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-01-20 20:01:03 +00:00
transifex-integration[bot]
50c7edc420 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-01-20 19:40:49 +00:00
transifex-integration[bot]
67fa3c1ce5 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-01-20 19:40:37 +00:00
transifex-integration[bot]
cd671da3e7 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-01-20 19:40:30 +00:00
transifex-integration[bot]
3b60ca04f1 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-01-20 11:46:31 +00:00
transifex-integration[bot]
e2e58499f5 Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2025-01-20 00:40:20 +00:00
5cadf09665 Export strings for translation 2025-01-19 12:29:10 -05:00
transifex-integration[bot]
1ca7b3462f Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:10 -05:00
transifex-integration[bot]
8a552d2b0f Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:10 -05:00
transifex-integration[bot]
9fa0f18f78 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:10 -05:00
transifex-integration[bot]
db672ca048 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:09 -05:00
transifex-integration[bot]
18ad73cd35 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:09 -05:00
transifex-integration[bot]
5719e9b37e Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:09 -05:00
transifex-integration[bot]
9fb2b3c0e5 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:09 -05:00
transifex-integration[bot]
5ec66feb06 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:09 -05:00
transifex-integration[bot]
ccc301cfcc Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:09 -05:00
transifex-integration[bot]
c1b9d0b55e Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:09 -05:00
transifex-integration[bot]
d9daa27016 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:08 -05:00
transifex-integration[bot]
fa3b5d57ed Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:08 -05:00
transifex-integration[bot]
7c3e598ca6 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:08 -05:00
transifex-integration[bot]
563d5c7881 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:08 -05:00
transifex-integration[bot]
b8cba0ee17 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:08 -05:00
transifex-integration[bot]
8556586af4 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:08 -05:00
transifex-integration[bot]
5fc52bb31b Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:08 -05:00
transifex-integration[bot]
a92c9f2c38 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:08 -05:00
transifex-integration[bot]
61e137696e Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:07 -05:00
transifex-integration[bot]
8fc3b124da Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:07 -05:00
transifex-integration[bot]
7852822295 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:07 -05:00
transifex-integration[bot]
85e55953b3 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:07 -05:00
transifex-integration[bot]
077f633f33 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:07 -05:00
transifex-integration[bot]
1c3d1598a3 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:07 -05:00
transifex-integration[bot]
314608627e Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:07 -05:00
transifex-integration[bot]
aeecc04b29 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:06 -05:00
transifex-integration[bot]
341389d438 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-01-19 12:21:06 -05:00
transifex-integration[bot]
fbeae64123 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2025-01-19 12:21:05 -05:00
ericholguin
7a4af31859 nwc: Coinos
This PR adds a button to allow users to easily connect to Coinos
Also cleans up and organizes assets.

Changelog-Added: Coinos connection button in Wallet view

Signed-off-by: ericholguin <ericholguin@apache.org>
2025-01-12 15:20:06 +09:00
Tomek ⚡ K
e106be1412 Add Alby Go to mobile wallets selection
Changelog-Added: Added Alby Go to mobile wallets selection menu
Signed-off-by: Tomek K <itstomekk@getalby.com>
2025-01-12 11:33:20 +09:00
Swift Coder
282bf80daa Cancel ongoing uploading operations after cancelling post
1] Cancel ongoing uploading operations after the user has pressed "cancel post"
2] Don't generate haptic feedback in this error case because user has already dismissed the Post View

Changelog-Fixed: Cancel ongoing uploading operations after the user cancels the post
Signed-off-by: Swift Coder <scoder1747@gmail.com>
2025-01-06 15:55:53 +09:00
Daniel D’Aquino
bcb861a61b Improve accessibility of EditPictureControl
This commit improves accessibility of EditPictureControl, by adding
better accessibility labels and hints.

Changelog-Added: Minor accessibility improvements around picture editing and onboarding
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-06 15:45:58 +09:00
Daniel D’Aquino
bb0ad18913 Implement profile image cropping and optimization
This commit implements profile image cropping and optimization, as well
as a major refactor on EditPictureControl.

It now employs the following techniques:
- Users can now crop their profile pictures to fit a square aspect
  ratio nicely and avoid issues with automatic resizing/cropping
- Profile images are resized to a 400px by 400px image before sending it
  over the wire for better bandwidth usage
- Profile pictures are now tagged as such to the media uploaders, to
  enable media optimization or special care on their end.

Integrating the cropping step was very difficult with the previous
structures, so `EditPictureControl` was heavily refactored to have
improved state handling and better testability:

1. Enums with associated values are being used to capture all of the
   state in the picture selection process, as that helps ensure the
   needed info in each step is there and more clearly delianeate
   different steps — all at compile-time
2. The view was split into a view-model architecture, with almost all of
   the view logic ported to the new view-model class, making the view
   and the logic more clear to read as concerns are separated. This also
   enables better testabilty

Several automated tests were added to cover EditPictureControl logic and
looks.

Closes: https://github.com/damus-io/damus/issues/2643
Changelog-Added: Profile image cropping tools
Changelog-Changed: Improved profile image bandwidth optimization
Changelog-Changed: Improved reliability of picture selector
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-06 15:45:57 +09:00
Daniel D’Aquino
81830c7540 Add SwiftyCrop dependency
This commit adds the SwiftyCrop dependency, to provide users with a way
to crop their profile images prior to upload

- Dependency version is commit-hash-locked for extra security and
  reproducibility
- Reviewed code contents of the library to check for any user tracking
  code. None was found

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-01-06 15:45:57 +09:00
transifex-integration[bot]
68128b5ff1 Translate Localizable.strings in hu_HU
100% translated source file: 'Localizable.strings'
on 'hu_HU'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
aebeb26bc6 Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
79cf3db279 Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
dcae0d2cc7 Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
2b12dc5920 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
51930e7a12 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
b04e09d2e0 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2024-12-27 20:36:57 +09:00
b6c4213515 Export strings for translation 2024-12-27 20:36:57 +09:00
transifex-integration[bot]
8230c6eded Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
e79590f795 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
79bced1246 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
896f4b55e3 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
52e65f9429 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
a22cc532e2 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
823227920c Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
3e2bbce25e Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
e05b2d9ecf Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
d7b31a1cd8 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
70f01c0880 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
2cf5f21f78 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
96e8f8b6b2 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
370cfd1b08 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
046af15734 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
9e4ab2d54c Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
7cf12e2e0d Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
a63a81b387 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
d994cd13dc Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
95e985cfce Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
3a69de9274 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
64f5acf98c Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
5167ab264d Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
e02895b29f Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
0009d11025 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
afc317bb52 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
629212ea23 Translate Localizable.strings in pt_PT
100% translated source file: 'Localizable.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
transifex-integration[bot]
ec1252200f Translate InfoPlist.strings in pt_PT
100% translated source file: 'InfoPlist.strings'
on 'pt_PT'.
2024-12-27 20:36:57 +09:00
Swift Coder
54ea1ab803 MacOS Damus Support allowing link and photo sharing option
Changelog-Fixed: Fixed link and photo sharing support on macOS
Signed-off-by: Swift Coder <scoder1747@gmail.com>
2024-12-27 19:13:45 +09:00
Swift Coder
4cf8097de4 Displaying suitable text instead of Empty Notification View
Changelog-Fixed:Handle empty notification pages by displaying suitable text
Signed-off-by: Swift Coder <scoder1747@gmail.com>
2024-12-20 11:01:02 +09:00
Daniel D’Aquino
2c7384b0a9 Fix button hidden behind software keyboard in create account view
This commit fixes an issue where the "next" button is hidden behind the
software keyboard in the account creation view, where it is very hard to
press.

The fix was done by dynamically shrinking the profile picture size when
keyboard appears in smartphone screens, so that there is enough space
for all content to appear.

Changelog-Fixed: Fixed issue where the "next" button would appear hidden and hard to click on the create account view
Closes: https://github.com/damus-io/damus/issues/2771
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2024-12-20 10:38:20 +09:00
Swift Coder
19e312a8fb Fix non scrollable wallet screen
Changelog-Fixed: Fix non scrollable wallet screen
Signed-off-by: Swift Coder <scoder1747@gmail.com>
2024-12-18 11:27:51 +09:00
Swift Coder
3986308638 Render Gif and video files while composing posts
Changelog-Added: Render Gif and video files while composing posts
Signed-off-by: Swift Coder <scoder1747@gmail.com>
2024-12-16 17:22:12 +09:00
fa7740948b Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2024-12-16 17:04:20 +09:00
892a1420f3 Fix suggested users category titles to be localizable
Changelog-Fixed: Fixed suggested users category titles to be localizable

Signed-off-by: Terry Yiu <git@tyiu.xyz>
2024-12-16 17:04:20 +09:00
ee4cbf7363 Fix GradientFollowButton to have consistent width and autoscale text limited to 1 line
Changelog-Fixed: Fixed GradientFollowButton to have consistent width and autoscale text limited to 1 line

Signed-off-by: Terry Yiu <git@tyiu.xyz>
2024-12-16 17:04:20 +09:00
a1b1ce949b Fix right-to-left localization issues
Changelog-Fixed: Fixed right-to-left localization issues

Signed-off-by: Terry Yiu <git@tyiu.xyz>
2024-12-16 17:04:20 +09:00
902e8c3950 Fix AddMuteItemView to trim leading and trailing whitespaces from mute text and disallow adding text with only whitespaces
Changelog-Fixed: Fixed AddMuteItemView to trim leading and trailing whitespaces from mute text and disallow adding text with only whitespaces

Signed-off-by: Terry Yiu <git@tyiu.xyz>
2024-12-16 17:04:20 +09:00
b776788b38 Fix SideMenuView text to autoscale and limit to 1 line
Changelog-Fixed: Fixed SideMenuView text to autoscale and limit to 1 line

Signed-off-by: Terry Yiu <git@tyiu.xyz>
2024-12-16 17:04:20 +09:00
Daniel D’Aquino
78066773f4 Improve clarity of word search label
Changelog-Changed: Improved UX around the label for searching words
Closes: https://github.com/damus-io/damus/issues/2733
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2024-12-13 14:00:00 +09:00
Daniel D’Aquino
0bac284eee Fix issues with inputting a profile twice to the search bar
This fixes an issue where a user would have to input a profile npub
twice in order to get a result.

The fix is composed of the following constituents:
1. The removal of the dependency on NostrDB having profile information.
   Previously the function relied on NostrDB having profile information
   about a freshly downloaded profile, which it sometimes does not. The
   function does not require the profile to be on NostrDB at the time
   the profile is found on the relay.
2. The increase in allowed relay attempts to all relays. Previously it
   would only look for about half of the relays, which could cause
   certain events to not be found
3. The closing of relay subscription on EOSE. Previously, the
   subscription would only be closed if an event was found, which could
   lead to a "leak" of open subscriptions if an event is not found.

Closes: https://github.com/damus-io/damus/issues/2635
Changelog-Fixed: Fixed an issue where a profile would need to be input twice in the search to be found
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2024-12-13 14:00:00 +09:00
Daniel D’Aquino
07c95d1003 Turn on strict concurrency checks
Turn on strict concurrency checks on the compiler to make potential
concurrency issues more visible and aid during debugging, as well as to
start preparing us for Swift 6.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2024-12-13 14:00:00 +09:00
218 changed files with 8868 additions and 1950 deletions

52
.github/ISSUE_TEMPLATE/app_release.md vendored Normal file
View File

@@ -0,0 +1,52 @@
---
name: App release process
about: Begin preparing for a new app release
title: 'Release: '
labels: release-tasks
assignees: ''
---
A new version release. Please attempt to follow the release process steps below in the order they are shown.
## TestFlight release candidates
### Release candidate 1
**Version:** _[Enter full build information for the release candidate, including major and minor version number, build number, and commit hash]_
1. [ ] Merge in all needed changes to `master`
2. [ ] Check CI, make sure it is passing
3. [ ] Prepare preliminary changelog as a draft PR: _[Enter PR link to changelog here]_
4. [ ] Make a _release_ build and submit to the internal TestFlight group via our new Release candidate workflow in Xcode Cloud.
5. [ ] Prepare short screencast style video with main changes for the announcement
6. [ ] Publish release build to these TestFlight groups:
- [ ] Alpha testers group
- [ ] Translators group
- [ ] Purple group
7. [ ] Publish announcement on Nostr
_[Duplicate this release candidate section if there is more than one release candidate]_
## App Store release
1. [ ] Release candidate checks:
- [ ] Release candidate has been on Purple TestFlight for at least one week
- [ ] No blocker issues came from feedback from Purple users (double-check)
- [ ] Check with stakeholders
- [ ] Check with developers & product for any release showstoppers (e.g., critical newfound bugs)
2. [ ] Thorough check on release notes
3. [ ] Submit to App Store review (with manual publishing setting enabled)
4. [ ] Get App Store approval from Apple
5. [ ] Prepare announcement
7. [ ] Publish on the App Store and make announcement
8. [ ] Publish changelog and tag commit hash corresponding to the release
9. [ ] Perform a version bump on the repository, in preparation for the next release
## Notes/others
_Enter any relevant notes here_

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)

5
ACKNOWLEDGEMENTS.md Normal file
View File

@@ -0,0 +1,5 @@
### Acknowledgements and licenses
1. This product contains code derived from [Nostr SDK iOS](https://github.com/nostr-sdk/nostr-sdk-ios). [License](https://github.com/nostr-sdk/nostr-sdk-ios/blob/40df800c6749d7ce0b6fd7328e76cbc0dc71c87b/LICENSE)
2. This product includes software developed by the "Marcin Krzyzanowski" (http://krzyzanowskim.com/). [License](https://github.com/krzyzanowskim/CryptoSwift/blob/e74bbbfbef939224b242ae7c342a90e60b88b5ce/LICENSE)

View File

@@ -1,3 +1,115 @@
## [1.13.1] - 2025-03-21
### Fixed
- Fixed an issue where threads would not load properly (Daniel DAquino)
[1.13.1]: https://github.com/damus-io/damus/releases/tag/v1.13.1
## [1.13] - 2025-03-14
### Added
- Added local persistence of note drafts (Daniel DAquino)
- Added user-friendly error view for errors around the app that would not fit in other places (Daniel DAquino)
- Coinos connection button in Wallet view (ericholguin)
- Added Alby Go to mobile wallets selection menu (Tomek ⚡ K)
- Minor accessibility improvements around picture editing and onboarding (Daniel DAquino)
- Profile image cropping tools (Daniel DAquino)
- Added Conversations tab to profiles (Terry Yiu)
- Added profile pictures to push notifications (William Casarin)
### Changed
- Don't show reposts for the same note more than once in your home feed (William Casarin)
- Improved profile image bandwidth optimization (Daniel DAquino)
- Improved reliability of picture selector (Daniel DAquino)
- Changed spaces to newlines in new posts to provide cleaner separation between text, uploaded media, and quoted notes (Terry Yiu)
### Fixed
- Fixed issue where some push notifications would not open in the app and leave users confused (Daniel DAquino)
- Fixed issue where app would need a restart for new NWC wallets to work (Daniel DAquino)
- Fixed overly sensitive horizontal swipe on thread chat view (Daniel DAquino)
- Trim whitespaces from Lightning addresses (Terry Yiu)
- Fixed translation export script by upgrading nostr-sdk-swift dependency to support Mac Catalyst (Terry Yiu)
- Fixed issue where users continue to receive push notifications after logout (Daniel DAquino)
- Fixed an issue where events on a thread view would occasionally disappear (Daniel DAquino)
- Improved robustness of the URL handler (Daniel DAquino)
- Translate notes even if they are in a preferred language but not the current language as that is what users expect (Terry Yiu)
- Cancel ongoing uploading operations after the user cancels the post (Swift Coder)
- Fixed link and photo sharing support on macOS (Swift Coder)
- Fix bug where profile view was showing more than just the notes and replies on the notes / notes & replies tabs (Terry Yiu)
- Fixed reposts banner to be localizable (Terry Yiu)
### Removed
- Removed language filtering from Universe feed because language detection can be inaccurate (Terry Yiu)
- Removed mystery tabs meant to fix tab switching bug that no longer exists (Terry Yiu)
[1.13](https://github.com/damus-io/damus/releases/tag/v1.13): https://github.com/damus-io/damus/releases/tag/v1.13
## [1.12.3] - 2025-02-06
### Added
- Purple members who have been active for more than a year now get a special badge (Daniel DAquino)
### Changed
- Improved clarity of the mute button to indicate it can be used for blocking a user (Daniel DAquino)
- Made the microphone access request message more clear to users (Daniel DAquino)
[v1.12.3]: https://github.com/damus-io/damus/releases/tag/v1.12.3
## [1.12](https://github.com/damus-io/damus/releases/tag/v1.12) - 2024-12-20
### Added
- Render Gif and video files while composing posts (Swift Coder)
- Add profile info text in stretchable banner with follow button (Swift Coder)
- Paste Gif image similar to jpeg and png files (Swift Coder)
### Changed
- Improved UX around the label for searching words (Daniel DAquino)
- Improved accessibility support on some elements (Daniel DAquino)
### Fixed
- Fixed issue where the "next" button would appear hidden and hard to click on the create account view (Daniel DAquino)
- Fix non scrollable wallet screen (Swift Coder)
- Fixed suggested users category titles to be localizable (Terry Yiu)
- Fixed GradientFollowButton to have consistent width and autoscale text limited to 1 line (Terry Yiu)
- Fixed right-to-left localization issues (Terry Yiu)
- Fixed AddMuteItemView to trim leading and trailing whitespaces from mute text and disallow adding text with only whitespaces (Terry Yiu)
- Fixed SideMenuView text to autoscale and limit to 1 line (Terry Yiu)
- Fixed an issue where a profile would need to be input twice in the search to be found (Daniel DAquino)
- Fixed non-breaking spaces in localized strings (Terry Yiu)
- Fixed localization issue on Add mute item button (Terry Yiu)
- Replace non-breaking spaces with regular spaces as Apple's NSLocalizedString macro does not seem to work with it (Terry Yiu)
- Fixed localization issues in RelayConfigView (Terry Yiu)
- Fix duplicate uploads (Swift Coder)
- Remove duplicate pubkey from Follow Suggestion list (Swift Coder)
- Fix Page control indicator (Swift Coder)
- Fix damus sharing issues (Swift Coder)
- Fixed issue where banner edit button is unclickable (Daniel DAquino)
- Handle empty notification pages by displaying suitable text (Swift Coder)
[v1.12](https://github.com/damus-io/damus/releases/tag/v1.12): [https://github.com/damus-io/damus/releases/tag/v1.12]
## [v1.11(10)](https://github.com/damus-io/damus/releases/tag/v1.11-10) - 2024-11-18
### Added

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

View File

@@ -8,7 +8,7 @@ A twitter-like [nostr][nostr] client for iPhone, iPad and MacOS.
[nostr]: https://github.com/fiatjaf/nostr
## How is Damus better than twitter?
## How is Damus better than X/Twitter?
There are no toxic algorithms.\
You can send or receive zaps (satoshis) without asking for permission.\
[There is no central database](https://fiatjaf.com/nostr.html). Therefore, Damus is censorship resistant.\

1
TODO
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"originHash" : "534c8e58993919d5ead25ceb4788c8e492c86bc2cf5833dc651ae60a0f30169c",
"originHash" : "06318d35ee2e6bd681b95591e67da33a9461b48a3c652e58bd9d1a6f0d82bdac",
"pins" : [
{
"identity" : "codescanner",
@@ -9,13 +9,21 @@
"revision" : "9fa582f4b36c69c2a55bff5fb3377eb170ae273c"
}
},
{
"identity" : "cryptoswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
"state" : {
"revision" : "e74bbbfbef939224b242ae7c342a90e60b88b5ce"
}
},
{
"identity" : "emojikit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiKit",
"state" : {
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
"version" : "0.1.2"
"revision" : "47a4b1402de26be0299dcb4d667c1faaf21a7874",
"version" : "0.2.0"
}
},
{
@@ -23,8 +31,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiPicker.git",
"state" : {
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
"version" : "0.1.1"
"revision" : "3f48903721eae223238ff0af17c22d6373d33813",
"version" : "0.2.0"
}
},
{
@@ -97,6 +105,14 @@
"version" : "0.1.2"
}
},
{
"identity" : "swiftycrop",
"kind" : "remoteSourceControl",
"location" : "https://github.com/benedom/SwiftyCrop",
"state" : {
"revision" : "454d0a0d4faf6f3a19c8d817ab9d7d27524bd79f"
}
},
{
"identity" : "swipeactions",
"kind" : "remoteSourceControl",

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 511 KiB

After

Width:  |  Height:  |  Size: 511 KiB

View File

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 187 KiB

View File

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 547 KiB

After

Width:  |  Height:  |  Size: 547 KiB

View File

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "alby.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "alby.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "alby.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 300 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 215 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "profile-banner.jpeg",
"filename" : "coinos.png",
"idiom" : "universal"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 176 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "alby-go.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,23 +0,0 @@
{
"images": [
{
"filename": "alby.svg",
"idiom": "universal",
"scale": "1x"
},
{
"idiom": "universal",
"scale": "2x",
"filename": "alby.svg"
},
{
"idiom": "universal",
"scale": "3x",
"filename": "alby.svg"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View File

@@ -1,15 +0,0 @@
//
// MutinyGradient.swift
// damus
//
// Created by eric on 3/9/24.
//
import SwiftUI
fileprivate let mutiny_grad_c1 = hex_col(r: 39, g: 95, b: 161)
fileprivate let mutiny_grad_c2 = hex_col(r: 13, g: 33, b: 56)
fileprivate let mutiny_grad = [mutiny_grad_c2, mutiny_grad_c1]
let MutinyGradient: LinearGradient =
LinearGradient(colors: mutiny_grad, startPoint: .top, endPoint: .bottom)

View File

@@ -232,7 +232,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
flusher = .once({ pe in
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
Task { @MainActor in
await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
await WalletConnect.send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
}
})
}
@@ -240,7 +240,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
let delay = damus_state.settings.nozaps ? nil : 5.0
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")

View File

@@ -10,22 +10,68 @@ import SwiftUI
struct Reposted: View {
let damus: DamusState
let pubkey: Pubkey
let target: NostrEvent
@State var reposts: Int
init(damus: DamusState, pubkey: Pubkey, target: NostrEvent) {
self.damus = damus
self.pubkey = pubkey
self.target = target
self.reposts = damus.boosts.counts[target.id] ?? 1
}
var body: some View {
HStack(alignment: .center) {
Image("repost")
.foregroundColor(Color.gray)
ProfileName(pubkey: pubkey, damus: damus, show_nip5_domain: false)
.foregroundColor(Color.gray)
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
.foregroundColor(Color.gray)
// Show profile picture of the reposter only if the reposter is not the author of the reposted note.
if pubkey != target.pubkey {
ProfilePicView(pubkey: pubkey, size: eventview_pfp_size(.small), highlight: .none, profiles: damus.profiles, disable_animation: damus.settings.disable_animation)
.onTapGesture {
show_profile_action_sheet_if_enabled(damus_state: damus, pubkey: pubkey)
}
.onLongPressGesture(minimumDuration: 0.1) {
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
damus.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
}
}
NavigationLink(value: Route.Reposts(reposts: .reposts(state: damus, target: target.id))) {
Text(people_reposted_text(profiles: damus.profiles, pubkey: pubkey, reposts: reposts))
.font(.subheadline)
.foregroundColor(.gray)
}
}
.onReceive(handle_notify(.update_stats), perform: { note_id in
guard note_id == target.id else { return }
let repost_count = damus.boosts.counts[target.id]
if let repost_count, reposts != repost_count {
reposts = repost_count
}
})
}
}
func people_reposted_text(profiles: Profiles, pubkey: Pubkey, reposts: Int, locale: Locale = Locale.current) -> String {
guard reposts > 0 else {
return ""
}
let bundle = bundleForLocale(locale: locale)
let other_reposts = reposts - 1
let display_name = event_author_name(profiles: profiles, pubkey: pubkey)
if other_reposts == 0 {
return String(format: NSLocalizedString("%@ reposted", bundle: bundle, comment: "Text indicating that the note was reposted (i.e. re-shared)."), locale: locale, display_name)
} else {
return String(format: localizedStringFormat(key: "people_reposted_count", locale: locale), locale: locale, other_reposts, display_name)
}
}
struct Reposted_Previews: PreviewProvider {
static var previews: some View {
let test_state = test_damus_state
Reposted(damus: test_state, pubkey: test_state.pubkey)
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note)
}
}

View File

@@ -63,7 +63,7 @@ struct SelectableText: View {
})) {
if let event, case .show_highlight_post_view(let highlighted_text) = self.selectedTextActionState {
PostView(
action: .highlighting(.init(selected_text: highlighted_text, source: .event(event))),
action: .highlighting(.init(selected_text: highlighted_text, source: .event(event.id))),
damus_state: damus_state
)
.presentationDragIndicator(.visible)

View File

@@ -12,6 +12,14 @@ struct SupporterBadge: View {
let purple_account: DamusPurple.Account?
let style: Style
let text_color: Color
var badge_variant: BadgeVariant {
if purple_account?.attributes.contains(.memberForMoreThanOneYear) == true {
return .oneYearSpecial
}
else {
return .normal
}
}
init(percent: Int?, purple_account: DamusPurple.Account? = nil, style: Style, text_color: Color = .secondary) {
self.percent = percent
@@ -26,13 +34,18 @@ struct SupporterBadge: View {
HStack {
if let purple_account, purple_account.active == true {
HStack(spacing: 1) {
Image("star.fill")
.resizable()
.frame(width:size, height:size)
.foregroundStyle(GoldGradient)
if self.style == .full {
let date = format_date(date: purple_account.created_at, time_style: .none)
Text(date)
switch self.badge_variant {
case .normal:
StarShape()
.frame(width:size, height:size)
.foregroundStyle(GoldGradient)
case .oneYearSpecial:
DoubleStar(size: size)
}
if self.style == .full,
let ordinal = self.purple_account?.ordinal() {
Text(ordinal)
.foregroundStyle(text_color)
.font(.caption)
}
@@ -56,8 +69,102 @@ struct SupporterBadge: View {
case full // Shows the entire badge with a purple subscriber number if present
case compact // Does not show purple subscriber number. Only shows the star (if applicable)
}
enum BadgeVariant {
/// A normal badge that people are used to
case normal
/// A special badge for users who have been members for more than a year
case oneYearSpecial
}
}
struct StarShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius: CGFloat = min(rect.width, rect.height) / 2
let points = 5
let adjustment: CGFloat = .pi / 2
for i in 0..<points * 2 {
let angle = (CGFloat(i) * .pi / CGFloat(points)) - adjustment
let pointRadius = i % 2 == 0 ? radius : radius * 0.4
let point = CGPoint(x: center.x + pointRadius * cos(angle), y: center.y + pointRadius * sin(angle))
if i == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.closeSubpath()
return path
}
}
struct DoubleStar: View {
let size: CGFloat
var starOffset: CGFloat = 5
var body: some View {
if #available(iOS 17.0, *) {
DoubleStarShape(starOffset: starOffset)
.frame(width: size, height: size)
.foregroundStyle(GoldGradient)
.padding(.trailing, starOffset)
} else {
Fallback(size: size, starOffset: starOffset)
}
}
@available(iOS 17.0, *)
struct DoubleStarShape: Shape {
var strokeSize: CGFloat = 3
var starOffset: CGFloat
func path(in rect: CGRect) -> Path {
let normalSizedStarPath = StarShape().path(in: rect)
let largerStarPath = StarShape().path(in: rect.insetBy(dx: -strokeSize, dy: -strokeSize))
let finalPath = normalSizedStarPath
.subtracting(
largerStarPath.offsetBy(dx: starOffset, dy: 0)
)
.union(
normalSizedStarPath.offsetBy(dx: starOffset, dy: 0)
)
return finalPath
}
}
/// A fallback view for those who cannot run iOS 17
struct Fallback: View {
var size: CGFloat
var starOffset: CGFloat
var body: some View {
HStack {
StarShape()
.frame(width: size, height: size)
.foregroundStyle(GoldGradient)
StarShape()
.fill(GoldGradient)
.overlay(
StarShape()
.stroke(Color.damusAdaptableWhite, lineWidth: 1)
)
.frame(width: size + 1, height: size + 1)
.padding(.leading, -size - starOffset)
}
.padding(.trailing, -3)
}
}
}
func support_level_color(_ percent: Int) -> Color {
if percent == 0 {
return .gray
@@ -86,7 +193,7 @@ struct SupporterBadge_Previews: PreviewProvider {
HStack(alignment: .center) {
SupporterBadge(
percent: nil,
purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: subscriber_number, active: true),
purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: subscriber_number, active: true, attributes: []),
style: .full
)
.frame(width: 100)
@@ -118,4 +225,52 @@ struct SupporterBadge_Previews: PreviewProvider {
}
}
#Preview("1 yr badge") {
VStack {
HStack(alignment: .center) {
SupporterBadge(
percent: nil,
purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: 3, active: true, attributes: []),
style: .full
)
.frame(width: 100)
}
HStack(alignment: .center) {
SupporterBadge(
percent: nil,
purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: 3, active: true, attributes: [.memberForMoreThanOneYear]),
style: .full
)
.frame(width: 100)
}
Text(verbatim: "Double star (just shape itself, with alt background color, to show it adapts to background well)")
.multilineTextAlignment(.center)
if #available(iOS 17.0, *) {
HStack(alignment: .center) {
DoubleStar.DoubleStarShape(starOffset: 5)
.frame(width: 17, height: 17)
.padding(.trailing, -8)
}
.background(Color.blue)
}
Text(verbatim: "Double star (fallback for iOS 16 and below)")
HStack(alignment: .center) {
DoubleStar.Fallback(size: 17, starOffset: 5)
}
Text(verbatim: "Double star (fallback for iOS 16 and below, with alt color limitation shown)")
.multilineTextAlignment(.center)
HStack(alignment: .center) {
DoubleStar.Fallback(size: 17, starOffset: 5)
}
.background(Color.blue)
}
}

View File

@@ -31,7 +31,8 @@ enum Sheets: Identifiable {
case onboardingSuggestions
case purple(DamusPurpleURL)
case purple_onboarding
case error(ErrorView.UserPresentableError)
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
return .zap(ZapSheet(target: target, lnurl: lnurl))
}
@@ -53,6 +54,7 @@ enum Sheets: Identifiable {
case .onboardingSuggestions: return "onboarding-suggestions"
case .purple(let purple_url): return "purple" + purple_url.url_string()
case .purple_onboarding: return "purple_onboarding"
case .error(_): return "error"
}
}
}
@@ -220,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))
@@ -310,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 {
@@ -339,36 +338,14 @@ struct ContentView: View {
DamusPurpleURLSheetView(damus_state: damus_state!, purple_url: purple_url)
case .purple_onboarding:
DamusPurpleNewUserOnboardingView(damus_state: damus_state)
case .error(let error):
ErrorView(damus_state: damus_state!, error: error)
}
}
.onOpenURL { url in
on_open_url(state: damus_state!, url: url) { res in
guard let res else {
return
}
switch res {
case .filter(let filt): self.open_search(filt: filt)
case .profile(let pk): self.open_profile(pubkey: pk)
case .event(let ev): self.open_event(ev: ev)
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
case .script(let data): self.open_script(data)
case .purple(let purple_url):
if case let .welcome(checkout_id) = purple_url.variant {
// If this is a welcome link, do the following before showing the onboarding screen:
// 1. Check if this is legitimate and good to go.
// 2. Mark as complete if this is good to go.
Task {
let is_good_to_go = try? await damus_state.purple.check_and_mark_ln_checkout_is_good_to_go(checkout_id: checkout_id)
if is_good_to_go == true {
self.active_sheet = .purple(purple_url)
}
}
}
else {
self.active_sheet = .purple(purple_url)
}
}
Task {
let open_action = await DamusURLHandler.handle_opening_url_and_compute_view_action(damus_state: self.damus_state, url: url)
self.execute_open_action(open_action)
}
}
.onReceive(handle_notify(.compose)) { action in
@@ -390,6 +367,10 @@ struct ContentView: View {
self.confirm_mute = true
}
.onReceive(handle_notify(.attached_wallet)) { nwc in
// Ensure to add NWC relay to the pool and connect it.
try? damus_state.pool.add_relay(.nwc(url: nwc.relay))
damus_state.pool.connect(to: [nwc.relay])
// update the lightning address on our profile when we attach a
// wallet with an associated
guard let ds = self.damus_state,
@@ -531,27 +512,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()
@@ -608,7 +568,7 @@ struct ContentView: View {
}, message: {
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
})
.alert(NSLocalizedString("Mute User", comment: "Title of alert for muting a user."), isPresented: $confirm_mute, actions: {
.alert(NSLocalizedString("Mute/Block User", comment: "Title of alert for muting/blocking a user."), isPresented: $confirm_mute, actions: {
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for muting a user."), role: .cancel) {
confirm_mute = false
}
@@ -661,6 +621,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()
@@ -766,22 +748,38 @@ struct ContentView: View {
damus_state.postbox.send(ev)
}
}
private func openEvent(noteId: NoteId, notificationType: LocalNotificationType) {
guard let target = damus_state.events.lookup(noteId) else {
/// 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,
/// for example a URL
///
/// ## Implementation notes
///
/// - The reason this was created was to separate URL parsing logic, the underlying actions that mutate the state of the app, and the action to be taken on the view layer as a result. This makes it easier to test, to read the URL handling code, and to add new functionality in between the two (e.g. a confirmation screen before proceeding with a given open action)
enum ViewOpenAction {
/// Open a page route
case route(Route)
/// Open a sheet
case sheet(Sheets)
/// Do nothing.
///
/// ## Implementation notes
/// - This is used here instead of Optional values to make semantics explicit and force better programming intent, instead of accidentally doing nothing because of Swift's syntax sugar.
case no_action
}
/// Executes an action to open something in the app view
///
/// - Parameter open_action: The action to perform
func execute_open_action(_ open_action: ViewOpenAction) {
switch open_action {
case .route(let route):
navigationCoordinator.push(route: route)
case .sheet(let sheet):
self.active_sheet = sheet
case .no_action:
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
}
}
}
@@ -931,14 +929,41 @@ enum FindEventType {
enum FoundEvent {
case profile(Pubkey)
case invalid_profile(NostrEvent)
case event(NostrEvent)
}
/// Finds an event from NostrDB if it exists, or from the network
///
/// This is the callback version. There is also an asyc/await version of this function.
///
/// - Parameters:
/// - state: Damus state
/// - query_: The query, including the event being looked for, and the relays to use when looking
/// - callback: The function to call with results
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
return find_event_with_subid(state: state, query: query_, subid: UUID().description, callback: callback)
}
/// Finds an event from NostrDB if it exists, or from the network
///
/// This is a the async/await version of `find_event`. Use this when using callbacks is impossible or cumbersome.
///
/// - Parameters:
/// - state: Damus state
/// - query_: The query, including the event being looked for, and the relays to use when looking
/// - callback: The function to call with results
func find_event(state: DamusState, query query_: FindEvent) async -> FoundEvent? {
await withCheckedContinuation { continuation in
find_event(state: state, query: query_) { event in
var already_resumed = false
if !already_resumed { // Ensure we do not resume twice, as it causes a crash
continuation.resume(returning: event)
already_resumed = true
}
}
}
}
func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: String, callback: @escaping (FoundEvent?) -> ()) {
var filter: NostrFilter? = nil
@@ -988,10 +1013,6 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
switch query {
case .profile:
if ev.known_kind == .metadata {
guard state.ndb.lookup_profile_key(ev.pubkey) != nil else {
callback(.invalid_profile(ev))
return
}
callback(.profile(ev.pubkey))
}
case .event:
@@ -1000,20 +1021,28 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
case .eose:
if !has_event {
attempts += 1
if attempts == state.pool.our_descriptors.count / 2 {
callback(nil)
if attempts >= state.pool.our_descriptors.count {
callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil
}
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
}
state.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
case .notice:
break
case .auth:
break
}
}
}
/// Finds a replaceable event based on an `naddr` address.
///
/// This is the callback version of the function. There is another function that makes use of async/await
///
/// - Parameters:
/// - damus_state: The Damus state
/// - naddr: the `naddr` address
/// - callback: A function to handle the found event
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
@@ -1042,6 +1071,26 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos
}
}
/// Finds a replaceable event based on an `naddr` address.
///
/// This is the async/await version of the function. Another version of this function which makes use of callback functions also exists .
///
/// - Parameters:
/// - damus_state: The Damus state
/// - naddr: the `naddr` address
/// - callback: A function to handle the found event
func naddrLookup(damus_state: DamusState, naddr: NAddr) async -> NostrEvent? {
await withCheckedContinuation { continuation in
var already_resumed = false
naddrLookup(damus_state: damus_state, naddr: naddr) { event in
if !already_resumed { // Ensure we do not resume twice, as it causes a crash
continuation.resume(returning: event)
already_resumed = true
}
}
}
}
func timeline_name(_ timeline: Timeline?) -> String {
guard let timeline else {
return ""
@@ -1152,60 +1201,32 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
}
}
enum OpenResult {
case profile(Pubkey)
case filter(NostrFilter)
case event(NostrEvent)
case wallet_connect(WalletConnectURL)
case script([UInt8])
case purple(DamusPurpleURL)
}
func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) {
if let purple_url = DamusPurpleURL(url: url) {
result(.purple(purple_url))
return
}
if let nwc = WalletConnectURL(str: url.absoluteString) {
result(.wallet_connect(nwc))
return
}
guard let link = decode_nostr_uri(url.absoluteString) else {
result(nil)
return
}
switch link {
case .ref(let ref):
switch ref {
case .pubkey(let pk):
result(.profile(pk))
case .event(let noteid):
find_event(state: state, query: .event(evid: noteid)) { res in
guard let res, case .event(let ev) = res else { return }
result(.event(ev))
}
case .hashtag(let ht):
result(.filter(.filter_hashtag([ht.hashtag])))
case .param, .quote, .reference:
// doesn't really make sense here
break
case .naddr(let naddr):
naddrLookup(damus_state: state, naddr: naddr) { res in
guard let res = res else { return }
result(.event(res))
}
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)))
}
case .filter(let filt):
result(.filter(filt))
break
// TODO: handle filter searches?
case .script(let script):
result(.script(script))
break
}
}

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>
@@ -48,6 +52,8 @@
<key>LSApplicationQueriesSchemes</key>
<array>
<string>river</string>
<string>alby</string>
<string>albygo</string>
<string>bitcoinbeach</string>
<string>breez</string>
<string>muun</string>
@@ -71,6 +77,6 @@
<key>NSAppleMusicUsageDescription</key>
<string>Damus needs access to your media library for playback statuses</string>
<key>NSMicrophoneUsageDescription</key>
<string>Damus needs access to your microphone for creating video recording posts</string>
<string>Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network</string>
</dict>
</plist>

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

@@ -175,6 +175,9 @@ class DamusState: HeadlessDamusState {
func close() {
print("txn: damus close")
Task {
try await self.push_notification_client.revoke_token()
}
wallet.disconnect()
pool.close()
ndb.close()

View File

@@ -6,14 +6,45 @@
//
import Foundation
import SwiftUICore
import UIKit
/// Represents artifacts in a post draft, which is rendered by `PostView`
///
/// ## Implementation notes
///
/// - This is NOT `Codable` because we store these persistently as NIP-37 drafts in NostrDB, instead of directly encoding the object.
/// - `NSMutableAttributedString` is the bottleneck for making this `Codable`, and replacing that with another type requires a very large refactor.
/// - Encoding/decoding logic is lossy, and is not fully round-trippable. This class does a best effort attempt at encoding and recovering as much information as possible, but the information is dispersed into many different places, types, and functions around the code, making round-trip guarantees very difficult without severely refactoring `PostView`, `TextViewWrapper`, and other associated classes, unfortunately. These are the known limitations at the moment:
/// - Image metadata is lost on decoding
/// - The `filtered_pubkeys` filter effectively gets applied upon encoding, causing them to change upon decoding
///
class DraftArtifacts: Equatable {
/// The text content of the note draft
///
/// ## Implementation notes
///
/// - This serves as the backing model for `PostView` and `TextViewWrapper`. It might be cleaner to use a specialized data model for this in the future and render to attributed string in real time, but that will require a big refactor. See https://github.com/damus-io/damus/issues/1862#issuecomment-2585756932
var content: NSMutableAttributedString
/// A list of media items that have been attached to the note draft.
var media: [UploadedMedia]
/// The references for this note, which will be translated into tags once the event is published.
var references: [RefId]
/// Pubkeys that should be filtered out from the references
///
/// For example, when replying to an event, the user can select which pubkey mentions they want to keep, and which ones to remove.
var filtered_pubkeys: Set<Pubkey> = []
init(content: NSMutableAttributedString = NSMutableAttributedString(string: ""), media: [UploadedMedia] = []) {
/// A unique ID for this draft that allows us to address these if we need to.
///
/// This will be the unique identifier in the NIP-37 note
let id: String
init(content: NSMutableAttributedString = NSMutableAttributedString(string: ""), media: [UploadedMedia] = [], references: [RefId], id: String) {
self.content = content
self.media = media
self.references = references
self.id = id
}
static func == (lhs: DraftArtifacts, rhs: DraftArtifacts) -> Bool {
@@ -22,11 +53,217 @@ class DraftArtifacts: Equatable {
lhs.content.string == rhs.content.string // Comparing the text content is not perfect but acceptable in this case because attributes for our post editor are determined purely from text content
)
}
// MARK: Encoding and decoding functions to and from NIP-37 nostr events
/// Converts the draft artifacts into a NIP-37 draft event that can be saved into NostrDB or any Nostr relay
///
/// - Parameters:
/// - action: The post action for this draft, which provides necessary context for the draft (e.g. Is it meant to highlight something? Reply to something?)
/// - damus_state: The damus state, needed for encrypting, fetching Nostr data depedencies, and forming the NIP-37 draft
/// - references: references in the post?
/// - Returns: The NIP-37 draft packaged in a way that can be easily wrapped/unwrapped.
func to_nip37_draft(action: PostAction, damus_state: DamusState) throws -> NIP37Draft? {
guard let keypair = damus_state.keypair.to_full() else { return nil }
let post = build_post(state: damus_state, action: action, draft: self)
guard let note = post.to_event(keypair: keypair) else { return nil }
return try NIP37Draft(unwrapped_note: note, draft_id: self.id, keypair: keypair)
}
/// Instantiates a draft object from a NIP-37 draft
/// - Parameters:
/// - nip37_draft: The NIP-37 draft object
/// - damus_state: Damus state of the user who wants to load this draft object. Needed for pulling profiles from Ndb, and decrypting contents.
/// - Returns: A draft artifacts object, or `nil` if such cannot be loaded.
static func from(nip37_draft: NIP37Draft, damus_state: DamusState) -> DraftArtifacts? {
return Self.from(
event: nip37_draft.unwrapped_note,
draft_id: nip37_draft.id ?? UUID().uuidString, // Generate random UUID as the draft ID if none is specified. It is always better to have an ID that we can use for addressing later.
damus_state: damus_state
)
}
/// Load a draft artifacts object from a plain, unwrapped NostrEvent
///
/// This function will parse the contents of a Nostr Event and turn it into an editable draft that we can use.
///
/// - Parameters:
/// - event: The Nostr event to use as a template
/// - draft_id: The unique ID of this draft, used for keeping draft identities stable. UUIDs are recommended but not required.
/// - damus_state: The user's Damus state, used for fetching profiles in NostrDB
/// - Returns: The draft that can be loaded into `PostView`.
static func from(event: NostrEvent, draft_id: String, damus_state: DamusState) -> DraftArtifacts {
let parsed_blocks = parse_note_content(content: .init(note: event, keypair: damus_state.keypair))
return Self.from(parsed_blocks: parsed_blocks, references: Array(event.references), draft_id: draft_id, damus_state: damus_state)
}
/// Load a draft artifacts object from parsed Nostr event blocks
///
/// - Parameters:
/// - parsed_blocks: The blocks parsed from a Nostr event
/// - references: The references in the Nostr event
/// - draft_id: The unique ID of the draft as per NIP-37
/// - damus_state: Damus state, used for fetching profile info in NostrDB
/// - Returns: The draft that can be loaded into `PostView`.
static func from(parsed_blocks: Blocks, references: [RefId], draft_id: String, damus_state: DamusState) -> DraftArtifacts {
let rich_text_content: NSMutableAttributedString = .init(string: "")
var media: [UploadedMedia] = []
for block in parsed_blocks.blocks {
switch block {
case .mention(let mention):
if case .pubkey(let pubkey) = mention.ref {
// A profile reference, format things properly.
let profile = damus_state.ndb.lookup_profile(pubkey)?.unsafeUnownedValue?.profile
let profile_name = DisplayName(profile: profile, pubkey: pubkey).username
guard let url_address = URL(string: block.asString) else {
rich_text_content.append(.init(string: block.asString))
continue
}
let attributed_string = NSMutableAttributedString(
string: "@\(profile_name)",
attributes: [
.link: url_address,
.foregroundColor: UIColor(Color.accentColor)
]
)
rich_text_content.append(attributed_string)
}
else if case .note(_) = mention.ref {
// These note references occur when we quote a note, and since that is tracked via `PostAction` in `PostView`, ignore it here to avoid attaching the same event twice in a note
continue
}
else {
// Other references
rich_text_content.append(.init(string: block.asString))
}
case .url(let url):
if isSupportedImage(url: url) {
// Image, add that to our media attachments
// TODO: Add metadata decoding support
media.append(UploadedMedia(localURL: url, uploadedURL: url, metadata: .none))
continue
}
else {
// Normal URL, plain text
rich_text_content.append(.init(string: block.asString))
}
case .invoice(_), .relay(_), .hashtag(_), .text(_):
// Everything else is currently plain text.
rich_text_content.append(.init(string: block.asString))
}
}
return DraftArtifacts(content: rich_text_content, media: media, references: references, id: draft_id)
}
}
/// Holds and keeps track of the note post drafts throughout the app.
class Drafts: ObservableObject {
@Published var post: DraftArtifacts? = nil
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
@Published var highlights: [HighlightSource: DraftArtifacts] = [:]
@Published var replies: [NoteId: DraftArtifacts] = [:]
@Published var quotes: [NoteId: DraftArtifacts] = [:]
/// The drafts we have for highlights
///
/// ## Implementation notes
/// - Although in practice we also load drafts based on the highlight source for better UX (making it easier to find a draft), we need the keys to be of type `HighlightContentDraft` because we need the selected text information to be able to construct the NIP-37 draft, as well as to load that into post view.
@Published var highlights: [HighlightContentDraft: DraftArtifacts] = [:]
/// Loads drafts from storage (NostrDB + UserDefaults)
func load(from damus_state: DamusState) {
guard let note_ids = damus_state.settings.draft_event_ids?.compactMap({ NoteId(hex: $0) }) else { return }
for note_id in note_ids {
let txn = damus_state.ndb.lookup_note(note_id)
guard let note = txn?.unsafeUnownedValue else { continue }
// Implementation note: This currently fails silently, because:
// 1. Errors are unlikely and not expected
// 2. It is not mission critical to recover from this error
// 3. The changes that add a error view sheet with useful info is not yet merged in as of writing.
try? self.load(wrapped_draft_note: note, with: damus_state)
}
}
/// Loads a specific NIP-37 note into this class
func load(wrapped_draft_note: NdbNote, with damus_state: DamusState) throws {
// Extract draft info from the NIP-37 note
guard let full_keypair = damus_state.keypair.to_full() else { return }
guard let nip37_draft = try NIP37Draft(wrapped_note: wrapped_draft_note, keypair: full_keypair) else { return }
guard let known_kind = nip37_draft.unwrapped_note.known_kind else { return }
guard let draft_artifacts = DraftArtifacts.from(
nip37_draft: nip37_draft,
damus_state: damus_state
) else { return }
// Find out where to place these drafts
let blocks = parse_note_content(content: .note(nip37_draft.unwrapped_note))
switch known_kind {
case .text:
if let replied_to_note_id = nip37_draft.unwrapped_note.direct_replies() {
self.replies[replied_to_note_id] = draft_artifacts
}
else {
for block in blocks.blocks {
if case .mention(let mention) = block {
if case .note(let note_id) = mention.ref {
self.quotes[note_id] = draft_artifacts
return
}
}
}
self.post = draft_artifacts
}
case .highlight:
guard let highlight = HighlightContentDraft(from: nip37_draft.unwrapped_note) else { return }
self.highlights[highlight] = draft_artifacts
default:
return
}
}
/// Saves the drafts tracked by this class persistently using NostrDB + UserDefaults
func save(damus_state: DamusState) {
var draft_events: [NdbNote] = []
post_artifact_block: if let post_artifacts = self.post {
let nip37_draft = try? post_artifacts.to_nip37_draft(action: .posting(.user(damus_state.pubkey)), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { break post_artifact_block }
draft_events.append(wrapped_note)
}
for (replied_to_note_id, reply_artifacts) in self.replies {
guard let replied_to_note = damus_state.ndb.lookup_note(replied_to_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
let nip37_draft = try? reply_artifacts.to_nip37_draft(action: .replying_to(replied_to_note), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
draft_events.append(wrapped_note)
}
for (quoted_note_id, quote_note_artifacts) in self.quotes {
guard let quoted_note = damus_state.ndb.lookup_note(quoted_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
let nip37_draft = try? quote_note_artifacts.to_nip37_draft(action: .quoting(quoted_note), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
draft_events.append(wrapped_note)
}
for (highlight, highlight_note_artifacts) in self.highlights {
let nip37_draft = try? highlight_note_artifacts.to_nip37_draft(action: .highlighting(highlight), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
draft_events.append(wrapped_note)
}
for draft_event in draft_events {
// Implementation note: We do not support draft synchronization with relays yet.
// TODO: Once it is time to implement draft syncing with relays, please consider the following:
// - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations
// - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again)
damus_state.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
}
damus_state.settings.draft_event_ids = draft_events.map({ $0.id.hex() })
}
}
// MARK: - Convenience extensions
fileprivate extension Array {
mutating func appendIfNotNil(_ element: Element?) {
if let element = element {
self.append(element)
}
}
}

View File

@@ -188,17 +188,29 @@ extension HighlightEvent {
struct HighlightContentDraft: Hashable {
let selected_text: String
let source: HighlightSource
init(selected_text: String, source: HighlightSource) {
self.selected_text = selected_text
self.source = source
}
init?(from note: NdbNote) {
guard let source = HighlightSource.from(tags: note.tags.strings()) else { return nil }
self.source = source
self.selected_text = note.content
}
}
enum HighlightSource: Hashable {
static let TAG_SOURCE_ELEMENT = "source"
case event(NostrEvent)
case event(NoteId)
case external_url(URL)
func tags() -> [[String]] {
switch self {
case .event(let event):
return [ ["e", "\(event.id)", HighlightSource.TAG_SOURCE_ELEMENT] ]
case .event(let event_id):
return [ ["e", "\(event_id)", HighlightSource.TAG_SOURCE_ELEMENT] ]
case .external_url(let url):
return [ ["r", "\(url)", HighlightSource.TAG_SOURCE_ELEMENT] ]
}
@@ -206,12 +218,26 @@ enum HighlightSource: Hashable {
func ref() -> RefId {
switch self {
case .event(let event):
return .event(event.id)
case .event(let event_id):
return .event(event_id)
case .external_url(let url):
return .reference(url.absoluteString)
}
}
static func from(tags: [[String]]) -> HighlightSource? {
for tag in tags {
if tag.count == 3 && tag[0] == "e" && tag[2] == HighlightSource.TAG_SOURCE_ELEMENT {
guard let event_id = NoteId(hex: tag[1]) else { continue }
return .event(event_id)
}
if tag.count == 3 && tag[0] == "r" && tag[2] == HighlightSource.TAG_SOURCE_ELEMENT {
guard let url = URL(string: tag[1]) else { continue }
return .external_url(url)
}
}
return nil
}
}
struct ShareContent {

View File

@@ -79,6 +79,7 @@ class HomeModel: ContactsDelegate {
var notifications = NotificationsModel()
var notification_status = NotificationStatusModel()
var events: EventHolder = EventHolder()
var already_reposted: Set<NoteId> = Set()
var zap_button: ZapButtonModel = ZapButtonModel()
init() {
@@ -122,6 +123,7 @@ class HomeModel: ContactsDelegate {
/// This is called whenever DamusState gets set. This function is used to load or setup anything we need from the new DamusState
func load_our_stuff_from_damus_state() {
self.load_latest_contact_event_from_damus_state()
self.load_drafts_from_damus_state()
}
/// This loads the latest contact event we have on file from NostrDB. This should be called as soon as we get the new DamusState
@@ -134,6 +136,10 @@ class HomeModel: ContactsDelegate {
process_contact_event(state: damus_state, ev: latest_contact_event)
}
func load_drafts_from_damus_state() {
damus_state.drafts.load(from: damus_state)
}
// MARK: - ContactsDelegate functions
func latest_contact_event_changed(new_event: NostrEvent) {
@@ -215,6 +221,10 @@ class HomeModel: ContactsDelegate {
break
case .status:
handle_status_event(ev)
case .draft:
// TODO: Implement draft syncing with relays. We intentionally do not support that as of writing. See `DraftsModel.swift` for other details
// try? damus_state.drafts.load(wrapped_draft_note: ev, with: damus_state)
break
}
}
@@ -250,7 +260,7 @@ class HomeModel: ContactsDelegate {
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: nwc_str),
let resp = await FullWalletResponse(from: ev, nwc: nwc) else {
let resp = await WalletConnect.FullWalletResponse(from: ev, nwc: nwc) else {
return
}
@@ -264,12 +274,24 @@ class HomeModel: ContactsDelegate {
guard resp.response.error == nil else {
print("nwc error: \(resp.response)")
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
WalletConnect.handle_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
return
}
if resp.response.result_type == .list_transactions {
Log.info("Received NWC transaction list from %s", for: .nwc, relay.absoluteString)
damus_state.wallet.handle_nwc_response(response: resp)
return
}
if resp.response.result_type == .get_balance {
Log.info("Received NWC balance information from %s", for: .nwc, relay.absoluteString)
damus_state.wallet.handle_nwc_response(response: resp)
return
}
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
nwc_success(state: self.damus_state, resp: resp)
WalletConnect.handle_zap_success(state: self.damus_state, resp: resp)
}
}
@@ -366,6 +388,8 @@ class HomeModel: ContactsDelegate {
boost_ev_id = inner_ev.id
Task {
// NOTE (jb55): remove this after nostrdb update, since nostrdb
// processess reposts when note is ingested
guard validate_event(ev: inner_ev) == .ok else {
return
}
@@ -385,7 +409,7 @@ class HomeModel: ContactsDelegate {
switch self.damus_state.boosts.add_event(ev, target: e) {
case .already_counted:
break
case .success(let n):
case .success(_):
notify(.update_stats(note_id: e))
}
}
@@ -394,7 +418,7 @@ class HomeModel: ContactsDelegate {
switch damus_state.quote_reposts.add_event(ev, target: target) {
case .already_counted:
break
case .success(let n):
case .success(_):
notify(.update_stats(note_id: target))
}
}
@@ -441,7 +465,7 @@ class HomeModel: ContactsDelegate {
let nwc = WalletConnectURL(str: nwc_str),
nwc.relay == relay_id
{
subscribe_to_nwc(url: nwc, pool: pool)
WalletConnect.subscribe(url: nwc, pool: pool)
}
case .error(let merr):
let desc = String(describing: merr)
@@ -723,6 +747,16 @@ class HomeModel: ContactsDelegate {
handle_quote_repost_event(ev, target: quoted_event.note_id)
}
// don't add duplicate reposts to home
if ev.known_kind == .boost, let target = ev.get_inner_event()?.id {
if already_reposted.contains(target) {
Log.info("Skipping duplicate repost for event %s", for: .timeline, target.hex())
return
} else {
already_reposted.insert(target)
}
}
if sub_id == home_subid {
insert_home_event(ev)
} else if sub_id == notifications_subid {

View File

@@ -77,11 +77,19 @@ enum MediaUpload {
}
}
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
protocol ImageUploadModelProtocol {
init()
func start(media: MediaUpload, uploader: any MediaUploaderProtocol, mediaType: ImageUploadMediaType, keypair: Keypair?) async -> ImageUploadResult
}
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject, ImageUploadModelProtocol {
@Published var progress: Double? = nil
func start(media: MediaUpload, uploader: MediaUploader, keypair: Keypair? = nil) async -> ImageUploadResult {
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self, keypair: keypair)
override required init() { }
func start(media: MediaUpload, uploader: any MediaUploaderProtocol, mediaType: ImageUploadMediaType, keypair: Keypair? = nil) async -> ImageUploadResult {
let res = await AttachMediaUtility.create_upload_request(mediaToUpload: media, mediaUploader: uploader, mediaType: mediaType, progress: self, keypair: keypair)
switch res {
case .success(_):
@@ -89,10 +97,17 @@ class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
self.progress = nil
UINotificationFeedbackGenerator().notificationOccurred(.success)
}
case .failed(_):
case .failed(let error):
DispatchQueue.main.async {
self.progress = nil
UINotificationFeedbackGenerator().notificationOccurred(.error)
if let nsError = error as NSError?,
nsError.domain == NSURLErrorDomain,
nsError.code == NSURLErrorCancelled {
print("Upload forced cancelled by user after Cancelling the Post, no feedback triggered.")
} else {
// Trigger feedback for all other errors
UINotificationFeedbackGenerator().notificationOccurred(.error)
}
}
}

View File

@@ -2,7 +2,7 @@
// LongformEvent.swift
// damus
//
// Created by Daniel Nogueira on 2023-11-24.
// Created by Daniel D'Aquino on 2023-11-24.
//
import Foundation

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