Compare commits

...

45 Commits

Author SHA1 Message Date
William Casarin 23138c5e03 v1.9 (2)
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-09 15:03:58 -07:00
William Casarin 213a622dde version: damus versions should be inherited from the project
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-09 15:02:24 -07:00
William Casarin 4ac3da7612 events: try nostrdb cache if we don't have one in-memory
Our nip10 logic looks for notes only in the in-memory cache. This means
sometimes threads don't get fully loaded if for whatever reason it's in
the nostrdb cache but not in-memory.

Ideally we would just remove our in-memory cache, but for now let's just
hack it so it falls back to nostrdb and makes an owned note.

Changelog-Fixed: Fixed threads not loading sometimes
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-09 14:44:22 -07:00
William Casarin bb1f912f78 nip10: consolidate event_ref logic into ThreadReply
These are overlapping concepts, lets slowly get rid of EventRef

Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-09 14:44:22 -07:00
William Casarin a190a5e8fb nip10: handle invalid reply-with-no-root
I noticed a few clients do this even though its not valid. Let's handle it.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-09 13:49:06 -07:00
William Casarin 514a053dce nip10: marker replies
This should drastically increase compatibility for damus replies in
other clients.

Also filter non-pubkey references when replying so we don't run into the
q-tag bug.

Changelog-Added: Added nip10 marker replies
Changelog-Fixed: Fixed issue where some replies were including the q tag
Fixes: https://github.com/damus-io/damus/issues/2239
Fixes: https://github.com/damus-io/damus/issues/2233
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-09 13:33:04 -07:00
William Casarin 0b199a18b4 Revert "perf: debounce scroll queue"
This perf change was experimental and probably minor anyways

This reverts commit d49cf5a505.

Fixes: https://github.com/damus-io/damus/issues/2235
Changelog-Fixed: Fixed issue where timeline was scrolling when it isn't supposed to
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-09 11:11:29 -07:00
William Casarin 23a125ea0f test: add missing mute test file
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-07 18:13:23 -07:00
Daniel D’Aquino f406d27507 Add basic word muting automated test
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-07 18:06:08 -07:00
Daniel D’Aquino ceb6eb03fb Implement cache on MutelistManager
To filter muted events on a timeline, we need to check if an event is
muted. However, we need to do so in a very efficient manner, to avoid performance degradation.

This commit improves performance by caching mute statuses for as long as
the mutelist stays the same.

Testing
--------

PASS

Device: iPhone 13 Mini
iOS: 17.4.1
Damus: This commit
Damus baseline: bb8dba6df6e2534dfb193402399b31a4fae8052d
Steps:
1. Downgrade to baseline version, Run SwiftUI profiler on Xcode instruments
2. Scroll down home feed. Try to notice hangs and microhangs on the UI
3. Upgrade to version under test. Start the same profiler test.
4. Check that hangs/microhangs frequency and severity are roughly the same.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-07 18:05:45 -07:00
Daniel D’Aquino b917b4e9d6 Apply mute rules to Timeline views by default
Mute rules were not applied in all timeline views. This commit adds code
to filter out muted events on timelines. It also provides an opt-out
parameter if there is ever a need to a timeline without mute filters

Testing
--------

PASS

Device: iPhone 13 Mini
iOS: 17.4.1
Damus: This commit
Coverage:
1. Add a muted keyword
2. Go to home view, check that events with that keyword are now not showing up

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-07 18:05:45 -07:00
Daniel D’Aquino e981ae247e Mute: Add `user_keypair` to MutelistManager
The user keypair is necessary to determine whether or not a given event
is muted or not. Previously, the user keypair had to be passed on each
function call as an optional parameter, and word filtering would not
work unless the caller remembered to add the keypair parameter.

All usages of MutelistManager functions indicate that the desired base
keypair is always the same as the DamusState's keypair that owns the
MutelistManager. Therefore, it is simpler and less error prone to simply
pass the keypair to MutelistManager during its initialization.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-07 18:05:45 -07:00
William Casarin dcd7b5b111 nip10: fix mixed nip10 markers
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-07 14:07:47 -07:00
William Casarin a721256e9b nip10: add marker nip10 support when reading notes
We still need to add these when writing notes.

Changelog-Added: Add marker nip10 support when reading notes
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-07 14:07:39 -07:00
William Casarin 007bcc8687 test: disable broken auth test
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-07 14:07:14 -07:00
Daniel D’Aquino ccb94e6d69
Merge pull request #2201 from tyiu/thai
Add Thai as a supported language
2024-05-06 13:06:55 -07:00
William Casarin 3c9fd36654 Merge commit 0a6e40798a ("docs: add NIP04 to readme for encrypted DM's") 2024-05-06 11:54:42 -07:00
Daniel D’Aquino 9a9b5d5f4f Merge Privacy report updates from release branch 'v1.8_relay_fix_and_video_player' 2024-05-06 11:25:52 -07:00
Daniel D’Aquino d4f041aead Fill up missing Privacy report information for App submission
This commit adds missing privacy report information for both damus and
the notification extension.

It details the reason we use
- File timestamps
- UserDefaults

The reason codes were taken from Apple's documentation: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api

Testing
--------

PASS

Damus: this commit
Steps:
1. Build app for archival
2. Access the local archive and perform a secondary click
3. Click on "Generate Privacy Report"
4. Open the privacy report PDF. It should show no errors. PASS

Closes: https://github.com/damus-io/damus/issues/2184
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-06 11:21:35 -07:00
Terry Yiu 8133da82c1
Add Thai as a supported language 2024-05-03 21:02:47 -04:00
Daniel D’Aquino fc8f211da2
Merge pull request #2199 from tyiu/tyiu/search-profile-sort
Refactor UserSearch profile sorting so that it can be used in SearchResultsView
2024-05-03 13:14:16 -07:00
Daniel D’Aquino cea4922442 Merge release branch 'v1.8_relay_fix_and_video_player'
Manually fixed simple merge conflict in damus.xcodeproj/project.pbxproj
2024-05-03 12:36:58 -07:00
Daniel D’Aquino 669a313f92 Relays: Always respect user's local relay list when present
This commit fixes an issue where the Damus relay (Or other bootstrap relays) would be added to the user's relay list even though they explicitly removed it.

The root cause of the issue lies in the way we load bootstrap relays. The default bootstrap relays would be initially loaded even though the user already has a bootstrap list stored, just in case all the relays on the user list fails. This would cause the app to inadvertently connect to relays that the user did not select whenever there is a connectivity issue with all their listed relays.

The fix is to simply not add the default bootstrap list when the user already has a list stored. We do not need to use bootstrap relays in order to get our relay list, because that list is already stored in both UserDefaults as well as on NostrDB through the user's contact list event. (A contact list which is also locally loaded on startup since the fix related to https://github.com/damus-io/damus/issues/2057)

Issue reproduction + Testing
----------------------------

Procedure:
1. Disconnect from all relays, and disconnect from the Damus relay last.
2. Connect to a local relay (that you control). Connection should be successful.
3. Quit the app completely.
4. Stop the local relay.
5. Restart the app.
6. Go to the relay list view.
7. Check the relay list. It should list the one local relay selected by the user

Issue reproduction:
- Device: iPhone 15 simulator
- iOS: 17.4
- Damus: 1.8 (`97169f4fa276723bfab28ca304953ec206c904d2`)
- Result: ISSUE REPRODUCED
- Details: On step 7, the relay list only lists the Damus relay

Fix test:
- Device: iPhone 15 simulator
- iOS: 17.4
- Damus: This commit
- Result: PASS
- Details: On step 7, the local relay is listed even though connection is unsuccessful. No notes are loaded since no relays were able to connect successfully

Quick regression check
----------------------

PASS

Device: iPhone 15 simulator
iOS: 17.4
Damus: This commit
Steps:
1. Reinstall app from scratch
2. Create a new account, go through onboarding
3. Make sure that new account connects to bootstrap relays. PASS
4. Sign out
5. Sign in with previously existing account (The one from the previous test) (Notice no UserDefaults exists for this user at that point)
6. Make sure relay list is loaded to the latest relay list known to the bootstrap relays (i.e. connects only to the Damus relay) (It cannot recover the latest relay list pointing only to the local relay, since the bootstrap relays have no knowledge about that relay or the contact lists stored there.). PASS

Note: The behavior on step 6 is not a bug, it is an expected limitation. In fact, this behavior is privacy protecting, as the user may not want those public relays from knowing about its connection preference to the local relay (and its address)

Other information
------------------

Q: How is this test using local relays related or equivalent to Tor relay list described in #2186?
A: Those Tor relays need dedicated software (such as Orbot VPN) to be running successfully in order for Damus to make a successful connection to them. If at any moment that VPN stops working, it would trigger the same situation as described in the test above, where all relay connections fail at once.

Q: In #2186, the user reports that the Damus relay is added, but does not describe the Damus relay replacing existing relays. What is the difference?
A: I believe the difference is in the order in which relays are added or removed. We have to remember that the relay we just disconnected from will likely still have version N-1 of our contact list event, where it still includes itself on that list.

Changelog-Fixed: Fix issue where bootstrap relays would inadvertently be added to the user's list on connectivity issues
Closes: https://github.com/damus-io/damus/issues/2186
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-03 12:16:59 -07:00
William Casarin ac7d6c65c7 Merge remote-tracking branch 'github/translations' 2024-05-03 10:53:36 -05:00
transifex-integration[bot] c15f0454de
Translate Localizable.stringsdict in th
100% translated source file: 'Localizable.stringsdict'
on 'th'.
2024-05-03 09:40:22 +00:00
Terry Yiu 6bddee0354
Refactor UserSearch profile sorting so that it can be used in SearchResultsView 2024-05-02 18:09:55 -04:00
transifex-integration[bot] 43fc662bf6
Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2024-05-02 13:59:49 +00:00
transifex-integration[bot] d2b7878d03
Translate InfoPlist.strings in th
100% translated source file: 'InfoPlist.strings'
on 'th'.
2024-05-02 02:13:52 +00:00
Daniel D’Aquino 97169f4fa2 Fix GIF uploads
This commit fixes GIF uploads and improves GIF support:
- MediaPicker will now skip location data removal processing, as it is not needed on GIF images and causes them to be converted to JPEG images
- The uploader now sets more accurate MIME types on the upload request

Issue Repro
-----------

Device: iPhone 13 Mini
iOS: 17.4.1
Damus: `ada99418f6fcdb1354bc5c1c3f3cc3b4db994ce6`
Steps:
1. Download a GIF from GIPHY to the iOS photo gallery
2. Upload that and attach into a post in Damus
3. Check if GIF is animated.
Results: GIF is not animated. Issue is reproduced.

Testing
-------

PASS

Device: iPhone 13 Mini
iOS: 17.4.1
Damus: this commit
Steps:
1. Create a new post
2. Upload the same GIF as the repro and post
3. Make sure GIF is animated. PASS
4. Create a new post
5. Upload a new GIF image (that has never been uploaded by the user on the app) and post
6. Make sure the GIF is animated on the post. PASS
7. Make sure that JPEGs can still be successfully uploaded. PASS
8. Make sure that MP4s can be uploaded.
9. Make a new post that contains 1 JPEG, 1 MP4 file, and 2 GIF files. Make sure they are all uploaded correctly and all GIF files are animated. PASS

Closes: https://github.com/damus-io/damus/issues/2157
Changelog-Fixed: Fix broken GIF uploads
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-01 10:25:19 -07:00
Daniel D’Aquino 862101a3f7 Fix Ghost notifications from Damus Purple Impending expiration
This commit fixes the "ghost notifications" experienced by Purple users
whose membership has expired (or about to expire).

It does that by using a similar mechanism as other notifications to keep
track of the last event date seen on the notifications tab in a
persistent way.

Testing
--------

iOS: 17.4
Device: iPhone 15 simulator
damus-api: bfe6c4240a0b3729724162896f0024a963586f7c
Damus: This commit
Setup:
1. Local Purple server
2. Damus running on local testing mode for Purple
3. An existing but expired Purple account (on the local server)

Steps:
1. Reopen app after pointing to the new server and setting things up.
2. Check that the bell icon shows there is a new notification. PASS
3. Check that purple expiration notifications are visible. PASS
4. Restart app.
5. Check the bell icon. This time there should be no new notifications. PASS
6. Using another account, engage with the primary test account to cause a new notification to appear.
7. After a second or two, the bell icon should indicate there is a new notification from the other user. PASS
8. Switch out and into the app. Check that the bell icon does not indicate any new notifications. PASS
9. Restart the app again. The bell icon should once again NOT indicate any new notifications. PASS

Changelog-Fixed: Fix ghost notifications caused by Purple impending expiration notifications
Closes: https://github.com/damus-io/damus/issues/2158
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-05-01 10:08:54 -07:00
Daniel D’Aquino a9a2a52881 ui: add First Aid view to settings
also create the contact list reset First Aid action

Automatically detecting whether or not to create a blank contact list
when we could not find any is very tricky. It could mean that no contact
list exists, but it could also mean that a temporary network or relay
outage occurred.

Since resetting the contact list when one already exists is a
destructive action, we should make no assumptions. Instead, we should
provide users the tool to fix it based on their own judgement.

For that reason, the first aid view was created. It detects if no
contact list was found, and in those cases, it gives them an option to
reset (with appropriate warning messages).

Testing 1: Contact list creation robustness
-----------------------------

Setup:
1. Network Link Conditioner installed and configured to this profile:
  - DNS delay: 400 ms
  - Downlink bandwidth: 100 kbps
  - Uplink bandwidth: 50 kbps
  - Packets dropped: 50% (On both uplink and downlink)
  - Delay: 1000 ms (Both uplink and downlink)

Procedure:
1. Turn Network Link conditioner ON
2. Go through the account creation steps
3. At the moment the onboarding follow suggestions screen shows up, quit the app
3. Turn Network Link conditioner OFF
4. Start the app again
5. Verify the home screen. It should present notes from the Damus account (the default follow)
6. Follow someone and wait for 5 seconds
7. Restart app
8. Look at the home feed. Notes from user from step 6 should appear, and that user should appear as being followed by you.

- Repro details:
  - Damus version: ada99418f6
  - Device: iPhone 15 simulator
  - iOS: 17.4
  - Number of runs: 3 times
  - Result: FAILS (issue is reproduced) 3 out of 3 times
- Test details:
  - Damus version: This commit
  - Device: iPhone 15 simulator
  - iOS: 17.4
  - Number of runs: 3 times
  - Result: PASSES all criteria 3 out of 3 times

Testing 2: Contact list First Aid
------------------------------

Setup:
1. Reproduce the issue with the old version as outlined in "Testing 1" above
2. Upgrade to the version in this commit

Steps:
1. Go to Settings > First Aid
2. A button to reset the contact list (and some text for context) should appear. PASS
3. Click on the button. A warning message should appear. PASS
4. Click "cancel". The action should be cancelled and nothing should have changed. PASS
5. Click on the reset button again.
6. Click "Continue" on the warning prompt. The reset button will now show "Contact list has been reset" with a green checkmark. PASS
5. Go back to the home tab. Notes from the Damus account should immediately appear. PASS
6. Try to follow someone and restart the app. Follows should now stick persistently. PASS
7. Go to the First Aid screen again. The reset option should no longer be present. PASS

Changelog-Added: Add First Aid solution for users who do not have a contact list created for their account
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Link: 20240422230912.65056-4-daniel@daquino.me
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-01 10:01:12 -07:00
Daniel D’Aquino c8aba00f85 contacts: save first list to storage during onboarding
This commit adds a mechanism to add the contact list to storage as soon
as it is generated, and thus it reduces the risk of poor network
conditions causing issues.

Changelog-Fixed: Improve reliability of contact list creation during onboarding
Closes: https://github.com/damus-io/damus/issues/2057
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Link: 20240422230912.65056-3-daniel@daquino.me
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-01 09:56:44 -07:00
Daniel D’Aquino bb321b6e8a contacts: save the users' latest contact event ID
... to a persistent setting, and try to load it from NostrDB on app start.

This commit causes the user's contact list event ID to be saved
persistently as a user-specific setting, and to be loaded immediately
after startup from the local NostrDB instance.

This helps improve reliability around contact lists, since we previously
relied on fetching that contact list from other relays.

Eventually we will not need the event ID to be stored at all, as we will
be able to query NostrDB, but for now having the latest event ID
persistently stored will allow us to get around this limitation in the
cleanest possible way (i.e. without having to store the event itself
into another mechanism, and migrating it later to NostrDB)

Other notes:

- It uses a mechanism similar to other user settings, so it is
  pubkey-specific and should handle login/logout cases

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Link: 20240422230912.65056-2-daniel@daquino.me
Signed-off-by: William Casarin <jb55@jb55.com>
2024-05-01 09:56:30 -07:00
transifex-integration[bot] 4544d1548c
Translate Localizable.strings in zh_TW
100% translated source file: 'Localizable.strings'
on 'zh_TW'.
2024-04-27 09:31:26 +00:00
transifex-integration[bot] e1b787c7ed
Translate Localizable.strings in zh_HK
100% translated source file: 'Localizable.strings'
on 'zh_HK'.
2024-04-27 09:31:21 +00:00
transifex-integration[bot] 108456fb59
Translate Localizable.strings in zh_CN
100% translated source file: 'Localizable.strings'
on 'zh_CN'.
2024-04-27 09:31:13 +00:00
transifex-integration[bot] 0982bfbb56
Translate Localizable.stringsdict in zh_TW
100% translated source file: 'Localizable.stringsdict'
on 'zh_TW'.
2024-04-27 08:44:47 +00:00
transifex-integration[bot] a9e563663a
Translate Localizable.stringsdict in zh_HK
100% translated source file: 'Localizable.stringsdict'
on 'zh_HK'.
2024-04-27 08:44:44 +00:00
transifex-integration[bot] 986cc715fa
Translate Localizable.stringsdict in zh_CN
100% translated source file: 'Localizable.stringsdict'
on 'zh_CN'.
2024-04-27 08:44:30 +00:00
transifex-integration[bot] ea4c2a1d1c
Translate Localizable.stringsdict in vi
100% translated source file: 'Localizable.stringsdict'
on 'vi'.
2024-04-25 20:05:12 +00:00
transifex-integration[bot] 3c77d58b11
Translate Localizable.strings in vi
100% translated source file: 'Localizable.strings'
on 'vi'.
2024-04-25 20:05:02 +00:00
transifex-integration[bot] 3cea556827
Translate Localizable.strings in es_ES
100% translated source file: 'Localizable.strings'
on 'es_ES'.
2024-04-25 15:47:20 -04:00
transifex-integration[bot] c5bdb22a86
Translate Localizable.strings in es_ES
100% translated source file: 'Localizable.strings'
on 'es_ES'.
2024-04-25 15:47:20 -04:00
transifex-integration[bot] 3142fd5700
Translate Localizable.strings in es_ES
100% translated source file: 'Localizable.strings'
on 'es_ES'.
2024-04-25 15:47:19 -04:00
Fonta1n3 0a6e40798a
docs: add NIP04 to readme for encrypted DM's 2024-03-13 09:23:18 +08:00
46 changed files with 1175 additions and 211 deletions

View File

@ -28,7 +28,7 @@ struct NotificationExtensionState: HeadlessDamusState {
self.settings = UserSettingsStore()
self.contacts = Contacts(our_pubkey: keypair.pubkey)
self.mutelist_manager = MutelistManager()
self.mutelist_manager = MutelistManager(user_keypair: keypair)
self.keypair = keypair
self.profiles = Profiles(ndb: ndb)
self.zaps = Zaps(our_pubkey: keypair.pubkey)

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>File Timestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
</array>
</dict>
</plist>

27
PrivacyInfo.xcprivacy Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>File Timestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -98,6 +98,7 @@
4C2B10282A7B0F5C008AA43E /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; };
4C2B7BF22A71B6540049DEE7 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B7BF12A71B6540049DEE7 /* Id.swift */; };
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
4C2D34412BDAF1B300F9FB44 /* NIP10Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */; };
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; };
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; };
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */; };
@ -174,6 +175,7 @@
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; };
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
4C45E5022BED4D000025A428 /* ThreadReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C45E5012BED4D000025A428 /* ThreadReply.swift */; };
4C463CBF2B960B96008A8C36 /* PurpleBackdrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C463CBE2B960B96008A8C36 /* PurpleBackdrop.swift */; };
4C4793012A993CDA00489948 /* mdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4793002A993B9A00489948 /* mdb.c */; settings = {COMPILER_FLAGS = "-w"; }; };
4C4793042A993DC000489948 /* midl.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4793032A993DB900489948 /* midl.c */; settings = {COMPILER_FLAGS = "-w"; }; };
@ -247,6 +249,7 @@
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */; };
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C45E5012BED4D000025A428 /* ThreadReply.swift */; };
4C9054852A6AEAA000811EEC /* NdbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9054842A6AEAA000811EEC /* NdbTests.swift */; };
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
@ -484,6 +487,7 @@
D74AAFD62B155F0C006CF0F4 /* WalletConnect+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */; };
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; };
D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; };
@ -636,6 +640,8 @@
D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */; };
D7EDED342B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */; };
D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; };
D7FB14222BE5970000398331 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */; };
D7FB14252BE5A9A800398331 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */; };
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */; };
D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */; };
E02429952B7E97740088B16C /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E02429942B7E97740088B16C /* CameraController.swift */; };
@ -772,6 +778,9 @@
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A994C4C2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A994C4D2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A994C4E2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
@ -883,6 +892,7 @@
4C2B10272A7B0F5C008AA43E /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
4C2B7BF12A71B6540049DEE7 /* Id.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Id.swift; sourceTree = "<group>"; };
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP10Tests.swift; sourceTree = "<group>"; };
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = "<group>"; };
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemView.swift; sourceTree = "<group>"; };
@ -986,6 +996,7 @@
4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; };
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; };
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
4C45E5012BED4D000025A428 /* ThreadReply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReply.swift; sourceTree = "<group>"; };
4C463CBE2B960B96008A8C36 /* PurpleBackdrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleBackdrop.swift; sourceTree = "<group>"; };
4C478E242A9932C100489948 /* Ndb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ndb.swift; sourceTree = "<group>"; };
4C478E262A99353500489948 /* threadpool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = threadpool.h; sourceTree = "<group>"; };
@ -1396,6 +1407,7 @@
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; };
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
@ -1430,6 +1442,8 @@
D7EDED202B117DCA0018B19C /* SequenceUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceUtils.swift; sourceTree = "<group>"; };
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = "<group>"; };
D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusUserDefaults.swift; sourceTree = "<group>"; };
D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstAidSettingsView.swift; sourceTree = "<group>"; };
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = "<group>"; };
E02429942B7E97740088B16C /* CameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = "<group>"; };
@ -1604,7 +1618,6 @@
4C363A93282704FA006E126D /* Post.swift */,
4C363A952827096D006E126D /* PostBlock.swift */,
4C363A9928283854006E126D /* Reply.swift */,
4C363A9B282838B9006E126D /* EventRef.swift */,
4C363AA328296DEE006E126D /* SearchModel.swift */,
0E8A4BB62AE4359200065E81 /* NostrFilter+Hashable.swift */,
4C3AC79A28306D7B00E1F516 /* Contacts.swift */,
@ -1777,6 +1790,15 @@
path = flatbuffers;
sourceTree = "<group>";
};
4C45E5002BED4CE10025A428 /* NIP10 */ = {
isa = PBXGroup;
children = (
4C363A9B282838B9006E126D /* EventRef.swift */,
4C45E5012BED4D000025A428 /* ThreadReply.swift */,
);
path = NIP10;
sourceTree = "<group>";
};
4C478E2A2A9935D300489948 /* bindings */ = {
isa = PBXGroup;
children = (
@ -2444,6 +2466,7 @@
4CE6DEDA27F7A08100C66700 = {
isa = PBXGroup;
children = (
D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */,
4C32B9362A9AD44700DC3548 /* flatbuffers */,
4C9054862A6AEB4500811EEC /* nostrdb */,
4C19AE4A2A5CEF7C00C90DB7 /* nostrscript */,
@ -2474,6 +2497,7 @@
4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup;
children = (
4C45E5002BED4CE10025A428 /* NIP10 */,
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
4CA3529C2A76AE47003BB08B /* Notify */,
4CC14FEC2A73FC9A007AEB17 /* Types */,
@ -2549,7 +2573,8 @@
E06336A92B75832100A88E6B /* ImageMetadataTest.swift */,
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */,
D72927AC2BAB515C00F93E90 /* RelayURLTests.swift */,
D7831AF72BBE11E2005DA780 /* VideoCacheTests.swift */,
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */,
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@ -2750,6 +2775,7 @@
D79C4C182AFEB061003A41B4 /* Info.plist */,
D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */,
D74AAFC42B1538DE006CF0F4 /* NotificationExtensionState.swift */,
D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */,
);
path = DamusNotificationService;
sourceTree = "<group>";
@ -2947,6 +2973,7 @@
ru,
"sv-SE",
sw,
th,
"tr-TR",
uk,
vi,
@ -2981,6 +3008,7 @@
buildActionMask = 2147483647;
files = (
4C1D4FB42A7967990024F453 /* build-git-hash.txt in Resources */,
D7FB14222BE5970000398331 /* PrivacyInfo.xcprivacy in Resources */,
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
@ -3014,6 +3042,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D7FB14252BE5A9A800398331 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -3205,6 +3234,7 @@
4C86F7C62A76C51100EC0817 /* AttachedWalletNotify.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
4C45E5022BED4D000025A428 /* ThreadReply.swift in Sources */,
D74AAFD42B155ECB006CF0F4 /* Zaps+.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4C7D09592A05BEAD00943473 /* KeyboardVisible.swift in Sources */,
@ -3511,6 +3541,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4C2D34412BDAF1B300F9FB44 /* NIP10Tests.swift in Sources */,
4C684A572A7FFAE6005E6031 /* UrlTests.swift in Sources */,
3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */,
4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */,
@ -3550,6 +3581,7 @@
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */,
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */,
@ -3570,6 +3602,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */,
D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */,
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */,
D7CB5D552B11758A00AD4105 /* UnmuteThreadNotify.swift in Sources */,
@ -3731,36 +3764,37 @@
3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */ = {
isa = PBXVariantGroup;
children = (
3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A929C22297F2CF80090925E /* it-IT */,
3AB5B86C2986D8A3006599D2 /* de */,
3AF6336A29884C6B0005672A /* pt-PT */,
3A93342B29884CA600D6A8F3 /* pl-PL */,
3AC524F0298C000B00693EBF /* ar */,
3A96D41C298DA94500388A2A /* nl */,
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
3A25EF152992DA5D008ABE69 /* el-GR */,
3A66D929299472FA008B44F4 /* ja */,
3A41E55B299D52BE001FA465 /* id */,
3AA5E70729B9E84A002701ED /* bg */,
3A8624DB299E82BE00BD8BE9 /* cs */,
3AB5B86C2986D8A3006599D2 /* de */,
3A25EF152992DA5D008ABE69 /* el-GR */,
3A2B8B0A296A8982009CC16D /* en-US */,
3A5C4575296A879E0032D398 /* es-419 */,
3A325AC929C9E0CF002BE7ED /* es-ES */,
3AD5662C29BD2F5300BF77C5 /* fa */,
3A47CB792BDA05A200728A7C /* fi */,
3A821C4029E819D500B4BCA7 /* fr */,
3AD14EB529C40F38009D2D9C /* hu-HU */,
3A41E55B299D52BE001FA465 /* id */,
3A929C22297F2CF80090925E /* it-IT */,
3A66D929299472FA008B44F4 /* ja */,
3AD5663229C0DA4B00BF77C5 /* ko */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A96D41C298DA94500388A2A /* nl */,
3A93342B29884CA600D6A8F3 /* pl-PL */,
3AC59CA929CDDB78007E04A6 /* pt-BR */,
3AF6336A29884C6B0005672A /* pt-PT */,
3A827A1A299FC69D00C4D171 /* ru */,
3AD14EB829C40F3F009D2D9C /* sv-SE */,
3ABACEC02A5B3ED10037A847 /* sw */,
3A994C4C2BE5B9370019F632 /* th */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3AA5E70429B682B3002701ED /* uk */,
3A325AC629C9E0B8002BE7ED /* vi */,
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
3A3040FB29A91F03008A0F29 /* zh-HK */,
3A3040FD29A91F31008A0F29 /* zh-TW */,
3AA5E70429B682B3002701ED /* uk */,
3AA5E70729B9E84A002701ED /* bg */,
3AD5662C29BD2F5300BF77C5 /* fa */,
3AD5663229C0DA4B00BF77C5 /* ko */,
3AD14EB529C40F38009D2D9C /* hu-HU */,
3AD14EB829C40F3F009D2D9C /* sv-SE */,
3A325AC629C9E0B8002BE7ED /* vi */,
3A325AC929C9E0CF002BE7ED /* es-ES */,
3AC59CA929CDDB78007E04A6 /* pt-BR */,
3A821C4029E819D500B4BCA7 /* fr */,
3ABACEC02A5B3ED10037A847 /* sw */,
3A47CB792BDA05A200728A7C /* fi */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@ -3768,35 +3802,36 @@
3ACB685A297633BC00C46468 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
3ACB685B297633BC00C46468 /* es-419 */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A929C20297F2CF80090925E /* it-IT */,
3AB5B86A2986D8A3006599D2 /* de */,
3AF6336829884C6B0005672A /* pt-PT */,
3A93342929884CA600D6A8F3 /* pl-PL */,
3AC524EE298C000B00693EBF /* ar */,
3A96D41A298DA94500388A2A /* nl */,
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
3A25EF132992DA5D008ABE69 /* el-GR */,
3A66D927299472FA008B44F4 /* ja */,
3A41E559299D52BE001FA465 /* id */,
3AA5E70529B9E83E002701ED /* bg */,
3A8624D9299E82BE00BD8BE9 /* cs */,
3AB5B86A2986D8A3006599D2 /* de */,
3A25EF132992DA5D008ABE69 /* el-GR */,
3ACB685B297633BC00C46468 /* es-419 */,
3A325AC829C9E0CF002BE7ED /* es-ES */,
3AD5662B29BD2F5300BF77C5 /* fa */,
3A47CB772BDA05A200728A7C /* fi */,
3A821C3F29E819D500B4BCA7 /* fr */,
3AD14EB629C40F38009D2D9C /* hu-HU */,
3A41E559299D52BE001FA465 /* id */,
3A929C20297F2CF80090925E /* it-IT */,
3A66D927299472FA008B44F4 /* ja */,
3AD5663329C0DA4B00BF77C5 /* ko */,
3A96D41A298DA94500388A2A /* nl */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A93342929884CA600D6A8F3 /* pl-PL */,
3AC59CA829CDDB78007E04A6 /* pt-BR */,
3AF6336829884C6B0005672A /* pt-PT */,
3A827A18299FC69D00C4D171 /* ru */,
3AD14EB929C40F3F009D2D9C /* sv-SE */,
3ABACEBF2A5B3ED10037A847 /* sw */,
3A994C4D2BE5B9370019F632 /* th */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3AA5E70329B682AD002701ED /* uk */,
3A325AC529C9E0B8002BE7ED /* vi */,
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
3A3040F929A91ED6008A0F29 /* zh-HK */,
3A3040FC29A91F31008A0F29 /* zh-TW */,
3AA5E70329B682AD002701ED /* uk */,
3AA5E70529B9E83E002701ED /* bg */,
3AD5662B29BD2F5300BF77C5 /* fa */,
3AD5663329C0DA4B00BF77C5 /* ko */,
3AD14EB629C40F38009D2D9C /* hu-HU */,
3AD14EB929C40F3F009D2D9C /* sv-SE */,
3A325AC529C9E0B8002BE7ED /* vi */,
3A325AC829C9E0CF002BE7ED /* es-ES */,
3AC59CA829CDDB78007E04A6 /* pt-BR */,
3A821C3F29E819D500B4BCA7 /* fr */,
3ABACEBF2A5B3ED10037A847 /* sw */,
3A47CB772BDA05A200728A7C /* fi */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@ -3804,36 +3839,37 @@
3ACB685D297633BC00C46468 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
3ACB685E297633BC00C46468 /* es-419 */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A929C21297F2CF80090925E /* it-IT */,
3AB5B86B2986D8A3006599D2 /* de */,
3AF6336929884C6B0005672A /* pt-PT */,
3A93342A29884CA600D6A8F3 /* pl-PL */,
3AC524EF298C000B00693EBF /* ar */,
3A96D41B298DA94500388A2A /* nl */,
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
3A25EF142992DA5D008ABE69 /* el-GR */,
3A66D928299472FA008B44F4 /* ja */,
3A41E55A299D52BE001FA465 /* id */,
3AA5E70629B9E844002701ED /* bg */,
3A8624DA299E82BE00BD8BE9 /* cs */,
3AB5B86B2986D8A3006599D2 /* de */,
3A25EF142992DA5D008ABE69 /* el-GR */,
3A3040FF29AB02D1008A0F29 /* en-US */,
3ACB685E297633BC00C46468 /* es-419 */,
3A325AC729C9E0CF002BE7ED /* es-ES */,
3AD5662D29BD2F5300BF77C5 /* fa */,
3A47CB782BDA05A200728A7C /* fi */,
3A821C3E29E819D500B4BCA7 /* fr */,
3A41E55A299D52BE001FA465 /* id */,
3AD14EB729C40F38009D2D9C /* hu-HU */,
3A929C21297F2CF80090925E /* it-IT */,
3A66D928299472FA008B44F4 /* ja */,
3AD5663129C0DA4B00BF77C5 /* ko */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A96D41B298DA94500388A2A /* nl */,
3A93342A29884CA600D6A8F3 /* pl-PL */,
3AC59CA729CDDB78007E04A6 /* pt-BR */,
3AF6336929884C6B0005672A /* pt-PT */,
3A827A19299FC69D00C4D171 /* ru */,
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
3ABACEC12A5B3ED10037A847 /* sw */,
3A994C4E2BE5B9370019F632 /* th */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3AA5E70229B682A5002701ED /* uk */,
3A325AC429C9E0B8002BE7ED /* vi */,
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
3A3040FA29A91EFC008A0F29 /* zh-HK */,
3A3040FE29A91F31008A0F29 /* zh-TW */,
3A3040FF29AB02D1008A0F29 /* en-US */,
3AA5E70229B682A5002701ED /* uk */,
3AA5E70629B9E844002701ED /* bg */,
3AD5662D29BD2F5300BF77C5 /* fa */,
3AD5663129C0DA4B00BF77C5 /* ko */,
3AD14EB729C40F38009D2D9C /* hu-HU */,
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
3A325AC429C9E0B8002BE7ED /* vi */,
3A325AC729C9E0CF002BE7ED /* es-ES */,
3AC59CA729CDDB78007E04A6 /* pt-BR */,
3A821C3E29E819D500B4BCA7 /* fr */,
3ABACEC12A5B3ED10037A847 /* sw */,
3A47CB782BDA05A200728A7C /* fi */,
);
name = Localizable.strings;
sourceTree = "<group>";
@ -3876,7 +3912,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -3897,7 +3933,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.8;
MARKETING_VERSION = 1.9;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -3943,7 +3979,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -3959,7 +3995,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.8;
MARKETING_VERSION = 1.9;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@ -4005,7 +4041,6 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.9;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -4055,7 +4090,6 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.9;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";

View File

@ -76,27 +76,35 @@ func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef]
}
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
var root_id: NoteRef? = nil
var any_marker: Bool = false
for ref in ev_tags {
if first {
first_ref = ref
evrefs.append(.thread_id(ref))
first = false
if let marker = ref.marker {
any_marker = true
switch marker {
case .root: root_id = ref
case .reply: evrefs.append(.reply(ref))
case .mention: evrefs.append(.mention(.noteref(ref)))
}
} else {
evrefs.append(.reply(ref))
if !any_marker && first {
root_id = ref
first = false
} else if !any_marker {
evrefs.append(.reply(ref))
}
}
count += 1
}
if let first_ref, count == 1 {
let r = first_ref
return [.reply_to_root(r)]
if let root_id {
if evrefs.count == 0 {
return [.reply_to_root(root_id)]
} else {
evrefs.insert(.thread_id(root_id), at: 0)
}
}
return evrefs

View File

@ -699,7 +699,7 @@ struct ContentView: View {
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
mutelist_manager: MutelistManager(),
mutelist_manager: MutelistManager(user_keypair: keypair),
profiles: Profiles(ndb: ndb),
dms: home.dms,
previews: PreviewCache(),

View File

@ -112,7 +112,7 @@ class DamusState: HeadlessDamusState {
likes: EventCounter(our_pubkey: empty_pub),
boosts: EventCounter(our_pubkey: empty_pub),
contacts: Contacts(our_pubkey: empty_pub),
mutelist_manager: MutelistManager(),
mutelist_manager: MutelistManager(user_keypair: kp),
profiles: Profiles(ndb: .empty),
dms: DirectMessagesModel(our_pubkey: empty_pub),
previews: PreviewCache(),

View File

@ -1148,8 +1148,8 @@ func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool {
)
}
func should_show_event(state: DamusState, ev: NostrEvent, keypair: Keypair? = nil) -> Bool {
let event_muted = state.mutelist_manager.is_event_muted(ev, keypair: keypair)
func should_show_event(state: DamusState, ev: NostrEvent) -> Bool {
let event_muted = state.mutelist_manager.is_event_muted(ev)
if event_muted {
return false
}

View File

@ -292,9 +292,8 @@ func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
}
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)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: post.tags)
let content = post_tags.blocks
.map(\.asString)
.joined(separator: "")

View File

@ -8,12 +8,27 @@
import Foundation
class MutelistManager {
let user_keypair: Keypair
private(set) var event: NostrEvent? = nil
var users: Set<MuteItem> = []
var hashtags: Set<MuteItem> = []
var threads: Set<MuteItem> = []
var words: Set<MuteItem> = []
var users: Set<MuteItem> = [] {
didSet { self.reset_cache() }
}
var hashtags: Set<MuteItem> = [] {
didSet { self.reset_cache() }
}
var threads: Set<MuteItem> = [] {
didSet { self.reset_cache() }
}
var words: Set<MuteItem> = [] {
didSet { self.reset_cache() }
}
var muted_notes_cache: [NoteId: EventMuteStatus] = [:]
init(user_keypair: Keypair) {
self.user_keypair = user_keypair
}
func refresh_sets() {
guard let referenced_mute_items = event?.referenced_mute_items else { return }
@ -41,6 +56,10 @@ class MutelistManager {
threads = new_threads
words = new_words
}
func reset_cache() {
self.muted_notes_cache = [:]
}
func is_muted(_ item: MuteItem) -> Bool {
switch item {
@ -55,8 +74,8 @@ class MutelistManager {
}
}
func is_event_muted(_ ev: NostrEvent, keypair: Keypair? = nil) -> Bool {
return event_muted_reason(ev, keypair: keypair) != nil
func is_event_muted(_ ev: NostrEvent) -> Bool {
return self.event_muted_reason(ev) != nil
}
func set_mutelist(_ ev: NostrEvent) {
@ -114,15 +133,27 @@ class MutelistManager {
threads.remove(item)
}
}
func event_muted_reason(_ ev: NostrEvent) -> MuteItem? {
if let cached_mute_status = self.muted_notes_cache[ev.id] {
return cached_mute_status.mute_reason()
}
if let reason = self.compute_event_muted_reason(ev) {
self.muted_notes_cache[ev.id] = .muted(reason: reason)
return reason
}
self.muted_notes_cache[ev.id] = .not_muted
return nil
}
/// Check if an event is muted given a collection of ``MutedItem``.
///
/// - Parameter ev: The ``NostrEvent`` that you want to check the muted reason for.
/// - Returns: The ``MuteItem`` that matched the event. Or `nil` if the event is not muted.
func event_muted_reason(_ ev: NostrEvent, keypair: Keypair? = nil) -> MuteItem? {
func compute_event_muted_reason(_ ev: NostrEvent) -> MuteItem? {
// Events from the current user should not be muted.
guard keypair?.pubkey != ev.pubkey else { return nil }
guard self.user_keypair.pubkey != ev.pubkey else { return nil }
// Check if user is muted
let check_user_item = MuteItem.user(ev.pubkey, nil)
@ -147,7 +178,7 @@ class MutelistManager {
}
// Check if word is muted
if let keypair, let content: String = ev.maybe_get_content(keypair)?.lowercased() {
if let content: String = ev.maybe_get_content(self.user_keypair)?.lowercased() {
for word in words {
if case .word(let string, _) = word {
if content.contains(string.lowercased()) {
@ -159,4 +190,18 @@ class MutelistManager {
return nil
}
enum EventMuteStatus {
case muted(reason: MuteItem)
case not_muted
func mute_reason() -> MuteItem? {
switch self {
case .muted(reason: let reason):
return reason
case .not_muted:
return nil
}
}
}
}

View File

@ -37,7 +37,7 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
}
// Don't show notifications that match mute list.
if state.mutelist_manager.is_event_muted(ev, keypair: state.keypair) {
if state.mutelist_manager.is_event_muted(ev) {
return false
}

View File

@ -10,12 +10,10 @@ import Foundation
struct NostrPost {
let kind: NostrKind
let content: String
let references: [RefId]
let tags: [[String]]
init(content: String, references: [RefId], kind: NostrKind = .text, tags: [[String]] = []) {
init(content: String, kind: NostrKind = .text, tags: [[String]] = []) {
self.content = content
self.references = references
self.kind = kind
self.tags = tags
}

View File

@ -13,6 +13,15 @@ enum EventRef: Equatable {
case reply(NoteRef)
case reply_to_root(NoteRef)
var note_ref: NoteRef {
switch self {
case .mention(let mnref): return mnref.ref
case .thread_id(let ref): return ref
case .reply(let ref): return ref
case .reply_to_root(let ref): return ref
}
}
var is_mention: NoteRef? {
if case .mention(let m) = self { return m.ref }
return nil
@ -140,8 +149,3 @@ func interpret_event_refs(blocks: [Block], tags: Tags) -> [EventRef] {
}
func event_is_reply(_ refs: [EventRef]) -> Bool {
return refs.contains { evref in
return evref.is_reply != nil
}
}

View File

@ -0,0 +1,63 @@
//
// ThreadReply.swift
// damus
//
// Created by William Casarin on 2024-05-09.
//
import Foundation
struct ThreadReply {
let root: NoteRef
let reply: NoteRef?
let mention: Mention<NoteRef>?
var is_reply_to_root: Bool {
guard let reply else {
// if we have no reply and only root then this is reply-to-root,
// but it should never really be in this form...
return true
}
return root.id == reply.id
}
init(root: NoteRef, reply: NoteRef?, mention: Mention<NoteRef>?) {
self.root = root
self.reply = reply
self.mention = mention
}
init?(event_refs: [EventRef]) {
var root: NoteRef? = nil
var reply: NoteRef? = nil
var mention: Mention<NoteRef>? = nil
for evref in event_refs {
switch evref {
case .mention(let m):
mention = m
case .thread_id(let r):
root = r
case .reply(let r):
reply = r
case .reply_to_root(let r):
root = r
reply = r
}
}
// reply with no root should be considered reply-to-root
if root == nil && reply != nil {
root = reply
}
// nip10 threads must have a root
guard let root else {
return nil
}
self = ThreadReply(root: root, reply: reply, mention: mention)
}
}

View File

@ -73,7 +73,7 @@ var test_damus_state: DamusState = ({
likes: .init(our_pubkey: our_pubkey),
boosts: .init(our_pubkey: our_pubkey),
contacts: .init(our_pubkey: our_pubkey),
mutelist_manager: MutelistManager(),
mutelist_manager: MutelistManager(user_keypair: test_keypair),
profiles: .init(ndb: ndb),
dms: .init(our_pubkey: our_pubkey),
previews: .init(),

View File

@ -169,7 +169,7 @@ class EventCache {
var ev = event
while true {
guard let direct_reply = ev.direct_replies(keypair).last,
guard let direct_reply = ev.direct_replies(keypair),
let next_ev = lookup(direct_reply), next_ev != ev
else {
break
@ -183,7 +183,7 @@ class EventCache {
}
func add_replies(ev: NostrEvent, keypair: Keypair) {
for reply in ev.direct_replies(keypair) {
if let reply = ev.direct_replies(keypair) {
replies.add(id: reply, reply_id: ev.id)
}
}
@ -218,7 +218,16 @@ class EventCache {
*/
func lookup(_ evid: NoteId) -> NostrEvent? {
return events[evid]
if let ev = events[evid] {
return ev
}
if let ev = self.ndb.lookup_note(evid)?.unsafeUnownedValue?.to_owned() {
events[ev.id] = ev
return ev
}
return nil
}
func insert(_ ev: NostrEvent) {

View File

@ -58,7 +58,7 @@ func load_bootstrap_relays(pubkey: Pubkey) -> [RelayURL] {
let relay_urls = relays.compactMap({ RelayURL($0) })
let loaded_relays = Array(Set(relay_urls + get_default_bootstrap_relays()))
let loaded_relays = Array(Set(relay_urls))
print("Loading custom bootstrap relays: \(loaded_relays)")
return loaded_relays
}

View File

@ -39,7 +39,7 @@ class ReplyCounter {
counted.insert(event.id)
for reply in event.direct_replies(keypair) {
if let reply = event.direct_replies(keypair) {
if event.pubkey == our_pubkey {
self.our_replies[reply] = event
}

View File

@ -20,7 +20,7 @@ struct DMChatView: View, KeyboardReadable {
ScrollViewReader { scroller in
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(Array(zip(dms.events, dms.events.indices)).filter { should_show_event(state: damus_state, ev: $0.0, keypair: damus_state.keypair)}, id: \.0.id) { (ev, ind) in
ForEach(Array(zip(dms.events, dms.events.indices)).filter { should_show_event(state: damus_state, ev: $0.0)}, id: \.0.id) { (ev, ind) in
DMView(event: dms.events[ind], damus_state: damus_state)
.contextMenu{MenuItems(damus_state: damus_state, event: ev, target_pubkey: ev.pubkey, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))}
}

View File

@ -55,7 +55,7 @@ struct DirectMessagesView: View {
func MaybeEvent(_ model: DirectMessageModel) -> some View {
Group {
if let ev = model.events.last(where: { should_show_event(state: damus_state, ev: $0, keypair: damus_state.keypair) }) {
if let ev = model.events.last(where: { should_show_event(state: damus_state, ev: $0) }) {
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
.onTapGesture {
self.model.set_active_dm_model(model)

View File

@ -13,18 +13,10 @@ struct ReplyPart: View {
let keypair: Keypair
let ndb: Ndb
var replying_to: NostrEvent? {
guard let note_ref = event.event_refs(keypair).first(where: { evref in evref.is_direct_reply != nil })?.is_direct_reply else {
return nil
}
return events.lookup(note_ref.note_id)
}
var body: some View {
Group {
if event_is_reply(event.event_refs(keypair)) {
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
if let reply_ref = event.thread_reply(keypair)?.reply {
ReplyDescription(event: event, replying_to: events.lookup(reply_ref.note_id), ndb: ndb)
} else {
EmptyView()
}

View File

@ -18,14 +18,6 @@ struct SelectedEventView: View {
@StateObject var bar: ActionBarModel
var replying_to: NostrEvent? {
guard let note_ref = event.event_refs(damus.keypair).first(where: { evref in evref.is_direct_reply != nil })?.is_direct_reply else {
return nil
}
return damus.events.lookup(note_ref.note_id)
}
init(damus: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus = damus
self.event = event
@ -48,8 +40,8 @@ struct SelectedEventView: View {
.minimumScaleFactor(0.75)
.lineLimit(1)
if event_is_reply(event.event_refs(damus.keypair)) {
ReplyDescription(event: event, replying_to: replying_to, ndb: damus.ndb)
if let reply_ref = event.thread_reply(damus.keypair)?.reply {
ReplyDescription(event: event, replying_to: damus.events.lookup(reply_ref.note_id), ndb: damus.ndb)
.padding(.horizontal)
}

View File

@ -92,13 +92,24 @@ struct PostView: View {
}
func send_post() {
let refs = references.filter { ref in
if case .pubkey(let pk) = ref, filtered_pubkeys.contains(pk) {
return false
// don't add duplicate pubkeys but retain order
var pkset = Set<Pubkey>()
// we only want pubkeys really
let pks = references.reduce(into: Array<Pubkey>()) { acc, ref in
guard case .pubkey(let pk) = ref else {
return
}
return true
if pkset.contains(pk) || filtered_pubkeys.contains(pk) {
return
}
pkset.insert(pk)
acc.append(pk)
}
let new_post = build_post(state: damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: refs)
let new_post = build_post(state: damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, pubkeys: pks)
notify(.post(.post(new_post)))
@ -604,7 +615,29 @@ private func isAlphanumeric(_ char: Character) -> Bool {
return char.isLetter || char.isNumber
}
func build_post(state: DamusState, post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId]) -> NostrPost {
func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] {
guard let nip10 = replying_to.thread_reply(keypair) else {
// we're replying to a post that isn't in a thread,
// just add a single reply-to-root tag
return [["e", replying_to.id.hex(), "", "root"]]
}
// otherwise use the root tag from the parent's nip10 reply and include the note
// that we are replying to's note id.
var tags = [
["e", nip10.root.note_id.hex(), nip10.root.relay ?? "", "root"],
["e", replying_to.id.hex(), "", "reply"]
]
// we also add the parent's nip10 reply tag as an additional e tag for context
if let reply = nip10.reply {
tags.append(["e", reply.note_id.hex(), reply.relay ?? ""])
}
return tags
}
func build_post(state: DamusState, post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], pubkeys: [Pubkey]) -> NostrPost {
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
if let link = attributes[.link] as? String {
let nextCharIndex = range.upperBound
@ -634,20 +667,35 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
var tags = uploadedMedias.compactMap { $0.metadata?.to_tag() }
if !imagesString.isEmpty {
content.append(" " + imagesString + " ")
}
if case .quoting(let ev) = action {
var tags: [[String]] = []
switch action {
case .replying_to(let replying_to):
// start off with the reply tags
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
case .quoting(let ev):
content.append(" nostr:" + bech32_note_id(ev.id))
if let quoted_ev = state.events.lookup(ev.id) {
tags.append(["p", quoted_ev.pubkey.hex()])
}
case .posting(let postTarget):
break
}
// include pubkeys
tags += pubkeys.map { pk in
["p", pk.hex()]
}
return NostrPost(content: content, references: references, kind: .text, tags: tags)
// append additional tags
tags += uploadedMedias.compactMap { $0.metadata?.to_tag() }
return NostrPost(content: content, kind: .text, tags: tags)
}

View File

@ -18,17 +18,7 @@ struct UserSearch: View {
var users: [Pubkey] {
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return [] }
return search_profiles(profiles: damus_state.profiles, search: search, txn: txn).sorted { a, b in
let aFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: a)?.priority ?? 0
let bFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: b)?.priority ?? 0
if aFriendTypePriority > bFriendTypePriority {
// `a` should be sorted before `b`
return true
} else {
return false
}
}
return search_profiles(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
}
func on_user_tapped(pk: Pubkey) {

View File

@ -113,11 +113,11 @@ struct SearchResultsView: View {
.frame(maxHeight: .infinity)
.onAppear {
guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return }
self.result = search_for_string(profiles: damus_state.profiles, search: search, txn: txn)
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
}
.onChange(of: search) { new in
guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return }
self.result = search_for_string(profiles: damus_state.profiles, search: search, txn: txn)
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
}
}
}
@ -131,7 +131,7 @@ struct SearchResultsView_Previews: PreviewProvider {
*/
func search_for_string<Y>(profiles: Profiles, search new: String, txn: NdbTxn<Y>) -> Search? {
func search_for_string<Y>(profiles: Profiles, contacts: Contacts, search new: String, txn: NdbTxn<Y>) -> Search? {
guard new.count != 0 else {
return nil
}
@ -174,7 +174,7 @@ func search_for_string<Y>(profiles: Profiles, search new: String, txn: NdbTxn<Y>
return .naddr(naddr)
}
let multisearch = MultiSearch(hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, search: new, txn: txn))
let multisearch = MultiSearch(hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new, txn: txn))
return .multi(multisearch)
}
@ -191,7 +191,7 @@ func make_hashtagable(_ str: String) -> String {
return String(new.filter{$0 != " "})
}
func search_profiles<Y>(profiles: Profiles, search: String, txn: NdbTxn<Y>) -> [Pubkey] {
func search_profiles<Y>(profiles: Profiles, contacts: Contacts, search: String, txn: NdbTxn<Y>) -> [Pubkey] {
// Search by hex pubkey.
if let pubkey = hex_decode_pubkey(search),
profiles.lookup_key_by_pubkey(pubkey) != nil
@ -208,8 +208,16 @@ func search_profiles<Y>(profiles: Profiles, search: String, txn: NdbTxn<Y>) -> [
return [pk]
}
let new = search.lowercased()
return profiles.search(search, limit: 10, txn: txn).sorted { a, b in
let aFriendTypePriority = get_friend_type(contacts: contacts, pubkey: a)?.priority ?? 0
let bFriendTypePriority = get_friend_type(contacts: contacts, pubkey: b)?.priority ?? 0
return profiles.search(search, limit: 10, txn: txn)
if aFriendTypePriority > bFriendTypePriority {
// `a` should be sorted before `b`
return true
} else {
return false
}
}
}

View File

@ -13,10 +13,10 @@ struct InnerTimelineView: View {
let state: DamusState
let filter: (NostrEvent) -> Bool
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool) {
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) {
self.events = events
self.state = damus
self.filter = filter
self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter
}
var event_options: EventViewOptions {

View File

@ -15,15 +15,15 @@ struct TimelineView<Content: View>: View {
let show_friend_icon: Bool
let filter: (NostrEvent) -> Bool
let content: Content?
let debouncer: Debouncer
let apply_mute_rules: Bool
init(events: EventHolder, loading: Binding<Bool>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, content: (() -> Content)? = nil) {
init(events: EventHolder, loading: Binding<Bool>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) {
self.events = events
self._loading = loading
self.damus = damus
self.show_friend_icon = show_friend_icon
self.filter = filter
self.debouncer = Debouncer(interval: 0.5)
self.apply_mute_rules = apply_mute_rules
self.content = content?()
}
@ -42,14 +42,12 @@ struct TimelineView<Content: View>: View {
.id("startblock")
.frame(height: 1)
InnerTimelineView(events: events, damus: damus, filter: loading ? { _ in true } : filter)
InnerTimelineView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules)
.redacted(reason: loading ? .placeholder : [])
.shimmer(loading)
.disabled(loading)
.background(GeometryReader { proxy -> Color in
debouncer.debounce_immediate {
handle_scroll_queue(proxy, queue: self.events)
}
handle_scroll_queue(proxy, queue: self.events)
return Color.clear
})
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,356 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>ผู้ติดตามโดย %2$@, %3$@, %4$@ &amp; %1$d คนอื่นๆ</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
<key>FOLLOWERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>ผู้ติดตาม</string>
</dict>
</dict>
<key>following_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWING@</string>
<key>FOLLOWING</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>กำลังติดตาม</string>
</dict>
</dict>
<key>imports_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@IMPORTS@</string>
<key>IMPORTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>นำเข้า</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
<key>REACTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@ และอีก %1$d คน react ต่อโน้ตที่คุณถูกแท็ก</string>
</dict>
</dict>
<key>reacted_your_note_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
<key>REACTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@ และอีก %1$d คน react ต่อโน้ตของคุณ</string>
</dict>
</dict>
<key>reacted_your_profile_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
<key>REACTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@ และอีก %1$d คน react ต่อโปรไฟล์ของคุณ</string>
</dict>
</dict>
<key>reactions_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTIONS@</string>
<key>REACTIONS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>Reactions</string>
</dict>
</dict>
<key>relays_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@RELAYS@</string>
<key>RELAYS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>รีเลย์</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>ตอบกลับ %2$@, %3$@ &amp; %1$d คนอื่นๆ</string>
</dict>
</dict>
<key>reposted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@ และอีก %1$d คนรีโพสต์โน้ตที่คุณถูกแท็ก</string>
</dict>
</dict>
<key>reposted_your_note_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@ และอีก %1$d คนรีโพสต์โน้ตของคุณ</string>
</dict>
</dict>
<key>reposted_your_profile_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@ และอีก %1$d คนรีโพสต์โปรไฟล์ของคุณ</string>
</dict>
</dict>
<key>reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTS@</string>
<key>REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>รีโพสต์</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>คำพูด</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@SATS@</string>
<key>SATS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>sats</string>
</dict>
</dict>
<key>sats_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@SATS@</string>
<key>SATS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>other</key>
<string>%2$@ sats</string>
</dict>
</dict>
<key>users_talking_about_it</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@USERS@</string>
<key>USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%d ผู้ใช้กำลังพูดถึงเรื่องนี้</string>
</dict>
</dict>
<key>word_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@WORDS@</string>
<key>WORDS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%d คำ</string>
</dict>
</dict>
<key>zap_notification_no_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>other</key>
<string>คุณได้รับ %2$@ sats จาก %3$@</string>
</dict>
</dict>
<key>zap_notification_with_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>other</key>
<string>คุณได้รับ %2$@ sats จาก %3$@: "%4$@"</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
<key>ZAPPED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@ และอีก %1$d คน Zap โน้ตที่คุณถูกแท็ก</string>
</dict>
</dict>
<key>zapped_your_note_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
<key>ZAPPED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@ และอีก %1$d คน Zap โน้ตของคุณ</string>
</dict>
</dict>
<key>zapped_your_profile_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
<key>ZAPPED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%2$@ และอีก %1$d คน Zap คุณ</string>
</dict>
</dict>
<key>zaps_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPS@</string>
<key>ZAPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>Zaps</string>
</dict>
</dict>
</dict>
</plist>

Binary file not shown.

View File

@ -198,6 +198,20 @@
<string>Đăng lại</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>Trích dẫn</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@ -198,6 +198,20 @@
<string>转发</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>引用</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@ -198,6 +198,20 @@
<string>轉發</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>引用</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@ -198,6 +198,20 @@
<string>轉發</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>引用</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@ -9,6 +9,7 @@ import XCTest
@testable import damus
final class AuthIntegrationTests: XCTestCase {
/*
func testAuthIntegrationFilterNostrWine() {
// Create relay pool and connect to `wss://filter.nostr.wine`
let relay_url = RelayURL("wss://filter.nostr.wine")!
@ -67,6 +68,7 @@ final class AuthIntegrationTests: XCTestCase {
XCTAssertEqual(sent_msg["kind"] as! Int, 22242)
XCTAssertEqual((sent_msg["tags"] as! [[String]]).first { $0[0] == "challenge" }![1], json_received[1] as! String)
}
*/
func testAuthIntegrationRelayDamusIo() {
// Create relay pool and connect to `wss://relay.damus.io`

View File

@ -25,7 +25,7 @@ func generate_test_damus_state(
return profiles
}()
let mutelist_manager = MutelistManager()
let mutelist_manager = MutelistManager(user_keypair: test_keypair)
let damus = DamusState(pool: pool,
keypair: test_keypair,
likes: .init(our_pubkey: our_pubkey),

View File

@ -0,0 +1,43 @@
//
// MutingTests.swift
// damusTests
//
// Created by Daniel DAquino on 2024-05-06.
//
import Foundation
import XCTest
@testable import damus
final class MutingTests: XCTestCase {
func testWordMuting() {
// Setup some test data
let test_note = NostrEvent(
content: "Nostr is the super app. Because its actually an ecosystem of apps, all of which make each other better. People havent grasped that yet. They will when its more accessible and onboarding is more straightforward and intuitive.",
keypair: jack_keypair,
createdAt: UInt32(Date().timeIntervalSince1970 - 100)
)!
let spammy_keypair = generate_new_keypair().to_keypair()
let spammy_test_note = NostrEvent(
content: "Some spammy airdrop just arrived! Why stack sats when you can get scammed instead with some random coin? Call 1-800-GET-SCAMMED to claim your airdrop today!",
keypair: spammy_keypair,
createdAt: UInt32(Date().timeIntervalSince1970 - 100)
)!
let mute_item: MuteItem = .word("airdrop", nil)
let existing_mutelist = test_damus_state.mutelist_manager.event
guard
let full_keypair = test_damus_state.keypair.to_full(),
let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: mute_item)
else {
return
}
test_damus_state.mutelist_manager.set_mutelist(mutelist)
test_damus_state.postbox.send(mutelist)
XCTAssert(test_damus_state.mutelist_manager.is_event_muted(spammy_test_note))
XCTAssertFalse(test_damus_state.mutelist_manager.is_event_muted(test_note))
}
}

272
damusTests/NIP10Tests.swift Normal file
View File

@ -0,0 +1,272 @@
//
// NIP10Tests.swift
// damusTests
//
// Created by William Casarin on 2024-04-25.
//
import XCTest
@testable import damus
final class NIP10Tests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}
func test_new_nip10() {
let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52"
let direct_reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d51"
let reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d53"
let tags = [
["e", direct_reply_hex, "", "reply"],
["e", root_note_id_hex, "", "root"],
["e", reply_hex, "", "reply"],
["e", "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d54", "", "mention"],
]
let root_note_id = NoteId(hex: root_note_id_hex)!
let direct_reply_id = NoteId(hex: direct_reply_hex)!
let reply_id = NoteId(hex: reply_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [direct_reply_id, reply_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [direct_reply_id, reply_id])
}
func test_repost_root() {
let mention_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52"
let tags = [
["e", mention_hex, "", "mention"],
]
let mention_id = NoteId(hex: mention_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [])
}
func test_direct_reply_old_nip10() {
let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52"
let tags = [
["e", root_note_id_hex],
]
let root_note_id = NoteId(hex: root_note_id_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
}
func test_direct_reply_new_nip10() {
let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52"
let tags = [
["e", root_note_id_hex, "", "root"],
]
let root_note_id = NoteId(hex: root_note_id_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [root_note_id])
let nip10 = note.thread_reply(test_keypair)!
XCTAssertEqual(nip10.is_reply_to_root, true)
XCTAssertEqual(nip10.root.note_id, root_note_id)
XCTAssertEqual(nip10.reply!.note_id, root_note_id)
}
// seen in the wild by the gleasonator
func test_single_marker() {
let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52"
let tags = [
["e", root_note_id_hex, "", "reply"],
]
let root_note_id = NoteId(hex: root_note_id_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
let thread_reply = ThreadReply(event_refs: refs)!
XCTAssertEqual(thread_reply.mention, nil)
XCTAssertEqual(thread_reply.root.note_id, root_note_id)
XCTAssertEqual(thread_reply.reply!.note_id, root_note_id)
XCTAssertEqual(thread_reply.is_reply_to_root, true)
}
func test_marker_reply() {
let note_json = """
{
"pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e",
"content": "Cant zap you btw",
"id": "a8dc8b74852d7ad114d5d650b2125459c0cba3c1fdcaaf527e03f24082e11ab3",
"created_at": 1715275773,
"sig": "4ee5d8f954c6c087ce51ad02d30dd226eea939cd9ef4e8a8ce4bfaf3aba0a852316cfda83ce3fc9a3d98392a738e7c6b036a3b2aced1392db1be3ca190835a17",
"kind": 1,
"tags": [
[
"e",
"1bb940ce0ba0d4a3b2a589355d908498dcd7452f941cf520072218f7e6ede75e",
"wss://relay.nostrplebs.com",
"reply"
],
[
"p",
"6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e"
],
[
"e",
"00152d2945459fb394fed2ea95af879c903c4ec42d96327a739fa27c023f20e0",
"wss://nostr.mutinywallet.com/",
"root"
]
]
}
""";
let replying_to_hex = "a8dc8b74852d7ad114d5d650b2125459c0cba3c1fdcaaf527e03f24082e11ab3"
let pk = Pubkey(hex: "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e")!
let last_reply_hex = "1bb940ce0ba0d4a3b2a589355d908498dcd7452f941cf520072218f7e6ede75e"
let note = decode_nostr_event_json(json: note_json)!
let reply = build_post(state: test_damus_state, post: .init(string: "hello"), action: .replying_to(note), uploadedMedias: [], pubkeys: [pk] + note.referenced_pubkeys.map({pk in pk}))
let root_hex = "00152d2945459fb394fed2ea95af879c903c4ec42d96327a739fa27c023f20e0"
XCTAssertEqual(reply.tags,
[
["e", root_hex, "wss://nostr.mutinywallet.com/", "root"],
["e", replying_to_hex, "", "reply"],
["e", last_reply_hex, "wss://relay.nostrplebs.com"],
["p", "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e"],
["p", "6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e"],
])
}
func test_mixed_nip10() {
let root_note_id_hex = "27e71cf53299dafb5dc7bcc0a078357418a4375cb1097bf5184662493f79a627"
let reply_hex = "1a616998552cf76e9786f76ac68f6104cdae46377330735c68bfe0b9426d2fa8"
let tags = [
[ "e", root_note_id_hex, "", "root" ],
[ "e", "f99046bd87be7508d55e139de48517c06ef90830d77a5d3213df858d77bb2f8f" ],
[ "e", reply_hex, "", "reply" ],
[ "p", "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" ],
[ "p", "8ea485266b2285463b13bf835907161c22bb3da1e652b443db14f9cee6720a43" ],
[ "p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" ]
]
let root_note_id = NoteId(hex: root_note_id_hex)!
let reply_id = NoteId(hex: reply_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [reply_id])
}
func test_deprecated_nip10() {
let root_note_id_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d52"
let direct_reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d51"
let reply_hex = "7c7d37bc8c04d2ec65cbc7d9275253e6b5cc34b5d10439f158194a3feefa8d53"
let tags = [
["e", root_note_id_hex],
["e", direct_reply_hex],
["e", reply_hex],
]
let root_note_id = NoteId(hex: root_note_id_hex)!
let direct_reply_id = NoteId(hex: direct_reply_hex)!
let reply_id = NoteId(hex: reply_hex)!
let note = NdbNote(content: "hi", keypair: test_keypair, kind: 1, tags: tags)!
let refs = interp_event_refs_without_mentions_ndb(note.referenced_noterefs)
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_thread_id?.note_id { xs.append(note_id) }
}), [root_note_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_direct_reply?.note_id { xs.append(note_id) }
}), [direct_reply_id, reply_id])
XCTAssertEqual(refs.reduce(into: Array<NoteId>(), { xs, r in
if let note_id = r.is_reply?.note_id { xs.append(note_id) }
}), [direct_reply_id, reply_id])
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}

View File

@ -123,7 +123,7 @@ class ReplyTests: XCTestCase {
post.append(user_tag_attr_string(profile: profile, pubkey: pk))
post.append(.init(string: "\n"))
let post_note = build_post(state: test_damus_state, post: post, action: .posting(.none), uploadedMedias: [], references: [.pubkey(pk)])
let post_note = build_post(state: test_damus_state, post: post, action: .posting(.none), uploadedMedias: [], pubkeys: [pk])
let expected_render = "nostr:\(pk.npub)\nnostr:\(pk.npub)"
XCTAssertEqual(post_note.content, expected_render)
@ -315,7 +315,7 @@ class ReplyTests: XCTestCase {
let pk = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
let content = "this is a @\(pk.npub) mention"
let blocks = parse_post_blocks(content: content)
let post = NostrPost(content: content, references: [.event(evid)])
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.tags.count, 2)
@ -330,7 +330,7 @@ class ReplyTests: XCTestCase {
let nsec = "nsec1jmzdz7d0ldqctdxwm5fzue277ttng2pk28n2u8wntc2r4a0w96ssnyukg7"
let content = "this is a @\(nsec) mention"
let blocks = parse_post_blocks(content: content)
let post = NostrPost(content: content, references: [.event(evid)])
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.tags.count, 2)
@ -344,13 +344,13 @@ class ReplyTests: XCTestCase {
let thread_id = NoteId(hex: "a250fc93570c3e87f9c9b08d6b3ef7b8e05d346df8a52c69e30ffecdb178fb9e")!
let reply_id = NoteId(hex: "9a180a10f16dac9566543ad1fc29616aab272b0cf123ab5d58843e16f4ef03a3")!
let refs: [RefId] = [
.event(thread_id),
.event(reply_id),
.pubkey(pubkey)
let tags = [
["e", thread_id.hex()],
["e", reply_id.hex()],
["p", pubkey.hex()]
]
let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", references: refs)
let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", tags: tags)
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.content, "this is a (nostr:\(pubkey.npub)) mention")

View File

@ -192,7 +192,7 @@ class damusTests: XCTestCase {
*/
func testMakeHashtagPost() {
let post = NostrPost(content: "#damus some content #bitcoin derp #かっこいい wow", references: [])
let post = NostrPost(content: "#damus some content #bitcoin derp #かっこいい wow", tags: [])
let ev = post_to_event(post: post, keypair: test_keypair_full)!
XCTAssertEqual(ev.tags.count, 3)
@ -269,7 +269,7 @@ class damusTests: XCTestCase {
}
private func createEventFromContentString(_ content: String) -> NostrEvent {
let post = NostrPost(content: content, references: [])
let post = NostrPost(content: content, tags: [])
guard let ev = post_to_event(post: post, keypair: test_keypair_full) else {
XCTFail("Could not create event")
return test_note

View File

@ -340,9 +340,8 @@ extension NdbNote {
References<RefId>(tags: self.tags)
}
func event_refs(_ keypair: Keypair) -> [EventRef] {
let refs = interpret_event_refs_ndb(blocks: self.blocks(keypair).blocks, tags: self.tags)
return refs
func thread_reply(_ keypair: Keypair) -> ThreadReply? {
ThreadReply(event_refs: interpret_event_refs_ndb(blocks: self.blocks(keypair).blocks, tags: self.tags))
}
func get_content(_ keypair: Keypair) -> String {
@ -388,23 +387,17 @@ extension NdbNote {
return dec
}
public func direct_replies(_ keypair: Keypair) -> [NoteId] {
return event_refs(keypair).reduce(into: []) { acc, evref in
if let direct_reply = evref.is_direct_reply {
acc.append(direct_reply.note_id)
}
}
public func direct_replies(_ keypair: Keypair) -> NoteId? {
return thread_reply(keypair)?.reply?.note_id
}
// NDBTODO: just use Id
public func thread_id(keypair: Keypair) -> NoteId {
for ref in event_refs(keypair) {
if let thread_id = ref.is_thread_id {
return thread_id.note_id
}
guard let root = self.thread_reply(keypair)?.root else {
return self.id
}
return self.id
return root.note_id
}
public func last_refid() -> NoteId? {
@ -429,7 +422,7 @@ extension NdbNote {
*/
func is_reply(_ keypair: Keypair) -> Bool {
return event_is_reply(self.event_refs(keypair))
return thread_reply(keypair)?.reply != nil
}
func note_language(_ keypair: Keypair) -> String? {