Compare commits

..

415 Commits

Author SHA1 Message Date
b6c811dedf Fix localization issues and export strings for translation
Changelog-Fixed: Fix localization issues and export strings for translation
2023-09-24 14:02:31 -04:00
Daniel D’Aquino
6d055be3cd Fix UI freeze after swiping back from profile (#1449)
On iOS 17.0, swiping back from a view that uses
`.navigationBarHidden(true)` caused the `NavigationStack` view to
freeze. This fixes the issue by creating a custom toolbar using
`.toolbar` instead.

Issue reproduction steps
------------------------

I found a very good clue to reproduce this issue from
[https://damus.io/note162rt4fctxepnj9cdtr9a82k7jtw3e33hj742ejly3q84tkwsfars4k9glr](https://damus.io/note162rt4fctxepnj9cdtr9a82k7jtw3e33hj742ejly3q84tkwsfars4k9glr)

**Device:** iPhone 13 mini (Physical device)
**iOS:** 17.0.1
**Damus:** 1.6 (18) 4377cf28ef
**Steps:**
1. Go to the home timeline view.
2. Click on a profile on any post
3. Swipe back to the home timeline view (Do not press "back" button)
4. Click on that same profile again

**Ideal behaviour:** On step 4, you should be taken to the profile view
**Actual behaviour:** The whole timeline view, top bar, etc seems to "freeze" and no longer respond.

Root causing investigation
--------------------------

I attempted various things until I could narrow it down. Here is a
summary of what I discovered:
1. First I attempted to investigate where the deadlock would live, by
   analyzing the thread states in the debugger. However:
    1. I did not find much differences between the thread states of a
       normal app running and the app running after the issue
    2. **I noticed that the tab bar at the bottom was still working, so
       unless those views are running on different threads, it might not
       have been a deadlock**
    3. NostrDB ingested and writer threads seemed to be waiting on a
       mutex most of the time I paused execution, but that also happened
       under normal conditions
    4. The crux of what made this difficult is that most of the UI
       related threads were in assembly, which was harder to interpret.
       However, the top of the stack in those threads were usually
       `mach_msg_trap`, which I believe is just the debugger
       interrupting execution. Below it, there were usually normal
       assembly instructions being run, such as `mov` and `ldp`
       instructions _(Move value and load a pair of registers)_, and
       stepping through some of those seemed to move the program
       counter. So I believe that the threads are running
    5. Running `thread info` on some of the threads (e.g. main) revealed
       that it seemed to be waiting on `mach_msg2_trap`, which again I
       believe is just the debugger pausing execution.
2. **After some more testing, I realized that swiping back only breaks
   when swiping back from the `ProfileView`, but not other views**
2. I tried to check if the issue was incorrect hashing of `Router`
   objects: `NavigationStack` uses `NavigationPath`, which needs a
   collection of hashable elements. I thought that if hashing was done
   incorrectly, the NavigationStack might have issues managing views.
   But that did not seem to be it either.
    1. I tried experimenting with the hashing logic for the Profile
       router. No changes
    2. I tried purposefully messing up with the hashing logic of a good,
       working view by adding random numbers into the hash. No issues on
       swiping out of that view either.
3. That lead me to the possibility that the issue is within the
   `ProfileView` body. I commented parts of the code out and tested each
   portion of it in a binary search fashion, and narrowed it down a
   specific line:

```
.navigationBarHidden(true)
```

Whenever I remove this line or set this to `false`, the freezing no
longer occurs. According to the Apple developer docs, this is
deprecated:
[https://developer.apple.com/documentation/swiftui/view/navigationbarhidden(_:)](https://developer.apple.com/documentation/swiftui/view/navigationbarhidden(_:))

I tried to replace it with its newer replacement: `.toolbar(.hidden,
for: .automatic)`, but that also causes the freeze.

So, just removing that line fixes the freeze, however it breaks the
layout by showing the unwanted back button.

Fix
---

I was able to fix it by implementing the custom toolbar under `.toolbar`
modifier, and hiding the back button (as opposed to the whole nav bar)

Testing of the fix
------------------

**PASS**

**Device:** iPhone 13 mini (Physical device)
**iOS:** iOS 17.0.1
**Damus:** This commit

**Special remarks:** Some entitlements removed locally to be able to
build to device without access to development certificate

**Test steps:** Same as reproduction steps

**Results:** Swiping back from profile does not cause any issues. View
layout of the custom navbar is unaltered in appearance.

iOS 16 smoke test
-----------------

**PASS**

**Device:** iPhone 14 Pro simulator
**iOS:** 16.4
**Damus:** This commit
**Special remarks:** Same as test above

**Test steps:** Same as reproduction steps. However here we are not
checking the freezing (as it was not reproducible in the simulator). We
are checking that the changes did not break navigation, nor layout, nor
caused any build issues.

**Results:** Working as expected

Closes: https://github.com/damus-io/damus/issues/1449
Changelog-Fixed: Fix UI freeze after swiping back from profile (#1449)
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-23 08:03:32 -07:00
William Casarin
01c6e3e9ab v1.6 (18) changelog 2023-09-21 18:08:56 -04:00
William Casarin
4377cf28ef v1.6 (18) 2023-09-21 18:05:59 -04:00
William Casarin
6f67c159ff ndb: switch to case-insensitive profile searches 2023-09-21 18:03:22 -04:00
William Casarin
7a85ae29ca search: switch to nostrdb profile searching
Changelog-Changed: Switch to nostrdb for @'s and user search
2023-09-21 13:19:22 -04:00
William Casarin
fafe3b4b3e ndb: add nostrdb migrations 2023-09-21 09:10:06 -04:00
William Casarin
69c7acea76 tests: add ndb support to tests
stops it from crashing
2023-09-21 09:10:06 -04:00
William Casarin
22d635d850 ndb: don't verify flatbuffers in release builds 2023-09-21 09:10:06 -04:00
William Casarin
fc9b9f2940 ndb: switch profile queries to use transactions
this should ensure no crashing occurs when querying profiles
2023-09-21 09:10:06 -04:00
William Casarin
622a436589 ndb: add NdbTxn transaction class
This will be used for transactions
2023-09-21 09:10:06 -04:00
William Casarin
9398877415 nostrdb/c: update to include transaction support 2023-09-21 09:10:06 -04:00
William Casarin
129d3ff101 ids: introduce NoteKey
These will be used to reference nostr notes from nostrdb
2023-09-21 09:10:06 -04:00
William Casarin
bb4fd75576 nostrdb: add profiles to nostrdb
This adds profiles to nostrdb

- Remove in-memory Profiles caches, nostrdb is as fast as an in-memory cache
- Remove ProfileDatabase and just use nostrdb directly

Changelog-Changed: Use nostrdb for profiles
2023-09-21 09:10:06 -04:00
Daniel D’Aquino
8586eed635 ui: add followed hashtags to FollowingView
When users view who a certain person follows, now they will see an extra
tab to see the hashtags that they follow.

This new tab contains a list of followed hashtags, each of which
includes an option to follow/unfollow the hashtag, as well as the
ability to visit the hashtag timeline

Testing

**iOS:** 17.0 (iPhone 14 Pro Simulator)
**Damus:** (This commit)
**Test steps:**
1. Go to search view, search for a couple of hashtags: #apple, #orange, #kiwi
2. Go to the test accounts own profile via the drawer menu
3. Click on "Following". Make sure there are two tabs now.
4. Scroll down, switch tabs between "People" and "Hashtags". Make sure that scrolling and switching tabs work
5. Unfollow and follow a user. Make sure that this still works
6. Make sure that #apple, #orange, #kiwi hashtags are visible under the "Hashtags" tab
7. Unfollow "#kiwi". Check that the button label now switches from "Unfollow" to "Follow"
8. Click on "#kiwi". Make sure that it takes you to the page where posts with that hashtag appears
9. Go to @jb55's profile
10. Click on "Following"
11. Ensure that there is a "Hashtags" tab
12. Check that @jb55's followed hashtags are shown (not your own)
13. Follow one of the same hashtags as @jb55's
14. Go back to your own profile and go to your own following view again.
15. Make sure that this newly added tag is present on the list, and that #kiwi is not.

Closes: https://github.com/damus-io/damus/issues/606
Changelog-Added: Add followed hashtags to your following list
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-21 09:10:06 -04:00
William Casarin
440e37c1d3 filters: generalize ContentFilter
This simplifies our content filters so that it is a bit more flexible
for future additions.

Fixes: 0957cc896cc8 ("Add "Do not show #nsfw tagged posts" setting")
2023-09-21 09:10:06 -04:00
Daniel D’Aquino
49283f2bb2 filters: add "Do not show #nsfw tagged posts" setting
This commit adds a setting where the user can choose to hide notes with
a #nsfw hashtag. This setting was implemented to allow users to filter
out adult or other unsafe content.

I moved the code logic for content filtering into a new file, and
defined a protocol for content filters. Although the logic is still
simple, this might help in developing a flexible API in case we have
more complex filtering needs in the future.

I also modified the name of the "Appearance" setting to "Appearance and
filters", to make it easier for users to intuitively find this setting.
(Note: Re-translations of this string might be necessary)

**PASS**
**iOS:**
- iOS 17.0 (iPhone 14 Pro)

**Damus:** (This commit)
**Steps:**
1. Follow another account that you control (Account B)
2. On account B, post a note saying "#test this is a test". This note should show up on the home feed.
3. On account B, post a note saying "#nsfw this is a test". This note should NOT show up on the home feed
4. Go to settings and disable the NSFW filter. Go back to the home view. The #nsfw post should now show up.
5. Close app and reopen. NSFW post should still show up (i.e. Setting should be persistent)
6. Unfollow account B
7. Close app and reopen.
8. Follow the "#grownostr" hashtag
9. Turn on the NSFW filter
10. On account B, post a note saying "#grownostr this is a test". This note should show up on the home view.
11. On account B, post a note saying "#grownostr #nsfw this is a test". This note should NOT show up.
12. Double-check the "notes and replies" tab. Note should NOT show up there either.
12. Turn off NSFW filter
13. Note from step 11 should now show up.
14. Go to Universe view and find a post with a hashtag. Remember where the post is.
14. Locally change the tag keyword from "nsfw" to that hashtag (Note: I had to test this way because my posts were not showing up in the Universe view)
15. Turn off the filter. Check post is there, in the Universe view.
16. Turn on the filter. Check post is no longer there in the Universe view. (Check the neighboring posts are the same, to make sure)
17. Bring back the code to its normal state.
18. Search for "#nsfw". Make sure that #nsfw appears (I believe this is ok, because it means the person is purposefully searching for it)

Closes: https://github.com/damus-io/damus/issues/1412
Changelog-Added: Add "Do not show #nsfw tagged posts" setting
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-21 09:10:06 -04:00
William Casarin
305ee03b0e relays: fix tld extraction performance issues
This uses a simpler variant that doesn't require a library. It is also
much faster and doesn't cause a delay when you open the relay view.

Not sure why I needed to touch other parts of the code to make the build
work. Probably xcode beta thing?
2023-09-21 09:10:06 -04:00
William Casarin
a88f5db10b Revert "deps: add tldextract"
This reverts commit 4263b9690f.
2023-09-21 08:48:20 -04:00
Suhail Saqan
d39a3da3b7 util: add separate_images and separate_invoices 2023-09-21 08:37:42 -04:00
ericholguin
40459e247e relays: user relay design 2023-09-16 14:15:27 -05:00
ericholguin
fff4549933 relays: remove usage of show action button binding 2023-09-16 14:15:27 -05:00
ericholguin
c4dfae9ede relays: update relay view to use new design
Changelog-Changed: Updated relay view
Closes: https://github.com/damus-io/damus/pull/1543
2023-09-16 14:15:27 -05:00
Daniel D’Aquino
bfda0d1b74 ui: increase size of the hitbox on note ellipsis button
Changelog-Changed: Increase size of the hitbox on note ellipsis button
Closes: https://github.com/damus-io/damus/issues/1454
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-16 14:15:20 -05:00
Daniel D’Aquino
01b8e43a6e compose: fix text wrapping issue when mentioning npub
Closes: https://github.com/damus-io/damus/issues/1211
Changelog-Fixed: Fix text composer wrapping issue when mentioning npub
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-16 13:58:27 -05:00
Jon Marrs
aa4ecc2139 test: add test cases for ASCII and UTF-8 characters in hashtags
Closes: https://github.com/damus-io/damus/pull/1546
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-15 12:31:17 -05:00
Jon Marrs
617dee3e6b damus-c: remove UTF-8 punctuation from hashtags
Check for UTF-8 punctuation (such as ellipsis) in addition to regular punctuation in hashtags.

Closes: https://github.com/damus-io/damus/issues/1518

Closes: https://github.com/damus-io/damus/pull/1546
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-15 12:31:17 -05:00
Daniel D’Aquino
510432bb98 ui: make blurred videos viewable by allowing blur to disappear once tapped
Closes: https://github.com/damus-io/damus/issues/1247
Changelog-Fixed: Make blurred videos viewable by allowing blur to disappear once tapped
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-15 12:31:05 -05:00
Jericho Hasselbush
c4a9f2fdb2 ui: hold tap to preview status URL
Applied a WKWebkitView inside a .contextMenu to show preview status for
URL links in user status messages.

Closes: https://github.com/damus-io/damus/issues/1523
Changelog-Added: Hold tap to preview status URL
Signed-off-by: Jericho Hasselbush <jericho@sal-et-lucem.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-15 12:30:26 -05:00
Daniel D’Aquino
b1e0a62109 nwc: fix parsing issue with NIP-47 compliant NWC urls without double-slashes
Closes: https://github.com/damus-io/damus/issues/1547
Changelog-Fixed: Fix parsing issue with NIP-47 compliant NWC urls without double-slashes
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-13 12:59:21 -06:00
William Casarin
1fc5ceff3b relays: fix withAnimation on older versions
Maybe this is an iOS17 thing?
2023-09-13 05:41:08 -07:00
William Casarin
16edc3fe13 relays: bouncy edit animation 2023-09-11 14:39:44 -07:00
William Casarin
6a88ca2777 relays: fix crash in new RelayPicView 2023-09-11 14:36:08 -07:00
William Casarin
e3ccf95780 ui: fix padding of username next to pfp on some views
Changelog-Fixed: Fix padding of username next to pfp on some views
2023-09-11 14:36:08 -07:00
Bryan Montz
9bac83352b ui: improve bottom spacing for ImageView's tab indicator dots
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-11 14:36:08 -07:00
Bryan Montz
0803594553 ui: make ImageView's tab indicator dots tappable
Changelog-Changed: Make carousel tab dots tappable
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-11 14:35:50 -07:00
Jericho Hasselbush
8dad8e6703 posting: fix issue with username and multiple emojis
Fixes issue where username with multiple emojis would place cursor in
strange position. Now properly moves the cursor to space past the
multiple emoji user name.

Any amount would be great. Not a complex issue to fix!

Tipjar: lnbc1pj0eddtpp5km07jgrfm47nfswqqp33ngv374gzad2hshkra7zm3l0cmpusnp3qdqqcqzzsxqyz5vqsp5rklkzj9upf32z3c3nmc9xg4pdlz5p5mp3s332ygefexf79tq8ucs9qyyssqxfh4kz3sg9zczsnj49w23aw35z87jwyx9m5su8kkyxlspyjk4ajy7vhxuw2rzw4lz8vfutfakm2rggvpzhzs9ehfus4nl683dl99f4sqgm9zkq
Changelog-Fixed: Fixes issue where username with multiple emojis would place cursor in strange position.
Signed-off-by: Jericho Hasselbush <jericho@sal-et-lucem.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-11 07:48:36 -07:00
William Casarin
e30d38e69f router: use tap gestures instead of nav links
I was hoping this would fix something but it did not
2023-09-10 18:27:37 -07:00
William Casarin
c13f29e98c router: hash bytes for a quick sanity check
probably a no-op
2023-09-10 18:27:37 -07:00
William Casarin
5b901656f3 perf: fix weird lag when switching timelines 2023-09-10 18:25:35 -07:00
William Casarin
36acdf420e perf: remove zstack on profile pictures
helps a bit? I think?
2023-09-10 18:25:35 -07:00
William Casarin
76a6dbc406 perf: remove unused zstack on like button 2023-09-10 18:25:35 -07:00
William Casarin
1b1d4bd6d1 perf: use plain images for actionbar buttons
The action bar is really slow to render for some reason, start
removing stuff
2023-09-10 18:25:35 -07:00
William Casarin
14586b616c log: remove some verbose preload logs 2023-09-10 18:25:35 -07:00
ericholguin
7baf7e66dc relays: add relay pic view for displaying relay icons 2023-09-10 09:54:35 -07:00
William Casarin
4263b9690f deps: add tldextract
This is needed for the new relay view
2023-09-10 09:52:54 -07:00
Suhail Saqan
7f6a702412 emojis: make width dynamic and font bigger
add calculateOverlayWidth to support this

Closes: https://github.com/damus-io/damus/pull/1542
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-09 10:24:14 -07:00
ericholguin
6f35de65f9 relays: add icon field to metadata 2023-09-09 09:48:18 -07:00
ericholguin
65f3651896 ui: dont display globe image for free relay types 2023-09-09 09:45:45 -07:00
ericholguin
94ce604b9d components: add neutral button style component 2023-09-09 09:45:16 -07:00
ericholguin
b934d66f64 components: add lighter gradient 2023-09-09 09:45:16 -07:00
ericholguin
20b6627799 colors: add variables for the new color assets 2023-09-09 09:45:16 -07:00
ericholguin
3e15f15a57 colors: add color sets from figma 2023-09-09 09:45:15 -07:00
petrikaj
5c87b8e610 transations: add finnish translation
Changelog-Added: Finnish translations
Closes: https://github.com/damus-io/damus/pull/1535
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-07 10:33:37 -07:00
William Casarin
42234b1cf3 remove timeline render logs 2023-09-07 10:33:37 -07:00
Bryan Montz
54ba64535d video: remove GSPlayer dependency
Changelog-Fixed: Fixed audio in video playing twice
Closes: https://github.com/damus-io/damus/pull/1539
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-07 10:33:34 -07:00
Bryan Montz
9cf53a9e93 video: remove VideoPlayer and switch to VideoController for cache
Closes: https://github.com/damus-io/damus/pull/1539
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-07 10:33:31 -07:00
Bryan Montz
3569da5687 video: switch player to use new view model
pass VideoController through containing views

Closes: https://github.com/damus-io/damus/pull/1539
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-07 10:33:19 -07:00
Bryan Montz
f1f3abfb98 video: add DamusVideoPlayerViewModel
Closes: https://github.com/damus-io/damus/pull/1539
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-07 10:33:14 -07:00
Bryan Montz
dec07df2c1 video: add VideoController, which hold cached metadata and mute states
Closes: https://github.com/damus-io/damus/pull/1539
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-07 10:33:12 -07:00
Bryan Montz
53734ea483 video: add AVPlayerView, a simple wrapper for AVPlayerViewController
Closes: https://github.com/damus-io/damus/pull/1539
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-07 10:33:07 -07:00
Grimless
b18a0c573e profile: move the "Follow you" badge into the profile header
Move the "Follow you" badge into the profile header he profile header
out-of-line with the often long and already space-constrained
username/display name text

Changelog-Changed: Move the "Follow you" badge into the profile header
Closes: https://github.com/damus-io/damus/pull/1529
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-03 18:02:54 -07:00
Grimless
f6f7d13f12 Properly implement top-level tests and fix one test using the wrong Block conversion property
Closes: https://github.com/damus-io/damus/pull/1528
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-03 18:02:32 -07:00
Grimless
6ee0be40e9 Create helper extensions for Block and update tests for the Block helper model
Closes: https://github.com/damus-io/damus/pull/1528
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-03 18:02:32 -07:00
Grimless
a64f898df7 Move the Block helper type to its own file, collapse the various standalone functions for parsing block data, and refactor consumers to initialize a Block with given data and access its members as needed.
Closes: https://github.com/damus-io/damus/pull/1528
Signed-off-by: William Casarin <jb55@jb55.com>
2023-09-03 18:02:32 -07:00
Jon Marrs
dd29e87146 test: pass keypair instead of privkey for test cases
Tests were not building due to recent changes in the Damus source code that replaced privkey with keypair. This patch extends those changes to the test cases, allowing the tests to build and pass.

Signed-off-by: Jon Marrs <jdmarrs@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-31 08:52:28 -07:00
William Casarin
c71b0ee916 blocks: pass keypair instead of privkey to avoid pubkey gen
Generating a pubkey is quite slow, so pass a keypair instead of privkey
2023-08-28 11:47:29 -07:00
William Casarin
8e92e28faf test: optionally remove lmdb db
otherwise tests fail on CI
2023-08-28 10:34:37 -07:00
William Casarin
5657512370 ndb: restore escaped slash fix 2023-08-28 10:09:49 -07:00
William Casarin
882f6e2534 ndb: update nostrdb, fix alignment issues 2023-08-28 08:19:03 -07:00
William Casarin
2f60888fb1 ndb: remove patch from copy script, just use sed 2023-08-28 08:18:25 -07:00
William Casarin
ba6792640d flatbuffers: update bindings, add verifier 2023-08-28 08:17:25 -07:00
William Casarin
984c7b6932 ndb: ensure profile flatbuffers are not copied
These are pointers into LMDB's virtual memory map of the database. No
copy required.
2023-08-28 08:00:45 -07:00
William Casarin
0bbc2c6348 ndb: save in documents instead of cache dir
This is more long term storage
2023-08-28 08:00:45 -07:00
William Casarin
c44c0d0863 profile: remove deleted flag
it's not used anymore
2023-08-28 08:00:45 -07:00
William Casarin
50d55572be Fix crash when long pressing custom reactions
Changelog-Fixed: Fix crash when long pressing custom reactions
2023-08-28 08:00:45 -07:00
William Casarin
caffa0398b nostrdb: profile flatbuffers in nostrdb working! 2023-08-26 20:46:42 -07:00
William Casarin
92bbc9766d project: disable compile warnings for lmdb and nostrdb 2023-08-26 20:46:42 -07:00
William Casarin
699f77d9e1 add extended virtual memory entitlement
This will allow larger nostrdb databases
2023-08-26 20:46:42 -07:00
William Casarin
4c0166bd31 add swift flatbuffers 2023-08-26 20:46:42 -07:00
William Casarin
35b67dc08d nostrdb: initial Ndb class 2023-08-26 17:11:41 -07:00
William Casarin
1f5f1e28a4 nostrdb: pull latest, adding flatcc and lmdb 2023-08-25 19:05:34 -07:00
William Casarin
f30f93f65c Revert "Move the Block helper type to its own file"
This fixes the broken tests

This reverts commit 286ae68fd6.
2023-08-25 19:05:34 -07:00
William Casarin
7255481705 v1.6 (17) changelog 2023-08-23 17:49:30 -07:00
William Casarin
16fa701509 v1.6 (17) 2023-08-23 17:48:32 -07:00
William Casarin
2c6999e15c status: support clickable status urls
Changelog-Added: Add support for status URLs
2023-08-23 17:46:31 -07:00
William Casarin
981d500c25 status: click music urls to display in spotify
Changelog-Added: Click music statuses to display in spotify
2023-08-23 17:17:53 -07:00
William Casarin
d02fc9142d status: add settings for disabling statuses in the UI
Suggested-by: Tanel
Changelog-Added: Add settings for disabling user statuses
2023-08-23 16:43:55 -07:00
William Casarin
db59f74970 status: add missing status to some thread event views 2023-08-23 16:31:10 -07:00
William Casarin
bf3ca4a186 status: truncate statuses to a single line
Changelog-Fixed: Fix long status lines
2023-08-23 16:23:18 -07:00
William Casarin
53c2b3a48d status: clear statuses if they only contain whitespace
Changelog-Changed: clear statuses if they only contain whitespace
2023-08-23 16:19:19 -07:00
William Casarin
23a8d6fb6b status: fix status events not expiring locally
Changelog-Fixed: Fix status events not expiring locally
2023-08-23 16:11:48 -07:00
William Casarin
042b7da315 status: ignore processing expired events 2023-08-23 15:56:41 -07:00
William Casarin
e62ba5826b v1.6-16 changelog 2023-08-23 13:31:41 -07:00
William Casarin
1d11bb40b5 v1.6 (16) 2023-08-23 13:30:38 -07:00
William Casarin
0338297bfe Live Music & Generic Statuses
Changelog-Added: Added live music statuses
Changelog-Added: Added generic user statuses
2023-08-23 13:26:55 -07:00
William Casarin
59cf8056bd sidemenu: split out profile section
We will be adding to this and it is getting messy
2023-08-23 09:52:50 -07:00
William Casarin
d34d417fcc home: collapse guard statement
small nit refactor
2023-08-23 09:27:09 -07:00
William Casarin
b665a40a11 fix build 2023-08-23 09:25:47 -07:00
gladiusKatana
5caa4a6e97 videos: improve precision & sensitivity of auto-pause mechanism
Closes: https://github.com/damus-io/damus/pull/1308
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-23 09:24:13 -07:00
Grimless
c5d8e4a4a1 Simplify and inline Report event logic.
Closes: https://github.com/damus-io/damus/pull/1498
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-23 09:13:38 -07:00
tappu75e@duck.com
8b600a9774 Avoid notification for zap from mute profiles
Changelog-Fixed: Avoid notification for zaps from muted profiles
Closes: https://github.com/damus-io/damus/pull/1494
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-23 09:10:22 -07:00
Grimless
286ae68fd6 Move the Block helper type to its own file
Collapse the various standalone functions for parsing block data, and
refactor consumers to initialize a Block with given data and access its
members as needed.

Closes: https://github.com/damus-io/damus/pull/1496
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-21 17:11:43 -07:00
William Casarin
6ab893a617 profile: remove redundant view builder 2023-08-21 13:26:33 -07:00
William Casarin
9bfb59c4cc docs: people like centralized tools
This was confusing people, make it clear that github PRs are fine
2023-08-21 13:26:33 -07:00
Daniel D’Aquino
dcb94635ea Fix text editing issues on characters added right after mention link
Changelog-Fixed: Fix text editing issues on characters added right after mention link
Closes: https://github.com/damus-io/damus/issues/1375
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Tested-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-20 17:25:06 -07:00
Fishcake
c464a26151 use nostr.build api v2 with optional nip98 support
Closes: https://github.com/damus-io/damus/pull/1471
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-20 16:29:33 -07:00
Fishcake
9104ddb051 add function to create nip98 http authorization header
Closes: https://github.com/damus-io/damus/pull/1471
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-20 16:29:33 -07:00
Fishcake
1432087edf add nostr event 27235 (nip-98)
Closes: https://github.com/damus-io/damus/pull/1471
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-20 16:29:33 -07:00
William Casarin
ae2f7255a7 Mute hellthreads everywhere
Changelog-Fixed: Mute hellthreads everywhere
Fixes: https://damus.io/note1rn3ckl76myga6xcefr0le52d8czd0wqe8apguewqknyv7m55mmpq3rv3hv
2023-08-20 11:45:25 -07:00
William Casarin
d5b944170f actually build 15 because reasons 2023-08-20 11:25:01 -07:00
William Casarin
9fb1cc5b57 v1.16 (13) changelog 2023-08-18 11:20:51 -07:00
William Casarin
2e512317e7 v1.6 (13) 2023-08-18 10:10:18 -07:00
tappu75e@duck.com
f9eb669132 replies: fix bug where it would sometimes show -1
Changelog-Fixed: Fix bug where it would sometimes show -1 in replies
Closes: https://github.com/damus-io/damus/pull/1476
2023-08-18 08:41:21 -07:00
Daniel D‘Aquino
066b3cdde8 Fix image links appearing with escaped slashes
Changelog-Fixed: Fix images and links occasionally appearing with escaped slashes
Closes: https://github.com/damus-io/damus/issues/1468
Signed-off-by: Daniel D‘Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
Rewarded-sats: 50000
2023-08-18 08:41:21 -07:00
William Casarin
7f313dcbd4 nostrscript: add comment about iOS virtual memory allocs
I'm really just doing this because I forgot a changelog entry

Changelog-Fixed: Fixed nostrscript not working on smaller phones
2023-08-18 08:41:21 -07:00
William Casarin
1dabd88355 nostrscript: reduce size of wasm page allocation
smaller phones don't like this
2023-08-11 07:47:15 -07:00
Suhail Saqan
4f33641244 change button scale effect
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-11 07:08:44 -07:00
William Casarin
006a6ef16f Show possibly invalid zaps if we don't have the event in cache
Changelog-Fixed: Fix zaps sometimes not appearing
2023-08-10 10:32:57 -07:00
William Casarin
7467a9d5b1 Fix empty lines in profile and reposting-the-wrong-thing bugs
Changelog-Fixed: Fixed issue where reposts would sometimes repost the wrong thing
Changelog-Fixed: Fixed issues where sometimes there would be empty entries on your profile
2023-08-09 09:16:29 -07:00
William Casarin
9f01cab2be simplify reduce_text_block 2023-08-08 17:09:45 -07:00
William Casarin
502917012c v1.6 (11) changelog 2023-08-07 08:46:23 -07:00
William Casarin
916f7d789e v1.6 (11) 2023-08-07 08:45:07 -07:00
William Casarin
21eda288c4 timeline: show renotes in Notes timelines
Changelog-Changed: Show renotes in Notes timeline
Fixes: https://github.com/damus-io/damus/issues/676
2023-08-07 08:24:02 -07:00
William Casarin
25e022d933 reply: ensure the person you're replying to is the first entry in the reply description
Suggested-by: Tanel
Changelog-Fixed: Ensure the person you're replying to is the first entry in the reply description
2023-08-06 15:37:32 -07:00
William Casarin
e642913944 notifications: don't cutoff text
Changelog-Fixed: don't cutoff text in notifications
2023-08-06 14:58:32 -07:00
cr0bar
967785392f note: fix paragraphs not appearing on iOS17
In some edge cases, the inflated UiTextView didn't render properly
causing a black screen which needed the user to scroll. Dropped the
inflate size and now only set where selectedTextHeight is .zero, seems
more reliable.

Closes: https://github.com/damus-io/damus/pull/1427
Changelog-Fixed: Fix paragraphs not appearing on iOS17
2023-08-06 14:23:11 -07:00
William Casarin
9e6fbeefcd url: smartparens hack
support urls like (https://jb55.com/something)
2023-08-06 14:16:43 -07:00
William Casarin
de58e52199 dms: move timestamp outside of bubble 2023-08-06 14:07:04 -07:00
William Casarin
53e9269da6 urls: fix wikipedia url detection with parenthesis
Fixes: f0df4aa218 ("Strip common punctuations from URLs")
Fixes: https://github.com/damus-io/damus/issues/1027
Closes: https://github.com/damus-io/damus/pull/1063
Changelog-Fixed: Fix wikipedia url detection with parenthesis
2023-08-06 13:53:28 -07:00
Joel Klabo
85930df8e3 tests: add url parens tests 2023-08-06 13:51:39 -07:00
William Casarin
cf3a9a576d test: move existing url tests to UrlTests 2023-08-06 13:50:20 -07:00
William Casarin
e397fc069b make: add tags target 2023-08-06 13:50:20 -07:00
William Casarin
2529797dfb todo: add local todo helper 2023-08-06 13:50:20 -07:00
William Casarin
bd2193251f build: fix some build issues with the last revert
Fixes: 1a2ac976a3 ("Fix old notifications always appearing on first start")
2023-08-06 11:30:28 -07:00
William Casarin
1a2ac976a3 Fix old notifications always appearing on first start
Revert "home: debounce last notified"

This is technically incorrect, as debouncing can prevent saving
important events.

The proper way to do this is to save it locally in memory, and then
debouncing the saving itself. Will do this soon.

Reverts: a9b4cfd424
Fixes: https://github.com/damus-io/damus/issues/1439
Changelog-Fixed: Fixed old notifications always appearing on first start
2023-08-06 09:22:28 -07:00
William Casarin
d4faacb99f relays: strip trailing / from relay urls
Fixes: https://github.com/damus-io/damus/issues/1443
Changelog-Fixed: Fix issue with slashes on relay urls causing relay connection problems
2023-08-06 09:07:33 -07:00
William Casarin
a73271e3d4 debug: remove note size debug
ThreadSanitizer was complaining about a data race
2023-08-06 09:07:33 -07:00
William Casarin
624a7b4e88 notifications: fix rare crash with local notification
This shouldn't happen, but I found a log that crashed here, so we will
fix this anyways.

Changelog-Fixed: Fix rare crash triggered by local notifications
2023-08-06 08:33:51 -07:00
William Casarin
5b9803d234 script: add build-git-hash.txt build output
Otherwise we get warnings
2023-08-06 07:54:23 -07:00
William Casarin
3098d4b4fa bar: fix crash when long pressing emoji selection
Changelog-Fixed: Fix crash when long-pressing reactions
2023-08-06 07:10:01 -07:00
William Casarin
0178478199 decoding: fix decoding of large events like nostr reports
I was trying to do an initial malloc that was somewhat efficient. Looks
like our ndb_builder needs a bit more space when allocating the
ndb_note.

Changelog-Fixed: Fixed nostr reporting decoding
2023-08-06 06:56:24 -07:00
William Casarin
d489bcc586 test: add test for failing nostr report event 2023-08-06 06:56:24 -07:00
William Casarin
453d540255 search: find_event_with_subid
I needed this to find a bug in event decoding
2023-08-06 06:56:24 -07:00
Suhail Saqan
5ded564bdc settings: change settings order: Reactions -> Developer
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-05 18:48:44 -07:00
Suhail Saqan
3908192fe2 reactions: add close button to custom reactions
Signed-off-by: William Casarin <jb55@jb55.com>
Changelog-Added: Add close button to custom reactions
2023-08-05 18:48:35 -07:00
Suhail Saqan
92020e551b reactions: add ability to change order of emojis
Signed-off-by: William Casarin <jb55@jb55.com>
Changelog-Added: Add ability to change order of custom reactions
2023-08-05 18:48:30 -07:00
Suhail Saqan
ccd52a09d8 reactions: remove some left padding from add and remove buttons
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-05 18:48:24 -07:00
Suhail Saqan
ced3c76996 reactions: only allow copy emoji when editing
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-05 18:48:16 -07:00
Suhail Saqan
29bba15230 qr: dismiss qrcode fullScreenCover on scan
Signed-off-by: William Casarin <jb55@jb55.com>
Changelog-Fixed: Dismiss qr screen on scan
2023-08-05 18:48:11 -07:00
Suhail Saqan
fb179ac1d4 qr: show QRCameraView regardless of same user
Signed-off-by: William Casarin <jb55@jb55.com>
Changelog-Fixed: Show QRCameraView regardless of same user
2023-08-05 18:48:11 -07:00
Suhail Saqan
7900865c02 bar: wiggle long press reactions
Signed-off-by: William Casarin <jb55@jb55.com>
Changelog-Fixed: Fix wiggle when long press reactions
2023-08-05 18:48:01 -07:00
Suhail Saqan
0350809e82 bar: fix reaction button breaking scrolling
Signed-off-by: William Casarin <jb55@jb55.com>
Changelog-Fixed: Fix reaction button breaking scrolling
2023-08-05 18:44:54 -07:00
Bryan Montz
cddb88b890 fix: crash when muting threads
Fixes a crash when the user mutes a thread. UserDefaults didn't know how
to serialize a NoteId for storage, so we'll convert it to the hex id
first.

Changelog-Fixed: Crash when muting threads
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-04 09:35:35 -07:00
William Casarin
14ba33674b setting: adjustable font size for jack the zapper
Changelog-Added: Adjustable font size
2023-08-03 18:38:20 -07:00
William Casarin
c0f4e3fe03 v1.6 (9) 2023-08-03 17:25:52 -07:00
William Casarin
dae2e8ef56 Revert "Fix for missing bottom half of a note"
This reverts commit 39dce64131.
2023-08-03 17:23:53 -07:00
William Casarin
b2d2fbee0d v1.6-8 changelog 2023-08-03 13:36:24 -07:00
William Casarin
cebd1f48ca ndb: switch to nostrdb notes
This is a refactor of the codebase to use a more memory-efficient
representation of notes. It should also be much faster at decoding since
we're using a custom C json parser now.

Changelog-Changed: Improved memory usage and performance when processing events
2023-08-03 13:20:36 -07:00
William Casarin
55bbe8f855 disable nostrscript test for now 2023-08-03 13:15:32 -07:00
cr0bar
39dce64131 Fix for missing bottom half of a note
Strange fix, but by increasing the height of a UiTextView past the size
of any legitimate content, then re-sizes back to the correct size
displaying the full content.

Changelog-Fixed: Fixed disappearing text on iOS17
2023-08-03 12:34:18 -07:00
William Casarin
b556257edd util: add structured logger 2023-08-03 12:17:56 -07:00
Daniel D‘Aquino
cdc4a7b7a4 Fix UTF support for hashtags
Changelog-Fixed: Fix UTF support for hashtags
Closes: https://github.com/damus-io/damus/issues/1411
Signed-off-by: Daniel D‘Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-03 12:17:32 -07:00
Daniel D‘Aquino
ef5a3030a6 Add unit tests surrounding creation of posts with non-latin hashtags, as well as the rendering of non-latin hashtag
Signed-off-by: Daniel D‘Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-03 12:17:32 -07:00
Daniel D‘Aquino
f0b8dcc5e9 Split view previews in NoteContentView to make both variants visible
Signed-off-by: Daniel D‘Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-03 12:17:32 -07:00
Daniel D‘Aquino
72b60573de Fix compilation error on test target in UserSearchCacheTests
Changelog-Fixed: Fix compilation error on test target in UserSearchCacheTests
Signed-off-by: Daniel D‘Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-08-03 12:17:32 -07:00
William Casarin
6e6c1eb7b6 ndb: make AsciiCharacter a CustomStringConvertible 2023-08-01 21:53:19 -07:00
William Casarin
07dfa3b1fb ndb: update nostrdb
This include various fixes for parsing and key decoding
2023-08-01 21:53:19 -07:00
William Casarin
88306d00a3 key: generate a FullKeypair when generating new keys 2023-08-01 21:53:19 -07:00
William Casarin
616de2eebc state: improve damus state init
It's a bit cleaner now
2023-08-01 21:53:19 -07:00
William Casarin
709aab549b nav: fix nav crashes and buggyness
just use the hashable for equality

Changelog-Fixed: Fix nav crashing and buggyness
2023-08-01 21:53:05 -07:00
William Casarin
15ab9f7135 scroll: allow any hashable target 2023-08-01 21:52:23 -07:00
William Casarin
d4aa8a5602 config: show git hash in version info
This will be useful for sanity checks and bisecting
2023-08-01 09:29:09 -07:00
William Casarin
a9b4cfd424 home: debounce last notified
Calling UserDefaults fast in a loop is not good
2023-07-31 05:38:19 -07:00
William Casarin
2b99f94d13 profiledb: disable database lookups for now
This is causing extremely bad lag in the UI
2023-07-31 05:38:19 -07:00
William Casarin
66e204eb91 notifications: don't do expensive id calculation 2023-07-31 05:38:19 -07:00
William Casarin
7040235605 refactor: add Pubkey, Privkey, NoteId string aliases
This is a non-behavioral change in preparation for the actual switchover
from Strings to Ids. The purpose of this kit is to reduce the size of
the switchover commit which is going to be very large.
2023-07-31 05:38:19 -07:00
William Casarin
f9d21ef901 test: rename test_event to test_note 2023-07-31 05:38:19 -07:00
William Casarin
a08d0a5a19 ndb: more id transition helpers 2023-07-31 04:08:07 -07:00
William Casarin
ff20cc4767 tests: enable code coverage 2023-07-31 03:25:50 -07:00
William Casarin
aacb336002 Update Translations 2023-07-30 11:57:18 -07:00
William Casarin
b40c595a7c notify: switch over to new typesafe notifications 2023-07-30 11:02:44 -07:00
William Casarin
80063af19a notify: add typesafe notifications 2023-07-30 11:02:44 -07:00
William Casarin
df3b94a1fc notify: add typesafe notify class 2023-07-30 11:02:44 -07:00
William Casarin
06a66a3709 add some type aliases to make the ndb move more incremental 2023-07-30 10:52:02 -07:00
William Casarin
1463ce5e3a profile: don't notify on notice
this is just a waste of cpu at this point and could cause main thread
blocking issues
2023-07-30 10:52:02 -07:00
Joel Klabo
480921db20 Suggested Users to Follow
ui: Add Suggested Users Views and Helpers
ui: Add Logic to Launch Suggested User Screen

Changelog-Added: Suggested Users to Follow
2023-07-29 10:25:24 -07:00
doffing.brett
f0de8721c7 Center and Pad buttons in EULA 2023-07-29 10:11:38 -07:00
Suhail Saqan
d11cd76e6a Add multiple reaction support
Changelog-Added: Add support for multiple reactions
Closes: https://github.com/damus-io/damus/issues/1335
2023-07-29 10:03:55 -07:00
Daniel D'Aquino' via patches
815f4d4a96 Allow relay logs to be opened in dev mode even if relay is disconnected
Changelog-Fixed: Allow relay logs to be opened in dev mode even if relay
Closes: https://github.com/damus-io/damus/issues/1368
Signed-off-by: Daniel D'Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-29 09:44:36 -07:00
Bryan Montz
4fecf72963 fix: endless connection attempt loop after user removes relay
This patch fixes an issue where, after the user removes a misbehaving
relay, the RelayConnection will keep trying to reconnect endlessly. You
can reproduce the issue prior to this change by adding the relay
wss://brb.io. It will fail to connect over and over. Then remove the
relay in the UI. In the console, you will see that it keeps trying to
connect, and the corresponding RelayConnection never gets deallocated.
After the change, it stops connecting and deallocates the
RelayConnection.

Changelog-Fixed: endless connection attempt loop after user removes relay
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-29 09:18:03 -07:00
William Casarin
593d0e2abe ndb: sync up a few remaining NdbNote tag differences 2023-07-25 16:22:25 -07:00
William Casarin
2f8aa29e92 ndb: make NostrEvents immutable
Since we can't mutate NdbNotes, let's update the existing codebase to
generate and sign ids on NostrEvent constructions. This will allow us to
match NdbNote's constructor
2023-07-25 15:34:05 -07:00
William Casarin
e3c04465fc ndb: move to uint32 for kind and created_at 2023-07-25 15:24:26 -07:00
William Casarin
54d40f7ffd ndb: move hexchar into header
since it's used in a few places
2023-07-25 15:23:36 -07:00
William Casarin
2053033b25 ndb: make note equatble
We need this for the switchover
2023-07-24 13:09:27 -07:00
William Casarin
45801f3e6c ndb: rename NostrEvent to NostrEventOld
This facilitates the switch to NdbNote by allowing us to switch back and
forth to fix things.
2023-07-24 13:08:55 -07:00
William Casarin
2d44f2744b ndb: switch to computed property for tags
this will allows us to change less code on the switchover
2023-07-24 13:08:18 -07:00
William Casarin
04e408bfea ndb: implement a few more event things
We're basically done. Time to try the switch-over
2023-07-24 12:41:12 -07:00
William Casarin
b3c87bdc07 test: remove unused var 2023-07-24 12:40:04 -07:00
William Casarin
b5dd90b36a notes: generalize event_is_reply a bit
so that it works with NdbNote as well
2023-07-24 12:39:55 -07:00
William Casarin
6fa9149939 ndb: avoid double constructor on References 2023-07-24 11:05:18 -07:00
William Casarin
1e9e4a7f3a ndb: implement eventref building from ndb notes 2023-07-24 10:55:34 -07:00
William Casarin
c8e236b6d5 ndb/test: add more test coverage on char iter 2023-07-23 12:21:36 -07:00
William Casarin
e8d0f1db8d test: fix some ndb test warnings 2023-07-23 12:12:42 -07:00
William Casarin
99b5dc94cb ndb: copy over perf improvements 2023-07-23 12:11:08 -07:00
William Casarin
e34351ca37 ndb: fix iterators, pack id tags, more tests 2023-07-23 11:55:36 -07:00
William Casarin
1a33d639ed test: remove some unused perf tests 2023-07-23 11:54:58 -07:00
William Casarin
5c1043b4e5 ndb: add cchar constructors to AsciiCharacter
This will be used for the cchar iterator
2023-07-23 11:54:07 -07:00
William Casarin
23b5763a6b git: ignore perf baselines
this is system-dependent
2023-07-23 11:50:02 -07:00
William Casarin
dd65209a20 Revert "ndb: remove TagIterators and just use sequences"
This reverts commit f0d07c3663.
2023-07-23 10:56:12 -07:00
William Casarin
f0d07c3663 ndb: remove TagIterators and just use sequences
Still learning...
2023-07-22 21:12:53 -07:00
William Casarin
b3119fa41e test: small test fix 2023-07-22 17:23:11 -07:00
William Casarin
7ec8da6c73 ndb: start implementing existing NostrEvent functionality
We eventually want to switch over to NdbNote instead of NostrEvent. To
facilitate this, the plan is to eventually make NostrEvent an alias of
NdbNote. For this to work, let's make sure the NostrEvent extensions are
implemented on NdbNote.

We will likely switch away from string properties as well, but for now
we will try to emulate as much as possible to make sure everything is
working first.
2023-07-22 17:19:47 -07:00
William Casarin
9e659c49b5 ndb/test: add a few more tests 2023-07-22 17:19:47 -07:00
William Casarin
c72666b352 ndb: add subscript and count for TagsSequence
These are helpful
2023-07-22 17:19:47 -07:00
William Casarin
1854e10486 mentions: add ndb mention parser 2023-07-22 17:19:47 -07:00
William Casarin
58e2fb40ef iter: make safer by using NdbNote instead of unsafe pointers
If we have an owned note, we could lose track of the lifetime and then
crash. Let's make sure we always have an NdbNote instead
2023-07-22 17:19:47 -07:00
William Casarin
af7ea7024f misc: don't immediately hex encode event commitment
keep it separate for now, since we're moving to more low level. We
probably won't even use this, but this is cleaner logicwise anyway.
2023-07-22 17:19:47 -07:00
William Casarin
0263c11a94 ndb: add content and owned_size 2023-07-22 17:19:47 -07:00
William Casarin
6d43754e71 ndb: add pubkey to NdbNote 2023-07-22 17:19:47 -07:00
William Casarin
4da23390f8 ndb: update lib 2023-07-22 17:19:47 -07:00
William Casarin
c74993366b move copyndb to the right folder 2023-07-22 17:19:47 -07:00
William Casarin
ad0e1f28b7 test: fix build and tests 2023-07-21 15:26:03 -07:00
William Casarin
61051ee853 nostrdb: add initial swift integration 2023-07-21 15:02:01 -07:00
William Casarin
dc7826c4e5 c: add nostrdb c lib 2023-07-21 15:02:01 -07:00
William Casarin
4eee715bcd c: add jsmn json parser
This is used by the nostrdb lib. Let's add it here.
This doesn't unescape things, so we'll still need to do that manually.
2023-07-21 14:56:24 -07:00
William Casarin
08bea16be0 c: add new cursor util
this is used by nostrdb as well. so add it here ahead of time.
2023-07-21 14:55:54 -07:00
William Casarin
8f04b12a90 c: add copy nostrdb devtool 2023-07-21 14:55:54 -07:00
William Casarin
9cfed9f3aa c: update protoverse_cursor to jb55_cursor
Will be using this in the new db implementation
2023-07-21 14:39:21 -07:00
William Casarin
123ca3b802 test: add my contact list for as json parsing test data 2023-07-21 14:39:21 -07:00
William Casarin
5e7b1f4ff3 event: separate logic from data using extensions
I'm not a huge fan of this pattern but it's getting messy in here
2023-07-21 14:39:11 -07:00
12594e35c1 Update translations
47	Translate Localizable.stringsdict in de
6	Translate Localizable.strings in de
3	Translate Localizable.strings in zh_CN
2	Translate Localizable.strings in sv_SE
2	Translate Localizable.strings in es_419
1	Translate Localizable.stringsdict in zh_TW
1	Translate Localizable.stringsdict in zh_HK
1	Translate Localizable.stringsdict in zh_CN
1	Translate Localizable.stringsdict in sv_SE
1	Translate Localizable.stringsdict in pl_PL
1	Translate Localizable.stringsdict in es_419
1	Translate Localizable.strings in zh_TW
1	Translate Localizable.strings in zh_HK
1	Translate Localizable.strings in pl_PL
1	Translate Localizable.strings in nl

Closes: https://github.com/damus-io/damus/pull/1373
2023-07-19 10:11:42 -07:00
ab92f7b561 Update localization issues and export strings for translation 2023-07-19 10:08:30 -07:00
William Casarin
11b9062865 test: fix some warnings 2023-07-19 10:04:25 -07:00
William Casarin
5c5b55bf67 v1.6 (7) changelog 2023-07-17 14:39:27 -07:00
William Casarin
dd6c082a8e v1.6 (7) 2023-07-17 14:35:54 -07:00
William Casarin
2a4ee6c48c zaps: don't spam lnurls when validate zaps
lnurls.lookup_or_fetch not fetched lnurl1dp68gurn8ghj7um9dej8xct5wvhxcmmv9uh8wetvdskkkmn0wahz7mrww4excup0df3r2dg3mj444
fetching static payreq lnurl1dp68gurn8ghj7um9dej8xct5wvhxcmmv9uh8wetvdskkkmn0wahz7mrww4excup0df3r2dg3mj444
lnurls.lookup_or_fetch already fetching lnurl1dp68gurn8ghj7um9dej8xct5wvhxcmmv9uh8wetvdskkkmn0wahz7mrww4excup0df3r2dg3mj444
lnurls.lookup_or_fetch already fetching lnurl1dp68gurn8ghj7um9dej8xct5wvhxcmmv9uh8wetvdskkkmn0wahz7mrww4excup0df3r2dg3mj444
lnurls.lookup_or_fetch already fetching lnurl1dp68gurn8ghj7um9dej8xct5wvhxcmmv9uh8wetvdskkkmn0wahz7mrww4excup0df3r2dg3mj444
lnurls.lookup_or_fetch already fetching lnurl1dp68gurn8ghj7um9dej8xct5wvhxcmmv9uh8wetvdskkkmn0wahz7mrww4excup0df3r2dg3mj444

Changelog-Fixed: Don't spam lnurls when validating zaps
2023-07-17 14:12:41 -07:00
William Casarin
fa520d48d3 zap: remove unnecessary main thread dispatches when zapping 2023-07-17 14:11:23 -07:00
William Casarin
160b293359 performance: don't spam nip05 validation on startup
Since we don't show these on events anymore, we don't need to spam nip05
validation. We can just check when we go to the profile page

Changelog-Fixed: Eliminate nostr address validation bandwidth on startup
2023-07-17 13:25:56 -07:00
William Casarin
7d17b9b476 nip05: hide nip05 username if it matches the username 2023-07-17 13:25:56 -07:00
William Casarin
d04f1c6867 login: allow user to login to deleted profile
If they every change their mind.

Changelog-Fixed: Allow user to login to deleted profile
2023-07-17 13:25:56 -07:00
William Casarin
5c87dd5bbb nip05: remove clickable option
they're always clickable now
2023-07-17 13:25:56 -07:00
William Casarin
12febf9671 view: extract ProfileEditButton to its own file
profile view file is getting cray cray
2023-07-17 13:25:56 -07:00
William Casarin
4033ad66ba test: fix crash in ci 2023-07-17 13:25:56 -07:00
William Casarin
2c0296cce3 project: bump deployment target
not sure how this is different than the previous setting that was
updated.

Cc: Bryan Montz <bryanmontz@me.com>
2023-07-17 13:25:56 -07:00
William Casarin
080aaf2d1b nip05: show username and support _ usernames
Changelog-Added: Show nostr address username and support abbreviated _ usernames
2023-07-17 11:01:57 -07:00
William Casarin
0e55b08b6c Revert removing nip05 badges on profiles
Changelog-Added: Re-add nip05 badges to profiles

This partially reverts commit 7ae7584135.
2023-07-17 10:52:20 -07:00
William Casarin
ff70cb7ebf posting: don't prepad user tag if its a newline
This fixes one more edgecase with the tag prepend logic.
2023-07-17 10:45:05 -07:00
William Casarin
fe82134a75 posting: switch to new tested composition logic
This switches to the new post composition logic in the post view. It
adds a space at the begging of a mention if it is needed.

We still need to make the state in these view more pure so we can test
more of the posting logic like cursor positions after posting, etc.

Changelog-Added: Add space when tagging users in posts if needed
Changelog-Fixed: Fix issue where typing cc@bob would produce brokenb ccnostr:bob mention
2023-07-17 10:25:09 -07:00
William Casarin
60a0c21272 test: add post composition tests
This adds post composition tests so that we can avoid composition bugs.
This still does not capture all of the dynamics of post composition,
because it ignores much of the mutable cursor position and related state
when editing posts.

We will need to make post editing more pure and less mutable in the
future to get test coverage on those.
2023-07-17 10:25:09 -07:00
William Casarin
8242ca27d2 profile: make constructor args optional
This makes it easier to create one-off profiles for testing. eg:

Profile(name: "jb55")
2023-07-17 10:25:09 -07:00
William Casarin
c7baa153af posting: add some functions for appending mention tags
These are easy-to-test functions for appending user tags to attributed
strings. We will use these in the next couple of commits to replace the
existing buggy functionality.
2023-07-17 10:25:09 -07:00
William Casarin
ff654c4e11 test: add text attribute testing function
This will be used for testing attributed strings
2023-07-17 10:25:09 -07:00
William Casarin
deaf5f042a search: refactor appendUserTag to make logic more clear
ocd mostly
2023-07-17 10:25:09 -07:00
William Casarin
4f56ff3dfb longform: add padding under words count
Changelog-Added: Added padding under word count on longform account
2023-07-17 10:25:09 -07:00
William Casarin
fd59407171 test: fix old markdown tests 2023-07-17 10:25:09 -07:00
William Casarin
9b759247ee v1.6 (6) changelog 2023-07-16 15:34:40 -07:00
William Casarin
cd7998b69d v1.6 (6) 2023-07-16 15:33:00 -07:00
William Casarin
bd4c29604f Fix broken markdown renderer
This switches away from the old markdown renderer to the new one at
https://github.com/damus-io/swift-markdown-ui

Changelog-Fixed: Fix broken markdown renderer
2023-07-16 15:27:24 -07:00
William Casarin
bf1175f22c markdown: add some helpers for counting markdown words
Will use this in the new word counter
2023-07-16 15:27:06 -07:00
William Casarin
064888f78d markdown: use a real-world longform preview 2023-07-16 15:26:31 -07:00
William Casarin
fc640b85ed add swift-markdown-ui
We will be using this lib which is much better than the builtin
framework for markdown rendering. We use a modified version that removes
html tag rendering which looks horrible.
2023-07-16 15:25:09 -07:00
William Casarin
d5766253cf build: fix unused variable warning 2023-07-16 15:24:06 -07:00
William Casarin
571ed39d52 Fixed issue where hashtags were leaking in DMs
Now we never add any tags to DMs, we only add the p tag of the user
you're talking to.

Changelog-Fixed: Fixed issue where hashtags were leaking in DMs
2023-07-16 15:24:06 -07:00
cr0bar
16d81ed40f Hide nsec when logging in
Fix for Hide nsec when logging in & add hide/show toggle

Closes: https://github.com/damus-io/damus/issues/1206
Changelog-Changed: Hide nsec when logging in
2023-07-16 13:05:18 -07:00
William Casarin
1135c19fea test: add setting property tests
Some initial UserSettingsStore property tests
2023-07-16 13:05:18 -07:00
William Casarin
77331644cb Fix issue with emojis next to hashtags and urls
Treat utf8 bytes next to hashtags and urls as boundary conditions

Changelog-Fixed: Fix issue with emojis next to hashtags and urls
2023-07-16 11:46:23 -07:00
William Casarin
8d14fdffb5 content: add utf8 char at url left boundary test 2023-07-16 11:46:23 -07:00
William Casarin
0c95071de7 project: rename parse_mentions to parse_note_content
This is more accurate
2023-07-16 11:46:23 -07:00
William Casarin
da78a217a3 docs: clarify the section on using -v2,v3, etc
Some patches are still not getting sent with version information. Let's
clarify that in the contribution docs.

Cc: dev@damus.io
2023-07-16 10:05:57 -07:00
William Casarin
f53b824122 docs: patch changelogs when submitting patches
This adds a section on creating patch changelogs when submitting
patches. It helps reviewers know what changed between many different
versions of a patch
2023-07-16 09:44:27 -07:00
Bryan Montz
45ab394b09 fixed: relay detail view is not immediately available after adding new relay
Changelog-Fixed: relay detail view is not immediately available after adding new relay
Closes: https://github.com/damus-io/damus/issues/1369
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-16 07:56:18 -07:00
Bryan Montz
47e7505573 fix typos
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-16 07:37:55 -07:00
Bryan Montz
0f1390f412 Swift cleanup: remove duplicate or unnecessary initializers using default values
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-16 07:37:55 -07:00
Bryan Montz
6bf5293701 Swift cleanup: don't capture case values only to ignore them in switch statements
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-16 07:37:55 -07:00
Bryan Montz
3d6909bf62 Swift cleanup: simplify "Task.init {}" to "Task {}"
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-16 07:37:55 -07:00
Bryan Montz
ecd8b64b8b Swift cleanup: prefer case list over fallthrough in switch statements
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-16 07:37:55 -07:00
Bryan Montz
0c627ae0a0 Swift cleanup: "init (" -> "init("
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-16 07:37:55 -07:00
William Casarin
16c86c1d1c update bad commit mailmap 2023-07-14 22:25:19 -07:00
Daniel D'Aquino' via patches
29140d956b Add feedback message when user adds a relay already in the list
Changelog-Added: Added feedback when user adds a relay that is already on the list
Closes: https://github.com/damus-io/damus/issues/1053
Signed-off-by: Daniel D'Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-14 22:11:34 -07:00
William Casarin
7ae7584135 ui: remove nip05 badge on events
Changelog-Changed: Remove nip05 on events
2023-07-14 17:31:28 -07:00
William Casarin
139be9eef2 Fix nostr: mention prefix bugs
The zero-width space was causing parsing issues. Not sure why we need
this so I just removed it.

Changelog-Fixed: Fix nostr:nostr:... bugs
2023-07-14 17:28:24 -07:00
William Casarin
72a060c7b3 nip05: rename nip05 to Nostr Address in search
Forgot this one
2023-07-14 17:05:01 -07:00
William Casarin
9db81fd6b8 views: refactor post_changed in PostView
Use some helper functions instead of the full switch
2023-07-14 15:54:17 -07:00
William Casarin
f08efd7e30 nip05: rename nip05 verification to nostr address
nip05 identifiers and nip05 verification is too confusing, and also
wrong. Let's use the "nostr address" terminology.

Suggested-by: Derek Ross
Suggested-by: Semisol <hi@semisol.dev>
Changelog-Changed: Rename NIP05 to "nostr address"
2023-07-14 13:26:10 -07:00
William Casarin
fb2a69acd8 project: fix test fixtures 2023-07-14 13:07:52 -07:00
8a9e3ea76b Fix localization issues and export strings for translation
Changelog-Fixed: Fix localization issues and export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-14 09:34:29 -07:00
William Casarin
4830a6f3b7 Run actions on pushes to the ci branch 2023-07-14 09:34:29 -07:00
William Casarin
9879c78e41 build 5 because I broked something 2023-07-13 15:43:21 -07:00
William Casarin
05e73a3711 actually subscribe to likes. oops 2023-07-13 11:43:52 -07:00
William Casarin
731fdb108b build: fix a few warnings and errors 2023-07-13 11:17:00 -07:00
William Casarin
2f3737c2b5 v1.6-4 changelog 2023-07-13 11:14:29 -07:00
William Casarin
e36747a81a v1.6 (4) 2023-07-13 11:13:50 -07:00
William Casarin
505ce0bd39 Add the ability to follow hashtags
Changelog-Added: Add the ability to follow hashtags
2023-07-13 11:10:53 -07:00
William Casarin
31fa63debf home: hide users and hashtags from home timeline when you unfollow
Add the ability to resubscribe to home filters so that it will be
updated when you follow and unfollow people

Changelog-Fixed: Hide users and hashtags from home timeline when you unfollow
2023-07-13 11:08:09 -07:00
William Casarin
122655bea3 home: separate home filters
we will want to resubscribe to these, so pull them out
2023-07-13 11:08:09 -07:00
William Casarin
9a714943fd contacts: get followed hashtags function
todo: cache these
2023-07-13 11:08:09 -07:00
William Casarin
17df2972d9 ui: add follow hashtag ui on search view 2023-07-13 11:08:04 -07:00
William Casarin
bebaffd247 contacts: unify following logic
We are about to add hashtag following, so let's prepare handle_follow
for this. Generalize pubkey following to ReferenceId follows in the
handle_{follow,unfollow} functions.

We also split out the notification part into its own function.
2023-07-13 09:32:42 -07:00
William Casarin
0fae54a98d components: make GradientButtonStyle padding configurable
There is too much padding on the follow hashtag button so we need to fix
that
2023-07-13 09:04:55 -07:00
William Casarin
90818c12e8 components: create PinkGradientView and use PinkGradient directly
Still need to do this for the other gradients as well but this is fine
for now.
2023-07-13 09:04:55 -07:00
William Casarin
1136808afa contacts: generalize following to allow any reference
I noticed we are not using the PostBox when following new users. Not
good! This is probably why following users sometimes does not work.

Changelog-Fixed: Fixed a bug where following a user might not work due to poor connectivity
2023-07-13 09:04:55 -07:00
William Casarin
b7d139ffb3 refid: add .t helper
This is used for quickly creating hashtag refs
2023-07-13 09:04:55 -07:00
William Casarin
7fc270725f test: add newline mention test
This is currently passing but it shouldn't be. This is because we are
not testing the build_post function directly. We will do this soon.
2023-07-13 07:32:27 -07:00
William Casarin
7b73a54de5 test: switch to test data file
We only added the file before, let's actually use it now
2023-07-13 07:32:27 -07:00
Bryan Montz
fdaf785869 fixed: icon color for developer mode setting is incorrect in low-light mode
Changelog-Fixed: icon color for developer mode setting is incorrect in low-light mode
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-13 07:24:44 -07:00
William Casarin
c0f9b0a8c0 views: allow embeddable views at top of timeline
This allows you to put stuff at the top of a timeline inside the scroll
view. We could also remove the scrollview from the timeline
eventually... but this works for now.
2023-07-13 06:56:23 -07:00
William Casarin
7123b225a1 search: make model an ObjservedObject
This should not be a state object because the data is passed in
elsewhere
2023-07-13 06:56:23 -07:00
William Casarin
b6f25a85f8 Fix nip05 badge icon 2023-07-13 06:56:23 -07:00
William Casarin
7046fe0d4f ui: add DamusBackground helper
We will be using this in more places
2023-07-13 06:56:23 -07:00
William Casarin
201e9a427f post: extract build_post from post view
I need to test this function because there is a bug with nostr: mentions
2023-07-13 06:56:23 -07:00
William Casarin
6481f96488 add bech32_pubkey_decode
I need this for a test
2023-07-13 06:56:23 -07:00
William Casarin
3845d32074 test: add test data file
We can organize test data in here
2023-07-13 06:56:23 -07:00
William Casarin
c1c33518ea don't follow jb55 by default
This was funny initially but it confuses people.

Changelog-Removed: Remove following Damus Will by default
2023-07-12 14:42:23 -07:00
William Casarin
f2cf30a728 Scroll to top for longform events only
Fixes: ad6a1962 ("Scroll to top of event instead of bottom")
2023-07-12 08:23:53 -07:00
William Casarin
69922b1d77 Remove LoadMoreButton
Was an old unused thing
2023-07-12 08:21:44 -07:00
William Casarin
7343fcd399 Allow longform content to be long
Changelog-Changed: Remove note size restriction for longform events
2023-07-12 05:38:48 -07:00
ericholguin
5571052cfd Update nav to use adaptable color for dark and light modes
Changelog-Fixed: Fixed nav bar color on login, eula, and account creation
Closes: https://github.com/damus-io/damus/pull/1361
2023-07-12 05:38:10 -07:00
William Casarin
de63e96664 v1.6-3 changelog
A few longform fixes
2023-07-11 12:59:09 -07:00
William Casarin
7f9371d85f v1.6 (3) 2023-07-11 12:58:15 -07:00
William Casarin
de4e8e5748 Only show longform preview in notifications
Changelog-Fixed: Show longform previews in notifications instead of the entire post
2023-07-11 12:56:30 -07:00
William Casarin
ad6a1962bb Scroll to top of event instead of bottom
This is pretty important for longform events

Changelog-Changed: Start at top when reading longform events
2023-07-11 12:55:54 -07:00
William Casarin
828e417726 Allow reposting and quote reposting multiple times
Changelog-Changed: Allow reposting and quote reposting multiple times
2023-07-11 12:28:38 -07:00
William Casarin
d2374aa6ec I broked dms. i fixed. 2023-07-11 12:28:38 -07:00
William Casarin
495859e07f Fix various padding issues related to longform posts
1. Make a proper threaded EventShell variant
2. Fix padding everywhere

Changelog-Fixed: Fix padding on longform events
2023-07-11 12:17:59 -07:00
William Casarin
d96ea593a5 search: allow searching longform articles by hashtag 2023-07-11 12:17:22 -07:00
William Casarin
7514a741c0 docs: make note to replace old bech32 parser 2023-07-11 12:17:09 -07:00
William Casarin
dc7b0004bc Hide action bar in longform quote reposts
Changelog-Fixed: Fix action bar appearing on quoted longform previews
2023-07-11 10:26:29 -07:00
William Casarin
8e33d5f6b9 v1.6-2 changelog 2023-07-11 09:22:39 -07:00
William Casarin
db2ec0a00a Fix npub mention bugs, fix slowness when parsing large posts
Switch the post parser to use the same code as the content parser. This
was causing many issues, including performance issues.

Changelog-Fixed: Fix lag when creating large posts
Changelog-Fixed: Fix npub mentions failing to parse in some cases
Changelog-Added: Add r tag when mentioning a url
Changelog-Removed: Remove old @ and & hex key mentions
2023-07-11 09:15:13 -07:00
cr0bar
dc21b6139c Add support for multilingual hashtags
Changelog-Added: Add support for multilingual hashtags
Reviewed-by: William Casarin <jb55@jb55.com>
Closes: https://github.com/damus-io/damus/issues/949
2023-07-11 07:22:44 -07:00
William Casarin
031c7823ae refactor: move hashtag tests to their own file 2023-07-11 07:21:16 -07:00
cr0bar
ac2b5b26bb Added non-latin test and amended emoji test to include emoji in hashtag 2023-07-11 06:39:12 -07:00
cr0bar
c1220f50af Handle percent encoding of colon for some hashtags 2023-07-11 06:39:12 -07:00
cr0bar
2353f97114 Change to is_hashtag_chat to support non-latin characters 2023-07-11 06:39:12 -07:00
cr0bar
e83e110adb Fix to is_boundary to support non-latin characters 2023-07-11 06:39:12 -07:00
William Casarin
aae97c5cb7 git: add .mailmap file
This ensures that author emails are correct when using various git tools
2023-07-11 06:39:05 -07:00
William Casarin
45d9121ed7 fix project issues 2023-07-10 20:56:52 -07:00
4c774f2dda Fix PostView initial string to skip mentioning self when on own profile
Changelog-Fixed: Fix PostView initial string to skip mentioning self when on own profile
Signed-off-by: Terry Yiu <git@tyiu.xyz>
Reviewed-by: William Casarin <jb55@jb55.com>
2023-07-10 20:16:18 -07:00
b8ec3493dc Fix freezing bug when tapping Developer settings menu
Changelog-Fixed: Fix freezing bug when tapping Developer settings menu
Signed-off-by: Terry Yiu <git@tyiu.xyz>
Reviewed-by: William Casarin <jb55@jb55.com>
2023-07-10 20:16:18 -07:00
William Casarin
e299389866 Add initial longform note support
Changelog-Added: Add initial longform note support
2023-07-10 18:29:18 -07:00
William Casarin
374610a21a artifacts: allow unseparated note artifacts
This is needed for longform events. Right now we treat unseparated note
artifacts as a list of blocks, but we will likely need to render these
blocks into lists of attributed texts with image blocks inbetween.
2023-07-10 18:24:43 -07:00
William Casarin
4d995fd04c Longform Notes 2023-07-10 17:39:13 -07:00
William Casarin
518886912c refactor: carve out TextEvent body into EventShell
We'll need this for other event types
2023-07-10 17:39:13 -07:00
William Casarin
ab5eea330a options: add no_mentions to event view options
We don't need mentions in longform previews so we'll need this
2023-07-10 17:39:13 -07:00
William Casarin
41de715067 query: add longform kind, add to home filter 2023-07-10 17:39:13 -07:00
William Casarin
6ca9bda01e notes: count words in notes during artifact parsing 2023-07-10 17:39:13 -07:00
William Casarin
fe077fa5c2 reposts: don't always show text events in reposts
This will allow longform reposts to work properly

Changelog-Fixed: Don't always show text events in reposts
2023-07-10 17:39:13 -07:00
William Casarin
cb2380e218 docs: add git-contacts example
git-contacts is a great way to cc people who have touched the same hunk
of code before.

Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-10 17:38:54 -07:00
Joel Klabo
196cfdec4b Fix Image Orientation 2023-07-10 17:27:51 -07:00
Joel Klabo
bfb47c0f85 Update Control Style to Stand Out More 2023-07-10 17:27:51 -07:00
Joel Klabo
9e7e128d9a Refactoring Edit Picture Views 2023-07-10 17:27:51 -07:00
Joel Klabo
bf95a8b328 Banner Image Upload
Changelog-Added: Enable banner image editing
2023-07-10 17:27:42 -07:00
William Casarin
e316d5d635 docs: move security.md to docs subdir 2023-07-10 16:37:45 -07:00
William Casarin
37a5abc9e3 gitignore: add tags 2023-07-10 16:35:43 -07:00
William Casarin
cf83ac1fe8 docs: add patch submission guidelines 2023-07-10 16:22:09 -07:00
cr0bar
7a1269bd68 Fix for test issue due to recently implemented RelayPool change 2023-07-10 13:49:07 -07:00
William Casarin
acb4e6d17e wasm: fix intptr warning 2023-07-10 11:14:03 -07:00
William Casarin
e957c3b703 wasm: fix clz64 warning 2023-07-10 11:14:03 -07:00
William Casarin
82fc4ff15e wasm: comment out some unnused code for now
fixes some warnings
2023-07-10 11:14:03 -07:00
William Casarin
15d633a42f project: update to recommend settings 2023-07-10 11:08:20 -07:00
William Casarin
7158f07bb1 Translate all the things 2023-07-10 08:20:28 -07:00
Bryan Montz
07abc5c04b Fix issue where first row is always selected on Form views
Changlog-Fixed: Fix issue where first row is always selected on Form views
Signed-off-by: Bryan Montz <bryanmontz@me.com>
2023-07-10 07:54:47 -07:00
transifex-integration[bot]
79fb352d96 Translate Localizable.strings in el_GR
100% translated source file: 'Localizable.strings'
on 'el_GR'.
2023-07-10 08:46:21 +00:00
transifex-integration[bot]
94ef9bb42a Translate Localizable.strings in el_GR
100% translated source file: 'Localizable.strings'
on 'el_GR'.
2023-07-10 08:46:09 +00:00
transifex-integration[bot]
78a64165e1 Translate Localizable.stringsdict in el_GR
100% translated source file: 'Localizable.stringsdict'
on 'el_GR'.
2023-07-10 08:43:55 +00:00
transifex-integration[bot]
ad216b1f11 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2023-07-10 08:35:54 +00:00
transifex-integration[bot]
4abd227cf7 Translate Localizable.stringsdict in nl
100% translated source file: 'Localizable.stringsdict'
on 'nl'.
2023-07-10 08:35:46 +00:00
transifex-integration[bot]
800ce44f5e Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2023-07-10 08:12:11 +00:00
transifex-integration[bot]
0e9e44d8f2 Translate Localizable.stringsdict in ja
100% translated source file: 'Localizable.stringsdict'
on 'ja'.
2023-07-10 07:51:47 +00:00
transifex-integration[bot]
3eba4b0af9 Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2023-07-10 07:51:06 +00:00
140c3505ba Update translations 2023-07-09 15:36:35 -04:00
fcd7d2beab Fix localization issues and export strings for translation 2023-07-09 15:33:15 -04:00
William Casarin
83ef50586a zaps/refactor: use guard instead of if block
not a fan of unncessary nesting
2023-07-09 07:44:33 -07:00
William Casarin
87992f4bb9 Add RelayLog in developer mode
Changelog-Added: Add relay log in developer mode
2023-07-09 07:41:45 -07:00
Bryan Montz
faaa3e3bd9 only show the relay log in developer mode
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-09 07:40:39 -07:00
Bryan Montz
2d9f7128ee fix crash when adding line to log from background thread
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-09 07:40:39 -07:00
Bryan Montz
51d71f11c1 replace RelayMetadatas with RelayModelCache in DamusState
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-09 07:40:39 -07:00
Bryan Montz
f619fef410 add RelayModel and RelayModelCache classes
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-09 07:40:39 -07:00
Bryan Montz
91f02ccff5 add RelayLog to the bottom of the RelayDetailView
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-09 07:40:39 -07:00
Bryan Montz
a63ea1e22b add network state changes to RelayLogs
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-09 07:40:39 -07:00
Bryan Montz
40e5e4a026 add a RelayLog to each RelayConnection and send events to it
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-09 07:40:39 -07:00
Bryan Montz
ef4aeb40e0 add RelayLog class
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-09 07:40:39 -07:00
William Casarin
13f98659a4 Prevent forged profile zap attacks
The fake note zap attack made me realize that there is a way to do fake
profile zaps using a similar technique. Since damus only checks the
first ptag if it is a profile zap, this means you could include multiple
ptags, the first one being the fake profile with the fake zapper, and
the second p tag as the real target.

This would allow a fake zapper to create a fake a zap, while the zap
notification would still appear for the second ptag because damus
listens for zap events via #p, and that would match the second ptag.

To fix this, ensure that zaps only have at most 1 ptag and 0 or 1 etag.
my CLN zapper checks this but if we don't check this here as well then
we run into fake zap issues.

Changelog-Fixed: Fix potential fake profile zap attacks
Cc: Tony Giorgio <tonygiorgio@protonmail.com>
Cc: benthecarman <benthecarman@live.com>
Cc: Vitor Pamplona <vitor@vitorpamplona.com>
2023-07-08 22:10:34 -07:00
William Casarin
f5ba909784 zaps: move pubkey check into standalone function 2023-07-08 22:09:30 -07:00
William Casarin
6031fe0847 Fix fake note zaps with forged p-tags
This fixes a zap issue where someone could send a fake zap with a zapper
that doesn't match the user's nostrPubkey zapper. This is possible
because damus looks up the zapper via the ptag on note zaps.

Fix this by first looking up the cached event's ptag instead. This
prevents zappers from trying to trick Damus into picking the wrong
zapper.

Fixes: #1357
Changelog-Fixed: Fix issue where malicious zappers can send fake zaps to another user's posts
Reported-by: benthecarman <benthecarman@live.com>
Cc: Tony Giorgio <tonygiorgio@protonmail.com>
2023-07-08 21:22:58 -07:00
William Casarin
1be2a9e1b1 ui: remove invalid zap text 2023-07-08 20:47:11 -07:00
cr0bar
4478348d10 Fix profile post button mentions
Fix for second part of issue #1352 where if you submit a reply from the
+ on a profile, it uses the hex nostr url rather than the bech32
version. When typing the @ manually it uses the bech32 so updated to
mirror this.

Changelog-Fixed: Fix profile post button mentions
Closes: #1355
2023-07-08 19:24:35 -07:00
Anthony de Broise
cf4131f867 Minor update to ConfigView.swift to fix key and search icon
Replaced icon names with names existing in assets to avoid them being left blank.

Changelog-Fixed: Fix icons on settings view
Closes: #1353
2023-07-08 08:14:26 -07:00
Bryan Montz
81b69bc2ea add explanatory footer to Developer Mode setting view
Signed-off-by: Bryan Montz <bryanmontz@me.com>
Reviewed-by: William Casarin <jb55@jb55.com>
2023-07-08 08:06:52 -07:00
William Casarin
0c736a18a9 docs: annotate might be causing issues for some people
suhail was having trouble when this option was enabled. let's remove it
just in case.
2023-07-07 09:25:33 -07:00
Bryan Montz
d2efe06610 make "Copy Note JSON" a developer mode setting
Signed-off-by: Bryan Montz <bryanmontz@me.com>
2023-07-07 09:02:52 -07:00
Bryan Montz
ebcfe3c25f add developer mode view and setting
Signed-off-by: Bryan Montz <bryanmontz@me.com>
2023-07-07 09:02:52 -07:00
William Casarin
6dfda93ff9 Fix Invalid Zap bug in reposts
Changelog-Fixed: Fix Invalid Zap bug in reposts
2023-07-04 13:48:49 -07:00
William Casarin
ea50f9214a Switch to navigation stack in BuilderEventView 2023-07-04 13:48:49 -07:00
William Casarin
6c8cf8421c zaps: make zap setting private 2023-07-04 13:47:44 -07:00
Ben Harvie
cbbe203d84 Create SECURITY.md 2023-07-04 12:45:15 -07:00
William Casarin
19217f47a4 v1.6 changelog 2023-07-04 12:21:12 -07:00
William Casarin
3451e7d88f v1.6 2023-07-04 12:18:33 -07:00
William Casarin
b5ea1e011e Revert "profile: make profile loading more lightweight for now"
Changelog-Fixed: Load more content on profile view
2023-07-04 11:51:07 -07:00
William Casarin
a04a401292 nscript: load script view
This allows you to open and run scripts for testing purposes, but only
from external links such as nostr:nscript...
2023-07-04 11:48:27 -07:00
640fbf23ea Fix UI bug with user search and fix race conditions on profiles NIP-05 cache
Signed-off-by: Terry Yiu <git@tyiu.xyz>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-04 09:09:14 -07:00
William Casarin
3d0448a929 smaller nostrscript 2023-07-03 17:10:57 -07:00
William Casarin
30e33a01c1 nostrscript: add a helper function 2023-07-03 16:59:50 -07:00
William Casarin
a6cbf50def settings: record bool option keys
so that NostrScripts know which bool settings can be set
2023-07-03 16:28:25 -07:00
prprhyt
94bd194287 Added event id validation 2023-07-03 15:03:33 -07:00
William Casarin
97f10e865f NostrScript
NostrScript is a WebAssembly implementation that interacts with Damus.
It enables dynamic scripting that can be used to power custom list views,
enabling pluggable algorithms.

The web has JavaScript, Damus has NostrScript. NostrScripts can be
written in any language that compiles to WASM.

This commit adds a WASM interpreter I've written as a mostly-single C
file for portability and embeddability. In the future we could
JIT-compile these for optimal performance if NostrScripts get large and
complicated. For now an interpreter is simple enough for algorithm list
view plugins.

Changelog-Added: Add initial NostrScript implementation
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-03 14:31:38 -07:00
0c0c58c0cc Fix bug with Trie search
Exact matches were not being returned first in the array of results

Signed-off-by: Terry Yiu <git@tyiu.xyz>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-07-03 13:37:52 -07:00
William Casarin
7d49d3d9f1 refactor: make guard statement a bit more readible
It's a bit confusing to guard on a negative boolean expression
2023-07-03 12:48:25 -07:00
cb1e16b1a4 Fix reports to conform to NIP-56
Changelog-Fixed: Fix reports to conform to NIP-56
Signed-off-by: Terry Yiu <git@tyiu.xyz>
Tested-by: William Casarin <jb55@jb55.com>
2023-07-03 12:25:12 -07:00
6e964f71ff Add trie-based user search cache to replace non-performant linear scans
Changelog-Added: Speed up user search
Tested-by: William Casarin <jb55@jb55.com>
Fixes: #1219
Closes: #1342
2023-07-03 12:06:01 -07:00
4b7444f338 Fix profile navigation bugs from muted users list and relay list views
Changelog-Fixed: Fix profile navigation bugs from muted users list and relay list views
Signed-off-by: Terry Yiu <git@tyiu.xyz>
Reviewed-by: William Casarin <jb55@jb55.com>
2023-07-03 08:59:28 -07:00
57159f7df9 Fix build warnings
Signed-off-by: Terry Yiu <git@tyiu.xyz>
Reviewed-by: William Casarin <jb55@jb55.com>
2023-07-03 08:50:17 -07:00
4712c6b288 Fix navigation to translation settings view
Changelog-Fixed: Fix navigation to translation settings view
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2023-07-03 08:25:04 -07:00
576 changed files with 72741 additions and 7381 deletions

2
.envrc
View File

@@ -1,4 +1,4 @@
#use nix
use nix
export TODO_FILE=$PWD/TODO

View File

@@ -5,6 +5,7 @@ on:
push:
branches:
- "master"
- "ci"
pull_request:
branches:
- "*"

3
.gitignore vendored
View File

@@ -1,5 +1,8 @@
xcuserdata
/.direnv
damus/TestingPrivate.swift
damus.xcodeproj/xcshareddata/xcbaselines
.DS_Store
TODO.bak
tags
build-git-hash.txt

6
.mailmap Normal file
View File

@@ -0,0 +1,6 @@
Terry Yiu <git@tyiu.xyz> <963907+tyiu@users.noreply.github.com>
Ben Weeks <ben.weeks@knowall.ai> <ben.weeks@outlook.com>
Suhail Saqan <suhail.saqan@gmail.com> <43693074+suhailsaqan@users.noreply.github.com>
cr0bar <cr0bar@cr0.bar> <cr0bar@users.noreply.github.com>
Swift <scoder1747@gmail.com> <120697811+scoder1747@users.noreply.github.com>
Daniel D'Aquino <daniel@daquino.me> <patches@damus.io>

View File

@@ -1,3 +1,282 @@
## [1.6-18] - 2023-09-21
### Added
- Add followed hashtags to your following list (Daniel DAquino)
- Add "Do not show #nsfw tagged posts" setting (Daniel DAquino)
- Hold tap to preview status URL (Jericho Hasselbush)
- Finnish translations (etrikaj)
### Changed
- Switch to nostrdb for @'s and user search (William Casarin)
- Use nostrdb for profiles (William Casarin)
- Updated relay view (ericholguin)
- Increase size of the hitbox on note ellipsis button (Daniel DAquino)
- Make carousel tab dots tappable (Bryan Montz)
- Move the "Follow you" badge into the profile header (Grimless)
### Fixed
- Fix text composer wrapping issue when mentioning npub (Daniel DAquino)
- Make blurred videos viewable by allowing blur to disappear once tapped (Daniel DAquino)
- Fix parsing issue with NIP-47 compliant NWC urls without double-slashes (Daniel DAquino)
- Fix padding of username next to pfp on some views (William Casarin)
- Fixes issue where username with multiple emojis would place cursor in strange position. (Jericho Hasselbush)
- Fixed audio in video playing twice (Bryan Montz)
- Fix crash when long pressing custom reactions (William Casarin)
- Fix random crashom due to old profile database (William Casarin)
[1.6-18]: https://github.com/damus-io/damus/releases/tag/v1.6-18
## [1.6-17] - 2023-08-23
### Added
- Add support for status URLs (William Casarin)
- Click music statuses to display in spotify (William Casarin)
- Add settings for disabling user statuses (William Casarin)
### Changed
- clear statuses if they only contain whitespace (William Casarin)
### Fixed
- Fix long status lines (William Casarin)
- Fix status events not expiring locally (William Casarin)
[1.6-17]: https://github.com/damus-io/damus/releases/tag/v1.6-17
## [1.6-16] - 2023-08-23
### Added
- Added live music statuses (William Casarin)
- Added generic user statuses (William Casarin)
### Fixed
- Avoid notification for zaps from muted profiles (tappu75e@duck.com)
- Fix text editing issues on characters added right after mention link (Daniel DAquino)
- Mute hellthreads everywhere (William Casarin)
[1.6-16]: https://github.com/damus-io/damus/releases/tag/v1.6-16
## [1.6-13] - 2023-08-18
### Fixed
- Fix bug where it would sometimes show -1 in replies (tappu75e@duck.com)
- Fix images and links occasionally appearing with escaped slashes (Daniel DAquino)
- Fixed nostrscript not working on smaller phones (William Casarin)
- Fix zaps sometimes not appearing (William Casarin)
- Fixed issue where reposts would sometimes repost the wrong thing (William Casarin)
- Fixed issue where sometimes there would be empty entries on your profile (William Casarin)
[1.6-13]: https://github.com/damus-io/damus/releases/tag/v1.6-13
## [1.6-11]: "Bugfix Sunday" - 2023-08-07
### Added
- Add close button to custom reactions (Suhail Saqan)
- Add ability to change order of custom reactions (Suhail Saqan)
- Adjustable font size (William Casarin)
### Changed
- Show renotes in Notes timeline (William Casarin)
### Fixed
- Ensure the person you're replying to is the first entry in the reply description (William Casarin)
- Don't cutoff text in notifications (William Casarin)
- Fix wikipedia url detection with parenthesis (William Casarin)
- Fixed old notifications always appearing on first start (William Casarin)
- Fix issue with slashes on relay urls causing relay connection problems (William Casarin)
- Fix rare crash triggered by local notifications (William Casarin)
- Fix crash when long-pressing reactions (William Casarin)
- Fixed nostr reporting decoding (William Casarin)
- Dismiss qr screen on scan (Suhail Saqan)
- Show QRCameraView regardless of same user (Suhail Saqan)
- Fix wiggle when long press reactions (Suhail Saqan)
- Fix reaction button breaking scrolling (Suhail Saqan)
- Fix crash when muting threads (Bryan Montz)
[1.6-11]: https://github.com/damus-io/damus/releases/tag/v1.6-11
## [1.6-8]: "nostrdb prep" 2023-08-03
### Added
- Suggested Users to Follow (Joel Klabo)
- Add support for multiple reactions (Suhail Saqan)
### Changed
- Improved memory usage and performance when processing events (William Casarin)
### Fixed
- Fixed disappearing text on iOS17 (cr0bar)
- Fix UTF support for hashtags (Daniel DAquino)
- Fix compilation error on test target in UserSearchCacheTests (Daniel DAquino)
- Fix nav crashing and buggyness (William Casarin)
- Allow relay logs to be opened in dev mode even if relay (Daniel D'Aquino)
- endless connection attempt loop after user removes relay (Bryan Montz)
[1.6-8]: https://github.com/damus-io/damus/releases/tag/v1.6-8
## 1.6 (7): "Less bad" - 2023-07-16
### Added
- Show nostr address username and support abbreviated _ usernames (William Casarin)
- Re-add nip05 badges to profiles (William Casarin)
- Add space when tagging users in posts if needed (William Casarin)
- Added padding under word count on longform account (William Casarin)
### Fixed
- Don't spam lnurls when validating zaps (William Casarin)
- Eliminate nostr address validation bandwidth on startup (William Casarin)
- Allow user to login to deleted profile (William Casarin)
- Fix issue where typing cc@bob would produce brokenb ccnostr:bob mention (William Casarin)
[1.6-7]: https://github.com/damus-io/damus/releases/tag/v1.6-7
## [1.6-6] - 2023-07-16
### Added
- New markdown renderer (William Casarin)
- Added feedback when user adds a relay that is already on the list (Daniel D'Aquino)
### Changed
- Hide nsec when logging in (cr0bar)
- Remove nip05 on events (William Casarin)
- Rename NIP05 to "nostr address" (William Casarin)
### Fixed
- Fixed issue where hashtags were leaking in DMs (William Casarin)
- Fix issue with emojis next to hashtags and urls (William Casarin)
- relay detail view is not immediately available after adding new relay (Bryan Montz)
- Fix nostr:nostr:... bugs (William Casarin)
[1.6-6]: https://github.com/damus-io/damus/releases/tag/v1.6-6
## [1.6-4] - 2023-07-13
### Added
- Add the ability to follow hashtags (William Casarin)
### Changed
- Remove note size restriction for longform events (William Casarin)
### Fixed
- Hide users and hashtags from home timeline when you unfollow (William Casarin)
- Fixed a bug where following a user might not work due to poor connectivity (William Casarin)
- Icon color for developer mode setting is incorrect in low-light mode (Bryan Montz)
- Fixed nav bar color on login, eula, and account creation (ericholguin)
### Removed
- Remove following Damus Will by default (William Casarin)
[1.6-4]: https://github.com/damus-io/damus/releases/tag/v1.6-4
## [1.6-3] - 2023-07-11
### Changed
- Start at top when reading longform events (William Casarin)
- Allow reposting and quote reposting multiple times (William Casarin)
### Fixed
- Show longform previews in notifications instead of the entire post (William Casarin)
- Fix padding on longform events (William Casarin)
- Fix action bar appearing on quoted longform previews (William Casarin)
[1.6-3]: https://github.com/damus-io/damus/releases/tag/v1.6-3
## [1.6-2] - 2023-07-11
### Added
- Add support for multilingual hashtags (cr0bar)
- Add r tag when mentioning a url (William Casarin)
- Add initial longform note support (William Casarin)
- Enable banner image editing (Joel Klabo)
- Add relay log in developer mode (Bryan Montz)
### Fixed
- Fix lag when creating large posts (William Casarin)
- Fix npub mentions failing to parse in some cases (William Casarin)
- Fix PostView initial string to skip mentioning self when on own profile (Terry Yiu)
- Fix freezing bug when tapping Developer settings menu (Terry Yiu)
- Fix potential fake profile zap attacks (William Casarin)
- Fix issue where malicious zappers can send fake zaps to another user's posts (William Casarin)
- Fix profile post button mentions (cr0bar)
- Fix icons on settings view (cr0bar)
- Fix Invalid Zap bug in reposts (William Casarin)
### Removed
- Remove old @ and & hex key mentions (William Casarin)
[1.6-2]: https://github.com/damus-io/damus/releases/tag/v1.6-2
## [1.6] - 2023-07-04
### Added
- Speed up user search (Terry Yiu)
- Add post button to profile pages (William Casarin)
- Add post button when logged in with private key and on own profile view (Terry Yiu)
### Changed
- Drop iOS15 support (Scott Penrose)
### Fixed
- Load more content on profile view (William Casarin)
- Fix reports to conform to NIP-56 (Terry Yiu)
- Fix profile navigation bugs from muted users list and relay list views (Terry Yiu)
- Fix navigation to translation settings view (Terry Yiu)
- Fixed all navigation issues (Scott Penrose)
- Disable post button when media upload in progress (Terry Yiu)
- Fix taps on mentions in note drafts to not redirect to other Nostr clients (Terry Yiu)
- Fix missing profile zap notification text (Terry Yiu)
[1.6]: https://github.com/damus-io/damus/releases/tag/v1.6
## [1.5-5] - 2023-06-24
### Fixed
@@ -1280,3 +1559,4 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2

13
Makefile Normal file
View File

@@ -0,0 +1,13 @@
all: nostrscript/primal.wasm
nostrscript/%.wasm: nostrscript/%.ts nostrscript/nostr.ts Makefile
asc $< --runtime stub --outFile $@ --optimize
tags:
find damus-c -name '*.c' -or -name '*.h' | xargs ctags
clean:
rm nostrscript/*.wasm
.PHONY: tags

View File

@@ -108,20 +108,9 @@ We have a few mailing lists that anyone can join to get involved in damus develo
[product-list]: https://damus.io/list/product
[design-list]: https://damus.io/list/design
### Code
### Contributing
[Email patches][git-send-email] to patches@damus.io are preferred, but I accept PRs on GitHub as well. Patches sent via email may include a bolt11 lightning invoice, choosing the price you think the patch is worth, and I will pay it once the patch is accepted and if I think the price isn't unreasonable. You can also send an any-amount invoice and I will pay what I think it's worth if you prefer not to choose. You can include the bolt11 in the commit body or email so that it can be paid once it is applied.
Recommended settings when submitting code via email:
```
$ git config sendemail.to "patches@damus.io"
$ git config format.subjectPrefix "PATCH damus"
$ git config --global sendemail.annotate yes
$ git config format.signOff yes
```
[git-send-email]: http://git-send-email.io
See [docs/CONTRIBUTING.md](./docs/CONTRIBUTING.md)
### Privacy
Your internet protocol (IP) address is exposed to the relays you connect to, and third party media hosters (e.g. nostr.build, imgur.com, giphy.com, youtube.com etc.) that render on Damus. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.

0
TODO Normal file
View File

View File

@@ -35,7 +35,7 @@ typedef struct mention_bech32_block {
struct nostr_bech32 bech32;
} mention_bech32_block_t;
typedef struct block {
typedef struct note_block {
enum block_type type;
union {
struct str_block str;
@@ -45,12 +45,13 @@ typedef struct block {
} block;
} block_t;
typedef struct blocks {
typedef struct note_blocks {
int words;
int num_blocks;
struct block *blocks;
struct note_block *blocks;
} blocks_t;
void blocks_init(struct blocks *blocks);
void blocks_free(struct blocks *blocks);
void blocks_init(struct note_blocks *blocks);
void blocks_free(struct note_blocks *blocks);
#endif /* block_h */

View File

@@ -1,57 +1,629 @@
//
// cursor.h
// damus
//
// Created by William Casarin on 2023-04-09.
//
#ifndef cursor_h
#define cursor_h
#ifndef JB55_CURSOR_H
#define JB55_CURSOR_H
#include "typedefs.h"
#include "varint.h"
#include <stdio.h>
#include <ctype.h>
#include <assert.h>
#include <string.h>
typedef unsigned char u8;
#define unlikely(x) __builtin_expect((x),0)
#define likely(x) __builtin_expect((x),1)
struct cursor {
const u8 *p;
const u8 *start;
const u8 *end;
unsigned char *start;
unsigned char *p;
unsigned char *end;
};
struct array {
struct cursor cur;
unsigned int elem_size;
};
static inline void reset_cursor(struct cursor *cursor)
{
cursor->p = cursor->start;
}
static inline void wipe_cursor(struct cursor *cursor)
{
reset_cursor(cursor);
memset(cursor->start, 0, cursor->end - cursor->start);
}
static inline void make_cursor(u8 *start, u8 *end, struct cursor *cursor)
{
cursor->start = start;
cursor->p = start;
cursor->end = end;
}
static inline void make_array(struct array *a, u8* start, u8 *end, unsigned int elem_size)
{
make_cursor(start, end, &a->cur);
a->elem_size = elem_size;
}
static inline int cursor_eof(struct cursor *c)
{
return c->p == c->end;
}
static inline void *cursor_malloc(struct cursor *mem, unsigned long size)
{
void *ret;
if (mem->p + size > mem->end) {
return NULL;
}
ret = mem->p;
mem->p += size;
return ret;
}
static inline void *cursor_alloc(struct cursor *mem, unsigned long size)
{
void *ret;
if (!(ret = cursor_malloc(mem, size))) {
return 0;
}
memset(ret, 0, size);
return ret;
}
static inline int cursor_slice(struct cursor *mem, struct cursor *slice, size_t size)
{
u8 *p;
if (!(p = cursor_alloc(mem, size))) {
return 0;
}
make_cursor(p, mem->p, slice);
return 1;
}
static inline void copy_cursor(struct cursor *src, struct cursor *dest)
{
dest->start = src->start;
dest->p = src->p;
dest->end = src->end;
}
static inline int pull_byte(struct cursor *cursor, u8 *c)
{
if (unlikely(cursor->p >= cursor->end))
return 0;
*c = *cursor->p;
cursor->p++;
return 1;
}
static inline int parse_byte(struct cursor *cursor, u8 *c)
{
if (unlikely(cursor->p >= cursor->end))
return 0;
*c = *cursor->p;
//cursor->p++;
return 1;
}
static inline int parse_char(struct cursor *cur, char c) {
if (cur->p >= cur->end)
return 0;
if (*cur->p == c) {
cur->p++;
return 1;
}
return 0;
}
static inline int peek_char(struct cursor *cur, int ind) {
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
return -1;
return *(cur->p + ind);
}
static inline int cursor_pull_c_str(struct cursor *cursor, const char **str)
{
*str = (const char*)cursor->p;
for (; cursor->p < cursor->end; cursor->p++) {
if (*cursor->p == 0) {
cursor->p++;
return 1;
}
}
return 0;
}
static inline int cursor_push_byte(struct cursor *cursor, u8 c)
{
if (unlikely(cursor->p + 1 > cursor->end)) {
return 0;
}
*cursor->p = c;
cursor->p++;
return 1;
}
static inline int cursor_pull(struct cursor *cursor, u8 *data, int len)
{
if (unlikely(cursor->p + len > cursor->end)) {
return 0;
}
memcpy(data, cursor->p, len);
cursor->p += len;
return 1;
}
static inline int pull_data_into_cursor(struct cursor *cursor,
struct cursor *dest,
unsigned char **data,
int len)
{
int ok;
if (unlikely(dest->p + len > dest->end)) {
printf("not enough room in dest buffer\n");
return 0;
}
ok = cursor_pull(cursor, dest->p, len);
if (!ok) return 0;
*data = dest->p;
dest->p += len;
return 1;
}
static inline int cursor_dropn(struct cursor *cur, int size, int n)
{
if (n == 0)
return 1;
if (unlikely(cur->p - size*n < cur->start)) {
return 0;
}
cur->p -= size*n;
return 1;
}
static inline int cursor_drop(struct cursor *cur, int size)
{
return cursor_dropn(cur, size, 1);
}
static inline unsigned char *cursor_topn(struct cursor *cur, int len, int n)
{
n += 1;
if (unlikely(cur->p - len*n < cur->start)) {
return NULL;
}
return cur->p - len*n;
}
static inline unsigned char *cursor_top(struct cursor *cur, int len)
{
if (unlikely(cur->p - len < cur->start)) {
return NULL;
}
return cur->p - len;
}
static inline int cursor_top_int(struct cursor *cur, int *i)
{
u8 *p;
if (unlikely(!(p = cursor_top(cur, sizeof(*i))))) {
return 0;
}
*i = *((int*)p);
return 1;
}
static inline int cursor_pop(struct cursor *cur, u8 *data, int len)
{
if (unlikely(cur->p - len < cur->start)) {
return 0;
}
cur->p -= len;
memcpy(data, cur->p, len);
return 1;
}
static inline int cursor_push(struct cursor *cursor, u8 *data, int len)
{
if (unlikely(cursor->p + len >= cursor->end)) {
return 0;
}
if (cursor->p != data)
memcpy(cursor->p, data, len);
cursor->p += len;
return 1;
}
static inline int cursor_push_int(struct cursor *cursor, int i)
{
return cursor_push(cursor, (u8*)&i, sizeof(i));
}
static inline size_t cursor_count(struct cursor *cursor, size_t elem_size)
{
return (cursor->p - cursor->start)/elem_size;
}
/* TODO: push_varint */
static inline int push_varint(struct cursor *cursor, int n)
{
int ok, len;
unsigned char b;
len = 0;
while (1) {
b = (n & 0xFF) | 0x80;
n >>= 7;
if (n == 0) {
b &= 0x7F;
ok = cursor_push_byte(cursor, b);
len++;
if (!ok) return 0;
break;
}
ok = cursor_push_byte(cursor, b);
len++;
if (!ok) return 0;
}
return len;
}
/* TODO: pull_varint */
static inline int pull_varint(struct cursor *cursor, int *n)
{
int ok, i;
unsigned char b;
*n = 0;
for (i = 0;; i++) {
ok = pull_byte(cursor, &b);
if (!ok) return 0;
*n |= ((int)b & 0x7F) << (i * 7);
/* is_last */
if ((b & 0x80) == 0) {
return i+1;
}
if (i == 4) return 0;
}
return 0;
}
static inline int cursor_pull_int(struct cursor *cursor, int *i)
{
return cursor_pull(cursor, (u8*)i, sizeof(*i));
}
static inline int cursor_push_u32(struct cursor *cursor, uint32_t i) {
return cursor_push(cursor, (unsigned char*)&i, sizeof(i));
}
static inline int cursor_push_u16(struct cursor *cursor, u16 i)
{
return cursor_push(cursor, (u8*)&i, sizeof(i));
}
static inline void *index_cursor(struct cursor *cursor, unsigned int index, int elem_size)
{
u8 *p;
p = &cursor->start[elem_size * index];
if (unlikely(p >= cursor->end))
return NULL;
return (void*)p;
}
static inline int push_sized_str(struct cursor *cursor, const char *str, int len)
{
return cursor_push(cursor, (u8*)str, len);
}
static inline int cursor_push_str(struct cursor *cursor, const char *str)
{
return cursor_push(cursor, (u8*)str, (int)strlen(str));
}
static inline int cursor_push_c_str(struct cursor *cursor, const char *str)
{
return cursor_push_str(cursor, str) && cursor_push_byte(cursor, 0);
}
/* TODO: push varint size */
static inline int push_prefixed_str(struct cursor *cursor, const char *str)
{
int ok, len;
len = (int)strlen(str);
ok = push_varint(cursor, len);
if (!ok) return 0;
return push_sized_str(cursor, str, len);
}
static inline int pull_prefixed_str(struct cursor *cursor, struct cursor *dest_buf, const char **str)
{
int len, ok;
ok = pull_varint(cursor, &len);
if (!ok) return 0;
if (unlikely(dest_buf->p + len > dest_buf->end)) {
return 0;
}
ok = pull_data_into_cursor(cursor, dest_buf, (unsigned char**)str, len);
if (!ok) return 0;
ok = cursor_push_byte(dest_buf, 0);
return 1;
}
static inline int cursor_remaining_capacity(struct cursor *cursor)
{
return (int)(cursor->end - cursor->p);
}
#define max(a,b) ((a) > (b) ? (a) : (b))
static inline void cursor_print_around(struct cursor *cur, int range)
{
unsigned char *c;
printf("[%ld/%ld]\n", cur->p - cur->start, cur->end - cur->start);
c = max(cur->p - range, cur->start);
for (; c < cur->end && c < (cur->p + range); c++) {
printf("%02x", *c);
}
printf("\n");
c = max(cur->p - range, cur->start);
for (; c < cur->end && c < (cur->p + range); c++) {
if (c == cur->p) {
printf("^");
continue;
}
printf(" ");
}
printf("\n");
}
#undef max
static inline int pull_bytes(struct cursor *cur, int count, const u8 **bytes) {
if (cur->p + count > cur->end)
return 0;
*bytes = cur->p;
cur->p += count;
return 1;
}
static inline int parse_str(struct cursor *cur, const char *str) {
int i;
char c, cs;
unsigned long len;
len = strlen(str);
if (cur->p + len >= cur->end)
return 0;
for (i = 0; i < len; i++) {
c = tolower(cur->p[i]);
cs = tolower(str[i]);
if (c != cs)
return 0;
}
cur->p += len;
return 1;
}
static inline int is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
static inline int is_boundary(char c) {
return !isalnum(c);
static inline int is_underscore(char c) {
return c == '_';
}
static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
static inline int is_utf8_byte(u8 c) {
return c & 0x80;
}
static inline int parse_utf8_char(struct cursor *cursor, unsigned int *code_point, unsigned int *utf8_length)
{
u8 first_byte;
if (!parse_byte(cursor, &first_byte))
return 0; // Not enough data
// Determine the number of bytes in this UTF-8 character
int remaining_bytes = 0;
if (first_byte < 0x80) {
*code_point = first_byte;
return 1;
} else if ((first_byte & 0xE0) == 0xC0) {
remaining_bytes = 1;
*utf8_length = remaining_bytes + 1;
*code_point = first_byte & 0x1F;
} else if ((first_byte & 0xF0) == 0xE0) {
remaining_bytes = 2;
*utf8_length = remaining_bytes + 1;
*code_point = first_byte & 0x0F;
} else if ((first_byte & 0xF8) == 0xF0) {
remaining_bytes = 3;
*utf8_length = remaining_bytes + 1;
*code_point = first_byte & 0x07;
} else {
remaining_bytes = 0;
*utf8_length = 1; // Assume 1 byte length for unrecognized UTF-8 characters
// TODO: We need to gracefully handle unrecognized UTF-8 characters
printf("Invalid UTF-8 byte: %x\n", *code_point);
*code_point = ((first_byte & 0xF0) << 6); // Prevent testing as punctuation
return 0; // Invalid first byte
}
// Peek at remaining bytes
for (int i = 0; i < remaining_bytes; ++i) {
signed char next_byte;
if ((next_byte = peek_char(cursor, i+1)) == -1) {
*utf8_length = 1;
return 0; // Not enough data
}
// Debugging lines
//printf("Cursor: %s\n", cursor->p);
//printf("Codepoint: %x\n", *code_point);
//printf("Codepoint <<6: %x\n", ((*code_point << 6) | (next_byte & 0x3F)));
//printf("Remaining bytes: %x\n", remaining_bytes);
//printf("First byte: %x\n", first_byte);
//printf("Next byte: %x\n", next_byte);
//printf("Bitwise AND result: %x\n", (next_byte & 0xC0));
if ((next_byte & 0xC0) != 0x80) {
*utf8_length = 1;
return 0; // Invalid byte in sequence
}
*code_point = (*code_point << 6) | (next_byte & 0x3F);
}
return 1;
}
/**
* Checks if a given Unicode code point is a punctuation character
*
* @param codepoint The Unicode code point to check. @return true if the
* code point is a punctuation character, false otherwise.
*/
static inline int is_punctuation(unsigned int codepoint) {
// Check for underscore (underscore is not treated as punctuation)
if (is_underscore(codepoint))
return 0;
// Check for ASCII punctuation
if (ispunct(codepoint))
return 1;
// Check for Unicode punctuation exceptions (punctuation allowed in hashtags)
if (codepoint == 0x301C || codepoint == 0xFF5E) // Japanese Wave Dash / Tilde
return 0;
// Check for Unicode punctuation
// NOTE: We may need to adjust the codepoint ranges in the future,
// to include/exclude certain types of Unicode characters in hashtags.
// Unicode Blocks Reference: https://www.compart.com/en/unicode/block
return (
// Latin-1 Supplement No-Break Space (NBSP): U+00A0
(codepoint == 0x00A0) ||
// Latin-1 Supplement Punctuation: U+00A1 to U+00BF
(codepoint >= 0x00A1 && codepoint <= 0x00BF) ||
// General Punctuation: U+2000 to U+206F
(codepoint >= 0x2000 && codepoint <= 0x206F) ||
// Currency Symbols: U+20A0 to U+20CF
(codepoint >= 0x20A0 && codepoint <= 0x20CF) ||
// Supplemental Punctuation: U+2E00 to U+2E7F
(codepoint >= 0x2E00 && codepoint <= 0x2E7F) ||
// CJK Symbols and Punctuation: U+3000 to U+303F
(codepoint >= 0x3000 && codepoint <= 0x303F) ||
// Ideographic Description Characters: U+2FF0 to U+2FFF
(codepoint >= 0x2FF0 && codepoint <= 0x2FFF)
);
}
static inline int is_right_boundary(int c) {
return is_whitespace(c) || is_punctuation(c);
}
static inline int is_left_boundary(char c) {
return is_right_boundary(c) || is_utf8_byte(c);
}
static inline int is_alphanumeric(char c) {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
}
static inline void make_cursor(struct cursor *c, const u8 *content, size_t len)
{
c->start = content;
c->end = content + len;
c->p = content;
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
}
static inline int consume_until_boundary(struct cursor *cur) {
char c;
unsigned int c;
unsigned int char_length = 1;
unsigned int *utf8_char_length = &char_length;
while (cur->p < cur->end) {
c = *cur->p;
if (is_boundary(c))
*utf8_char_length = 1;
if (is_whitespace(c))
return 1;
cur->p++;
// Need to check for UTF-8 characters, which can be multiple bytes long
if (is_utf8_byte(c)) {
if (!parse_utf8_char(cur, &c, utf8_char_length)) {
if (!is_right_boundary(c)){
// TODO: We should work towards handling all UTF-8 characters.
printf("Invalid UTF-8 code point: %x\n", c);
}
}
}
if (is_right_boundary(c))
return 1;
// Need to use a variable character byte length for UTF-8 (2-4 bytes)
if (cur->p + *utf8_char_length <= cur->end)
cur->p += *utf8_char_length;
else
cur->p++;
}
return 1;
@@ -91,66 +663,4 @@ static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end)
return or_end;
}
static inline int parse_char(struct cursor *cur, char c) {
if (cur->p >= cur->end)
return 0;
if (*cur->p == c) {
cur->p++;
return 1;
}
return 0;
}
static inline int peek_char(struct cursor *cur, int ind) {
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
return -1;
return *(cur->p + ind);
}
static inline int pull_byte(struct cursor *cur, u8 *byte) {
if (cur->p >= cur->end)
return 0;
*byte = *cur->p;
cur->p++;
return 1;
}
static inline int pull_bytes(struct cursor *cur, int count, const u8 **bytes) {
if (cur->p + count > cur->end)
return 0;
*bytes = cur->p;
cur->p += count;
return 1;
}
static inline int parse_str(struct cursor *cur, const char *str) {
int i;
char c, cs;
unsigned long len;
len = strlen(str);
if (cur->p + len >= cur->end)
return 0;
for (i = 0; i < len; i++) {
c = tolower(cur->p[i]);
cs = tolower(str[i]);
if (c != cs)
return 0;
}
cur->p += len;
return 1;
}
#endif /* cursor_h */
#endif

View File

@@ -5,3 +5,9 @@
#include "damus.h"
#include "bolt11.h"
#include "amount.h"
#include "nostr_bech32.h"
#include "wasm.h"
#include "nostrscript.h"
#include "nostrdb.h"
#include "lmdb.h"

View File

@@ -28,9 +28,9 @@ static int parse_digit(struct cursor *cur, int *digit) {
}
static int parse_mention_index(struct cursor *cur, struct block *block) {
static int parse_mention_index(struct cursor *cur, struct note_block *block) {
int d1, d2, d3, ind;
const u8 *start = cur->p;
u8 *start = cur->p;
if (!parse_str(cur, "#["))
return 0;
@@ -59,9 +59,9 @@ static int parse_mention_index(struct cursor *cur, struct block *block) {
return 1;
}
static int parse_hashtag(struct cursor *cur, struct block *block) {
static int parse_hashtag(struct cursor *cur, struct note_block *block) {
int c;
const u8 *start = cur->p;
u8 *start = cur->p;
if (!parse_char(cur, '#'))
return 0;
@@ -81,7 +81,7 @@ static int parse_hashtag(struct cursor *cur, struct block *block) {
return 1;
}
static int add_block(struct blocks *blocks, struct block block)
static int add_block(struct note_blocks *blocks, struct note_block block)
{
if (blocks->num_blocks + 1 >= MAX_BLOCKS)
return 0;
@@ -90,9 +90,9 @@ static int add_block(struct blocks *blocks, struct block block)
return 1;
}
static int add_text_block(struct blocks *blocks, const u8 *start, const u8 *end)
static int add_text_block(struct note_blocks *blocks, const u8 *start, const u8 *end)
{
struct block b;
struct note_block b;
if (start == end)
return 1;
@@ -104,8 +104,71 @@ static int add_text_block(struct blocks *blocks, const u8 *start, const u8 *end)
return add_block(blocks, b);
}
static int parse_url(struct cursor *cur, struct block *block) {
const u8 *start = cur->p;
static int consume_url_fragment(struct cursor *cur)
{
int c;
if ((c = peek_char(cur, 0)) < 0)
return 1;
if (c != '#' && c != '?') {
return 1;
}
cur->p++;
return consume_until_whitespace(cur, 1);
}
static int consume_url_path(struct cursor *cur)
{
int c;
if ((c = peek_char(cur, 0)) < 0)
return 1;
if (c != '/') {
return 1;
}
while (cur->p < cur->end) {
c = *cur->p;
if (c == '?' || c == '#' || is_whitespace(c)) {
return 1;
}
cur->p++;
}
return 1;
}
static int consume_url_host(struct cursor *cur)
{
char c;
int count = 0;
while (cur->p < cur->end) {
c = *cur->p;
// TODO: handle IDNs
if (is_alphanumeric(c) || c == '.' || c == '-')
{
count++;
cur->p++;
continue;
}
return count != 0;
}
// this means the end of the URL hostname is the end of the buffer and we finished
return count != 0;
}
static int parse_url(struct cursor *cur, struct note_block *block) {
u8 *start = cur->p;
if (!parse_str(cur, "http"))
return 0;
@@ -121,15 +184,25 @@ static int parse_url(struct cursor *cur, struct block *block) {
return 0;
}
}
if (!consume_until_whitespace(cur, 1)) {
if (!(consume_url_host(cur) &&
consume_url_path(cur) &&
consume_url_fragment(cur)))
{
cur->p = start;
return 0;
}
// strip any unwanted characters
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
// smart parens
if (start - 1 >= 0 &&
start < cur->end &&
*(start - 1) == '(' &&
(cur->p - 1) < cur->end &&
*(cur->p - 1) == ')')
{
cur->p--;
}
block->type = BLOCK_URL;
block->block.str.start = (const char *)start;
block->block.str.end = (const char *)cur->p;
@@ -137,8 +210,8 @@ static int parse_url(struct cursor *cur, struct block *block) {
return 1;
}
static int parse_invoice(struct cursor *cur, struct block *block) {
const u8 *start, *end;
static int parse_invoice(struct cursor *cur, struct note_block *block) {
u8 *start, *end;
char *fail;
struct bolt11 *bolt11;
// optional
@@ -177,12 +250,12 @@ static int parse_invoice(struct cursor *cur, struct block *block) {
}
static int parse_mention_bech32(struct cursor *cur, struct block *block) {
const u8 *start = cur->p;
if (!parse_str(cur, "nostr:"))
return 0;
static int parse_mention_bech32(struct cursor *cur, struct note_block *block) {
u8 *start = cur->p;
parse_char(cur, '@');
parse_str(cur, "nostr:");
block->block.str.start = (const char *)cur->p;
if (!parse_nostr_bech32(cur, &block->block.mention_bech32.bech32)) {
@@ -197,7 +270,7 @@ static int parse_mention_bech32(struct cursor *cur, struct block *block) {
return 1;
}
static int add_text_then_block(struct cursor *cur, struct blocks *blocks, struct block block, const u8 **start, const u8 *pre_mention)
static int add_text_then_block(struct cursor *cur, struct note_blocks *blocks, struct note_block block, u8 **start, const u8 *pre_mention)
{
if (!add_text_block(blocks, *start, pre_mention))
return 0;
@@ -210,22 +283,28 @@ static int add_text_then_block(struct cursor *cur, struct blocks *blocks, struct
return 1;
}
int damus_parse_content(struct blocks *blocks, const char *content) {
int damus_parse_content(struct note_blocks *blocks, const char *content) {
int cp, c;
struct cursor cur;
struct block block;
const u8 *start, *pre_mention;
struct note_block block;
u8 *start, *pre_mention;
blocks->words = 0;
blocks->num_blocks = 0;
make_cursor(&cur, (const u8*)content, strlen(content));
make_cursor((u8*)content, (u8*)content + strlen(content), &cur);
start = cur.p;
while (cur.p < cur.end && blocks->num_blocks < MAX_BLOCKS) {
cp = peek_char(&cur, -1);
c = peek_char(&cur, 0);
// new word
if (is_whitespace(cp) && !is_whitespace(c)) {
blocks->words++;
}
pre_mention = cur.p;
if (cp == -1 || is_whitespace(cp) || c == '#') {
if (cp == -1 || is_left_boundary(cp) || c == '#') {
if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0;
@@ -238,7 +317,7 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0;
continue;
} else if (c == 'n' && parse_mention_bech32(&cur, &block)) {
} else if ((c == 'n' || c == '@') && parse_mention_bech32(&cur, &block)) {
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0;
continue;
@@ -256,12 +335,12 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
return 1;
}
void blocks_init(struct blocks *blocks) {
blocks->blocks = malloc(sizeof(struct block) * MAX_BLOCKS);
void blocks_init(struct note_blocks *blocks) {
blocks->blocks = malloc(sizeof(struct note_block) * MAX_BLOCKS);
blocks->num_blocks = 0;
}
void blocks_free(struct blocks *blocks) {
void blocks_free(struct note_blocks *blocks) {
if (!blocks->blocks) {
return;
}

View File

@@ -9,10 +9,10 @@
#define damus_h
#include <stdio.h>
#include "nostr_bech32.h"
#include "block.h"
typedef unsigned char u8;
int damus_parse_content(struct blocks *blocks, const char *content);
int damus_parse_content(struct note_blocks *blocks, const char *content);
#endif /* damus_h */

15
damus-c/debug.h Normal file
View File

@@ -0,0 +1,15 @@
#ifndef PROTOVERSE_DEBUG_H
#define PROTOVERSE_DEBUG_H
#include <stdio.h>
#define unusual(...) fprintf(stderr, "UNUSUAL: " __VA_ARGS__)
#ifdef DEBUG
#define debug(...) printf(__VA_ARGS__)
#else
#define debug(...)
#endif
#endif /* PROTOVERSE_DEBUG_H */

34
damus-c/error.c Normal file
View File

@@ -0,0 +1,34 @@
#include "error.h"
#include <stdlib.h>
#include <stdarg.h>
int note_error_(struct errors *errs_, struct cursor *p, const char *fmt, ...)
{
static char buf[512];
struct error err;
struct cursor *errs;
va_list ap;
errs = &errs_->cur;
if (errs_->enabled == 0)
return 0;
va_start(ap, fmt);
vsprintf(buf, fmt, ap);
va_end(ap);
err.msg = buf;
err.pos = p ? (int)(p->p - p->start) : 0;
if (!cursor_push_error(errs, &err)) {
fprintf(stderr, "arena OOM when recording error, ");
fprintf(stderr, "errs->p at %ld, remaining %ld, strlen %ld\n",
errs->p - errs->start, errs->end - errs->p, strlen(buf));
}
return 0;
}

33
damus-c/error.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef PROTOVERSE_ERROR_H
#define PROTOVERSE_ERROR_H
#include "cursor.h"
struct error {
int pos;
const char *msg;
};
struct errors {
struct cursor cur;
int enabled;
};
#define note_error(errs, p, fmt, ...) note_error_(errs, p, "%s: " fmt, __FUNCTION__, ##__VA_ARGS__)
static inline int cursor_push_error(struct cursor *cur, struct error *err)
{
return cursor_push_int(cur, err->pos) &&
cursor_push_c_str(cur, err->msg);
}
static inline int cursor_pull_error(struct cursor *cur, struct error *err)
{
return cursor_pull_int(cur, &err->pos) &&
cursor_pull_c_str(cur, &err->msg);
}
int note_error_(struct errors *errs, struct cursor *p, const char *fmt, ...);
#endif /* PROTOVERSE_ERROR_H */

View File

@@ -39,15 +39,6 @@ bool hex_decode(const char *str, size_t slen, void *buf, size_t bufsize)
return slen == 0 && bufsize == 0;
}
static char hexchar(unsigned int val)
{
if (val < 10)
return '0' + val;
if (val < 16)
return 'a' + val - 10;
abort();
}
bool hex_encode(const void *buf, size_t bufsize, char *dest, size_t destsize)
{
size_t i;

View File

@@ -70,4 +70,15 @@ static inline size_t hex_data_size(size_t strlen)
{
return strlen / 2;
}
static inline char hexchar(unsigned int val)
{
if (val < 10)
return '0' + val;
if (val < 16)
return 'a' + val - 10;
abort();
}
#endif /* CCAN_HEX_H */

View File

@@ -52,9 +52,13 @@
*/
#define unlikely(cond) __builtin_expect(!!(cond), 0)
#else
#ifndef likely
#define likely(cond) (!!(cond))
#endif
#ifndef unlikely
#define unlikely(cond) (!!(cond))
#endif
#endif
#else /* CCAN_LIKELY_DEBUG versions */
#include <ccan/str/str.h>

View File

@@ -91,6 +91,9 @@ static int parse_nostr_bech32_type(const char *prefix, enum nostr_bech32_type *t
} else if (strcmp(prefix, "npub") == 0) {
*type = NOSTR_BECH32_NPUB;
return 1;
} else if (strcmp(prefix, "nsec") == 0) {
*type = NOSTR_BECH32_NSEC;
return 1;
} else if (strcmp(prefix, "nprofile") == 0) {
*type = NOSTR_BECH32_NPROFILE;
return 1;
@@ -116,6 +119,10 @@ static int parse_nostr_bech32_npub(struct cursor *cur, struct bech32_npub *npub)
return pull_bytes(cur, 32, &npub->pubkey);
}
static int parse_nostr_bech32_nsec(struct cursor *cur, struct bech32_nsec *nsec) {
return pull_bytes(cur, 32, &nsec->nsec);
}
static int tlvs_to_relays(struct nostr_tlvs *tlvs, struct relays *relays) {
struct nostr_tlv *tlv;
struct str_block *str;
@@ -218,7 +225,7 @@ static int parse_nostr_bech32_nrelay(struct cursor *cur, struct bech32_nrelay *n
}
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
const u8 *start, *end;
u8 *start, *end;
start = cur->p;
@@ -257,7 +264,7 @@ int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
}
struct cursor bcur;
make_cursor(&bcur, obj->buffer, obj->buflen);
make_cursor(obj->buffer, obj->buffer + obj->buflen, &bcur);
switch (obj->type) {
case NOSTR_BECH32_NOTE:
@@ -268,6 +275,10 @@ int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
if (!parse_nostr_bech32_npub(&bcur, &obj->data.npub))
goto fail;
break;
case NOSTR_BECH32_NSEC:
if (!parse_nostr_bech32_nsec(&bcur, &obj->data.nsec))
goto fail;
break;
case NOSTR_BECH32_NEVENT:
if (!parse_nostr_bech32_nevent(&bcur, &obj->data.nevent))
goto fail;

View File

@@ -26,6 +26,7 @@ enum nostr_bech32_type {
NOSTR_BECH32_NEVENT = 4,
NOSTR_BECH32_NRELAY = 5,
NOSTR_BECH32_NADDR = 6,
NOSTR_BECH32_NSEC = 7,
};
struct bech32_note {
@@ -36,6 +37,10 @@ struct bech32_npub {
const u8 *pubkey;
};
struct bech32_nsec {
const u8 *nsec;
};
struct bech32_nevent {
struct relays relays;
const u8 *event_id;
@@ -65,6 +70,7 @@ typedef struct nostr_bech32 {
union {
struct bech32_note note;
struct bech32_npub npub;
struct bech32_nsec nsec;
struct bech32_nevent nevent;
struct bech32_nprofile nprofile;
struct bech32_naddr naddr;

42
damus-c/parser.h Normal file
View File

@@ -0,0 +1,42 @@
#ifndef CURSOR_PARSER
#define CURSOR_PARSER
#include "cursor.h"
static int consume_bytes(struct cursor *cursor, const unsigned char *match, int len)
{
int i;
if (cursor->p + len > cursor->end) {
fprintf(stderr, "consume_bytes overflow\n");
return 0;
}
for (i = 0; i < len; i++) {
if (cursor->p[i] != match[i])
return 0;
}
cursor->p += len;
return 1;
}
static inline int consume_byte(struct cursor *cursor, unsigned char match)
{
if (unlikely(cursor->p >= cursor->end))
return 0;
if (*cursor->p != match)
return 0;
cursor->p++;
return 1;
}
static inline int consume_u32(struct cursor *cursor, unsigned int match)
{
return consume_bytes(cursor, (unsigned char*)&match, sizeof(match));
}
#endif /* CURSOR_PARSER */

14
damus-c/typedefs.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef PROTOVERSE_TYPEDEFS_H
#define PROTOVERSE_TYPEDEFS_H
#include <stdint.h>
typedef unsigned char u8;
typedef unsigned int u32;
typedef unsigned short u16;
typedef uint64_t u64;
typedef int64_t s64;
#endif /* PROTOVERSE_TYPEDEFS_H */

14
damus-c/varint.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef PROTOVERSE_VARINT_H
#define PROTOVERSE_VARINT_H
#define VARINT_MAX_LEN 9
#include <stddef.h>
#include <stdint.h>
size_t varint_put(unsigned char buf[VARINT_MAX_LEN], uint64_t v);
size_t varint_size(uint64_t v);
size_t varint_get(const unsigned char *p, size_t max, int64_t *val);
#endif /* PROTOVERSE_VARINT_H */

7299
damus-c/wasm.c Normal file

File diff suppressed because it is too large Load Diff

850
damus-c/wasm.h Normal file
View File

@@ -0,0 +1,850 @@
#ifndef PROTOVERSE_WASM_H
#define PROTOVERSE_WASM_H
static const unsigned char WASM_MAGIC[] = {0,'a','s','m'};
#define WASM_VERSION 0x01
#define MAX_U32_LEB128_BYTES 5
#define MAX_U64_LEB128_BYTES 10
#define MAX_CUSTOM_SECTIONS 32
#define MAX_BUILTINS 64
#define BUILTIN_SUSPEND 42
#define FUNC_TYPE_TAG 0x60
#include "cursor.h"
#include "error.h"
#ifdef NOINLINE
#define INLINE __attribute__((noinline))
#else
#define INLINE inline
#endif
#define interp_error(p, fmt, ...) note_error(&((p)->errors), interp_codeptr(p), fmt, ##__VA_ARGS__)
#define parse_err(p, fmt, ...) note_error(&((p)->errs), &(p)->cur, fmt, ##__VA_ARGS__)
enum valtype {
val_i32 = 0x7F,
val_i64 = 0x7E,
val_f32 = 0x7D,
val_f64 = 0x7C,
val_ref_null = 0xD0,
val_ref_func = 0x70,
val_ref_extern = 0x6F,
};
enum const_instr {
ci_const_i32 = 0x41,
ci_const_i64 = 0x42,
ci_const_f32 = 0x43,
ci_const_f64 = 0x44,
ci_ref_null = 0xD0,
ci_ref_func = 0xD2,
ci_global_get = 0x23,
ci_end = 0x0B,
};
enum limit_type {
limit_min = 0x00,
limit_min_max = 0x01,
};
struct limits {
u32 min;
u32 max;
enum limit_type type;
};
enum section_tag {
section_custom,
section_type,
section_import,
section_function,
section_table,
section_memory,
section_global,
section_export,
section_start,
section_element,
section_code,
section_data,
section_data_count,
section_name,
num_sections,
};
enum name_subsection_tag {
name_subsection_module,
name_subsection_funcs,
name_subsection_locals,
num_name_subsections,
};
enum reftype {
funcref = 0x70,
externref = 0x6F,
};
struct resulttype {
unsigned char *valtypes; /* enum valtype */
u32 num_valtypes;
};
struct functype {
struct resulttype params;
struct resulttype result;
};
struct table {
enum reftype reftype;
struct limits limits;
};
struct tablesec {
struct table *tables;
u32 num_tables;
};
enum elem_mode {
elem_mode_passive,
elem_mode_active,
elem_mode_declarative,
};
struct expr {
u8 *code;
u32 code_len;
};
struct refval {
u32 addr;
};
struct table_inst {
struct refval *refs;
enum reftype reftype;
u32 num_refs;
};
struct numval {
union {
int i32;
u32 u32;
int64_t i64;
uint64_t u64;
float f32;
double f64;
};
};
struct val {
enum valtype type;
union {
struct numval num;
struct refval ref;
};
};
struct elem_inst {
struct val val;
u16 elem;
u16 init;
};
struct elem {
struct expr offset;
u32 tableidx;
struct expr *inits;
u32 num_inits;
enum elem_mode mode;
enum reftype reftype;
struct val val;
};
struct customsec {
const char *name;
unsigned char *data;
u32 data_len;
};
struct elemsec {
struct elem *elements;
u32 num_elements;
};
struct memsec {
struct limits *mems; /* memtype */
u32 num_mems;
};
struct funcsec {
u32 *type_indices;
u32 num_indices;
};
enum mut {
mut_const,
mut_var,
};
struct globaltype {
enum valtype valtype;
enum mut mut;
};
struct globalsec {
struct global *globals;
u32 num_globals;
};
struct typesec {
struct functype *functypes;
u32 num_functypes;
};
enum import_type {
import_func,
import_table,
import_mem,
import_global,
};
struct importdesc {
enum import_type type;
union {
u32 typeidx;
struct limits tabletype;
struct limits memtype;
struct globaltype globaltype;
};
};
struct import {
const char *module_name;
const char *name;
struct importdesc desc;
int resolved_builtin;
};
struct importsec {
struct import *imports;
u32 num_imports;
};
struct global {
struct globaltype type;
struct expr init;
struct val val;
};
struct local_def {
u32 num_types;
enum valtype type;
};
/* "code" */
struct wasm_func {
struct expr code;
struct local_def *local_defs;
u32 num_local_defs;
};
enum func_type {
func_type_wasm,
func_type_builtin,
};
struct func {
union {
struct wasm_func *wasm_func;
struct builtin *builtin;
};
u32 num_locals;
struct functype *functype;
enum func_type type;
const char *name;
u32 idx;
};
struct codesec {
struct wasm_func *funcs;
u32 num_funcs;
};
enum exportdesc {
export_func,
export_table,
export_mem,
export_global,
};
struct wexport {
const char *name;
u32 index;
enum exportdesc desc;
};
struct exportsec {
struct wexport *exports;
u32 num_exports;
};
struct nameassoc {
u32 index;
const char *name;
};
struct namemap {
struct nameassoc *names;
u32 num_names;
};
struct namesec {
const char *module_name;
struct namemap func_names;
int parsed;
};
struct wsection {
enum section_tag tag;
};
enum bulk_tag {
i_memory_copy = 10,
i_memory_fill = 11,
i_table_init = 12,
i_elem_drop = 13,
i_table_copy = 14,
i_table_grow = 15,
i_table_size = 16,
i_table_fill = 17,
};
enum instr_tag {
/* control instructions */
i_unreachable = 0x00,
i_nop = 0x01,
i_block = 0x02,
i_loop = 0x03,
i_if = 0x04,
i_else = 0x05,
i_end = 0x0B,
i_br = 0x0C,
i_br_if = 0x0D,
i_br_table = 0x0E,
i_return = 0x0F,
i_call = 0x10,
i_call_indirect = 0x11,
/* parametric instructions */
i_drop = 0x1A,
i_select = 0x1B,
i_selects = 0x1C,
/* variable instructions */
i_local_get = 0x20,
i_local_set = 0x21,
i_local_tee = 0x22,
i_global_get = 0x23,
i_global_set = 0x24,
i_table_get = 0x25,
i_table_set = 0x26,
/* memory instructions */
i_i32_load = 0x28,
i_i64_load = 0x29,
i_f32_load = 0x2A,
i_f64_load = 0x2B,
i_i32_load8_s = 0x2C,
i_i32_load8_u = 0x2D,
i_i32_load16_s = 0x2E,
i_i32_load16_u = 0x2F,
i_i64_load8_s = 0x30,
i_i64_load8_u = 0x31,
i_i64_load16_s = 0x32,
i_i64_load16_u = 0x33,
i_i64_load32_s = 0x34,
i_i64_load32_u = 0x35,
i_i32_store = 0x36,
i_i64_store = 0x37,
i_f32_store = 0x38,
i_f64_store = 0x39,
i_i32_store8 = 0x3A,
i_i32_store16 = 0x3B,
i_i64_store8 = 0x3C,
i_i64_store16 = 0x3D,
i_i64_store32 = 0x3E,
i_memory_size = 0x3F,
i_memory_grow = 0x40,
/* numeric instructions */
i_i32_const = 0x41,
i_i64_const = 0x42,
i_f32_const = 0x43,
i_f64_const = 0x44,
i_i32_eqz = 0x45,
i_i32_eq = 0x46,
i_i32_ne = 0x47,
i_i32_lt_s = 0x48,
i_i32_lt_u = 0x49,
i_i32_gt_s = 0x4A,
i_i32_gt_u = 0x4B,
i_i32_le_s = 0x4C,
i_i32_le_u = 0x4D,
i_i32_ge_s = 0x4E,
i_i32_ge_u = 0x4F,
i_i64_eqz = 0x50,
i_i64_eq = 0x51,
i_i64_ne = 0x52,
i_i64_lt_s = 0x53,
i_i64_lt_u = 0x54,
i_i64_gt_s = 0x55,
i_i64_gt_u = 0x56,
i_i64_le_s = 0x57,
i_i64_le_u = 0x58,
i_i64_ge_s = 0x59,
i_i64_ge_u = 0x5A,
i_f32_eq = 0x5B,
i_f32_ne = 0x5C,
i_f32_lt = 0x5D,
i_f32_gt = 0x5E,
i_f32_le = 0x5F,
i_f32_ge = 0x60,
i_f64_eq = 0x61,
i_f64_ne = 0x62,
i_f64_lt = 0x63,
i_f64_gt = 0x64,
i_f64_le = 0x65,
i_f64_ge = 0x66,
i_i32_clz = 0x67,
i_i32_ctz = 0x68,
i_i32_popcnt = 0x69,
i_i32_add = 0x6A,
i_i32_sub = 0x6B,
i_i32_mul = 0x6C,
i_i32_div_s = 0x6D,
i_i32_div_u = 0x6E,
i_i32_rem_s = 0x6F,
i_i32_rem_u = 0x70,
i_i32_and = 0x71,
i_i32_or = 0x72,
i_i32_xor = 0x73,
i_i32_shl = 0x74,
i_i32_shr_s = 0x75,
i_i32_shr_u = 0x76,
i_i32_rotl = 0x77,
i_i32_rotr = 0x78,
i_i64_clz = 0x79,
i_i64_ctz = 0x7A,
i_i64_popcnt = 0x7B,
i_i64_add = 0x7C,
i_i64_sub = 0x7D,
i_i64_mul = 0x7E,
i_i64_div_s = 0x7F,
i_i64_div_u = 0x80,
i_i64_rem_s = 0x81,
i_i64_rem_u = 0x82,
i_i64_and = 0x83,
i_i64_or = 0x84,
i_i64_xor = 0x85,
i_i64_shl = 0x86,
i_i64_shr_s = 0x87,
i_i64_shr_u = 0x88,
i_i64_rotl = 0x89,
i_i64_rotr = 0x8A,
i_f32_abs = 0x8b,
i_f32_neg = 0x8c,
i_f32_ceil = 0x8d,
i_f32_floor = 0x8e,
i_f32_trunc = 0x8f,
i_f32_nearest = 0x90,
i_f32_sqrt = 0x91,
i_f32_add = 0x92,
i_f32_sub = 0x93,
i_f32_mul = 0x94,
i_f32_div = 0x95,
i_f32_min = 0x96,
i_f32_max = 0x97,
i_f32_copysign = 0x98,
i_f64_abs = 0x99,
i_f64_neg = 0x9a,
i_f64_ceil = 0x9b,
i_f64_floor = 0x9c,
i_f64_trunc = 0x9d,
i_f64_nearest = 0x9e,
i_f64_sqrt = 0x9f,
i_f64_add = 0xa0,
i_f64_sub = 0xa1,
i_f64_mul = 0xa2,
i_f64_div = 0xa3,
i_f64_min = 0xa4,
i_f64_max = 0xa5,
i_f64_copysign = 0xa6,
i_i32_wrap_i64 = 0xa7,
i_i32_trunc_f32_s = 0xa8,
i_i32_trunc_f32_u = 0xa9,
i_i32_trunc_f64_s = 0xaa,
i_i32_trunc_f64_u = 0xab,
i_i64_extend_i32_s = 0xac,
i_i64_extend_i32_u = 0xad,
i_i64_trunc_f32_s = 0xae,
i_i64_trunc_f32_u = 0xaf,
i_i64_trunc_f64_s = 0xb0,
i_i64_trunc_f64_u = 0xb1,
i_f32_convert_i32_s = 0xb2,
i_f32_convert_i32_u = 0xb3,
i_f32_convert_i64_s = 0xb4,
i_f32_convert_i64_u = 0xb5,
i_f32_demote_f64 = 0xb6,
i_f64_convert_i32_s = 0xb7,
i_f64_convert_i32_u = 0xb8,
i_f64_convert_i64_s = 0xb9,
i_f64_convert_i64_u = 0xba,
i_f64_promote_f32 = 0xbb,
i_i32_reinterpret_f32 = 0xbc,
i_i64_reinterpret_f64 = 0xbd,
i_f32_reinterpret_i32 = 0xbe,
i_f64_reinterpret_i64 = 0xbf,
i_i32_extend8_s = 0xc0,
i_i32_extend16_s = 0xc1,
i_i64_extend8_s = 0xc2,
i_i64_extend16_s = 0xc3,
i_i64_extend32_s = 0xc4,
i_ref_null = 0xD0,
i_ref_is_null = 0xD1,
i_ref_func = 0xD2,
i_bulk_op = 0xFC,
/* TODO: more instrs */
};
enum blocktype_tag {
blocktype_empty,
blocktype_valtype,
blocktype_index,
};
struct blocktype {
enum blocktype_tag tag;
union {
enum valtype valtype;
int type_index;
};
};
struct instrs {
unsigned char *data;
u32 len;
};
struct block {
struct blocktype type;
struct expr instrs;
};
struct memarg {
u32 offset;
u32 align;
};
struct br_table {
u32 num_label_indices;
u32 label_indices[512];
u32 default_label;
};
struct call_indirect {
u32 tableidx;
u32 typeidx;
};
struct table_init {
u32 tableidx;
u32 elemidx;
};
struct table_copy {
u32 from;
u32 to;
};
struct bulk_op {
enum bulk_tag tag;
union {
struct table_init table_init;
struct table_copy table_copy;
u32 idx;
};
};
struct select_instr {
u8 *valtypes;
u32 num_valtypes;
};
struct instr {
enum instr_tag tag;
int pos;
union {
struct br_table br_table;
struct bulk_op bulk_op;
struct call_indirect call_indirect;
struct memarg memarg;
struct select_instr select;
struct block block;
struct expr else_block;
double f64;
float f32;
int i32;
u32 u32;
int64_t i64;
u64 u64;
unsigned char memidx;
enum reftype reftype;
};
};
enum datamode {
datamode_active,
datamode_passive,
};
struct wdata_active {
u32 mem_index;
struct expr offset_expr;
};
struct wdata {
struct wdata_active active;
u8 *bytes;
u32 bytes_len;
enum datamode mode;
};
struct datasec {
struct wdata *datas;
u32 num_datas;
};
struct startsec {
u32 start_fn;
};
struct module {
unsigned int parsed;
unsigned int custom_sections;
struct func *funcs;
u32 num_funcs;
struct customsec custom_section[MAX_CUSTOM_SECTIONS];
struct typesec type_section;
struct funcsec func_section;
struct importsec import_section;
struct exportsec export_section;
struct codesec code_section;
struct tablesec table_section;
struct memsec memory_section;
struct globalsec global_section;
struct startsec start_section;
struct elemsec element_section;
struct datasec data_section;
struct namesec name_section;
};
// make sure the struct is packed so that
struct label {
u32 instr_pos; // resolved status is stored in HOB of pos
u32 jump;
};
struct callframe {
struct cursor code;
struct val *locals;
struct func *func;
u16 prev_stack_items;
};
struct resolver {
u16 label;
u8 end_tag;
u8 start_tag;
};
struct global_inst {
struct val val;
};
struct module_inst {
struct table_inst *tables;
struct global_inst *globals;
struct elem_inst *elements;
u32 num_tables;
u32 num_globals;
u32 num_elements;
int start_fn;
unsigned char *globals_init;
};
struct wasi {
int argc;
const char **argv;
int environc;
const char **environ;
};
struct wasm_interp;
struct builtin {
const char *name;
int (*fn)(struct wasm_interp *);
int (*prepare_args)(struct wasm_interp *);
};
struct wasm_interp {
struct module *module;
struct module_inst module_inst;
struct wasi wasi;
void *context;
struct builtin builtins[MAX_BUILTINS];
int num_builtins;
int prev_resolvers, quitting;
struct errors errors; /* struct error */
size_t ops;
struct cursor callframes; /* struct callframe */
struct cursor stack; /* struct val */
struct cursor mem; /* u8/mixed */
struct cursor memory; /* memory pages (65536 blocks) */
struct cursor locals; /* struct val */
struct cursor labels; /* struct labels */
struct cursor num_labels;
// resolve stack for the current function. every time a control
// instruction is encountered, the label index is pushed. When an
// instruction is popped, we can resolve the label
struct cursor resolver_stack; /* struct resolver */
struct cursor resolver_offsets; /* int */
};
struct wasm_parser {
struct module module;
struct builtin *builtins;
u32 num_builtins;
struct cursor cur;
struct cursor mem;
struct errors errs;
};
int run_wasm(unsigned char *wasm, unsigned long len, int argc, const char **argv, char **env, int *retval);
int parse_wasm(struct wasm_parser *p);
int wasm_interp_init(struct wasm_interp *interp, struct module *module);
void wasm_parser_free(struct wasm_parser *parser);
void wasm_parser_init(struct wasm_parser *p, u8 *wasm, size_t wasm_len, size_t arena_size, struct builtin *, int num_builtins);
void wasm_interp_free(struct wasm_interp *interp);
int interp_wasm_module(struct wasm_interp *interp, int *retval);
int interp_wasm_module_resume(struct wasm_interp *interp, int *retval);
void print_error_backtrace(struct errors *errors);
void setup_wasi(struct wasm_interp *interp, int argc, const char **argv, char **env);
void print_callstack(struct wasm_interp *interp);
// builtin helpers
int get_params(struct wasm_interp *interp, struct val** vals, u32 num_vals);
int get_var_params(struct wasm_interp *interp, struct val** vals, u32 *num_vals);
u8 *interp_mem_ptr(struct wasm_interp *interp, u32 ptr, int size);
static INLINE struct callframe *top_callframe(struct cursor *cur)
{
return (struct callframe*)cursor_top(cur, sizeof(struct callframe));
}
static INLINE struct cursor *interp_codeptr(struct wasm_interp *interp)
{
struct callframe *frame;
if (unlikely(!(frame = top_callframe(&interp->callframes))))
return 0;
return &frame->code;
}
static INLINE int mem_ptr_str(struct wasm_interp *interp, u32 ptr,
const char **str)
{
// still technically unsafe if the string runs over the end of memory...
if (!(*str = (const char*)interp_mem_ptr(interp, ptr, 1))) {
return interp_error(interp, "int memptr");
}
return 1;
}
static INLINE int mem_ptr_i32(struct wasm_interp *interp, u32 ptr, int **i)
{
if (!(*i = (int*)interp_mem_ptr(interp, ptr, sizeof(int))))
return interp_error(interp, "int memptr");
return 1;
}
static INLINE int cursor_pushval(struct cursor *cur, struct val *val)
{
return cursor_push(cur, (u8*)val, sizeof(*val));
}
static INLINE int cursor_push_i32(struct cursor *stack, int i)
{
struct val val;
val.type = val_i32;
val.num.i32 = i;
return cursor_pushval(stack, &val);
}
static INLINE int stack_push_i32(struct wasm_interp *interp, int i)
{
return cursor_push_i32(&interp->stack, i);
}
static INLINE struct callframe *top_callframes(struct cursor *cur, int top)
{
return (struct callframe*)cursor_topn(cur, sizeof(struct callframe), top);
}
static INLINE int was_section_parsed(struct module *module,
enum section_tag section)
{
if (section == section_custom)
return module->custom_sections > 0;
return module->parsed & (1 << section);
}
#endif /* PROTOVERSE_WASM_H */

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,14 @@
"state" : {
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
"location" : "https://github.com/damus-io/swift-markdown-ui",
"state" : {
"revision" : "76bb7971da7fbf429de1c84f1244adf657242fee"
}
}
],
"version" : 2

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
LastUpgradeVersion = "1500"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
LastUpgradeVersion = "1500"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -26,7 +26,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xE3",
"green" : "0xD7",
"red" : "0xF7"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x20",
"green" : "0x13",
"red" : "0x61"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x63",
"green" : "0x11",
"red" : "0xF5"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x6E",
"green" : "0x20",
"red" : "0xF8"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xEE",
"green" : "0xE8",
"red" : "0xF7"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x35",
"green" : "0x04",
"red" : "0x8B"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x3D",
"green" : "0x07",
"red" : "0x9C"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x44",
"green" : "0x06",
"red" : "0xB2"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2D",
"green" : "0x05",
"red" : "0x75"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xD8",
"green" : "0xC2",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFA",
"green" : "0xFA",
"red" : "0xF9"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x24",
"green" : "0x22",
"red" : "0x20"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xE3",
"green" : "0xE1",
"red" : "0xDD"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2A",
"green" : "0x26",
"red" : "0x23"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x59",
"green" : "0x53",
"red" : "0x4A"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x85",
"green" : "0x7A",
"red" : "0x6A"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xE4",
"green" : "0xF1",
"red" : "0xD6"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x38",
"green" : "0x5C",
"red" : "0x12"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x5A",
"green" : "0xAB",
"red" : "0x04"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x64",
"green" : "0xBF",
"red" : "0x03"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF0",
"green" : "0xF7",
"red" : "0xE8"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1F",
"green" : "0x33",
"red" : "0x0A"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x34",
"green" : "0x64",
"red" : "0x02"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x3F",
"green" : "0x79",
"red" : "0x02"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1F",
"green" : "0x3C",
"red" : "0x01"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xE4",
"green" : "0xFF",
"red" : "0xAD"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xD1",
"green" : "0xEE",
"red" : "0xFE"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x12",
"green" : "0x43",
"red" : "0x5C"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1C",
"green" : "0xAD",
"red" : "0xF9"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2C",
"green" : "0xB5",
"red" : "0xFC"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xE1",
"green" : "0xF4",
"red" : "0xFE"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x0A",
"green" : "0x25",
"red" : "0x33"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x06",
"green" : "0x85",
"red" : "0xC6"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x03",
"green" : "0x93",
"red" : "0xDD"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x04",
"green" : "0x6A",
"red" : "0x9F"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xCC",
"green" : "0xF5",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -22,5 +22,23 @@ class DamusColors {
static let purple = Color("DamusPurple")
static let deepPurple = Color("DamusDeepPurple")
static let blue = Color("DamusBlue")
static let success = Color("DamusSuccessPrimary")
static let successSecondary = Color("DamusSuccessSecondary")
static let successTertiary = Color("DamusSuccessTertiary")
static let successQuaternary = Color("DamusSuccessQuaternary")
static let successBorder = Color("DamusSuccessBorder")
static let warning = Color("DamusWarningPrimary")
static let warningSecondary = Color("DamusWarningSecondary")
static let warningTertiary = Color("DamusWarningTertiary")
static let warningQuaternary = Color("DamusWarningQuaternary")
static let warningBorder = Color("DamusWarningBorder")
static let danger = Color("DamusDangerPrimary")
static let dangerSecondary = Color("DamusDangerSecondary")
static let dangerTertiary = Color("DamusDangerTertiary")
static let dangerQuaternary = Color("DamusDangerQuaternary")
static let dangerBorder = Color("DamusDangerBorder")
static let neutral1 = Color("DamusNeutral1")
static let neutral3 = Color("DamusNeutral3")
static let neutral6 = Color("DamusNeutral6")
}

View File

@@ -10,11 +10,7 @@ import SwiftUI
struct EndBlock: View {
let height: CGFloat
init () {
self.height = 10.0
}
init (height: Float) {
init(height: Float = 10) {
self.height = CGFloat(height)
}

View File

@@ -8,15 +8,21 @@
import SwiftUI
struct GradientButtonStyle: ButtonStyle {
let padding: CGFloat
init(padding: CGFloat = 16.0) {
self.padding = padding
}
func makeBody(configuration: Self.Configuration) -> some View {
return configuration.label
.padding()
.padding(padding)
.foregroundColor(Color.white)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(PinkGradient.gradient)
.fill(PinkGradient)
}
.scaleEffect(configuration.isPressed ? 0.8 : 1)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
}
}

View File

@@ -0,0 +1,30 @@
//
// DamusBackground.swift
// damus
//
// Created by William Casarin on 2023-07-12.
//
import Foundation
import SwiftUI
struct DamusBackground: View {
let maxHeight: CGFloat
init(maxHeight: CGFloat = 250.0) {
self.maxHeight = maxHeight
}
var body: some View {
Image("login-header")
.resizable()
.frame(maxWidth: .infinity, maxHeight: maxHeight, alignment: .center)
.ignoresSafeArea()
}
}
struct DamusBackground_Previews: PreviewProvider {
static var previews: some View {
DamusBackground()
}
}

View File

@@ -0,0 +1,30 @@
//
// DamusLightGradient.swift
// damus
//
// Created by eric on 9/8/23.
//
import SwiftUI
fileprivate let damus_grad_c1 = hex_col(r: 0xd3, g: 0x2d, b: 0xc3)
fileprivate let damus_grad_c2 = hex_col(r: 0x33, g: 0xc5, b: 0xbc)
fileprivate let damus_grad = [damus_grad_c1, damus_grad_c2]
struct DamusLightGradient: View {
var body: some View {
DamusLightGradient.gradient
.opacity(0.5)
.edgesIgnoringSafeArea([.top,.bottom])
}
static var gradient: LinearGradient {
LinearGradient(colors: damus_grad, startPoint: .topLeading, endPoint: .bottomTrailing)
}
}
struct DamusLightGradient_Previews: PreviewProvider {
static var previews: some View {
DamusLightGradient()
}
}

View File

@@ -0,0 +1,26 @@
//
// GrayGradient.swift
// damus
//
// Created by klabo on 7/20/23.
//
import SwiftUI
let GrayGradient = LinearGradient(gradient:
Gradient(colors: [Color(#colorLiteral(red: 0.9764705882, green: 0.9803921569, blue: 0.9803921569, alpha: 1))]),
startPoint: .leading,
endPoint: .trailing)
struct GrayGradientView: View {
var body: some View {
GrayGradient
.edgesIgnoringSafeArea([.top, .bottom])
}
}
struct GrayGradient_Previews: PreviewProvider {
static var previews: some View {
GrayGradientView()
}
}

View File

@@ -11,20 +11,18 @@ fileprivate let damus_grad_c1 = hex_col(r: 0xd3, g: 0x4c, b: 0xd9)
fileprivate let damus_grad_c2 = hex_col(r: 0xf8, g: 0x69, b: 0xb6)
fileprivate let pink_grad = [damus_grad_c1, damus_grad_c2]
struct PinkGradient: View {
let PinkGradient = LinearGradient(colors: pink_grad, startPoint: .topTrailing, endPoint: .bottom)
struct PinkGradientView: View {
var body: some View {
PinkGradient.gradient
PinkGradient
.edgesIgnoringSafeArea([.top,.bottom])
}
static var gradient: LinearGradient {
LinearGradient(colors: pink_grad, startPoint: .topTrailing, endPoint: .bottom)
}
}
struct PinkGradient_Previews: PreviewProvider {
struct PinkGradientView_Previews: PreviewProvider {
static var previews: some View {
PinkGradient()
PinkGradientView()
}
}

View File

@@ -57,7 +57,7 @@ enum ImageShape {
struct ImageCarousel: View {
var urls: [MediaUrl]
let evid: String
let evid: NoteId
let state: DamusState
@@ -72,7 +72,7 @@ struct ImageCarousel: View {
@State private var selectedIndex = 0
@State private var video_size: CGSize? = nil
init(state: DamusState, evid: String, urls: [MediaUrl]) {
init(state: DamusState, evid: NoteId, urls: [MediaUrl]) {
_open_sheet = State(initialValue: false)
_current_url = State(initialValue: nil)
let media_model = state.events.get_cache_data(evid).media_metadata_model
@@ -105,17 +105,13 @@ struct ImageCarousel: View {
}
}
.onAppear {
if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) {
if self.image_fill == nil, let size = state.video.size_for_url(url) {
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
self.image_fill = fill
}
}
}
func video_model(_ url: URL) -> VideoPlayerModel {
return state.events.get_video_player_model(url: url)
}
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
Group {
switch url {
@@ -125,7 +121,7 @@ struct ImageCarousel: View {
open_sheet = true
}
case .video(let url):
DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size)
DamusVideoPlayer(url: url, video_size: $video_size, controller: state.video)
.onChange(of: video_size) { size in
guard let size else { return }
@@ -194,7 +190,7 @@ struct ImageCarousel: View {
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.fullScreenCover(isPresented: $open_sheet) {
ImageView(cache: state.events, urls: urls, disable_animation: state.settings.disable_animation)
ImageView(video_controller: state.video, urls: urls, disable_animation: state.settings.disable_animation)
}
.frame(height: height)
.onChange(of: selectedIndex) { value in
@@ -289,7 +285,7 @@ public struct ImageFill {
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [url, url])
ImageCarousel(state: test_damus_state, evid: test_note.id, urls: [url, url])
}
}

View File

@@ -9,7 +9,7 @@ import SwiftUI
struct InvoiceView: View {
@Environment(\.colorScheme) var colorScheme
let our_pubkey: String
let our_pubkey: Pubkey
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@State var copied = false
@@ -108,12 +108,12 @@ let test_invoice = Invoice(description: .description("this is a description"), a
struct InvoiceView_Previews: PreviewProvider {
static var previews: some View {
InvoiceView(our_pubkey: "", invoice: test_invoice, settings: test_damus_state().settings)
InvoiceView(our_pubkey: .empty, invoice: test_invoice, settings: test_damus_state.settings)
.frame(width: 300, height: 200)
}
}
func present_sheet(_ sheet: Sheets) {
notify(.present_sheet, sheet)
notify(.present_sheet(sheet))
}

View File

@@ -8,7 +8,7 @@
import SwiftUI
struct InvoicesView: View {
let our_pubkey: String
let our_pubkey: Pubkey
var invoices: [Invoice]
let settings: UserSettingsStore
@@ -29,7 +29,7 @@ struct InvoicesView: View {
struct InvoicesView_Previews: PreviewProvider {
static var previews: some View {
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)], settings: test_damus_state().settings)
InvoicesView(our_pubkey: test_note.pubkey, invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)], settings: test_damus_state.settings)
.frame(width: 300)
}
}

View File

@@ -9,19 +9,19 @@ import SwiftUI
struct NIP05Badge: View {
let nip05: NIP05
let pubkey: String
let pubkey: Pubkey
let contacts: Contacts
let show_domain: Bool
let clickable: Bool
let profiles: Profiles
@Environment(\.openURL) var openURL
init (nip05: NIP05, pubkey: String, contacts: Contacts, show_domain: Bool, clickable: Bool) {
init(nip05: NIP05, pubkey: Pubkey, contacts: Contacts, show_domain: Bool, profiles: Profiles) {
self.nip05 = nip05
self.pubkey = pubkey
self.contacts = contacts
self.show_domain = show_domain
self.clickable = clickable
self.profiles = profiles
}
var nip05_color: Bool {
@@ -32,34 +32,47 @@ struct NIP05Badge: View {
Group {
if nip05_color {
LINEAR_GRADIENT
.mask(Image("check-circle.fill")
.mask(Image("verified.fill")
.resizable()
).frame(width: 14, height: 14)
).frame(width: 18, height: 18)
} else if show_domain {
Image("check-circle.fill")
.font(.footnote)
Image("verified")
.resizable()
.frame(width: 18, height: 18)
.nip05_colorized(gradient: nip05_color)
}
}
}
var username_matches_nip05: Bool {
guard let name = profiles.lookup(id: pubkey).map({ p in p?.name }).value
else {
return false
}
return name.lowercased() == nip05.username.lowercased()
}
var nip05_string: String {
if nip05.username == "_" || username_matches_nip05 {
return nip05.host
} else {
return "\(nip05.username)@\(nip05.host)"
}
}
var body: some View {
HStack(spacing: 2) {
Seal
if show_domain {
if clickable {
Text(nip05.host)
.nip05_colorized(gradient: nip05_color)
.onTapGesture {
if let nip5url = nip05.siteUrl {
openURL(nip5url)
}
Text(nip05_string)
.nip05_colorized(gradient: nip05_color)
.onTapGesture {
if let nip5url = nip05.siteUrl {
openURL(nip5url)
}
} else {
Text(nip05.host)
.foregroundColor(.gray)
}
}
}
}
@@ -77,14 +90,22 @@ extension View {
}
}
func use_nip05_color(pubkey: String, contacts: Contacts) -> Bool {
func use_nip05_color(pubkey: Pubkey, contacts: Contacts) -> Bool {
return contacts.is_friend_or_self(pubkey) ? true : false
}
struct NIP05Badge_Previews: PreviewProvider {
static var previews: some View {
let test_state = test_damus_state()
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, clickable: false)
let test_state = test_damus_state
VStack {
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: test_pubkey), show_domain: true, profiles: test_state.profiles)
}
}
}

View File

@@ -0,0 +1,49 @@
//
// NeutralButtonStyle.swift
// damus
//
// Created by eric on 9/1/23.
//
import SwiftUI
struct NeutralButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
return configuration.label
.background(DamusColors.neutral1)
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
}
}
struct NeutralButtonStyle_Previews: PreviewProvider {
static var previews: some View {
VStack {
Button(action: {
print("dynamic size")
}) {
Text(verbatim: "Dynamic Size")
.padding()
}
.buttonStyle(NeutralButtonStyle())
Button(action: {
print("infinite width")
}) {
HStack {
Text(verbatim: "Infinite Width")
.padding()
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
}
.buttonStyle(NeutralButtonStyle())
.padding()
}
}
}

View File

@@ -9,14 +9,13 @@ import SwiftUI
struct Reposted: View {
let damus: DamusState
let pubkey: String
let profile: Profile?
let pubkey: Pubkey
var body: some View {
HStack(alignment: .center) {
Image("repost")
.foregroundColor(Color.gray)
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false)
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)
@@ -26,7 +25,7 @@ struct Reposted: View {
struct Reposted_Previews: PreviewProvider {
static var previews: some View {
let test_state = test_damus_state()
Reposted(damus: test_state, pubkey: test_state.pubkey, profile: make_test_profile())
let test_state = test_damus_state
Reposted(damus: test_state, pubkey: test_state.pubkey)
}
}

View File

@@ -0,0 +1,176 @@
//
// SearchIconView.swift
// damus
//
// Created by William Casarin on 2023-07-12.
//
import SwiftUI
struct SearchHeaderView: View {
let state: DamusState
let described: DescribedSearch
@State var is_following: Bool
init(state: DamusState, described: DescribedSearch) {
self.state = state
self.described = described
let is_following = (described.is_hashtag.map {
ht in is_following_hashtag(contacts: state.contacts.event, hashtag: ht)
}) ?? false
self._is_following = State(wrappedValue: is_following)
}
var Icon: some View {
ZStack {
switch described {
case .hashtag:
SingleCharacterAvatar(character: "#")
case .unknown:
SystemIconAvatar(system_name: "magnifyingglass")
}
}
}
var SearchText: Text {
Text(described.description)
}
var body: some View {
HStack(alignment: .center, spacing: 30) {
Icon
VStack(alignment: .leading, spacing: 10.0) {
SearchText
.foregroundStyle(DamusLogoGradient.gradient)
.font(.title.bold())
if state.is_privkey_user, case .hashtag(let ht) = described {
if is_following {
HashtagUnfollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
} else {
HashtagFollowButton(damus_state: state, hashtag: ht, is_following: $is_following)
}
}
}
}
.onReceive(handle_notify(.followed)) { ref in
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
self.is_following = true
}
.onReceive(handle_notify(.unfollowed)) { ref in
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
self.is_following = false
}
}
}
struct SystemIconAvatar: View {
let system_name: String
var body: some View {
NonImageAvatar {
Image(systemName: system_name)
.font(.title.bold())
}
}
}
struct SingleCharacterAvatar: View {
let character: String
var body: some View {
NonImageAvatar {
Text(verbatim: character)
.font(.largeTitle.bold())
.mask(Text(verbatim: character)
.font(.largeTitle.bold()))
}
}
}
struct NonImageAvatar<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
ZStack {
Circle()
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
.frame(width: 54, height: 54)
content
.foregroundStyle(PinkGradient)
}
}
}
struct HashtagUnfollowButton: View {
let damus_state: DamusState
let hashtag: String
@Binding var is_following: Bool
var body: some View {
return Button(action: { unfollow(hashtag) }) {
Text("Unfollow hashtag", comment: "Button to unfollow a given hashtag.")
.font(.footnote.bold())
}
.buttonStyle(GradientButtonStyle(padding: 10))
}
func unfollow(_ hashtag: String) {
is_following = false
handle_unfollow(state: damus_state, unfollow: FollowRef.hashtag(hashtag))
}
}
struct HashtagFollowButton: View {
let damus_state: DamusState
let hashtag: String
@Binding var is_following: Bool
var body: some View {
return Button(action: { follow(hashtag) }) {
Text("Follow hashtag", comment: "Button to follow a given hashtag.")
.font(.footnote.bold())
}
.buttonStyle(GradientButtonStyle(padding: 10))
}
func follow(_ hashtag: String) {
is_following = true
handle_follow(state: damus_state, follow: .hashtag(hashtag))
}
}
func hashtag_matches_search(desc: DescribedSearch, ref: FollowRef) -> Bool {
guard case .hashtag(let follow_ht) = ref,
case .hashtag(let search_ht) = desc,
follow_ht == search_ht
else {
return false
}
return true
}
func is_following_hashtag(contacts: NostrEvent?, hashtag: String) -> Bool {
guard let contacts else { return false }
return is_already_following(contacts: contacts, follow: .hashtag(hashtag))
}
struct SearchHeaderView_Previews: PreviewProvider {
static var previews: some View {
VStack(alignment: .leading) {
SearchHeaderView(state: test_damus_state, described: .hashtag("damus"))
SearchHeaderView(state: test_damus_state, described: .unknown)
}
}
}

View File

@@ -28,7 +28,11 @@ struct SelectableText: View {
)
.padding([.leading, .trailing], -1.0)
.onAppear {
self.selectedTextWidth = geo.size.width
if geo.size.width == .zero {
self.selectedTextHeight = 1000.0
} else {
self.selectedTextWidth = geo.size.width
}
}
.onChange(of: geo.size) { newSize in
self.selectedTextWidth = newSize.width

View File

@@ -0,0 +1,48 @@
//
// MusicController.swift
// damus
//
// Created by William Casarin on 2023-08-21.
//
import SwiftUI
import MediaPlayer
enum MusicState {
case playback_state(MPMusicPlaybackState)
case song(MPMediaItem?)
}
class MusicController {
let player: MPMusicPlayerController
let onChange: (MusicState) -> ()
init(onChange: @escaping (MusicState) -> ()) {
player = .systemMusicPlayer
player.beginGeneratingPlaybackNotifications()
self.onChange = onChange
print("Playback State: \(player.playbackState)")
print("Now Playing Item: \(player.nowPlayingItem?.title ?? "None")")
NotificationCenter.default.addObserver(self, selector: #selector(self.songChanged(notification:)), name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: player)
NotificationCenter.default.addObserver(self, selector: #selector(self.playbackStatusChanged(notification:)), name: .MPMusicPlayerControllerPlaybackStateDidChange, object: player)
}
deinit {
print("deinit musiccontroller")
}
@objc
func songChanged(notification: Notification) {
onChange(.song(player.nowPlayingItem))
}
@objc
func playbackStatusChanged(notification: Notification) {
onChange(.playback_state(player.playbackState))
}
}

View File

@@ -0,0 +1,183 @@
//
// UserStatus.swift
// damus
//
// Created by William Casarin on 2023-08-22.
//
import Foundation
import MediaPlayer
struct Song {
let started_playing: Date
let content: String
}
struct UserStatus {
let type: UserStatusType
let expires_at: Date?
var content: String
let created_at: UInt32
var url: URL?
func to_note(keypair: FullKeypair) -> NostrEvent? {
return make_user_status_note(status: self, keypair: keypair)
}
init(type: UserStatusType, expires_at: Date?, content: String, created_at: UInt32, url: URL? = nil) {
self.type = type
self.expires_at = expires_at
self.content = content
self.created_at = created_at
self.url = url
}
func expired() -> Bool {
guard let expires_at else { return false }
return Date.now >= expires_at
}
init?(ev: NostrEvent) {
guard let tag = ev.referenced_params.just_one() else {
return nil
}
let str = tag.param.string()
if str == "general" {
self.type = .general
} else if str == "music" {
self.type = .music
} else {
return nil
}
if let tag = ev.tags.first(where: { t in t.count >= 2 && t[0].matches_char("r") }),
tag.count >= 2,
let url = URL(string: tag[1].string())
{
self.url = url
} else {
self.url = nil
}
if let tag = ev.tags.first(where: { t in t.count >= 2 && t[0].matches_str("expiration") }),
tag.count == 2,
let expires = UInt32(tag[1].string())
{
self.expires_at = Date(timeIntervalSince1970: TimeInterval(expires))
} else {
self.expires_at = nil
}
self.content = ev.content
self.created_at = ev.created_at
}
}
enum UserStatusType: String {
case music
case general
}
class UserStatusModel: ObservableObject {
@Published var general: UserStatus?
@Published var music: UserStatus?
func update_status(_ s: UserStatus) {
// whitespace = delete
let del = s.content.allSatisfy({ c in c.isWhitespace })
switch s.type {
case .music:
if del {
self.music = nil
} else {
self.music = s
}
case .general:
if del {
self.general = nil
} else {
self.general = s
}
}
}
func try_expire() {
if let general, general.expired() {
self.general = nil
}
if let music, music.expired() {
self.music = nil
}
}
var _playing_enabled: Bool
var playing_enabled: Bool {
set {
var new_val = newValue
if newValue {
MPMediaLibrary.requestAuthorization { astatus in
switch astatus {
case .notDetermined: new_val = false
case .denied: new_val = false
case .restricted: new_val = false
case .authorized: new_val = true
@unknown default:
new_val = false
}
}
}
if new_val != playing_enabled {
_playing_enabled = new_val
self.objectWillChange.send()
}
}
get {
return _playing_enabled
}
}
init(playing: UserStatus? = nil, status: UserStatus? = nil) {
self.general = status
self.music = playing
self._playing_enabled = false
self.playing_enabled = false
}
static var current_track: String? {
let player = MPMusicPlayerController.systemMusicPlayer
guard let nowPlayingItem = player.nowPlayingItem else { return nil }
return nowPlayingItem.title
}
}
func make_user_status_note(status: UserStatus, keypair: FullKeypair, expiry: Date? = nil) -> NostrEvent?
{
var tags: [[String]] = [ ["d", status.type.rawValue] ]
if let expiry {
tags.append(["expiration", String(UInt32(expiry.timeIntervalSince1970))])
} else if let expiry = status.expires_at {
tags.append(["expiration", String(UInt32(expiry.timeIntervalSince1970))])
}
if let url = status.url {
tags.append(["r", url.absoluteString])
}
let kind = NostrKind.status.rawValue
guard let ev = NostrEvent(content: status.content, keypair: keypair.to_keypair(), kind: kind, tags: tags) else {
return nil
}
return ev
}

View File

@@ -0,0 +1,160 @@
//
// UserStatusSheet.swift
// damus
//
// Created by William Casarin on 2023-08-23.
//
import SwiftUI
enum StatusDuration: CustomStringConvertible, CaseIterable {
case never
case thirty_mins
case hour
case four_hours
case day
case week
var timeInterval: TimeInterval? {
switch self {
case .never:
return nil
case .thirty_mins:
return 60 * 30
case .hour:
return 60 * 60
case .four_hours:
return 60 * 60 * 4
case .day:
return 60 * 60 * 24
case .week:
return 60 * 60 * 24 * 7
}
}
var expiration: Date? {
guard let timeInterval else {
return nil
}
return Date.now.addingTimeInterval(timeInterval)
}
var description: String {
guard let timeInterval else {
return NSLocalizedString("Never", comment: "Profile status duration setting of never expiring.")
}
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.allowedUnits = [.minute, .hour, .day, .weekOfMonth]
return formatter.string(from: timeInterval) ?? "\(timeInterval) seconds"
}
}
struct UserStatusSheet: View {
let postbox: PostBox
let keypair: Keypair
@State var duration: StatusDuration = .never
@ObservedObject var status: UserStatusModel
@Environment(\.dismiss) var dismiss
var status_binding: Binding<String> {
Binding(get: {
status.general?.content ?? ""
}, set: { v in
if let general = status.general {
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: v, created_at: UInt32(Date.now.timeIntervalSince1970), url: general.url)
} else {
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: v, created_at: UInt32(Date.now.timeIntervalSince1970), url: nil)
}
})
}
var url_binding: Binding<String> {
Binding(get: {
status.general?.url?.absoluteString ?? ""
}, set: { v in
if let general = status.general {
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: general.content, created_at: UInt32(Date.now.timeIntervalSince1970), url: URL(string: v))
} else {
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: "", created_at: UInt32(Date.now.timeIntervalSince1970), url: URL(string: v))
}
})
}
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text("Set Status", comment: "Title of view that allows the user to set their profile status (e.g. working, studying, coding)")
.font(.largeTitle)
TextField(text: status_binding, label: {
Text("📋 Working", comment: "Placeholder as an example of what the user could set as their profile status.")
})
HStack {
Image("link")
TextField(text: url_binding, label: {
Text("https://example.com", comment: "Placeholder as an example of what the user could set so that the link is opened when the status is tapped.")
})
}
HStack {
Text("Clear status", comment: "Label to prompt user to select an expiration time for the profile status to clear.")
Spacer()
Picker(NSLocalizedString("Duration", comment: "Label for profile status expiration duration picker."), selection: $duration) {
ForEach(StatusDuration.allCases, id: \.self) { d in
Text(verbatim: d.description)
.tag(d)
}
}
}
Toggle(isOn: $status.playing_enabled, label: {
Text("Broadcast music playing on Apple Music", comment: "Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.")
})
HStack(alignment: .center) {
Button(action: {
dismiss()
}, label: {
Text("Cancel", comment: "Cancel button text for dismissing profile status settings view.")
})
Spacer()
Button(action: {
guard let status = self.status.general,
let kp = keypair.to_full(),
let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration)
else {
return
}
postbox.send(ev)
dismiss()
}, label: {
Text("Save", comment: "Save button text for saving profile status settings.")
})
.buttonStyle(GradientButtonStyle())
}
.padding([.top], 30)
Spacer()
}
.padding(30)
}
}
struct UserStatusSheet_Previews: PreviewProvider {
static var previews: some View {
UserStatusSheet(postbox: test_damus_state.postbox, keypair: test_keypair, status: .init())
}
}

View File

@@ -0,0 +1,83 @@
//
// UserStatus.swift
// damus
//
// Created by William Casarin on 2023-08-21.
//
import SwiftUI
import MediaPlayer
import WebKit
struct UserStatusView: View {
@ObservedObject var status: UserStatusModel
var show_general: Bool
var show_music: Bool
@Environment(\.openURL) var openURL
func Status(st: UserStatus, prefix: String = "") -> some View {
HStack {
Text(verbatim: "\(prefix)\(st.content)")
.lineLimit(1)
.foregroundColor(.gray)
.font(.callout.italic())
if st.url != nil {
Image("link")
.resizable()
.frame(width: 16, height: 16)
.foregroundColor(.gray)
}
}
.onTapGesture {
if let url = st.url {
openURL(url)
}
}
.contextMenu(
menuItems: {
if let url = st.url {
Button(url.absoluteString, action: { openURL(url) }) }
}, preview: {
if let url = st.url {
URLPreview(url: url)
}
})
}
var body: some View {
VStack(alignment: .leading, spacing: 2) {
if show_general, let general = status.general {
Status(st: general)
}
if show_music, let playing = status.music {
Status(st: playing, prefix: "🎵")
}
}
}
struct URLPreview: UIViewRepresentable {
var url: URL
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ wkView: WKWebView, context: Context) {
let request = URLRequest(url: url)
wkView.load(request)
}
}
}
/*
struct UserStatusView_Previews: PreviewProvider {
static var previews: some View {
UserStatusView(status: UserStatus(type: .music, expires_at: nil, content: "Track - Artist", created_at: 0, url: URL(string: "spotify:search:abc")), show_general: true, show_music: true)
}
}
*/

View File

@@ -10,7 +10,7 @@ import NaturalLanguage
struct Translated: Equatable {
let artifacts: NoteArtifacts
let artifacts: NoteArtifactsSeparated
let language: String
}
@@ -42,9 +42,10 @@ struct TranslateView: View {
.translate_button_style()
}
func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> some View {
func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated, font_size: Double) -> some View {
return VStack(alignment: .leading) {
Text(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja"))
let translatedFromLanguageString = String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja")
Text(translatedFromLanguageString)
.foregroundColor(.gray)
.font(.footnote)
.padding([.top, .bottom], 10)
@@ -53,7 +54,7 @@ struct TranslateView: View {
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
} else {
artifacts.content.text
.font(eventviewsize_to_font(self.size))
.font(eventviewsize_to_font(self.size, font_size: font_size))
}
}
}
@@ -63,7 +64,7 @@ struct TranslateView: View {
guard let note_language = translations_model.note_language else {
return
}
let res = await translate_note(profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, event: event, settings: damus_state.settings, note_lang: note_language)
let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language)
DispatchQueue.main.async {
self.translations_model.state = res
}
@@ -97,7 +98,7 @@ struct TranslateView: View {
Text("")
case .translated(let translated):
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
TranslatedView(lang: languageName, artifacts: translated.artifacts)
TranslatedView(lang: languageName, artifacts: translated.artifacts, font_size: damus_state.settings.font_size)
case .not_needed:
Text("")
}
@@ -119,16 +120,16 @@ extension View {
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
TranslateView(damus_state: ds, event: test_event, size: .normal)
let ds = test_damus_state
TranslateView(damus_state: ds, event: test_note, size: .normal)
}
}
func translate_note(profiles: Profiles, privkey: String?, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
// If the note language is different from our preferred languages, send a translation request.
let translator = Translator(settings)
let originalContent = event.get_content(privkey)
let originalContent = event.get_content(keypair)
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
guard let translated_note else {
@@ -142,7 +143,7 @@ func translate_note(profiles: Profiles, privkey: String?, event: NostrEvent, set
}
// Render translated note
let translated_blocks = event.get_blocks(content: translated_note)
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles)
// and cache it

View File

@@ -9,8 +9,8 @@ import SwiftUI
struct UserViewRow: View {
let damus_state: DamusState
let pubkey: String
let pubkey: Pubkey
var body: some View {
UserView(damus_state: damus_state, pubkey: pubkey)
.contentShape(Rectangle())
@@ -20,12 +20,12 @@ struct UserViewRow: View {
struct UserView: View {
let damus_state: DamusState
let pubkey: String
let pubkey: Pubkey
let spacer: Bool
@State var about_text: Text? = nil
init(damus_state: DamusState, pubkey: String, spacer: Bool = true) {
init(damus_state: DamusState, pubkey: Pubkey, spacer: Bool = true) {
self.damus_state = damus_state
self.pubkey = pubkey
self.spacer = spacer
@@ -37,8 +37,7 @@ struct UserView: View {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
ProfileName(pubkey: pubkey, damus: damus_state, show_nip5_domain: false)
if let about_text {
about_text
.lineLimit(3)
@@ -56,6 +55,6 @@ struct UserView: View {
struct UserView_Previews: PreviewProvider {
static var previews: some View {
UserView(damus_state: test_damus_state(), pubkey: "pk")
UserView(damus_state: test_damus_state, pubkey: test_note.pubkey)
}
}

View File

@@ -141,10 +141,10 @@ struct ZapButton: View {
struct ZapButton_Previews: PreviewProvider {
static var previews: some View {
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
let zaps = ZapsDataModel([.pending(pending_zap)])
ZapButton(damus_state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "lnurl", zaps: zaps)
ZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "lnurl", zaps: zaps)
}
}
@@ -183,90 +183,74 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
damus_state.add_zap(zap: .pending(pending_zap))
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
if mpayreq == nil {
mpayreq = await fetch_static_payreq(lnurl)
}
guard let payreq = mpayreq else {
Task { @MainActor in
guard let payreq = await damus_state.lnurls.lookup_or_fetch(pubkey: target.pubkey, lnurl: lnurl) else {
// TODO: show error
DispatchQueue.main.async {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.bad_lnurl)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
}
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.bad_lnurl)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping(ev))
return
}
DispatchQueue.main.async {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else {
DispatchQueue.main.async {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.fetching_invoice)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
}
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.fetching_invoice)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping(ev))
return
}
DispatchQueue.main.async {
switch pending_zap_state {
case .nwc(let nwc_state):
// don't both continuing, user has canceled
if case .cancel_fetching_invoice = nwc_state.state {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.canceled)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
return
}
var flusher: OnFlush? = nil
// donations are only enabled on one-tap zaps and off appstore
if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 {
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)
}
})
}
// 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)
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
let typ = ZappingEventType.failed(.send_failed)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
return
}
print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)")
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
// we don't need to trigger a ZapsDataModel update here
}
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
notify(.zapping, ev)
case .external(let pending_ext):
pending_ext.state = .done
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
notify(.zapping, ev)
switch pending_zap_state {
case .nwc(let nwc_state):
// don't both continuing, user has canceled
if case .cancel_fetching_invoice = nwc_state.state {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.canceled)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping(ev))
return
}
var flusher: OnFlush? = nil
// donations are only enabled on one-tap zaps and off appstore
if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 {
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)
}
})
}
// 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)
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
let typ = ZappingEventType.failed(.send_failed)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping(ev))
return
}
print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)")
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
// we don't need to trigger a ZapsDataModel update here
}
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
notify(.zapping(ev))
case .external(let pending_ext):
pending_ext.state = .done
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
notify(.zapping(ev))
}
}

124
damus/ContentParsing.swift Normal file
View File

@@ -0,0 +1,124 @@
//
// ContentParsing.swift
// damus
//
// Created by William Casarin on 2023-07-22.
//
import Foundation
enum NoteContent {
case note(NostrEvent)
case content(String, TagsSequence?)
init(note: NostrEvent, keypair: Keypair) {
if note.known_kind == .dm {
self = .content(note.get_content(keypair), note.tags)
} else {
self = .note(note)
}
}
}
func parsed_blocks_finish(bs: inout note_blocks, tags: TagsSequence?) -> Blocks {
var out: [Block] = []
var i = 0
while (i < bs.num_blocks) {
let block = bs.blocks[i]
if let converted = Block(block, tags: tags) {
out.append(converted)
}
i += 1
}
let words = Int(bs.words)
blocks_free(&bs)
return Blocks(words: words, blocks: out)
}
func parse_note_content(content: NoteContent) -> Blocks {
var bs = note_blocks()
bs.num_blocks = 0;
blocks_init(&bs)
switch content {
case .content(let s, let tags):
return s.withCString { cptr in
damus_parse_content(&bs, cptr)
return parsed_blocks_finish(bs: &bs, tags: tags)
}
case .note(let note):
damus_parse_content(&bs, note.content_raw)
return parsed_blocks_finish(bs: &bs, tags: note.tags)
}
}
func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef] {
if tags.count == 0 {
return []
}
/// build a set of indices for each event mention
let mention_indices = build_mention_indices(blocks, type: .e)
/// simpler case with no mentions
if mention_indices.count == 0 {
return interp_event_refs_without_mentions_ndb(tags.note.referenced_noterefs)
}
return interp_event_refs_with_mentions_ndb(tags: tags, mention_indices: mention_indices)
}
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [EventRef] {
var count = 0
var evrefs: [EventRef] = []
var first: Bool = true
var first_ref: NoteRef? = nil
for ref in ev_tags {
if first {
first_ref = ref
evrefs.append(.thread_id(ref))
first = false
} else {
evrefs.append(.reply(ref))
}
count += 1
}
if let first_ref, count == 1 {
let r = first_ref
return [.reply_to_root(r)]
}
return evrefs
}
func interp_event_refs_with_mentions_ndb(tags: TagsSequence, mention_indices: Set<Int>) -> [EventRef] {
var mentions: [EventRef] = []
var ev_refs: [NoteRef] = []
var i: Int = 0
for tag in tags {
if let note_id = NoteRef.from_tag(tag: tag) {
if mention_indices.contains(i) {
mentions.append(.mention(.noteref(note_id, index: i)))
} else {
ev_refs.append(note_id)
}
}
i += 1
}
var replies = interp_event_refs_without_mentions(ev_refs)
replies.append(contentsOf: mentions)
return replies
}

View File

@@ -7,12 +7,7 @@
import SwiftUI
import AVKit
struct TimestampedProfile {
let profile: Profile
let timestamp: Int64
let event: NostrEvent
}
import MediaPlayer
struct ZapSheet {
let target: ZapTarget
@@ -30,6 +25,8 @@ enum Sheets: Identifiable {
case zap(ZapSheet)
case select_wallet(SelectWallet)
case filter
case user_status
case suggestedUsers
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
return .zap(ZapSheet(target: target, lnurl: lnurl))
@@ -42,25 +39,13 @@ enum Sheets: Identifiable {
var id: String {
switch self {
case .report: return "report"
case .post(let action): return "post-" + (action.ev?.id ?? "")
case .event(let ev): return "event-" + ev.id
case .zap(let sheet): return "zap-" + sheet.target.id
case .user_status: return "user_status"
case .post(let action): return "post-" + (action.ev?.id.hex() ?? "")
case .event(let ev): return "event-" + ev.id.hex()
case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id)
case .select_wallet: return "select-wallet"
case .filter: return "filter"
}
}
}
enum FilterState : Int {
case posts_and_replies = 1
case posts = 0
func filter(ev: NostrEvent) -> Bool {
switch self {
case .posts:
return !ev.is_reply(nil)
case .posts_and_replies:
return true
case .suggestedUsers: return "suggested-users"
}
}
}
@@ -68,11 +53,11 @@ enum FilterState : Int {
struct ContentView: View {
let keypair: Keypair
var pubkey: String {
var pubkey: Pubkey {
return keypair.pubkey
}
var privkey: String? {
var privkey: Privkey? {
return keypair.privkey
}
@@ -81,8 +66,7 @@ struct ContentView: View {
@State var active_sheet: Sheets? = nil
@State var damus_state: DamusState? = nil
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
@State var is_deleted_account: Bool = false
@State var muting: String? = nil
@State var muting: Pubkey? = nil
@State var confirm_mute: Bool = false
@State var user_muted_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@@ -90,9 +74,9 @@ struct ContentView: View {
@State private var isSideBarOpened = false
var home: HomeModel = HomeModel()
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
@AppStorage("has_seen_suggested_users") private var hasSeenSuggestedUsers = false
let sub_id = UUID().description
@Environment(\.colorScheme) var colorScheme
// connect retry timer
@@ -102,7 +86,13 @@ struct ContentView: View {
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
.id("what")
}
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
var filters = ContentFilters.defaults(damus_state!.settings)
filters.append(fstate.filter)
return ContentFilters(filters: filters).filter
}
var PostingTimelineView: some View {
VStack {
ZStack {
@@ -110,10 +100,10 @@ struct ContentView: View {
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
mystery
contentTimelineView(filter: FilterState.posts.filter)
contentTimelineView(filter: content_filter(.posts))
.tag(FilterState.posts)
.id(FilterState.posts)
contentTimelineView(filter: FilterState.posts_and_replies.filter)
contentTimelineView(filter: content_filter(.posts_and_replies))
.tag(FilterState.posts_and_replies)
.id(FilterState.posts_and_replies)
}
@@ -142,7 +132,7 @@ struct ContentView: View {
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack {
if let damus = self.damus_state {
TimelineView(events: home.events, loading: .constant(false), damus: damus, show_friend_icon: false, filter: filter)
TimelineView<AnyView>(events: home.events, loading: .constant(false), damus: damus, show_friend_icon: false, filter: filter)
}
}
}
@@ -203,8 +193,8 @@ struct ContentView: View {
func MaybeReportView(target: ReportTarget) -> some View {
Group {
if let damus_state {
if let sec = damus_state.keypair.privkey {
ReportView(postbox: damus_state.postbox, target: target, privkey: sec)
if let keypair = damus_state.keypair.to_full() {
ReportView(postbox: damus_state.postbox, target: target, keypair: keypair)
} else {
EmptyView()
}
@@ -224,9 +214,15 @@ struct ContentView: View {
navigationCoordinator.push(route: Route.Wallet(wallet: damus_state!.wallet))
}
func open_profile(id: String) {
let profile_model = ProfileModel(pubkey: id, damus: damus_state!)
let followers = FollowersModel(damus_state: damus_state!, target: id)
func open_script(_ script: [UInt8]) {
print("pushing script nav")
let model = ScriptModel(data: script, state: .not_loaded)
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))
}
@@ -279,7 +275,7 @@ struct ContentView: View {
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
)
.navigationDestination(for: Route.self) { route in
route.view(navigationCordinator: navigationCoordinator, damusState: damus_state!)
route.view(navigationCoordinator: navigationCoordinator, damusState: damus_state!)
}
.onReceive(handle_notify(.switched_timeline)) { _ in
navigationCoordinator.popToRoot()
@@ -297,6 +293,10 @@ struct ContentView: View {
self.connect()
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
setup_notifications()
if !hasSeenSuggestedUsers {
active_sheet = .suggestedUsers
hasSeenSuggestedUsers = true
}
}
.sheet(item: $active_sheet) { item in
switch item {
@@ -304,6 +304,8 @@ struct ContentView: View {
MaybeReportView(target: target)
case .post(let action):
PostView(action: action, damus_state: damus_state!)
case .user_status:
UserStatusSheet(postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
case .event:
EventDetailView()
case .zap(let zapsheet):
@@ -319,6 +321,8 @@ struct ContentView: View {
} else {
RelayFilterView(state: damus_state!, timeline: timeline)
}
case .suggestedUsers:
SuggestedUsersView(model: SuggestedUsersViewModel(damus_state: damus_state!))
}
}
.onOpenURL { url in
@@ -329,101 +333,97 @@ struct ContentView: View {
switch res {
case .filter(let filt): self.open_search(filt: filt)
case .profile(let id): self.open_profile(id: id)
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 .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
case .script(let data): self.open_script(data)
}
}
}
.onReceive(handle_notify(.compose)) { notif in
let action = notif.object as! PostAction
.onReceive(handle_notify(.compose)) { action in
self.active_sheet = .post(action)
}
.onReceive(timer) { n in
self.damus_state?.postbox.try_flushing_events()
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
}
.onReceive(handle_notify(.deleted_account)) { notif in
self.is_deleted_account = true
}
.onReceive(handle_notify(.report)) { notif in
let target = notif.object as! ReportTarget
.onReceive(handle_notify(.report)) { target in
self.active_sheet = .report(target)
}
.onReceive(handle_notify(.mute)) { notif in
let pubkey = notif.object as! String
.onReceive(handle_notify(.mute)) { pubkey in
self.muting = pubkey
self.confirm_mute = true
}
.onReceive(handle_notify(.attached_wallet)) { notif in
.onReceive(handle_notify(.attached_wallet)) { nwc in
// update the lightning address on our profile when we attach a
// wallet with an associated
let nwc = notif.object as! WalletConnectURL
guard let ds = self.damus_state,
let lud16 = nwc.lud16,
let keypair = ds.keypair.to_full(),
let profile = ds.profiles.lookup(id: ds.pubkey),
lud16 != profile.lud16
let keypair = ds.keypair.to_full()
else {
return
}
let profile_txn = ds.profiles.lookup(id: ds.pubkey)
guard let profile = profile_txn.unsafeUnownedValue,
lud16 != profile.lud16 else {
return
}
// clear zapper cache for old lud16
if profile.lud16 != nil {
// TODO: should this be somewhere else, where we process profile events!?
invalidate_zapper_cache(pubkey: keypair.pubkey, profiles: ds.profiles, lnurl: ds.lnurls)
}
profile.lud16 = lud16
let ev = make_metadata_event(keypair: keypair, metadata: profile)
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
ds.postbox.send(ev)
}
.onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent
guard let ds = self.damus_state else {
return
}
.onReceive(handle_notify(.broadcast)) { ev in
guard let ds = self.damus_state else { return }
ds.postbox.send(ev)
if let profile = ds.profiles.lookup_with_timestamp(id: ev.pubkey) {
ds.postbox.send(profile.event)
}
}
.onReceive(handle_notify(.unfollow)) { notif in
guard let state = self.damus_state else {
return
}
handle_unfollow(state: state, notif: notif)
.onReceive(handle_notify(.unfollow)) { target in
guard let state = self.damus_state else { return }
_ = handle_unfollow(state: state, unfollow: target.follow_ref)
}
.onReceive(handle_notify(.follow)) { notif in
guard let state = self.damus_state else {
return
}
handle_follow(state: state, notif: notif)
.onReceive(handle_notify(.unfollowed)) { unfollow in
home.resubscribe(.unfollowing(unfollow))
}
.onReceive(handle_notify(.post)) { notif in
.onReceive(handle_notify(.follow)) { target in
guard let state = self.damus_state else { return }
handle_follow_notif(state: state, target: target)
}
.onReceive(handle_notify(.followed)) { _ in
home.resubscribe(.following)
}
.onReceive(handle_notify(.post)) { post in
guard let state = self.damus_state,
let keypair = state.keypair.to_full() else {
return
}
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, notif: notif) {
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) {
self.active_sheet = nil
}
}
.onReceive(handle_notify(.new_mutes)) { notif in
.onReceive(handle_notify(.new_mutes)) { _ in
home.filter_events()
}
.onReceive(handle_notify(.mute_thread)) { notif in
.onReceive(handle_notify(.mute_thread)) { _ in
home.filter_events()
}
.onReceive(handle_notify(.unmute_thread)) { notif in
.onReceive(handle_notify(.unmute_thread)) { _ in
home.filter_events()
}
.onReceive(handle_notify(.present_sheet)) { notif in
let sheet = notif.object as! Sheets
.onReceive(handle_notify(.present_sheet)) { sheet in
self.active_sheet = sheet
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
.onReceive(handle_notify(.zapping)) { zap_ev in
guard !zap_ev.is_custom else {
return
}
@@ -458,57 +458,49 @@ struct ContentView: View {
break
}
}
.onReceive(handle_notify(.local_notification)) { notif in
guard let local = notif.object as? LossyLocalNotification,
let damus_state else {
return
.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):
guard let target = damus_state.events.lookup(noteId) else {
return
}
switch local.type {
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:
open_event(ev: target)
case .profile_zap:
// Handled separately above.
break
}
}
if local.type == .profile_zap {
open_profile(id: local.event_id)
return
}
guard let target = damus_state.events.lookup(local.event_id) else {
return
}
switch local.type {
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: fallthrough
case .zap: fallthrough
case .mention: fallthrough
case .repost:
open_event(ev: target)
case .profile_zap:
// Handled separately above.
break
}
}
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
let hide = notif.object as! Bool
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
home.filter_events()
guard let damus_state,
let profile = damus_state.profiles.lookup(id: damus_state.pubkey),
let keypair = damus_state.keypair.to_full()
guard let ds = damus_state else { return }
let profile_txn = ds.profiles.lookup(id: ds.pubkey)
guard let profile = profile_txn.unsafeUnownedValue,
let keypair = ds.keypair.to_full()
else {
return
}
profile.reactions = !hide
let profile_ev = make_metadata_event(keypair: keypair, metadata: profile)
damus_state.postbox.send(profile_ev)
}
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
is_deleted_account = false
notify(.logout, ())
}
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
ds.postbox.send(profile_ev)
}
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
@@ -516,11 +508,12 @@ struct ContentView: View {
}
}, message: {
if let pubkey = self.muting {
let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
let name = damus_state!.profiles.lookup(id: pubkey).map { profile in
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
}.value
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
} else {
Text("User has been muted", comment: "Alert message that informs a user was d.")
Text("User has been muted", comment: "Alert message that informs a user was muted.")
}
})
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
@@ -533,7 +526,7 @@ struct ContentView: View {
guard let ds = damus_state,
let keypair = ds.keypair.to_full(),
let pubkey = muting,
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey)
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(pubkey))
else {
return
}
@@ -560,24 +553,25 @@ struct ContentView: View {
if ds.contacts.mutelist == nil {
confirm_overwrite_mutelist = true
} else {
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = muting else {
guard let keypair = ds.keypair.to_full(),
let pubkey = muting
else {
return
}
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: .pubkey(pubkey)) else {
return
}
damus_state?.contacts.set_mutelist(ev)
ds.postbox.send(ev)
}
}
}, message: {
if let pubkey = muting {
let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
let name = damus_state?.profiles.lookup(id: pubkey).map({ profile in
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
}).value ?? "unknown"
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
} else {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
@@ -587,12 +581,12 @@ struct ContentView: View {
func switch_timeline(_ timeline: Timeline) {
self.isSideBarOpened = false
self.popToRoot()
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
notify(.switched_timeline(timeline))
if timeline == self.selected_timeline {
NotificationCenter.default.post(name: .scroll_to_top, object: nil)
notify(.scroll_to_top)
return
}
@@ -600,69 +594,100 @@ struct ContentView: View {
}
func connect() {
let pool = RelayPool()
let metadatas = RelayMetadatas()
// nostrdb
let ndb = Ndb()!
let pool = RelayPool(ndb: ndb)
let model_cache = RelayModelCache()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays {
if let url = RelayURL(relay) {
let descriptor = RelayDescriptor(url: url, info: .rw)
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters)
}
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
// dumb stuff needed for property wrappers
UserSettingsStore.pubkey = pubkey
let settings = UserSettingsStore()
UserSettingsStore.shared = settings
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays {
if let url = RelayURL(relay) {
let descriptor = RelayDescriptor(url: url, info: .rw)
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
}
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
if let nwc_str = settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: nwc_str) {
try? pool.add_relay(.nwc(url: nwc.relay))
}
let user_search_cache = UserSearchCache()
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
profiles: Profiles(),
profiles: Profiles(ndb: ndb),
dms: home.dms,
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: settings,
relay_filters: relay_filters,
relay_metadata: metadatas,
relay_model_cache: model_cache,
drafts: Drafts(),
events: EventCache(),
events: EventCache(ndb: ndb),
bookmarks: BookmarksManager(pubkey: pubkey),
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
muted_threads: MutedThreadsManager(keypair: keypair),
wallet: WalletModel(settings: settings),
nav: self.navigationCoordinator
nav: self.navigationCoordinator,
music: MusicController(onChange: music_changed),
video: VideoController(),
ndb: ndb
)
home.damus_state = self.damus_state!
pool.connect()
}
func music_changed(_ state: MusicState) {
guard let damus_state else { return }
switch state {
case .playback_state:
break
case .song(let song):
guard let song, let kp = damus_state.keypair.to_full() else { return }
let pdata = damus_state.profiles.profile_data(damus_state.pubkey)
let desc = "\(song.title ?? "Unknown") - \(song.artist ?? "Unknown")"
let encodedDesc = desc.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let url = encodedDesc.flatMap { enc in
URL(string: "spotify:search:\(enc)")
}
let music = UserStatus(type: .music, expires_at: Date.now.addingTimeInterval(song.playbackDuration), content: desc, created_at: UInt32(Date.now.timeIntervalSince1970), url: url)
pdata.status.music = music
guard let ev = music.to_note(keypair: kp) else { return }
damus_state.postbox.send(ev)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(keypair: Keypair(pubkey: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", privkey: nil))
ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil))
}
}
func get_since_time(last_event: NostrEvent?) -> Int64? {
func get_since_time(last_event: NostrEvent?) -> UInt32? {
if let last_event = last_event {
return last_event.created_at - 60 * 10
}
@@ -682,7 +707,7 @@ extension UINavigationController: UIGestureRecognizerDelegate {
}
struct LastNotification {
let id: String
let id: NoteId
let created_at: Int64
}
@@ -691,29 +716,32 @@ func get_last_event(_ timeline: Timeline) -> LastNotification? {
let last = UserDefaults.standard.string(forKey: "last_\(str)")
let last_created = UserDefaults.standard.string(forKey: "last_\(str)_time")
.flatMap { Int64($0) }
return last.flatMap { id in
last_created.map { created in
return LastNotification(id: id, created_at: created)
}
guard let last,
let note_id = NoteId(hex: last),
let last_created
else {
return nil
}
return LastNotification(id: note_id, created_at: last_created)
}
func save_last_event(_ ev: NostrEvent, timeline: Timeline) {
let str = timeline.rawValue
UserDefaults.standard.set(ev.id, forKey: "last_\(str)")
UserDefaults.standard.set(ev.id.hex(), forKey: "last_\(str)")
UserDefaults.standard.set(String(ev.created_at), forKey: "last_\(str)_time")
}
func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
return filters.map { filter in
let kinds = filter.kinds ?? []
let initial: Int64? = nil
let initial: UInt32? = nil
let earliest = kinds.reduce(initial) { earliest, kind in
let last = last_of_kind[kind.rawValue]
let since: Int64? = get_since_time(last_event: last)
let since: UInt32? = get_since_time(last_event: last)
if earliest == nil {
if since == nil {
return nil
@@ -740,7 +768,6 @@ func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrF
func setup_notifications() {
UIApplication.shared.registerForRemoteNotifications()
let center = UNUserNotificationCenter.current()
@@ -759,36 +786,42 @@ struct FindEvent {
let type: FindEventType
let find_from: [String]?
static func profile(pubkey: String, find_from: [String]? = nil) -> FindEvent {
static func profile(pubkey: Pubkey, find_from: [String]? = nil) -> FindEvent {
return FindEvent(type: .profile(pubkey), find_from: find_from)
}
static func event(evid: String, find_from: [String]? = nil) -> FindEvent {
static func event(evid: NoteId, find_from: [String]? = nil) -> FindEvent {
return FindEvent(type: .event(evid), find_from: find_from)
}
}
enum FindEventType {
case profile(String)
case event(String)
case profile(Pubkey)
case event(NoteId)
}
enum FoundEvent {
case profile(Profile, NostrEvent)
case profile(Pubkey)
case invalid_profile(NostrEvent)
case event(NostrEvent)
}
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)
}
func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: String, callback: @escaping (FoundEvent?) -> ()) {
var filter: NostrFilter? = nil
let find_from = query_.find_from
let query = query_.type
switch query {
case .profile(let pubkey):
if let profile = state.profiles.lookup_with_timestamp(id: pubkey) {
callback(.profile(profile.profile, profile.event))
if let record = state.ndb.lookup_profile(pubkey).unsafeUnownedValue,
record.profile != nil
{
callback(.profile(pubkey))
return
}
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
@@ -802,7 +835,6 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
filter = NostrFilter(ids: [evid], limit: 1)
}
let subid = UUID().description
var attempts: Int = 0
var has_event = false
guard let filter else { return }
@@ -826,14 +858,11 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
switch query {
case .profile:
if ev.known_kind == .metadata {
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) { profile in
guard let profile else {
callback(.invalid_profile(ev))
return
}
callback(.profile(profile, ev))
guard state.ndb.lookup_profile_key(ev.pubkey) != nil else {
callback(.invalid_profile(ev))
return
}
callback(.profile(ev.pubkey))
}
case .event:
callback(.event(ev))
@@ -846,7 +875,7 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
}
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
}
case .notice(_):
case .notice:
break
}
@@ -869,64 +898,84 @@ func timeline_name(_ timeline: Timeline?) -> String {
}
}
func handle_unfollow(state: DamusState, notif: Notification) {
guard let privkey = state.keypair.privkey else {
return
@discardableResult
func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
guard let keypair = state.keypair.to_full() else {
return false
}
let target = notif.object as! FollowTarget
let pk = target.pubkey
if let ev = unfollow_user(postbox: state.postbox,
our_contacts: state.contacts.event,
pubkey: state.pubkey,
privkey: privkey,
unfollow: pk) {
notify(.unfollowed, pk)
state.contacts.event = ev
let old_contacts = state.contacts.event
guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
else {
return false
}
notify(.unfollowed(unfollow))
state.contacts.event = ev
switch unfollow {
case .pubkey(let pk):
state.contacts.remove_friend(pk)
//friend_events = friend_events.filter { $0.pubkey != pk }
case .hashtag:
// nothing to handle here really
break
}
return true
}
func handle_follow(state: DamusState, notif: Notification) {
guard let privkey = state.keypair.privkey else {
return
@discardableResult
func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
guard let keypair = state.keypair.to_full() else {
return false
}
let fnotify = notif.object as! FollowTarget
if let ev = follow_user(pool: state.pool,
our_contacts: state.contacts.event,
pubkey: state.pubkey,
privkey: privkey,
follow: ReferencedId(ref_id: fnotify.pubkey, relay_id: nil, key: "p")) {
notify(.followed, fnotify.pubkey)
state.contacts.event = ev
switch fnotify {
case .pubkey(let pk):
state.contacts.add_friend_pubkey(pk)
case .contact(let ev):
state.contacts.add_friend_contact(ev)
}
guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
else {
return false
}
notify(.followed(follow))
state.contacts.event = ev
switch follow {
case .pubkey(let pubkey):
state.contacts.add_friend_pubkey(pubkey)
case .hashtag:
// nothing to do
break
}
return true
}
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, notif: Notification) -> Bool {
let post_res = notif.object as! NostrPostResult
switch post_res {
@discardableResult
func handle_follow_notif(state: DamusState, target: FollowTarget) -> Bool {
switch target {
case .pubkey(let pk):
state.contacts.add_friend_pubkey(pk)
case .contact(let ev):
state.contacts.add_friend_contact(ev)
}
return handle_follow(state: state, follow: target.follow_ref)
}
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, post: NostrPostResult) -> Bool {
switch post {
case .post(let post):
//let post = tup.0
//let to_relays = tup.1
print("post \(post.content)")
let new_ev = post_to_event(post: post, privkey: keypair.privkey, pubkey: keypair.pubkey)
guard let new_ev = post_to_event(post: post, keypair: keypair) else {
return false
}
postbox.send(new_ev)
for eref in new_ev.referenced_ids.prefix(3) {
// also broadcast at most 3 referenced events
if let ev = events.lookup(eref.ref_id) {
if let ev = events.lookup(eref) {
postbox.send(ev)
}
}
@@ -939,10 +988,11 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
enum OpenResult {
case profile(String)
case profile(Pubkey)
case filter(NostrFilter)
case event(NostrEvent)
case wallet_connect(WalletConnectURL)
case script([UInt8])
}
func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) {
@@ -958,17 +1008,27 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
switch link {
case .ref(let ref):
if ref.key == "p" {
result(.profile(ref.ref_id))
} else if ref.key == "e" {
find_event(state: state, query: .event(evid: ref.ref_id)) { res in
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.string()])))
case .param, .quote:
// doesn't really make sense here
break
}
case .filter(let filt):
result(.filter(filt))
break
// TODO: handle filter searches?
case .script(let script):
result(.script(script))
break
}
}

View File

@@ -68,6 +68,8 @@
</dict>
<key>NSCameraUsageDescription</key>
<string>Damus needs access to your camera if you want to upload photos from it</string>
<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 if you want to upload recorded videos from it</string>
</dict>

View File

@@ -28,19 +28,7 @@ class ActionBarModel: ObservableObject {
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
}
init() {
self.our_like = nil
self.our_boost = nil
self.our_reply = nil
self.our_zap = nil
self.likes = 0
self.boosts = 0
self.zaps = 0
self.zap_total = 0
self.replies = 0
}
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zapping?, our_reply: NostrEvent?) {
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil) {
self.likes = likes
self.boosts = boosts
self.zaps = zaps
@@ -52,7 +40,7 @@ class ActionBarModel: ObservableObject {
self.our_reply = our_reply
}
func update(damus: DamusState, evid: String) {
func update(damus: DamusState, evid: NoteId) {
self.likes = damus.likes.counts[evid] ?? 0
self.boosts = damus.boosts.counts[evid] ?? 0
self.zaps = damus.zaps.event_counts[evid] ?? 0

View File

@@ -7,18 +7,18 @@
import Foundation
fileprivate func get_bookmarks_key(pubkey: String) -> String {
fileprivate func get_bookmarks_key(pubkey: Pubkey) -> String {
pk_setting_key(pubkey, key: "bookmarks")
}
func load_bookmarks(pubkey: String) -> [NostrEvent] {
func load_bookmarks(pubkey: Pubkey) -> [NostrEvent] {
let key = get_bookmarks_key(pubkey: pubkey)
return (UserDefaults.standard.stringArray(forKey: key) ?? []).compactMap {
event_from_json(dat: $0)
}
}
func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
func save_bookmarks(pubkey: Pubkey, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
let uniq_bookmarks = uniq(value)
if uniq_bookmarks != current_value {
@@ -32,8 +32,8 @@ func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEv
class BookmarksManager: ObservableObject {
private let pubkey: String
private let pubkey: Pubkey
private var _bookmarks: [NostrEvent]
var bookmarks: [NostrEvent] {
get {
@@ -47,7 +47,7 @@ class BookmarksManager: ObservableObject {
}
}
init(pubkey: String) {
init(pubkey: Pubkey) {
self._bookmarks = load_bookmarks(pubkey: pubkey)
self.pubkey = pubkey
}

View File

@@ -9,56 +9,56 @@ import Foundation
class Contacts {
private var friends: Set<String> = Set()
private var friend_of_friends: Set<String> = Set()
private var friends: Set<Pubkey> = Set()
private var friend_of_friends: Set<Pubkey> = Set()
/// Tracks which friends are friends of a given pubkey.
private var pubkey_to_our_friends = [String : Set<String>]()
private var muted: Set<String> = Set()
let our_pubkey: String
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
private var muted: Set<Pubkey> = Set()
let our_pubkey: Pubkey
var event: NostrEvent?
var mutelist: NostrEvent?
init(our_pubkey: String) {
init(our_pubkey: Pubkey) {
self.our_pubkey = our_pubkey
}
func is_muted(_ pk: String) -> Bool {
func is_muted(_ pk: Pubkey) -> Bool {
return muted.contains(pk)
}
func set_mutelist(_ ev: NostrEvent) {
let oldlist = self.mutelist
self.mutelist = ev
let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? [])
let new = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
let old = oldlist.map({ ev in Set(ev.referenced_pubkeys) }) ?? Set<Pubkey>()
let new = Set(ev.referenced_pubkeys)
let diff = old.symmetricDifference(new)
var new_mutes = Array<String>()
var new_unmutes = Array<String>()
var new_mutes = Set<Pubkey>()
var new_unmutes = Set<Pubkey>()
for d in diff {
if new.contains(d) {
new_mutes.append(d)
new_mutes.insert(d)
} else {
new_unmutes.append(d)
new_unmutes.insert(d)
}
}
// TODO: set local mutelist here
self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
self.muted = Set(ev.referenced_pubkeys)
if new_mutes.count > 0 {
notify(.new_mutes, new_mutes)
notify(.new_mutes(new_mutes))
}
if new_unmutes.count > 0 {
notify(.new_unmutes, new_unmutes)
notify(.new_unmutes(new_unmutes))
}
}
func remove_friend(_ pubkey: String) {
func remove_friend(_ pubkey: Pubkey) {
friends.remove(pubkey)
pubkey_to_our_friends.forEach {
@@ -66,99 +66,105 @@ class Contacts {
}
}
func get_friend_list() -> [String] {
return Array(friends)
func get_friend_list() -> Set<Pubkey> {
return friends
}
func get_followed_hashtags() -> Set<String> {
guard let ev = self.event else { return Set() }
return Set(ev.referenced_hashtags.map({ $0.hashtag }))
}
func add_friend_pubkey(_ pubkey: String) {
func follows(hashtag: Hashtag) -> Bool {
guard let ev = self.event else { return false }
return ev.referenced_hashtags.first(where: { $0 == hashtag }) != nil
}
func add_friend_pubkey(_ pubkey: Pubkey) {
friends.insert(pubkey)
}
func add_friend_contact(_ contact: NostrEvent) {
friends.insert(contact.pubkey)
for tag in contact.tags {
if tag.count >= 2 && tag[0] == "p" {
friend_of_friends.insert(tag[1])
for pk in contact.referenced_pubkeys {
friend_of_friends.insert(pk)
// Exclude themself and us.
if contact.pubkey != our_pubkey && contact.pubkey != tag[1] {
if pubkey_to_our_friends[tag[1]] == nil {
pubkey_to_our_friends[tag[1]] = Set<String>()
}
pubkey_to_our_friends[tag[1]]?.insert(contact.pubkey)
// Exclude themself and us.
if contact.pubkey != our_pubkey && contact.pubkey != pk {
if pubkey_to_our_friends[pk] == nil {
pubkey_to_our_friends[pk] = Set<Pubkey>()
}
pubkey_to_our_friends[pk]?.insert(contact.pubkey)
}
}
}
func is_friend_of_friend(_ pubkey: String) -> Bool {
func is_friend_of_friend(_ pubkey: Pubkey) -> Bool {
return friend_of_friends.contains(pubkey)
}
func is_in_friendosphere(_ pubkey: String) -> Bool {
func is_in_friendosphere(_ pubkey: Pubkey) -> Bool {
return friends.contains(pubkey) || friend_of_friends.contains(pubkey)
}
func is_friend(_ pubkey: String) -> Bool {
func is_friend(_ pubkey: Pubkey) -> Bool {
return friends.contains(pubkey)
}
func is_friend_or_self(_ pubkey: String) -> Bool {
func is_friend_or_self(_ pubkey: Pubkey) -> Bool {
return pubkey == our_pubkey || is_friend(pubkey)
}
func follow_state(_ pubkey: String) -> FollowState {
func follow_state(_ pubkey: Pubkey) -> FollowState {
return is_friend(pubkey) ? .follows : .unfollows
}
/// Gets the list of pubkeys of our friends who follow the given pubkey.
func get_friended_followers(_ pubkey: String) -> [String] {
func get_friended_followers(_ pubkey: Pubkey) -> [Pubkey] {
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
}
}
func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, follow: ReferencedId) -> NostrEvent? {
guard let ev = follow_user_event(our_contacts: our_contacts, our_pubkey: pubkey, follow: follow) else {
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else {
return nil
}
ev.calculate_id()
ev.sign(privkey: privkey)
pool.send(.event(ev))
box.send(ev)
return ev
}
func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
guard let cs = our_contacts else {
return nil
}
let ev = unfollow_user_event(our_contacts: cs, our_pubkey: pubkey, unfollow: unfollow)
ev.calculate_id()
ev.sign(privkey: privkey)
guard let ev = unfollow_reference_event(our_contacts: cs, keypair: keypair, unfollow: unfollow) else {
return nil
}
postbox.send(ev)
return ev
}
func unfollow_user_event(our_contacts: NostrEvent, our_pubkey: String, unfollow: String) -> NostrEvent {
let tags = our_contacts.tags.filter { tag in
if tag.count >= 2 && tag[0] == "p" && tag[1] == unfollow {
return false
func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
let tags = our_contacts.tags.reduce(into: [[String]]()) { ts, tag in
if let tag = FollowRef.from_tag(tag: tag), tag == unfollow {
return
}
return true
ts.append(tag.strings())
}
let kind = NostrKind.contacts.rawValue
return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags)
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: Array(tags))
}
func follow_user_event(our_contacts: NostrEvent?, our_pubkey: String, follow: ReferencedId) -> NostrEvent? {
func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
guard let cs = our_contacts else {
// don't create contacts for now so we don't nuke our contact list due to connectivity issues
// we should only create contacts during profile creation
@@ -166,7 +172,7 @@ func follow_user_event(our_contacts: NostrEvent?, our_pubkey: String, follow: Re
return nil
}
guard let ev = follow_with_existing_contacts(our_pubkey: our_pubkey, our_contacts: cs, follow: follow) else {
guard let ev = follow_with_existing_contacts(keypair: keypair, our_contacts: cs, follow: follow) else {
return nil
}
@@ -178,23 +184,18 @@ func decode_json_relays(_ content: String) -> [String: RelayInfo]? {
return decode_json(content)
}
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], privkey: String, relay: String) -> NostrEvent? {
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: String) -> NostrEvent?{
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
relays.removeValue(forKey: relay)
print("remove_relay \(relays)")
guard let content = encode_json(relays) else {
return nil
}
let new_ev = NostrEvent(content: content, pubkey: ev.pubkey, kind: 3, tags: ev.tags)
new_ev.calculate_id()
new_ev.sign(privkey: privkey)
return new_ev
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
}
func add_relay(ev: NostrEvent, privkey: String, current_relays: [RelayDescriptor], relay: String, info: RelayInfo) -> NostrEvent? {
func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: String, info: RelayInfo) -> NostrEvent? {
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
guard relays.index(forKey: relay) == nil else {
@@ -207,10 +208,7 @@ func add_relay(ev: NostrEvent, privkey: String, current_relays: [RelayDescriptor
return nil
}
let new_ev = NostrEvent(content: content, pubkey: ev.pubkey, kind: 3, tags: ev.tags)
new_ev.calculate_id()
new_ev.sign(privkey: privkey)
return new_ev
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
}
func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: RelayInfo] {
@@ -220,16 +218,31 @@ func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: R
return relay_info
}
func follow_with_existing_contacts(our_pubkey: String, our_contacts: NostrEvent, follow: ReferencedId) -> NostrEvent? {
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
return contacts.references.contains { ref in
switch (ref, follow) {
case let (.hashtag(ht), .hashtag(follow_ht)):
return ht.string() == follow_ht
case let (.pubkey(pk), .pubkey(follow_pk)):
return pk == follow_pk
case (.hashtag, .pubkey), (.pubkey, .hashtag),
(.event, _), (.quote, _), (.param, _):
return false
}
}
}
func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: FollowRef) -> NostrEvent? {
// don't update if we're already following
if our_contacts.references(id: follow.ref_id, key: "p") {
if is_already_following(contacts: our_contacts, follow: follow) {
return nil
}
let kind = NostrKind.contacts.rawValue
var tags = our_contacts.tags
tags.append(refid_to_tag(follow))
return NostrEvent(content: our_contacts.content, pubkey: our_pubkey, kind: kind, tags: tags)
var tags = our_contacts.tags.strings()
tags.append(follow.tag)
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
}
func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {

View File

@@ -0,0 +1,54 @@
//
// ContentFilters.swift
// damus
//
// Created by Daniel DAquino on 2023-09-18.
//
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
func filter(ev: NostrEvent) -> Bool {
switch self {
case .posts:
return ev.known_kind == .boost || !ev.is_reply(.empty)
case .posts_and_replies:
return true
}
}
}
/// Simple filter to determine whether to show posts with #nsfw tags
func nsfw_tag_filter(ev: NostrEvent) -> Bool {
return ev.referenced_hashtags.first(where: { t in t.hashtag == "nsfw" }) == nil
}
/// Generic filter with various tweakable settings
struct ContentFilters {
var filters: [(NostrEvent) -> Bool]
func filter(ev: NostrEvent) -> Bool {
for filter in filters {
if !filter(ev) {
return false
}
}
return true
}
}
extension ContentFilters {
static func defaults(_ settings: UserSettingsStore) -> [(NostrEvent) -> Bool] {
var filters = Array<(NostrEvent) -> Bool>()
if settings.hide_nsfw_tagged_content {
filters.append(nsfw_tag_filter)
}
return filters
}
}

View File

@@ -12,18 +12,10 @@ class CreateAccountModel: ObservableObject {
@Published var real_name: String = ""
@Published var nick_name: String = ""
@Published var about: String = ""
@Published var pubkey: String = ""
@Published var privkey: String = ""
@Published var profile_image: String? = nil
var pubkey_bech32: String {
return bech32_pubkey(self.pubkey) ?? ""
}
var privkey_bech32: String {
return bech32_privkey(self.privkey) ?? ""
}
@Published var pubkey: Pubkey = .empty
@Published var privkey: Privkey = .empty
@Published var profile_image: URL? = nil
var rendered_name: String {
if real_name.isEmpty {
return nick_name
@@ -35,17 +27,11 @@ class CreateAccountModel: ObservableObject {
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
}
init() {
init(real: String = "", nick: String = "", about: String = "") {
let keypair = generate_new_keypair()
self.pubkey = keypair.pubkey
self.privkey = keypair.privkey!
}
init(real: String, nick: String, about: String) {
let keypair = generate_new_keypair()
self.pubkey = keypair.pubkey
self.privkey = keypair.privkey!
self.privkey = keypair.privkey
self.real_name = real
self.nick_name = nick
self.about = about

View File

@@ -21,7 +21,7 @@ struct DamusState {
let lnurls: LNUrls
let settings: UserSettingsStore
let relay_filters: RelayFilters
let relay_metadata: RelayMetadatas
let relay_model_cache: RelayModelCache
let drafts: Drafts
let events: EventCache
let bookmarks: BookmarksManager
@@ -31,7 +31,10 @@ struct DamusState {
let muted_threads: MutedThreadsManager
let wallet: WalletModel
let nav: NavigationCoordinator
let music: MusicController?
let video: VideoController
let ndb: Ndb
@discardableResult
func add_zap(zap: Zapping) -> Bool {
// store generic zap mapping
@@ -41,15 +44,15 @@ struct DamusState {
// thread zaps
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
// [nozaps]: thread zaps are only available outside of the app store
replies.count_replies(ev)
events.add_replies(ev: ev)
replies.count_replies(ev, keypair: self.keypair)
events.add_replies(ev: ev, keypair: self.keypair)
}
// associate with events as well
return stored
}
var pubkey: String {
var pubkey: Pubkey {
return keypair.pubkey
}
@@ -58,5 +61,36 @@ struct DamusState {
}
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore()), nav: NavigationCoordinator()) }
let empty_pub: Pubkey = .empty
let empty_sec: Privkey = .empty
let kp = Keypair(pubkey: empty_pub, privkey: nil)
return DamusState.init(
pool: RelayPool(ndb: .empty),
keypair: Keypair(pubkey: empty_pub, privkey: empty_sec),
likes: EventCounter(our_pubkey: empty_pub),
boosts: EventCounter(our_pubkey: empty_pub),
contacts: Contacts(our_pubkey: empty_pub),
profiles: Profiles(ndb: .empty),
dms: DirectMessagesModel(our_pubkey: empty_pub),
previews: PreviewCache(),
zaps: Zaps(our_pubkey: empty_pub),
lnurls: LNUrls(),
settings: UserSettingsStore(),
relay_filters: RelayFilters(our_pubkey: empty_pub),
relay_model_cache: RelayModelCache(),
drafts: Drafts(),
events: EventCache(ndb: .empty),
bookmarks: BookmarksManager(pubkey: empty_pub),
postbox: PostBox(pool: RelayPool(ndb: .empty)),
bootstrap_relays: [],
replies: ReplyCounter(our_pubkey: empty_pub),
muted_threads: MutedThreadsManager(keypair: kp),
wallet: WalletModel(settings: UserSettingsStore()),
nav: NavigationCoordinator(),
music: nil,
video: VideoController(),
ndb: .empty
)
}
}

View File

@@ -14,13 +14,13 @@ class DirectMessageModel: ObservableObject {
}
}
@Published var draft: String
let pubkey: String
var is_request: Bool
var our_pubkey: String
@Published var draft: String = ""
let pubkey: Pubkey
var is_request = false
var our_pubkey: Pubkey
func determine_is_request() -> Bool {
for event in events {
if event.pubkey == our_pubkey {
@@ -31,19 +31,9 @@ class DirectMessageModel: ObservableObject {
return true
}
init(events: [NostrEvent], our_pubkey: String, pubkey: String) {
init(events: [NostrEvent] = [], our_pubkey: Pubkey, pubkey: Pubkey) {
self.events = events
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
self.pubkey = pubkey
}
init(our_pubkey: String, pubkey: String) {
self.events = []
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
self.pubkey = pubkey
}
}

View File

@@ -11,10 +11,10 @@ class DirectMessagesModel: ObservableObject {
@Published var dms: [DirectMessageModel] = []
@Published var loading: Bool = false
@Published var open_dm: Bool = false
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: "", pubkey: "")
let our_pubkey: String
init(our_pubkey: String) {
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: .empty, pubkey: .empty)
let our_pubkey: Pubkey
init(our_pubkey: Pubkey) {
self.our_pubkey = our_pubkey
}
@@ -30,14 +30,14 @@ class DirectMessagesModel: ObservableObject {
self.active_model = model
}
func set_active_dm(_ pubkey: String) {
func set_active_dm(_ pubkey: Pubkey) {
for model in self.dms where model.pubkey == pubkey {
self.set_active_dm_model(model)
break
}
}
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
func lookup_or_create(_ pubkey: Pubkey) -> DirectMessageModel {
if let dm = lookup(pubkey) {
return dm
}
@@ -47,7 +47,7 @@ class DirectMessagesModel: ObservableObject {
return new
}
func lookup(_ pubkey: String) -> DirectMessageModel? {
func lookup(_ pubkey: Pubkey) -> DirectMessageModel? {
for dm in dms {
if pubkey == dm.pubkey {
return dm

View File

@@ -11,12 +11,7 @@ class DraftArtifacts {
var content: NSMutableAttributedString
var media: [UploadedMedia]
init() {
self.content = NSMutableAttributedString(string: "")
self.media = []
}
init(content: NSMutableAttributedString, media: [UploadedMedia]) {
init(content: NSMutableAttributedString = NSMutableAttributedString(string: ""), media: [UploadedMedia] = []) {
self.content = content
self.media = media
}

View File

@@ -7,20 +7,18 @@
import Foundation
enum EventRef {
case mention(Mention)
case thread_id(ReferencedId)
case reply(ReferencedId)
case reply_to_root(ReferencedId)
var is_mention: Mention? {
if case .mention(let m) = self {
return m
}
enum EventRef: Equatable {
case mention(Mention<NoteRef>)
case thread_id(NoteRef)
case reply(NoteRef)
case reply_to_root(NoteRef)
var is_mention: NoteRef? {
if case .mention(let m) = self { return m.ref }
return nil
}
var is_direct_reply: ReferencedId? {
var is_direct_reply: NoteRef? {
switch self {
case .mention:
return nil
@@ -33,7 +31,7 @@ enum EventRef {
}
}
var is_thread_id: ReferencedId? {
var is_thread_id: NoteRef? {
switch self {
case .mention:
return nil
@@ -46,7 +44,7 @@ enum EventRef {
}
}
var is_reply: ReferencedId? {
var is_reply: NoteRef? {
switch self {
case .mention:
return nil
@@ -64,10 +62,8 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
return blocks.reduce(into: []) { acc, block in
switch block {
case .mention(let m):
if m.type == type {
if let idx = m.index {
acc.insert(idx)
}
if m.ref.key == type, let idx = m.index {
acc.insert(idx)
}
case .relay:
return
@@ -83,7 +79,7 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
}
}
func interp_event_refs_without_mentions(_ refs: [ReferencedId]) -> [EventRef] {
func interp_event_refs_without_mentions(_ refs: [NoteRef]) -> [EventRef] {
if refs.count == 0 {
return []
}
@@ -105,16 +101,15 @@ func interp_event_refs_without_mentions(_ refs: [ReferencedId]) -> [EventRef] {
return evrefs
}
func interp_event_refs_with_mentions(tags: [[String]], mention_indices: Set<Int>) -> [EventRef] {
func interp_event_refs_with_mentions(tags: Tags, mention_indices: Set<Int>) -> [EventRef] {
var mentions: [EventRef] = []
var ev_refs: [ReferencedId] = []
var ev_refs: [NoteRef] = []
var i: Int = 0
for tag in tags {
if tag.count >= 2 && tag[0] == "e" {
let ref = tag_to_refid(tag)!
if let ref = NoteRef.from_tag(tag: tag) {
if mention_indices.contains(i) {
let mention = Mention(index: i, type: .event, ref: ref)
let mention = Mention<NoteRef>(index: i, ref: ref)
mentions.append(.mention(mention))
} else {
ev_refs.append(ref)
@@ -128,26 +123,25 @@ func interp_event_refs_with_mentions(tags: [[String]], mention_indices: Set<Int>
return replies
}
func interpret_event_refs(blocks: [Block], tags: [[String]]) -> [EventRef] {
func interpret_event_refs(blocks: [Block], tags: Tags) -> [EventRef] {
if tags.count == 0 {
return []
}
/// build a set of indices for each event mention
let mention_indices = build_mention_indices(blocks, type: .event)
let mention_indices = build_mention_indices(blocks, type: .e)
/// simpler case with no mentions
if mention_indices.count == 0 {
let ev_refs = get_referenced_ids(tags: tags, key: "e")
return interp_event_refs_without_mentions(ev_refs)
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
}
return interp_event_refs_with_mentions(tags: tags, mention_indices: mention_indices)
}
func event_is_reply(_ ev: NostrEvent, privkey: String?) -> Bool {
return ev.event_refs(privkey).contains { evref in
func event_is_reply(_ refs: [EventRef]) -> Bool {
return refs.contains { evref in
return evref.is_reply != nil
}
}

View File

@@ -10,14 +10,14 @@ import Foundation
class EventsModel: ObservableObject {
let state: DamusState
let target: String
let target: NoteId
let kind: NostrKind
let sub_id = UUID().uuidString
let profiles_id = UUID().uuidString
@Published var events: [NostrEvent] = []
init(state: DamusState, target: String, kind: NostrKind) {
init(state: DamusState, target: NoteId, kind: NostrKind) {
self.state = state
self.target = target
self.kind = kind
@@ -41,14 +41,11 @@ class EventsModel: ObservableObject {
}
private func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == kind.rawValue else {
guard ev.kind == kind.rawValue,
ev.referenced_ids.last == target else {
return
}
guard last_etag(tags: ev.tags) == target else {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
@@ -62,11 +59,11 @@ class EventsModel: ObservableObject {
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
case .notice:
break
case .ok:
break
case .eose(_):
case .eose:
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
}
}

View File

@@ -7,17 +7,18 @@
import Foundation
enum FollowTarget {
case pubkey(String)
case pubkey(Pubkey)
case contact(NostrEvent)
var pubkey: String {
var follow_ref: FollowRef {
FollowRef.pubkey(pubkey)
}
var pubkey: Pubkey {
switch self {
case .pubkey(let pk):
return pk
case .contact(let ev):
return ev.pubkey
case .pubkey(let pk): return pk
case .contact(let ev): return ev.pubkey
}
}
}

View File

@@ -9,11 +9,11 @@ import Foundation
class FollowersModel: ObservableObject {
let damus_state: DamusState
let target: String
@Published var contacts: [String]? = nil
var has_contact: Set<String> = Set()
let target: Pubkey
@Published var contacts: [Pubkey]? = nil
var has_contact: Set<Pubkey> = Set()
let sub_id: String = UUID().description
let profiles_id: String = UUID().description
@@ -24,20 +24,19 @@ class FollowersModel: ObservableObject {
return contacts.count
}
init(damus_state: DamusState, target: String) {
init(damus_state: DamusState, target: Pubkey) {
self.damus_state = damus_state
self.target = target
}
func get_filter() -> NostrFilter {
NostrFilter(kinds: [.contacts],
pubkeys: [target])
NostrFilter(kinds: [.contacts], pubkeys: [target])
}
func subscribe() {
let filter = get_filter()
let filters = [filter]
print_filters(relay_id: "following", filters: [filters])
//print_filters(relay_id: "following", filters: [filters])
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
}
@@ -78,10 +77,7 @@ class FollowersModel: ObservableObject {
if ev.known_kind == .contacts {
handle_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
print("followingmodel notice: \(msg)")

View File

@@ -11,18 +11,20 @@ class FollowingModel {
let damus_state: DamusState
var needs_sub: Bool = true
let contacts: [String]
let contacts: [Pubkey]
let hashtags: [Hashtag]
let sub_id: String = UUID().description
init(damus_state: DamusState, contacts: [String]) {
init(damus_state: DamusState, contacts: [Pubkey], hashtags: [Hashtag]) {
self.damus_state = damus_state
self.contacts = contacts
self.hashtags = hashtags
}
func get_filter() -> NostrFilter {
var f = NostrFilter(kinds: [.metadata])
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
f.authors = self.contacts.reduce(into: Array<Pubkey>()) { acc, pk in
// don't fetch profiles we already have
if damus_state.profiles.has_fresh_profile(id: pk) {
return
@@ -39,7 +41,7 @@ class FollowingModel {
return
}
let filters = [filter]
print_filters(relay_id: "following", filters: [filters])
//print_filters(relay_id: "following", filters: [filters])
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
}
@@ -52,22 +54,6 @@ class FollowingModel {
}
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
switch ev {
case .ws_event:
break
case .nostr_event(let nev):
switch nev {
case .ok:
break
case .event(_, let ev):
if ev.kind == 0 {
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
print("followingmodel notice: \(msg)")
case .eose:
break
}
}
// don't need to do anything here really
}
}

View File

@@ -23,19 +23,53 @@ struct NewEventsBits: OptionSet {
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
}
enum Resubscribe {
case following
case unfollowing(FollowRef)
}
enum HomeResubFilter {
case pubkey(Pubkey)
case hashtag(String)
init?(from: FollowRef) {
switch from {
case .hashtag(let ht): self = .hashtag(ht.string())
case .pubkey(let pk): self = .pubkey(pk)
}
return nil
}
func filter(contacts: Contacts, ev: NostrEvent) -> Bool {
switch self {
case .pubkey(let pk):
return ev.pubkey == pk
case .hashtag(let ht):
if contacts.is_friend(ev.pubkey) {
return false
}
return ev.referenced_hashtags.contains(where: { ref_ht in
ht == ref_ht.hashtag
})
}
}
}
class HomeModel {
// Don't trigger a user notification for events older than a certain age
static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60
var damus_state: DamusState
var has_event: [String: Set<String>] = [:]
var deleted_events: Set<String> = Set()
var channels: [String: NostrEvent] = [:]
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
// NDBTODO: let's get rid of this entirely, let nostrdb handle it
var has_event: [String: Set<NoteId>] = [:]
var deleted_events: Set<NoteId> = Set()
var last_event_of_kind: [String: [UInt32: NostrEvent]] = [:]
var done_init: Bool = false
var incoming_dms: [NostrEvent] = []
let dm_debouncer = Debouncer(interval: 0.5)
let resub_debouncer = Debouncer(interval: 3.0)
var should_debounce_dms = true
let home_subid = UUID().description
@@ -74,7 +108,7 @@ class HomeModel {
return damus_state.dms
}
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
func has_sub_id_event(sub_id: String, ev_id: NoteId) -> Bool {
if !has_event.keys.contains(sub_id) {
has_event[sub_id] = Set()
return false
@@ -90,6 +124,32 @@ class HomeModel {
}
}
func resubscribe(_ resubbing: Resubscribe) {
if self.should_debounce_dms {
// don't resub on initial load
return
}
print("hit resub debouncer")
resub_debouncer.debounce {
print("resub")
self.unsubscribe_to_home_filters()
switch resubbing {
case .following:
break
case .unfollowing(let r):
if let filter = HomeResubFilter(from: r) {
self.events.filter { ev in !filter.filter(contacts: self.damus_state.contacts, ev: ev) }
}
}
self.subscribe_to_home_filters()
}
}
@MainActor
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
return
@@ -105,13 +165,13 @@ class HomeModel {
}
switch kind {
case .chat: fallthrough
case .text:
case .chat, .longform, .text:
handle_text_event(sub_id: sub_id, ev)
case .contacts:
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
case .metadata:
handle_metadata_event(ev)
// profile metadata processing is handled by nostrdb
break
case .list:
handle_list_event(ev)
case .boost:
@@ -122,10 +182,6 @@ class HomeModel {
handle_dm(ev)
case .delete:
handle_delete_event(ev)
case .channel_create:
handle_channel_create(ev)
case .channel_meta:
break
case .zap:
handle_zap_event(ev)
case .zap_request:
@@ -134,9 +190,40 @@ class HomeModel {
break
case .nwc_response:
handle_nwc_response(ev, relay: relay_id)
case .http_auth:
break
case .status:
handle_status_event(ev)
}
}
@MainActor
func handle_status_event(_ ev: NostrEvent) {
guard let st = UserStatus(ev: ev) else {
return
}
// don't process expired events
if let expires = st.expires_at, Date.now >= expires {
return
}
let pdata = damus_state.profiles.profile_data(ev.pubkey)
// don't use old events
if st.type == .music,
let music = pdata.status.music,
ev.created_at < music.created_at {
return
} else if st.type == .general,
let general = pdata.status.general,
ev.created_at < general.created_at {
return
}
pdata.status.update_status(st)
}
func handle_nwc_response(_ ev: NostrEvent, relay: String) {
Task { @MainActor in
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
@@ -154,22 +241,23 @@ class HomeModel {
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
}
guard let err = resp.response.error else {
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
nwc_success(state: self.damus_state, resp: resp)
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)
return
}
print("nwc error: \(resp.response)")
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
nwc_success(state: self.damus_state, resp: resp)
}
}
@MainActor
func handle_zap_event(_ ev: NostrEvent) {
process_zap_event(damus_state: damus_state, ev: ev) { zapres in
guard case .done(let zap) = zapres else { return }
guard zap.target.pubkey == self.damus_state.keypair.pubkey else {
guard case .done(let zap) = zapres,
zap.target.pubkey == self.damus_state.keypair.pubkey,
should_show_event(keypair: self.damus_state.keypair, hellthreads: self.damus_state.muted_threads, contacts: self.damus_state.contacts, ev: zap.request.ev) else {
return
}
@@ -201,10 +289,6 @@ class HomeModel {
}
func handle_channel_create(_ ev: NostrEvent) {
self.channels[ev.id] = ev
}
func filter_events() {
events.filter { ev in
!damus_state.contacts.is_muted(ev.pubkey)
@@ -219,7 +303,7 @@ class HomeModel {
return false
}
return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey)
return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, keypair: damus_state.keypair)
}
}
@@ -240,13 +324,12 @@ class HomeModel {
}
func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
var boost_ev_id = ev.last_refid()?.ref_id
var boost_ev_id = ev.last_refid()
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
boost_ev_id = inner_ev.id
Task.init {
Task {
guard validate_event(ev: inner_ev) == .ok else {
return
}
@@ -257,7 +340,6 @@ class HomeModel {
}
}
}
}
guard let e = boost_ev_id else {
@@ -269,8 +351,8 @@ class HomeModel {
break
case .success(let n):
let boosted = Counted(event: ev, id: e, total: n)
notify(.boosted, boosted)
notify(.update_stats, e)
notify(.reposted(boosted))
notify(.update_stats(note_id: e))
}
}
@@ -284,18 +366,18 @@ class HomeModel {
return
}
switch damus_state.likes.add_event(ev, target: e.ref_id) {
switch damus_state.likes.add_event(ev, target: e) {
case .already_counted:
break
case .success(let n):
handle_notification(ev: ev)
let liked = Counted(event: ev, id: e.ref_id, total: n)
notify(.liked, liked)
notify(.update_stats, e.ref_id)
let liked = Counted(event: ev, id: e, total: n)
notify(.liked(liked))
notify(.update_stats(note_id: e))
}
}
@MainActor
func handle_event(relay_id: String, conn_event: NostrConnectionEvent) {
switch conn_event {
case .ws_event(let ev):
@@ -381,8 +463,7 @@ class HomeModel {
// TODO: since times should be based on events from a specific relay
// perhaps we could mark this in the relay pool somehow
var friends = damus_state.contacts.get_friend_list()
friends.append(damus_state.pubkey)
let friends = get_friends()
var contacts_filter = NostrFilter(kinds: [.metadata])
contacts_filter.authors = friends
@@ -404,19 +485,6 @@ class HomeModel {
dms_filter.pubkeys = [ damus_state.pubkey ]
our_dms_filter.authors = [ damus_state.pubkey ]
// TODO: separate likes?
var home_filter_kinds: [NostrKind] = [
.text,
.boost
]
if !damus_state.settings.onlyzaps_mode {
home_filter_kinds.append(.like)
}
var home_filter = NostrFilter(kinds: home_filter_kinds)
// include our pubkey as well even if we're not technically a friend
home_filter.authors = friends
home_filter.limit = 500
var notifications_filter_kinds: [NostrKind] = [
.text,
.boost,
@@ -429,33 +497,76 @@ class HomeModel {
notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 500
var home_filters = [home_filter]
var notifications_filters = [notifications_filter]
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
var dms_filters = [dms_filter, our_dms_filter]
let last_of_kind = get_last_of_kind(relay_id: relay_id)
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
home_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: home_filters)
contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
//print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
if let relay_id {
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id])
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id])
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: [relay_id])
} else {
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)))
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)))
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)))
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
}
subscribe_to_home_filters(relay_id: relay_id)
let relay_ids = relay_id.map { [$0] }
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: relay_ids)
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: relay_ids)
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: relay_ids)
}
func get_last_of_kind(relay_id: String?) -> [UInt32: NostrEvent] {
return relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
}
func unsubscribe_to_home_filters() {
pool.send(.unsubscribe(home_subid))
}
func get_friends() -> [Pubkey] {
var friends = damus_state.contacts.get_friend_list()
friends.insert(damus_state.pubkey)
return Array(friends)
}
func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: String? = nil) {
// TODO: separate likes?
var home_filter_kinds: [NostrKind] = [
.text, .longform, .boost
]
if !damus_state.settings.onlyzaps_mode {
home_filter_kinds.append(.like)
}
// only pull status data if we care for it
if damus_state.settings.show_music_statuses || damus_state.settings.show_general_statuses {
home_filter_kinds.append(.status)
}
let friends = fs ?? get_friends()
var home_filter = NostrFilter(kinds: home_filter_kinds)
// include our pubkey as well even if we're not technically a friend
home_filter.authors = friends
home_filter.limit = 500
var home_filters = [home_filter]
let followed_hashtags = Array(damus_state.contacts.get_followed_hashtags())
if followed_hashtags.count != 0 {
var hashtag_filter = NostrFilter.filter_hashtag(followed_hashtags)
hashtag_filter.limit = 100
home_filters.append(hashtag_filter)
}
let relay_ids = relay_id.map { [$0] }
home_filters = update_filters_with_since(last_of_kind: get_last_of_kind(relay_id: relay_id), filters: home_filters)
let sub = NostrSubscribe(filters: home_filters, sub_id: home_subid)
pool.send(.subscribe(sub), to: relay_ids)
}
func handle_list_event(_ ev: NostrEvent) {
// we only care about our lists
guard ev.pubkey == damus_state.pubkey else {
@@ -468,22 +579,14 @@ class HomeModel {
}
}
guard let name = get_referenced_ids(tags: ev.tags, key: "d").first else {
guard ev.referenced_params.contains(where: { p in p.param.matches_str("mute") }) else {
return
}
guard name.ref_id == "mute" else {
return
}
damus_state.contacts.set_mutelist(ev)
}
func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
func get_last_event_of_kind(relay_id: String, kind: UInt32) -> NostrEvent? {
guard let m = last_event_of_kind[relay_id] else {
last_event_of_kind[relay_id] = [:]
return nil
@@ -494,15 +597,9 @@ class HomeModel {
func handle_notification(ev: NostrEvent) {
// don't show notifications from ourselves
guard ev.pubkey != damus_state.pubkey else {
return
}
guard event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey) else {
return
}
guard should_show_event(contacts: damus_state.contacts, ev: ev) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) else {
guard ev.pubkey != damus_state.pubkey,
event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey),
should_show_event(keypair: self.damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
return
}
@@ -540,13 +637,13 @@ class HomeModel {
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
return
}
// TODO: will we need to process this in other places like zap request contents, etc?
process_image_metadatas(cache: damus_state.events, ev: ev)
damus_state.replies.count_replies(ev)
damus_state.replies.count_replies(ev, keypair: self.damus_state.keypair)
damus_state.events.insert(ev)
if sub_id == home_subid {
@@ -560,14 +657,14 @@ class HomeModel {
notification_status.new_events = notifs
if damus_state.settings.dm_notification && ev.age < HomeModel.event_max_age_for_notification {
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
let convo = ev.decrypted(keypair: self.damus_state.keypair) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
let notify = LocalNotification(type: .dm, event: ev, target: ev, content: convo)
create_local_notification(profiles: damus_state.profiles, notify: notify)
}
}
func handle_dm(_ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else {
return
}
@@ -612,33 +709,29 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
contacts.add_friend_contact(ev)
}
func load_our_contacts(contacts: Contacts, m_old_ev: NostrEvent?, ev: NostrEvent) {
var new_pks = Set<String>()
// our contacts
for tag in ev.tags {
if tag.count >= 2 && tag[0] == "p" {
new_pks.insert(tag[1])
}
}
var old_pks = Set<String>()
// find removed contacts
if let old_ev = m_old_ev {
for tag in old_ev.tags {
if tag.count >= 2 && tag[0] == "p" {
old_pks.insert(tag[1])
func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let contacts = state.contacts
let new_refs = Set<FollowRef>(ev.referenced_follows)
let old_refs = m_old_ev.map({ old_ev in Set(old_ev.referenced_follows) }) ?? Set()
let diff = new_refs.symmetricDifference(old_refs)
for ref in diff {
if new_refs.contains(ref) {
notify(.followed(ref))
switch ref {
case .pubkey(let pk):
contacts.add_friend_pubkey(pk)
case .hashtag:
// I guess I could cache followed hashtags here... whatever
break
}
}
}
let diff = new_pks.symmetricDifference(old_pks)
for pk in diff {
if new_pks.contains(pk) {
notify(.followed, pk)
contacts.add_friend_pubkey(pk)
} else {
notify(.unfollowed, pk)
contacts.remove_friend(pk)
notify(.unfollowed(ref))
switch ref {
case .pubkey(let pk):
contacts.remove_friend(pk)
case .hashtag: break
}
}
}
}
@@ -668,6 +761,7 @@ func abbrev_ids_field(_ n: String, _ ids: [String]?) -> String {
return "\(n): \(abbrev_ids(ids))"
}
/*
func print_filter(_ f: NostrFilter) {
let fmt = [
abbrev_ids_field("ids", f.ids),
@@ -693,60 +787,9 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
}
print("-----")
}
*/
func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: Profile, ev: NostrEvent) {
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
notify(.deleted_account, ())
return
}
var old_nip05: String? = nil
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
old_nip05 = mprof.profile.nip05
if mprof.event.created_at > ev.created_at {
// skip if we already have an newer profile
return
}
}
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at, event: ev)
profiles.add(id: ev.pubkey, profile: tprof)
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
Task.detached(priority: .background) {
let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05)
if validated != nil {
print("validated nip05 for '\(nip05)'")
}
Task { @MainActor in
profiles.validated[ev.pubkey] = validated
profiles.nip05_pubkey[nip05] = ev.pubkey
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
}
}
// load pfps asap
var changed = false
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if URL(string: picture) != nil {
changed = true
}
let banner = tprof.profile.banner ?? ""
if URL(string: banner) != nil {
changed = true
}
if changed {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
}
// TODO: remove this, let nostrdb handle all validation
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
let validated = events.is_event_valid(ev.id)
@@ -767,32 +810,13 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
case .ok:
callback()
case .bad_id: fallthrough
case .bad_sig:
case .bad_id, .bad_sig:
break
}
}
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: ((Profile?) -> Void)? = nil) {
guard_valid_event(events: events, ev: ev) {
DispatchQueue.global(qos: .background).async {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
completion?(nil)
return
}
profile.cache_lnurl()
DispatchQueue.main.async {
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
completion?(profile)
}
}
}
}
func robohash(_ pk: String) -> String {
return "https://robohash.org/" + pk
func robohash(_ pk: Pubkey) -> String {
return "https://robohash.org/" + pk.hex()
}
func load_our_stuff(state: DamusState, ev: NostrEvent) {
@@ -810,7 +834,7 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) {
let m_old_ev = state.contacts.event
state.contacts.event = ev
load_our_contacts(contacts: state.contacts, m_old_ev: m_old_ev, ev: ev)
load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
}
@@ -849,7 +873,7 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
if new.contains(d) {
if let url = RelayURL(d) {
let descriptor = RelayDescriptor(url: url, info: decoded[d] ?? .rw)
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters)
add_new_relay(model_cache: state.relay_model_cache, relay_filters: state.relay_filters, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: state.settings.developer_mode)
}
} else {
state.pool.remove_relay(d)
@@ -859,16 +883,16 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
if changed {
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
state.pool.connect()
notify(.relays_changed, ())
notify(.relays_changed)
}
}
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool) {
func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) {
try? pool.add_relay(descriptor)
let url = descriptor.url
let relay_id = url.id
guard metadatas.lookup(relay_id: relay_id) == nil else {
guard model_cache.model(withURL: url) == nil else {
return
}
@@ -877,8 +901,13 @@ func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool:
return
}
DispatchQueue.main.async {
metadatas.insert(relay_id: relay_id, metadata: meta)
await MainActor.run {
let model = RelayModel(url, metadata: meta)
model_cache.insert(model: model)
if logging_enabled {
pool.setLog(model.log, for: relay_id)
}
// if this is the first time adding filters, we should filter non-paid relays
if new_relay_filters && !meta.is_paid {
@@ -912,7 +941,7 @@ func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
}
@discardableResult
func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
func handle_incoming_dm(ev: NostrEvent, our_pubkey: Pubkey, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
var inserted = false
var found = false
@@ -922,7 +951,7 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
var the_pk = ev.pubkey
if ours {
if let ref_pk = ev.referenced_pubkeys.first {
the_pk = ref_pk.ref_id
the_pk = ref_pk
} else {
// self dm!?
print("TODO: handle self dm?")
@@ -956,7 +985,7 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
}
@discardableResult
func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, evs: [NostrEvent]) -> NewEventsBits? {
func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: Pubkey, evs: [NostrEvent]) -> NewEventsBits? {
var inserted = false
var new_events: NewEventsBits? = nil
@@ -1036,21 +1065,20 @@ func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Tim
/// Sometimes we get garbage in our notifications. Ensure we have our pubkey on this event
func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
for tag in ev.tags {
if tag.count >= 2 && tag[0] == "p" && tag[1] == our_pubkey {
return true
}
}
return false
func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool {
return ev.referenced_pubkeys.contains(our_pubkey)
}
func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
func should_show_event(keypair: Keypair, hellthreads: MutedThreadsManager, contacts: Contacts, ev: NostrEvent) -> Bool {
if contacts.is_muted(ev.pubkey) {
return false
}
if hellthreads.isMutedThread(ev, keypair: keypair) {
return false
}
return ev.should_show_event
}
@@ -1078,10 +1106,13 @@ func zap_notification_title(_ zap: Zap) -> String {
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
let src = zap.request.ev
let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey
let profile = profiles.lookup(id: pk)
let name = profiles.lookup(id: pk).map { profile in
Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50)
}.value
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
let formattedSats = format_msats_abbrev(zap.invoice.amount)
let name = Profile.displayName(profile: profile, pubkey: pk).display_name.truncate(maxLength: 50)
if src.content.isEmpty {
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
@@ -1092,13 +1123,13 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
}
}
func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: String) {
func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: Pubkey) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .profile_zap, event_id: profile_id).to_user_info()
content.userInfo = LossyLocalNotification(type: .profile_zap, mention: .pubkey(profile_id)).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
@@ -1113,13 +1144,13 @@ func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale
}
}
func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: NoteId) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .zap, event_id: evId).to_user_info()
content.userInfo = LossyLocalNotification(type: .zap, mention: .note(evId)).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
@@ -1134,6 +1165,27 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale:
}
}
func render_notification_content_preview(cache: EventCache, ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> String {
let prefix_len = 300
let artifacts = cache.get_cache_data(ev.id).artifacts.artifacts ?? render_note_content(ev: ev, profiles: profiles, keypair: keypair)
// special case for longform events
if ev.known_kind == .longform {
let longform = LongformEvent(event: ev)
return longform.title ?? longform.summary ?? "Longform Event"
}
switch artifacts {
case .longform:
// we should never hit this until we have more note types built out of parts
// since we handle this case above in known_kind == .longform
return String(ev.content.prefix(prefix_len))
case .separated(let artifacts):
return String(NSAttributedString(artifacts.content.attributed).string.prefix(prefix_len))
}
}
func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
guard let type = ev.known_kind else {
@@ -1147,7 +1199,7 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
}
// Don't show notifications from muted threads.
if damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) {
if damus_state.muted_threads.isMutedThread(ev, keypair: damus_state.keypair) {
return
}
@@ -1156,24 +1208,30 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
return
}
if type == .text && damus_state.settings.mention_notification {
let blocks = ev.blocks(damus_state.keypair.privkey)
for case .mention(let mention) in blocks where mention.ref.ref_id == damus_state.keypair.pubkey {
let content = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content)
if type == .text, damus_state.settings.mention_notification {
let blocks = ev.blocks(damus_state.keypair).blocks
for case .mention(let mention) in blocks {
guard case .pubkey(let pk) = mention.ref, pk == damus_state.keypair.pubkey else {
continue
}
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: ev, profiles: damus_state.profiles, keypair: damus_state.keypair)
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content_preview)
create_local_notification(profiles: damus_state.profiles, notify: notify )
}
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.get_inner_event(cache: damus_state.events) {
let content = NSAttributedString(render_note_content(ev: inner_ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content)
} else if type == .boost,
damus_state.settings.repost_notification,
let inner_ev = ev.get_inner_event(cache: damus_state.events)
{
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: inner_ev, profiles: damus_state.profiles, keypair: damus_state.keypair)
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview)
create_local_notification(profiles: damus_state.profiles, notify: notify)
} else if type == .like && damus_state.settings.like_notification,
let evid = ev.referenced_ids.last?.ref_id,
} else if type == .like,
damus_state.settings.like_notification,
let evid = ev.referenced_ids.last,
let liked_event = damus_state.events.lookup(evid)
{
let content = NSAttributedString(render_note_content(ev: liked_event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content)
let content_preview = render_notification_content_preview(cache: damus_state.events, ev: liked_event, profiles: damus_state.profiles, keypair: damus_state.keypair)
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview)
create_local_notification(profiles: damus_state.profiles, notify: notify)
}
@@ -1228,13 +1286,66 @@ enum ProcessZapResult {
case failed
}
extension Sequence {
func just_one() -> Element? {
var got_one = false
var the_x: Element? = nil
for x in self {
guard !got_one else {
return nil
}
the_x = x
got_one = true
}
return the_x
}
}
// securely get the zap target's pubkey. this can be faked so we need to be
// careful
func get_zap_target_pubkey(ev: NostrEvent, events: EventCache) -> Pubkey? {
let etags = Array(ev.referenced_ids)
guard let etag = etags.first else {
// no etags, ptag-only case
guard let a = ev.referenced_pubkeys.just_one() else {
return nil
}
// TODO: just return data here
return a
}
// we have an e-tag
// ensure that there is only 1 etag to stop fake note zap attacks
guard etags.count == 1 else {
return nil
}
// we can't trust the p tag on note zaps because they can be faked
guard let pk = events.lookup(etag)?.pubkey else {
// We don't have the event in cache so we can't check the pubkey.
// We could return this as an invalid zap but that wouldn't be correct
// all of the time, and may reject valid zaps. What we need is a new
// unvalidated zap state, but for now we simply leak a bit of correctness...
return ev.referenced_pubkeys.just_one()
}
return pk
}
@MainActor
func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @escaping (ProcessZapResult) -> Void) {
// These are zap notifications
guard let ptag = event_tag(ev, name: "p") else {
guard let ptag = get_zap_target_pubkey(ev: ev, events: damus_state.events) else {
completion(.failed)
return
}
// just return the zap if we already have it
if let zap = damus_state.zaps.zaps[ev.id], case .zap(let z) = zap {
completion(.already_processed(z))
@@ -1251,24 +1362,20 @@ func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @esc
return
}
guard let profile = damus_state.profiles.lookup(id: ptag) else {
guard let lnurl = damus_state.profiles.lookup_with_timestamp(ptag)
.map({ pr in pr?.lnurl }).value else {
completion(.failed)
return
}
guard let lnurl = profile.lnurl else {
completion(.failed)
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
Task { [lnurl] in
guard let zapper = await fetch_zapper_from_lnurl(lnurls: damus_state.lnurls, pubkey: ptag, lnurl: lnurl) else {
completion(.failed)
return
}
DispatchQueue.main.async {
damus_state.profiles.zappers[ptag] = zapper
damus_state.profiles.profile_data(ptag).zapper = zapper
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: zapper) else {
completion(.failed)
return
@@ -1281,7 +1388,7 @@ func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @esc
}
fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: String) -> Zap? {
fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: Pubkey) -> Zap? {
let our_keypair = damus_state.keypair
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {

View File

@@ -47,8 +47,8 @@ enum MediaUpload {
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
@Published var progress: Double? = nil
func start(media: MediaUpload, uploader: MediaUploader) async -> ImageUploadResult {
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self)
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)
DispatchQueue.main.async {
self.progress = nil
}

View File

@@ -13,16 +13,16 @@ enum CountResult {
}
class EventCounter {
var counts: [String: Int] = [:]
var user_events: [String: Set<String>] = [:]
var our_events: [String: NostrEvent] = [:]
var our_pubkey: String
init (our_pubkey: String) {
var counts: [NoteId: Int] = [:]
var user_events: [Pubkey: Set<NoteId>] = [:]
var our_events: [NoteId: NostrEvent] = [:]
var our_pubkey: Pubkey
init(our_pubkey: Pubkey) {
self.our_pubkey = our_pubkey
}
func add_event(_ ev: NostrEvent, target: String) -> CountResult {
func add_event(_ ev: NostrEvent, target: NoteId) -> CountResult {
let pubkey = ev.pubkey
if self.user_events[pubkey] == nil {

View File

@@ -9,6 +9,6 @@ import Foundation
struct Counted {
let event: NostrEvent
let id: String
let id: NoteId
let total: Int
}

View File

@@ -7,24 +7,94 @@
import Foundation
enum MentionType {
case pubkey
case event
enum MentionType: AsciiCharacter, TagKey {
case p
case e
var ref: String {
var keychar: AsciiCharacter {
self.rawValue
}
}
enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
case pubkey(Pubkey) // TODO: handle nprofile
case note(NoteId)
var key: MentionType {
switch self {
case .pubkey:
return "p"
case .event:
return "e"
case .pubkey: return .p
case .note: return .e
}
}
var bech32: String {
switch self {
case .pubkey(let pubkey): return bech32_pubkey(pubkey)
case .note(let noteId): return bech32_note_id(noteId)
}
}
static func from_bech32(str: String) -> MentionRef? {
switch Bech32Object.parse(str) {
case .note(let noteid): return .note(noteid)
case .npub(let pubkey): return .pubkey(pubkey)
default: return nil
}
}
var pubkey: Pubkey? {
switch self {
case .pubkey(let pubkey): return pubkey
case .note: return nil
}
}
var tag: [String] {
switch self {
case .pubkey(let pubkey): return ["p", pubkey.hex()]
case .note(let noteId): return ["e", noteId.hex()]
}
}
static func from_tag(tag: TagSequence) -> MentionRef? {
guard tag.count >= 2 else { return nil }
var i = tag.makeIterator()
guard let t0 = i.next(),
let chr = t0.single_char,
let mention_type = MentionType(rawValue: chr),
let id = i.next()?.id()
else {
return nil
}
switch mention_type {
case .p: return .pubkey(Pubkey(id))
case .e: return .note(NoteId(id))
}
}
}
struct Mention: Equatable {
struct Mention<T: Equatable>: Equatable {
let index: Int?
let type: MentionType
let ref: ReferencedId
let ref: T
static func any(_ mention_id: MentionRef, index: Int? = nil) -> Mention<MentionRef> {
return Mention<MentionRef>(index: index, ref: mention_id)
}
static func noteref(_ id: NoteRef, index: Int? = nil) -> Mention<NoteRef> {
return Mention<NoteRef>(index: index, ref: id)
}
static func note(_ id: NoteId, index: Int? = nil) -> Mention<NoteId> {
return Mention<NoteId>(index: index, ref: id)
}
static func pubkey(_ pubkey: Pubkey, index: Int? = nil) -> Mention<Pubkey> {
return Mention<Pubkey>(index: index, ref: pubkey)
}
}
typealias Invoice = LightningInvoice<Amount>
@@ -53,170 +123,9 @@ struct LightningInvoice<T> {
}
}
enum Block: Equatable {
static func == (lhs: Block, rhs: Block) -> Bool {
switch (lhs, rhs) {
case (.text(let a), .text(let b)):
return a == b
case (.mention(let a), .mention(let b)):
return a == b
case (.hashtag(let a), .hashtag(let b)):
return a == b
case (.url(let a), .url(let b)):
return a == b
case (.invoice(let a), .invoice(let b)):
return a.string == b.string
case (_, _):
return false
}
}
case text(String)
case mention(Mention)
case hashtag(String)
case url(URL)
case invoice(Invoice)
case relay(String)
var is_invoice: Invoice? {
if case .invoice(let invoice) = self {
return invoice
}
return nil
}
var is_hashtag: String? {
if case .hashtag(let htag) = self {
return htag
}
return nil
}
var is_url: URL? {
if case .url(let url) = self {
return url
}
return nil
}
var is_text: String? {
if case .text(let txt) = self {
return txt
}
return nil
}
var is_note_mention: Bool {
guard case .mention(let mention) = self else {
return false
}
return mention.type == .event
}
var is_mention: Bool {
if case .mention = self {
return true
}
return false
}
}
func render_blocks(blocks: [Block]) -> String {
return blocks.reduce("") { str, block in
switch block {
case .mention(let m):
if let idx = m.index {
return str + "#[\(idx)]"
} else if m.type == .pubkey, let pk = bech32_pubkey(m.ref.ref_id) {
return str + "nostr:\(pk)"
} else if let note_id = bech32_note_id(m.ref.ref_id) {
return str + "nostr:\(note_id)"
} else {
return str + m.ref.ref_id
}
case .relay(let relay):
return str + relay
case .text(let txt):
return str + txt
case .hashtag(let htag):
return str + "#" + htag
case .url(let url):
return str + url.absoluteString
case .invoice(let inv):
return str + inv.string
}
}
}
func parse_mentions(content: String, tags: [[String]]) -> [Block] {
var out: [Block] = []
var bs = blocks()
bs.num_blocks = 0;
blocks_init(&bs)
let bytes = content.utf8CString
let _ = bytes.withUnsafeBufferPointer { p in
damus_parse_content(&bs, p.baseAddress)
}
var i = 0
while (i < bs.num_blocks) {
let block = bs.blocks[i]
if let converted = convert_block(block, tags: tags) {
out.append(converted)
}
i += 1
}
blocks_free(&bs)
return out
}
func strblock_to_string(_ s: str_block_t) -> String? {
let len = s.end - s.start
let bytes = Data(bytes: s.start, count: len)
return String(bytes: bytes, encoding: .utf8)
}
func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
if b.type == BLOCK_HASHTAG {
guard let str = strblock_to_string(b.block.str) else {
return nil
}
return .hashtag(str)
} else if b.type == BLOCK_TEXT {
guard let str = strblock_to_string(b.block.str) else {
return nil
}
return .text(str)
} else if b.type == BLOCK_MENTION_INDEX {
return convert_mention_index_block(ind: b.block.mention_index, tags: tags)
} else if b.type == BLOCK_URL {
return convert_url_block(b.block.str)
} else if b.type == BLOCK_INVOICE {
return convert_invoice_block(b.block.invoice)
} else if b.type == BLOCK_MENTION_BECH32 {
return convert_mention_bech32_block(b.block.mention_bech32)
}
return nil
}
func convert_url_block(_ b: str_block) -> Block? {
guard let str = strblock_to_string(b) else {
return nil
}
guard let url = URL(string: str) else {
return .text(str)
}
return .url(url)
struct Blocks: Equatable {
let words: Int
let blocks: [Block]
}
func maybe_pointee<T>(_ p: UnsafeMutablePointer<T>!) -> T? {
@@ -281,81 +190,6 @@ func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {
guard let invstr = strblock_to_string(b.invstr) else {
return nil
}
guard var b11 = maybe_pointee(b.bolt11) else {
return nil
}
guard let description = convert_invoice_description(b11: b11) else {
return nil
}
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
let payment_hash = Data(bytes: &b11.payment_hash, count: 32)
let created_at = b11.timestamp
tal_free(b.bolt11)
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
}
func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
{
switch b.bech32.type {
case NOSTR_BECH32_NOTE:
let note = b.bech32.data.note;
let event_id = hex_encode(Data(bytes: note.event_id, count: 32))
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: nil, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
case NOSTR_BECH32_NEVENT:
let nevent = b.bech32.data.nevent;
let event_id = hex_encode(Data(bytes: nevent.event_id, count: 32))
var relay_id: String? = nil
if nevent.relays.num_relays > 0 {
relay_id = strblock_to_string(nevent.relays.relays.0)
}
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: relay_id, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
case NOSTR_BECH32_NPUB:
let npub = b.bech32.data.npub
let pubkey = hex_encode(Data(bytes: npub.pubkey, count: 32))
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
case NOSTR_BECH32_NPROFILE:
let nprofile = b.bech32.data.nprofile
let pubkey = hex_encode(Data(bytes: nprofile.pubkey, count: 32))
var relay_id: String? = nil
if nprofile.relays.num_relays > 0 {
relay_id = strblock_to_string(nprofile.relays.relays.0)
}
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: relay_id, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
case NOSTR_BECH32_NRELAY:
let nrelay = b.bech32.data.nrelay
guard let relay_str = strblock_to_string(nrelay.relay) else {
return nil
}
return .relay(relay_str)
case NOSTR_BECH32_NADDR:
// TODO: wtf do I do with this
guard let naddr = strblock_to_string(b.str) else {
return nil
}
return .text("nostr:" + naddr)
default:
return nil
}
}
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
if let desc = b11.description {
return .description(String(cString: desc))
@@ -368,85 +202,6 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
return nil
}
func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block?
{
let ind = Int(ind)
if ind < 0 || (ind + 1 > tags.count) || tags[ind].count < 2 {
return .text("#[\(ind)]")
}
let tag = tags[ind]
guard let mention_type = parse_mention_type(tag[0]) else {
return .text("#[\(ind)]")
}
guard let ref = tag_to_refid(tag) else {
return .text("#[\(ind)]")
}
return .mention(Mention(index: ind, type: mention_type, ref: ref))
}
func parse_while(_ p: Parser, match: (Character) -> Bool) -> String? {
var i: Int = 0
let sub = substring(p.str, start: p.pos, end: p.str.count)
let start = p.pos
for c in sub {
if match(c) {
p.pos += 1
} else {
break
}
i += 1
}
let end = start + i
if start == end {
return nil
}
return String(substring(p.str, start: start, end: end))
}
func is_hashtag_char(_ c: Character) -> Bool {
return c.isLetter || c.isNumber
}
func prev_char(_ p: Parser, n: Int) -> Character? {
if p.pos - n < 0 {
return nil
}
let ind = p.str.index(p.str.startIndex, offsetBy: p.pos - n)
return p.str[ind]
}
func is_punctuation(_ c: Character) -> Bool {
return c.isWhitespace || c.isPunctuation
}
func parse_hashtag(_ p: Parser) -> String? {
let start = p.pos
if !parse_char(p, "#") {
return nil
}
if let prev = prev_char(p, n: 2) {
// we don't allow adjacent hashtags
if !is_punctuation(prev) {
return nil
}
}
guard let str = parse_while(p, match: is_hashtag_char) else {
p.pos = start
return nil
}
return str
}
func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
var i: Int = 0
for tag in tags {
@@ -466,68 +221,39 @@ struct PostTags {
let tags: [[String]]
}
func parse_mention_type(_ c: String) -> MentionType? {
if c == "e" {
return .event
} else if c == "p" {
return .pubkey
}
return nil
}
/// Convert
func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions: Bool) -> PostTags {
func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
var new_tags = tags
var blocks: [Block] = []
for post_block in post_blocks {
switch post_block {
case .ref(let ref):
guard let mention_type = parse_mention_type(ref.key) else {
case .mention(let mention):
if case .note = mention.ref {
continue
}
if silent_mentions || mention_type == .event {
let mention = Mention(index: nil, type: mention_type, ref: ref)
let block = Block.mention(mention)
blocks.append(block)
continue
}
if find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) != nil {
// Mention index is nil because indexed mentions from NIP-08 is deprecated.
// It has been replaced with NIP-27 text note references with nostr: prefixed URIs.
let mention = Mention(index: nil, type: mention_type, ref: ref)
let block = Block.mention(mention)
blocks.append(block)
} else {
new_tags.append(refid_to_tag(ref))
// Mention index is nil because indexed mentions from NIP-08 is deprecated.
// It has been replaced with NIP-27 text note references with nostr: prefixed URIs.
let mention = Mention(index: nil, type: mention_type, ref: ref)
let block = Block.mention(mention)
blocks.append(block)
}
new_tags.append(mention.ref.tag)
case .hashtag(let hashtag):
new_tags.append(["t", hashtag.lowercased()])
blocks.append(.hashtag(hashtag))
case .text(let txt):
blocks.append(Block.text(txt))
case .text: break
case .invoice: break
case .relay: break
case .url(let url):
new_tags.append(["r", url.absoluteString])
break
}
}
return PostTags(blocks: blocks, tags: new_tags)
return PostTags(blocks: post_blocks, tags: new_tags)
}
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
let tags = post.references.map(refid_to_tag) + post.tags
func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? {
let tags = post.references.map({ r in r.tag }) + post.tags
let post_blocks = parse_post_blocks(content: post.content)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: false)
let content = render_blocks(blocks: post_tags.blocks)
let new_ev = NostrEvent(content: content, pubkey: pubkey, kind: post.kind.rawValue, tags: post_tags.tags)
new_ev.calculate_id()
new_ev.sign(privkey: privkey)
return new_ev
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
let content = post_tags.blocks
.map(\.asString)
.joined(separator: "")
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: post.kind.rawValue, tags: post_tags.tags)
}

View File

@@ -7,20 +7,25 @@
import Foundation
fileprivate func getMutedThreadsKey(pubkey: String) -> String {
fileprivate func getMutedThreadsKey(pubkey: Pubkey) -> String {
pk_setting_key(pubkey, key: "muted_threads")
}
func loadMutedThreads(pubkey: String) -> [String] {
func loadMutedThreads(pubkey: Pubkey) -> [NoteId] {
let key = getMutedThreadsKey(pubkey: pubkey)
return UserDefaults.standard.stringArray(forKey: key) ?? []
let xs = UserDefaults.standard.stringArray(forKey: key) ?? []
return xs.reduce(into: [NoteId]()) { ids, k in
guard let note_id = hex_decode(k) else { return }
ids.append(NoteId(Data(note_id)))
}
}
func saveMutedThreads(pubkey: String, currentValue: [String], value: [String]) -> Bool {
func saveMutedThreads(pubkey: Pubkey, currentValue: [NoteId], value: [NoteId]) -> Bool {
let uniqueMutedThreads = Array(Set(value))
if uniqueMutedThreads != currentValue {
UserDefaults.standard.set(uniqueMutedThreads, forKey: getMutedThreadsKey(pubkey: pubkey))
let ids = uniqueMutedThreads.map { note_id in return note_id.hex() }
UserDefaults.standard.set(ids, forKey: getMutedThreadsKey(pubkey: pubkey))
return true
}
@@ -31,9 +36,9 @@ class MutedThreadsManager: ObservableObject {
private let keypair: Keypair
private var _mutedThreadsSet: Set<String>
private var _mutedThreads: [String]
var mutedThreads: [String] {
private var _mutedThreadsSet: Set<NoteId>
private var _mutedThreads: [NoteId]
var mutedThreads: [NoteId] {
get {
return _mutedThreads
}
@@ -51,20 +56,20 @@ class MutedThreadsManager: ObservableObject {
self.keypair = keypair
}
func isMutedThread(_ ev: NostrEvent, privkey: String?) -> Bool {
return _mutedThreadsSet.contains(ev.thread_id(privkey: privkey))
func isMutedThread(_ ev: NostrEvent, keypair: Keypair) -> Bool {
return _mutedThreadsSet.contains(ev.thread_id(keypair: keypair))
}
func updateMutedThread(_ ev: NostrEvent) {
let threadId = ev.thread_id(privkey: nil)
if isMutedThread(ev, privkey: keypair.privkey) {
let threadId = ev.thread_id(keypair: keypair)
if isMutedThread(ev, keypair: keypair) {
mutedThreads = mutedThreads.filter { $0 != threadId }
_mutedThreadsSet.remove(threadId)
notify(.unmute_thread, ev)
notify(.unmute_thread(ev))
} else {
mutedThreads.append(threadId)
_mutedThreadsSet.insert(threadId)
notify(.mute_thread, ev)
notify(.mute_thread(ev))
}
}
}

View File

@@ -10,7 +10,7 @@ import Foundation
class EventGroup {
var events: [NostrEvent]
var last_event_at: Int64 {
var last_event_at: UInt32 {
guard let first = self.events.first else {
return 0
}
@@ -18,11 +18,7 @@ class EventGroup {
return first.created_at
}
init() {
self.events = []
}
init(events: [NostrEvent]) {
init(events: [NostrEvent] = []) {
self.events = events
}

View File

@@ -8,11 +8,11 @@
import Foundation
class ZapGroup {
var zaps: [Zapping]
var msat_total: Int64
var zappers: Set<String>
var last_event_at: Int64 {
var zaps: [Zapping] = []
var msat_total: Int64 = 0
var zappers = Set<Pubkey>()
var last_event_at: UInt32 {
guard let first = zaps.first else {
return 0
}
@@ -46,12 +46,6 @@ class ZapGroup {
return grp
}
init() {
self.zaps = []
self.msat_total = 0
self.zappers = Set()
}
@discardableResult
func insert(_ zap: Zapping) -> Bool {
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {

View File

@@ -8,10 +8,10 @@
import Foundation
enum NotificationItem {
case repost(String, EventGroup)
case reaction(String, EventGroup)
case repost(NoteId, EventGroup)
case reaction(NoteId, EventGroup)
case profile_zap(ZapGroup)
case event_zap(String, ZapGroup)
case event_zap(NoteId, ZapGroup)
case reply(NostrEvent)
var is_reply: NostrEvent? {
@@ -35,23 +35,8 @@ enum NotificationItem {
return nil
}
}
var id: String {
switch self {
case .repost(let evid, _):
return "repost_" + evid
case .reaction(let evid, _):
return "reaction_" + evid
case .profile_zap:
return "profile_zap"
case .event_zap(let evid, _):
return "event_zap_" + evid
case .reply(let ev):
return "reply_" + ev.id
}
}
var last_event_at: Int64 {
var last_event_at: UInt32 {
switch self {
case .reaction(_, let evgrp):
return evgrp.last_event_at
@@ -99,42 +84,28 @@ enum NotificationItem {
}
class NotificationsModel: ObservableObject, ScrollQueue {
var incoming_zaps: [Zapping]
var incoming_events: [NostrEvent]
var should_queue: Bool
var incoming_zaps: [Zapping] = []
var incoming_events: [NostrEvent] = []
var should_queue: Bool = true
// mappings from events to
var zaps: [String: ZapGroup]
var profile_zaps: ZapGroup
var reactions: [String: EventGroup]
var reposts: [String: EventGroup]
var replies: [NostrEvent]
var has_reply: Set<String>
var has_ev: Set<String>
@Published var notifications: [NotificationItem]
init() {
self.zaps = [:]
self.reactions = [:]
self.reposts = [:]
self.replies = []
self.has_reply = Set()
self.should_queue = true
self.incoming_zaps = []
self.incoming_events = []
self.profile_zaps = ZapGroup()
self.notifications = []
self.has_ev = Set()
}
var zaps: [NoteId: ZapGroup] = [:]
var profile_zaps = ZapGroup()
var reactions: [NoteId: EventGroup] = [:]
var reposts: [NoteId: EventGroup] = [:]
var replies: [NostrEvent] = []
var has_reply = Set<NoteId>()
var has_ev = Set<NoteId>()
@Published var notifications: [NotificationItem] = []
func set_should_queue(_ val: Bool) {
self.should_queue = val
}
func uniq_pubkeys() -> [String] {
var pks = Set<String>()
func uniq_pubkeys() -> [Pubkey] {
var pks = Set<Pubkey>()
for ev in incoming_events {
pks.insert(ev.pubkey)
}
@@ -222,12 +193,10 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
private func insert_reaction(_ ev: NostrEvent) -> Bool {
guard let ref_id = ev.referenced_ids.last else {
guard let id = ev.referenced_ids.last else {
return false
}
let id = ref_id.id
if let evgrp = self.reactions[id] {
return evgrp.insert(ev)
} else {

View File

@@ -10,10 +10,10 @@ import Foundation
struct NostrPost {
let kind: NostrKind
let content: String
let references: [ReferencedId]
let references: [RefId]
let tags: [[String]]
init (content: String, references: [ReferencedId], kind: NostrKind = .text, tags: [[String]] = []) {
init(content: String, references: [RefId], kind: NostrKind = .text, tags: [[String]] = []) {
self.content = content
self.references = references
self.kind = kind
@@ -21,120 +21,9 @@ struct NostrPost {
}
}
func parse_post_mention_type(_ p: Parser) -> MentionType? {
if parse_char(p, "@") {
return .pubkey
}
if parse_char(p, "&") {
return .event
}
return nil
}
func parse_post_reference(_ p: Parser) -> ReferencedId? {
let start = p.pos
guard let typ = parse_post_mention_type(p) else {
return parse_nostr_ref_uri(p)
}
if let ref = parse_post_mention(p, mention_type: typ) {
return ref
}
p.pos = start
return nil
}
func is_bech32_char(_ c: Character) -> Bool {
let contains = "qpzry9x8gf2tvdw0s3jn54khce6mua7l".contains(c)
return contains
}
func parse_post_mention(_ p: Parser, mention_type: MentionType) -> ReferencedId? {
if let id = parse_hexstr(p, len: 64) {
return ReferencedId(ref_id: id, relay_id: nil, key: mention_type.ref)
} else if let bech32_ref = parse_post_bech32_mention(p) {
return bech32_ref
} else {
return nil
}
}
func parse_post_bech32_mention(_ p: Parser) -> ReferencedId? {
let start = p.pos
if parse_str(p, "note") {
} else if parse_str(p, "npub") {
} else if parse_str(p, "nsec") {
} else {
return nil
}
if !parse_char(p, "1") {
p.pos = start
return nil
}
guard consume_until(p, match: { c in !is_bech32_char(c) }, end_ok: true) else {
return nil
}
let end = p.pos
let sliced = String(substring(p.str, start: start, end: end))
guard let decoded = try? bech32_decode(sliced) else {
p.pos = start
return nil
}
let hex = hex_encode(decoded.data)
switch decoded.hrp {
case "note":
return ReferencedId(ref_id: hex, relay_id: nil, key: "e")
case "npub":
return ReferencedId(ref_id: hex, relay_id: nil, key: "p")
case "nsec":
guard let pubkey = privkey_to_pubkey(privkey: hex) else {
p.pos = start
return nil
}
return ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
default:
p.pos = start
return nil
}
}
/// Return a list of tags
func parse_post_blocks(content: String) -> [PostBlock] {
let p = Parser(pos: 0, str: content)
var blocks: [PostBlock] = []
var starting_from: Int = 0
if content.count == 0 {
return []
}
while p.pos < content.count {
let pre_mention = p.pos
if let reference = parse_post_reference(p) {
blocks.append(parse_post_textblock(str: p.str, from: starting_from, to: pre_mention))
blocks.append(.ref(reference))
starting_from = p.pos
} else if let hashtag = parse_hashtag(p) {
blocks.append(parse_post_textblock(str: p.str, from: starting_from, to: pre_mention))
blocks.append(.hashtag(hashtag))
starting_from = p.pos
} else {
p.pos += 1
}
}
blocks.append(parse_post_textblock(str: content, from: starting_from, to: content.count))
return blocks
func parse_post_blocks(content: String) -> [Block] {
return parse_note_content(content: .content(content, nil)).blocks
}

View File

@@ -6,34 +6,3 @@
//
import Foundation
enum PostBlock {
case text(String)
case ref(ReferencedId)
case hashtag(String)
var is_text: String? {
if case .text(let txt) = self {
return txt
}
return nil
}
var is_hashtag: String? {
if case .hashtag(let ht) = self {
return ht
}
return nil
}
var is_ref: ReferencedId? {
if case .ref(let ref) = self {
return ref
}
return nil
}
}
func parse_post_textblock(str: String, from: Int, to: Int) -> PostBlock {
return .text(String(substring(str, start: from, end: to)))
}

View File

@@ -14,14 +14,14 @@ class ProfileModel: ObservableObject, Equatable {
@Published var progress: Int = 0
var events: EventHolder
let pubkey: String
let pubkey: Pubkey
let damus: DamusState
var seen_event: Set<String> = Set()
var seen_event: Set<NoteId> = Set()
var sub_id = UUID().description
var prof_subid = UUID().description
init(pubkey: String, damus: DamusState) {
init(pubkey: Pubkey, damus: DamusState) {
self.pubkey = pubkey
self.damus = damus
self.events = EventHolder(on_queue: { ev in
@@ -29,22 +29,12 @@ class ProfileModel: ObservableObject, Equatable {
})
}
func follows(pubkey: String) -> Bool {
func follows(pubkey: Pubkey) -> Bool {
guard let contacts = self.contacts else {
return false
}
for tag in contacts.tags {
guard tag.count >= 2 && tag[0] == "p" else {
continue
}
if tag[1] == pubkey {
return true
}
}
return false
return contacts.referenced_pubkeys.contains(pubkey)
}
func get_follow_target() -> FollowTarget {
@@ -69,17 +59,16 @@ class ProfileModel: ObservableObject, Equatable {
}
func subscribe() {
var text_filter = NostrFilter(kinds: [.text, .chat])
var text_filter = NostrFilter(kinds: [.text, .longform])
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
profile_filter.authors = [pubkey]
text_filter.authors = [pubkey]
text_filter.limit = 50
text_filter.limit = 500
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event)
}
@@ -113,8 +102,6 @@ class ProfileModel: ObservableObject, Equatable {
}
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(events: damus.events, our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
}
seen_event.insert(ev.id)
}
@@ -132,8 +119,9 @@ class ProfileModel: ObservableObject, Equatable {
break
case .event(_, let ev):
add_event(ev)
case .notice(let notice):
notify(.notice, notice)
case .notice:
break
//notify(.notice, notice)
case .eose:
if resp.subid == sub_id {
load_profiles(profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus)
@@ -146,10 +134,10 @@ class ProfileModel: ObservableObject, Equatable {
}
func count_pubkeys(_ tags: [[String]]) -> Int {
func count_pubkeys(_ tags: Tags) -> Int {
var c: Int = 0
for tag in tags {
if tag.count >= 2 && tag[0] == "p" {
if tag.count >= 2 && tag[0].matches_char("p") {
c += 1
}
}

View File

@@ -8,7 +8,16 @@
import Foundation
struct ProfileUpdate {
let pubkey: String
let profile: Profile
enum ProfileUpdate {
case manual(pubkey: Pubkey, profile: Profile)
case remote(pubkey: Pubkey)
var pubkey: Pubkey {
switch self {
case .manual(let pubkey, _):
return pubkey
case .remote(let pubkey):
return pubkey
}
}
}

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