Compare commits

..

100 Commits

Author SHA1 Message Date
tyiu 1110ffa8af Add workaround to fix note language recognition and reduce wasteful translation requests 2024-01-08 20:28:45 -05:00
Charlie Fish 84cfeb1604 nip42: add initial relay auth support
Lightning-Invoice: lnbc1pjcpaakpp5gjs4f626hf8w6slx84xz3wwhlf309z503rjutckdxv6wwg5ldavsdqqcqzpgxqrrs0fppqjaxxw43p7em4g59vedv7pzl76kt0qyjfsp5qcp9de7a7t8h6zs5mcssfaqp4exrnkehqtg2hf0ary3z5cjnasvs9qyyssq55523e4h3cazhkv7f8jqf5qp0n8spykls49crsu5t3m636u3yj4qdqjkdl2nxf6jet5t2r2pfrxmm8rjpqjd3ylrzqq89m4gqt5l6ycqf92c7h
Closes: https://github.com/damus-io/damus/issues/940
Signed-off-by: Charlie Fish <contact@charlie.fish>
Signed-off-by: William Casarin <jb55@jb55.com>
Changelog-Added: Add NIP-42 relay auth support
2024-01-05 10:36:03 -08:00
Charlie Fish 4c37bfc128 profile: filter events that don’t match pubkey
Closes: https://github.com/damus-io/damus/issues/1846
Lightning-Invoice: lnbc1pjef2gupp5ffv0he47r6s6us9s2pfxy023mx8lutwlh3sq365rzgmmj6efl8nsdqqcqzpgxqrrs0fppq65gwnyvf5pn5zj5ryx9s4n7y58clk7yqsp5v7pa2ges4rgvtt0nh6lnt4cevm8n2ql9p7kqstwfp4wutf8faa8q9qyyssqwx8t9kk0m3jj6vu0kvftl3nc8zqyfl5l8ne058q5dnqyad3cqfz8vdnna5g0vy9f2ttwugc0sr20p0hsem84g8xd85ptnwgmryrf4lqqmygv34
Signed-off-by: Charlie Fish <contact@charlie.fish>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
Changelog-Fixed: Fixed bug where sometimes notes from other profiles appear on profile pages
2024-01-05 10:36:02 -08:00
kernelkind 548af2bf9d localization: add support for purple strings
Add string localizations so the purple landing pages can have multi language support.

LNURL1DP68GURN8GHJ7EM9W3SKCCNE9E3K7MF0D3H82UNVWQHKWUN9V4HXGCTHDC6RZVGR8SW3G

Closes: https://github.com/damus-io/damus/issues/1816
Signed-off-by: kernelkind <kernelkind@gmail.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Reviewed-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-01-05 10:36:02 -08:00
kernelkind 692146fe00 add comma as disallowed char at end of url
Do not include comma as part of a URL if it is followed by whitespace.
This allows users to make lists of URLs.

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

LNURL1DP68GURN8GHJ7EM9W3SKCCNE9E3K7MF0D3H82UNVWQHKWUN9V4HXGCTHDC6RZVGR8SW3G

Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-01-02 12:07:55 -08:00
William Casarin 40134b4365 Merge translations 2024-01-01 14:54:50 -08:00
Daniel D’Aquino 9a547077c1 Hook up Damus Purple translation service
This commit integrates the Damus Purple translation service:
- Automatically handles translation settings change after purchase
- Asks for permission to override translation settings if the user already has translation setup
- Translation settings can be changed with Damus Purple, if desired
- Translation requests working with the Damus API server

Testing
--------

PASS

Device: iPhone 15 simulator
iOS: 17.2
Damus: This commit
Damus Purple API server: `9397201d7d55ddcec4c18fcd337f759b61dce697` running on Ubuntu 22.04 LTS VM (npm run dev)
iOS setting: English set as the only preferred language.
Steps:
1. Enable Damus Purple feature flag on developer settings, set purple localhost mode, and restart app
2. Set translation setting to something other than none (e.g. DeepL)
3. Simulate Damus Purple purchase
4. Check that when dismissing welcome view, a confirmation prompt will ask the user whether they want to switch translator to Damus Purple. PASS
5. Click "Yes".
6. Go to translation settings. Check that translation settings are set to "Purple". PASS
7. Go to a non-English profile. Check that translations appear with "Mock translation" (Which is the translation text provided by the mock translation server). PASS
8. Reinstall app
9. Repeat the test, but this time starting with no translation settings. Make sure that translation settings will automatically switch to Damus Purple. PASS

Feature flag testing
--------------------

PASS

Preconditions: Same as above
Steps:
1. Turn off translation
2. Turn off Damus Purple feature flag
3. Go to translation settings. Make sure that Damus Purple is not an option. PASS

Closes: https://github.com/damus-io/damus/issues/1836
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-01-01 04:44:24 -08:00
transifex-integration[bot] 0d71cc18ad Translate Localizable.stringsdict in sv_SE
100% translated source file: 'Localizable.stringsdict'
on 'sv_SE'.
2023-12-31 07:59:57 +00:00
transifex-integration[bot] d10554ab6c Translate InfoPlist.strings in sv_SE
100% translated source file: 'InfoPlist.strings'
on 'sv_SE'.
2023-12-31 07:58:53 +00:00
transifex-integration[bot] 2656c30832 Translate Localizable.strings in sv_SE
100% translated source file: 'Localizable.strings'
on 'sv_SE'.
2023-12-31 07:56:49 +00:00
Daniel D’Aquino 39b6dfb47e purple: update client to work with damus-api post express refactor
these changes were made to adapt the client-side to the new improvements
done to the Purple API server (See
https://github.com/damus-io/api/issues/1)

Testing
-------

PASS

Device: iPhone 15 Pro simulator
iOS: 17.2
Damus: This commit
damus-api: bda56590be7eb47e21dfd61ab94b17f6a8595d0c
Server: Ubuntu 22.04 (VM)
Setup:
1. On the server, delete the `mdb` database files to start from scratch
2. Enable subscriptions support via developer settings with localhost
   test mode and restart app
3. Get Damus Purple (Purchase it via Xcode test environment)
4. Start server with mock parameters (Run `npm run dev`)
5. Restart app

Steps
-----

1. Post something
2. Gold star should appear beside your name
3. Look at the server logs. There should be some requests to create the
   account (POST), to send the receipt (POST), and to get account
   status. Those three requests should have returned HTTP status 200.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-27 18:42:38 -08:00
Daniel D’Aquino 5ca5420ce2 Damus Purple: Add npub authentication for account management API calls
Testing
---------

PASS

Device: iPhone 15 Pro simulator
iOS: 17.2
Damus: This commit
damus-api: 626fb9665d8d6c576dd635d5224869cd9b69d190
Server: Ubuntu 22.04 (VM)
Setup:
1. On the server, delete the `mdb` database files to start from scratch
2. In iOS, reinstall the app if necessary to make sure there are no in-app purchases
3. Enable subscriptions support via developer settings with localhost test mode and restart app
4. Start server with mock parameters (Run `npm run dev`)

Steps:
1. Open top bar and click on "Purple"
2. Purple screen should appear and show both benefits and the purchase options. PASS
3. Click on "monthly". An Apple screen to confirm purchase should appear. PASS
4. Welcome screen with animation should appear. PASS
5. Click continue and restart app (Due to known issue tracked at damus-io#1814)
6. Post something
7. Gold star should appear beside your name
8. Look at the server logs. There should be some requests to create the account (POST), to send the receipt (POST), and to get account status
9. Go to purple view. There should be some information about the subscription, as well as a "manage" button. PASS
10. Click on "manage" button. An iOS sheet should appear allow the user to unsubscribe or manage their subscription to Damus Purple.

Closes: https://github.com/damus-io/damus/issues/1809
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-24 09:30:26 -08:00
Daniel D’Aquino 4703ed80a7 Damus Purple initial Proof-of-Concept support
This commit includes various code changes necessary to get a basic proof of concept of the feature working.

This is NOT a full working feature yet, only a preliminary prototype/PoC. It includes:
- [X] Basic Storekit configuration
- [X] Basic purchase mechanism
- [X] Basic layout and copywriting
- [X] Basic design
- [X] Manage button (To help user cancel their subscription)
- [X] Thank you confirmation + special welcome view
- [X] Star badge on profile (by checking the Damus Purple API)
- [X] Connection to Damus purple API for fetching account info, registering for an account and sending over the App Store receipt data

The feature sits behind a feature flag which is OFF by default (it can be turned ON via Settings --> Developer settings --> Enable experimental Purple API and restarting the app)

Testing
---------

PASS

Device: iPhone 15 Pro simulator
iOS: 17.0.1
Damus: This commit
damus-api: 59ce44a92cff1c1aaed9886f9befbd5f1053821d
Server: Ubuntu 22.04 (VM)
Setup:
1. On the server, delete the `mdb` database files to start from scratch
2. In iOS, reinstall the app if necessary to make sure there are no in-app purchases
3. Enable subscriptions support via developer settings with localhost test mode and restart app
4. Start server with mock parameters (Run `npm run dev`)

Steps:
1. Open top bar and click on "Purple"
2. Purple screen should appear and show both benefits and the purchase options. PASS
3. Click on "monthly". An Apple screen to confirm purchase should appear. PASS
4. Welcome screen with animation should appear. PASS
5. Click continue and restart app (Due to known issue tracked at https://github.com/damus-io/damus/issues/1814)
6. Post something
7. Gold star should appear beside your name
8. Look at the server logs. There should be some requests to create the account (POST), to send the receipt (POST), and to get account status
9. Go to purple view. There should be some information about the subscription, as well as a "manage" button. PASS
10. Click on "manage" button. An iOS sheet should appear allow the user to unsubscribe or manage their subscription to Damus Purple.

Feature flag testing
--------------------

PASS

Preconditions: Continue from above test
Steps:
1. Disable Damus Purple experiment support on developer settings. Restart the app.
2. Check your post. There should be no star beside your profile name. PASS
3. Check side menu. There should be no "Damus Purple" option. PASS
4. Check server logs. There should be no new requests being done to the server. PASS

Closes: https://github.com/damus-io/damus/issues/1422
2023-12-24 09:30:26 -08:00
Daniel D’Aquino f7e407e030 Fix crash on very large notes
This commit fixes a crash that occured on large notes with several artifacts.

The crash was caused by an empirically observed limitation on the amount
of `Text` objects that can be added together. Adding several `Text`
objects together causes a infinite recursion and subsequent stack
overflow.

The fix applied changes the `CompatibleText` class to store several
items in a list, which concatenates attributed strings when possible to
reducethe amount of `Text` objects used.

This commit also:

- Changes the structure to avoid storing SwiftUI objects on a variable,
  but making it into a computed property instead.

- Renders a nice error message when the note is too large to be rendered
  (instead of crashing)

With this new commit, we can render much larger notes, and the only ones
that will not be displayed are those containing more than 50 custom
hashtags.

Since we do not even have 50 custom hashtag types, the only notes that
won't be rendered are spammy notes that repeat the same hashtags over
and over again.

Testing
-------

PASS

Device: iPhone 13 Mini (Physical device)
iOS: 17.2
Damus: This commit
Setup:

- Local test relay and a test account running on a simulator to post
  those long test notes.

- Local web page server to serve a link to the problematic note.
  (nostr:note1ttfgneka3lt6yuutmr0uls5xd6z975fgdzpfkxwwf40dd38pjcqqvzvxaj)

Steps
-----

1. Click on the link to open the note
2. Check that no crash occurs and that the note is rendered correctly. PASS
3. Click on the note to render the SelectableText view (Different code
   path). Make sure that no crash occurs and that it is rendered
   correctly. PASS
4. On the simulator, post a note with 50 "#bitcoin" hashtags displayed
   (100 items).
5. Open the note on the physical iPhone. Make sure that no crash occurs
   and that the note is rendered correctly. PASS
6. Ensure that the hashtag and hashtag icons are rendered. PASS
7. Click on the note and make sure that the SelectableText view is
   rendered correctly. PASS[1]
8. On the simulator, post a note with 51 "#bitcoin" hashtags displayed
   (102 items).
9. Open the note on the physical iPhone. Make sure that a nice error
   message is displayed. PASS
10. Click on the note and make sure that the SelectableText view is
    rendered correctly. PASS[1]

Notes

[1] On the SelectableText view, special hashtags always render with the
purple color. This behavior was already present before the changes, so
it is not a regression.

Changelog-Fixed: Fix crash on very large notes
Closes: https://github.com/damus-io/damus/issues/1826
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-24 09:27:46 -08:00
kernelkind e547e26d99 Handle period at end of URL
Fix parsing URL when encountering a period at the end of the url by
setting it as disallowed from being present at the end of a
URL.

Some characters are disallowed to be present at the end of URLs.
Presently, the period character is the only disallowed character.
A character is the last character in the URL if it is followed by
is_whitespace() or if it's the last character in the string.

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

LNURL1DP68GURN8GHJ7EM9W3SKCCNE9E3K7MF0D3H82UNVWQHKWUN9V4HXGCTHDC6RZVGR8SW3G

Signed-off-by: kernelkind <kernelkind@gmail.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-22 14:00:15 -08:00
kunigaku f6044a9eea Fix Issue #1820 Hashtags including U+5009 to U+500D are not correctly parsed
Closes: https://github.com/damus-io/damus/pull/1830
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-22 13:59:31 -08:00
Daniel D’Aquino 26bd50c948 Revert "nostrdb: close database when backgrounded"
This reverts commit da2bdad18d.

This commit was reverted because it was causing crashes (Occasional `EXC_BAD_ACCESS` errors when accessing database transactions), and because the push notification extension uses Ndb in a read-only manner, which means we no longer need these changes

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2023-12-22 13:57:21 -08:00
Daniel D’Aquino 6e0af0ba10 tests: Disable NostrScriptTests.test_bool_set to reduce noise on CI/CD
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-22 13:56:57 -08:00
transifex-integration[bot] 44b7ae2054 Translate Localizable.strings in zh_HK
100% translated source file: 'Localizable.strings'
on 'zh_HK'.
2023-12-21 15:15:36 +00:00
transifex-integration[bot] 9cc21fc860 Translate Localizable.strings in zh_TW
100% translated source file: 'Localizable.strings'
on 'zh_TW'.
2023-12-21 15:10:49 +00:00
transifex-integration[bot] c9526b7aa6 Translate Localizable.strings in zh_TW
100% translated source file: 'Localizable.strings'
on 'zh_TW'.
2023-12-21 15:09:44 +00:00
transifex-integration[bot] 055b7af1a3 Translate Localizable.strings in zh_TW
100% translated source file: 'Localizable.strings'
on 'zh_TW'.
2023-12-21 15:09:26 +00:00
transifex-integration[bot] de0b1dbda2 Translate Localizable.strings in zh_CN
100% translated source file: 'Localizable.strings'
on 'zh_CN'.
2023-12-21 15:06:43 +00:00
transifex-integration[bot] 7605af84b5 Translate Localizable.strings in zh_CN
100% translated source file: 'Localizable.strings'
on 'zh_CN'.
2023-12-21 14:58:13 +00:00
transifex-integration[bot] d45eadef35 Translate Localizable.stringsdict in zh_HK
100% translated source file: 'Localizable.stringsdict'
on 'zh_HK'.
2023-12-21 14:54:30 +00:00
transifex-integration[bot] 9cae934062 Translate InfoPlist.strings in zh_HK
100% translated source file: 'InfoPlist.strings'
on 'zh_HK'.
2023-12-21 14:54:10 +00:00
transifex-integration[bot] 7fb5cdf6c0 Translate InfoPlist.strings in zh_TW
100% translated source file: 'InfoPlist.strings'
on 'zh_TW'.
2023-12-21 14:53:43 +00:00
transifex-integration[bot] 49bbe62d2a Translate Localizable.stringsdict in zh_TW
100% translated source file: 'Localizable.stringsdict'
on 'zh_TW'.
2023-12-21 14:51:53 +00:00
transifex-integration[bot] 705accd309 Translate Localizable.strings in zh_CN
100% translated source file: 'Localizable.strings'
on 'zh_CN'.
2023-12-21 12:49:48 +00:00
transifex-integration[bot] 3375ccc4fa Translate InfoPlist.strings in zh_CN
100% translated source file: 'InfoPlist.strings'
on 'zh_CN'.
2023-12-21 12:25:37 +00:00
transifex-integration[bot] a9fecc3047 Translate Localizable.stringsdict in zh_CN
100% translated source file: 'Localizable.stringsdict'
on 'zh_CN'.
2023-12-21 12:23:51 +00:00
transifex-integration[bot] 31b3ad9825 Translate Localizable.strings in fa
100% translated source file: 'Localizable.strings'
on 'fa'.
2023-12-20 18:13:21 +00:00
transifex-integration[bot] 2bde3a9217 Translate Localizable.stringsdict in fa
100% translated source file: 'Localizable.stringsdict'
on 'fa'.
2023-12-20 15:26:19 +00:00
transifex-integration[bot] 6050116314 Translate InfoPlist.strings in fa
100% translated source file: 'InfoPlist.strings'
on 'fa'.
2023-12-20 15:23:11 +00:00
kernelkind a2cac142c0 Remove private key leak warning
Remove the private key leak warning in the DMChatView and PostView since
they are no longer needed since nsec is automatically converted into
npub.

Changelog-Removed: Removed old nsec key warning, nsec automatically convert to npub when posting
Signed-off-by: kernelkind <kernelkind@gmail.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-18 10:28:31 -08:00
kernelkind 8a20e5845e Remove line break at the end of direct messages
Closes: https://github.com/damus-io/damus/issues/1599
Changelog-Fixed: Remove extra space at the end of DM messages
Signed-off-by: kernelkind <kernelkind@gmail.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-18 10:24:10 -08:00
William Casarin 641e2564fb Merge remote-tracking branch 'github/translations' 2023-12-18 10:24:01 -08:00
transifex-integration[bot] 50810033c0 Translate Localizable.stringsdict in es_ES
100% translated source file: 'Localizable.stringsdict'
on 'es_ES'.
2023-12-17 16:51:02 +00:00
transifex-integration[bot] fab4e231b6 Translate Localizable.strings in es_ES
100% translated source file: 'Localizable.strings'
on 'es_ES'.
2023-12-17 16:50:15 +00:00
transifex-integration[bot] 42ff49a803 Translate Localizable.strings in es_ES
100% translated source file: 'Localizable.strings'
on 'es_ES'.
2023-12-17 16:50:09 +00:00
transifex-integration[bot] 8c878cbc4c Translate InfoPlist.strings in es_ES
100% translated source file: 'InfoPlist.strings'
on 'es_ES'.
2023-12-17 16:16:51 +00:00
transifex-integration[bot] face4268bf Translate Localizable.strings in es_419
100% translated source file: 'Localizable.strings'
on 'es_419'.
2023-12-17 16:12:23 +00:00
transifex-integration[bot] fed6c47835 Translate Localizable.strings in es_419
100% translated source file: 'Localizable.strings'
on 'es_419'.
2023-12-17 16:09:43 +00:00
transifex-integration[bot] 8e9fb308f9 Translate Localizable.strings in es_419
100% translated source file: 'Localizable.strings'
on 'es_419'.
2023-12-17 16:08:55 +00:00
transifex-integration[bot] 89b48db92d Translate Localizable.stringsdict in es_419
100% translated source file: 'Localizable.stringsdict'
on 'es_419'.
2023-12-17 16:06:28 +00:00
transifex-integration[bot] 9581cc994d Translate Localizable.strings in es_419
100% translated source file: 'Localizable.strings'
on 'es_419'.
2023-12-17 16:05:30 +00:00
kernelkind 34e32bc930 handle extra slashes for relay url
Closes: https://github.com/damus-io/damus/issues/1766

Signed-off-by: kernelkind <kernelkind@gmail.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-16 17:43:00 -08:00
ericholguin dfcef0ba95 refactor: rename show image references to blur images
Closes: https://github.com/damus-io/damus/pull/1745
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-13 11:02:15 -08:00
William Casarin 3c11ba53ce add transifex mailmap 2023-12-13 11:02:15 -08:00
transifex-integration[bot] 9759787c95 Translate Localizable.stringsdict in hu_HU
100% translated source file: 'Localizable.stringsdict'
on 'hu_HU'.
2023-12-12 12:09:45 +00:00
transifex-integration[bot] eef428ce4f Translate Localizable.strings in hu_HU
100% translated source file: 'Localizable.strings'
on 'hu_HU'.
2023-12-12 12:09:10 +00:00
transifex-integration[bot] d69647e071 Translate InfoPlist.strings in hu_HU
100% translated source file: 'InfoPlist.strings'
on 'hu_HU'.
2023-12-12 11:47:23 +00:00
Daniel D’Aquino c22f5e90a3 Add regional relay suggestions to Relay Config View
This patch makes sure that regional relays (e.g. Japan relays) are suggested to the relevant users on the Relay configuration view.

It also makes some small improvements to the recommended relays view layout, to allow it to comfortably show more than 4 relay suggestions without "squeezing" items.

Testing
-------

PASS

Devices:
- iPhone 15 Pro simulator running iOS 17.0.1
- iPad Air 5th generation running iOS 16.4
Damus: This commit
Configuration: No relays selected, region set to Canada
Steps:
1. Go to Relay Config view
2. Check that recommended relays are: Damus, nos.lol, nostr.wine, nostr.land. PASS
3. Change region to Japan on settings
4. Open relay config view again
5. Check that Japan relays are suggested on top of the list presented in step 2. PASS
6. Check that layout looks ok, not broken. PASS
7. Check that it is possible to scroll horizontally to see all the options. PASS
8. Add some relays. Check that it is possible to add relays. PASS
9. Check that borders of the recommended relay view fades away nicely (not just clip). PASS
10. Remove some relays. Check that they return to the suggested relays list. PASS

Real device smoke test
----------------------

Device: iPhone 13 mini running iOS 17.1
Damus: This commit
Configuration: Several relays selected. Region set to Canada.
Steps:
1. Check relay config view does not look broken. PASS
2. Remove some recommended relays. Check that they display nicely on the recommended view. PASS
3. Scroll horizontally. Scrolling should work and layout should not be broken. PASS
4. Add those recommended relays again. Should work. PASS

Closes: https://github.com/damus-io/damus/issues/1730
Changelog-Added: Add regional relay recommendations to Relay configuration view (currently for Japanese users only)
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-11 14:59:56 -08:00
ericholguin f2fe02032e refactor: add customizable properties to neutral button style
Closes: https://github.com/damus-io/damus/pull/1805
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>
2023-12-11 14:59:33 -08:00
William Casarin da2bdad18d nostrdb: close database when backgrounded
Otherwise iOS gets mad because we are holding onto a lockfile in a
shared container which is apparently not allowed.

Fixes: a1e6be214e ("Migrate NostrDB files to shared app group file container")
2023-12-11 14:59:33 -08:00
William Casarin c7cc8df5ba nostrdb: don't begin query if we have a bad lmdb env
In some weird multithreaded situations after we close the database,
this can be an issue.
2023-12-11 14:59:33 -08:00
William Casarin 92df446d72 Update Translations 2023-12-11 14:59:33 -08:00
transifex-integration[bot] 7ea2af6172 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2023-12-11 16:24:29 +00:00
transifex-integration[bot] 184eea6e68 Translate Localizable.stringsdict in pl_PL
100% translated source file: 'Localizable.stringsdict'
on 'pl_PL'.
2023-12-11 09:54:47 +00:00
transifex-integration[bot] 82372d1bf5 Translate InfoPlist.strings in pl_PL
100% translated source file: 'InfoPlist.strings'
on 'pl_PL'.
2023-12-11 09:53:49 +00:00
transifex-integration[bot] 39f59eb798 Translate Localizable.stringsdict in ja
100% translated source file: 'Localizable.stringsdict'
on 'ja'.
2023-12-11 06:13:42 +00:00
transifex-integration[bot] 639deec1a2 Translate Localizable.strings in ja
100% translated source file: 'Localizable.strings'
on 'ja'.
2023-12-11 06:12:32 +00:00
transifex-integration[bot] 18780002bb Translate Localizable.stringsdict in fi
100% translated source file: 'Localizable.stringsdict'
on 'fi'.
2023-12-10 16:32:31 +00:00
transifex-integration[bot] 722180bb9a Translate Localizable.stringsdict in fi
100% translated source file: 'Localizable.stringsdict'
on 'fi'.
2023-12-10 16:32:24 +00:00
transifex-integration[bot] 366a584934 Translate Localizable.stringsdict in fi
100% translated source file: 'Localizable.stringsdict'
on 'fi'.
2023-12-10 16:32:15 +00:00
transifex-integration[bot] 9ee09c3b59 Translate Localizable.stringsdict in fi
100% translated source file: 'Localizable.stringsdict'
on 'fi'.
2023-12-10 16:32:03 +00:00
transifex-integration[bot] e8caf3a7f4 Translate Localizable.stringsdict in fi
100% translated source file: 'Localizable.stringsdict'
on 'fi'.
2023-12-10 16:31:50 +00:00
transifex-integration[bot] b4ff6ee614 Translate Localizable.stringsdict in fi
100% translated source file: 'Localizable.stringsdict'
on 'fi'.
2023-12-10 16:31:35 +00:00
transifex-integration[bot] ed652db3d3 Translate InfoPlist.strings in es_419
100% translated source file: 'InfoPlist.strings'
on 'es_419'.
2023-12-09 23:36:14 +00:00
transifex-integration[bot] 3d01c29148 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2023-12-09 17:39:48 +00:00
transifex-integration[bot] 1c1bb599ed Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2023-12-09 17:39:42 +00:00
transifex-integration[bot] 25e6c77d9b Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2023-12-09 17:39:18 +00:00
transifex-integration[bot] 5aae81c47d Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2023-12-09 17:39:10 +00:00
transifex-integration[bot] 6b2fd4cec1 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2023-12-09 17:37:59 +00:00
transifex-integration[bot] d486af6704 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2023-12-09 17:22:50 +00:00
transifex-integration[bot] 323f920848 Translate InfoPlist.strings in fi
100% translated source file: 'InfoPlist.strings'
on 'fi'.
2023-12-09 15:08:51 +00:00
transifex-integration[bot] c58c200acb Translate Localizable.strings in fi
100% translated source file: 'Localizable.strings'
on 'fi'.
2023-12-09 15:06:07 +00:00
transifex-integration[bot] c3786bf849 Translate Localizable.stringsdict in vi
100% translated source file: 'Localizable.stringsdict'
on 'vi'.
2023-12-09 14:56:42 +00:00
transifex-integration[bot] a0e882db64 Translate InfoPlist.strings in vi
100% translated source file: 'InfoPlist.strings'
on 'vi'.
2023-12-09 14:55:29 +00:00
transifex-integration[bot] eedf734dae Translate Localizable.strings in vi
100% translated source file: 'Localizable.strings'
on 'vi'.
2023-12-09 14:55:06 +00:00
transifex-integration[bot] cfa06797b7 Translate Localizable.stringsdict in ko
100% translated source file: 'Localizable.stringsdict'
on 'ko'.
2023-12-09 11:43:35 +00:00
transifex-integration[bot] 824279742c Translate Localizable.strings in ko
100% translated source file: 'Localizable.strings'
on 'ko'.
2023-12-09 11:42:54 +00:00
transifex-integration[bot] 2cdbadd09d Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2023-12-09 10:25:50 +00:00
transifex-integration[bot] 1ea70c8427 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2023-12-09 10:25:31 +00:00
transifex-integration[bot] 09876c06d0 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2023-12-09 08:49:20 +00:00
transifex-integration[bot] 7a063f8aa0 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2023-12-09 08:49:02 +00:00
transifex-integration[bot] b8ac026a3d Translate InfoPlist.strings in de
100% translated source file: 'InfoPlist.strings'
on 'de'.
2023-12-09 08:48:06 +00:00
transifex-integration[bot] c18853c957 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2023-12-09 08:45:58 +00:00
transifex-integration[bot] 0a09dbfe1c Translate InfoPlist.strings in ko
100% translated source file: 'InfoPlist.strings'
on 'ko'.
2023-12-09 08:10:15 +00:00
transifex-integration[bot] 78e840734a Translate InfoPlist.strings in nl
100% translated source file: 'InfoPlist.strings'
on 'nl'.
2023-12-08 10:06:05 +00:00
transifex-integration[bot] 1ccb300dd1 Translate InfoPlist.strings in nl
100% translated source file: 'InfoPlist.strings'
on 'nl'.
2023-12-08 10:05:59 +00:00
transifex-integration[bot] 049a32db41 Translate Localizable.stringsdict in nl
100% translated source file: 'Localizable.stringsdict'
on 'nl'.
2023-12-08 10:05:15 +00:00
transifex-integration[bot] d82add1080 Translate InfoPlist.strings in ja
100% translated source file: 'InfoPlist.strings'
on 'ja'.
2023-12-08 08:01:06 +00:00
tyiu 9d42715d76 Fix localization issues and export strings for translation 2023-12-07 15:13:57 -08:00
transifex-integration[bot] 14fd06c052 Translate Localizable.strings in cs
100% translated source file: 'Localizable.strings'
on 'cs'.
2023-12-07 14:46:17 -08:00
transifex-integration[bot] e2ab3a41b4 Translate InfoPlist.strings in cs
100% translated source file: 'InfoPlist.strings'
on 'cs'.
2023-12-07 14:46:17 -08:00
transifex-integration[bot] 6d7c2af504 Translate Localizable.strings in zh_HK
100% translated source file: 'Localizable.strings'
on 'zh_HK'.
2023-12-07 14:46:17 -08:00
transifex-integration[bot] f5fbd1d3c1 Translate Localizable.stringsdict in zh_HK
100% translated source file: 'Localizable.stringsdict'
on 'zh_HK'.
2023-12-07 14:46:16 -08:00
transifex-integration[bot] c4333280dd Translate Localizable.stringsdict in zh_TW
100% translated source file: 'Localizable.stringsdict'
on 'zh_TW'.
2023-12-07 14:46:16 -08:00
transifex-integration[bot] 6b6a98b71f Translate Localizable.stringsdict in zh_CN
100% translated source file: 'Localizable.stringsdict'
on 'zh_CN'.
2023-12-07 14:46:15 -08:00
158 changed files with 3429 additions and 382 deletions
+1
View File
@@ -4,3 +4,4 @@ Suhail Saqan <suhail.saqan@gmail.com> <43693074+suhailsaqan@users.noreply.github
cr0bar <cr0bar@cr0.bar> <cr0bar@users.noreply.github.com>
Swift <scoder1747@gmail.com> <120697811+scoder1747@users.noreply.github.com>
Daniel D'Aquino <daniel@daquino.me> <patches@damus.io>
Transifex <transifex@transifex.com> <43880903+transifex-integration[bot]@users.noreply.github.com>
+125
View File
@@ -0,0 +1,125 @@
{
"identifier" : "64C21A2D",
"nonRenewingSubscriptions" : [
],
"products" : [
],
"settings" : {
"_applicationInternalID" : "1628663131",
"_developerTeamID" : "XK7H4JAB3D",
"_failTransactionsEnabled" : false,
"_lastSynchronizedDate" : 704848066.26849198,
"_locale" : "en_US",
"_storefront" : "USA",
"_storeKitErrors" : [
{
"current" : null,
"enabled" : false,
"name" : "Load Products"
},
{
"current" : null,
"enabled" : false,
"name" : "Purchase"
},
{
"current" : null,
"enabled" : false,
"name" : "Verification"
},
{
"current" : null,
"enabled" : false,
"name" : "App Store Sync"
},
{
"current" : null,
"enabled" : false,
"name" : "Subscription Status"
},
{
"current" : null,
"enabled" : false,
"name" : "App Transaction"
},
{
"current" : null,
"enabled" : false,
"name" : "Manage Subscriptions Sheet"
},
{
"current" : null,
"enabled" : false,
"name" : "Refund Request Sheet"
},
{
"current" : null,
"enabled" : false,
"name" : "Offer Code Redeem Sheet"
}
]
},
"subscriptionGroups" : [
{
"id" : "21283177",
"localizations" : [
],
"name" : "Purple",
"subscriptions" : [
{
"adHocOffers" : [
],
"codeOffers" : [
],
"displayPrice" : "6.99",
"familyShareable" : false,
"groupNumber" : 1,
"internalID" : "6446591615",
"introductoryOffer" : null,
"localizations" : [
{
"description" : "Support damus development with Damus Purple!",
"displayName" : "Damus Purple",
"locale" : "en_CA"
}
],
"productID" : "purple",
"recurringSubscriptionPeriod" : "P1M",
"referenceName" : "Purple",
"subscriptionGroupID" : "21283177",
"type" : "RecurringSubscription"
},
{
"adHocOffers" : [
],
"codeOffers" : [
],
"displayPrice" : "69.99",
"familyShareable" : false,
"groupNumber" : 2,
"internalID" : "6448764101",
"introductoryOffer" : null,
"localizations" : [
],
"productID" : "purpleyearly",
"recurringSubscriptionPeriod" : "P1Y",
"referenceName" : "Purple Yearly",
"subscriptionGroupID" : "21283177",
"type" : "RecurringSubscription"
}
]
}
],
"version" : {
"major" : 3,
"minor" : 0
}
}
+2
View File
@@ -16,12 +16,14 @@ damus implements the following [Nostr Implementation Possibilities][nips]
- [NIP-08: Mentions][nip08]
- [NIP-10: Reply conventions][nip10]
- [NIP-12: Generic tag queries (hashtags)][nip12]
- [NIP-42: Authentication of clients to relays][nip42]
[nips]: https://github.com/nostr-protocol/nips
[nip01]: https://github.com/nostr-protocol/nips/blob/master/01.md
[nip08]: https://github.com/nostr-protocol/nips/blob/master/08.md
[nip10]: https://github.com/nostr-protocol/nips/blob/master/10.md
[nip12]: https://github.com/nostr-protocol/nips/blob/master/12.md
[nip42]: https://github.com/nostr-protocol/nips/blob/master/42.md
## Getting Started on Damus
+45 -2
View File
@@ -485,11 +485,37 @@ static inline int parse_str(struct cursor *cur, const char *str) {
return 1;
}
static inline int is_whitespace(char c) {
static inline int is_whitespace(int c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
static inline int is_underscore(char c) {
static inline int next_char_is_whitespace(unsigned char *curChar, unsigned char *endChar) {
unsigned char * next = curChar + 1;
if(next > endChar) return 0;
else if(next == endChar) return 1;
return is_whitespace(*next);
}
static int char_disallowed_at_end_url(char c){
return c == '.' || c == ',';
}
static inline int is_final_url_char(unsigned char *curChar, unsigned char *endChar){
if(is_whitespace(*curChar)){
return 1;
}
else if(next_char_is_whitespace(curChar, endChar)) {
// next char is whitespace so this char could be the final char in the url
return char_disallowed_at_end_url(*curChar);
}
else{
// next char isn't whitespace so it can't be a final char
return 0;
}
}
static inline int is_underscore(int c) {
return c == '_';
}
@@ -670,6 +696,23 @@ static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
return or_end;
}
static inline int consume_until_end_url(struct cursor *cur, int or_end) {
char c;
int consumedAtLeastOne = 0;
while (cur->p < cur->end) {
c = *cur->p;
if (is_final_url_char(cur->p, cur->end))
return consumedAtLeastOne;
cur->p++;
consumedAtLeastOne = 1;
}
return or_end;
}
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
char c;
int consumedAtLeastOne = 0;
+3 -3
View File
@@ -117,7 +117,7 @@ static int consume_url_fragment(struct cursor *cur)
cur->p++;
return consume_until_whitespace(cur, 1);
return consume_until_end_url(cur, 1);
}
static int consume_url_path(struct cursor *cur)
@@ -134,7 +134,7 @@ static int consume_url_path(struct cursor *cur)
while (cur->p < cur->end) {
c = *cur->p;
if (c == '?' || c == '#' || is_whitespace(c)) {
if (c == '?' || c == '#' || is_final_url_char(cur->p, cur->end)) {
return 1;
}
@@ -152,7 +152,7 @@ static int consume_url_host(struct cursor *cur)
while (cur->p < cur->end) {
c = *cur->p;
// TODO: handle IDNs
if (is_alphanumeric(c) || c == '.' || c == '-')
if ((is_alphanumeric(c) || c == '.' || c == '-') && !is_final_url_char(cur->p, cur->end))
{
count++;
cur->p++;
+70
View File
@@ -25,6 +25,8 @@
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */; };
3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */; };
3A9ADA302B4CB5F400756AA0 /* TranslatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9ADA2F2B4CB5F400756AA0 /* TranslatorTests.swift */; };
3A9ADA322B4CBFD000756AA0 /* BlocksExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9ADA312B4CBFD000756AA0 /* BlocksExtensionTests.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@@ -260,6 +262,7 @@
4C9B0DF32A65C46800CBDA21 /* ProfileEditButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */; };
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; };
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */; };
4C9D6D1B2B1D35D7004E5CD9 /* PullDownSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */; };
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
@@ -374,6 +377,7 @@
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4CF38C882A9442DC00BE01B6 /* UserStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */; };
4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFD502E2A2DA45800A229DB /* MediaView.swift */; };
4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */; };
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; };
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; };
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */; };
@@ -418,6 +422,11 @@
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; };
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B501062C2B363036003874F5 /* AuthIntegrationTests.swift */; };
B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */; };
B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */; };
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C652B312C3700A232C0 /* NostrAuth.swift */; };
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
@@ -439,6 +448,9 @@
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; };
D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; };
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; };
@@ -467,6 +479,7 @@
D79C4C1B2AFEB061003A41B4 /* DamusNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */; };
D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343EF2AD0D77C00CED48B /* SnapshotTesting */; };
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; };
D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90548A2A6AEDEE00811EEC /* NdbNote.swift */; };
D7CCFC082B05834500323D86 /* NoteId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF42A740BB7007AEB17 /* NoteId.swift */; };
D7CCFC0B2B0585EA00323D86 /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9FBB82A6B3B26007E485C /* nostrdb.c */; settings = {COMPILER_FLAGS = "-w"; }; };
@@ -658,6 +671,8 @@
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A9ADA2F2B4CB5F400756AA0 /* TranslatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslatorTests.swift; sourceTree = "<group>"; };
3A9ADA312B4CBFD000756AA0 /* BlocksExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksExtensionTests.swift; sourceTree = "<group>"; };
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
@@ -1033,6 +1048,7 @@
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
4C86F7C32A76C44C00EC0817 /* ZappingNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZappingNotify.swift; sourceTree = "<group>"; };
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachedWalletNotify.swift; sourceTree = "<group>"; };
4C8AE1182A0320BE00B944E6 /* Purple.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Purple.storekit; sourceTree = "<group>"; };
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; };
4C8D00C929DF80350036AF10 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; };
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtags.swift; sourceTree = "<group>"; };
@@ -1058,6 +1074,7 @@
4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditButton.swift; sourceTree = "<group>"; };
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; };
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; };
4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayTabBarNotify.swift; sourceTree = "<group>"; };
4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullDownSearch.swift; sourceTree = "<group>"; };
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
@@ -1184,6 +1201,7 @@
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusView.swift; sourceTree = "<group>"; };
4CFD502E2A2DA45800A229DB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleView.swift; sourceTree = "<group>"; };
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; };
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = "<group>"; };
@@ -1228,6 +1246,11 @@
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; };
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanNSECView.swift; sourceTree = "<group>"; };
B501062C2B363036003874F5 /* AuthIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthIntegrationTests.swift; sourceTree = "<group>"; usesTabs = 0; };
B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReconnectRelaysNotify.swift; sourceTree = "<group>"; };
B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayAuthenticationDetail.swift; sourceTree = "<group>"; };
B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = "<group>"; };
B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
@@ -1249,6 +1272,9 @@
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; };
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = "<group>"; };
@@ -1262,6 +1288,7 @@
D79C4C162AFEB061003A41B4 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DamusNotificationService.entitlements; sourceTree = "<group>"; };
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP98AuthenticatedRequest.swift; sourceTree = "<group>"; };
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = "<group>"; };
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
@@ -1410,6 +1437,7 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
D74F43082B23F09300425B75 /* Purple */,
BA3759882ABCCDE30018D73B /* Camera */,
4C190F1E2A535FC200027FD5 /* Zaps */,
4C54AA0829A55416003E4487 /* Notifications */,
@@ -1799,6 +1827,7 @@
4C1A9A2829DDF53B00516EAC /* Video */,
4C1A9A1B29DDCF8B00516EAC /* Settings */,
4CFF8F6129CC9A80008DB934 /* Images */,
4CFF8F5729C9FD07008DB934 /* Purple */,
4CCEB7AC29B53D180078AA28 /* Search */,
4C30AC7029A5676F00E2BD5A /* Notifications */,
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
@@ -1883,6 +1912,8 @@
4C2B7BF12A71B6540049DEE7 /* Id.swift */,
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */,
D798D22B2B086C7400234419 /* NostrEvent+.swift */,
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */,
B57B4C652B312C3700A232C0 /* NostrAuth.swift */,
);
path = Nostr;
sourceTree = "<group>";
@@ -2024,6 +2055,8 @@
isa = PBXGroup;
children = (
4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */,
B5B4D1422B37D47600844320 /* NdbExtensions.swift */,
3A9ADA2F2B4CB5F400756AA0 /* TranslatorTests.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -2032,6 +2065,7 @@
isa = PBXGroup;
children = (
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */,
4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */,
4C1253552A76C8C60004F4B8 /* BroadcastNotify.swift */,
4C1253512A76C6130004F4B8 /* ComposeNotify.swift */,
4CA352AD2A76C1AC003BB08B /* FollowedNotify.swift */,
@@ -2059,6 +2093,7 @@
4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */,
4C4E137A2A76D5FB00BDD832 /* MuteThreadNotify.swift */,
4C4E137C2A76D63600BDD832 /* UnmuteThreadNotify.swift */,
B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */,
);
path = Notify;
sourceTree = "<group>";
@@ -2241,6 +2276,7 @@
4C32B9362A9AD44700DC3548 /* flatbuffers */,
4C9054862A6AEB4500811EEC /* nostrdb */,
4C19AE4A2A5CEF7C00C90DB7 /* nostrscript */,
4C8AE1182A0320BE00B944E6 /* Purple.storekit */,
4C06670728FDE62900038D2A /* damus-c */,
4CE6DEE527F7A08100C66700 /* damus */,
4CE6DEF627F7A08200C66700 /* damusTests */,
@@ -2335,6 +2371,8 @@
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */,
D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */,
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */,
B501062C2B363036003874F5 /* AuthIntegrationTests.swift */,
3A9ADA312B4CBFD000756AA0 /* BlocksExtensionTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -2363,6 +2401,7 @@
isa = PBXGroup;
children = (
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */,
B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */,
);
path = Detail;
sourceTree = "<group>";
@@ -2421,6 +2460,15 @@
path = Posting;
sourceTree = "<group>";
};
4CFF8F5729C9FD07008DB934 /* Purple */ = {
isa = PBXGroup;
children = (
4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */,
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */,
);
path = Purple;
sourceTree = "<group>";
};
4CFF8F6129CC9A80008DB934 /* Images */ = {
isa = PBXGroup;
children = (
@@ -2472,6 +2520,15 @@
path = Mocking;
sourceTree = "<group>";
};
D74F43082B23F09300425B75 /* Purple */ = {
isa = PBXGroup;
children = (
D74F43092B23F0BE00425B75 /* DamusPurple.swift */,
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */,
);
path = Purple;
sourceTree = "<group>";
};
D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = {
isa = PBXGroup;
children = (
@@ -2795,6 +2852,7 @@
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
@@ -2848,6 +2906,7 @@
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */,
B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
@@ -2875,6 +2934,7 @@
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */,
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
@@ -2970,9 +3030,11 @@
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */,
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */,
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */,
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */,
4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
@@ -2992,6 +3054,7 @@
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */,
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */,
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
@@ -3038,6 +3101,7 @@
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */,
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */,
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
@@ -3054,6 +3118,7 @@
4C7D096F2A0AEA0400943473 /* ScannerViewController.swift in Sources */,
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */,
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */,
@@ -3145,6 +3210,7 @@
4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */,
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
@@ -3178,12 +3244,14 @@
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */,
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
3A9ADA322B4CBFD000756AA0 /* BlocksExtensionTests.swift in Sources */,
4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */,
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */,
@@ -3193,6 +3261,7 @@
75AD872B2AA23A460085EF2C /* Block+Tests.swift in Sources */,
F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */,
3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */,
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */,
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
@@ -3202,6 +3271,7 @@
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */,
3A9ADA302B4CB5F400756AA0 /* TranslatorTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -71,6 +71,9 @@
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../../Purple.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Damus dark-gray.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Damus dark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "special-features.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "special-features.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "special-features.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "stars-bg.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "shadow-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "shadow.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

+3
View File
@@ -41,5 +41,8 @@ class DamusColors {
static let neutral1 = Color("DamusNeutral1")
static let neutral3 = Color("DamusNeutral3")
static let neutral6 = Color("DamusNeutral6")
static let pink = Color(red: 211/255.0, green: 76/255.0, blue: 217/255.0)
static let lighterPink = Color(red: 248/255.0, green: 105/255.0, blue: 182/255.0)
static let lightBackgroundPink = Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0)
}
+44 -22
View File
@@ -7,37 +7,49 @@
import SwiftUI
enum NeutralButtonShape {
case rounded, capsule, circle
var style: NeutralButtonStyle {
switch self {
case .rounded:
return NeutralButtonStyle(padding: EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10), cornerRadius: 12)
case .capsule:
return NeutralButtonStyle(padding: EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15), cornerRadius: 20)
case .circle:
return NeutralButtonStyle(padding: EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20), cornerRadius: 9999)
}
}
}
struct NeutralButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
return configuration.label
let padding: EdgeInsets
let cornerRadius: CGFloat
let scaleEffect: CGFloat
init(padding: EdgeInsets = EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0), cornerRadius: CGFloat = 15, scaleEffect: CGFloat = 0.95) {
self.padding = padding
self.cornerRadius = cornerRadius
self.scaleEffect = scaleEffect
}
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(padding)
.background(DamusColors.neutral1)
.cornerRadius(12)
.cornerRadius(cornerRadius)
.overlay(
RoundedRectangle(cornerRadius: 12)
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
.scaleEffect(configuration.isPressed ? scaleEffect : 1)
}
}
struct NeutralCircleButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
return configuration.label
.padding(20)
.background(DamusColors.neutral1)
.cornerRadius(9999)
.overlay(
RoundedRectangle(cornerRadius: 9999)
.stroke(DamusColors.neutral3, lineWidth: 1)
)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
}
}
struct NeutralButtonStyle_Previews: PreviewProvider {
static var previews: some View {
VStack {
Button(action: {
print("dynamic size")
}) {
@@ -45,8 +57,7 @@ struct NeutralButtonStyle_Previews: PreviewProvider {
.padding()
}
.buttonStyle(NeutralButtonStyle())
Button(action: {
print("infinite width")
}) {
@@ -58,6 +69,17 @@ struct NeutralButtonStyle_Previews: PreviewProvider {
}
.buttonStyle(NeutralButtonStyle())
.padding()
Button("Rounded Button", action: {})
.buttonStyle(NeutralButtonShape.rounded.style)
.padding()
Button("Capsule Button", action: {})
.buttonStyle(NeutralButtonShape.capsule.style)
.padding()
Button(action: {}, label: {Image("messages")})
.buttonStyle(NeutralButtonShape.circle.style)
}
}
}
@@ -101,7 +101,7 @@ struct NonImageAvatar<Content: View>: View {
var body: some View {
ZStack {
Circle()
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
.fill(DamusColors.lightBackgroundPink)
.frame(width: 54, height: 54)
content
+3 -3
View File
@@ -64,7 +64,7 @@ struct TranslateView: View {
guard let note_language = translations_model.note_language else {
return
}
let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language)
let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language, purple: damus_state.purple)
DispatchQueue.main.async {
self.translations_model.state = res
}
@@ -125,10 +125,10 @@ struct TranslateView_Previews: PreviewProvider {
}
}
func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String, purple: DamusPurple) async -> TranslateStatus {
// If the note language is different from our preferred languages, send a translation request.
let translator = Translator(settings)
let translator = Translator(settings, purple: purple)
let originalContent = event.get_content(keypair)
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
+47 -22
View File
@@ -71,6 +71,7 @@ struct ContentView: View {
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
@State var muting: Pubkey? = nil
@State var confirm_mute: Bool = false
@State var hide_bar: Bool = false
@State var user_muted_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
@@ -153,25 +154,26 @@ struct ContentView: View {
}
func MainContent(damus: DamusState) -> some View {
ZStack {
if #available(iOS 16.0, *) {
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
.scrollDismissesKeyboard(.immediately)
.opacity(selected_timeline == .search ? 1 : 0)
} else {
// Fallback on earlier versions
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
.opacity(selected_timeline == .search ? 1 : 0)
VStack {
switch selected_timeline {
case .search:
if #available(iOS 16.0, *) {
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
.scrollDismissesKeyboard(.immediately)
} else {
// Fallback on earlier versions
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
}
case .home:
PostingTimelineView
case .notifications:
NotificationsView(state: damus, notifications: home.notifications)
case .dms:
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
}
PostingTimelineView
.opacity(selected_timeline == .home ? 1 : 0)
NotificationsView(state: damus, notifications: home.notifications)
.opacity(selected_timeline == .notifications ? 1 : 0)
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
.opacity(selected_timeline == .dms ? 1 : 0)
}
.navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
.toolbar {
@@ -283,12 +285,17 @@ struct ContentView: View {
}
.navigationViewStyle(.stack)
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
.padding([.bottom], 8)
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
if !hide_bar {
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
.padding([.bottom], 8)
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
} else {
Text("")
}
}
}
.ignoresSafeArea(.keyboard)
.edgesIgnoringSafeArea(hide_bar ? [.bottom] : [])
.onAppear() {
self.connect()
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
@@ -343,6 +350,10 @@ struct ContentView: View {
.onReceive(handle_notify(.compose)) { action in
self.active_sheet = .post(action)
}
.onReceive(handle_notify(.display_tabbar)) { display in
let show = display
self.hide_bar = !show
}
.onReceive(timer) { n in
self.damus_state?.postbox.try_flushing_events()
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
@@ -447,6 +458,9 @@ struct ContentView: View {
break
}
}
.onReceive(handle_notify(.disconnect_relays)) { () in
damus_state.pool.disconnect()
}
.onChange(of: scenePhase) { (phase: ScenePhase) in
switch phase {
case .background:
@@ -616,7 +630,7 @@ struct ContentView: View {
guard let ndb = mndb else { return }
let pool = RelayPool(ndb: ndb)
let pool = RelayPool(ndb: ndb, keypair: keypair)
let model_cache = RelayModelCache()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
@@ -667,8 +681,17 @@ struct ContentView: View {
video: VideoController(),
ndb: ndb
)
home.damus_state = self.damus_state!
if let damus_state, damus_state.settings.enable_experimental_purple_api {
// Assign delegate so that we can send receipts to the Purple API server as soon as we get updates from user's purchases
StoreObserver.standard.delegate = damus_state.purple
}
else {
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
}
pool.connect()
}
@@ -894,6 +917,8 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
}
case .notice:
break
case .auth:
break
}
}
+4
View File
@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict/>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
+33
View File
@@ -34,6 +34,39 @@ struct DamusState {
let music: MusicController?
let video: VideoController
let ndb: Ndb
var purple: DamusPurple
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, muted_threads: MutedThreadsManager, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) {
self.pool = pool
self.keypair = keypair
self.likes = likes
self.boosts = boosts
self.contacts = contacts
self.profiles = profiles
self.dms = dms
self.previews = previews
self.zaps = zaps
self.lnurls = lnurls
self.settings = settings
self.relay_filters = relay_filters
self.relay_model_cache = relay_model_cache
self.drafts = drafts
self.events = events
self.bookmarks = bookmarks
self.postbox = postbox
self.bootstrap_relays = bootstrap_relays
self.replies = replies
self.muted_threads = muted_threads
self.wallet = wallet
self.nav = nav
self.music = music
self.video = video
self.ndb = ndb
self.purple = purple ?? DamusPurple(
environment: settings.purple_api_local_test_mode ? .local_test : .production,
keypair: keypair
)
}
@discardableResult
func add_zap(zap: Zapping) -> Bool {
+2
View File
@@ -63,6 +63,8 @@ class EventsModel: ObservableObject {
break
case .ok:
break
case .auth:
break
case .eose:
let txn = NdbTxn(ndb: self.state.ndb)
load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn)
+2
View File
@@ -91,6 +91,8 @@ class FollowersModel: ObservableObject {
case .ok:
break
case .auth:
break
}
}
}
+2
View File
@@ -446,6 +446,8 @@ class HomeModel {
case .ok:
break
case .auth:
break
}
}
+7
View File
@@ -118,6 +118,11 @@ class ProfileModel: ObservableObject, Equatable {
case .ok:
break
case .event(_, let ev):
// Ensure the event public key matches this profiles public key
// This is done to protect against a relay not properly filtering events by the pubkey
// See https://github.com/damus-io/damus/issues/1846 for more information
guard self.pubkey == ev.pubkey else { break }
add_event(ev)
case .notice:
break
@@ -129,6 +134,8 @@ class ProfileModel: ObservableObject, Equatable {
}
progress += 1
break
case .auth:
break
}
}
}
+197
View File
@@ -0,0 +1,197 @@
//
// DamusPurple.swift
// damus
//
// Created by Daniel DAquino on 2023-12-08.
//
import Foundation
class DamusPurple: StoreObserverDelegate {
let environment: ServerEnvironment
let keypair: Keypair
var starred_profiles_cache: [Pubkey: Bool]
init(environment: ServerEnvironment, keypair: Keypair) {
self.environment = environment
self.keypair = keypair
self.starred_profiles_cache = [:]
}
// MARK: Functions
func is_profile_subscribed_to_purple(pubkey: Pubkey) async -> Bool? {
if let cached_result = self.starred_profiles_cache[pubkey] {
return cached_result
}
guard let data = await self.get_account_data(pubkey: pubkey) else { return nil }
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let active = json["active"] as? Bool {
self.starred_profiles_cache[pubkey] = active
return active
}
return nil
}
func account_exists(pubkey: Pubkey) async -> Bool? {
guard let account_data = await self.get_account_data(pubkey: pubkey) else { return nil }
if let account_info = try? JSONDecoder().decode(AccountInfo.self, from: account_data) {
return account_info.pubkey == pubkey.hex()
}
return false
}
func get_account_data(pubkey: Pubkey) async -> Data? {
let url = environment.get_base_url().appendingPathComponent("accounts/\(pubkey.hex())")
var request = URLRequest(url: url)
request.httpMethod = "GET"
do {
let (data, _) = try await URLSession.shared.data(for: request)
return data
} catch {
print("Failed to fetch data: \(error)")
}
return nil
}
func create_account(pubkey: Pubkey) async throws {
let url = environment.get_base_url().appendingPathComponent("accounts")
Log.info("Creating account with Damus Purple server", for: .damus_purple)
let (data, response) = try await make_nip98_authenticated_request(
method: .post,
url: url,
payload: nil,
payload_type: nil,
auth_keypair: self.keypair
)
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
Log.info("Created an account with Damus Purple server", for: .damus_purple)
default:
Log.error("Error in creating account with Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
}
}
return
}
func create_account_if_not_existing(pubkey: Pubkey) async throws {
guard await !(self.account_exists(pubkey: pubkey) ?? false) else { return }
try await self.create_account(pubkey: pubkey)
}
func send_receipt() async {
// Get the receipt if it's available.
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
try? await create_account_if_not_existing(pubkey: keypair.pubkey)
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let url = environment.get_base_url().appendingPathComponent("accounts/\(keypair.pubkey.hex())/app-store-receipt")
Log.info("Sending in-app purchase receipt to Damus Purple server", for: .damus_purple)
let (data, response) = try await make_nip98_authenticated_request(
method: .post,
url: url,
payload: receiptData,
payload_type: .binary,
auth_keypair: self.keypair
)
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
Log.info("Sent in-app purchase receipt to Damus Purple server successfully", for: .damus_purple)
default:
Log.error("Error in sending in-app purchase receipt to Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
}
}
}
catch {
Log.error("Couldn't read receipt data with error: %s", for: .damus_purple, error.localizedDescription)
}
}
}
func translate(text: String, source source_language: String, target target_language: String) async throws -> String {
var url = environment.get_base_url()
url.append(path: "/translate")
url.append(queryItems: [
.init(name: "source", value: source_language),
.init(name: "target", value: target_language),
.init(name: "q", value: text)
])
let (data, response) = try await make_nip98_authenticated_request(
method: .get,
url: url,
payload: nil,
payload_type: nil,
auth_keypair: self.keypair
)
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
return try JSONDecoder().decode(TranslationResult.self, from: data).text
default:
Log.error("Translation error with Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
throw PurpleError.translation_error(status_code: httpResponse.statusCode, response: data)
}
}
else {
throw PurpleError.translation_no_response
}
}
}
// MARK: API types
extension DamusPurple {
fileprivate struct AccountInfo: Codable {
let pubkey: String
let created_at: UInt64
let expiry: UInt64?
let active: Bool
}
}
// MARK: Helper structures
extension DamusPurple {
enum ServerEnvironment {
case local_test
case production
func get_base_url() -> URL {
switch self {
case .local_test:
Constants.PURPLE_API_TEST_BASE_URL
case .production:
Constants.PURPLE_API_PRODUCTION_BASE_URL
}
}
}
enum PurpleError: Error {
case translation_error(status_code: Int, response: Data)
case translation_no_response
}
struct TranslationResult: Codable {
let text: String
}
}
+33
View File
@@ -0,0 +1,33 @@
//
// StoreObserver.swift
// damus
//
// Created by Daniel DAquino on 2023-12-08.
//
import Foundation
import StoreKit
class StoreObserver: NSObject, SKPaymentTransactionObserver {
static let standard = StoreObserver()
var delegate: StoreObserverDelegate?
init(delegate: StoreObserverDelegate? = nil) {
self.delegate = delegate
super.init()
}
//Observe transaction updates.
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
//Handle transaction states here.
Task {
await self.delegate?.send_receipt()
}
}
}
protocol StoreObserverDelegate {
func send_receipt() async
}
+4
View File
@@ -87,6 +87,8 @@ class SearchHomeModel: ObservableObject {
load_profiles(context: "universe", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn)
}
break
case .auth:
break
}
}
@@ -159,6 +161,8 @@ func load_profiles<Y>(context: String, profiles_subid: String, relay_id: String,
break
case .notice:
break
case .auth:
break
}
}
+3
View File
@@ -130,6 +130,9 @@ func handle_subid_event(pool: RelayPool, relay_id: String, ev: NostrConnectionEv
case .eose(let subid):
return (subid, true)
case .auth:
return (nil, false)
}
}
}
+3
View File
@@ -29,6 +29,7 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
}
case none
case purple
case libretranslate
case deepl
case nokyctranslate
@@ -38,6 +39,8 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
switch self {
case .none:
return .init(tag: self.rawValue, displayName: NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service."))
case .purple:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Damus Purple", comment: "Dropdown option for selecting Damus Purple as a translation service."))
case .libretranslate:
return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service."))
case .deepl:
+10 -2
View File
@@ -107,8 +107,8 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "left_handed", default_value: false)
var left_handed: Bool
@Setting(key: "always_show_images", default_value: false)
var always_show_images: Bool
@Setting(key: "blur_images", default_value: true)
var blur_images: Bool
@Setting(key: "media_previews", default_value: true)
var media_previews: Bool
@@ -201,6 +201,12 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "send_device_token_to_localhost", default_value: false)
var send_device_token_to_localhost: Bool
@Setting(key: "enable_experimental_purple_api", default_value: false)
var enable_experimental_purple_api: Bool
@Setting(key: "purple_api_local_test_mode", default_value: false)
var purple_api_local_test_mode: Bool
@Setting(key: "emoji_reactions", default_value: default_emoji_reactions)
var emoji_reactions: [String]
@@ -290,6 +296,8 @@ class UserSettingsStore: ObservableObject {
switch translation_service {
case .none:
return false
case .purple:
return true
case .libretranslate:
return URLComponents(string: libretranslate_url) != nil
case .deepl:
+2
View File
@@ -66,6 +66,8 @@ class ZapsModel: ObservableObject {
}
self.state.add_zap(zap: .zap(zap))
case .auth:
break
}
@@ -0,0 +1,54 @@
//
// NIP98AuthenticatedRequest.swift
// damus
//
// Created by Daniel DAquino on 2023-12-15.
//
import Foundation
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
enum HTTPPayloadType: String {
case json = "application/json"
case binary = "application/octet-stream"
}
func make_nip98_authenticated_request(method: HTTPMethod, url: URL, payload: Data?, payload_type: HTTPPayloadType?, auth_keypair: Keypair) async throws -> (data: Data, response: URLResponse) {
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.httpBody = payload
var tag_pairs = [
["u", url.absoluteString],
["method", method.rawValue],
]
if let payload {
let payload_hash = sha256(payload)
let payload_hash_hex = hex_encode(payload_hash)
tag_pairs.append(["payload", payload_hash_hex])
}
let auth_note = NdbNote(
content: "",
keypair: auth_keypair,
kind: 27235,
tags: tag_pairs,
createdAt: UInt32(Date().timeIntervalSince1970)
)
let auth_note_json_data: Data = try JSONEncoder().encode(auth_note)
let auth_note_base64: String = base64_encode(auth_note_json_data.bytes)
request.setValue("Nostr " + auth_note_base64, forHTTPHeaderField: "Authorization")
if let payload_type {
request.setValue(payload_type.rawValue, forHTTPHeaderField: "Content-Type")
}
return try await URLSession.shared.data(for: request)
}
+14
View File
@@ -0,0 +1,14 @@
//
// NostrAuth.swift
// damus
//
// Created by Charlie Fish on 12/18/23.
//
import Foundation
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: Relay) -> NostrEvent? {
let tags: [[String]] = [["relay", relay.descriptor.url.id],["challenge", challenge_string]]
let event = NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 22242, tags: tags)
return event
}
+4 -1
View File
@@ -38,7 +38,8 @@ enum NostrRequest {
case subscribe(NostrSubscribe)
case unsubscribe(String)
case event(NostrEvent)
case auth(NostrEvent)
var is_write: Bool {
switch self {
case .subscribe:
@@ -47,6 +48,8 @@ enum NostrRequest {
return false
case .event:
return true
case .auth:
return false
}
}
+14 -1
View File
@@ -23,7 +23,11 @@ enum NostrResponse {
case notice(String)
case eose(String)
case ok(CommandResult)
/// An [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md) `auth` challenge.
///
/// The associated type of this case is the challenge string sent by the server.
case auth(String)
var subid: String? {
switch self {
case .ok:
@@ -34,6 +38,8 @@ enum NostrResponse {
return sub_id
case .notice:
return nil
case .auth(let challenge_string):
return challenge_string
}
}
@@ -94,6 +100,13 @@ enum NostrResponse {
case NDB_TCE_NOTICE:
free(data)
return .notice("")
case NDB_TCE_AUTH:
defer { free(data) }
guard let challenge_string = sized_cstr(cstr: tce.subid, len: tce.subid_len) else {
return nil
}
return .auth(challenge_string)
default:
free(data)
return nil
+31 -2
View File
@@ -57,6 +57,25 @@ enum RelayFlags: Int {
case broken = 1
}
enum RelayAuthenticationError {
/// Only a public key was provided in keypair to sign challenge.
///
/// A private key is required to sign `auth` challenge.
case no_private_key
/// No keypair was provided to sign challenge.
case no_key
}
enum RelayAuthenticationState: Equatable {
/// No `auth` request has been made from this relay
case none
/// We have received an `auth` challenge, but have not yet replied to the challenge
case pending
/// We have received an `auth` challenge and replied with an `auth` event
case verified
/// We received an `auth` challenge but failed to reply to the challenge
case error(RelayAuthenticationError)
}
struct Limitations: Codable {
let payment_required: Bool?
@@ -85,13 +104,15 @@ struct RelayMetadata: Codable {
class Relay: Identifiable {
let descriptor: RelayDescriptor
let connection: RelayConnection
var authentication_state: RelayAuthenticationState
var flags: Int
init(descriptor: RelayDescriptor, connection: RelayConnection) {
self.flags = 0
self.descriptor = descriptor
self.connection = connection
self.authentication_state = RelayAuthenticationState.none
}
var is_broken: Bool {
@@ -109,5 +130,13 @@ enum RelayError: Error {
}
func get_relay_id(_ url: RelayURL) -> String {
return url.url.absoluteString
let trimTrailingSlashes: (String) -> String = { url in
var trimmedUrl = url
while trimmedUrl.hasSuffix("/") {
trimmedUrl.removeLast()
}
return trimmedUrl
}
return trimTrailingSlashes(url.url.absoluteString)
}
+14 -1
View File
@@ -97,7 +97,7 @@ final class RelayConnection: ObservableObject {
socket.send(.string(req))
}
func send(_ req: NostrRequestType) {
func send(_ req: NostrRequestType, callback: ((String) -> Void)? = nil) {
switch req {
case .typical(let req):
guard let req = make_nostr_req(req) else {
@@ -105,9 +105,11 @@ final class RelayConnection: ObservableObject {
return
}
send_raw(req)
callback?(req)
case .custom(let req):
send_raw(req)
callback?(req)
}
}
@@ -201,9 +203,20 @@ func make_nostr_req(_ req: NostrRequest) -> String? {
return make_nostr_unsubscribe_req(sub_id)
case .event(let ev):
return make_nostr_push_event(ev: ev)
case .auth(let ev):
return make_nostr_auth_event(ev: ev)
}
}
func make_nostr_auth_event(ev: NostrEvent) -> String? {
guard let event = encode_json(ev) else {
return nil
}
let encoded = "[\"AUTH\",\(event)]"
print(encoded)
return encoded
}
func make_nostr_push_event(ev: NostrEvent) -> String? {
guard let event = encode_json(ev) else {
return nil
+37 -3
View File
@@ -31,13 +31,17 @@ class RelayPool {
var seen: Set<SeenEvent> = Set()
var counts: [String: UInt64] = [:]
var ndb: Ndb
var keypair: Keypair?
var message_received_function: (((String, RelayDescriptor)) -> Void)?
var message_sent_function: (((String, Relay)) -> Void)?
private let network_monitor = NWPathMonitor()
private let network_monitor_queue = DispatchQueue(label: "io.damus.network_monitor")
private var last_network_status: NWPath.Status = .unsatisfied
init(ndb: Ndb) {
init(ndb: Ndb, keypair: Keypair? = nil) {
self.ndb = ndb
self.keypair = keypair
network_monitor.pathUpdateHandler = { [weak self] path in
if (path.status == .satisfied || path.status == .requiresConnection) && self?.last_network_status != path.status {
@@ -121,6 +125,7 @@ class RelayPool {
else { return }
let _ = self.ndb.process_event(str)
self.message_received_function?((str, desc))
})
let relay = Relay(descriptor: desc, connection: conn)
self.relays.append(relay)
@@ -244,7 +249,9 @@ class RelayPool {
continue
}
relay.connection.send(req)
relay.connection.send(req, callback: { str in
self.message_sent_function?((str, relay))
})
}
}
@@ -298,7 +305,34 @@ class RelayPool {
run_queue(relay_id)
}
}
// Handle auth
if case let .nostr_event(nostrResponse) = event,
case let .auth(challenge_string) = nostrResponse {
if let relay = get_relay(relay_id) {
print("received auth request from \(relay.descriptor.url.id)")
relay.authentication_state = .pending
if let keypair {
if let fullKeypair = keypair.to_full() {
if let authRequest = make_auth_request(keypair: fullKeypair, challenge_string: challenge_string, relay: relay) {
send(.auth(authRequest), to: [relay_id], skip_ephemeral: false)
relay.authentication_state = .verified
} else {
print("failed to make auth request")
}
} else {
print("keypair provided did not contain private key, can not sign auth request")
relay.authentication_state = .error(.no_private_key)
}
} else {
print("no keypair to reply to auth request")
relay.authentication_state = .error(.no_key)
}
} else {
print("no relay found for \(relay_id)")
}
}
for handler in handlers {
handler.callback(relay_id, event)
}
+25
View File
@@ -0,0 +1,25 @@
//
// DisplayTabBarNotify.swift
// damus
//
// Created by William Casarin on 2023-12-01.
//
import Foundation
struct DisplayTabBarNotify: Notify {
typealias Payload = Bool
var payload: Payload
}
extension NotifyHandler {
static var display_tabbar: NotifyHandler<DisplayTabBarNotify> {
.init()
}
}
extension Notifications {
static func display_tabbar(_ payload: Bool) -> Notifications<DisplayTabBarNotify> {
.init(.init(payload: payload))
}
}
+26
View File
@@ -0,0 +1,26 @@
//
// ReconnectRelaysNotify.swift
// damus
//
// Created by Charlie Fish on 12/18/23.
//
import Foundation
struct ReconnectRelaysNotify: Notify {
typealias Payload = ()
var payload: ()
}
extension NotifyHandler {
static var disconnect_relays: NotifyHandler<ReconnectRelaysNotify> {
.init()
}
}
extension Notifications {
/// Reconnects all relays.
static var disconnect_relays: Notifications<ReconnectRelaysNotify> {
.init(.init(payload: ()))
}
}
File diff suppressed because one or more lines are too long
+49 -1
View File
@@ -6,7 +6,7 @@
//
import Foundation
import NaturalLanguage
fileprivate extension String {
/// Failable initializer to build a Swift.String from a C-backed `str_block_t`.
@@ -218,3 +218,51 @@ extension Block {
}
}
}
extension Blocks {
/// Returns a language hypothesis represented as an ``NLLanguage`` (determined by ``NLLanguageRecognizer``),
/// which is the most likely language detected using the combination of blocks. If it cannot determine one, `nil` is returned.
var languageHypothesis: NLLanguage? {
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the blocks are in
// and filter on only the text portions of the content as URLs, hashtags, and anything else confuse the language recognizer.
let originalOnlyText = blocks.compactMap {
if case .text(let txt) = $0 {
// Replacing right single quotation marks () with "typewriter or ASCII apostrophes" (')
// as a workaround to get Apple's language recognizer to predict language the correctly.
// It is important to add this workaround to get the language right because it wastes users' money to send translation requests.
// Until Apple fixes their language model, this workaround will be kept in place.
// See https://en.wikipedia.org/wiki/Apostrophe#Unicode for an explanation of the differences between the two characters.
//
// For example,
// "nevent1qqs0wsknetaju06xk39cv8sttd064amkykqalvfue7ydtg3p0lyfksqzyrhxagf6h8l9cjngatumrg60uq22v66qz979pm32v985ek54ndh8gj42wtp"
// has the note content "Its a meme".
// Without the character replacement, it is 61% confident that the text is in Turkish (tr) and 8% confident that the text is in English (en),
// which is a wildly incorrect hypothesis.
// With the character replacement, it is 65% confident that the text is in English (en) and 24% confident that the text is in Turkish (tr), which is more accurate.
//
// Similarly,
// "nevent1qqspjqlln6wvxrqg6kzl2p7gk0rgr5stc7zz5sstl34cxlw55gvtylgpp4mhxue69uhkummn9ekx7mqpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5qy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsygpx6655ve67vqlcme9ld7ww73pqx7msclhwzu8lqmkhvuluxnyc7yhf3xut"
// has the note content "Youre funner".
// Without the character replacement, it is 52% confident that the text is in Norwegian Bokmål (nb) and 41% confident that the text is in English (en).
// With the character replacement, it is 93% confident that the text is in English (en) and 4% confident that the text is in Norwegian Bokmål (nb).
return txt.replacingOccurrences(of: "", with: "'")
}
else {
return nil
}
}
.joined(separator: " ")
// If there is no text, there's nothing to use to detect language.
guard !originalOnlyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return nil
}
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
return languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key
}
}
+81 -22
View File
@@ -8,37 +8,96 @@
import Foundation
import SwiftUI
// Concatening too many `Text` objects can cause crashes (See https://github.com/damus-io/damus/issues/1826)
fileprivate let MAX_TEXT_ITEMS = 100
class CompatibleText: Equatable {
var text: Text
var attributed: AttributedString
var text: some View {
if items.count > MAX_TEXT_ITEMS {
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"))
.multilineTextAlignment(.center)
}
.foregroundColor(.secondary)
)
}
return AnyView(
items.reduce(Text(""), { (accumulated, item) in
return accumulated + item.render_to_text()
})
)
}
var attributed: AttributedString {
return items.reduce(AttributedString(stringLiteral: ""), { (accumulated, item) in
guard let item_attributed_string = item.attributed_string() else { return accumulated }
return accumulated + item_attributed_string
})
}
var items: [Item]
init() {
self.text = Text("")
self.attributed = AttributedString(stringLiteral: "")
self.items = [.attributed_string(AttributedString(stringLiteral: ""))]
}
init(stringLiteral: String) {
self.text = Text(stringLiteral)
self.attributed = AttributedString(stringLiteral: stringLiteral)
self.items = [.attributed_string(AttributedString(stringLiteral: stringLiteral))]
}
init(text: Text, attributed: AttributedString) {
self.text = text
self.attributed = attributed
}
init(attributed: AttributedString) {
self.text = Text(attributed)
self.attributed = attributed
self.items = [.attributed_string(attributed)]
}
init(items: [Item]) {
self.items = items
}
static func == (lhs: CompatibleText, rhs: CompatibleText) -> Bool {
return lhs.attributed == rhs.attributed
return lhs.items == rhs.items
}
static func +(lhs: CompatibleText, rhs: CompatibleText) -> CompatibleText {
let combinedText = lhs.text + rhs.text
let combinedAttributes = lhs.attributed + rhs.attributed
return CompatibleText(text: combinedText, attributed: combinedAttributes)
if case .attributed_string(let las) = lhs.items.last,
case .attributed_string(let ras) = rhs.items.first
{
// Concatenate attributed strings whenever possible to reduce item count
let combined_attributed_string = las + ras
return CompatibleText(items:
Array(lhs.items.prefix(upTo: lhs.items.count - 1)) +
[.attributed_string(combined_attributed_string)] +
Array(rhs.items.suffix(from: 1))
)
}
else {
return CompatibleText(items: lhs.items + rhs.items)
}
}
}
extension CompatibleText {
enum Item: Equatable {
case attributed_string(AttributedString)
case icon(named: String, offset: CGFloat)
func render_to_text() -> Text {
switch self {
case .attributed_string(let attributed_string):
return Text(attributed_string)
case .icon(named: let image_name, offset: let offset):
return Text(Image(image_name)).baselineOffset(offset)
}
}
func attributed_string() -> AttributedString? {
switch self {
case .attributed_string(let attributed_string):
return attributed_string
case .icon(named: let name, offset: _):
guard let img = UIImage(named: name) else { return nil }
return icon_attributed_string(img: img)
}
}
}
}
+4
View File
@@ -8,5 +8,9 @@
import Foundation
class Constants {
static let PURPLE_API_PRODUCTION_BASE_URL: URL = URL(string: "https://purple.damus.io")!
static let PURPLE_API_TEST_BASE_URL: URL = URL(string: "http://127.0.0.1:8989")!
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info")!
static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")!
static let EXAMPLE_DEMOS: DamusState = .empty
}
+1 -2
View File
@@ -306,7 +306,6 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSet
}
func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current_status: TranslateStatus, settings: UserSettingsStore, note_lang: String?) -> Bool {
switch current_status {
case .havent_tried:
return should_translate(event: event, our_keypair: our_keypair, settings: settings, note_lang: note_lang) && settings.auto_translate
@@ -445,7 +444,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async {
// We have to recheck should_translate here now that we have note_language
if plan.load_translations && should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate
{
translations = await translate_note(profiles: profiles, keypair: our_keypair, event: plan.event, settings: settings, note_lang: note_language)
translations = await translate_note(profiles: profiles, keypair: our_keypair, event: plan.event, settings: settings, note_lang: note_language, purple: state.purple)
}
let ts = translations
+7 -15
View File
@@ -43,28 +43,20 @@ let custom_hashtags: [String: CustomHashtag] = [
func hashtag_str(_ htag: String) -> CompatibleText {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "damus:t:\(htag.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? htag)")
let lowertag = htag.lowercased()
var text = Text(attributedString)
if let custom_hashtag = custom_hashtags[lowertag] {
if let col = custom_hashtag.color {
attributedString.foregroundColor = col
}
let name = custom_hashtag.name
if let img = UIImage(named: "\(name)-hashtag") {
attributedString = attributedString + " "
attributed_string_attach_icon(&attributedString, img: img)
}
text = Text(attributedString)
let img = Image("\(name)-hashtag")
text = text + Text(img).baselineOffset(custom_hashtag.offset ?? 0.0)
attributedString = attributedString + " "
return CompatibleText(items: [.attributed_string(attributedString), .icon(named: "\(name)-hashtag", offset: custom_hashtag.offset ?? 0.0)])
} else {
attributedString.foregroundColor = DamusColors.purple
return CompatibleText(items: [.attributed_string(attributedString)])
}
return CompatibleText(text: text, attributed: attributedString)
}
+1
View File
@@ -14,6 +14,7 @@ enum LogCategory: String {
case render
case storage
case push_notifications
case damus_purple
}
/// Damus structured logger
+3 -2
View File
@@ -7,14 +7,15 @@
import Foundation
let BOOTSTRAP_RELAYS = [
// This is `fileprivate` because external code should use the `get_default_bootstrap_relays` instead.
fileprivate let BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://nostr.wine",
"wss://nos.lol",
]
let REGION_SPECIFIC_BOOTSTRAP_RELAYS: [Locale.Region: [String]] = [
fileprivate let REGION_SPECIFIC_BOOTSTRAP_RELAYS: [Locale.Region: [String]] = [
Locale.Region.japan: [
"wss://relay-jp.nostr.wirednet.jp",
"wss://yabu.me",
+1 -1
View File
@@ -81,7 +81,7 @@ enum Route: Hashable {
case .ZapSettings(let settings):
ZapSettingsView(settings: settings)
case .TranslationSettings(let settings):
TranslationSettingsView(settings: settings)
TranslationSettingsView(settings: settings, damus_state: damusState)
case .ReactionsSettings(let settings):
ReactionsSettingsView(settings: settings)
case .SearchSettings(let settings):
+20 -2
View File
@@ -12,16 +12,30 @@ import FoundationNetworking
public struct Translator {
private let userSettingsStore: UserSettingsStore
private let purple: DamusPurple
private let session = URLSession.shared
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
init(_ userSettingsStore: UserSettingsStore) {
init(_ userSettingsStore: UserSettingsStore, purple: DamusPurple) {
self.userSettingsStore = userSettingsStore
self.purple = purple
}
/// Returns true if the translator should attempt to translate from the source language to the target language and valid translation service settings are configured.
func shouldTranslate(from sourceLanguage: String, to targetLanguage: String) -> Bool {
return sourceLanguage != targetLanguage && userSettingsStore.can_translate
}
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
// Do not attempt to translate if the source and target languages are the same.
guard shouldTranslate(from: sourceLanguage, to: targetLanguage) else {
return nil
}
switch userSettingsStore.translation_service {
case .purple:
return try await translateWithPurple(text, from: sourceLanguage, to: targetLanguage)
case .libretranslate:
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
case .nokyctranslate:
@@ -31,7 +45,7 @@ public struct Translator {
case .deepl:
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
case .none:
return text
return nil
}
}
@@ -90,6 +104,10 @@ public struct Translator {
return response.translations.map { $0.text }.joined(separator: " ")
}
private func translateWithPurple(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
return try await self.purple.translate(text: text, source: sourceLanguage, target: targetLanguage)
}
private func translateWithNoKYCTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
let url = try makeURL("https://translate.nokyctranslate.com", path: "/translate")
+1 -14
View File
@@ -11,7 +11,6 @@ import Combine
struct DMChatView: View, KeyboardReadable {
let damus_state: DamusState
@ObservedObject var dms: DirectMessageModel
@State var showPrivateKeyWarning: Bool = false
var pubkey: Pubkey {
dms.pubkey
@@ -106,11 +105,7 @@ struct DMChatView: View, KeyboardReadable {
Button(
role: .none,
action: {
showPrivateKeyWarning = contentContainsPrivateKey(dms.draft)
if !showPrivateKeyWarning {
send_message()
}
send_message()
}
) {
Label("", image: "send")
@@ -165,14 +160,6 @@ struct DMChatView: View, KeyboardReadable {
dms.draft = ""
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false
}
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
send_message()
}
})
}
}
+3 -3
View File
@@ -39,13 +39,13 @@ struct DMView: View {
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
}
let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
let should_blur_img = should_blur_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
VStack(alignment: .trailing) {
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, options: dm_options)
NoteContentView(damus_state: damus_state, event: event, blur_images: should_blur_img, size: .normal, options: dm_options)
.fixedSize(horizontal: false, vertical: true)
.padding([.top, .leading, .trailing], 10)
.padding([.bottom], 25)
.padding([.bottom], 10)
.background(VisualEffectView(effect: UIBlurEffect(style: .prominent))
.background(is_ours ? Color.accentColor.opacity(0.9) : Color.secondary.opacity(0.15))
)
+4 -7
View File
@@ -18,7 +18,6 @@ struct DirectMessagesView: View {
@State var dm_type: DMType = .friend
@ObservedObject var model: DirectMessagesModel
@ObservedObject var settings: UserSettingsStore
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
func MainContent(requests: Bool) -> some View {
ScrollView {
@@ -91,12 +90,10 @@ struct DirectMessagesView: View {
.tabViewStyle(.page(indexDisplayMode: .never))
}
.toolbar {
if selected_timeline == .dms {
ToolbarItem(placement: .navigationBarTrailing) {
if would_filter_non_friends_from_dms(contacts: damus_state.contacts, dms: self.model.dms) {
FriendsButton(filter: $settings.friend_filter)
}
ToolbarItem(placement: .navigationBarTrailing) {
if would_filter_non_friends_from_dms(contacts: damus_state.contacts, dms: self.model.dms) {
FriendsButton(filter: $settings.friend_filter)
}
}
}
+7 -7
View File
@@ -54,21 +54,21 @@ struct EventView: View {
}
// blame the porn bots for this code
func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool {
if settings.always_show_images {
return true
func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool {
if !settings.blur_images {
return false
}
if ev.pubkey == our_pubkey {
return true
return false
}
if contacts.is_in_friendosphere(ev.pubkey) {
return true
return false
}
if let boost_key = booster_pubkey, contacts.is_in_friendosphere(boost_key) {
return true
return false
}
return false
return true
}
func format_relative_time(_ created_at: UInt32) -> String
+4 -4
View File
@@ -11,19 +11,19 @@ struct EventBody: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
let should_show_img: Bool
let should_blur_img: Bool
let options: EventViewOptions
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil, options: EventViewOptions) {
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_blur_img: Bool? = nil, options: EventViewOptions) {
self.damus_state = damus_state
self.event = event
self.size = size
self.options = options
self.should_show_img = should_show_img ?? should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
self.should_blur_img = should_blur_img ?? should_blur_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
}
var note_content: some View {
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, options: options)
NoteContentView(damus_state: damus_state, event: event, blur_images: should_blur_img, size: size, options: options)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -54,7 +54,7 @@ struct LongformView: View {
EventShell(state: state, event: event.event, options: options) {
SelectableText(attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title)
NoteContentView(damus_state: state, event: event.event, show_images: true, size: .selected, options: options)
NoteContentView(damus_state: state, event: event.event, blur_images: false, size: .selected, options: options)
}
}
}
+2 -2
View File
@@ -45,11 +45,11 @@ struct TextEvent: View {
}
func EvBody(options: EventViewOptions) -> some View {
let show_imgs = should_show_images(settings: damus.settings, contacts: damus.contacts, ev: event, our_pubkey: damus.pubkey)
let blur_imgs = should_blur_images(settings: damus.settings, contacts: damus.contacts, ev: event, our_pubkey: damus.pubkey)
return NoteContentView(
damus_state: damus,
event: event,
show_images: show_imgs,
blur_images: blur_imgs,
size: .normal,
options: options
)
+1 -2
View File
@@ -87,7 +87,6 @@ struct LoginView: View {
}
if let p = parsed {
Button(action: {
Task {
do {
@@ -361,7 +360,7 @@ struct SignInEntry: View {
shouldSaveKey: shouldSaveKey,
privKeyFound: $privKeyFound)
if privKeyFound {
Toggle("Save Key in Secure Keychain", isOn: shouldSaveKey)
Toggle(NSLocalizedString("Save Key in Secure Keychain", comment: "Toggle to save private key to the Apple secure keychain."), isOn: shouldSaveKey)
}
}
}
+23 -15
View File
@@ -26,7 +26,7 @@ struct NoteContentView: View {
let damus_state: DamusState
let event: NostrEvent
@State var show_images: Bool
@State var blur_images: Bool
@State var load_media: Bool = false
let size: EventViewKind
let preview_height: CGFloat?
@@ -40,10 +40,10 @@ struct NoteContentView: View {
return self.artifacts_model.state.artifacts ?? .separated(.just_content(event.get_content(damus_state.keypair)))
}
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, options: EventViewOptions) {
init(damus_state: DamusState, event: NostrEvent, blur_images: Bool, size: EventViewKind, options: EventViewOptions) {
self.damus_state = damus_state
self.event = event
self.show_images = show_images
self.blur_images = blur_images
self.size = size
self.options = options
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
@@ -62,7 +62,7 @@ struct NoteContentView: View {
}
var preview: LinkViewRepresentable? {
guard show_images,
guard blur_images,
case .loaded(let preview) = preview_model.state,
case .value(let cached) = preview else {
return nil
@@ -93,7 +93,7 @@ struct NoteContentView: View {
func previewView(links: [URL]) -> some View {
Group {
if let preview = self.preview, show_images {
if let preview = self.preview, blur_images {
if let preview_height {
preview
.frame(height: preview_height)
@@ -133,18 +133,18 @@ struct NoteContentView: View {
translateView
}
}
if artifacts.media.count > 0 {
if !damus_state.settings.media_previews && !load_media {
loadMediaButton(artifacts: artifacts)
} else if show_images || (show_images && !damus_state.settings.media_previews && load_media) {
} else if !blur_images || (!blur_images && !damus_state.settings.media_previews && load_media) {
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
} else if !show_images || (!show_images && !damus_state.settings.media_previews && load_media) {
} else if blur_images || (blur_images && !damus_state.settings.media_previews && load_media) {
ZStack {
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
Blur()
.onTapGesture {
show_images = true
blur_images = false
}
}
}
@@ -188,7 +188,7 @@ struct NoteContentView: View {
.frame(height: 1)
switch artifacts.media[index] {
case .image(let url), .video(let url):
Text("\(url)")
Text(url.absoluteString)
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
.foregroundStyle(DamusColors.neutral6)
.multilineTextAlignment(.leading)
@@ -208,6 +208,10 @@ struct NoteContentView: View {
}
func load(force_artifacts: Bool = false) {
if case .loading = damus_state.events.get_cache_data(event.id).artifacts_model.state {
return
}
// always reload artifacts on load
let plan = get_preload_plan(evcache: damus_state.events, ev: event, our_keypair: damus_state.keypair, settings: damus_state.settings)
@@ -297,11 +301,15 @@ struct NoteContentView: View {
}
func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) {
let wrapped = icon_attributed_string(img: img)
astr.append(wrapped)
}
func icon_attributed_string(img: UIImage) -> AttributedString {
let attachment = NSTextAttachment()
attachment.image = img
let attachmentString = NSAttributedString(attachment: attachment)
let wrapped = AttributedString(attachmentString)
astr.append(wrapped)
return AttributedString(attachmentString)
}
func url_str(_ url: URL) -> CompatibleText {
@@ -661,17 +669,17 @@ struct NoteContentView_Previews: PreviewProvider {
Group {
VStack {
NoteContentView(damus_state: state, event: test_note, show_images: true, size: .normal, options: [])
NoteContentView(damus_state: state, event: test_note, blur_images: false, size: .normal, options: [])
}
.previewDisplayName("Short note")
VStack {
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, show_images: true, size: .normal, options: [])
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, blur_images: false, size: .normal, options: [])
}
.previewDisplayName("Note with image")
VStack {
NoteContentView(damus_state: state2, event: test_longform_event.event, show_images: true, size: .normal, options: [.wide])
NoteContentView(damus_state: state2, event: test_longform_event.event, blur_images: false, size: .normal, options: [.wide])
.border(Color.red)
}
.previewDisplayName("Long-form note")
@@ -82,8 +82,7 @@ struct NotificationsView: View {
@ObservedObject var notifications: NotificationsModel
@StateObject var filter = NotificationFilter()
@SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
@Environment(\.colorScheme) var colorScheme
var mystery: some View {
@@ -97,6 +97,9 @@ class SuggestedUsersViewModel: ObservableObject {
case .ok:
break
case .auth:
break
}
}
}
+1 -14
View File
@@ -46,7 +46,6 @@ enum PostAction {
struct PostView: View {
@State var post: NSMutableAttributedString = NSMutableAttributedString()
@FocusState var focus: Bool
@State var showPrivateKeyWarning: Bool = false
@State var attach_media: Bool = false
@State var attach_camera: Bool = false
@State var error: String? = nil
@@ -159,11 +158,7 @@ struct PostView: View {
var PostButton: some View {
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
showPrivateKeyWarning = contentContainsPrivateKey(self.post.string)
if !showPrivateKeyWarning {
self.send_post()
}
self.send_post()
}
.disabled(posting_disabled)
.opacity(posting_disabled ? 0.5 : 1.0)
@@ -480,14 +475,6 @@ struct PostView: View {
clear_draft()
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false
}
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
self.send_post()
}
})
}
}
}
+17 -3
View File
@@ -10,12 +10,13 @@ import SwiftUI
/// Profile Name used when displaying an event in the timeline
@MainActor
struct EventProfileName: View {
let damus_state: DamusState
var damus_state: DamusState
let pubkey: Pubkey
@State var display_name: DisplayName?
@State var nip05: NIP05?
@State var donation: Int?
@State var is_purple_user: Bool?
let size: EventViewKind
@@ -25,6 +26,7 @@ struct EventProfileName: View {
self.size = size
let donation = damus.ndb.lookup_profile(pubkey).map({ p in p?.profile?.damus_donation }).value
self._donation = State(wrappedValue: donation)
is_purple_user = nil
}
var friend_type: FriendType? {
@@ -47,7 +49,12 @@ struct EventProfileName: View {
return profile.reactions == false
}
var supporter: Int? {
func supporter_percentage() -> Int? {
if damus_state.settings.enable_experimental_purple_api,
is_purple_user == true {
return 100
}
guard let donation, donation > 0
else {
return nil
@@ -92,7 +99,7 @@ struct EventProfileName: View {
.frame(width: 14, height: 14)
}
if let supporter {
if let supporter = self.supporter_percentage() {
SupporterBadge(percent: supporter)
}
}
@@ -119,6 +126,13 @@ struct EventProfileName: View {
donation = profile.damus_donation
}
}
.onAppear(perform: {
Task {
if damus_state.settings.enable_experimental_purple_api {
is_purple_user = await damus_state.purple.is_profile_subscribed_to_purple(pubkey: self.pubkey) ?? false
}
}
})
}
}
+4 -5
View File
@@ -58,7 +58,7 @@ struct ProfileActionSheetView: View {
.profile_button_style(scheme: colorScheme)
}
)
.buttonStyle(NeutralCircleButtonStyle())
.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"))
.foregroundStyle(.secondary)
.font(.caption)
@@ -121,8 +121,7 @@ struct ProfileActionSheetView: View {
}
)
.buttonStyle(NeutralCircleButtonStyle())
.buttonStyle(NeutralButtonShape.circle.style)
}
.padding()
.padding(.top, 20)
@@ -165,7 +164,7 @@ fileprivate struct ProfileActionSheetFollowButton: View {
}
)
.buttonStyle(NeutralCircleButtonStyle())
.buttonStyle(NeutralButtonShape.circle.style)
Text(verbatim: "\(follow_btn_txt(follow_state, follows_you: follows_you))")
.foregroundStyle(.secondary)
@@ -292,7 +291,7 @@ fileprivate struct ProfileActionSheetZapButton: View {
return true
}
}())
.buttonStyle(NeutralCircleButtonStyle())
.buttonStyle(NeutralButtonShape.circle.style)
Text(button_label)
.foregroundStyle(.secondary)
+455
View File
@@ -0,0 +1,455 @@
//
// DamusPurpleView.swift
// damus
//
// Created by William Casarin on 2023-03-21.
//
import SwiftUI
import StoreKit
fileprivate let damus_products = ["purpleyearly","purple"]
enum ProductState {
case loading
case loaded([Product])
case failed
var products: [Product]? {
switch self {
case .loading:
return nil
case .loaded(let ps):
return ps
case .failed:
return nil
}
}
}
func non_discounted_price(_ product: Product) -> String {
return (product.price * 1.1984569224).formatted(product.priceFormatStyle)
}
enum DamusPurpleType: String {
case yearly = "purpleyearly"
case monthly = "purple"
}
struct PurchasedProduct {
let tx: StoreKit.Transaction
let product: Product
}
struct DamusPurpleView: View {
let damus_state: DamusState
let keypair: Keypair
@State var products: ProductState
@State var purchased: PurchasedProduct? = nil
@State var selection: DamusPurpleType = .yearly
@State var show_welcome_sheet: Bool = false
@State var show_manage_subscriptions = false
@State var show_settings_change_confirmation_dialog = false
@Environment(\.dismiss) var dismiss
init(damus_state: DamusState) {
self._products = State(wrappedValue: .loading)
self.damus_state = damus_state
self.keypair = damus_state.keypair
}
var body: some View {
ZStack {
Rectangle()
.background(.black)
ScrollView {
MainContent
.padding(.top, 75)
.background(content: {
ZStack {
Image("purple-blue-gradient-1")
.offset(CGSize(width: 300.0, height: -0.0))
Image("purple-blue-gradient-1")
.offset(CGSize(width: 300.0, height: -0.0))
}
})
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.onAppear {
notify(.display_tabbar(false))
}
.onDisappear {
notify(.display_tabbar(true))
}
.task {
await load_products()
}
.ignoresSafeArea(.all)
.sheet(isPresented: $show_welcome_sheet, onDismiss: {
update_user_settings_to_purple()
}, content: {
DamusPurpleWelcomeView()
})
.confirmationDialog(
NSLocalizedString("It seems that you already have a translation service configured. Would you like to switch to Damus Purple as your translator?", comment: "Confirmation dialog question asking users if they want their translation settings to be automatically switched to the Damus Purple translation service"),
isPresented: $show_settings_change_confirmation_dialog,
titleVisibility: .visible
) {
Button(NSLocalizedString("Yes", comment: "User confirm Yes")) {
set_translation_settings_to_purple()
}.keyboardShortcut(.defaultAction)
Button(NSLocalizedString("No", comment: "User confirm No"), role: .cancel) {}
}
.manageSubscriptionsSheet(isPresented: $show_manage_subscriptions)
}
func update_user_settings_to_purple() {
if damus_state.settings.translation_service == .none {
set_translation_settings_to_purple()
}
else {
show_settings_change_confirmation_dialog = true
}
}
func set_translation_settings_to_purple() {
damus_state.settings.translation_service = .purple
damus_state.settings.auto_translate = true
}
func handle_transactions(products: [Product]) async {
for await update in StoreKit.Transaction.updates {
switch update {
case .verified(let tx):
let prod = products.filter({ prod in tx.productID == prod.id }).first
if let prod,
let expiration = tx.expirationDate,
Date.now < expiration
{
self.purchased = PurchasedProduct(tx: tx, product: prod)
break
}
case .unverified:
continue
}
}
}
func load_products() async {
do {
let products = try await Product.products(for: damus_products)
self.products = .loaded(products)
await handle_transactions(products: products)
print("loaded products", products)
} catch {
self.products = .failed
print("Failed to fetch products: \(error.localizedDescription)")
}
}
func IconOnBox(_ name: String) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 20.0)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20.0))
.frame(width: 80, height: 80)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(LinearGradient(
colors: [DamusColors.pink, .white.opacity(0), .white.opacity(0.5), .white.opacity(0)],
startPoint: .topLeading,
endPoint: .bottomTrailing), lineWidth: 1)
)
Image(name)
.resizable()
.frame(width: 50, height: 50)
.foregroundColor(.white)
}
}
func Icon(_ name: String) -> some View {
Image(name)
.resizable()
.frame(width: 50, height: 50)
.foregroundColor(.white)
}
func Title(_ txt: String) -> some View {
Text(txt)
.font(.title3)
.bold()
.foregroundColor(.white)
.padding(.bottom, 3)
}
func Subtitle(_ txt: String) -> some View {
Text(txt)
.foregroundColor(.white.opacity(0.65))
}
var ProductLoadError: some View {
Text(NSLocalizedString("Subscription Error", comment: "Ah dang there was an error loading subscription information from the AppStore. Please try again later :("))
.foregroundColor(.white)
}
var SaveText: Text {
Text(NSLocalizedString("Save 14%", comment: "Percentage of purchase price the user will save"))
.font(.callout)
.italic()
.foregroundColor(DamusColors.green)
}
func subscribe(_ product: Product) async throws {
let result = try await product.purchase()
switch result {
case .success(.verified(let tx)):
print("success \(tx.debugDescription)")
show_welcome_sheet = true
case .success(.unverified(let tx, let res)):
print("success unverified \(tx.debugDescription) \(res.localizedDescription)")
show_welcome_sheet = true
case .pending:
break
case .userCancelled:
break
@unknown default:
break
}
switch result {
case .success:
self.damus_state.purple.starred_profiles_cache[keypair.pubkey] = nil
Task {
await self.damus_state.purple.send_receipt()
}
default:
break
}
}
var product: Product? {
return self.products.products?.filter({
prod in prod.id == selection.rawValue
}).first
}
func price_description(product: Product) -> some View {
if product.id == "purpleyearly" {
return (
AnyView(
HStack(spacing: 10) {
Text(NSLocalizedString("Annually", comment: "Annual renewal of purple subscription"))
Spacer()
Text(verbatim: non_discounted_price(product)).strikethrough().foregroundColor(DamusColors.white.opacity(0.5))
Text(verbatim: product.displayPrice).fontWeight(.bold)
}
)
)
} else {
return (
AnyView(
HStack(spacing: 10) {
Text(NSLocalizedString("Monthly", comment: "Monthly renewal of purple subscription"))
Spacer()
Text(verbatim: product.displayPrice).fontWeight(.bold)
}
)
)
}
}
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"))
.font(.callout.bold())
.foregroundColor(.white)
ForEach(products) { product in
Button(action: {
Task { @MainActor in
do {
try await subscribe(product)
} catch {
print(error.localizedDescription)
}
}
}, label: {
price_description(product: product)
})
.buttonStyle(GradientButtonStyle())
}
}
.padding(.horizontal, 20)
}
func PurchasedView(_ purchased: PurchasedProduct) -> some View {
VStack(spacing: 10) {
Text(NSLocalizedString("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"))
.font(.title2)
.foregroundColor(.white)
Text(format_date(UInt32(purchased.tx.purchaseDate.timeIntervalSince1970)))
.foregroundColor(.white)
.opacity(0.65)
if let expiry = purchased.tx.expirationDate {
Text(NSLocalizedString("Renews on", comment: "Indicating when the subscription will renew"))
.font(.title2)
.foregroundColor(.white)
Text(format_date(UInt32(expiry.timeIntervalSince1970)))
.foregroundColor(.white)
.opacity(0.65)
}
Button(action: {
show_manage_subscriptions = true
}, label: {
Text(NSLocalizedString("Manage", comment: "Manage the damus subscription"))
})
.buttonStyle(GradientButtonStyle())
}
}
var ProductStateView: some View {
Group {
switch self.products {
case .failed:
ProductLoadError
case .loaded(let products):
if let purchased {
PurchasedView(purchased)
} else {
ProductsView(products)
}
case .loading:
ProgressView()
.progressViewStyle(.circular)
}
}
}
var MainContent: some View {
VStack {
HStack(spacing: 20) {
Image("damus-dark-logo")
.resizable()
.frame(width: 60, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 15.0))
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(LinearGradient(
colors: [DamusColors.lighterPink.opacity(0.8), .white.opacity(0), DamusColors.deepPurple.opacity(0.6)],
startPoint: .topLeading,
endPoint: .bottomTrailing), lineWidth: 1)
)
.shadow(radius: 5)
VStack(alignment: .leading) {
Text(NSLocalizedString("Purple", comment: "Subscription service name"))
.font(.system(size: 60.0).weight(.bold))
.foregroundStyle(
LinearGradient(
colors: [DamusColors.lighterPink, DamusColors.deepPurple],
startPoint: .bottomLeading,
endPoint: .topTrailing
)
)
.foregroundColor(.white)
.tracking(-2)
}
}
.padding(.bottom, 30)
VStack(alignment: .leading, spacing: 30) {
Subtitle(NSLocalizedString("Help us stay independent in our mission for Freedom tech with our Purple subscription, and look cool doing it!", comment: "Damus purple subscription pitch"))
.multilineTextAlignment(.center)
HStack(spacing: 20) {
IconOnBox("heart.fill")
VStack(alignment: .leading) {
Title(NSLocalizedString("Help Build The Future", comment: "Title for funding future damus development"))
Subtitle(NSLocalizedString("Support Damus development to help build the future of decentralized communication on the web.", comment: "Reason for supporting damus development"))
}
}
HStack(spacing: 20) {
IconOnBox("ai-3-stars.fill")
VStack(alignment: .leading) {
Title(NSLocalizedString("Exclusive features", comment: "Features only available on subscription service"))
.padding(.bottom, -3)
HStack(spacing: 3) {
Image("calendar")
.resizable()
.frame(width: 15, height: 15)
Text(NSLocalizedString("Coming soon", comment: "Feature is still in development and will be available soon"))
.font(.caption)
.bold()
}
.foregroundColor(DamusColors.pink)
.padding(.vertical, 3)
.padding(.horizontal, 8)
.background(DamusColors.lightBackgroundPink)
.cornerRadius(30.0)
Subtitle(NSLocalizedString("Be the first to access upcoming premium features: Automatic translations, longer note storage, and more", comment: "Description of new features to be expected"))
.padding(.top, 3)
}
}
HStack(spacing: 20) {
IconOnBox("badge")
VStack(alignment: .leading) {
Title(NSLocalizedString("Supporter Badge", comment: "Title for supporter badge"))
Subtitle(NSLocalizedString("Get a special badge on your profile to show everyone your contribution to Freedom tech", comment: "Supporter badge description"))
}
}
}
.padding([.trailing, .leading], 30)
.padding(.bottom, 20)
VStack(alignment: .center) {
ProductStateView
}
.padding([.top], 20)
Spacer()
}
}
}
struct DamusPurpleView_Previews: PreviewProvider {
static var previews: some View {
/*
DamusPurpleView(products: [
DamusProduct(name: "Yearly", id: "purpleyearly", price: Decimal(69.99)),
DamusProduct(name: "Monthly", id: "purple", price: Decimal(6.99)),
])
*/
DamusPurpleView(damus_state: test_damus_state)
}
}
@@ -0,0 +1,127 @@
//
// DamusPurpleWelcomeView.swift
// damus
//
// Created by Daniel DAquino on 2023-12-04.
//
import Foundation
import SwiftUI
fileprivate extension Animation {
static func content() -> Animation {
Animation.easeInOut(duration: 1).delay(3)
}
}
struct DamusPurpleWelcomeView: View {
@Environment(\.dismiss) var dismiss
@State var start = false
var body: some View {
VStack {
Image("damus-dark-logo")
.resizable()
.frame(width: 50, height: 50)
.clipShape(RoundedRectangle(cornerRadius: 10.0))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(LinearGradient(
colors: [DamusColors.lighterPink.opacity(0.8), .white.opacity(0), DamusColors.deepPurple.opacity(0.6)],
startPoint: .topLeading,
endPoint: .bottomTrailing), lineWidth: 1)
)
.shadow(radius: 5)
.padding(20)
.opacity(start ? 1.0 : 0.0)
.animation(.content(), value: start)
Text(NSLocalizedString("Welcome to Purple", comment: "Greeting to subscription service"))
.font(.largeTitle)
.fontWeight(.bold)
.foregroundStyle(
LinearGradient(
colors: [.black, .black, DamusColors.pink, DamusColors.lighterPink],
startPoint: start ? .init(x: -3, y: 4) : .bottomLeading,
endPoint: start ? .topTrailing : .init(x: 3, y: -4)
)
)
.opacity(start ? 1.0 : 0.0)
.animation(Animation.easeInOut(duration: 3).delay(0), value: start)
Image(systemName: "star.fill")
.resizable()
.frame(width: 96, height: 90)
.foregroundStyle(
LinearGradient(
colors: [.black, DamusColors.purple, .white, .white],
startPoint: start ? .init(x: -1, y: 1.5) : .bottomLeading,
endPoint: start ? .topTrailing : .init(x: 10, y: -11)
)
)
.animation(Animation.snappy(duration: 3).delay(1), value: start)
.shadow(
color: start ? DamusColors.lightBackgroundPink : DamusColors.purple.opacity(0.3),
radius: start ? 30 : 10
)
.animation(Animation.snappy(duration: 3).delay(0), value: start)
.scaleEffect(x: start ? 1 : 3, y: start ? 1 : 3)
.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"))
.lineSpacing(5)
.multilineTextAlignment(.center)
.foregroundStyle(.white.opacity(0.8))
.padding(.horizontal, 20)
.padding(.top, 50)
.padding(.bottom, 20)
.opacity(start ? 1.0 : 0.0)
.animation(.content(), value: start)
Button(action: {
dismiss()
}, label: {
HStack {
Spacer()
Text(NSLocalizedString("Continue", comment: "Prompt to user to continue"))
Spacer()
}
})
.padding(.horizontal, 30)
.buttonStyle(GradientButtonStyle())
.opacity(start ? 1.0 : 0.0)
.animation(Animation.easeInOut(duration: 2).delay(5), value: start)
}
.background(content: {
ZStack {
Rectangle()
.background(.black)
Image("purple-blue-gradient-1")
.offset(CGSize(width: 300.0, height: -0.0))
.opacity(start ? 1.0 : 0.2)
Image("stars-bg")
.resizable(resizingMode: .stretch)
.frame(width: 500, height: 500)
.offset(x: -100, y: 50)
.scaleEffect(start ? 1 : 1.1)
.animation(Animation.easeOut(duration: 3).delay(0), value: start)
Image("purple-blue-gradient-1")
.offset(CGSize(width: 300.0, height: -0.0))
.opacity(start ? 1.0 : 0.2)
}
})
.onAppear(perform: {
withAnimation(.easeOut(duration: 6), {
start = true
})
})
}
}
struct DamusPurpleWelcomeView_Previews: PreviewProvider {
static var previews: some View {
DamusPurpleWelcomeView()
}
}
@@ -0,0 +1,35 @@
//
// RelayAuthenticationDetail.swift
// damus
//
// Created by Charlie Fish on 12/18/23.
//
import SwiftUI
struct RelayAuthenticationDetail: View {
let state: RelayAuthenticationState
var body: some View {
switch state {
case .none:
EmptyView()
case .pending:
Text(NSLocalizedString("Pending", comment: "Label to display that authentication to a server is pending."))
case .verified:
Text(NSLocalizedString("Authenticated", comment: "Label to display that authentication to a server has succeeded."))
.foregroundStyle(DamusColors.success)
case .error:
Text(NSLocalizedString("Error", comment: "Label to display that authentication to a server has failed."))
.foregroundStyle(DamusColors.danger)
}
}
}
struct RelayAuthenticationDetail_Previews: PreviewProvider {
static var previews: some View {
RelayAuthenticationDetail(state: .none)
RelayAuthenticationDetail(state: .pending)
RelayAuthenticationDetail(state: .verified)
}
}
@@ -23,15 +23,6 @@ struct RecommendedRelayView: View {
self.model_cache = damus.relay_model_cache
}
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in
if damus.pool.get_relay(x) == nil, let url = RelayURL(x) {
xs.append(RelayDescriptor(url: url, info: .rw))
}
}
}
var body: some View {
let meta = model_cache.model(with_relay_id: relay)?.metadata
@@ -84,6 +75,8 @@ struct RecommendedRelayView: View {
HStack {
Text(meta?.name ?? relay)
.lineLimit(1)
.frame(maxWidth: 150)
.padding(.vertical, 5)
}
.contextMenu {
CopyAction(relay: relay)
+23 -4
View File
@@ -23,7 +23,8 @@ struct RelayConfigView: View {
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in
let recommended_relay_addresses = get_default_bootstrap_relays()
return recommended_relay_addresses.reduce(into: rs) { xs, x in
if state.pool.get_relay(x) == nil, let url = RelayURL(x) {
xs.append(RelayDescriptor(url: url, info: .rw))
}
@@ -87,11 +88,29 @@ struct RelayConfigView: View {
.stroke(DamusLightGradient.gradient)
}
HStack(spacing: 20) {
ForEach(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.id)
ScrollView(.horizontal) {
HStack(spacing: 20) {
ForEach(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.id)
}
}
.padding(.horizontal, 30)
.padding(.vertical, 5)
}
.scrollIndicators(.hidden)
.mask(
HStack(spacing: 0) {
LinearGradient(gradient: Gradient(colors: [Color.clear, Color.white]), startPoint: .leading, endPoint: .trailing)
.frame(width: 30)
Rectangle()
.fill(Color.white)
.frame(maxWidth: .infinity)
LinearGradient(gradient: Gradient(colors: [Color.white, Color.clear]), startPoint: .leading, endPoint: .trailing)
.frame(width: 30)
}
)
.padding()
}
.frame(minWidth: 250, maxWidth: .infinity, minHeight: 250, alignment: .center)
+14 -3
View File
@@ -92,7 +92,14 @@ struct RelayDetailView: View {
}
}
}
if let authentication_state: RelayAuthenticationState = relay_object?.authentication_state,
authentication_state != .none {
Section(NSLocalizedString("Authentication", comment: "Header label to display authentication details for a given relay.")) {
RelayAuthenticationDetail(state: authentication_state)
}
}
if let pubkey = nip11?.pubkey {
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
UserViewRow(damus_state: state, pubkey: pubkey)
@@ -175,9 +182,13 @@ struct RelayDetailView: View {
}
return attrString
}
private var relay_object: Relay? {
state.pool.get_relay(relay)
}
private var relay_connection: RelayConnection? {
state.pool.get_relay(relay)?.connection
relay_object?.connection
}
}
+2
View File
@@ -165,6 +165,8 @@ struct SaveKeysView: View {
break
case .ok:
break
case .auth:
break
}
}
}
+2 -2
View File
@@ -55,7 +55,7 @@ struct PullDownSearchView: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
TextField("Search", text: $search_text)
TextField(NSLocalizedString("Search", comment: "Title of the text field for searching."), text: $search_text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: search_text) { query in
debouncer.debounce {
@@ -75,7 +75,7 @@ struct PullDownSearchView: View {
end_editing()
on_cancel()
}, label: {
Text("Cancel")
Text("Cancel", comment: "Button to cancel out of search text entry mode.")
})
}
}
@@ -78,7 +78,7 @@ struct AppearanceSettingsView: View {
// MARK: - Images
Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) {
self.EnableAnimationsToggle
Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images)
Toggle(NSLocalizedString("Blur images", comment: "Setting to blur images"), isOn: $settings.blur_images)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Media previews", comment: "Setting to show media"), isOn: $settings.media_previews)
@@ -17,12 +17,18 @@ struct DeveloperSettingsView: View {
Toggle(NSLocalizedString("Developer Mode", comment: "Setting to enable developer mode"), isOn: $settings.developer_mode)
.toggleStyle(.switch)
if settings.developer_mode {
Toggle("Always show onboarding", isOn: $settings.always_show_onboarding_suggestions)
Toggle(NSLocalizedString("Always show onboarding", comment: "Developer mode setting to always show onboarding suggestions."), isOn: $settings.always_show_onboarding_suggestions)
Toggle("Enable experimental push notifications", isOn: $settings.enable_experimental_push_notifications)
Toggle(NSLocalizedString("Enable experimental push notifications", comment: "Developer mode setting to enable experimental push notifications."), isOn: $settings.enable_experimental_push_notifications)
.toggleStyle(.switch)
Toggle("Send device token to localhost", isOn: $settings.send_device_token_to_localhost)
Toggle(NSLocalizedString("Send device token to localhost", comment: "Developer mode setting to send device token metadata to a local server instead of the damus.io server."), isOn: $settings.send_device_token_to_localhost)
.toggleStyle(.switch)
Toggle("Enable experimental Purple API support", isOn: $settings.enable_experimental_purple_api)
.toggleStyle(.switch)
Toggle("Purple API localhost test mode", isOn: $settings.purple_api_local_test_mode)
.toggleStyle(.switch)
}
}
@@ -9,6 +9,7 @@ import SwiftUI
struct TranslationSettingsView: View {
@ObservedObject var settings: UserSettingsStore
var damus_state: DamusState
@Environment(\.dismiss) var dismiss
@@ -19,11 +20,17 @@ struct TranslationSettingsView: View {
.toggleStyle(.switch)
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
ForEach(TranslationService.allCases, id: \.self) { server in
ForEach(TranslationService.allCases.filter({ settings.enable_experimental_purple_api ? true : $0 != .purple }), id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if settings.translation_service == .purple && settings.enable_experimental_purple_api {
NavigationLink(destination: DamusPurpleView(damus_state: damus_state)) {
Text(NSLocalizedString("Configure Damus Purple", comment: "Button to allow Damus Purple to be configured"))
}
}
if settings.translation_service == .libretranslate {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
@@ -103,6 +110,6 @@ struct TranslationSettingsView: View {
struct TranslationSettingsView_Previews: PreviewProvider {
static var previews: some View {
TranslationSettingsView(settings: UserSettingsStore())
TranslationSettingsView(settings: UserSettingsStore(), damus_state: test_damus_state)
}
}
+12 -10
View File
@@ -39,6 +39,7 @@ struct SideMenuView: View {
.onTapGesture {
isSidebarVisible.toggle()
}
content
}
}
@@ -51,17 +52,18 @@ struct SideMenuView: View {
NavigationLink(value: Route.Wallet(wallet: damus_state.wallet)) {
navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), img: "wallet")
/*
HStack {
Image("wallet")
.tint(DamusColors.adaptableBlack)
}
Text(NSLocalizedString("wallet", comment: "Sidebar menu label for Wallet view."))
.font(.title2)
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
.dynamicTypeSize(.xSmall)
}*/
if damus_state.settings.enable_experimental_purple_api {
NavigationLink(destination: DamusPurpleView(damus_state: damus_state)) {
HStack(spacing: 13) {
Image("nostr-hashtag")
Text("Purple")
.foregroundColor(DamusColors.purple)
.font(.title2.weight(.bold))
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
NavigationLink(value: Route.MuteList(users: get_mutelist_users(damus_state.contacts.mutelist))) {
+3 -6
View File
@@ -102,14 +102,11 @@ struct SuggestedHashtagsView: View {
SingleCharacterAvatar(character: "#")
VStack(alignment: .leading, spacing: 10) {
Text("#\(hashtag)")
Text(verbatim: "#\(hashtag)")
.bold()
Text(self.count != 1 ? String(
format: NSLocalizedString("%d users talking about it", comment: "A label indicating how many users have been talking about a hashtag"),
self.count
) : NSLocalizedString("1 user talking about it", comment: "A label indicating 1 user has been talking about a hashtag"))
.foregroundStyle(.secondary)
Text(pluralizedString(key: "users_talking_about_it", count: self.count))
.foregroundStyle(.secondary)
}
Spacer()
+1 -1
View File
@@ -100,7 +100,7 @@ struct DamusVideoPlayer: View {
private var live_indicator: some View {
VStack {
HStack {
Text("LIVE")
Text("LIVE", comment: "Text indicator that the video is a livestream.")
.bold()
.foregroundColor(.red)
.padding(.horizontal)
Binary file not shown.
Binary file not shown.
+7 -1
View File
@@ -6,6 +6,7 @@
//
import SwiftUI
import StoreKit
@main
struct damusApp: App {
@@ -43,6 +44,9 @@ struct MainView: View {
.onReceive(handle_notify(.logout)) { () in
try? clear_keypair()
keypair = nil
// We need to disconnect and reconnect to all relays when the user signs out
// This is to conform to NIP-42 and ensure we aren't persisting old connections
notify(.disconnect_relays)
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientationTracker.setDeviceMajorAxis()
@@ -61,6 +65,8 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
SKPaymentQueue.default().add(StoreObserver.standard)
return true
}
@@ -84,7 +90,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
// create post request
let url = URL(string: settings.send_device_token_to_localhost ? "http://localhost:8000/user-info" : "https://notify.damus.io:8000/user-info")!
let url = settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL
var request = URLRequest(url: url)
request.httpMethod = "POST"
Binary file not shown.
Binary file not shown.
+16
View File
@@ -258,6 +258,22 @@
<string>%2$@ Sats</string>
</dict>
</dict>
<key>users_talking_about_it</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@USERS@</string>
<key>USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d Benutzer spricht darüber</string>
<key>other</key>
<string>%d Benutzer sprechen darüber</string>
</dict>
</dict>
<key>word_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
+16
View File
@@ -258,6 +258,22 @@
<string>%2$@ sats</string>
</dict>
</dict>
<key>users_talking_about_it</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@USERS@</string>
<key>USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d user talking about it</string>
<key>other</key>
<string>%d users talking about it</string>
</dict>
</dict>
<key>word_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
+341 -25
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.0" build-num="15A240d"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0.1" build-num="15A507"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -21,8 +21,8 @@
<note>Privacy - Media Library Usage Description</note>
</trans-unit>
<trans-unit id="NSCameraUsageDescription" xml:space="preserve">
<source>Damus needs access to your camera if you want to scan QR codes and upload photos</source>
<target>Damus needs access to your camera if you want to scan QR codes and upload photos</target>
<source>Damus needs access to your camera in order to upload photos and scan QR codes.</source>
<target>Damus needs access to your camera in order to upload photos and scan QR codes.</target>
<note>Privacy - Camera Usage Description</note>
</trans-unit>
<trans-unit id="NSFaceIDUsageDescription" xml:space="preserve">
@@ -31,8 +31,8 @@
<note>Privacy - Face ID Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>Damus needs access to your microphone if you want to upload recorded videos from it</source>
<target>Damus needs access to your microphone if you want to upload recorded videos from it</target>
<source>Damus needs access to your microphone for creating video recording posts</source>
<target>Damus needs access to your microphone for creating video recording posts</target>
<note>Privacy - Microphone Usage Description</note>
</trans-unit>
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
@@ -44,7 +44,7 @@
</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.0" build-num="15A240d"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0.1" build-num="15A507"/>
</header>
<body>
<trans-unit id="%@ %@" xml:space="preserve">
@@ -160,6 +160,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Add all</target>
<note>Button label to re-add all original participants as profiles to reply to in a note</note>
</trans-unit>
<trans-unit id="Add an external link" xml:space="preserve">
<source>Add an external link</source>
<target>Add an external link</target>
<note>Placeholder as an example of what the user could set so that the link is opened when the status is tapped.</note>
</trans-unit>
<trans-unit id="Add bookmark" xml:space="preserve">
<source>Add bookmark</source>
<target>Add bookmark</target>
@@ -170,6 +175,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Add relay</target>
<note>Title text to indicate user to an 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="Additional information" xml:space="preserve">
<source>Additional information</source>
<target>Additional information</target>
@@ -185,6 +195,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>All</target>
<note>Label for filter for all notifications.</note>
</trans-unit>
<trans-unit id="All recent notes" xml:space="preserve">
<source>All recent notes</source>
<target>All recent notes</target>
<note>A label indicating that the notes being displayed below it are all recent notes</note>
</trans-unit>
<trans-unit id="Already on Nostr?" xml:space="preserve">
<source>Already on Nostr?</source>
<target>Already on Nostr?</target>
@@ -195,6 +210,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Always show images</target>
<note>Setting to always show and never blur images</note>
</trans-unit>
<trans-unit id="Always show onboarding" xml:space="preserve">
<source>Always show onboarding</source>
<target>Always show onboarding</target>
<note>Developer mode setting to always show onboarding suggestions.</note>
</trans-unit>
<trans-unit id="An additional percentage of each zap will be sent to support Damus development" xml:space="preserve">
<source>An additional percentage of each zap will be sent to support Damus development</source>
<target>An additional percentage of each zap will be sent to support Damus development</target>
@@ -237,6 +257,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<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 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>
@@ -309,11 +334,17 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Broadcast music playing on Apple Music</target>
<note>Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.</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>
<note>Message indicating that the cache was successfully cleared.</note>
</trans-unit>
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Cancel</target>
<note>Alert button to cancel out of alert for muting a user.
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.
@@ -323,6 +354,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
Cancel out of logging out the user.
Text for button to cancel out of connecting Nostr Wallet Connect lightning ewallet.</note>
</trans-unit>
<trans-unit id="Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?" xml:space="preserve">
<source>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</source>
<target>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</target>
<note>Message explaining consequences of changing the 'enable animation' setting</note>
</trans-unit>
<trans-unit id="Choose from Library" xml:space="preserve">
<source>Choose from Library</source>
<target>Choose from Library</target>
@@ -343,6 +379,16 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Clear status</target>
<note>Label to prompt user to select an expiration time for the profile status to clear.</note>
</trans-unit>
<trans-unit id="Clearing Cache" xml:space="preserve">
<source>Clearing Cache</source>
<target>Clearing Cache</target>
<note>Loading message indicating that the cache is being cleared.</note>
</trans-unit>
<trans-unit id="Confirmation" xml:space="preserve">
<source>Confirmation</source>
<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>
@@ -380,6 +426,7 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<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>
<trans-unit id="Copy Account ID" xml:space="preserve">
@@ -547,6 +594,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Disconnect Wallet</target>
<note>Text for button to disconnect from Nostr Wallet Connect lightning wallet.</note>
</trans-unit>
<trans-unit id="Dismiss" xml:space="preserve">
<source>Dismiss</source>
<target>Dismiss</target>
<note>Button to dismiss alert</note>
</trans-unit>
<trans-unit id="Display name" xml:space="preserve">
<source>Display name</source>
<target>Display name</target>
@@ -587,6 +639,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Emoji Reactions</target>
<note>Section title for emoji reactions that are currently added.</note>
</trans-unit>
<trans-unit id="Enable experimental push notifications" xml:space="preserve">
<source>Enable experimental push notifications</source>
<target>Enable experimental push notifications</target>
<note>Developer mode setting to enable experimental push notifications.</note>
</trans-unit>
<trans-unit id="Encrypted" xml:space="preserve">
<source>Encrypted</source>
<target>Encrypted</target>
@@ -632,11 +689,6 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Failed to parse</target>
<note>NostrScript error message when it fails to parse a script.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<target>Filter</target>
<note>Button label text for filtering relay servers.</note>
</trans-unit>
<trans-unit id="Follow" xml:space="preserve">
<source>Follow</source>
<target>Follow</target>
@@ -707,6 +759,22 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Font Size</target>
<note>Section label for font size settings.</note>
</trans-unit>
<trans-unit id="For #Introductions! Im a software developer.&#10;&#10;My side interests include languages and I am striving to be a #polyglot - I am a native English speaker and can speak French, German and Japanese." xml:space="preserve">
<source>For #Introductions! Im a software developer.
My side interests include languages and I am striving to be a #polyglot - I am a native English speaker and can speak French, German and Japanese.</source>
<target>For #Introductions! Im a software developer.
My side interests include languages and I am striving to be a #polyglot - I am a native English speaker and can speak French, German and Japanese.</target>
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
</trans-unit>
<trans-unit id="Found&#10; (qrCodeValue)" xml:space="preserve">
<source>Found
(qrCodeValue)</source>
<target>Found
(qrCodeValue)</target>
<note>Alert message asking if the user wants to open the link.</note>
</trans-unit>
<trans-unit id="Free" xml:space="preserve">
<source>Free</source>
<target>Free</target>
@@ -720,13 +788,27 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<trans-unit id="Get API Key with BTC/Lightning" xml:space="preserve">
<source>Get API Key with BTC/Lightning</source>
<target>Get API Key with BTC/Lightning</target>
<note>Button to navigate to nokyctranslate website to get a translation API key.</note>
<note>Button to navigate to nokyctranslate website to get a translation API key.
Button to navigate to translate.nostr.wine to get a translation API key.</note>
</trans-unit>
<trans-unit id="Hashtags" xml:space="preserve">
<source>Hashtags</source>
<target>Hashtags</target>
<note>Label for filter for seeing only hashtag follows.</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!
This is my first post on Damus, I am happy to meet you all 🤙. Whats up?
#introductions</source>
<target>Hello everybody!
This is my first post on Damus, I am happy to meet you all 🤙. Whats up?
#introductions</target>
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
</trans-unit>
<trans-unit id="Help build the future of decentralized communication on the web." xml:space="preserve">
<source>Help build the future of decentralized communication on the web.</source>
<target>Help build the future of decentralized communication on the web.</target>
@@ -752,6 +834,15 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Home</target>
<note>Navigation bar title for Home view where notes and replies appear from those who the user is following.</note>
</trans-unit>
<trans-unit id="Howdy! Im a graphic designer during the day and coder at night, but Im also trying to spend more time outdoors.&#10;&#10;Hope to meet folks who are on their own journeys to a peaceful and free life!" xml:space="preserve">
<source>Howdy! Im a graphic designer during the day and coder at night, but Im also trying to spend more time outdoors.
Hope to meet folks who are on their own journeys to a peaceful and free life!</source>
<target>Howdy! Im a graphic designer during the day and coder at night, but Im also trying to spend more time outdoors.
Hope to meet folks who are on their own journeys to a peaceful and free life!</target>
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
</trans-unit>
<trans-unit id="Illegal Content" xml:space="preserve">
<source>Illegal Content</source>
<target>Illegal Content</target>
@@ -798,6 +889,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<note>Navigation title for managing keys.
Settings section for managing keys</note>
</trans-unit>
<trans-unit id="LIVE" xml:space="preserve">
<source>LIVE</source>
<target>LIVE</target>
<note>Text indicator that the video is a livestream.</note>
</trans-unit>
<trans-unit id="Learn more about Nostr" xml:space="preserve">
<source>Learn more about Nostr</source>
<target>Learn more about Nostr</target>
@@ -843,6 +939,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Likes</target>
<note>Setting to enable Like Local Notification</note>
</trans-unit>
<trans-unit id="Load media" xml:space="preserve">
<source>Load media</source>
<target>Load media</target>
<note>Button to show media in note.</note>
</trans-unit>
<trans-unit id="Local Notifications" xml:space="preserve">
<source>Local Notifications</source>
<target>Local Notifications</target>
@@ -889,6 +990,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Make sure your nsec account key is saved before you logout or you will lose access to this account</target>
<note>Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.</note>
</trans-unit>
<trans-unit id="Media previews" xml:space="preserve">
<source>Media previews</source>
<target>Media previews</target>
<note>Setting to show media</note>
</trans-unit>
<trans-unit id="Mentioned by %@" xml:space="preserve">
<source>Mentioned by %@</source>
<target>Mentioned by %@</target>
@@ -899,6 +1005,16 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>Mentions</target>
<note>Setting to enable Mention Local Notification</note>
</trans-unit>
<trans-unit id="Merch" xml:space="preserve">
<source>Merch</source>
<target>Merch</target>
<note>Sidebar menu label for merch store link.</note>
</trans-unit>
<trans-unit id="Message" xml:space="preserve">
<source>Message</source>
<target>Message</target>
<note>Button label that allows the user to start a direct message conversation with the user shown on-screen</note>
</trans-unit>
<trans-unit id="Mute" xml:space="preserve">
<source>Mute</source>
<target>Mute</target>
@@ -980,6 +1096,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<target>No one will see that you zapped</target>
<note>Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.</note>
</trans-unit>
<trans-unit id="No results" xml:space="preserve">
<source>No results</source>
<target>No results</target>
<note>A label indicating that note search resulted in no results</note>
</trans-unit>
<trans-unit id="No zaps will be sent, only a lightning payment." xml:space="preserve">
<source>No zaps will be sent, only a lightning payment.</source>
<target>No zaps will be sent, only a lightning payment.</target>
@@ -1028,8 +1149,7 @@ Sentence composed of 2 variables to describe how many reposts. In source English
<trans-unit id="Notes" xml:space="preserve">
<source>Notes</source>
<target>Notes</target>
<note>Label for filter for seeing only your notes (instead of notes and replies).
Label for filter for seeing only notes (instead of notes and replies).</note>
<note>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>
@@ -1068,6 +1188,12 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Nudity</target>
<note>Description of report type for nudity.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<target>OK</target>
<note>Button label indicating user wants to proceed.
Button label to dismiss an error dialog</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
@@ -1098,6 +1224,16 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>OnlyZaps mode</target>
<note>Setting toggle to hide reactions.</note>
</trans-unit>
<trans-unit id="Open in browser" xml:space="preserve">
<source>Open in browser</source>
<target>Open in browser</target>
<note>Button to open the value found in browser.</note>
</trans-unit>
<trans-unit id="Open in wallet" xml:space="preserve">
<source>Open in wallet</source>
<target>Open in wallet</target>
<note>Button to open the value found in browser.</note>
</trans-unit>
<trans-unit id="Optional" xml:space="preserve">
<source>Optional</source>
<target>Optional</target>
@@ -1184,6 +1320,16 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Profile Picture</target>
<note>Label for Profile Picture section of user profile form.</note>
</trans-unit>
<trans-unit id="Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile" xml:space="preserve">
<source>Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile</source>
<target>Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile</target>
<note>Section footer clarifying what the profile action sheet feature does</note>
</trans-unit>
<trans-unit id="Profiles" xml:space="preserve">
<source>Profiles</source>
<target>Profiles</target>
<note>Section title for profile view configuration.</note>
</trans-unit>
<trans-unit id="Public" xml:space="preserve">
<source>Public</source>
<target>Public</target>
@@ -1386,21 +1532,41 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Save Image</target>
<note>Context menu option to save an image.</note>
</trans-unit>
<trans-unit id="Save Key in Secure Keychain" xml:space="preserve">
<source>Save Key in Secure Keychain</source>
<target>Save Key in Secure Keychain</target>
<note>Toggle to save private key to the Apple secure keychain.</note>
</trans-unit>
<trans-unit id="Scan Code" xml:space="preserve">
<source>Scan Code</source>
<target>Scan Code</target>
<note>Button to switch to scan QR Code page.</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>
<note>Text to prompt scanning a QR code of a user's privkey to login to their profile.</note>
</trans-unit>
<trans-unit id="Scan a user's pubkey" xml:space="preserve">
<source>Scan a user's pubkey</source>
<target>Scan a user's pubkey</target>
<note>Text to prompt scanning a QR code of a user's pubkey to open their profile.</note>
</trans-unit>
<trans-unit id="Scan for QR Code" xml:space="preserve">
<source>Scan for QR Code</source>
<target>Scan for QR Code</target>
<note>Context menu option to scan image for a QR Code.</note>
</trans-unit>
<trans-unit id="Scan the code" xml:space="preserve">
<source>Scan the code</source>
<target>Scan the code</target>
<note>Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.</note>
</trans-unit>
<trans-unit id="Search" xml:space="preserve">
<source>Search</source>
<target>Search</target>
<note>Title of the text field for searching.</note>
</trans-unit>
<trans-unit id="Search hashtag: #%@" xml:space="preserve">
<source>Search hashtag: #%@</source>
<target>Search hashtag: #%@</target>
@@ -1447,6 +1613,11 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Send a message with your zap...</target>
<note>Placeholder text for a comment to send as part of a zap to the user.</note>
</trans-unit>
<trans-unit id="Send device token to localhost" xml:space="preserve">
<source>Send device token to localhost</source>
<target>Send device token to localhost</target>
<note>Developer mode setting to send device token metadata to a local server instead of the damus.io server.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<target>Server</target>
@@ -1522,6 +1693,16 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<target>Show only preferred languages on Universe feed</target>
<note>Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages.</note>
</trans-unit>
<trans-unit id="Show profile action sheets" xml:space="preserve">
<source>Show profile action sheets</source>
<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>
@@ -1563,6 +1744,16 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
<note>Description of report type for spam.
Section header for Universe/Search spam</note>
</trans-unit>
<trans-unit id="Staying humble..." xml:space="preserve">
<source>Staying humble...</source>
<target>Staying humble...</target>
<note>Placeholder as an example of what the user could set as their profile status.</note>
</trans-unit>
<trans-unit id="Suggested hashtags" xml:space="preserve">
<source>Suggested hashtags</source>
<target>Suggested hashtags</target>
<note>A label indicating that the items below it are suggested hashtags</note>
</trans-unit>
<trans-unit id="Support Damus" xml:space="preserve">
<source>Support Damus</source>
<target>Support Damus</target>
@@ -1615,6 +1806,15 @@ You're all set!</target>
<target>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.</target>
<note>Warning that the inputted account key is a public key and the result of what happens because of it.</note>
</trans-unit>
<trans-unit id="This is my first post on Nostr 💜. I love drawing and folding Origami!&#10;&#10;Nice to meet you all! #introductions #plebchain " xml:space="preserve">
<source>This is my first post on Nostr 💜. I love drawing and folding Origami!
Nice to meet you all! #introductions #plebchain </source>
<target>This is my first post on Nostr 💜. I love drawing and folding Origami!
Nice to meet you all! #introductions #plebchain </target>
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
</trans-unit>
<trans-unit id="This is your account ID, you can give this to your friends so that they can follow you. Tap to copy." xml:space="preserve">
<source>This is your account ID, you can give this to your friends so that they can follow you. Tap to copy.</source>
<target>This is your account ID, you can give this to your friends so that they can follow you. Tap to copy.</target>
@@ -1635,6 +1835,11 @@ You're all set!</target>
<target>Top Zap</target>
<note>Text indicating that this zap is the one with the highest amount of sats.</note>
</trans-unit>
<trans-unit id="Top hits" xml:space="preserve">
<source>Top hits</source>
<target>Top hits</target>
<note>A label indicating that the notes being displayed below it are all top note search results</note>
</trans-unit>
<trans-unit id="Translate DMs" xml:space="preserve">
<source>Translate DMs</source>
<target>Translate DMs</target>
@@ -1686,6 +1891,11 @@ You're all set!</target>
<target>URL</target>
<note>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>
<target>Unable to find a QR Code</target>
<note>Alert message letting user know a QR Code was not found.</note>
</trans-unit>
<trans-unit id="Unfollow" xml:space="preserve">
<source>Unfollow</source>
<target>Unfollow</target>
@@ -1757,6 +1967,11 @@ You're all set!</target>
<target>View QR Code</target>
<note>Button to switch to view users QR Code</note>
</trans-unit>
<trans-unit id="View full profile" xml:space="preserve">
<source>View full profile</source>
<target>View full profile</target>
<note>A button label that allows the user to see the full profile of the profile they are previewing</note>
</trans-unit>
<trans-unit id="View multiple events per user" xml:space="preserve">
<source>View multiple events per user</source>
<target>View multiple events per user</target>
@@ -1870,6 +2085,7 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<source>Zap</source>
<target>Zap</target>
<note>Accessibility label for zap button
Button label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen
Title of notification when a non-private zap is received.</note>
</trans-unit>
<trans-unit id="Zap User" xml:space="preserve">
@@ -1892,11 +2108,27 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>Zap attempt from connected wallet was canceled.</target>
<note>Message to display when a zap from the user's connected wallet was canceled.</note>
</trans-unit>
<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>
</trans-unit>
<trans-unit id="Zap type" xml:space="preserve">
<source>Zap type</source>
<target>Zap type</target>
<note>Text to indicate that the buttons below it is for choosing the type of zap to send.</note>
</trans-unit>
<trans-unit id="Zapped!" xml:space="preserve">
<source>Zapped!</source>
<target>Zapped!</target>
<note>Button label indicating that a zap action was successful (i.e. the user is successfully sent a Bitcoin tip via the lightning network to the user shown on-screen)</note>
</trans-unit>
<trans-unit id="Zapping" xml:space="preserve">
<source>Zapping</source>
<target>Zapping</target>
<note>Button label indicating that a zap action is in progress (i.e. the user is currently sending a Bitcoin tip via the lightning network to the user shown on-screen)</note>
</trans-unit>
<trans-unit id="Zapping..." xml:space="preserve">
<source>Zapping...</source>
<target>Zapping...</target>
@@ -1911,11 +2143,6 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
Setting to enable Zap Local Notification
Title for section in zap settings that controls general zap preferences.</note>
</trans-unit>
<trans-unit id="https://example.com" xml:space="preserve">
<source>https://example.com</source>
<target>https://example.com</target>
<note>Placeholder as an example of what the user could set so that the link is opened when the status is tapped.</note>
</trans-unit>
<trans-unit id="https://example.com/pic.jpg" xml:space="preserve">
<source>https://example.com/pic.jpg</source>
<target>https://example.com/pic.jpg</target>
@@ -2026,6 +2253,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>self</target>
<note>Part of a larger sentence 'Replying to self' in US English. 'self' indicates that the user is replying to themself and no one else.</note>
</trans-unit>
<trans-unit id="translate.nostr.wine (DeepL, Pay with BTC)" xml:space="preserve">
<source>translate.nostr.wine (DeepL, Pay with BTC)</source>
<target>translate.nostr.wine (DeepL, Pay with BTC)</target>
<note>Dropdown option for selecting translate.nostr.wine as the translation service.</note>
</trans-unit>
<trans-unit id="wallet" xml:space="preserve">
<source>wallet</source>
<target>wallet</target>
@@ -2076,16 +2308,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>⚡</target>
<note>Placeholder example for an emoji reaction</note>
</trans-unit>
<trans-unit id="📋 Working" xml:space="preserve">
<source>📋 Working</source>
<target>📋 Working</target>
<note>Placeholder as an example of what the user could set as their profile status.</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.0" build-num="15A240d"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0.1" build-num="15A507"/>
</header>
<body>
<trans-unit id="/followed_by_three_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
@@ -2328,6 +2555,21 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>%2$@ sats</target>
<note/>
</trans-unit>
<trans-unit id="/users_talking_about_it:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@USERS@</source>
<target>%#@USERS@</target>
<note/>
</trans-unit>
<trans-unit id="/users_talking_about_it:dict/USERS:dict/one:dict/:string" xml:space="preserve">
<source>%d user talking about it</source>
<target>%d user talking about it</target>
<note/>
</trans-unit>
<trans-unit id="/users_talking_about_it:dict/USERS:dict/other:dict/:string" xml:space="preserve">
<source>%d users talking about it</source>
<target>%d users talking about it</target>
<note/>
</trans-unit>
<trans-unit id="/word_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@WORDS@</source>
<target>%#@WORDS@</target>
@@ -2435,4 +2677,78 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
</trans-unit>
</body>
</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.0.1" build-num="15A507"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
<source>DamusNotificationService</source>
<target state="new">DamusNotificationService</target>
<note>Bundle display name</note>
</trans-unit>
<trans-unit id="CFBundleName" xml:space="preserve">
<source>DamusNotificationService</source>
<target state="new">DamusNotificationService</target>
<note>Bundle name</note>
</trans-unit>
<trans-unit id="NSHumanReadableCopyright" xml:space="preserve">
<source/>
<target state="new"/>
<note>Copyright (human-readable)</note>
</trans-unit>
</body>
</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.0.1" build-num="15A507"/>
</header>
<body>
<trans-unit id="(Contents are encrypted)" xml:space="preserve">
<source>(Contents are encrypted)</source>
<target state="new">(Contents are encrypted)</target>
<note>Label on push notification indicating that the contents of the message are encrypted</note>
</trans-unit>
<trans-unit id="Anonymous" xml:space="preserve">
<source>Anonymous</source>
<target state="new">Anonymous</target>
<note>Placeholder display name of anonymous user.</note>
</trans-unit>
<trans-unit id="Any" xml:space="preserve">
<source>Any</source>
<target state="new">Any</target>
<note>Any amount of sats</note>
</trans-unit>
<trans-unit id="New message" xml:space="preserve">
<source>New message</source>
<target state="new">New message</target>
<note>Title label for push notifications where a direct message was sent to the user</note>
</trans-unit>
<trans-unit id="New note reaction" xml:space="preserve">
<source>New note reaction</source>
<target state="new">New note reaction</target>
<note>Title label for push notifications where someone reacted to the user's post with a specific emoji</note>
</trans-unit>
<trans-unit id="Someone posted a note" xml:space="preserve">
<source>Someone posted a note</source>
<target state="new">Someone posted a note</target>
<note>Title label for push notification where someone posted a note</note>
</trans-unit>
<trans-unit id="Someone reacted to your note" xml:space="preserve">
<source>Someone reacted to your note</source>
<target state="new">Someone reacted to your note</target>
<note>Generic title label for push notifications where someone reacted to the user's post</note>
</trans-unit>
<trans-unit id="Someone reacted to your note with %@" xml:space="preserve">
<source>Someone reacted to your note with %@</source>
<target state="new">Someone reacted to your note with %@</target>
<note>Body label for push notifications where someone reacted to the user's post with a specific emoji</note>
</trans-unit>
<trans-unit id="Someone zapped you ⚡️" xml:space="preserve">
<source>Someone zapped you ⚡️</source>
<target state="new">Someone zapped you ⚡️</target>
<note>Title label for a push notification where someone zapped the user</note>
</trans-unit>
</body>
</file>
</xliff>
@@ -0,0 +1,42 @@
{
"sourceLanguage" : "en-US",
"strings" : {
"CFBundleDisplayName" : {
"comment" : "Bundle display name",
"extractionState" : "extracted_with_value",
"localizations" : {
"en-US" : {
"stringUnit" : {
"state" : "new",
"value" : "DamusNotificationService"
}
}
}
},
"CFBundleName" : {
"comment" : "Bundle name",
"extractionState" : "extracted_with_value",
"localizations" : {
"en-US" : {
"stringUnit" : {
"state" : "new",
"value" : "DamusNotificationService"
}
}
}
},
"NSHumanReadableCopyright" : {
"comment" : "Copyright (human-readable)",
"extractionState" : "extracted_with_value",
"localizations" : {
"en-US" : {
"stringUnit" : {
"state" : "new",
"value" : ""
}
}
}
}
},
"version" : "1.0"
}
@@ -0,0 +1,33 @@
{
"sourceLanguage" : "en-US",
"strings" : {
"(Contents are encrypted)" : {
"comment" : "Label on push notification indicating that the contents of the message are encrypted"
},
"Anonymous" : {
"comment" : "Placeholder display name of anonymous user."
},
"Any" : {
"comment" : "Any amount of sats"
},
"New message" : {
"comment" : "Title label for push notifications where a direct message was sent to the user"
},
"New note reaction" : {
"comment" : "Title label for push notifications where someone reacted to the user's post with a specific emoji"
},
"Someone posted a note" : {
"comment" : "Title label for push notification where someone posted a note"
},
"Someone reacted to your note" : {
"comment" : "Generic title label for push notifications where someone reacted to the user's post"
},
"Someone reacted to your note with %@" : {
"comment" : "Body label for push notifications where someone reacted to the user's post with a specific emoji"
},
"Someone zapped you ⚡️" : {
"comment" : "Title label for a push notification where someone zapped the user"
}
},
"version" : "1.0"
}
@@ -5,10 +5,10 @@
/* Privacy - Media Library Usage Description */
"NSAppleMusicUsageDescription" = "Damus needs access to your media library for playback statuses";
/* Privacy - Camera Usage Description */
"NSCameraUsageDescription" = "Damus needs access to your camera if you want to upload photos from it";
"NSCameraUsageDescription" = "Damus needs access to your camera in order to upload photos and scan QR codes.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "Local authentication to access private key";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone if you want to upload recorded videos from it";
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone for creating video recording posts";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Granting Damus access to your photos allows you to save images.";
@@ -258,6 +258,22 @@
<string>%2$@ sats</string>
</dict>
</dict>
<key>users_talking_about_it</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@USERS@</string>
<key>USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d user talking about it</string>
<key>other</key>
<string>%d users talking about it</string>
</dict>
</dict>
<key>word_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
+2 -2
View File
@@ -3,10 +3,10 @@
"project" : "damus.xcodeproj",
"targetLocale" : "en-US",
"toolInfo" : {
"toolBuildNumber" : "15A240d",
"toolBuildNumber" : "15A507",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
"toolVersion" : "15.0.1"
},
"version" : "1.0"
}

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