Compare commits

...

30 Commits

Author SHA1 Message Date
tyiu 6bddee0354 Refactor UserSearch profile sorting so that it can be used in SearchResultsView 2024-05-02 18:09:55 -04:00
Daniel D’Aquino c6d9e0b3c9 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:27:05 -07:00
Daniel D’Aquino 6d5a152c17 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-04-29 17:04:06 -07:00
William Casarin 76529f69d0 Revert "Cache videos"
This reverts commit 26d2627a1c.
2024-04-25 14:56:49 -07:00
William Casarin 052ea9b727 Revert "Custom video loader caching technique"
This reverts commit ba494f94ab.
2024-04-25 14:56:29 -07:00
William Casarin 95cf45073d Merge translations 2024-04-25 12:41:12 -07:00
transifex-integration[bot] efd3c95c10 Translate Localizable.stringsdict in es_ES
100% translated source file: 'Localizable.stringsdict'
on 'es_ES'.
2024-04-25 19:08:23 +00:00
transifex-integration[bot] d198206cc2 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2024-04-25 18:13:02 +00:00
transifex-integration[bot] 0b0bcedb1e Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2024-04-25 17:57:59 +00:00
transifex-integration[bot] 5f7855d6d3 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2024-04-25 17:56:48 +00:00
transifex-integration[bot] e3db84778a Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2024-04-25 17:22:18 +00:00
transifex-integration[bot] 5363a0313f Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2024-04-25 17:21:37 +00:00
transifex-integration[bot] cb116b0f85 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2024-04-25 16:20:39 +00:00
transifex-integration[bot] 62e40d2824 Translate Localizable.stringsdict in nl
100% translated source file: 'Localizable.stringsdict'
on 'nl'.
2024-04-25 15:31:39 +00:00
tyiu e0b1985df5 Fix localization issues 2024-04-25 09:30:27 -04:00
tyiu be585e914d Add Finnish translations 2024-04-24 23:34:50 -04:00
transifex-integration[bot] e515bf7322 Translate Localizable.strings in fa
100% translated source file: 'Localizable.strings'
on 'fa'.
2024-04-24 23:11:36 -04:00
transifex-integration[bot] 050f38feac Translate Localizable.strings in ko
100% translated source file: 'Localizable.strings'
on 'ko'.
2024-04-24 23:11:36 -04:00
transifex-integration[bot] b94a435a9b Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2024-04-24 23:11:35 -04:00
Daniel D’Aquino 279854a9fd 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-04-23 16:41:01 -07:00
Daniel D’Aquino 19ba020bd0 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-04-23 16:40:50 -07:00
Daniel D’Aquino 43a5bbd53a 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-04-23 16:40:25 -07:00
William Casarin 43630cbfa6 zaps: don't verify deschash
seems like most clients don't do this and apparently simplfies some
zapper implementations. It's not a huge deal for us since people can
fake bolt11s anyways.

Suggested-by: bumi, calle
Link: 20240418230321.1907519-1-jb55@jb55.com
Signed-off-by: William Casarin <jb55@jb55.com>
2024-04-23 09:05:13 -07:00
tyiu ae2f48484a Change reactions to use a native looking emoji picker
Changelog-Changed: Change reactions to use a native looking emoji picker
Signed-off-by: William Casarin <jb55@jb55.com>
2024-04-22 17:24:14 -07:00
William Casarin 2c9b280a04 translations: only translate kind 1s
Deepl-Savings: 8k/year
Reported-by: Semisol
Signed-off-by: William Casarin <jb55@jb55.com>
2024-04-22 17:11:19 -07:00
Daniel D’Aquino ba494f94ab Custom video loader caching technique
This commit brings significant improvements to the video cache feature.

Previously, the cache would merely download the video when requested, in
parallel with AVPlayer which also triggers a video download.

The video cache has been updated to tap into the AVPlayer loading
process, removing the download duplication.

Here is how that works:
1. The player requests an AVAsset from the cache.
2. The cache will return a cached asset if possible, or a special AVURLAsset with a custom `AVAssetResourceLoaderDelegate`.
3. The video player will start sending loading requests to this loader delegate.
4. Upon receiving the first request, the loader delegate begins to download the video data on the background.
5. Upon receiving these requests, the loader delegate will also record the requests, so that it can serve them once possible
6. The loader delegate keeps track of all video data chunks as it receives them from the download task, through the `URLSessionDataDelegate` and `URLSessionTaskDelegate` protocols
7. As it receives data, it checks all pending loading requests from the AVPlayer, and fulfills them as soon as possible
8. If the download fails (e.g. timeout errors, loss of connection), it attempts to restart the download.
9. If the download succeeds, it saves the video to the cache on disk.

Closes: https://github.com/damus-io/damus/issues/1717
Changelog-Added: Add video cache to save network bandwidth
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Link: 20240411004129.84436-4-daniel@daquino.me
Signed-off-by: William Casarin <jb55@jb55.com>
2024-04-17 15:06:12 -07:00
Daniel D’Aquino 26d2627a1c Cache videos
This commit implements a simple but functional video cache.

It works by providing a method called `maybe_cached_url`, where a video
URL can be passed in, and this method will either return the URL of a
cached version of this video if available, or the original URL if not. It also
downloads new video URLs on the background into the cache folder for use
next time.

Functional testing
-------------------

PASS

Device: iPhone 15 simulator
iOS: 17.4
Damus: Approximately this commit
Setup:
- Debug connection
- Expiry time locally changed to 5 minutes

Steps:
1. Basic functionality
  1. Go to a profile with lots of videos
  2. Scroll down
  3. Filter logs to only logs that start with "Loading video with URL"
  4. Check that most videos are being loaded from external URLs. PASS
  5. Now restart the app and go to that same profile
  6. Scroll down and watch logs. Videos should now be loaded with an internal file URL. PASS
2. Automatic cache refresh after expiry
  1. Go to the video-heavy profile, make note of the external URL.
  2. Go to a different screen and then come back to that video. Make sure the file was loaded from cache. PASS
  3. Now go to a different screen and wait 5 minutes.
  4. Come back to the same video. It should be loaded from the external URL. PASS
3. "Clear cache" button functionality
  1. Go to the video-heavy profile, make note of the external URL.
  2. Go to a different screen and then come back to that video. Make sure the file was loaded from cache. PASS
  3. Now quit the app (to ensure file is not in use when trying to delete it)
  4. Clear cache in settings
  5. Go back to the same video. It should now be loaded from the external URL. PASS

Performance testing
-----------------------

Device: iPhone 13 mini
iOS: 17.3.1
Damus: This commit
Baseline: 87de88861adb3b41d73998452e7c876ab5ee06bf
Setup:
- Debug connection
- Expiry time locally changed to 5 minutes
- Running on Profile mode, with XCode Instruments

Steps:
1. Start recording network activity with XCode Instruments
2. Go to a video-heavy profile (e.g. Julian Figueroa)
3. Scroll down to a specific video (Make sure to scroll through at least 5 videos)
4. Stop recording and measure the "Bytes In" from "Network connections"
5. Repeat this for all test configurations

Results:
- Baseline (No caching support): 26.74 MiB
- This commit (First run, cleared cache): 40.52 MiB
- This commit (Second run, cache filled with videos): 8.13 MiB

Automated test coverage
------------------------

PASS

Device: iPhone 15 simulator
iOS: 17.4
Damus: This commit
Coverage:
- Ran new automated tests multiple times. PASS 3/3 times
- Ran all other automated tests. PASS

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Link: 20240411004129.84436-3-daniel@daquino.me
Signed-off-by: William Casarin <jb55@jb55.com>
2024-04-17 15:06:12 -07:00
Daniel D’Aquino c2918aaf16 Remove no-op performance tests that were causing issues
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2024-04-17 15:05:35 -07:00
ericholguin e332a7f82c ui: Longform Improvements
This patch improves longform previews by including the image title and tags.
In addition with minor UI touch ups.

Testing:

iPhone 15 Pro Max (17.3.1) Dark Mode:
https://v.nostr.build/9zgvv.mp4

iPhone SE (3rd generation) (16.4) Light Mode:
https://v.nostr.build/VwEKQ.mp4

Closes: https://github.com/damus-io/damus/issues/1742
Changelog-Added: Added title image and tags to longform events
Signed-off-by: ericholguin <ericholguin@apache.org>
Link: 20240415031636.68846-1-ericholguin@apache.org
Signed-off-by: William Casarin <jb55@jb55.com>
2024-04-17 15:04:38 -07:00
William Casarin 8fbc9dc773 nwc: disable mutinywallet button for now
async nwc with mutiny is wayyy too complicated for the average user.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-04-13 17:51:00 -07:00
95 changed files with 1160 additions and 802 deletions
+29 -16
View File
@@ -36,6 +36,7 @@
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */; };
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; };
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
@@ -281,7 +282,6 @@
4CA927632A290EB10098A105 /* EventTop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927622A290EB10098A105 /* EventTop.swift */; };
4CA927652A290F1A0098A105 /* TimeDot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927642A290F1A0098A105 /* TimeDot.swift */; };
4CA927672A290F8B0098A105 /* RelativeTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927662A290F8B0098A105 /* RelativeTime.swift */; };
4CA9276A2A290FC00098A105 /* ContextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927692A290FC00098A105 /* ContextButton.swift */; };
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9276B2A2910D10098A105 /* ReplyPart.swift */; };
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
@@ -318,7 +318,6 @@
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; };
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
@@ -444,8 +443,6 @@
BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; };
BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; };
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; };
BA4AB0AE2A63B9270070A32A /* AddEmojiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */; };
BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
@@ -639,6 +636,7 @@
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 */; };
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 */; };
E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */; };
@@ -740,6 +738,9 @@
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CondensedProfilePicturesView.swift; sourceTree = "<group>"; };
3A47CB772BDA05A200728A7C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A47CB782BDA05A200728A7C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
3A47CB792BDA05A200728A7C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -1193,7 +1194,6 @@
4CA927622A290EB10098A105 /* EventTop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTop.swift; sourceTree = "<group>"; };
4CA927642A290F1A0098A105 /* TimeDot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeDot.swift; sourceTree = "<group>"; };
4CA927662A290F8B0098A105 /* RelativeTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelativeTime.swift; sourceTree = "<group>"; };
4CA927692A290FC00098A105 /* ContextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextButton.swift; sourceTree = "<group>"; };
4CA9276B2A2910D10098A105 /* ReplyPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyPart.swift; sourceTree = "<group>"; };
4CA9276D2A2A5D110098A105 /* wasm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = wasm.h; sourceTree = "<group>"; };
4CA9276E2A2A5D110098A105 /* wasm.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = wasm.c; sourceTree = "<group>"; };
@@ -1237,7 +1237,6 @@
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; };
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
@@ -1366,8 +1365,6 @@
BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = "<group>"; };
BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = "<group>"; };
BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = "<group>"; };
BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
@@ -1433,6 +1430,7 @@
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>"; };
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>"; };
E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bech32ObjectTests.swift; sourceTree = "<group>"; };
@@ -1469,6 +1467,7 @@
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */,
3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1722,8 +1721,7 @@
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */,
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */,
5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */,
BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */,
BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */,
D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
@@ -2288,7 +2286,6 @@
4CA927622A290EB10098A105 /* EventTop.swift */,
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */,
4CA927662A290F8B0098A105 /* RelativeTime.swift */,
4CA927692A290FC00098A105 /* ContextButton.swift */,
4CA9276B2A2910D10098A105 /* ReplyPart.swift */,
5C7389B02B6EFA7100781E0A /* ProxyView.swift */,
);
@@ -2399,7 +2396,6 @@
isa = PBXGroup;
children = (
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */,
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */,
4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */,
);
path = Search;
@@ -2553,6 +2549,7 @@
E06336A92B75832100A88E6B /* ImageMetadataTest.swift */,
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */,
D72927AC2BAB515C00F93E90 /* RelayURLTests.swift */,
D7831AF72BBE11E2005DA780 /* VideoCacheTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -2825,6 +2822,7 @@
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
4C27C9312A64766F007DBC75 /* MarkdownUI */,
3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -2934,6 +2932,7 @@
"es-419",
"es-ES",
fa,
fi,
fr,
"hu-HU",
id,
@@ -2962,6 +2961,7 @@
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */,
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */,
3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -3075,7 +3075,6 @@
4C32B9572A9AD44700DC3548 /* Root.swift in Sources */,
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
504323A72A34915F006AE6DC /* RelayModel.swift in Sources */,
4CA9276A2A290FC00098A105 /* ContextButton.swift in Sources */,
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */,
D7EDED1C2B1178FE0018B19C /* NoteContent.swift in Sources */,
@@ -3106,6 +3105,7 @@
D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */,
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */,
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */,
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
@@ -3131,7 +3131,6 @@
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
BA4AB0AE2A63B9270070A32A /* AddEmojiView.swift in Sources */,
4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
@@ -3174,7 +3173,6 @@
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
4C7D09602A098C5D00943473 /* WalletView.swift in Sources */,
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */,
B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
@@ -3251,7 +3249,6 @@
4C2B7BF22A71B6540049DEE7 /* Id.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */,
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */,
4C463CBF2B960B96008A8C36 /* PurpleBackdrop.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
@@ -3763,6 +3760,7 @@
3AC59CA929CDDB78007E04A6 /* pt-BR */,
3A821C4029E819D500B4BCA7 /* fr */,
3ABACEC02A5B3ED10037A847 /* sw */,
3A47CB792BDA05A200728A7C /* fi */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -3798,6 +3796,7 @@
3AC59CA829CDDB78007E04A6 /* pt-BR */,
3A821C3F29E819D500B4BCA7 /* fr */,
3ABACEBF2A5B3ED10037A847 /* sw */,
3A47CB772BDA05A200728A7C /* fi */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -3834,6 +3833,7 @@
3AC59CA729CDDB78007E04A6 /* pt-BR */,
3A821C3E29E819D500B4BCA7 /* fr */,
3ABACEC12A5B3ED10037A847 /* sw */,
3A47CB782BDA05A200728A7C /* fi */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -4260,6 +4260,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/izyumkin/MCEmojiPicker";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.3;
};
};
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher";
@@ -4303,6 +4311,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */ = {
isa = XCSwiftPackageProductDependency;
package = 3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */;
productName = MCEmojiPicker;
};
4C06670328FC7EC500038D2A /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */;
@@ -18,6 +18,15 @@
"version" : "7.6.1"
}
},
{
"identity" : "mcemojipicker",
"kind" : "remoteSourceControl",
"location" : "https://github.com/izyumkin/MCEmojiPicker",
"state" : {
"revision" : "e0b4903b75ae1cc418d276d84d1cb946b8a1d73c",
"version" : "1.2.3"
}
},
{
"identity" : "secp256k1.swift",
"kind" : "remoteSourceControl",
@@ -35,7 +35,7 @@ struct SearchHeaderView: View {
}
var SearchText: Text {
Text(verbatim: described.description)
Text(described.description)
}
var body: some View {
@@ -83,9 +83,9 @@ struct SingleCharacterAvatar: View {
var body: some View {
NonImageAvatar {
Text(verbatim: character)
Text(character)
.font(.largeTitle.bold())
.mask(Text(verbatim: character)
.mask(Text(character)
.font(.largeTitle.bold()))
}
}
@@ -192,7 +192,7 @@ struct UserStatusSheet: View {
Picker(NSLocalizedString("Duration", comment: "Label for profile status expiration duration picker."), selection: $duration) {
ForEach(StatusDuration.allCases, id: \.self) { d in
Text(verbatim: d.description)
Text(d.description)
.tag(d)
}
}
+2 -1
View File
@@ -29,7 +29,8 @@ struct SupporterBadge: View {
.frame(width:size, height:size)
.foregroundStyle(GoldGradient)
if self.style == .full {
Text(verbatim: format_date(date: purple_account.created_at, time_style: .none))
let date = format_date(date: purple_account.created_at, time_style: .none)
Text(date)
.foregroundStyle(.secondary)
.font(.caption)
}
+7 -4
View File
@@ -679,10 +679,7 @@ struct ContentView: View {
let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
// dumb stuff needed for property wrappers
UserSettingsStore.pubkey = pubkey
let settings = UserSettingsStore()
UserSettingsStore.shared = settings
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays {
@@ -835,6 +832,12 @@ func save_last_event(_ ev: NostrEvent, timeline: Timeline) {
UserDefaults.standard.set(String(ev.created_at), forKey: "last_\(str)_time")
}
func save_last_event(_ ev_id: NoteId, created_at: UInt32, timeline: Timeline) {
let str = timeline.rawValue
UserDefaults.standard.set(ev_id.hex(), forKey: "last_\(str)")
UserDefaults.standard.set(String(created_at), forKey: "last_\(str)_time")
}
func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
return filters.map { filter in
+12 -2
View File
@@ -7,7 +7,6 @@
import Foundation
class Contacts {
private var friends: Set<Pubkey> = Set()
private var friend_of_friends: Set<Pubkey> = Set()
@@ -15,7 +14,13 @@ class Contacts {
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
let our_pubkey: Pubkey
var event: NostrEvent?
var delegate: ContactsDelegate? = nil
var event: NostrEvent? {
didSet {
guard let event else { return }
self.delegate?.latest_contact_event_changed(new_event: event)
}
}
init(our_pubkey: Pubkey) {
self.our_pubkey = our_pubkey
@@ -88,3 +93,8 @@ class Contacts {
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
}
}
/// Delegate protocol for `Contacts`. Use this to listen to significant updates from a `Contacts` instance
protocol ContactsDelegate {
func latest_contact_event_changed(new_event: NostrEvent)
}
+40 -5
View File
@@ -41,11 +41,15 @@ enum HomeResubFilter {
}
}
class HomeModel {
class HomeModel: ContactsDelegate {
// Don't trigger a user notification for events older than a certain age
static let event_max_age_for_notification: TimeInterval = EVENT_MAX_AGE_FOR_NOTIFICATION
var damus_state: DamusState
var damus_state: DamusState {
didSet {
self.load_our_stuff_from_damus_state()
}
}
// NDBTODO: let's get rid of this entirely, let nostrdb handle it
var has_event: [String: Set<NoteId>] = [:]
@@ -109,6 +113,32 @@ class HomeModel {
}
}
// MARK: - Loading items from DamusState
/// This is called whenever DamusState gets set. This function is used to load or setup anything we need from the new DamusState
func load_our_stuff_from_damus_state() {
self.load_latest_contact_event_from_damus_state()
}
/// This loads the latest contact event we have on file from NostrDB. This should be called as soon as we get the new DamusState
/// Loading the latest contact list event into our `Contacts` instance from storage is important to avoid getting into weird states when the network is unreliable or when relays delete such information
func load_latest_contact_event_from_damus_state() {
guard let latest_contact_event_id_hex = damus_state.settings.latest_contact_event_id_hex else { return }
guard let latest_contact_event_id = NoteId(hex: latest_contact_event_id_hex) else { return }
guard let latest_contact_event: NdbNote = damus_state.ndb.lookup_note( latest_contact_event_id)?.unsafeUnownedValue?.to_owned() else { return }
process_contact_event(state: damus_state, ev: latest_contact_event)
damus_state.contacts.delegate = self
}
// MARK: - ContactsDelegate functions
func latest_contact_event_changed(new_event: NostrEvent) {
// When the latest user contact event has changed, save its ID so we know exactly where to find it next time
damus_state.settings.latest_contact_event_id_hex = new_event.id.hex()
}
// MARK: - Nostr event and subscription handling
func resubscribe(_ resubbing: Resubscribe) {
if self.should_debounce_dms {
// don't resub on initial load
@@ -279,9 +309,14 @@ class HomeModel {
@MainActor
func handle_damus_app_notification(_ notification: DamusAppNotification) async {
if self.notifications.insert_app_notification(notification: notification) {
// If we successfully inserted a new Damus App notification, switch ON the Damus App notification bit on our NewsEventsBits
// This will cause the bell icon on the tab bar to display the purple dot indicating there is an unread notification
self.notification_status.new_events = NewEventsBits(rawValue: self.notification_status.new_events.rawValue | NewEventsBits.damus_app_notifications.rawValue)
let last_notification = get_last_event(.notifications)
if last_notification == nil || last_notification!.created_at < notification.last_event_at {
save_last_event(NoteId.empty, created_at: notification.last_event_at, timeline: .notifications)
// If we successfully inserted a new Damus App notification, switch ON the Damus App notification bit on our NewsEventsBits
// This will cause the bell icon on the tab bar to display the purple dot indicating there is an unread notification
self.notification_status.new_events = NewEventsBits(rawValue: self.notification_status.new_events.rawValue | NewEventsBits.damus_app_notifications.rawValue)
}
return
}
}
+26
View File
@@ -49,6 +49,32 @@ enum MediaUpload {
return false
}
var mime_type: String {
switch self.file_extension {
case "jpg", "jpeg":
return "image/jpg"
case "png":
return "image/png"
case "gif":
return "image/gif"
case "tiff", "tif":
return "image/tiff"
case "mp4":
return "video/mp4"
case "ogg":
return "video/ogg"
case "webm":
return "video/webm"
default:
switch self {
case .image:
return "image/jpg"
case .video:
return "video/mp4"
}
}
}
}
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
+5
View File
@@ -14,6 +14,7 @@ struct LongformEvent {
var image: URL? = nil
var summary: String? = nil
var published_at: Date? = nil
var labels: [String]? = nil
static func parse(from ev: NostrEvent) -> LongformEvent {
var longform = LongformEvent(event: ev)
@@ -26,6 +27,10 @@ struct LongformEvent {
case "summary": longform.summary = tag[1].string()
case "published_at":
longform.published_at = Double(tag[1].string()).map { d in Date(timeIntervalSince1970: d) }
case "t":
if (longform.labels?.append(tag[1].string())) == nil {
longform.labels = [tag[1].string()]
}
default:
break
}
+14
View File
@@ -96,6 +96,14 @@ class UserSettingsStore: ObservableObject {
static var shared: UserSettingsStore? = nil
static var bool_options = Set<String>()
static func globally_load_for(pubkey: Pubkey) -> UserSettingsStore {
// dumb stuff needed for property wrappers
UserSettingsStore.pubkey = pubkey
let settings = UserSettingsStore()
UserSettingsStore.shared = settings
return settings
}
@StringSetting(key: "default_wallet", default_value: .system_default_wallet)
var default_wallet: Wallet
@@ -312,6 +320,12 @@ class UserSettingsStore: ObservableObject {
return internal_winetranslate_api_key != nil
}
}
// MARK: Internal, hidden settings
@Setting(key: "latest_contact_event_id", default_value: nil)
var latest_contact_event_id_hex: String?
}
func pk_setting_key(_ pubkey: Pubkey, key: String) -> String {
+114
View File
@@ -0,0 +1,114 @@
//
// VideoCache.swift
// damus
//
// Created by Daniel D'Aquino on 2024-04-01.
//
import Foundation
import CryptoKit
// Default expiry time of only 1 day to prevent using too much storage
fileprivate let DEFAULT_EXPIRY_TIME: TimeInterval = 60*60*24
// Default cache directory is in the system-provided caches directory, so that the operating system can delete files when it needs storage space
// (https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)
fileprivate let DEFAULT_CACHE_DIRECTORY_PATH: URL? = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("video_cache")
struct VideoCache {
private let cache_url: URL
private let expiry_time: TimeInterval
static let standard: VideoCache? = try? VideoCache()
init?(cache_url: URL? = nil, expiry_time: TimeInterval = DEFAULT_EXPIRY_TIME) throws {
guard let cache_url_to_apply = cache_url ?? DEFAULT_CACHE_DIRECTORY_PATH else { return nil }
self.cache_url = cache_url_to_apply
self.expiry_time = expiry_time
// Create the cache directory if it doesn't exist
do {
try FileManager.default.createDirectory(at: self.cache_url, withIntermediateDirectories: true, attributes: nil)
} catch {
Log.error("Could not create cache directory: %s", for: .storage, error.localizedDescription)
throw error
}
}
/// Checks for a cached video and returns its URL if available, otherwise downloads and caches the video.
func maybe_cached_url_for(video_url: URL) throws -> URL {
let cached_url = url_to_cached_url(url: video_url)
if FileManager.default.fileExists(atPath: cached_url.path) {
// Check if the cached video has expired
let file_attributes = try FileManager.default.attributesOfItem(atPath: cached_url.path)
if let modification_date = file_attributes[.modificationDate] as? Date, Date().timeIntervalSince(modification_date) <= expiry_time {
// Video is not expired
return cached_url
} else {
Task {
// Video is expired, delete and re-download on the background
try FileManager.default.removeItem(at: cached_url)
return try await download_and_cache_video(from: video_url)
}
return video_url
}
} else {
Task {
// Video is not cached, download and cache on the background
return try await download_and_cache_video(from: video_url)
}
return video_url
}
}
/// Downloads video content using URLSession and caches it to disk.
private func download_and_cache_video(from url: URL) async throws -> URL {
let (data, response) = try await URLSession.shared.data(from: url)
guard let http_response = response as? HTTPURLResponse,
200..<300 ~= http_response.statusCode else {
throw URLError(.badServerResponse)
}
let destination_url = url_to_cached_url(url: url)
try data.write(to: destination_url)
return destination_url
}
func url_to_cached_url(url: URL) -> URL {
let hashed_url = hash_url(url)
let file_extension = url.pathExtension
return self.cache_url.appendingPathComponent(hashed_url + "." + file_extension)
}
/// Deletes all cached videos older than the expiry time.
func periodic_purge(completion: ((Error?) -> Void)? = nil) {
DispatchQueue.global(qos: .background).async {
Log.info("Starting periodic video cache purge", for: .storage)
let file_manager = FileManager.default
do {
let cached_files = try file_manager.contentsOfDirectory(at: self.cache_url, includingPropertiesForKeys: [.contentModificationDateKey], options: .skipsHiddenFiles)
for file in cached_files {
let attributes = try file.resourceValues(forKeys: [.contentModificationDateKey])
if let modification_date = attributes.contentModificationDate, Date().timeIntervalSince(modification_date) > self.expiry_time {
try file_manager.removeItem(at: file)
}
}
DispatchQueue.main.async {
completion?(nil)
}
} catch {
DispatchQueue.main.async {
completion?(error)
}
}
}
}
/// Hashes the URL using SHA-256
private func hash_url(_ url: URL) -> String {
let data = Data(url.absoluteString.utf8)
let hashed_data = SHA256.hash(data: data)
return hashed_data.compactMap { String(format: "%02x", $0) }.joined()
}
}
+13 -9
View File
@@ -227,18 +227,22 @@ class RelayPool {
request_queue.append(QueuedRequest(req: r, relay: relay, skip_ephemeral: skip_ephemeral))
}
func send_raw_to_local_ndb(_ req: NostrRequestType) {
// send to local relay (nostrdb)
switch req {
case .typical(let r):
if case .event = r, let rstr = make_nostr_req(r) {
let _ = ndb.process_client_event(rstr)
}
case .custom(let string):
let _ = ndb.process_client_event(string)
}
}
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
let relays = to.map{ get_relays($0) } ?? self.relays
// send to local relay (nostrdb)
switch req {
case .typical(let r):
if case .event = r, let rstr = make_nostr_req(r) {
let _ = ndb.process_client_event(rstr)
}
case .custom(let string):
let _ = ndb.process_client_event(string)
}
self.send_raw_to_local_ndb(req)
for relay in relays {
if req.is_read && !(relay.descriptor.info.read ?? true) {
+1 -1
View File
@@ -17,7 +17,7 @@ class CompatibleText: Equatable {
return AnyView(
VStack {
Image("warning")
Text(NSLocalizedString("This note contains too many items and cannot be rendered", comment: "Error message indicating that a note is too big and cannot be rendered"))
Text("This note contains too many items and cannot be rendered", comment: "Error message indicating that a note is too big and cannot be rendered")
.multilineTextAlignment(.center)
}
.foregroundColor(.secondary)
+5
View File
@@ -240,6 +240,11 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSet
return false
}
// don't translate reposts, longform, etc
if event.kind != 1 {
return false;
}
// Do not translate self-authored notes if logged in with a private key
// as we can assume the user can understand their own notes.
// The detected language prediction could be incorrect and not in the list of preferred languages.
+3 -3
View File
@@ -30,7 +30,7 @@ func processImage(image: UIImage) -> URL? {
}
fileprivate func processImage(source: CGImageSource, fileExtension: String) -> URL? {
let destinationURL = createMediaURL(fileExtension: fileExtension)
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: fileExtension)
guard let destination = removeGPSDataFromImage(source: source, url: destinationURL) else { return nil }
@@ -45,7 +45,7 @@ func processVideo(videoURL: URL) -> URL? {
}
fileprivate func saveVideoToTemporaryFolder(videoURL: URL) -> URL? {
let destinationURL = createMediaURL(fileExtension: videoURL.pathExtension)
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: videoURL.pathExtension)
do {
try FileManager.default.copyItem(at: videoURL, to: destinationURL)
@@ -57,7 +57,7 @@ fileprivate func saveVideoToTemporaryFolder(videoURL: URL) -> URL? {
}
/// Generate a temporary URL with a unique filename
fileprivate func createMediaURL(fileExtension: String) -> URL {
func generateUniqueTemporaryMediaURL(fileExtension: String) -> URL {
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let uniqueMediaName = "\(UUID().uuidString).\(fileExtension)"
let temporaryMediaURL = temporaryDirectoryURL.appendingPathComponent(uniqueMediaName)
+1
View File
@@ -15,6 +15,7 @@ enum LogCategory: String {
case storage
case push_notifications
case damus_purple
case image_uploading
}
/// Damus structured logger
+5
View File
@@ -30,6 +30,7 @@ enum Route: Hashable {
case ReactionsSettings(settings: UserSettingsStore)
case SearchSettings(settings: UserSettingsStore)
case DeveloperSettings(settings: UserSettingsStore)
case FirstAidSettings(settings: UserSettingsStore)
case Thread(thread: ThreadModel)
case Reposts(reposts: EventsModel)
case QuoteReposts(quotes: EventsModel)
@@ -89,6 +90,8 @@ enum Route: Hashable {
SearchSettingsView(settings: settings)
case .DeveloperSettings(let settings):
DeveloperSettingsView(settings: settings)
case .FirstAidSettings(settings: let settings):
FirstAidSettingsView(damus_state: damusState, settings: settings)
case .Thread(let thread):
ThreadView(state: damusState, thread: thread)
case .Reposts(let reposts):
@@ -175,6 +178,8 @@ enum Route: Hashable {
hasher.combine("searchSettings")
case .DeveloperSettings:
hasher.combine("developerSettings")
case .FirstAidSettings:
hasher.combine("firstAidSettings")
case .Thread(let threadModel):
hasher.combine("thread")
hasher.combine(threadModel.event.id)
-3
View File
@@ -411,9 +411,6 @@ func get_zap_description(_ ev: NostrEvent, inv_desc: InvoiceDescription) -> Stri
guard let data = desc.data(using: .utf8) else {
return nil
}
guard sha256(data) == deschash else {
return nil
}
return desc
}
+30 -106
View File
@@ -6,8 +6,7 @@
//
import SwiftUI
import UIKit
import MCEmojiPicker
struct EventActionBar: View {
let damus_state: DamusState
@@ -20,6 +19,8 @@ struct EventActionBar: View {
@State var show_share_action: Bool = false
@State var show_repost_action: Bool = false
@State private var isOnTopHalfOfScreen: Bool = false
@ObservedObject var bar: ActionBarModel
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil) {
@@ -72,7 +73,7 @@ struct EventActionBar: View {
Spacer()
HStack(spacing: 4) {
LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil) { emoji in
LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil, isOnTopHalfOfScreen: $isOnTopHalfOfScreen) { emoji in
if bar.liked {
//notify(.delete, bar.our_like)
} else {
@@ -135,6 +136,20 @@ struct EventActionBar: View {
self.bar.our_like = liked.event
}
}
.background(
GeometryReader { geometry in
EmptyView()
.onAppear {
let eventActionBarY = geometry.frame(in: .global).midY
let screenMidY = UIScreen.main.bounds.midY
self.isOnTopHalfOfScreen = eventActionBarY > screenMidY
}
.onChange(of: geometry.frame(in: .global).midY) { newY in
let screenMidY = UIScreen.main.bounds.midY
self.isOnTopHalfOfScreen = newY > screenMidY
}
}
)
}
func send_like(emoji: String) {
@@ -168,15 +183,17 @@ struct LikeButton: View {
let damus_state: DamusState
let liked: Bool
let liked_emoji: String?
@Binding var isOnTopHalfOfScreen: Bool
let action: (_ emoji: String) -> Void
// For reactions background
@State private var showReactionsBG = 0
@State private var showEmojis: [Int] = []
@State private var rotateThumb = -45
@State private var isReactionsVisible = false
@State private var selectedEmoji: String = ""
// Following four are Shaka animation properties
let timer = Timer.publish(every: 0.10, on: .main, in: .common).autoconnect()
@State private var shouldAnimate = false
@@ -228,7 +245,15 @@ struct LikeButton: View {
amountOfAngleIncrease = 20.0
}
})
.overlay(reactionsOverlay())
.emojiPicker(
isPresented: $isReactionsVisible,
selectedEmoji: $selectedEmoji,
arrowDirection: isOnTopHalfOfScreen ? .down : .up,
isDismissAfterChoosing: true
)
.onChange(of: selectedEmoji) { newSelectedEmoji in
self.action(newSelectedEmoji)
}
}
func shakaAnimationLogic() {
@@ -251,110 +276,11 @@ struct LikeButton: View {
}
}
func reactionsOverlay() -> some View {
Group {
if isReactionsVisible {
ZStack {
RoundedRectangle(cornerRadius: 20)
.frame(width: calculateOverlayWidth(), height: 50)
.foregroundColor(DamusColors.black)
.scaleEffect(Double(showReactionsBG), anchor: .topTrailing)
.animation(
.interpolatingSpring(stiffness: 170, damping: 15).delay(0.05),
value: showReactionsBG
)
.overlay(
Rectangle()
.foregroundColor(Color.white.opacity(0.2))
.frame(width: calculateOverlayWidth(), height: 50)
.clipShape(
RoundedRectangle(cornerRadius: 20)
)
)
.overlay(reactions())
}
.offset(y: -40)
.onTapGesture {
withAnimation(.easeOut(duration: 0.2)) {
isReactionsVisible = false
showReactionsBG = 0
}
showEmojis = []
}
} else {
EmptyView()
}
}
}
func calculateOverlayWidth() -> CGFloat {
let maxWidth: CGFloat = 250
let numberOfEmojis = emojis.count
let minimumWidth: CGFloat = 75
if numberOfEmojis > 0 {
let emojiWidth: CGFloat = 25
let padding: CGFloat = 15
let buttonWidth: CGFloat = 18
let buttonPadding: CGFloat = 20
let totalWidth = CGFloat(numberOfEmojis) * (emojiWidth + padding) + buttonWidth + buttonPadding
return min(maxWidth, max(minimumWidth, totalWidth))
} else {
return minimumWidth
}
}
func reactions() -> some View {
HStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 15) {
ForEach(emojis, id: \.self) { emoji in
if let index = emojis.firstIndex(of: emoji) {
let scale = index < showEmojis.count ? showEmojis[index] : 0
Text(emoji)
.font(.system(size: 25))
.scaleEffect(Double(scale))
.onTapGesture {
emojiTapped(emoji)
}
}
}
}
.padding(.leading, 10)
}
Button(action: {
withAnimation(.easeOut(duration: 0.2)) {
isReactionsVisible = false
showReactionsBG = 0
}
showEmojis = []
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 18))
.foregroundColor(.gray)
}
.padding(.trailing, 7.5)
}
}
// When reaction button is long pressed, it displays the multiple emojis overlay and displays the user's selected emojis with an animation
private func reactionLongPressed() {
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
showEmojis = Array(repeating: 0, count: emojis.count) // Initialize the showEmojis array
for (index, _) in emojis.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1 * Double(index)) {
withAnimation(.interpolatingSpring(stiffness: 170, damping: 8)) {
if index < showEmojis.count {
showEmojis[index] = 1
}
}
}
}
isReactionsVisible = true
showReactionsBG = 1
}
private func emojiTapped(_ emoji: String) {
@@ -364,9 +290,7 @@ struct LikeButton: View {
withAnimation(.easeOut(duration: 0.2)) {
isReactionsVisible = false
showReactionsBG = 0
}
showEmojis = []
withAnimation(Animation.easeOut(duration: 0.15)) {
shouldAnimate = true
@@ -36,7 +36,7 @@ struct ShareActionButton: View {
.frame(width: 55.0, height: 55.0)
}
.frame(height: 25)
Text(verbatim: text)
Text(text)
.foregroundColor(col)
.font(.footnote)
.multilineTextAlignment(.center)
+1 -1
View File
@@ -121,7 +121,7 @@ struct AddRelayView: View {
dismiss()
}) {
HStack {
Text(verbatim: "Add relay")
Text("Add relay", comment: "Button to add a relay.")
.bold()
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
+1 -1
View File
@@ -17,7 +17,7 @@ enum ImageUploadResult {
fileprivate func create_upload_body(mediaData: Data, boundary: String, mediaUploader: MediaUploader, mediaToUpload: MediaUpload) -> Data {
let body = NSMutableData();
let contentType = mediaToUpload.is_image ? "image/jpg" : "video/mp4"
let contentType = mediaToUpload.mime_type
body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n")
body.appendString(string: "--\(boundary)\r\n")
body.appendString(string: "Content-Disposition: form-data; name=\(mediaUploader.nameParam); filename=\(mediaToUpload.genericFileName)\r\n")
+1 -1
View File
@@ -33,7 +33,7 @@ struct BookmarksView: View {
.resizable()
.scaledToFit()
.frame(width: 32.0, height: 32.0)
Text(NSLocalizedString("You have no bookmarks yet, add them in the context menu", comment: "Text indicating that there are no bookmarks to be viewed"))
Text("You have no bookmarks yet, add them in the context menu", comment: "Text indicating that there are no bookmarks to be viewed")
}
} else {
ScrollView {
@@ -22,7 +22,8 @@ struct GradientFollowButton: View {
Button(action: {
follow_state = perform_follow_btn_action(follow_state, target: target)
}) {
Text(follow_btn_txt(follow_state, follows_you: follows_you))
let followButtonText = follow_btn_txt(follow_state, follows_you: follows_you)
Text(followButtonText)
.foregroundColor(follow_state == .unfollows ? .white : grayTextColor)
.font(.callout)
.fontWeight(.medium)
+5 -1
View File
@@ -67,6 +67,10 @@ struct ConfigView: View {
NavigationLink(value: Route.DeveloperSettings(settings: settings)) {
IconLabel(NSLocalizedString("Developer", comment: "Section header for developer settings"), img_name: "magic-stick2.fill", color: DamusColors.adaptableBlack)
}
NavigationLink(value: Route.FirstAidSettings(settings: settings)) {
IconLabel(NSLocalizedString("First Aid", comment: "Section header for first aid tools and settings"), img_name: "help2", color: .red)
}
}
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
@@ -84,7 +88,7 @@ struct ConfigView: View {
}
if state.is_privkey_user {
Section(header: Text(NSLocalizedString("Permanently Delete Account", comment: "Section title for deleting the user"))) {
Section(header: Text("Permanently Delete Account", comment: "Section title for deleting the user")) {
Button(action: {
delete_account_warning = true
}, label: {
+1 -1
View File
@@ -28,7 +28,7 @@ struct CreateAccountView: View {
VStack(alignment: .center) {
EditPictureControl(uploader: .nostrBuild, pubkey: account.pubkey, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture)
Text(NSLocalizedString("Public Key", comment: "Label to indicate the public key of the account."))
Text("Public Key", comment: "Label to indicate the public key of the account.")
.bold()
.padding()
.onTapGesture {
@@ -1,20 +0,0 @@
//
// ContextButton.swift
// damus
//
// Created by William Casarin on 2023-06-01.
//
import SwiftUI
struct ContextButton: View {
var body: some View {
Text(verbatim: /*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct ContextButton_Previews: PreviewProvider {
static var previews: some View {
ContextButton()
}
}
@@ -36,7 +36,7 @@ struct ProxyView: View {
HStack {
let protocolLogo = get_protocol_image(protocolName: proxy.protocolName)
if protocolLogo.isEmpty {
Text("\(proxy.protocolName)")
Text(proxy.protocolName)
.font(.caption)
} else {
Image(protocolLogo)
+1 -1
View File
@@ -29,7 +29,7 @@ struct EventBody: View {
var body: some View {
if event.known_kind == .longform {
LongformPreviewBody(state: damus_state, ev: event, options: options)
LongformPreviewBody(state: damus_state, ev: event, options: options, header: true)
// truncated longform bodies are just the preview
if !options.contains(.truncate_content) {
+126 -11
View File
@@ -6,25 +6,31 @@
//
import SwiftUI
import Kingfisher
struct LongformPreviewBody: View {
let state: DamusState
let event: LongformEvent
let options: EventViewOptions
let header: Bool
@State var blur_images: Bool = true
@ObservedObject var artifacts: NoteArtifactsModel
init(state: DamusState, ev: LongformEvent, options: EventViewOptions) {
init(state: DamusState, ev: LongformEvent, options: EventViewOptions, header: Bool) {
self.state = state
self.event = ev
self.options = options
self.header = header
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model)
}
init(state: DamusState, ev: NostrEvent, options: EventViewOptions) {
init(state: DamusState, ev: NostrEvent, options: EventViewOptions, header: Bool) {
self.state = state
self.event = LongformEvent.parse(from: ev)
self.options = options
self.header = header
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model)
}
@@ -34,6 +40,67 @@ struct LongformPreviewBody: View {
return Text(wordCount)
}
var truncate: Bool {
return options.contains(.truncate_content)
}
var truncate_very_short: Bool {
return options.contains(.truncate_content_very_short)
}
func truncatedText(content: CompatibleText) -> some View {
Group {
if truncate_very_short {
TruncatedText(text: content, maxChars: 140)
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
}
else if truncate {
TruncatedText(text: content)
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
} else {
content.text
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
}
}
}
func Placeholder(url: URL) -> some View {
Group {
if let meta = state.events.lookup_img_metadata(url: url),
case .processed(let blurhash) = meta.state {
Image(uiImage: blurhash)
.resizable()
.frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150)
} else {
DamusColors.adaptableWhite
}
}
}
func titleImage(url: URL) -> some View {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: state.settings.disable_animation)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.background {
Placeholder(url: url)
}
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150)
.cornerRadius(1)
}
var body: some View {
Group {
if options.contains(.wide) {
@@ -46,23 +113,71 @@ struct LongformPreviewBody: View {
var Main: some View {
VStack(alignment: .leading, spacing: 10) {
if let title = event.title {
Text(title)
.font(.title)
} else {
Text("Untitled", comment: "Text indicating that the long-form note title is untitled.")
.font(.title)
if let url = event.image {
if (self.options.contains(.no_media)) {
EmptyView()
} else if !blur_images || (!blur_images && !state.settings.media_previews) {
titleImage(url: url)
} else if blur_images || (blur_images && !state.settings.media_previews) {
ZStack {
titleImage(url: url)
Blur()
.onTapGesture {
blur_images = false
}
}
}
}
Text(event.title ?? "Untitled")
.font(header ? .title : .headline)
.padding(.horizontal, 10)
.padding(.top, 5)
if let summary = event.summary {
truncatedText(content: CompatibleText(stringLiteral: summary))
}
if let labels = event.labels {
ScrollView(.horizontal) {
HStack {
ForEach(labels, id: \.self) { label in
Text(label)
.font(.caption)
.foregroundColor(.gray)
.padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))
.background(DamusColors.neutral1)
.cornerRadius(20)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
}
}
}
.scrollIndicators(.hidden)
.padding(10)
}
Text(event.summary ?? "")
.foregroundColor(.gray)
if case .loaded(let arts) = artifacts.state,
case .longform(let longform) = arts
{
Words(longform.words).font(.footnote)
.padding([.horizontal, .bottom], 10)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.background(DamusColors.neutral3)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral1, lineWidth: 1)
)
.padding(.top, 10)
.onAppear {
blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event.event, our_pubkey: state.pubkey)
}
}
}
@@ -79,7 +194,7 @@ struct LongformPreview: View {
var body: some View {
EventShell(state: state, event: event.event, options: options) {
LongformPreviewBody(state: state, ev: event, options: options)
LongformPreviewBody(state: state, ev: event, options: options, header: false)
}
}
}
@@ -145,7 +145,7 @@ struct FullScreenCarouselView_Previews: PreviewProvider {
HStack {
Spacer()
Text("Some content")
Text(verbatim: "Some content")
.padding()
.foregroundColor(.white)
+23 -1
View File
@@ -36,7 +36,29 @@ struct MediaPicker: UIViewControllerRepresentable {
result.itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (item, error) in
guard let url = item as? URL else { return }
if canGetSourceTypeFromUrl(url: url) {
if(url.pathExtension == "gif") {
// GIFs do not natively support location metadata (See https://superuser.com/a/556320 and https://www.w3.org/Graphics/GIF/spec-gif89a.txt)
// It is better to avoid any GPS data processing at all, as it can cause the image to be converted to JPEG.
// Therefore, we should load the file directtly and deliver it as "already processed".
// Load the data for the GIF image
// - Don't load it as an UIImage since that can only get exported into JPEG/PNG
// - Don't load it as a file representation because it gets deleted before the upload can occur
_ = result.itemProvider.loadDataRepresentation(for: .gif, completionHandler: { imageData, error in
guard let imageData else { return }
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: "gif")
do {
try imageData.write(to: destinationURL)
Task {
await self.chooseMedia(.processed_image(destinationURL))
}
}
catch {
Log.error("Failed to write GIF image data from Photo picker into a local copy", for: .image_uploading)
}
})
}
else if canGetSourceTypeFromUrl(url: url) {
// Media was not taken from camera
self.attemptAcquireResourceAndChooseMedia(
url: url,
+2 -2
View File
@@ -17,7 +17,7 @@ struct MuteDurationMenu<T: View>: View {
Button {
action(duration)
} label: {
Text("\(duration.title)")
Text(duration.title)
}
}
} label: {
@@ -30,6 +30,6 @@ struct MuteDurationMenu<T: View>: View {
MuteDurationMenu { _ in
} label: {
Text("Mute hashtag")
Text(verbatim: "Mute hashtag")
}
}
+3 -3
View File
@@ -62,7 +62,7 @@ struct MutelistView: View {
Section(NSLocalizedString("Hashtags", comment: "Section header title for a list of hashtags that are muted.")) {
ForEach(hashtags, id: \.self) { item in
if case let MuteItem.hashtag(hashtag, _) = item {
Text("#\(hashtag.hashtag)")
Text(verbatim: "#\(hashtag.hashtag)")
.id(hashtag.hashtag)
.swipeActions {
RemoveAction(item: .hashtag(hashtag, nil))
@@ -76,7 +76,7 @@ struct MutelistView: View {
Section(NSLocalizedString("Words", comment: "Section header title for a list of words that are muted.")) {
ForEach(words, id: \.self) { item in
if case let MuteItem.word(word, _) = item {
Text("\(word)")
Text(word)
.id(word)
.swipeActions {
RemoveAction(item: .word(word, nil))
@@ -94,7 +94,7 @@ struct MutelistView: View {
RemoveAction(item: .thread(note_id, nil))
}
} else {
Text(NSLocalizedString("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for."))
Text("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for.")
}
}
}
@@ -37,9 +37,9 @@ struct DamusAppNotificationView: View {
.shadow(radius: 5, y: 5)
VStack(alignment: .leading, spacing: 5) {
HStack(alignment: .center, spacing: 3) {
Text(NSLocalizedString("Damus", comment: "Name of the app for the title of an internal notification"))
Text("Damus", comment: "Name of the app for the title of an internal notification")
.font(.body.weight(.bold))
Text("·")
Text(verbatim: "·")
.foregroundStyle(.secondary)
Text(relative_date)
.font(.system(size: 16))
@@ -49,7 +49,7 @@ struct DamusAppNotificationView: View {
Image("check-circle.fill")
.resizable()
.frame(width: 15, height: 15)
Text(NSLocalizedString("Internal app notification", comment: "Badge indicating that a notification is an official internal app notification"))
Text("Internal app notification", comment: "Badge indicating that a notification is an official internal app notification")
.font(.caption2)
.bold()
}
@@ -196,7 +196,7 @@ struct EventGroupView: View {
return VStack(alignment: .center) {
Image("zap.fill")
.foregroundColor(.orange)
Text(verbatim: fmt)
Text(fmt)
.foregroundColor(Color.orange)
}
}
@@ -36,7 +36,7 @@ struct OnboardingSuggestionsView: View {
.navigationBarItems(leading: Button(action: {
self.next_page()
}, label: {
Text(NSLocalizedString("Skip", comment: "Button to dismiss the suggested users screen"))
Text("Skip", comment: "Button to dismiss the suggested users screen")
.font(.subheadline.weight(.semibold))
}))
.tag(0)
@@ -48,7 +48,7 @@ struct OnboardingSuggestionsView: View {
AnyView(
HStack {
Image(systemName: "sparkles")
Text(NSLocalizedString("Add your first post", comment: "Prompt given to the user during onboarding, suggesting them to write their first post"))
Text("Add your first post", comment: "Prompt given to the user during onboarding, suggesting them to write their first post")
}
.foregroundColor(.secondary)
.font(.callout)
@@ -97,7 +97,7 @@ fileprivate struct SuggestedUsersPageView: View {
Button(action: {
self.next_page()
}) {
Text(NSLocalizedString("Continue", comment: "Button to dismiss suggested users view and continue to the main app"))
Text("Continue", comment: "Button to dismiss suggested users view and continue to the main app")
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
+1 -1
View File
@@ -268,7 +268,7 @@ struct PostView: View {
Button(action: {
self.cancel()
}, label: {
Text(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note."))
Text("Cancel", comment: "Button to cancel out of posting a note.")
.padding(10)
})
.buttonStyle(NeutralButtonStyle())
+1 -11
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) {
+1 -1
View File
@@ -199,7 +199,7 @@ struct ProfileView: View {
MuteDurationMenu { duration in
notify(.mute(.user(profile.pubkey, duration?.date_from_now)))
} label: {
Text(NSLocalizedString("Mute", comment: "Button to mute a profile."))
Text("Mute", comment: "Button to mute a profile.")
.foregroundStyle(.red)
}
}
+4 -4
View File
@@ -59,7 +59,7 @@ struct ProfileActionSheetView: View {
}
)
.buttonStyle(NeutralButtonShape.circle.style)
Text(NSLocalizedString("Message", comment: "Button label that allows the user to start a direct message conversation with the user shown on-screen"))
Text("Message", comment: "Button label that allows the user to start a direct message conversation with the user shown on-screen")
.foregroundStyle(.secondary)
.font(.caption)
}
@@ -114,7 +114,7 @@ struct ProfileActionSheetView: View {
label: {
HStack {
Spacer()
Text(NSLocalizedString("View full profile", comment: "A button label that allows the user to see the full profile of the profile they are previewing"))
Text("View full profile", comment: "A button label that allows the user to see the full profile of the profile they are previewing")
Image(systemName: "arrow.up.right")
Spacer()
}
@@ -305,9 +305,9 @@ fileprivate struct ProfileActionSheetZapButton: View {
})
.alert(isPresented: $show_error_alert) {
Alert(
title: Text(NSLocalizedString("Zap failed", comment: "Title of an alert indicating that a zap action failed")),
title: Text("Zap failed", comment: "Title of an alert indicating that a zap action failed"),
message: Text(zap_state.error_message() ?? ""),
dismissButton: .default(Text(NSLocalizedString("OK", comment: "Button label to dismiss an error dialog")))
dismissButton: .default(Text("OK", comment: "Button label to dismiss an error dialog"))
)
}
.onChange(of: zap_state) { new_zap_state in
+1 -1
View File
@@ -57,7 +57,7 @@ struct PubkeyView: View {
.resizable()
.foregroundColor(DamusColors.green)
.frame(width: 20, height: 20)
Text(NSLocalizedString("Copied", comment: "Label indicating that a user's key was copied."))
Text("Copied", comment: "Label indicating that a user's key was copied.")
.font(.footnote)
.layoutPriority(1)
.foregroundColor(DamusColors.green)
@@ -31,9 +31,10 @@ struct DamusPurpleAccountView: View {
// TODO: Generalize this view instead of setting up dividers and paddings manually
VStack {
HStack {
Text(NSLocalizedString("Expiry date", comment: "Label for Purple subscription expiry date"))
Text("Expiry date", comment: "Label for Purple subscription expiry date")
Spacer()
Text(DateFormatter.localizedString(from: account.expiry, dateStyle: .short, timeStyle: .none))
let formattedDate = DateFormatter.localizedString(from: account.expiry, dateStyle: .short, timeStyle: .none)
Text(formattedDate)
}
.padding(.horizontal)
.padding(.top, 20)
@@ -43,9 +44,10 @@ struct DamusPurpleAccountView: View {
.padding(.vertical, 10)
HStack {
Text(NSLocalizedString("Account creation", comment: "Label for Purple account creation date"))
Text("Account creation", comment: "Label for Purple account creation date")
Spacer()
Text(DateFormatter.localizedString(from: account.created_at, dateStyle: .short, timeStyle: .none))
let formattedDate = DateFormatter.localizedString(from: account.created_at, dateStyle: .short, timeStyle: .none)
Text(formattedDate)
}
.padding(.horizontal)
@@ -54,7 +56,7 @@ struct DamusPurpleAccountView: View {
.padding(.vertical, 10)
HStack {
Text(NSLocalizedString("Subscriber number", comment: "Label for Purple account subscriber number"))
Text("Subscriber number", comment: "Label for Purple account subscriber number")
Spacer()
Text(verbatim: "#\(account.subscriber_number)")
}
@@ -90,7 +92,7 @@ struct DamusPurpleAccountView: View {
.resizable()
.frame(width: 15, height: 15)
Text(NSLocalizedString("Active account", comment: "Badge indicating user has an active Damus Purple account"))
Text("Active account", comment: "Badge indicating user has an active Damus Purple account")
.font(.caption)
.bold()
}
@@ -107,7 +109,7 @@ struct DamusPurpleAccountView: View {
.resizable()
.frame(width: 15, height: 15)
Text(NSLocalizedString("Expired account", comment: "Badge indicating user has an expired Damus Purple account"))
Text("Expired account", comment: "Badge indicating user has an expired Damus Purple account")
.font(.caption)
.bold()
}
@@ -61,7 +61,7 @@ struct DamusPurpleTranslationSetupView: View {
.opacity(start ? 1.0 : 0.0)
.animation(.content(), value: start)
Text(NSLocalizedString("You unlocked", comment: "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple" ))
Text("You unlocked", comment: "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple" )
.font(.largeTitle)
.fontWeight(.bold)
.foregroundStyle(
@@ -95,7 +95,7 @@ struct DamusPurpleTranslationSetupView: View {
.opacity(start ? 1.0 : 0.0)
.animation(Animation.snappy(duration: 2).delay(0), value: start)
Text(NSLocalizedString("Automatic translations", comment: "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple"))
Text("Automatic translations", comment: "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple")
.font(.headline)
.fontWeight(.bold)
.foregroundStyle(
@@ -110,7 +110,7 @@ struct DamusPurpleTranslationSetupView: View {
.animation(.content(), value: start)
.padding(.top, 10)
Text(NSLocalizedString("As part of your Damus Purple membership, you get complimentary and automated translations. Would you like to enable Damus Purple translations?\n\nTip: You can always change this later in Settings → Translations", comment: "Message notifying the user that they get auto-translations as part of their service"))
Text("As part of your Damus Purple membership, you get complimentary and automated translations. Would you like to enable Damus Purple translations?\n\nTip: You can always change this later in Settings → Translations", comment: "Message notifying the user that they get auto-translations as part of their service")
.lineSpacing(5)
.multilineTextAlignment(.center)
.foregroundStyle(.white.opacity(0.8))
@@ -125,7 +125,7 @@ struct DamusPurpleTranslationSetupView: View {
}, label: {
HStack {
Spacer()
Text(NSLocalizedString("Enable Purple auto-translations", comment: "Label for button that allows users to enable Damus Purple translations"))
Text("Enable Purple auto-translations", comment: "Label for button that allows users to enable Damus Purple translations")
Spacer()
}
})
@@ -139,7 +139,7 @@ struct DamusPurpleTranslationSetupView: View {
}, label: {
HStack {
Spacer()
Text(NSLocalizedString("No, thanks", comment: "Label for button that allows users to reject enabling Damus Purple translations"))
Text("No, thanks", comment: "Label for button that allows users to reject enabling Damus Purple translations")
Spacer()
}
})
@@ -53,7 +53,7 @@ struct DamusPurpleVerifyNpubView: View {
}, label: {
HStack {
Spacer()
Text(NSLocalizedString("Verify my npub", comment: "Button label to verify the user's npub for the purpose of Purple subscription checkout"))
Text("Verify my npub", comment: "Button label to verify the user's npub for the purpose of Purple subscription checkout")
Spacer()
}
})
@@ -61,7 +61,7 @@ struct DamusPurpleVerifyNpubView: View {
.buttonStyle(GradientButtonStyle())
}
else {
Text(NSLocalizedString("Verified!", comment: "Instructions after the user has verified their npub for Damus Purple purchase checkout"))
Text("Verified!", comment: "Instructions after the user has verified their npub for Damus Purple purchase checkout")
.frame(height: subtitle_height)
.multilineTextAlignment(.center)
.foregroundColor(.green)
@@ -71,7 +71,7 @@ struct DamusPurpleVerifyNpubView: View {
}, label: {
HStack {
Spacer()
Text(NSLocalizedString("Continue", comment: "Prompt to user to continue"))
Text("Continue", comment: "Prompt to user to continue")
Spacer()
}
})
+2 -2
View File
@@ -138,7 +138,7 @@ struct DamusPurpleView: View, DamusPurpleStoreKitManagerDelegate {
if let account_uuid {
DamusPurpleView.IAPProductStateView(products: products, purchased: purchased, account_uuid: account_uuid, subscribe: subscribe)
if let iap_error {
Text(String(format: NSLocalizedString("There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@", comment: "In-app purchase error message for the user"), iap_error))
Text("There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: \(iap_error)", comment: "In-app purchase error message for the user")
.foregroundStyle(.red)
.multilineTextAlignment(.center)
.padding(.horizontal)
@@ -158,7 +158,7 @@ struct DamusPurpleView: View, DamusPurpleStoreKitManagerDelegate {
}
var ManageOnWebsiteNote: some View {
Text(NSLocalizedString("Visit the Damus website on a web browser to manage billing", comment: "Instruction on how to manage billing externally"))
Text("Visit the Damus website on a web browser to manage billing", comment: "Instruction on how to manage billing externally")
.font(.caption)
.foregroundColor(.white.opacity(0.6))
.multilineTextAlignment(.center)
@@ -37,7 +37,7 @@ struct DamusPurpleWelcomeView: View {
.opacity(start ? 1.0 : 0.0)
.animation(.content(), value: start)
Text(NSLocalizedString("Welcome to Purple", comment: "Greeting to subscription service"))
Text("Welcome to Purple", comment: "Greeting to subscription service")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundStyle(
@@ -70,7 +70,7 @@ struct DamusPurpleWelcomeView: View {
.opacity(start ? 1.0 : 0.0)
.animation(Animation.snappy(duration: 2).delay(0), value: start)
Text(NSLocalizedString("Thank you very much for signing up for Damus\u{00A0}Purple. Your contribution helps us continue our fight for a more Open and Free\u{00A0}internet.\n\nYou will also get access to premium features, and a star badge on your profile.\n\nEnjoy!", comment: "Appreciation to user for purchasing subscription service"))
Text("Thank you very much for signing up for Damus\u{00A0}Purple. Your contribution helps us continue our fight for a more Open and Free\u{00A0}internet.\n\nYou will also get access to premium features, and a star badge on your profile.\n\nEnjoy!", comment: "Appreciation to user for purchasing subscription service")
.lineSpacing(5)
.multilineTextAlignment(.center)
.foregroundStyle(.white.opacity(0.8))
@@ -85,7 +85,7 @@ struct DamusPurpleWelcomeView: View {
}, label: {
HStack {
Spacer()
Text(NSLocalizedString("Continue", comment: "Prompt to user to continue"))
Text("Continue", comment: "Prompt to user to continue")
Spacer()
}
})
@@ -26,7 +26,7 @@ extension DamusPurpleView {
var body: some View {
if subscription_purchase_loading {
HStack(spacing: 10) {
Text(NSLocalizedString("Purchasing", comment: "Loading label indicating the purchase action is in progress"))
Text("Purchasing", comment: "Loading label indicating the purchase action is in progress")
.foregroundStyle(.white)
ProgressView()
.progressViewStyle(.circular)
@@ -66,7 +66,7 @@ extension DamusPurpleView {
}
func PurchasedUnmanageableView(_ purchased: PurchasedProduct) -> some View {
Text(NSLocalizedString("This device's in-app purchase is registered to a different Nostr account. Unable to manage this Purple account. If you believe this was a mistake, please contact us via support@damus.io.", comment: "Notice label that user cannot manage their In-App purchases"))
Text("This device's in-app purchase is registered to a different Nostr account. Unable to manage this Purple account. If you believe this was a mistake, please contact us via support@damus.io.", comment: "Notice label that user cannot manage their In-App purchases")
.font(.caption)
.foregroundColor(.white.opacity(0.6))
.multilineTextAlignment(.center)
@@ -76,21 +76,21 @@ extension DamusPurpleView {
func PurchasedManageView(_ purchased: PurchasedProduct) -> some View {
VStack(spacing: 10) {
if SHOW_IAP_DEBUG_INFO == true {
Text(NSLocalizedString("Purchased!", comment: "User purchased a subscription"))
Text("Purchased!", comment: "User purchased a subscription")
.font(.title2)
.foregroundColor(.white)
price_description(product: purchased.product)
.foregroundColor(.white)
.opacity(0.65)
.frame(width: 200)
Text(NSLocalizedString("Purchased on", comment: "Indicating when the user purchased the subscription"))
Text("Purchased on", comment: "Indicating when the user purchased the subscription")
.font(.title2)
.foregroundColor(.white)
Text(format_date(date: purchased.tx.purchaseDate))
.foregroundColor(.white)
.opacity(0.65)
if let expiry = purchased.tx.expirationDate {
Text(NSLocalizedString("Renews on", comment: "Indicating when the subscription will renew"))
Text("Renews on", comment: "Indicating when the subscription will renew")
.font(.title2)
.foregroundColor(.white)
Text(format_date(date: expiry))
@@ -101,7 +101,7 @@ extension DamusPurpleView {
Button(action: {
show_manage_subscriptions = true
}, label: {
Text(NSLocalizedString("Manage", comment: "Manage the damus subscription"))
Text("Manage", comment: "Manage the damus subscription")
.padding(.horizontal, 20)
})
.buttonStyle(GradientButtonStyle())
@@ -112,7 +112,7 @@ extension DamusPurpleView {
func ProductsView(_ products: [Product]) -> some View {
VStack(spacing: 10) {
Text(NSLocalizedString("Save 20% off on an annual subscription", comment: "Savings for purchasing an annual subscription"))
Text("Save 20% off on an annual subscription", comment: "Savings for purchasing an annual subscription")
.font(.callout.bold())
.foregroundColor(.white)
ForEach(products) { product in
@@ -132,7 +132,7 @@ extension DamusPurpleView {
.buttonStyle(GradientButtonStyle())
}
Text("By subscribing to Damus Purple you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)")
Text("By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)", comment: "Text explaining the terms and conditions of subscribing to Damus Purple. EULA stands for End User License Agreement.")
.foregroundColor(.white.opacity(0.6))
.font(.caption)
.padding()
@@ -148,11 +148,11 @@ extension DamusPurpleView {
Text(purple_type?.label() ?? product.displayName)
Spacer()
if let non_discounted_price = purple_type?.non_discounted_price(product: product) {
Text(verbatim: non_discounted_price)
Text(non_discounted_price)
.strikethrough()
.foregroundColor(DamusColors.white.opacity(0.5))
}
Text(verbatim: product.displayPrice)
Text(product.displayPrice)
.fontWeight(.bold)
}
)
+1 -1
View File
@@ -27,7 +27,7 @@ extension DamusPurpleView {
.shadow(radius: 5)
VStack(alignment: .leading) {
Text(NSLocalizedString("Purple", comment: "Subscription service name"))
Text("Purple", comment: "Subscription service name")
.font(.system(size: 60.0).weight(.bold))
.foregroundStyle(
LinearGradient(
@@ -38,7 +38,7 @@ extension DamusPurpleView {
.resizable()
.frame(width: 15, height: 15)
Text(NSLocalizedString("Coming soon", comment: "Feature is still in development and will be available soon"))
Text("Coming soon", comment: "Feature is still in development and will be available soon")
.font(.caption)
.bold()
}
@@ -25,6 +25,6 @@ struct PurpleBackdrop<T: View>: View {
#Preview {
PurpleBackdrop {
Text("Hello, World")
Text(verbatim: "Hello, World")
}
}
@@ -67,14 +67,14 @@ struct PurpleViewPrimitives {
struct ProductLoadErrorView: View {
var body: some View {
Text(NSLocalizedString("Subscription Error", comment: "Ah dang there was an error loading subscription information from the AppStore. Please try again later :("))
Text("Subscription Error", comment: "Ah dang there was an error loading subscription information from the AppStore. Please try again later :(")
.foregroundColor(.white)
}
}
struct SaveTextView: View {
var body: some View {
Text(NSLocalizedString("Save 14%", comment: "Percentage of purchase price the user will save"))
Text("Save 14%", comment: "Percentage of purchase price the user will save")
.font(.callout)
.italic()
.foregroundColor(DamusColors.green)
@@ -15,7 +15,7 @@ struct RelayAdminDetail: View {
var body: some View {
HStack(spacing: 15) {
VStack(spacing: 10) {
Text("ADMIN")
Text("ADMIN", comment: "Text label indicating the profile picture underneath it is the admin of the Nostr relay.")
.font(.caption)
.fontWeight(.heavy)
.foregroundColor(DamusColors.mediumGrey)
@@ -36,18 +36,18 @@ struct RelayAdminDetail: View {
Divider().frame(width: 1)
VStack {
Text("CONTACT")
Text("CONTACT", comment: "Text label indicating that the information below is the contact information of the admin of the Nostr relay.")
.font(.caption)
.fontWeight(.heavy)
.foregroundColor(DamusColors.mediumGrey)
Image("messages")
.foregroundColor(.gray)
if nip11?.contact == "" {
Text("N/A")
if let contact = nip11?.contact, !contact.isEmpty {
Text(contact)
.font(.subheadline)
.foregroundColor(.gray)
} else {
Text(nip11?.contact ?? "N/A")
Text("N/A", comment: "Text label indicating that there is no NIP-11 relay admin contact information found. In English, N/A stands for not applicable.")
.font(.subheadline)
.foregroundColor(.gray)
}
@@ -15,7 +15,7 @@ struct RelayAuthenticationDetail: View {
case .none:
EmptyView()
case .pending:
Text(NSLocalizedString("Pending", comment: "Label to display that authentication to a server is pending."))
Text("Pending", comment: "Label to display that authentication to a server is pending.")
.font(.caption)
.frame(height: 20)
.padding(.horizontal, 10)
@@ -27,7 +27,7 @@ struct RelayAuthenticationDetail: View {
.stroke(DamusColors.warningBorder, lineWidth: 1)
)
case .verified:
Text(NSLocalizedString("Authenticated", comment: "Label to display that authentication to a server has succeeded."))
Text("Authenticated", comment: "Label to display that authentication to a server has succeeded.")
.font(.caption)
.frame(height: 20)
.padding(.horizontal, 10)
@@ -39,7 +39,7 @@ struct RelayAuthenticationDetail: View {
.stroke(DamusColors.successBorder, lineWidth: 1)
)
case .error:
Text(NSLocalizedString("Error", comment: "Label to display that authentication to a server has failed."))
Text("Error", comment: "Label to display that authentication to a server has failed.")
.font(.caption)
.frame(height: 20)
.padding(.horizontal, 10)
+1 -1
View File
@@ -47,7 +47,7 @@ struct RelayNipList: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(NSLocalizedString("Supported NIPs", comment: "Label to display relay's supported NIPs."))
Text("Supported NIPs", comment: "Label to display relay's supported NIPs.")
.font(.callout)
.fontWeight(.bold)
.foregroundColor(DamusColors.mediumGrey)
+16 -15
View File
@@ -20,17 +20,20 @@ struct RelayPaidDetail: View {
return formattedString
}
func displayAmount(unit: String, amount: Int64) -> String {
if unit == "msats" {
format_msats(amount)
} else {
"\(amount) \(unit)"
}
}
func Amount(unit: String, amount: Int64) -> some View {
HStack {
if unit == "msats" {
Text("\(format_msats(amount))")
.font(.system(size: 13, weight: .heavy))
.foregroundColor(DamusColors.white)
} else {
Text("\(amount) \(unit)")
.font(.system(size: 13, weight: .heavy))
.foregroundColor(DamusColors.white)
}
let displayString = displayAmount(unit: unit, amount: amount)
Text(displayString)
.font(.system(size: 13, weight: .heavy))
.foregroundColor(DamusColors.white)
}
}
@@ -48,26 +51,24 @@ struct RelayPaidDetail: View {
if !admission.isEmpty {
Amount(unit: admission[0].unit, amount: admission[0].amount)
} else {
Text(verbatim: "Paid Relay")
Text("Paid Relay", comment: "Text indicating that this is a paid relay.")
.font(.system(size: 13, weight: .heavy))
.foregroundColor(DamusColors.white)
}
} else if let subscription = fees?.subscription {
if !subscription.isEmpty {
Amount(unit: subscription[0].unit, amount: subscription[0].amount)
Text("/ \(timeString(time: subscription[0].period))")
Text("\(displayAmount(unit: subscription[0].unit, amount: subscription[0].amount)) / \(timeString(time: subscription[0].period))", comment: "Amount of money required to subscribe to the Nostr relay. In English, this would look something like '4,000 sats / 30 days', meaning it costs 4000 sats to subscribe to the Nostr relay for 30 days.")
.font(.system(size: 13, weight: .heavy))
.foregroundColor(DamusColors.white)
}
} else if let publication = fees?.publication {
if !publication.isEmpty {
Amount(unit: publication[0].unit, amount: publication[0].amount)
Text("/ event")
Text("\(displayAmount(unit: publication[0].unit, amount: publication[0].amount)) / event", comment: "Amount of money required to publish to the Nostr relay. In English, this would look something like '10 sats / event', meaning it costs 10 sats to publish one event.")
.font(.system(size: 13, weight: .heavy))
.foregroundColor(DamusColors.white)
}
} else {
Text(verbatim: "Paid Relay")
Text("Paid Relay", comment: "Text indicating that this is a paid relay.")
.font(.system(size: 13, weight: .heavy))
.foregroundColor(DamusColors.white)
}
@@ -14,7 +14,7 @@ struct RelaySoftwareDetail: View {
var body: some View {
HStack(spacing: 15) {
VStack {
Text("SOFTWARE")
Text("SOFTWARE", comment: "Text label indicating which relay software is used to run this Nostr relay.")
.font(.caption)
.fontWeight(.heavy)
.foregroundColor(DamusColors.mediumGrey)
@@ -24,16 +24,21 @@ struct RelaySoftwareDetail: View {
let software = nip11?.software
let softwareSeparated = software?.components(separatedBy: "/")
let softwareShortened = softwareSeparated?.last
Text(softwareShortened ?? "N/A")
.font(.subheadline)
.foregroundColor(.gray)
if let softwareShortened = softwareSeparated?.last {
Text(softwareShortened)
.font(.subheadline)
.foregroundColor(.gray)
} else {
Text("N/A", comment: "Text label indicating that there is no NIP-11 relay software information found. In English, N/A stands for not applicable.")
.font(.subheadline)
.foregroundColor(.gray)
}
}
Divider().frame(width: 1)
VStack {
Text("VERSION")
Text("VERSION", comment: "Text label indicating which version of the relay software is being run for this Nostr relay.")
.font(.caption)
.fontWeight(.heavy)
.foregroundColor(DamusColors.mediumGrey)
@@ -41,9 +46,15 @@ struct RelaySoftwareDetail: View {
Image("branches")
.foregroundColor(.gray)
Text(nip11?.version ?? "N/A")
.font(.subheadline)
.foregroundColor(.gray)
if let version = nip11?.version, !version.isEmpty {
Text(version)
.font(.subheadline)
.foregroundColor(.gray)
} else {
Text("N/A", comment: "Text label indicating that there is no NIP-11 relay software version information found. In English, N/A stands for not applicable.")
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
}
+2 -2
View File
@@ -112,7 +112,7 @@ struct RelayConfigView: View {
func RelayList(title: String, relayList: [RelayDescriptor], recommended: Bool) -> some View {
ScrollView(showsIndicators: false) {
HStack {
Text(NSLocalizedString(title, comment: "Section title for type of relay server list"))
Text(title)
.font(.system(size: 32, weight: .bold))
@@ -123,7 +123,7 @@ struct RelayConfigView: View {
show_add_relay.toggle()
}) {
HStack {
Text(verbatim: "Add relay")
Text("Add relay", comment: "Button text to add a relay")
.padding(10)
}
}
+9 -4
View File
@@ -144,12 +144,17 @@ struct RelayDetailView: View {
Divider()
Text("Description")
Text("Description", comment: "Description of the specific Nostr relay server.")
.font(.subheadline)
.foregroundColor(DamusColors.mediumGrey)
Text(nip11?.description ?? "N/A")
.font(.subheadline)
if let description = nip11?.description, !description.isEmpty {
Text(description)
.font(.subheadline)
} else {
Text("N/A", comment: "Text label indicating that there is no NIP-11 relay description information found. In English, N/A stands for not applicable.")
.font(.subheadline)
}
Divider()
@@ -175,7 +180,7 @@ struct RelayDetailView: View {
}
if state.settings.developer_mode {
Text("Relay Logs")
Text("Relay Logs", comment: "Text label indicating that the text below it are developer mode logs.")
.padding(.top)
Divider()
Text(log.contents ?? NSLocalizedString("No logs to display", comment: "Label to indicate that there are no developer mode logs available to be displayed on the screen"))
+3 -3
View File
@@ -75,7 +75,7 @@ struct RelayView: View {
Button(action: {
remove_action(privkey: keypair.privkey)
}) {
Text(NSLocalizedString("Added", comment: "Button to show relay server is already added to list."))
Text("Added", comment: "Button to show relay server is already added to list.")
.font(.caption)
}
.buttonStyle(NeutralButtonShape.capsule.style)
@@ -147,7 +147,7 @@ struct RelayView: View {
Button(action: {
add_action(keypair: keypair)
}) {
Text(NSLocalizedString("Add", comment: "Button to add relay server to list."))
Text("Add", comment: "Button to add relay server to list.")
.font(.caption)
}
.buttonStyle(NeutralButtonShape.capsule.style)
@@ -166,7 +166,7 @@ struct RelayView: View {
remove_action(privkey: privkey)
}) {
if showText {
Text(NSLocalizedString("Disconnect", comment: "Button to disconnect from a relay server."))
Text("Disconnect", comment: "Button to disconnect from a relay server.")
}
Image("minus-circle")
+25 -3
View File
@@ -21,6 +21,13 @@ struct SaveKeysView: View {
@FocusState var pubkey_focused: Bool
@FocusState var privkey_focused: Bool
let first_contact_event: NdbNote?
init(account: CreateAccountModel) {
self.account = account
self.first_contact_event = make_first_contact_event(keypair: account.keypair)
}
var body: some View {
ZStack(alignment: .top) {
VStack(alignment: .center) {
@@ -102,6 +109,13 @@ struct SaveKeysView: View {
}
func complete_account_creation(_ account: CreateAccountModel) {
guard let first_contact_event else {
error = NSLocalizedString("Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.", comment: "Error message to the user indicating that the initial contact list failed to be created.")
return
}
// Save contact list to storage right away so that we don't need to depend on the network to complete this important step
self.save_to_storage(first_contact_event: first_contact_event, for: account)
let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey)
for relay in bootstrap_relays {
add_rw_relay(self.pool, relay)
@@ -116,21 +130,29 @@ struct SaveKeysView: View {
self.pool.connect()
}
func save_to_storage(first_contact_event: NdbNote, for account: CreateAccountModel) {
// Send to NostrDB so that we have a local copy in storage
self.pool.send_raw_to_local_ndb(.typical(.event(first_contact_event)))
// Save the ID to user settings so that we can easily find it later.
let settings = UserSettingsStore.globally_load_for(pubkey: account.pubkey)
settings.latest_contact_event_id_hex = first_contact_event.id.hex()
}
func handle_event(relay: RelayURL, ev: NostrConnectionEvent) {
switch ev {
case .ws_event(let wsev):
switch wsev {
case .connected:
let metadata = create_account_to_metadata(account)
let contacts_ev = make_first_contact_event(keypair: account.keypair)
if let keypair = account.keypair.to_full(),
let metadata_ev = make_metadata_event(keypair: keypair, metadata: metadata) {
self.pool.send(.event(metadata_ev))
}
if let contacts_ev {
self.pool.send(.event(contacts_ev))
if let first_contact_event {
self.pool.send(.event(first_contact_event))
}
do {
+3 -3
View File
@@ -84,7 +84,7 @@ struct PullDownSearchView: View {
if results.count > 0 {
HStack {
Image("search")
Text(NSLocalizedString("Top hits", comment: "A label indicating that the notes being displayed below it are all top note search results"))
Text("Top hits", comment: "A label indicating that the notes being displayed below it are all top note search results")
Spacer()
}
.padding(.horizontal)
@@ -101,7 +101,7 @@ struct PullDownSearchView: View {
HStack {
Image("notes.fill")
Text(NSLocalizedString("Notes", comment: "A label indicating that the notes being displayed below it are from a timeline, not search results"))
Text("Notes", comment: "A label indicating that the notes being displayed below it are from a timeline, not search results")
Spacer()
}
.foregroundColor(.secondary)
@@ -109,7 +109,7 @@ struct PullDownSearchView: View {
} else if results.count == 0 && !search_text.isEmpty {
HStack {
Image("search")
Text(NSLocalizedString("No results", comment: "A label indicating that note search resulted in no results"))
Text("No results", comment: "A label indicating that note search resulted in no results")
Spacer()
}
.padding(.horizontal)
@@ -1,20 +0,0 @@
//
// SearchingProfileView.swift
// damus
//
// Created by William Casarin on 2023-03-05.
//
import SwiftUI
struct SearchingProfileView: View {
var body: some View {
Text(verbatim: /*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct SearchingProfileView_Previews: PreviewProvider {
static var previews: some View {
SearchingProfileView()
}
}
+1 -1
View File
@@ -85,7 +85,7 @@ struct SearchHomeView: View {
HStack {
Image("notes.fill")
Text(NSLocalizedString("All recent notes", comment: "A label indicating that the notes being displayed below it are all recent notes"))
Text("All recent notes", comment: "A label indicating that the notes being displayed below it are all recent notes")
Spacer()
}
.foregroundColor(.secondary)
+15 -7
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
}
}
}
-48
View File
@@ -1,48 +0,0 @@
//
// AddEmojiView.swift
// damus
//
// Created by Suhail Saqan on 7/16/23.
//
import SwiftUI
struct AddEmojiView: View {
@Binding var emoji: String
var body: some View {
ZStack(alignment: .leading) {
HStack{
TextField(NSLocalizedString("", comment: "Placeholder example for an emoji reaction"), text: $emoji)
.padding(2)
.padding(.leading, 25)
.opacity(emoji == "" ? 0.5 : 1)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.onChange(of: emoji) { newEmoji in
if let lastEmoji = newEmoji.last.map(String.init), isValidEmoji(lastEmoji) {
self.emoji = lastEmoji
} else {
self.emoji = ""
}
}
Label("", image: "close-circle")
.foregroundColor(.accentColor)
.padding(.trailing, -25.0)
.opacity((emoji == "") ? 0.0 : 1.0)
.onTapGesture {
self.emoji = ""
}
}
Label("", image: "copy2")
.padding(.leading, -10)
.onTapGesture {
if let pastedEmoji = UIPasteboard.general.string {
self.emoji = pastedEmoji
}
}
}
}
}
@@ -54,7 +54,7 @@ struct AppearanceSettingsView: View {
}
// MARK: - Text Truncation
Section(header: Text(NSLocalizedString("Text Truncation", comment: "Section header for damus text truncation user configuration"))) {
Section(header: Text("Text Truncation", comment: "Section header for damus text truncation user configuration")) {
Toggle(NSLocalizedString("Truncate timeline text", comment: "Setting to truncate text in timeline"), isOn: $settings.truncate_timeline_text)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Truncate notification mention text", comment: "Setting to truncate text in mention notifications"), isOn: $settings.truncate_mention_text)
@@ -70,7 +70,7 @@ struct AppearanceSettingsView: View {
}
// MARK: - Accessibility
Section(header: Text(NSLocalizedString("Accessibility", comment: "Section header for accessibility settings"))) {
Section(header: Text("Accessibility", comment: "Section header for accessibility settings")) {
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
.toggleStyle(.switch)
}
@@ -97,8 +97,8 @@ struct AppearanceSettingsView: View {
// MARK: - Content filters and moderation
Section(
header: Text(NSLocalizedString("Content filters", comment: "Section title for content filtering/moderation configuration.")),
footer: Text(NSLocalizedString("Notes with the #nsfw tag usually contains adult content or other \"Not safe for work\" content", comment: "Section footer clarifying what #nsfw (not safe for work) tags mean"))
header: Text("Content filters", comment: "Section title for content filtering/moderation configuration."),
footer: Text("Notes with the #nsfw tag usually contains adult content or other \"Not safe for work\" content", comment: "Section footer clarifying what #nsfw (not safe for work) tags mean")
) {
Toggle(NSLocalizedString("Hide notes with #nsfw tags", comment: "Setting to hide notes with the #nsfw (not safe for work) tags"), isOn: $settings.hide_nsfw_tagged_content)
.toggleStyle(.switch)
@@ -106,8 +106,8 @@ struct AppearanceSettingsView: View {
// MARK: - Profiles
Section(
header: Text(NSLocalizedString("Profiles", comment: "Section title for profile view configuration.")),
footer: Text(NSLocalizedString("Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile", comment: "Section footer clarifying what the profile action sheet feature does"))
header: Text("Profiles", comment: "Section title for profile view configuration."),
footer: Text("Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile", comment: "Section footer clarifying what the profile action sheet feature does")
) {
Toggle(NSLocalizedString("Show profile action sheets", comment: "Setting to show profile action sheets when clicking on a user's profile picture"), isOn: $settings.show_profile_action_sheet_on_pfp_click)
.toggleStyle(.switch)
@@ -157,9 +157,9 @@ struct AppearanceSettingsView: View {
}
}
.alert(isPresented: $showing_enable_animation_alert) {
Alert(title: Text(NSLocalizedString("Confirmation", comment: "Confirmation dialog title")),
message: Text(NSLocalizedString("Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?", comment: "Message explaining consequences of changing the 'enable animation' setting")),
primaryButton: .default(Text(NSLocalizedString("OK", comment: "Button label indicating user wants to proceed."))) {
Alert(title: Text("Confirmation", comment: "Confirmation dialog title"),
message: Text("Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?", comment: "Message explaining consequences of changing the 'enable animation' setting"),
primaryButton: .default(Text("OK", comment: "Button label indicating user wants to proceed.")) {
self.clear_cache_button_action()
},
secondaryButton: .cancel() {
@@ -176,22 +176,22 @@ struct AppearanceSettingsView: View {
HStack(spacing: 6) {
switch cache_clearing_state {
case .not_cleared:
Text(NSLocalizedString("Clear Cache", comment: "Button to clear image cache."))
Text("Clear Cache", comment: "Button to clear image cache.")
case .clearing:
ProgressView()
Text(NSLocalizedString("Clearing Cache", comment: "Loading message indicating that the cache is being cleared."))
Text("Clearing Cache", comment: "Loading message indicating that the cache is being cleared.")
case .cleared:
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text(NSLocalizedString("Cache has been cleared", comment: "Message indicating that the cache was successfully cleared."))
Text("Cache has been cleared", comment: "Message indicating that the cache was successfully cleared.")
}
}
})
.disabled(self.cache_clearing_state != .not_cleared)
.alert(isPresented: $showing_cache_clear_alert) {
Alert(title: Text(NSLocalizedString("Confirmation", comment: "Confirmation dialog title")),
message: Text(NSLocalizedString("Are you sure you want to clear the cache? This will free space, but images may take longer to load again.", comment: "Message explaining what it means to clear the cache, asking if user wants to proceed.")),
primaryButton: .default(Text(NSLocalizedString("OK", comment: "Button label indicating user wants to proceed."))) {
Alert(title: Text("Confirmation", comment: "Confirmation dialog title"),
message: Text("Are you sure you want to clear the cache? This will free space, but images may take longer to load again.", comment: "Message explaining what it means to clear the cache, asking if user wants to proceed."),
primaryButton: .default(Text("OK", comment: "Button label indicating user wants to proceed.")) {
self.clear_cache_button_action()
},
secondaryButton: .cancel())
@@ -13,7 +13,7 @@ struct DeveloperSettingsView: View {
var body: some View {
Form {
Section(footer: Text(NSLocalizedString("Developer Mode enables features and options that may help developers diagnose issues and improve this app. Most users will not need Developer Mode.", comment: "Section header for Developer Settings view"))) {
Section(footer: Text("Developer Mode enables features and options that may help developers diagnose issues and improve this app. Most users will not need Developer Mode.", comment: "Section header for Developer Settings view")) {
Toggle(NSLocalizedString("Developer Mode", comment: "Setting to enable developer mode"), isOn: $settings.developer_mode)
.toggleStyle(.switch)
if settings.developer_mode {
@@ -1,81 +0,0 @@
//
// EmojiListItemView.swift
// damus
//
// Created by Suhail Saqan on 7/16/23.
//
import SwiftUI
struct EmojiListItemView: View {
@ObservedObject var settings: UserSettingsStore
let emoji: String
let recommended: Bool
@Binding var showActionButtons: Bool
var body: some View {
Group {
HStack {
if showActionButtons {
if recommended {
AddButton()
} else {
RemoveButton()
}
}
Text(emoji)
}
}
.swipeActions {
if !recommended {
RemoveButton()
.tint(.red)
} else {
AddButton()
.tint(.green)
}
}
.contextMenu {
if !showActionButtons {
CopyAction(emoji: emoji)
}
}
}
func CopyAction(emoji: String) -> some View {
Button {
UIPasteboard.general.setValue(emoji, forPasteboardType: "public.plain-text")
} label: {
Label(NSLocalizedString("Copy", comment: "Button to copy an emoji reaction"), image: "copy2")
}
}
func RemoveButton() -> some View {
Button(action: {
if let index = settings.emoji_reactions.firstIndex(of: emoji) {
settings.emoji_reactions.remove(at: index)
}
}) {
Image(systemName: "minus.circle")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.red)
.padding(.leading, -5)
}
}
func AddButton() -> some View {
Button(action: {
settings.emoji_reactions.append(emoji)
}) {
Image(systemName: "plus.circle")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.green)
.padding(.leading, -5)
}
}
}
@@ -0,0 +1,84 @@
//
// FirstAidSettingsView.swift
// damus
//
// Created by Daniel DAquino on 2024-04-19.
//
import SwiftUI
struct FirstAidSettingsView: View {
let damus_state: DamusState
@ObservedObject var settings: UserSettingsStore
@State var reset_contact_list_state: ContactListResetState = .not_started
enum ContactListResetState: Equatable {
case not_started
case confirming_with_user
case error(String)
case in_progress
case completed
}
var body: some View {
Form {
if damus_state.contacts.event == nil {
Section(
header: Text(NSLocalizedString("Contact list (Follows + Relay list)", comment: "Section title for Contact list first aid tools")),
footer: Text(NSLocalizedString("No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it", comment: "Section footer for Contact list first aid tools"))
) {
Button(action: {
reset_contact_list_state = .confirming_with_user
}, label: {
HStack(spacing: 6) {
switch reset_contact_list_state {
case .not_started, .error:
Label(NSLocalizedString("Reset contact list", comment: "Button to reset contact list."), image: "broom")
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.red)
case .confirming_with_user, .in_progress:
ProgressView()
Text(NSLocalizedString("In progress…", comment: "Loading message indicating that a contact list reset operation is in progress."))
case .completed:
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text(NSLocalizedString("Contact list has been reset", comment: "Message indicating that the contact list was successfully reset."))
}
}
})
.disabled(reset_contact_list_state == .in_progress || reset_contact_list_state == .completed)
if case let .error(error_message) = reset_contact_list_state {
Text(error_message)
.foregroundStyle(.red)
}
}
.alert(NSLocalizedString("WARNING:\n\nThis will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.", comment: "Alert for resetting the user's contact list."),
isPresented: Binding(get: { reset_contact_list_state == .confirming_with_user }, set: { _ in return })
) {
Button(NSLocalizedString("Cancel", comment: "Cancel resetting the contact list."), role: .cancel) {
reset_contact_list_state = .not_started
}
Button(NSLocalizedString("Continue", comment: "Continue with resetting the contact list.")) {
guard let new_contact_list_event = make_first_contact_event(keypair: damus_state.keypair) else {
reset_contact_list_state = .error(NSLocalizedString("An unexpected error happened while trying to create the new contact list. Please contact support.", comment: "Error message for a failed contact list reset operation"))
return
}
damus_state.pool.send(.event(new_contact_list_event))
reset_contact_list_state = .completed
}
}
}
if damus_state.contacts.event != nil {
Text(NSLocalizedString("We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support", comment: "Message indicating that no First Aid actions are available."))
}
}
.navigationTitle(NSLocalizedString("First Aid", comment: "Navigation title for first aid settings and tools"))
}
}
#Preview {
FirstAidSettingsView(damus_state: test_damus_state, settings: test_damus_state.settings)
}
@@ -26,7 +26,7 @@ struct NotificationSettingsView: View {
var body: some View {
Form {
Section(header: Text(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"))) {
Section(header: Text("Local Notifications", comment: "Section header for damus local notifications user configuration")) {
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: $settings.zap_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: $settings.mention_notification)
@@ -39,12 +39,12 @@ struct NotificationSettingsView: View {
.toggleStyle(.switch)
}
Section(header: Text(NSLocalizedString("Notification Preference", comment: "Section header for Notification Preferences"))) {
Section(header: Text("Notification Preference", comment: "Section header for Notification Preferences")) {
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: $settings.notification_only_from_following)
.toggleStyle(.switch)
}
Section(header: Text(NSLocalizedString("Notification Dots", comment: "Section header for notification indicator dot settings"))) {
Section(header: Text("Notification Dots", comment: "Section header for notification indicator dot settings")) {
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: indicator_binding(.zaps))
.toggleStyle(.switch)
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: indicator_binding(.mentions))
@@ -6,114 +6,31 @@
//
import SwiftUI
import Combine
import MCEmojiPicker
struct ReactionsSettingsView: View {
@ObservedObject var settings: UserSettingsStore
@State var new_emoji: String = ""
@State private var showActionButtons = false
@Environment(\.dismiss) var dismiss
var recommended: [String] {
return getMissingRecommendedEmojis(added: settings.emoji_reactions)
}
@State private var isReactionsVisible: Bool = false
var body: some View {
Form {
Section {
AddEmojiView(emoji: $new_emoji)
Text(settings.default_emoji_reaction)
.emojiPicker(
isPresented: $isReactionsVisible,
selectedEmoji: $settings.default_emoji_reaction,
arrowDirection: .up,
isDismissAfterChoosing: true
)
.onTapGesture {
isReactionsVisible = true
}
} header: {
Text(NSLocalizedString("Add Emoji", comment: "Label for section for adding an emoji to the reactions list."))
.font(.system(size: 18, weight: .heavy))
.padding(.bottom, 5)
} footer: {
HStack {
Spacer()
if !new_emoji.isEmpty {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of view adding user inputted emoji.")) {
new_emoji = ""
}
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
.padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0))
Button(NSLocalizedString("Add", comment: "Button to confirm adding user inputted emoji.")) {
if isValidEmoji(new_emoji) {
settings.emoji_reactions.append(new_emoji)
new_emoji = ""
}
}
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
.padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0))
}
}
}
Picker(NSLocalizedString("Select default emoji", comment: "Prompt selection of user's default emoji reaction"),
selection: $settings.default_emoji_reaction) {
ForEach(settings.emoji_reactions, id: \.self) { emoji in
Text(emoji)
}
}
Section {
List {
ForEach(Array(zip(settings.emoji_reactions, 1...)), id: \.1) { tup in
EmojiListItemView(settings: settings, emoji: tup.0, recommended: false, showActionButtons: $showActionButtons)
}
.onMove(perform: showActionButtons ? move: nil)
}
} header: {
Text("Emoji Reactions", comment: "Section title for emoji reactions that are currently added.")
.font(.system(size: 18, weight: .heavy))
.padding(.bottom, 5)
}
if recommended.count > 0 {
Section {
List(Array(zip(recommended, 1...)), id: \.1) { tup in
EmojiListItemView(settings: settings, emoji: tup.0, recommended: true, showActionButtons: $showActionButtons)
}
} header: {
Text("Recommended Emojis", comment: "Section title for recommend emojis")
.font(.system(size: 18, weight: .heavy))
.padding(.bottom, 5)
}
Text("Select default emoji", comment: "Prompt selection of user's default emoji reaction")
}
}
.navigationTitle(NSLocalizedString("Reactions", comment: "Title of emoji reactions view"))
.navigationBarTitleDisplayMode(.large)
.toolbar {
if showActionButtons {
Button("Done") {
showActionButtons.toggle()
}
} else {
Button("Edit") {
showActionButtons.toggle()
}
}
}
}
private func move(from: IndexSet, to: Int) {
settings.emoji_reactions.move(fromOffsets: from, toOffset: to)
}
// Returns the emojis that are in the recommended list but the user has not added yet
func getMissingRecommendedEmojis(added: [String], recommended: [String] = default_emoji_reactions) -> [String] {
let addedSet = Set(added)
let missingEmojis = recommended.filter { !addedSet.contains($0) }
return missingEmojis
}
}
@@ -13,7 +13,7 @@ struct SearchSettingsView: View {
var body: some View {
Form {
Section(header: Text(NSLocalizedString("Spam", comment: "Section header for Universe/Search spam"))) {
Section(header: Text("Spam", comment: "Section header for Universe/Search spam")) {
Toggle(NSLocalizedString("View multiple events per user", comment: "Setting to only see 1 event per user (npub) in the search/universe"), isOn: $settings.multiple_events_per_pubkey)
.toggleStyle(.switch)
}
@@ -28,7 +28,7 @@ struct TranslationSettingsView: View {
if settings.translation_service == .purple && damus_state.purple.enable_purple {
NavigationLink(destination: DamusPurpleView(damus_state: damus_state)) {
Text(NSLocalizedString("Configure Damus Purple", comment: "Button to allow Damus Purple to be configured"))
Text("Configure Damus Purple", comment: "Button to allow Damus Purple to be configured")
}
}
+2 -2
View File
@@ -22,8 +22,8 @@ struct ZapSettingsView: View {
var body: some View {
Form {
Section(
header: Text(NSLocalizedString("OnlyZaps", comment: "Section header for enabling OnlyZaps mode (hide reactions)")),
footer: Text(NSLocalizedString("Hide all 🤙's", comment: "Section footer describing OnlyZaps mode"))
header: Text("OnlyZaps", comment: "Section header for enabling OnlyZaps mode (hide reactions)"),
footer: Text("Hide all 🤙's", comment: "Section footer describing OnlyZaps mode")
) {
Toggle(NSLocalizedString("OnlyZaps mode", comment: "Setting toggle to hide reactions."), isOn: $settings.onlyzaps_mode)
+3 -2
View File
@@ -57,7 +57,7 @@ struct SuggestedHashtagsView: View {
VStack {
HStack {
Image(systemName: "sparkles")
Text(NSLocalizedString("Suggested hashtags", comment: "A label indicating that the items below it are suggested hashtags"))
Text("Suggested hashtags", comment: "A label indicating that the items below it are suggested hashtags")
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 0.2)) {
@@ -105,7 +105,8 @@ struct SuggestedHashtagsView: View {
Text(verbatim: "#\(hashtag)")
.bold()
Text(pluralizedString(key: "users_talking_about_it", count: self.count))
let pluralizedString = pluralizedString(key: "users_talking_about_it", count: self.count)
Text(pluralizedString)
.foregroundStyle(.secondary)
}
+13 -6
View File
@@ -39,9 +39,9 @@ struct ConnectWalletView: View {
}
.alert(isPresented: $showAlert) {
Alert(
title: Text(NSLocalizedString("Invalid Nostr wallet connection string", comment: "Error message when an invalid Nostr wallet connection string is provided.")),
message: Text("Make sure the wallet you are connecting to supports NWC."),
dismissButton: .default(Text(NSLocalizedString("OK", comment: "Button label indicating user wants to proceed."))) {
title: Text("Invalid Nostr wallet connection string", comment: "Error message when an invalid Nostr wallet connection string is provided."),
message: Text("Make sure the wallet you are connecting to supports NWC.", comment: "Hint message when an invalid Nostr wallet connection string is provided."),
dismissButton: .default(Text("OK", comment: "Button label indicating user wants to proceed.")) {
wallet_scan_result = .scanning
}
)
@@ -80,7 +80,7 @@ struct ConnectWalletView: View {
model.cancel()
}) {
HStack {
Text(NSLocalizedString("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning wallet."))
Text("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning wallet.")
.padding()
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
@@ -96,9 +96,16 @@ struct ConnectWalletView: View {
openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!)
}
//
// Mutiny Wallet NWC is way too advanced to recommend for normal
// users until they have a way to do async receive.
//
/*
MutinyButton() {
openURL(URL(string:"https://app.mutinywallet.com/settings/connections?callbackUri=nostr%2bwalletconnect&name=Damus")!)
}
*/
Button(action: {
if let pasted_nwc = UIPasteboard.general.string {
@@ -168,10 +175,10 @@ struct ConnectWalletView: View {
var TitleSection: some View {
VStack(spacing: 25) {
Text("Damus Wallet")
Text("Damus Wallet", comment: "Title text for Damus Wallet view.")
.fontWeight(.bold)
Text("Securely connect your Damus app to your wallet\nusing Nostr Wallet Connect")
Text("Securely connect your Damus app to your wallet using Nostr\u{00A0}Wallet\u{00A0}Connect", comment: "Text to prompt user to connect their wallet using 'Nostr Wallet Connect'.")
.font(.caption)
.multilineTextAlignment(.center)
}
+4 -4
View File
@@ -28,7 +28,7 @@ struct WalletView: View {
VStack(spacing: 5) {
VStack(spacing: 10) {
Text("Wallet Relay")
Text("Wallet Relay", comment: "Label text indicating that below it is the information about the wallet relay.")
.fontWeight(.semibold)
.padding(.top)
@@ -47,12 +47,12 @@ struct WalletView: View {
if let lud16 = nwc.lud16 {
VStack(spacing: 10) {
Text("Wallet Address")
Text("Wallet Address", comment: "Label text indicating that below it is the wallet address.")
.fontWeight(.semibold)
Divider()
Text(verbatim: lud16)
Text(lud16)
}
.frame(maxWidth: .infinity, minHeight: 75, alignment: .center)
.padding(.horizontal, 10)
@@ -69,7 +69,7 @@ struct WalletView: View {
self.model.disconnect()
}) {
HStack {
Text(NSLocalizedString("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet."))
Text("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 18, alignment: .center)
}
+1 -1
View File
@@ -173,7 +173,7 @@ struct CustomizeZapView: View {
model.zapping = true
}) {
HStack {
Text(NSLocalizedString("Zap User", comment: "Button to send a zap."))
Text("Zap User", comment: "Button to send a zap.")
.font(.system(size: 20, weight: .bold))
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
Binary file not shown.
+16
View File
@@ -226,6 +226,22 @@
<string>geteilte Beiträge</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>one</key>
<string>Zitate</string>
<key>other</key>
<string>Zitat</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
+299 -179
View File
@@ -2,7 +2,7 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
<file original="damus/en-US.lproj/InfoPlist.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.2" build-num="15C500b"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -44,15 +44,25 @@
</file>
<file original="damus/en-US.lproj/Localizable.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.2" build-num="15C500b"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="%@ %@" xml:space="preserve">
<source>%@ %@</source>
<target>%@ %@</target>
<note>Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.
<note>Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.
Sentence composed of 2 variables to describe how many imports were performed from loading a NostrScript. In source English, the first variable is the number of imports, and the second variable is 'Import' or 'Imports'.
Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.</note>
Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.</note>
</trans-unit>
<trans-unit id="%@ / %@" xml:space="preserve">
<source>%@ / %@</source>
<target>%@ / %@</target>
<note>Amount of money required to subscribe to the Nostr relay. In English, this would look something like '4,000 sats / 30 days', meaning it costs 4000 sats to subscribe to the Nostr relay for 30 days.</note>
</trans-unit>
<trans-unit id="%@ / event" xml:space="preserve">
<source>%@ / event</source>
<target>%@ / event</target>
<note>Amount of money required to publish to the Nostr relay. In English, this would look something like '10 sats / event', meaning it costs 10 sats to publish one event.</note>
</trans-unit>
<trans-unit id="%@ has been muted" xml:space="preserve">
<source>%@ has been muted</source>
@@ -104,6 +114,26 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>(Contents are encrypted)</target>
<note>Label on push notification indicating that the contents of the message are encrypted</note>
</trans-unit>
<trans-unit id="1 month" xml:space="preserve">
<source>1 month</source>
<target>1 month</target>
<note>A duration of 1 month to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.</note>
</trans-unit>
<trans-unit id="1 week" xml:space="preserve">
<source>1 week</source>
<target>1 week</target>
<note>A duration of 1 week to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.</note>
</trans-unit>
<trans-unit id="24 hours" xml:space="preserve">
<source>24 hours</source>
<target>24 hours</target>
<note>A duration of 24 hours/1 day to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.</note>
</trans-unit>
<trans-unit id="ADMIN" xml:space="preserve">
<source>ADMIN</source>
<target>ADMIN</target>
<note>Text label indicating the profile picture underneath it is the admin of the Nostr relay.</note>
</trans-unit>
<trans-unit id="API Key (optional)" xml:space="preserve">
<source>API Key (optional)</source>
<target>API Key (optional)</target>
@@ -152,19 +182,13 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<trans-unit id="Add" xml:space="preserve">
<source>Add</source>
<target>Add</target>
<note>Button to add relay server to list.
Button to confirm adding user inputted emoji.</note>
<note>Button to add relay server to list.</note>
</trans-unit>
<trans-unit id="Add Bookmark" xml:space="preserve">
<source>Add Bookmark</source>
<target>Add Bookmark</target>
<note>Button text to add bookmark to a note.</note>
</trans-unit>
<trans-unit id="Add Emoji" xml:space="preserve">
<source>Add Emoji</source>
<target>Add Emoji</target>
<note>Label for section for adding an emoji to the reactions list.</note>
</trans-unit>
<trans-unit id="Add all" xml:space="preserve">
<source>Add all</source>
<target>Add all</target>
@@ -180,26 +204,32 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Add bookmark</target>
<note>Context menu option for adding a note bookmark.</note>
</trans-unit>
<trans-unit id="Add mute item" xml:space="preserve">
<source>Add mute item</source>
<target>Add mute item</target>
<note>Title text to indicate user to an add an item to their mutelist.</note>
</trans-unit>
<trans-unit id="Add relay" xml:space="preserve">
<source>Add relay</source>
<target>Add relay</target>
<note>Title text to indicate user to an add a relay.</note>
<note>Title text to indicate user to an add a relay.
Button text to add a relay</note>
</trans-unit>
<trans-unit id="Add your first post" xml:space="preserve">
<source>Add your first post</source>
<target>Add your first post</target>
<note>Prompt given to the user during onboarding, suggesting them to write their first post</note>
</trans-unit>
<trans-unit id="Added" xml:space="preserve">
<source>Added</source>
<target>Added</target>
<note>Button to show relay server is already added to list.</note>
</trans-unit>
<trans-unit id="Additional information" xml:space="preserve">
<source>Additional information</source>
<target>Additional information</target>
<note>Header text to prompt user to optionally provide additional information when reporting a user or note.</note>
</trans-unit>
<trans-unit id="Admin" xml:space="preserve">
<source>Admin</source>
<target>Admin</target>
<note>Label to display relay contact user.</note>
</trans-unit>
<trans-unit id="All" xml:space="preserve">
<source>All</source>
<target>All</target>
@@ -262,16 +292,16 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Are you lost?</target>
<note>Text asking the user if they are lost in the app.</note>
</trans-unit>
<trans-unit id="Are you sure you want to attach this wallet?" xml:space="preserve">
<source>Are you sure you want to attach this wallet?</source>
<target>Are you sure you want to attach this wallet?</target>
<note>Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Are you sure you want to clear the cache? This will free space, but images may take longer to load again." xml:space="preserve">
<source>Are you sure you want to clear the cache? This will free space, but images may take longer to load again.</source>
<target>Are you sure you want to clear the cache? This will free space, but images may take longer to load again.</target>
<note>Message explaining what it means to clear the cache, asking if user wants to proceed.</note>
</trans-unit>
<trans-unit id="Are you sure you want to connect this wallet?" xml:space="preserve">
<source>Are you sure you want to connect this wallet?</source>
<target>Are you sure you want to connect this wallet?</target>
<note>Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Are you sure you want to delete all of your bookmarks?" xml:space="preserve">
<source>Are you sure you want to delete all of your bookmarks?</source>
<target>Are you sure you want to delete all of your bookmarks?</target>
@@ -296,36 +326,11 @@ Tip: You can always change this later in Settings → Translations</source>
Tip: You can always change this later in Settings → Translations</target>
<note>Message notifying the user that they get auto-translations as part of their service</note>
</trans-unit>
<trans-unit id="Attach" xml:space="preserve">
<source>Attach</source>
<target>Attach</target>
<note>Text for button to attach Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Attach Alby Wallet" xml:space="preserve">
<source>Attach Alby Wallet</source>
<target>Attach Alby Wallet</target>
<note>Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.</note>
</trans-unit>
<trans-unit id="Attach Wallet" xml:space="preserve">
<source>Attach Wallet</source>
<target>Attach Wallet</target>
<note>Text for button to attach Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Attach a Wallet" xml:space="preserve">
<source>Attach a Wallet</source>
<target>Attach a Wallet</target>
<note>Navigation title for attaching Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Authenticated" xml:space="preserve">
<source>Authenticated</source>
<target>Authenticated</target>
<note>Label to display that authentication to a server has succeeded.</note>
</trans-unit>
<trans-unit id="Authentication" xml:space="preserve">
<source>Authentication</source>
<target>Authentication</target>
<note>Header label to display authentication details for a given relay.</note>
</trans-unit>
<trans-unit id="Automatic translations" xml:space="preserve">
<source>Automatic translations</source>
<target>Automatic translations</target>
@@ -383,6 +388,16 @@ Tip: You can always change this later in Settings → Translations</target>
<target>By signing up, you agree to our </target>
<note>Ask the user if they already have an account on Nostr</note>
</trans-unit>
<trans-unit id="By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)" xml:space="preserve">
<source>By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)</source>
<target>By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)</target>
<note>Text explaining the terms and conditions of subscribing to Damus Purple. EULA stands for End User License Agreement.</note>
</trans-unit>
<trans-unit id="CONTACT" xml:space="preserve">
<source>CONTACT</source>
<target>CONTACT</target>
<note>Text label indicating that the information below is the contact information of the admin of the Nostr relay.</note>
</trans-unit>
<trans-unit id="Cache has been cleared" xml:space="preserve">
<source>Cache has been cleared</source>
<target>Cache has been cleared</target>
@@ -395,13 +410,10 @@ Tip: You can always change this later in Settings → Translations</target>
Button to cancel a repost.
Button to cancel any interaction with the QRCode link.
Button to cancel out of alert that creates a new mutelist.
Button to cancel out of posting a note.
Button to cancel out of view adding user inputted emoji.
Button to cancel the upload.
Cancel deleting bookmarks.
Cancel deleting the user.
Cancel out of logging out the user.
Text for button to cancel out of connecting Nostr Wallet Connect lightning ewallet.</note>
Cancel out of logging out the user.</note>
</trans-unit>
<trans-unit id="Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?" xml:space="preserve">
<source>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</source>
@@ -453,21 +465,27 @@ Tip: You can always change this later in Settings → Translations</target>
<target>Confirmation</target>
<note>Confirmation dialog title</note>
</trans-unit>
<trans-unit id="Connect To Relay" xml:space="preserve">
<source>Connect To Relay</source>
<target>Connect To Relay</target>
<note>Button to connect to the relay.</note>
<trans-unit id="Connect" xml:space="preserve">
<source>Connect</source>
<target>Connect</target>
<note>Text for button to conect to Nostr Wallet Connect lightning wallet.
Button to connect to the relay.</note>
</trans-unit>
<trans-unit id="Connect to Alby Wallet" xml:space="preserve">
<source>Connect to Alby Wallet</source>
<target>Connect to Alby Wallet</target>
<note>Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.</note>
</trans-unit>
<trans-unit id="Connect to Mutiny Wallet" xml:space="preserve">
<source>Connect to Mutiny Wallet</source>
<target>Connect to Mutiny Wallet</target>
<note>Button to attach an Mutiny Wallet, a service that provides a Lightning wallet for zapping sats. Mutiny is the name of the service and should not be translated.</note>
</trans-unit>
<trans-unit id="Connecting" xml:space="preserve">
<source>Connecting</source>
<target>Connecting</target>
<note>Relay status label that indicates a relay is connecting.</note>
</trans-unit>
<trans-unit id="Contact" xml:space="preserve">
<source>Contact</source>
<target>Contact</target>
<note>Label to display relay contact information.</note>
</trans-unit>
<trans-unit id="Content filters" xml:space="preserve">
<source>Content filters</source>
<target>Content filters</target>
@@ -476,10 +494,8 @@ Tip: You can always change this later in Settings → Translations</target>
<trans-unit id="Continue" xml:space="preserve">
<source>Continue</source>
<target>Continue</target>
<note>Button to dismiss suggested users view and continue to the main app
Continue with bookmarks.
Continue with deleting the user.
Prompt to user to continue</note>
<note>Continue with bookmarks.
Continue with deleting the user.</note>
</trans-unit>
<trans-unit id="Copied" xml:space="preserve">
<source>Copied</source>
@@ -490,7 +506,6 @@ Tip: You can always change this later in Settings → Translations</target>
<source>Copy</source>
<target>Copy</target>
<note>Button to copy a relay server address.
Button to copy an emoji reaction
Button to copy the value found.
Context menu option for copying the version of damus.</note>
</trans-unit>
@@ -597,6 +612,11 @@ Tip: You can always change this later in Settings → Translations</target>
Setting to enable DM Local Notification
Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Damus" xml:space="preserve">
<source>Damus</source>
<target>Damus</target>
<note>Name of the app for the title of an internal notification</note>
</trans-unit>
<trans-unit id="Damus Purple" xml:space="preserve">
<source>Damus Purple</source>
<target>Damus Purple</target>
@@ -607,6 +627,11 @@ Tip: You can always change this later in Settings → Translations</target>
<target>Damus Purple environment</target>
<note>Prompt selection of the Damus purple environment (Developer feature to switch between real/production mode to test modes).</note>
</trans-unit>
<trans-unit id="Damus Wallet" xml:space="preserve">
<source>Damus Wallet</source>
<target>Damus Wallet</target>
<note>Title text for Damus Wallet view.</note>
</trans-unit>
<trans-unit id="DeepL (Proprietary, Higher Accuracy)" xml:space="preserve">
<source>DeepL (Proprietary, Higher Accuracy)</source>
<target>DeepL (Proprietary, Higher Accuracy)</target>
@@ -636,7 +661,7 @@ Tip: You can always change this later in Settings → Translations</target>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Description</target>
<note>Label to display relay description.</note>
<note>Description of the specific Nostr relay server.</note>
</trans-unit>
<trans-unit id="Developer" xml:space="preserve">
<source>Developer</source>
@@ -657,12 +682,8 @@ Tip: You can always change this later in Settings → Translations</target>
<trans-unit id="Disconnect" xml:space="preserve">
<source>Disconnect</source>
<target>Disconnect</target>
<note>Button to disconnect from a relay server.</note>
</trans-unit>
<trans-unit id="Disconnect From Relay" xml:space="preserve">
<source>Disconnect From Relay</source>
<target>Disconnect From Relay</target>
<note>Button to disconnect from the relay.</note>
<note>Button to disconnect from the relay.
Button to disconnect from a relay server.</note>
</trans-unit>
<trans-unit id="Disconnect Wallet" xml:space="preserve">
<source>Disconnect Wallet</source>
@@ -709,11 +730,6 @@ Tip: You can always change this later in Settings → Translations</target>
<target>Edit</target>
<note>Button to edit user's profile.</note>
</trans-unit>
<trans-unit id="Emoji Reactions" xml:space="preserve">
<source>Emoji Reactions</source>
<target>Emoji Reactions</target>
<note>Section title for emoji reactions that are currently added.</note>
</trans-unit>
<trans-unit id="Enable Purple auto-translations" xml:space="preserve">
<source>Enable Purple auto-translations</source>
<target>Enable Purple auto-translations</target>
@@ -747,13 +763,19 @@ Tip: You can always change this later in Settings → Translations</target>
<trans-unit id="Error" xml:space="preserve">
<source>Error</source>
<target>Error</target>
<note>Label to display that authentication to a server has failed.</note>
<note>Label to display that authentication to a server has failed.
Relay status label that indicates a relay had an error when connecting</note>
</trans-unit>
<trans-unit id="Error fetching lightning invoice" xml:space="preserve">
<source>Error fetching lightning invoice</source>
<target>Error fetching lightning invoice</target>
<note>Message to display when there was an error fetching a lightning invoice while attempting to zap.</note>
</trans-unit>
<trans-unit id="Error retrieving muted event" xml:space="preserve">
<source>Error retrieving muted event</source>
<target>Error retrieving muted event</target>
<note>Text for an item that application failed to retrieve the muted event for.</note>
</trans-unit>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Error: %@</target>
@@ -904,7 +926,7 @@ My side interests include languages and I am striving to be a #polyglot - I am a
<trans-unit id="Hashtags" xml:space="preserve">
<source>Hashtags</source>
<target>Hashtags</target>
<note>Label for filter for seeing only hashtag follows.</note>
<note>Section header title for a list of hashtags that are muted.</note>
</trans-unit>
<trans-unit id="Hello everybody!&#10;&#10;This is my first post on Damus, I am happy to meet you all 🤙. Whats up?&#10;&#10;#introductions" xml:space="preserve">
<source>Hello everybody!
@@ -937,7 +959,7 @@ This is my first post on Damus, I am happy to meet you all 🤙. Whats up?
<trans-unit id="Hide" xml:space="preserve">
<source>Hide</source>
<target>Hide</target>
<note>Button to hide a note from a user who has been muted.</note>
<note>Button to hide a note which has been muted.</note>
</trans-unit>
<trans-unit id="Hide all 🤙's" xml:space="preserve">
<source>Hide all 🤙's</source>
@@ -983,6 +1005,16 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>Impersonation</target>
<note>Description of report type for impersonation.</note>
</trans-unit>
<trans-unit id="Indefinite" xml:space="preserve">
<source>Indefinite</source>
<target>Indefinite</target>
<note>Mute a given item indefinitly (until user unmutes it). As opposed to muting the item for a given period of time.</note>
</trans-unit>
<trans-unit id="Internal app notification" xml:space="preserve">
<source>Internal app notification</source>
<target>Internal app notification</target>
<note>Badge indicating that a notification is an official internal app notification</note>
</trans-unit>
<trans-unit id="Invalid Nostr wallet connection string" xml:space="preserve">
<source>Invalid Nostr wallet connection string</source>
<target>Invalid Nostr wallet connection string</target>
@@ -1089,11 +1121,6 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>Local default</target>
<note>Dropdown option label for system default for Lightning wallet.</note>
</trans-unit>
<trans-unit id="Log" xml:space="preserve">
<source>Log</source>
<target>Log</target>
<note>Label to display developer mode logs.</note>
</trans-unit>
<trans-unit id="Login" xml:space="preserve">
<source>Login</source>
<target>Login</target>
@@ -1115,6 +1142,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>Make Default</target>
<note>Button label to indicate that tapping it will make the selected zap type be the default for future zaps.</note>
</trans-unit>
<trans-unit id="Make sure the wallet you are connecting to supports NWC." xml:space="preserve">
<source>Make sure the wallet you are connecting to supports NWC.</source>
<target>Make sure the wallet you are connecting to supports NWC.</target>
<note>Hint message when an invalid Nostr wallet connection string is provided.</note>
</trans-unit>
<trans-unit id="Make sure your nsec account key is saved before you logout or you will lose access to this account" xml:space="preserve">
<source>Make sure your nsec account key is saved before you logout or you will lose access to this account</source>
<target>Make sure your nsec account key is saved before you logout or you will lose access to this account</target>
@@ -1125,6 +1157,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>Manage</target>
<note>Manage the damus subscription</note>
</trans-unit>
<trans-unit id="Manage subscription" xml:space="preserve">
<source>Manage subscription</source>
<target>Manage subscription</target>
<note>Button to take user to manage Damus Purple subscription</note>
</trans-unit>
<trans-unit id="Media previews" xml:space="preserve">
<source>Media previews</source>
<target>Media previews</target>
@@ -1158,14 +1195,18 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<trans-unit id="Mute" xml:space="preserve">
<source>Mute</source>
<target>Mute</target>
<note>Alert button to mute a user.
Button to mute a profile.</note>
<note>Alert button to mute a user.</note>
</trans-unit>
<trans-unit id="Mute %@?" xml:space="preserve">
<source>Mute %@?</source>
<target>Mute %@?</target>
<note>Alert message prompt to ask if a user should be muted.</note>
</trans-unit>
<trans-unit id="Mute Hashtag" xml:space="preserve">
<source>Mute Hashtag</source>
<target>Mute Hashtag</target>
<note>Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore.</note>
</trans-unit>
<trans-unit id="Mute User" xml:space="preserve">
<source>Mute User</source>
<target>Mute User</target>
@@ -1184,17 +1225,15 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<trans-unit id="Muted" xml:space="preserve">
<source>Muted</source>
<target>Muted</target>
<note>Sidebar menu label for muted users view.</note>
<note>Navigation title of view to see list of muted users &amp; phrases.
Sidebar menu label for muted users view.</note>
</trans-unit>
<trans-unit id="Muted Users" xml:space="preserve">
<source>Muted Users</source>
<target>Muted Users</target>
<note>Navigation title of view to see list of muted users.</note>
</trans-unit>
<trans-unit id="My Relays" xml:space="preserve">
<source>My Relays</source>
<target>My Relays</target>
<note>Section title for relay servers that the user is connected to.</note>
<trans-unit id="N/A" xml:space="preserve">
<source>N/A</source>
<target>N/A</target>
<note>Text label indicating that there is no NIP-11 relay admin contact information found. In English, N/A stands for not applicable.
Text label indicating that there is no NIP-11 relay description information found. In English, N/A stands for not applicable.
Text label indicating that there is no NIP-11 relay software information found. In English, N/A stands for not applicable.</note>
</trans-unit>
<trans-unit id="Never" xml:space="preserve">
<source>Never</source>
@@ -1226,11 +1265,6 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>No</target>
<note>User confirm No</note>
</trans-unit>
<trans-unit id="No data available" xml:space="preserve">
<source>No data available</source>
<target>No data available</target>
<note>Text indicating that there is no data available to show for specific metadata about a relay server.</note>
</trans-unit>
<trans-unit id="No logs to display" xml:space="preserve">
<source>No logs to display</source>
<target>No logs to display</target>
@@ -1291,21 +1325,28 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>NostrScript Error</target>
<note>Text indicating that there was an error with loading NostrScript. There is a more descriptive error message shown separately underneath.</note>
</trans-unit>
<trans-unit id="Note from a user you've muted" xml:space="preserve">
<source>Note from a user you've muted</source>
<target>Note from a user you've muted</target>
<note>Text to indicate that what is being shown is a note from a user who has been muted.</note>
<trans-unit id="Note from a %@ you've muted" xml:space="preserve">
<source>Note from a %@ you've muted</source>
<target>Note from a %@ you've muted</target>
<note>Text to indicate that what is being shown is a note which has been muted.</note>
</trans-unit>
<trans-unit id="Note you've muted" xml:space="preserve">
<source>Note you've muted</source>
<target>Note you've muted</target>
<note>Text to indicate that what is being shown is a note which has been muted.</note>
</trans-unit>
<trans-unit id="Notes" xml:space="preserve">
<source>Notes</source>
<target>Notes</target>
<note>A label indicating that the notes being displayed below it are from a timeline, not search results</note>
<note>Label for filter for seeing only notes (instead of notes and replies).
Label for filter for seeing only your notes (instead of notes and replies).
A label indicating that the notes being displayed below it are from a timeline, not search results</note>
</trans-unit>
<trans-unit id="Notes &amp; Replies" xml:space="preserve">
<source>Notes &amp; Replies</source>
<target>Notes &amp; Replies</target>
<note>Label for filter for seeing your notes and replies (instead of only your notes).
Label for filter for seeing notes and replies (instead of only notes).</note>
<note>Label for filter for seeing notes and replies (instead of only notes).
Label for filter for seeing your notes and replies (instead of only your notes).</note>
</trans-unit>
<trans-unit id="Notes with the #nsfw tag usually contains adult content or other &quot;Not safe for work&quot; content" xml:space="preserve">
<source>Notes with the #nsfw tag usually contains adult content or other "Not safe for work" content</source>
@@ -1342,7 +1383,7 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<source>OK</source>
<target>OK</target>
<note>Button label indicating user wants to proceed.
Button label to dismiss an error dialog</note>
Button label to dismiss an error dialog</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
@@ -1392,12 +1433,12 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<trans-unit id="Paid Relay" xml:space="preserve">
<source>Paid Relay</source>
<target>Paid Relay</target>
<note>Section header that indicates the relay server requires payment.</note>
<note>Text indicating that this is a paid relay.</note>
</trans-unit>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>Paste</target>
<note>Button to paste a Nostr Wallet Connect string to connect the wallet for use in Damus for zaps.</note>
<trans-unit id="Paste NWC Address" xml:space="preserve">
<source>Paste NWC Address</source>
<target>Paste NWC Address</target>
<note>Text for button to connect a lightning wallet.</note>
</trans-unit>
<trans-unit id="Pay" xml:space="preserve">
<source>Pay</source>
@@ -1422,8 +1463,7 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<trans-unit id="Permanently Delete Account" xml:space="preserve">
<source>Permanently Delete Account</source>
<target>Permanently Delete Account</target>
<note>Alert for deleting the users account.
Section title for deleting the user</note>
<note>Alert for deleting the users account.</note>
</trans-unit>
<trans-unit id="Plan" xml:space="preserve">
<source>Plan</source>
@@ -1520,6 +1560,11 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Purchased!</target>
<note>User purchased a subscription</note>
</trans-unit>
<trans-unit id="Purchasing" xml:space="preserve">
<source>Purchasing</source>
<target>Purchasing</target>
<note>Loading label indicating the purchase action is in progress</note>
</trans-unit>
<trans-unit id="Purple" xml:space="preserve">
<source>Purple</source>
<target>Purple</target>
@@ -1535,6 +1580,11 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Quote</target>
<note>Button to compose a quoted note</note>
</trans-unit>
<trans-unit id="Quotes" xml:space="preserve">
<source>Quotes</source>
<target>Quotes</target>
<note>Navigation bar title for Quote Reposts view.</note>
</trans-unit>
<trans-unit id="Ran to suspension." xml:space="preserve">
<source>Ran to suspension.</source>
<target>Ran to suspension.</target>
@@ -1547,20 +1597,10 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
Section header for reactions settings
Title of emoji reactions view</note>
</trans-unit>
<trans-unit id="Recommended Emojis" xml:space="preserve">
<source>Recommended Emojis</source>
<target>Recommended Emojis</target>
<note>Section title for recommend emojis</note>
</trans-unit>
<trans-unit id="Recommended relays" xml:space="preserve">
<source>Recommended relays</source>
<target>Recommended relays</target>
<note>Title for view of recommended relays.</note>
</trans-unit>
<trans-unit id="Relay" xml:space="preserve">
<source>Relay</source>
<target>Relay</target>
<note>Label to display relay address.</note>
<trans-unit id="Relay Logs" xml:space="preserve">
<source>Relay Logs</source>
<target>Relay Logs</target>
<note>Text label indicating that the text below it are developer mode logs.</note>
</trans-unit>
<trans-unit id="Relays" xml:space="preserve">
<source>Relays</source>
@@ -1589,6 +1629,16 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Remove bookmark</target>
<note>Context menu option for removing a note bookmark.</note>
</trans-unit>
<trans-unit id="Renew (1 mo)" xml:space="preserve">
<source>Renew (1 mo)</source>
<target>Renew (1 mo)</target>
<note>Button to take user to renew subscription for one month</note>
</trans-unit>
<trans-unit id="Renew (1 yr)" xml:space="preserve">
<source>Renew (1 yr)</source>
<target>Renew (1 yr)</target>
<note>Button to take user to renew subscription for one year</note>
</trans-unit>
<trans-unit id="Renews on" xml:space="preserve">
<source>Renews on</source>
<target>Renews on</target>
@@ -1692,6 +1742,11 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Runtime error</target>
<note>Indication that a runtime error occurred when running a NostrScript.</note>
</trans-unit>
<trans-unit id="SOFTWARE" xml:space="preserve">
<source>SOFTWARE</source>
<target>SOFTWARE</target>
<note>Text label indicating which relay software is used to run this Nostr relay.</note>
</trans-unit>
<trans-unit id="Satoshi Nakamoto" xml:space="preserve">
<source>Satoshi Nakamoto</source>
<target>Satoshi Nakamoto</target>
@@ -1727,6 +1782,11 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Scan Code</target>
<note>Button to switch to scan QR Code page.</note>
</trans-unit>
<trans-unit id="Scan NWC Address" xml:space="preserve">
<source>Scan NWC Address</source>
<target>Scan NWC Address</target>
<note>Text for button to connect a lightning wallet.</note>
</trans-unit>
<trans-unit id="Scan Your Private Key QR" xml:space="preserve">
<source>Scan Your Private Key QR</source>
<target>Scan Your Private Key QR</target>
@@ -1774,6 +1834,11 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Secret Account Login Key</target>
<note>Section title for user's secret account login key.</note>
</trans-unit>
<trans-unit id="Securely connect your Damus app to your wallet using Nostr Wallet Connect" xml:space="preserve">
<source>Securely connect your Damus app to your wallet using Nostr Wallet Connect</source>
<target>Securely connect your Damus app to your wallet using Nostr Wallet Connect</target>
<note>Text to prompt user to connect their wallet using 'Nostr Wallet Connect'.</note>
</trans-unit>
<trans-unit id="Select a Lightning wallet" xml:space="preserve">
<source>Select a Lightning wallet</source>
<target>Select a Lightning wallet</target>
@@ -1845,7 +1910,7 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<trans-unit id="Show" xml:space="preserve">
<source>Show</source>
<target>Show</target>
<note>Button to show a note from a user who has been muted.
<note>Button to show a note which has been muted.
Toggle to show or hide user's secret account login key.</note>
</trans-unit>
<trans-unit id="Show general statuses" xml:space="preserve">
@@ -1884,11 +1949,6 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Show profile action sheets</target>
<note>Setting to show profile action sheets when clicking on a user's profile picture</note>
</trans-unit>
<trans-unit id="Show recommended relays" xml:space="preserve">
<source>Show recommended relays</source>
<target>Show recommended relays</target>
<note>Button to show recommended relays.</note>
</trans-unit>
<trans-unit id="Show wallet selector" xml:space="preserve">
<source>Show wallet selector</source>
<target>Show wallet selector</target>
@@ -1919,11 +1979,6 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken</target>
<note>Description about why Nostr is needed.</note>
</trans-unit>
<trans-unit id="Software" xml:space="preserve">
<source>Software</source>
<target>Software</target>
<note>Label to display relay software.</note>
</trans-unit>
<trans-unit id="Someone posted a note" xml:space="preserve">
<source>Someone posted a note</source>
<target>Someone posted a note</target>
@@ -1947,8 +2002,7 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<trans-unit id="Spam" xml:space="preserve">
<source>Spam</source>
<target>Spam</target>
<note>Description of report type for spam.
Section header for Universe/Search spam</note>
<note>Description of report type for spam.</note>
</trans-unit>
<trans-unit id="Staging" xml:space="preserve">
<source>Staging</source>
@@ -2000,23 +2054,23 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Take Photo</target>
<note>Option to take a photo with the camera</note>
</trans-unit>
<trans-unit id="Test (localhost)" xml:space="preserve">
<source>Test (localhost)</source>
<target>Test (localhost)</target>
<note>Label indicating a localhost test environment for Damus Purple functionality (Developer feature)</note>
<trans-unit id="Test (local)" xml:space="preserve">
<source>Test (local)</source>
<target>Test (local)</target>
<note>Label indicating a local test environment for Damus Purple functionality (Developer feature)</note>
</trans-unit>
<trans-unit id="Text Truncation" xml:space="preserve">
<source>Text Truncation</source>
<target>Text Truncation</target>
<note>Section header for damus text truncation user configuration</note>
</trans-unit>
<trans-unit id="Thank you very much for signing up for Damusu{00A0}Purple. Your contribution helps us continue our fight for a more Open and Freeu{00A0}internet.&#10;&#10;You will also get access to premium features, and a star badge on your profile.&#10;&#10;Enjoy!" xml:space="preserve">
<source>Thank you very much for signing up for Damusu{00A0}Purple. Your contribution helps us continue our fight for a more Open and Freeu{00A0}internet.
<trans-unit id="Thank you very much for signing up for Damus Purple. Your contribution helps us continue our fight for a more Open and Free internet.&#10;&#10;You will also get access to premium features, and a star badge on your profile.&#10;&#10;Enjoy!" xml:space="preserve">
<source>Thank you very much for signing up for Damus Purple. Your contribution helps us continue our fight for a more Open and Free internet.
You will also get access to premium features, and a star badge on your profile.
Enjoy!</source>
<target>Thank you very much for signing up for Damusu{00A0}Purple. Your contribution helps us continue our fight for a more Open and Freeu{00A0}internet.
<target>Thank you very much for signing up for Damus Purple. Your contribution helps us continue our fight for a more Open and Free internet.
You will also get access to premium features, and a star badge on your profile.
@@ -2045,15 +2099,20 @@ You're all set!</source>
You're all set!</target>
<note>An error message that appears when the user attempts to add a relay that has already been added.</note>
</trans-unit>
<trans-unit id="There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@" xml:space="preserve">
<source>There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@</source>
<target>There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@</target>
<note>In-app purchase error message for the user</note>
</trans-unit>
<trans-unit id="There was an error loading your account. Please try again later. If problem persists, please contact us at support@damus.io" xml:space="preserve">
<source>There was an error loading your account. Please try again later. If problem persists, please contact us at support@damus.io</source>
<target>There was an error loading your account. Please try again later. If problem persists, please contact us at support@damus.io</target>
<note>Error label when Purple account information fails to load</note>
</trans-unit>
<trans-unit id="This is a paid relay, you must pay for notes to be accepted." xml:space="preserve">
<source>This is a paid relay, you must pay for notes to be accepted.</source>
<target>This is a paid relay, you must pay for notes to be accepted.</target>
<note>Footer description that explains that the relay server requires payment to post.</note>
<trans-unit id="This device's in-app purchase is registered to a different Nostr account. Unable to manage this Purple account. If you believe this was a mistake, please contact us via support@damus.io." xml:space="preserve">
<source>This device's in-app purchase is registered to a different Nostr account. Unable to manage this Purple account. If you believe this was a mistake, please contact us via support@damus.io.</source>
<target>This device's in-app purchase is registered to a different Nostr account. Unable to manage this Purple account. If you believe this was a mistake, please contact us via support@damus.io.</target>
<note>Notice label that user cannot manage their In-App purchases</note>
</trans-unit>
<trans-unit id="This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective." xml:space="preserve">
<source>This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective.</source>
@@ -2084,6 +2143,11 @@ Nice to meet you all! #introductions #plebchain </target>
<target>Thread</target>
<note>Navigation bar title for note thread.</note>
</trans-unit>
<trans-unit id="Threads" xml:space="preserve">
<source>Threads</source>
<target>Threads</target>
<note>Section header title for a list of threads that are muted.</note>
</trans-unit>
<trans-unit id="To continue your Purple subscription checkout, please verify your npub by clicking on the button below" xml:space="preserve">
<source>To continue your Purple subscription checkout, please verify your npub by clicking on the button below</source>
<target>To continue your Purple subscription checkout, please verify your npub by clicking on the button below</target>
@@ -2148,7 +2212,8 @@ Nice to meet you all! #introductions #plebchain </target>
<trans-unit id="URL" xml:space="preserve">
<source>URL</source>
<target>URL</target>
<note>Example URL to LibreTranslate server</note>
<note>Custom URL host for Damus Purple testing
Example URL to LibreTranslate server</note>
</trans-unit>
<trans-unit id="Unable to find a QR Code" xml:space="preserve">
<source>Unable to find a QR Code</source>
@@ -2180,16 +2245,16 @@ Nice to meet you all! #introductions #plebchain </target>
<target>Unmute</target>
<note>Button to unmute a profile.</note>
</trans-unit>
<trans-unit id="Unmute Hashtag" xml:space="preserve">
<source>Unmute Hashtag</source>
<target>Unmute Hashtag</target>
<note>Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.</note>
</trans-unit>
<trans-unit id="Unmute conversation" xml:space="preserve">
<source>Unmute conversation</source>
<target>Unmute conversation</target>
<note>Context menu option for unmuting a conversation.</note>
</trans-unit>
<trans-unit id="Untitled" xml:space="preserve">
<source>Untitled</source>
<target>Untitled</target>
<note>Text indicating that the long-form note title is untitled.</note>
</trans-unit>
<trans-unit id="Upload" xml:space="preserve">
<source>Upload</source>
<target>Upload</target>
@@ -2215,6 +2280,16 @@ Nice to meet you all! #introductions #plebchain </target>
<target>Username</target>
<note>Label for Username section of user profile form.</note>
</trans-unit>
<trans-unit id="Users" xml:space="preserve">
<source>Users</source>
<target>Users</target>
<note>Section header title for a list of muted users.</note>
</trans-unit>
<trans-unit id="VERSION" xml:space="preserve">
<source>VERSION</source>
<target>VERSION</target>
<note>Text label indicating which version of the relay software is being run for this Nostr relay.</note>
</trans-unit>
<trans-unit id="Verified!" xml:space="preserve">
<source>Verified!</source>
<target>Verified!</target>
@@ -2228,8 +2303,7 @@ Nice to meet you all! #introductions #plebchain </target>
<trans-unit id="Version" xml:space="preserve">
<source>Version</source>
<target>Version</target>
<note>Label to display relay software version.
Section title for displaying the version number of the Damus app.</note>
<note>Section title for displaying the version number of the Damus app.</note>
</trans-unit>
<trans-unit id="View QR Code" xml:space="preserve">
<source>View QR Code</source>
@@ -2276,10 +2350,21 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<trans-unit id="Wallet" xml:space="preserve">
<source>Wallet</source>
<target>Wallet</target>
<note>Navigation title for Wallet view
<note>Navigation title for attaching Nostr Wallet Connect lightning wallet.
Navigation title for Wallet view
Sidebar menu label for Wallet view.
Title for section in zap settings that controls the Lightning wallet selection.</note>
</trans-unit>
<trans-unit id="Wallet Address" xml:space="preserve">
<source>Wallet Address</source>
<target>Wallet Address</target>
<note>Label text indicating that below it is the wallet address.</note>
</trans-unit>
<trans-unit id="Wallet Relay" xml:space="preserve">
<source>Wallet Relay</source>
<target>Wallet Relay</target>
<note>Label text indicating that below it is the information about the wallet relay.</note>
</trans-unit>
<trans-unit id="Website" xml:space="preserve">
<source>Website</source>
<target>Website</target>
@@ -2335,6 +2420,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>Why we need Nostr?</target>
<note>Heading text for section describing why Nostr is needed.</note>
</trans-unit>
<trans-unit id="Words" xml:space="preserve">
<source>Words</source>
<target>Words</target>
<note>Section header title for a list of words that are muted.</note>
</trans-unit>
<trans-unit id="Yes" xml:space="preserve">
<source>Yes</source>
<target>Yes</target>
@@ -2365,6 +2455,21 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>Your Name</target>
<note>Label for Your Name section of user profile form.</note>
</trans-unit>
<trans-unit id="Your Purple subscription expires in %@ days. Renew?" xml:space="preserve">
<source>Your Purple subscription expires in %@ days. Renew?</source>
<target>Your Purple subscription expires in %@ days. Renew?</target>
<note>A notification message explaining to the user that their Damus Purple Subscription is expiring soon, prompting them to renew.</note>
</trans-unit>
<trans-unit id="Your Purple subscription expires in 1 day. Renew?" xml:space="preserve">
<source>Your Purple subscription expires in 1 day. Renew?</source>
<target>Your Purple subscription expires in 1 day. Renew?</target>
<note>A notification message explaining to the user that their Damus Purple Subscription is expiring in one day, prompting them to renew.</note>
</trans-unit>
<trans-unit id="Your Purple subscription has expired. Renew?" xml:space="preserve">
<source>Your Purple subscription has expired. Renew?</source>
<target>Your Purple subscription has expired. Renew?</target>
<note>A notification message explaining to the user that their Damus Purple Subscription has expired, prompting them to renew.</note>
</trans-unit>
<trans-unit id="Your report will be sent to the relays you are connected to" xml:space="preserve">
<source>Your report will be sent to the relays you are connected to</source>
<target>Your report will be sent to the relays you are connected to</target>
@@ -2400,8 +2505,7 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<trans-unit id="Zap failed" xml:space="preserve">
<source>Zap failed</source>
<target>Zap failed</target>
<note>Button label indicating that a zap action was unsuccessful (i.e. the user was unable to send a Bitcoin tip via the lightning network to the user shown on-screen)
Title of an alert indicating that a zap action failed</note>
<note>Button label indicating that a zap action was unsuccessful (i.e. the user was unable to send a Bitcoin tip via the lightning network to the user shown on-screen)</note>
</trans-unit>
<trans-unit id="Zap type" xml:space="preserve">
<source>Zap type</source>
@@ -2460,7 +2564,13 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<trans-unit id="now" xml:space="preserve">
<source>now</source>
<target>now</target>
<note>String indicating that a given timestamp just occurred</note>
<note>Relative time label that indicates a notification happened now
String indicating that a given timestamp just occurred</note>
</trans-unit>
<trans-unit id="npub, #hashtag, phrase" xml:space="preserve">
<source>npub, #hashtag, phrase</source>
<target>npub, #hashtag, phrase</target>
<note>Placeholder example for relay server address.</note>
</trans-unit>
<trans-unit id="nsec1..." xml:space="preserve">
<source>nsec1...</source>
@@ -2587,16 +2697,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>%@ and %@ zapped you</target>
<note>Notification that 2 users zapped the current user's profile</note>
</trans-unit>
<trans-unit id="⚡" xml:space="preserve">
<source>⚡</source>
<target>⚡</target>
<note>Placeholder example for an emoji reaction</note>
</trans-unit>
</body>
</file>
<file original="damus/en-US.lproj/Localizable.stringsdict" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.2" build-num="15C500b"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="/followed_by_three_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
@@ -2659,6 +2764,21 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>%#@IMPORTS@</target>
<note/>
</trans-unit>
<trans-unit id="/quoted_reposts_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@QUOTE_REPOSTS@</source>
<target>%#@QUOTE_REPOSTS@</target>
<note/>
</trans-unit>
<trans-unit id="/quoted_reposts_count:dict/QUOTE_REPOSTS:dict/one:dict/:string" xml:space="preserve">
<source>Quote</source>
<target>Quote</target>
<note/>
</trans-unit>
<trans-unit id="/quoted_reposts_count:dict/QUOTE_REPOSTS:dict/other:dict/:string" xml:space="preserve">
<source>Quotes</source>
<target>Quotes</target>
<note/>
</trans-unit>
<trans-unit id="/reacted_tagged_in_3:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@REACTED@</source>
<target>%#@REACTED@</target>
@@ -2963,7 +3083,7 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
</file>
<file original="DamusNotificationService/InfoPlist.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.2" build-num="15C500b"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -2985,7 +3105,7 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
</file>
<file original="DamusNotificationService/Localizable.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.2" build-num="15C500b"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="" xml:space="preserve">
@@ -3113,10 +3233,10 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target state="new">Staging</target>
<note>Label indicating a staging test environment for Damus Purple functionality (Developer feature)</note>
</trans-unit>
<trans-unit id="Test (localhost)" xml:space="preserve">
<source>Test (localhost)</source>
<target state="new">Test (localhost)</target>
<note>Label indicating a localhost test environment for Damus Purple functionality (Developer feature)</note>
<trans-unit id="Test (local)" xml:space="preserve">
<source>Test (local)</source>
<target state="new">Test (local)</target>
<note>Label indicating a local test environment for Damus Purple functionality (Developer feature)</note>
</trans-unit>
<trans-unit id="This note contains too many items and cannot be rendered" xml:space="preserve">
<source>This note contains too many items and cannot be rendered</source>
@@ -96,8 +96,8 @@
"Staging" : {
"comment" : "Label indicating a staging test environment for Damus Purple functionality (Developer feature)"
},
"Test (localhost)" : {
"comment" : "Label indicating a localhost test environment for Damus Purple functionality (Developer feature)"
"Test (local)" : {
"comment" : "Label indicating a local test environment for Damus Purple functionality (Developer feature)"
},
"This note contains too many items and cannot be rendered" : {
"comment" : "Error message indicating that a note is too big and cannot be rendered"
@@ -226,6 +226,22 @@
<string>Reposts</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>one</key>
<string>Quote</string>
<key>other</key>
<string>Quotes</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
+2 -2
View File
@@ -3,10 +3,10 @@
"project" : "damus.xcodeproj",
"targetLocale" : "en-US",
"toolInfo" : {
"toolBuildNumber" : "15C500b",
"toolBuildNumber" : "15E204a",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.2"
"toolVersion" : "15.3"
},
"version" : "1.0"
}
+18
View File
@@ -254,6 +254,24 @@
<string>Republicaciones</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>one</key>
<string>Cita</string>
<key>many</key>
<string>Citas</string>
<key>other</key>
<string>Citas</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16
View File
@@ -226,6 +226,22 @@
<string>Herplaatsingen</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>one</key>
<string>Citaat</string>
<key>other</key>
<string>Citaten</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
-8
View File
@@ -99,12 +99,4 @@ final class WalletConnectTests: XCTestCase {
XCTAssertEqual(ev.remaining.count, 1)
XCTAssertEqual(ev.remaining[0].relay.url.absoluteString, "ws://127.0.0.1")
}
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.
}
}
}
-7
View File
@@ -29,13 +29,6 @@ class damusTests: XCTestCase {
XCTAssertEqual(pubkey, pubkey_same)
}
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.
}
}
func testRandomBytes() {
let bytes = random_bytes(count: 32)