Compare commits

..

1 Commits

Author SHA1 Message Date
55b5309c4d Show EULA prior to login 2023-01-28 01:01:16 -05:00
250 changed files with 4148 additions and 34286 deletions

View File

@@ -1,123 +1,10 @@
## [1.1.0-2] - 2023-02-14
### Added
- Save drafts to posts, replies and DMs (Terry Yiu)
### Fixed
- Ensure stats get updated in realtime on action bars (William Casarin)
- Fix reposts not getting counted properly (William Casarin)
- Fix a bug where zaps on other people's posts weren't showing (William Casarin)
- Fix punctuation getting included in some urls (Gert Goet)
- Improve language detection (Terry Yiu)
- Fix some animated image crashes (William Casarin)
[1.1.0-2]: https://github.com/damus-io/damus/releases/tag/v1.1.0-2
## [1.0.0-15] - 2023-02-10
### Added
- Relay Filtering (William Casarin)
- Japanese translations (Terry Yiu)
- Add password autofill on account login and creation (Terry Yiu)
- Show if relay is paid (William Casarin)
- Add "Follows You" indicator on profile (William Casarin)
- Add screen to select individual relays when posting/broadcasting (Andrii Sievrikov)
- Relay Detail View (Joel Klabo)
- Warn when attempting to post an nsec key (Terry Yiu)
- DeepL translation integration (Terry Yiu)
- Use local authentication (faceid) to access private key (Andrii Sievrikov)
- Add accessibility labels to action bar (Bryan Montz)
- Copy invoice button (Joel Klabo)
- Receive Lightning Zaps (William Casarin)
- Allow text selection in bio (Suhail Saqan)
### Changed
- Show "Follow Back" button on profile page (William Casarin)
- When on your profile page, open relay view instead for your own relays (Terry Yiu)
- Updated QR code view, include profile image, etc (ericholguin)
- Clicking relay numbers now goes to relay config (radixrat)
### Fixed
- Load zaps, likes and reposts when you open a thread (William Casarin)
- Fix bug where sidebar navigation fails to pop when switching timelines (William Casarin)
- Use lnaddress before lnurl for tip addresses to avoid Anigma scamming (William Casarin)
- Fix sidebar navigation bugs (OlegAba)
- Fix issue where navigation fails pop to root when switching timelines (William Casarin)
- Make @ mentions case insensitive (William Casarin)
- Fix some lnurls not getting decoded properly (William Casarin)
- Hide incoming DMs from blocked users (William Casarin)
- Hide blocked users from search results (William Casarin)
- Fix Cash App invoice payments (Rob Seward)
- DM Padding (OlegAba)
- Check for broken lnurls (William Casarin)
[1.0.0-15]: https://github.com/damus-io/damus/releases/tag/v1.0.0-15
## [1.0.0-13] - 2023-01-30
### Added
- LibreTranslate note translations (Terry Yiu)
- Added support for account deletion (William Casarin)
- User tagging and autocompletion in posts (Swift)
### Changed
- Remove redundant logout button from settings (Jonathan Milligan)
- Moved relay config to its own sidebar entry (William Casarin)
- New stylized tabs (ericholguin)
### Fixed
- Fix hidden profile action sheet when clicking ... (William Casarin)
- Fixed height of DM input (Terry Yiu)
- Fixed bug where copying pubkey from context menu only copied your own pubkey (Terry Yiu)
[1.0.0-13]: https://github.com/damus-io/damus/releases/tag/v1.0.0-13
## [1.0.0-12] - 2023-01-28
### Added
- Added Arabic and Portuguese translations (Barodane, Antonio Chagas)
- Add QRCode view for sharing your pubkey (ericholguin)
- Added nostr: uri handling (William Casarin)
### Changed
- Remove markdown link support from posts (Joel Klabo)
### Fixed
- Fixed crash on some SVG profile pictures (OlegAba)
- Localization fixes
- Don't allow blocking yourself (Terry)
- Hide muted users from global (William Casarin)
- Fixed profiles sometimes not loading from other clients (William Casarin)
- Fixed bug where `spam` was always the report type (William Casarin)
[1.0.0-12]: https://github.com/damus-io/damus/releases/tag/v1.0.0-12
## [1.0.0-11] - 2023-01-25
### Added
- Reposts view (Terry Yiu)
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (Nicolò Carcagnì, Solobalbo, Gregor, Peter Gerstbach, SYX)
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (William Casarin)
- Added ability to block users (William Casarin)
- Added a way to report content (William Casarin)
- Stretchable profile cover header (Swift)
@@ -144,7 +31,7 @@
- Show website on profiles (William Casarin)
- Add the ability to choose participants when replying (Joel Klabo)
- Translations for de_AT, de_DE, tr_TR, fr_FR (Gregor, Peter Gerstbach, Taylan Benli, Solobalbo)
- Translations for de_AT, de_DE, tr_TR, fr_FR (William Casarin)
- Add DM Message Requests (William Casarin)
@@ -164,7 +51,7 @@
- Drastically improved image viewer (OlegAba)
- Added pinch to zoom on images (Swift)
- Add Latin American Spanish translations (Nicolás Valencia)
- Add Latin American Spanish translations (William Casarin)
- Added SVG profile picture support (OlegAba)

View File

@@ -1,4 +1,3 @@
[![Run Test Suite](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml/badge.svg?branch=master)](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml)
# damus
@@ -26,7 +25,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
## Getting Started on Damus
### Damus iOS
1) Get the Damus app on the iOS App Store: https://apps.apple.com/ca/app/damus/id1628663131
1) Get the Damus app on TestFlight: https://testflight.apple.com/join/CLwjLxWl
#### ⚙️ Settings (gear icon, top right)
- Relays: You can add more relays to send your notes to by tapping the "+".
@@ -49,7 +48,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
4. Add @ direcly followed by the pubkey (e.g., `@npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s`)
- You can also long-press a Note to grab their User ID aka pubkey or Note ID to link directly to a Note.
- Currently you can't delete your Notes in the iOS app
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `https://i.ibb.co/2SHZbwm/alpha60.jpg`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `(https://i.ibb.co/2SHZbwm/alpha60.jpg)`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
- Engaging with Notes
- 💬 Replying to a Note: Tap the chat icon underneath the note. This will show up in the users notifications and in your 🏠 Personal and 🔍 Global Feeds
- ♺ Reposts: Tap the repost icon which will show up in your 🏠 Personal and 🔍 Global Feeds
@@ -92,41 +91,15 @@ damus implements the following [Nostr Implementation Possibilities][nips]
## Contributing
Contributors welcome!
### Code
[Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on GitHub as well.
Contributors welcome! [Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on github as well.
[git-send-email]: http://git-send-email.io
### Translations
## git log bot
Translators welcome! Join the [Transifex][transifex] project.
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
All user-facing strings must have a comment in order to provide context to translators. If a SwiftUI component has a `comment` parameter, use that. Otherwise, wrap your string with `NSLocalizedString` with the `comment` field populated.
[transifex]: https://explore.transifex.com/damus/damus-ios/
#### Export Source Translations
If user-facing strings have been added or changed, please export them for translation as part of your pull request or commit by running:
```zsh
./devtools/export-source-translation.sh
```
This command will export source translations to `translations/en-US.xcloc/Localized Contents/en-US.xliff`, which the Transifex integration will read from the `master` branch and allow translators to translate those strings.
#### Import Translations
Once 100% of strings have been translated for a given locale, Transifex will open up a pull request with the `translations/<locale>.xliff` file changed. Currently, it must be manually imported into the project before merging the pull request by running:
```zsh
./devtools/import-translation.sh <locale_code_in_snake_case>
```
### Awards
### Awards
There may be nostr badges awarded for contributors in the future... :)
@@ -134,7 +107,3 @@ First contributors:
1. @randymcmillan
2. @jcarucci27
### git log bot
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas

View File

@@ -15,11 +15,6 @@
<target>damus</target>
<note>Bundle name</note>
</trans-unit>
<trans-unit id="NSFaceIDUsageDescription" xml:space="preserve">
<source>Local authentication to access private key</source>
<target>Local authentication to access private key</target>
<note>Privacy - Face ID Usage Description</note>
</trans-unit>
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
<source>Granting Damus access to your photos allows you to save images.</source>
<target>Granting Damus access to your photos allows you to save images.</target>
@@ -32,11 +27,22 @@
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/>
</header>
<body>
<trans-unit id=" " xml:space="preserve">
<source> </source>
<target> </target>
<note>Blank space to separate profile picture from profile editor form.</note>
</trans-unit>
<trans-unit id="%@" xml:space="preserve">
<source>%@</source>
<target>%@</target>
<note>Amount of time that has passed since reply quote event occurred.
Abbreviated version of a nostr public key.</note>
</trans-unit>
<trans-unit id="%@ %@" xml:space="preserve">
<source>%@ %@</source>
<target>%@ %@</target>
<note>Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.
Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.</note>
<note>Sentence composed of 2 variables to describe how many tip payments there are on a post. In source English, the first variable is the number of tip payments, and the second variable is 'Tip' or 'Tips'.
Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.</note>
</trans-unit>
<trans-unit id="%@ has been blocked" xml:space="preserve">
<source>%@ has been blocked</source>
@@ -58,6 +64,12 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet.</target>
<note>Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
<trans-unit id="%lld" xml:space="preserve">
<source>%lld</source>
<target>%lld</target>
<note>Number of tip payments on a post.
Number of profiles a user is following.</note>
</trans-unit>
<trans-unit id="%lld/%lld" xml:space="preserve">
<source>%lld/%lld</source>
<target>%lld/%lld</target>
@@ -68,9 +80,9 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>'%@' at '%@' will be used for verification</target>
<note>Description of how the nip05 identifier would be used for verification.</note>
</trans-unit>
<trans-unit id="'%@' is an invalid NIP-05 identifier. It should look like an email." xml:space="preserve">
<source>'%@' is an invalid NIP-05 identifier. It should look like an email.</source>
<target>'%@' is an invalid NIP-05 identifier. It should look like an email.</target>
<trans-unit id="'%@' is an invalid nip05 identifier. It should look like an email." xml:space="preserve">
<source>'%@' is an invalid nip05 identifier. It should look like an email.</source>
<target>'%@' is an invalid nip05 identifier. It should look like an email.</target>
<note>Description of why the nip05 identifier is invalid.</note>
</trans-unit>
<trans-unit id="(Profile.displayName(profile: profile, pubkey: whos))'s Followers" xml:space="preserve">
@@ -83,15 +95,15 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>(who) following</target>
<note>Navigation bar title for view that shows who a user is following.</note>
</trans-unit>
<trans-unit id="API Key (optional)" xml:space="preserve">
<source>API Key (optional)</source>
<target>API Key (optional)</target>
<note>Prompt for optional entry of API Key to use translation server.</note>
<trans-unit id="&lt; e &gt;" xml:space="preserve">
<source>&lt; e &gt;</source>
<target>&lt; e &gt;</target>
<note>Placeholder for event mention.</note>
</trans-unit>
<trans-unit id="API Key (required)" xml:space="preserve">
<source>API Key (required)</source>
<target>API Key (required)</target>
<note>Prompt for required entry of API Key to use translation server.</note>
<trans-unit id="@" xml:space="preserve">
<source>@</source>
<target>@</target>
<note>Prefix character to username.</note>
</trans-unit>
<trans-unit id="About" xml:space="preserve">
<source>About</source>
@@ -139,11 +151,6 @@ Sentence composed of 2 variables to describe how many people are following a use
<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="Admin" xml:space="preserve">
<source>Admin</source>
<target>Admin</target>
<note>Label to display relay contact user.</note>
</trans-unit>
<trans-unit id="Any" xml:space="preserve">
<source>Any</source>
<target>Any</target>
@@ -211,11 +218,6 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Blue Wallet</target>
<note>Dropdown option label for Lightning wallet, Blue Wallet.</note>
</trans-unit>
<trans-unit id="Boosts" xml:space="preserve">
<source>Boosts</source>
<target>Boosts</target>
<note>Accessibility label for boosts button</note>
</trans-unit>
<trans-unit id="Breez" xml:space="preserve">
<source>Breez</source>
<target>Breez</target>
@@ -234,7 +236,6 @@ Sentence composed of 2 variables to describe how many people are following a use
Button to cancel out of posting a note.
Button to cancel out of reposting a post.
Button to cancel out of view adding user inputted relay.
Cancel deleting the user.
Cancel out of logging out the user.</note>
</trans-unit>
<trans-unit id="Cash App" xml:space="preserve">
@@ -242,6 +243,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Cash App</target>
<note>Dropdown option label for Lightning wallet, Cash App.</note>
</trans-unit>
<trans-unit id="Chat" xml:space="preserve">
<source>Chat</source>
<target>Chat</target>
<note>Navigation bar title for Chatroom view.</note>
</trans-unit>
<trans-unit id="Clear" xml:space="preserve">
<source>Clear</source>
<target>Clear</target>
@@ -252,11 +258,6 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Clear Cache</target>
<note>Section title for clearing cached data.</note>
</trans-unit>
<trans-unit id="Contact" xml:space="preserve">
<source>Contact</source>
<target>Contact</target>
<note>Label to display relay contact information.</note>
</trans-unit>
<trans-unit id="Copied" xml:space="preserve">
<source>Copied</source>
<target>Copied</target>
@@ -343,10 +344,10 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Creator(s) of Bitcoin. Absolute legend.</target>
<note>Example description about Bitcoin creator(s), Satoshi Nakamoto.</note>
</trans-unit>
<trans-unit id="Custom" xml:space="preserve">
<source>Custom</source>
<target>Custom</target>
<note>Dropdown option for selecting a custom translation server.</note>
<trans-unit id="DM Type" xml:space="preserve">
<source>DM Type</source>
<target>DM Type</target>
<note>DM selector for seeing either DMs or message requests, which are messages that have not been responded to yet. DM is the English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="DMs" xml:space="preserve">
<source>DMs</source>
@@ -359,43 +360,16 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Damus</target>
<note>Name of the app, shown on the first screen when user is not logged in.</note>
</trans-unit>
<trans-unit id="DeepL (Proprietary, Higher Accuracy)" xml:space="preserve">
<source>DeepL (Proprietary, Higher Accuracy)</source>
<target>DeepL (Proprietary, Higher Accuracy)</target>
<note>Dropdown option for selecting DeepL as the translation service.</note>
</trans-unit>
<trans-unit id="Default Wallet" xml:space="preserve">
<source>Default Wallet</source>
<target>Default Wallet</target>
<note>Button to pay a Lightning invoice with the user's default Lightning wallet.</note>
</trans-unit>
<trans-unit id="Default Zap Amount in sats" xml:space="preserve">
<source>Default Zap Amount in sats</source>
<target>Default Zap Amount in sats</target>
<note>Section title for zap configuration</note>
</trans-unit>
<trans-unit id="Delete" xml:space="preserve">
<source>Delete</source>
<target>Delete</target>
<note>Button for deleting the users account.
Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist.
Section title for deleting the user</note>
</trans-unit>
<trans-unit id="Delete Account" xml:space="preserve">
<source>Delete Account</source>
<target>Delete Account</target>
<note>Button to delete the user's account.</note>
</trans-unit>
<trans-unit id="Deleted Account" xml:space="preserve">
<source>Deleted Account</source>
<target>Deleted Account</target>
<note>Alert message to indicate this is a deleted account</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Description</target>
<note>Label to display relay description.</note>
<note>Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist.</note>
</trans-unit>
<trans-unit id="Dismiss" xml:space="preserve">
<source>Dismiss</source>
@@ -447,26 +421,16 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Error: %@</target>
<note>Error message indicating why saving keys failed.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<target>Filter</target>
<note>No comment provided by engineer.</note>
<trans-unit id="Filter State" xml:space="preserve">
<source>Filter State</source>
<target>Filter State</target>
<note>Filter state for seeing either only posts, or posts &amp; replies.</note>
</trans-unit>
<trans-unit id="Follow" xml:space="preserve">
<source>Follow</source>
<target>Follow</target>
<note>Button to follow a user.</note>
</trans-unit>
<trans-unit id="Follow Back" xml:space="preserve">
<source>Follow Back</source>
<target>Follow Back</target>
<note>Button to follow a user back.</note>
</trans-unit>
<trans-unit id="Follow me on nostr" xml:space="preserve">
<source>Follow me on nostr</source>
<target>Follow me on nostr</target>
<note>Text on QR code view to prompt viewer looking at screen to follow the user.</note>
</trans-unit>
<trans-unit id="Followers" xml:space="preserve">
<source>Followers</source>
<target>Followers</target>
@@ -488,21 +452,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Follows</target>
<note>Text to indicate that button next to it is in a state that will follow a profile when tapped.</note>
</trans-unit>
<trans-unit id="Follows you" xml:space="preserve">
<source>Follows you</source>
<target>Follows you</target>
<note>Text to indicate that a user is following your profile.</note>
</trans-unit>
<trans-unit id="Free" xml:space="preserve">
<source>Free</source>
<target>Free</target>
<note>Dropdown option for selecting Free plan for DeepL translation service.</note>
</trans-unit>
<trans-unit id="Get API Key" xml:space="preserve">
<source>Get API Key</source>
<target>Get API Key</target>
<note>Button to navigate to DeepL website to get a translation API key.</note>
</trans-unit>
<trans-unit id="Global" xml:space="preserve">
<source>Global</source>
<target>Global</target>
@@ -518,17 +467,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Goto profile %@</target>
<note>Navigation link to go to profile.</note>
</trans-unit>
<trans-unit id="Hide" xml:space="preserve">
<source>Hide</source>
<target>Hide</target>
<note>Button to hide a post from a user who has been blocked.</note>
</trans-unit>
<trans-unit id="Hide API Key" xml:space="preserve">
<source>Hide API Key</source>
<target>Hide API Key</target>
<note>Button to hide the DeepL translation API key.
Button to hide the LibreTranslate server API key.</note>
</trans-unit>
<trans-unit id="Home" xml:space="preserve">
<source>Home</source>
<target>Home</target>
@@ -564,11 +502,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Let's go!</target>
<note>Button to complete account creation and start using the app.</note>
</trans-unit>
<trans-unit id="LibreTranslate (Open Source)" xml:space="preserve">
<source>LibreTranslate (Open Source)</source>
<target>LibreTranslate (Open Source)</target>
<note>Dropdown option for selecting LibreTranslate as the translation service.</note>
</trans-unit>
<trans-unit id="Lightning Address or LNURL" xml:space="preserve">
<source>Lightning Address or LNURL</source>
<target>Lightning Address or LNURL</target>
@@ -579,16 +512,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Lightning Invoice</target>
<note>Indicates that the view is for paying a Lightning invoice.</note>
</trans-unit>
<trans-unit id="Like" xml:space="preserve">
<source>Like</source>
<target>Like</target>
<note>Accessibility Label for Like button</note>
</trans-unit>
<trans-unit id="Local authentication to access private key" xml:space="preserve">
<source>Local authentication to access private key</source>
<target>Local authentication to access private key</target>
<note>Face ID usage description shown when trying to access private key</note>
</trans-unit>
<trans-unit id="Local default" xml:space="preserve">
<source>Local default</source>
<target>Local default</target>
@@ -605,7 +528,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Logout</target>
<note>Alert for logging out the user.
Button for logging out the user.
Button to close the alert that informs that the current account has been deleted.</note>
Button to logout the user.</note>
</trans-unit>
<trans-unit id="Make sure your nsec account key is saved before you logout or you will lose access to this account" xml:space="preserve">
<source>Make sure your nsec account key is saved before you logout or you will lose access to this account</source>
@@ -622,26 +545,11 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>NIP-05 Verification</target>
<note>Label for NIP-05 Verification section of user profile form.</note>
</trans-unit>
<trans-unit id="No" xml:space="preserve">
<source>No</source>
<target>No</target>
<note>Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key.</note>
</trans-unit>
<trans-unit id="No block list found, create a new one? This will overwrite any previous block lists." xml:space="preserve">
<source>No block list found, create a new one? This will overwrite any previous block lists.</source>
<target>No block list found, create a new one? This will overwrite any previous block lists.</target>
<note>Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists.</note>
</trans-unit>
<trans-unit id="None" xml:space="preserve">
<source>None</source>
<target>None</target>
<note>Dropdown option for selecting no translation service.</note>
</trans-unit>
<trans-unit id="Note contains &quot;nsec1&quot; private key. Are you sure?" xml:space="preserve">
<source>Note contains "nsec1" private key. Are you sure?</source>
<target>Note contains "nsec1" private key. Are you sure?</target>
<note>Alert user that they might be attempting to paste a private key and ask them to confirm.</note>
</trans-unit>
<trans-unit id="Nothing to see here. Check back later!" xml:space="preserve">
<source>Nothing to see here. Check back later!</source>
<target>Nothing to see here. Check back later!</target>
@@ -650,18 +558,13 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Notifications</target>
<note>Toolbar label for Notifications view.</note>
<note>Navigation title for notifications.</note>
</trans-unit>
<trans-unit id="Nudity or explicit content" xml:space="preserve">
<source>Nudity or explicit content</source>
<target>Nudity or explicit content</target>
<note>Button for user to report that the account or content has nudity or explicit content.</note>
</trans-unit>
<trans-unit id="Paid Relay" xml:space="preserve">
<source>Paid Relay</source>
<target>Paid Relay</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Pay" xml:space="preserve">
<source>Pay</source>
<target>Pay</target>
@@ -672,31 +575,16 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Pay the Lightning invoice</target>
<note>Navigation bar title for view to pay Lightning invoice.</note>
</trans-unit>
<trans-unit id="Permanently Delete Account" xml:space="preserve">
<source>Permanently Delete Account</source>
<target>Permanently Delete Account</target>
<note>Alert for deleting the users account.</note>
</trans-unit>
<trans-unit id="Phoenix" xml:space="preserve">
<source>Phoenix</source>
<target>Phoenix</target>
<note>Dropdown option label for Lightning wallet, Phoenix.</note>
</trans-unit>
<trans-unit id="Plan" xml:space="preserve">
<source>Plan</source>
<target>Plan</target>
<note>Prompt selection of DeepL subscription plan to perform machine translations on notes</note>
</trans-unit>
<trans-unit id="Post" xml:space="preserve">
<source>Post</source>
<target>Post</target>
<note>Button to post a note.</note>
</trans-unit>
<trans-unit id="Post from a user you've blocked" xml:space="preserve">
<source>Post from a user you've blocked</source>
<target>Post from a user you've blocked</target>
<note>Text to indicate that what is being shown is a post from a user who has been blocked.</note>
</trans-unit>
<trans-unit id="Posts" xml:space="preserve">
<source>Posts</source>
<target>Posts</target>
@@ -717,11 +605,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Private Key</target>
<note>Title of the secure field that holds the user's private key.</note>
</trans-unit>
<trans-unit id="Pro" xml:space="preserve">
<source>Pro</source>
<target>Pro</target>
<note>Dropdown option for selecting Pro plan for DeepL translation service.</note>
</trans-unit>
<trans-unit id="Profile" xml:space="preserve">
<source>Profile</source>
<target>Profile</target>
@@ -770,13 +653,12 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<trans-unit id="Relay" xml:space="preserve">
<source>Relay</source>
<target>Relay</target>
<note>Label to display relay address.
Text field for relay server. Used for testing purposes.</note>
<note>Text field for relay server. Used for testing purposes.</note>
</trans-unit>
<trans-unit id="Relays" xml:space="preserve">
<source>Relays</source>
<target>Relays</target>
<note>Sidebar menu label for Relays view.</note>
<note>Sidebar menu label for Relay servers view</note>
</trans-unit>
<trans-unit id="Relays have been notified and clients will be able to use this information to filter content. Thank you!" xml:space="preserve">
<source>Relays have been notified and clients will be able to use this information to filter content. Thank you!</source>
@@ -788,11 +670,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Remove all</target>
<note>Button label to remove all participants from a note reply.</note>
</trans-unit>
<trans-unit id="Reply" xml:space="preserve">
<source>Reply</source>
<target>Reply</target>
<note>Accessibility label for reply button</note>
</trans-unit>
<trans-unit id="Reply to self" xml:space="preserve">
<source>Reply to self</source>
<target>Reply to self</target>
@@ -845,6 +722,11 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Requests</target>
<note>Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet). DM is the English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Reset" xml:space="preserve">
<source>Reset</source>
<target>Reset</target>
<note>Section title for resetting the user</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<target>Retry</target>
@@ -870,11 +752,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Save Image</target>
<note>Context menu option to save an image.</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 hashtag: #%@" xml:space="preserve">
<source>Search hashtag: #%@</source>
<target>Search hashtag: #%@</target>
@@ -905,16 +782,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Send a message to start the conversation...</target>
<note>Text prompt for user to send a message to the other user.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<target>Server</target>
<note>Prompt selection of LibreTranslate server to perform machine translations on notes</note>
</trans-unit>
<trans-unit id="Service" xml:space="preserve">
<source>Service</source>
<target>Service</target>
<note>Prompt selection of translation service provider.</note>
</trans-unit>
<trans-unit id="Settings" xml:space="preserve">
<source>Settings</source>
<target>Settings</target>
@@ -924,21 +791,13 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<trans-unit id="Share" xml:space="preserve">
<source>Share</source>
<target>Share</target>
<note>Button to share a post
Button to share an image.
<note>Button to share an image.
Button to share the link to a profile.</note>
</trans-unit>
<trans-unit id="Show" xml:space="preserve">
<source>Show</source>
<target>Show</target>
<note>Button to show a post from a user who has been blocked.
Toggle to show or hide user's secret account login key.</note>
</trans-unit>
<trans-unit id="Show API Key" xml:space="preserve">
<source>Show API Key</source>
<target>Show API Key</target>
<note>Button to show the DeepL translation API key.
Button to show the LibreTranslate server API key.</note>
<note>Toggle to show or hide user's secret account login key.</note>
</trans-unit>
<trans-unit id="Show wallet selector" xml:space="preserve">
<source>Show wallet selector</source>
@@ -950,21 +809,11 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Sign out</target>
<note>Sidebar menu label to sign out of the account.</note>
</trans-unit>
<trans-unit id="Software" xml:space="preserve">
<source>Software</source>
<target>Software</target>
<note>Label to display relay software.</note>
</trans-unit>
<trans-unit id="Strike" xml:space="preserve">
<source>Strike</source>
<target>Strike</target>
<note>Dropdown option label for Lightning wallet, Strike.</note>
</trans-unit>
<trans-unit id="Supported NIPs" xml:space="preserve">
<source>Supported NIPs</source>
<target>Supported NIPs</target>
<note>Label to display relay's supported NIPs.</note>
</trans-unit>
<trans-unit id="Thanks!" xml:space="preserve">
<source>Thanks!</source>
<target>Thanks!</target>
@@ -975,11 +824,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>They are impersonating someone</target>
<note>Button for user to report that the account is impersonating someone.</note>
</trans-unit>
<trans-unit id="This is a paid relay, you must pay for posts to be accepted." xml:space="preserve">
<source>This is a paid relay, you must pay for posts to be accepted.</source>
<target>This is a paid relay, you must pay for posts to be accepted.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective." xml:space="preserve">
<source>This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.</source>
<target>This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.</target>
@@ -1003,48 +847,14 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<trans-unit id="Thread" xml:space="preserve">
<source>Thread</source>
<target>Thread</target>
<note>Navigation bar title for note thread.</note>
</trans-unit>
<trans-unit id="To filter your %@ feed, please choose applicable relays from the list below:" xml:space="preserve">
<source>To filter your %@ feed, please choose applicable relays from the list below:</source>
<target>To filter your %@ feed, please choose applicable relays from the list below:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Translate Note" xml:space="preserve">
<source>Translate Note</source>
<target>Translate Note</target>
<note>Button to translate note from different language.</note>
</trans-unit>
<trans-unit id="Translated from (lang)" xml:space="preserve">
<source>Translated from (lang)</source>
<target>Translated from (lang)</target>
<note>Button to indicate that the note has been translated from a different language.</note>
</trans-unit>
<trans-unit id="Translating from (lang)..." xml:space="preserve">
<source>Translating from (lang)...</source>
<target>Translating from (lang)...</target>
<note>Button to indicate that the note is in the process of being translated from a different language.</note>
</trans-unit>
<trans-unit id="Translations" xml:space="preserve">
<source>Translations</source>
<target>Translations</target>
<note>Section title for selecting the translation service.</note>
</trans-unit>
<trans-unit id="Type DELETE to delete" xml:space="preserve">
<source>Type DELETE to delete</source>
<target>Type DELETE to delete</target>
<note>Text field prompt asking user to type the word DELETE to confirm that they want to proceed with deleting their account. The all caps lock DELETE word should not be translated. Everything else should.</note>
<note>Navigation bar title for note thread.
Navigation bar title for threaded event detail view.</note>
</trans-unit>
<trans-unit id="Type your post here..." xml:space="preserve">
<source>Type your post here...</source>
<target>Type your post here...</target>
<note>Text box prompt to ask user to type their post.</note>
</trans-unit>
<trans-unit id="URL" xml:space="preserve">
<source>URL</source>
<target>URL</target>
<note>Example URL to LibreTranslate server</note>
</trans-unit>
<trans-unit id="Unfollow" xml:space="preserve">
<source>Unfollow</source>
<target>Unfollow</target>
@@ -1068,7 +878,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<trans-unit id="User blocked" xml:space="preserve">
<source>User blocked</source>
<target>User blocked</target>
<note>Alert message to indicate the user has been blocked</note>
<note>Alert message to indicate</note>
</trans-unit>
<trans-unit id="User has been blocked" xml:space="preserve">
<source>User has been blocked</source>
@@ -1080,28 +890,22 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Username</target>
<note>Label for Username section of user profile form.
Label to prompt username entry.</note>
</trans-unit>
<trans-unit id="Version" xml:space="preserve">
<source>Version</source>
<target>Version</target>
<note>Label to display relay software version.
Section title for displaying the version number of the Damus app.</note>
</trans-unit>
<trans-unit id="Wallet" xml:space="preserve">
<source>Wallet</source>
<target>Wallet</target>
<note>Sidebar menu label for Wallet view.</note>
</trans-unit>
<trans-unit id="Wallet Of Satoshi" xml:space="preserve">
<source>Wallet Of Satoshi</source>
<target>Wallet Of Satoshi</target>
<note>Dropdown option label for Lightning wallet, Wallet Of Satoshi.</note>
</trans-unit>
<trans-unit id="Wallet Selector" xml:space="preserve">
<source>Wallet Selector</source>
<target>Wallet Selector</target>
<note>Section title for selection of wallet.</note>
</trans-unit>
<trans-unit id="Wallet of Satoshi" xml:space="preserve">
<source>Wallet of Satoshi</source>
<target>Wallet of Satoshi</target>
<note>Dropdown option label for Lightning wallet, Wallet of Satoshi.</note>
</trans-unit>
<trans-unit id="Website" xml:space="preserve">
<source>Website</source>
<target>Website</target>
@@ -1127,11 +931,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Yes, Overwrite</target>
<note>Text of button that confirms to overwrite the existing mutelist.</note>
</trans-unit>
<trans-unit id="Yes, Post with Private Key" xml:space="preserve">
<source>Yes, Post with Private Key</source>
<target>Yes, Post with Private Key</target>
<note>Button to proceed with posting a note even though it looks like they might be posting a private key.</note>
</trans-unit>
<trans-unit id="Your Name" xml:space="preserve">
<source>Your Name</source>
<target>Your Name</target>
@@ -1142,16 +941,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Your report will be sent to the relays you are connected to</target>
<note>Footer text to inform user what will happen when the report is submitted.</note>
</trans-unit>
<trans-unit id="Zap" xml:space="preserve">
<source>Zap</source>
<target>Zap</target>
<note>Accessibility label for zap button</note>
</trans-unit>
<trans-unit id="Zaps" xml:space="preserve">
<source>Zaps</source>
<target>Zaps</target>
<note>Navigation bar title for the Zaps view.</note>
</trans-unit>
<trans-unit id="Zebedee" xml:space="preserve">
<source>Zebedee</source>
<target>Zebedee</target>
@@ -1162,6 +951,11 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Zeus LN</target>
<note>Dropdown option label for Lightning wallet, Zeus LN.</note>
</trans-unit>
<trans-unit id="collapsed_event_view_other_notes" translate="no" xml:space="preserve">
<source>collapsed_event_view_other_notes</source>
<target>collapsed_event_view_other_notes</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="followers_count" translate="no" xml:space="preserve">
<source>followers_count</source>
<target>followers_count</target>
@@ -1237,6 +1031,16 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>sats_count</target>
<note>Amount of sats. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="tips_count" translate="no" xml:space="preserve">
<source>tips_count</source>
<target>tips_count</target>
<note>Part of a larger sentence to describe how many tip payments there are on a post. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="u{00A0}" xml:space="preserve">
<source>u{00A0}</source>
<target>u{00A0}</target>
<note>Non-breaking space character to fill in blank space next to event action button icons.</note>
</trans-unit>
<trans-unit id="wss://some.relay.com" xml:space="preserve">
<source>wss://some.relay.com</source>
<target>wss://some.relay.com</target>
@@ -1247,16 +1051,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>you</target>
<note>You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself.</note>
</trans-unit>
<trans-unit id="zaps_count" translate="no" xml:space="preserve">
<source>zaps_count</source>
<target>zaps_count</target>
<note>Part of a larger sentence to describe how many zap payments there are on a post. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="⚡️ %@" xml:space="preserve">
<source>⚡️ %@</source>
<target>⚡️ %@</target>
<note>Text indicating the zap amount. i.e. number of satoshis that were tipped to a user</note>
</trans-unit>
</body>
</file>
<file original="damus/en-US.lproj/Localizable.stringsdict" source-language="en-US" target-language="en-US" datatype="plaintext">
@@ -1267,17 +1061,17 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<trans-unit id="/collapsed_event_view_other_notes:dict/NOTES:dict/one:dict/:string" xml:space="preserve">
<source>%d other note</source>
<target>%d other note</target>
<note/>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/collapsed_event_view_other_notes:dict/NOTES:dict/other:dict/:string" xml:space="preserve">
<source>%d other notes</source>
<target>%d other notes</target>
<note/>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/collapsed_event_view_other_notes:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>··· %#@NOTES@ ···</source>
<target>··· %#@NOTES@ ···</target>
<note/>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
<trans-unit id="/followers_count:dict/FOLLOWERS:dict/one:dict/:string" xml:space="preserve">
<source>Follower</source>
@@ -1394,20 +1188,20 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>%2$@ sats</target>
<note>Amount of sats.</note>
</trans-unit>
<trans-unit id="/zaps_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@ZAPS@</source>
<target>%#@ZAPS@</target>
<note>Part of a larger sentence to describe how many zap payments there are on a post.</note>
<trans-unit id="/tips_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@TIPS@</source>
<target>%#@TIPS@</target>
<note>Part of a larger sentence to describe how many tip payments there are on a post.</note>
</trans-unit>
<trans-unit id="/zaps_count:dict/ZAPS:dict/one:dict/:string" xml:space="preserve">
<source>Zap</source>
<target>Zap</target>
<note>Part of a larger sentence to describe how many zap payments there are on a post.</note>
<trans-unit id="/tips_count:dict/TIPS:dict/one:dict/:string" xml:space="preserve">
<source>Tip</source>
<target>Tip</target>
<note>Part of a larger sentence to describe how many tip payments there are on a post.</note>
</trans-unit>
<trans-unit id="/zaps_count:dict/ZAPS:dict/other:dict/:string" xml:space="preserve">
<source>Zaps</source>
<target>Zaps</target>
<note>Part of a larger sentence to describe how many zap payments there are on a post.</note>
<trans-unit id="/tips_count:dict/TIPS:dict/other:dict/:string" xml:space="preserve">
<source>Tips</source>
<target>Tips</target>
<note>Part of a larger sentence to describe how many tip payments there are on a post.</note>
</trans-unit>
</body>
</file>

View File

@@ -2,7 +2,5 @@
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "Local authentication to access private key";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Granting Damus access to your photos allows you to save images.";

View File

@@ -134,20 +134,20 @@
<string>%2$@ sats</string>
</dict>
</dict>
<key>zaps_count</key>
<key>tips_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPS@</string>
<key>ZAPS</key>
<string>%#@TIPS@</string>
<key>TIPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Zap</string>
<string>Tip</string>
<key>other</key>
<string>Zaps</string>
<string>Tips</string>
</dict>
</dict>
</dict>

View File

@@ -26,10 +26,6 @@ static inline int is_boundary(char c) {
return !isalnum(c);
}
static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
}
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
{
c->start = content;
@@ -59,8 +55,8 @@ static int consume_until_whitespace(struct cursor *cur, int or_end) {
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c))
return consumedAtLeastOne;
if (is_whitespace(c) && consumedAtLeastOne)
return 1;
cur->p++;
consumedAtLeastOne = true;
@@ -225,9 +221,6 @@ static int parse_url(struct cursor *cur, struct block *block) {
return 0;
}
// strip any unwanted characters
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
block->type = BLOCK_URL;
block->block.str.start = (const char *)start;
block->block.str.end = (const char *)cur->p;

View File

@@ -15,14 +15,9 @@
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 */; };
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
@@ -32,6 +27,8 @@
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; };
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
@@ -42,7 +39,6 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */; };
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; };
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; };
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; };
4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; };
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
@@ -73,8 +69,6 @@
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */; };
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDB281DCE6100B3DE84 /* Liked.swift */; };
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */; };
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */; };
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D52B7298DB5C6001C5831 /* TextEvent.swift */; };
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA63C28FF52D600C48A62 /* bolt11.c */; };
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64028FF553900C48A62 /* hash_u5.c */; };
4C3EA64428FF558100C48A62 /* sha256.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64328FF558100C48A62 /* sha256.c */; };
@@ -91,7 +85,6 @@
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */; };
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; };
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
@@ -123,8 +116,6 @@
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */; };
@@ -137,16 +128,7 @@
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88392296F798300DC99E7 /* ReactionsModel.swift */; };
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */; };
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88399297322D200DC99E7 /* DMTests.swift */; };
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */; };
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A72975FC1800DC99E7 /* Zaps.swift */; };
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A9297612FF00DC99E7 /* ZapTests.swift */; };
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; };
@@ -169,14 +151,6 @@
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; };
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; };
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; };
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */; };
4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; };
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; };
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8795A2996C47A00F758CC /* ZapsModel.swift */; };
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; };
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */; };
@@ -196,28 +170,21 @@
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE829844AF100D66079 /* AnyCodable.swift */; };
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */; };
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; };
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6C297352F90031D7BC /* SVGKit */; };
7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6E297352F90031D7BC /* SVGKitSwift */; };
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; };
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; };
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; };
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
/* End PBXBuildFile section */
@@ -247,61 +214,29 @@
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A5CAE1F298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A5EA10F297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-AT"; path = "de-AT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5EA110297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-AT"; path = "de-AT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A5EA111297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "de-AT"; path = "de-AT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A93342929884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A93342A29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A93342B29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pl-PL"; path = "pl-PL.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
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>"; };
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>"; };
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AB5B86B2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
3AB5B86C2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AB72AB8298ECF30004BB58C /* Translator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translator.swift; sourceTree = "<group>"; };
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTranslateServer.swift; sourceTree = "<group>"; };
3AEABD20297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEABD21297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AEABD22297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "de-DE"; path = "de-DE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AEB8005297CCEA900713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "tr-TR"; path = "tr-TR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AF6336829884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; };
4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -313,6 +248,8 @@
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
@@ -323,7 +260,6 @@
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureSelector.swift; sourceTree = "<group>"; };
4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; };
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
4C363A8728236948006E126D /* BlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksView.swift; sourceTree = "<group>"; };
4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
@@ -354,8 +290,6 @@
4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeCounter.swift; sourceTree = "<group>"; };
4C3BEFDB281DCE6100B3DE84 /* Liked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Liked.swift; sourceTree = "<group>"; };
4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusState.swift; sourceTree = "<group>"; };
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapEvent.swift; sourceTree = "<group>"; };
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEvent.swift; sourceTree = "<group>"; };
4C3EA63B28FF52D600C48A62 /* bolt11.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bolt11.h; sourceTree = "<group>"; };
4C3EA63C28FF52D600C48A62 /* bolt11.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = bolt11.c; sourceTree = "<group>"; };
4C3EA63E28FF54BD00C48A62 /* short_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = short_types.h; sourceTree = "<group>"; };
@@ -401,7 +335,6 @@
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceTests.swift; sourceTree = "<group>"; };
4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; };
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; };
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; };
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
@@ -434,8 +367,6 @@
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; };
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; };
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = "<group>"; };
@@ -448,16 +379,7 @@
4CB88392296F798300DC99E7 /* ReactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsModel.swift; sourceTree = "<group>"; };
4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
4CB88399297322D200DC99E7 /* DMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMTests.swift; sourceTree = "<group>"; };
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrlPayRequest.swift; sourceTree = "<group>"; };
4CB883A72975FC1800DC99E7 /* Zaps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zaps.swift; sourceTree = "<group>"; };
4CB883A9297612FF00DC99E7 /* ZapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapTests.swift; sourceTree = "<group>"; };
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; };
@@ -482,14 +404,6 @@
4CE6DF0127F7A08200C66700 /* damusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITests.swift; sourceTree = "<group>"; };
4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITestsLaunchTests.swift; sourceTree = "<group>"; };
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConnection.swift; sourceTree = "<group>"; };
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; };
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatus.swift; sourceTree = "<group>"; };
4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; };
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; };
4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; };
4CE8795A2996C47A00F758CC /* ZapsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsModel.swift; sourceTree = "<group>"; };
4CEE2AE72804F57C00AB5EEF /* libsecp256k1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libsecp256k1.a; sourceTree = "<group>"; };
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRequest.swift; sourceTree = "<group>"; };
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; };
@@ -510,27 +424,18 @@
4CF0ABE829844AF100D66079 /* AnyCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCodable.swift; sourceTree = "<group>"; };
4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = "<group>"; };
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = "<group>"; };
7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = "<group>"; };
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; };
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -540,6 +445,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */,
7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */,
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
6C7DE41F2955169800E66263 /* Vault in Frameworks */,
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */,
@@ -668,13 +575,9 @@
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -682,10 +585,6 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CE879562996C44A00F758CC /* Zaps */,
4CB9D4A52992D01900A9A7E4 /* Profile */,
4CAAD8AE29888A9B00060CEA /* Relays */,
4CF0ABF42985CD4200D66079 /* Posting */,
4CF0ABDF2981A83000D66079 /* Muting */,
4CC7AAEE297F11B300430951 /* Events */,
3AA24800297E3DAE0090C62D /* Reposts */,
@@ -719,6 +618,9 @@
4C8682862814DE470026224F /* ProfileView.swift */,
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */,
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
4C06670028FC7C5900038D2A /* RelayView.swift */,
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
@@ -728,6 +630,7 @@
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */,
4C3AC7A02835A81400E1F516 /* SetupView.swift */,
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */,
4C0A3F96280F8E02000448DE /* ThreadView.swift */,
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */,
647D9A8C2968520300A295DE /* SideMenuView.swift */,
@@ -737,8 +640,6 @@
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */,
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -766,11 +667,8 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4CE879492995B58700F758CC /* Relays */,
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
4C3A1D322960DB0500558C0F /* Markdown.swift */,
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */,
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
4C363A8328233689006E126D /* Parser.swift */,
@@ -785,34 +683,10 @@
64FBD06E296255C400D9D3B2 /* Theme.swift */,
4CB8838529656C8B00DC99E7 /* NIP05.swift */,
4CF0ABD72981980C00D66079 /* Lists.swift */,
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */,
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */,
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
3AB72AB8298ECF30004BB58C /* Translator.swift */,
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
);
path = Util;
sourceTree = "<group>";
};
4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup;
children = (
4CE879532996BA0000F758CC /* Detail */,
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
4C06670028FC7C5900038D2A /* RelayView.swift */,
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
4CE879512996B68900F758CC /* RelayType.swift */,
);
path = Relays;
sourceTree = "<group>";
};
4CB88387296AF97C00DC99E7 /* ActionBar */ = {
isa = PBXGroup;
children = (
@@ -830,15 +704,6 @@
path = Reactions;
sourceTree = "<group>";
};
4CB9D4A52992D01900A9A7E4 /* Profile */ = {
isa = PBXGroup;
children = (
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
);
path = Profile;
sourceTree = "<group>";
};
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
@@ -850,8 +715,6 @@
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */,
4CC7AAF9297F64AC00430951 /* EventMenu.swift */,
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */,
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */,
);
path = Events;
sourceTree = "<group>";
@@ -869,12 +732,8 @@
4CB8838C296F710400DC99E7 /* Reposted.swift */,
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */,
4CC7AAEC297F0B9E00430951 /* Highlight.swift */,
5C513FB9297F72980072348F /* CustomPicker.swift */,
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */,
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -943,8 +802,6 @@
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
4CB88399297322D200DC99E7 /* DMTests.swift */,
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -958,31 +815,6 @@
path = damusUITests;
sourceTree = "<group>";
};
4CE879492995B58700F758CC /* Relays */ = {
isa = PBXGroup;
children = (
4CE8794729941DA700F758CC /* RelayFilters.swift */,
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */,
);
path = Relays;
sourceTree = "<group>";
};
4CE879532996BA0000F758CC /* Detail */ = {
isa = PBXGroup;
children = (
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */,
);
path = Detail;
sourceTree = "<group>";
};
4CE879562996C44A00F758CC /* Zaps */ = {
isa = PBXGroup;
children = (
4CE879572996C45300F758CC /* ZapsView.swift */,
);
path = Zaps;
sourceTree = "<group>";
};
4CEE2AE62804F57B00AB5EEF /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -1009,14 +841,6 @@
path = AnyCodable;
sourceTree = "<group>";
};
4CF0ABF42985CD4200D66079 /* Posting */ = {
isa = PBXGroup;
children = (
4CF0ABF52985CD5500D66079 /* UserSearch.swift */,
);
path = Posting;
sourceTree = "<group>";
};
F7F0BA23297892AE009531F3 /* Modifiers */ = {
isa = PBXGroup;
children = (
@@ -1046,6 +870,8 @@
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
6C7DE41E2955169800E66263 /* Vault */,
7C45AE6C297352F90031D7BC /* SVGKit */,
7C45AE6E297352F90031D7BC /* SVGKitSwift */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -1119,21 +945,12 @@
Base,
"es-419",
"en-US",
"de-AT",
"de-DE",
"tr-TR",
"fr-FR",
"lv-LV",
"it-IT",
de,
"pt-PT",
"pl-PL",
ar,
nl,
"zh-CN",
"el-GR",
ja,
id,
cs,
ru,
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1141,6 +958,7 @@
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */,
7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -1194,12 +1012,10 @@
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */,
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
@@ -1215,25 +1031,19 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
@@ -1242,7 +1052,6 @@
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
4C363A8428233689006E126D /* Parser.swift in Sources */,
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
@@ -1253,62 +1062,51 @@
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
4C363A94282704FA006E126D /* Post.swift in Sources */,
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */,
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */,
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */,
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */,
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
@@ -1318,7 +1116,6 @@
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
@@ -1336,37 +1133,27 @@
4C06670E28FDEAA000038D2A /* utf8.c in Sources */,
4C3EA66D28FF782800C48A62 /* amount.c in Sources */,
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */,
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */,
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */,
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
@@ -1375,9 +1162,7 @@
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1389,9 +1174,7 @@
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
@@ -1429,21 +1212,12 @@
children = (
3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */,
3A5EA111297CCF6C00569477 /* de-AT */,
3AEABD22297CCFA8003F2975 /* de-DE */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A929C22297F2CF80090925E /* it-IT */,
3AB5B86C2986D8A3006599D2 /* de */,
3AF6336A29884C6B0005672A /* pt-PT */,
3A93342B29884CA600D6A8F3 /* pl-PL */,
3AC524F0298C000B00693EBF /* ar */,
3A96D41C298DA94500388A2A /* nl */,
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
3A25EF152992DA5D008ABE69 /* el-GR */,
3A66D929299472FA008B44F4 /* ja */,
3A41E55B299D52BE001FA465 /* id */,
3A8624DB299E82BE00BD8BE9 /* cs */,
3A827A1A299FC69D00C4D171 /* ru */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1452,21 +1226,12 @@
isa = PBXVariantGroup;
children = (
3ACB685B297633BC00C46468 /* es-419 */,
3A5EA10F297CCF6C00569477 /* de-AT */,
3AEABD20297CCFA8003F2975 /* de-DE */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A929C20297F2CF80090925E /* it-IT */,
3AB5B86A2986D8A3006599D2 /* de */,
3AF6336829884C6B0005672A /* pt-PT */,
3A93342929884CA600D6A8F3 /* pl-PL */,
3AC524EE298C000B00693EBF /* ar */,
3A96D41A298DA94500388A2A /* nl */,
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
3A25EF132992DA5D008ABE69 /* el-GR */,
3A66D927299472FA008B44F4 /* ja */,
3A41E559299D52BE001FA465 /* id */,
3A8624D9299E82BE00BD8BE9 /* cs */,
3A827A18299FC69D00C4D171 /* ru */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1475,21 +1240,12 @@
isa = PBXVariantGroup;
children = (
3ACB685E297633BC00C46468 /* es-419 */,
3A5EA110297CCF6C00569477 /* de-AT */,
3AEABD21297CCFA8003F2975 /* de-DE */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A929C21297F2CF80090925E /* it-IT */,
3AB5B86B2986D8A3006599D2 /* de */,
3AF6336929884C6B0005672A /* pt-PT */,
3A93342A29884CA600D6A8F3 /* pl-PL */,
3AC524EF298C000B00693EBF /* ar */,
3A96D41B298DA94500388A2A /* nl */,
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
3A25EF142992DA5D008ABE69 /* el-GR */,
3A66D928299472FA008B44F4 /* ja */,
3A41E55A299D52BE001FA465 /* id */,
3A8624DA299E82BE00BD8BE9 /* cs */,
3A827A19299FC69D00C4D171 /* ru */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1625,7 +1381,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1633,7 +1389,6 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -1648,7 +1403,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.1.0;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1667,7 +1422,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1675,7 +1430,6 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -1690,7 +1444,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.1.0;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1850,6 +1604,14 @@
minimumVersion = 1.0.0;
};
};
7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SVGKit/SVGKit";
requirement = {
kind = revision;
revision = e1f13e27b1e4c0ffe20e7d8d3984bf49c2a584d0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -1873,6 +1635,16 @@
package = 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */;
productName = Vault;
};
7C45AE6C297352F90031D7BC /* SVGKit */ = {
isa = XCSwiftPackageProductDependency;
package = 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKit;
};
7C45AE6E297352F90031D7BC /* SVGKitSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKitSwift;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;

View File

@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher",
"state" : {
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
"version" : "7.6.1"
"revision" : "017f94ccfdacabb1ae7f45b75b4217b24c06e6ac",
"version" : "7.4.0"
}
},
{
@@ -26,6 +26,14 @@
"version" : "4.0.4"
}
},
{
"identity" : "svgkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SVGKit/SVGKit",
"state" : {
"revision" : "e1f13e27b1e4c0ffe20e7d8d3984bf49c2a584d0"
}
},
{
"identity" : "vault",
"kind" : "remoteSourceControl",

View File

@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x4D",
"red" : "0x4B"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1C",
"red" : "0x1C"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x4F",
"green" : "0xC3",
"red" : "0x66"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF4",
"green" : "0xEE",
"red" : "0xEE"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x5F",
"green" : "0x5F",
"red" : "0x5F"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-copy.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: 354 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-key.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: 400 B

View File

@@ -0,0 +1,52 @@
{
"images" : [
{
"filename" : "ic-message-black.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "ic-message-white 1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-nipverified.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: 950 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-qr.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: 252 B

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "profile-banner.jpeg",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "bbw.jpg",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "bitcoin-p2p.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "blixt-wallet.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "bluewallet.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "breez.jpg",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "cashapp.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "digital-nomad.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "encrypted-message.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-lightning.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: 458 B

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-tick.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: 671 B

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "lnlink.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "damus-nobg.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "muun.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "phoenix.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "river.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "strike.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "undercover.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "walletofsatoshi.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "zebedee.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "zeus.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {

View File

@@ -1,60 +0,0 @@
//
// CustomPicker.swift
// damus
//
// Created by Eric Holguin on 1/22/23.
//
import SwiftUI
let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
Color("DamusPurple"),
Color("DamusBlue")
]), startPoint: .leading, endPoint: .trailing)
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
@Environment(\.colorScheme) var colorScheme
@Namespace var picker
@Binding var selection: SelectionValue
@ViewBuilder let content: Content
public var body: some View {
let contentMirror = Mirror(reflecting: content)
let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count
HStack {
ForEach(0..<blocksCount, id: \.self) { index in
let tupleBlock = contentMirror.descendant("value", ".\(index)")
let text = Mirror(reflecting: tupleBlock!).descendant("content") as! Text
let tag = Mirror(reflecting: tupleBlock!).descendant("modifier", "value", "tagged") as! SelectionValue
Button {
withAnimation(.spring()) {
selection = tag
}
} label: {
text
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
.font(.system(size: 14, weight: .heavy))
}
.background(
Group {
if tag == selection {
Rectangle().fill(RECTANGLE_GRADIENT).frame(height: 2.5)
.matchedGeometryEffect(id: "selector", in: picker)
.cornerRadius(2.5)
}
},
alignment: .bottom
)
.frame(maxWidth: .infinity)
.accentColor(tag == selection ? textColor() : .gray)
}
}
}
func textColor() -> Color {
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
}

View File

@@ -66,11 +66,20 @@ struct ImageContextMenuModifier: ViewModifier {
private struct ImageContainerView: View {
let url: URL?
@ObservedObject var imageModel: KFImageModel
@State private var image: UIImage?
@State private var showShareSheet = false
init(url: URL?) {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 2000000, // 2 MB
downsampleSize: CGSize(width: 400, height: 400)
)
}
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
@@ -82,17 +91,30 @@ private struct ImageContainerView: View {
var body: some View {
KFAnimatedImage(url)
.imageContext(.note)
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.configure { view in
view.framePreloadCount = 3
view.framePreloadCount = 1
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image))
.clipped()
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [url])
.onFailure { _ in
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
.clipped()
.modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [imageModel.url])
}
// TODO: Update ImageCarousel with serializer and processor
// .serialize(by: imageModel.serializer)
// .setProcessor(imageModel.processor)
}
}
@@ -105,6 +127,14 @@ struct ImageView: View {
@State private var selectedIndex = 0
@State var showMenu = true
var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
}
var navBarView: some View {
VStack {
HStack {
@@ -150,8 +180,8 @@ struct ImageView: View {
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
.padding(.top, safeAreaInsets?.top)
.padding(.bottom, safeAreaInsets?.bottom)
}
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
@@ -180,7 +210,7 @@ struct ImageView: View {
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
.padding(.bottom, safeAreaInsets?.bottom)
)
}
}
@@ -199,8 +229,12 @@ struct ImageCarousel: View {
.foregroundColor(Color.clear)
.overlay {
KFAnimatedImage(url)
.imageContext(.note)
.cancelOnDisappear(true)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.configure { view in
view.framePreloadCount = 3
}

View File

@@ -7,84 +7,6 @@
import SwiftUI
struct InvoiceView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) private var openURL
let our_pubkey: String
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@State var copied = false
var CopyButton: some View {
Button {
copied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
copied = false
}
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
UIPasteboard.general.string = invoice.string
} label: {
if !copied {
Image(systemName: "doc.on.clipboard")
.foregroundColor(.gray)
} else {
Image(systemName: "checkmark.circle")
.foregroundColor(Color("DamusGreen"))
}
}
}
var PayButton: some View {
Button {
if should_show_wallet_selector(our_pubkey) {
showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(our_pubkey).model, invoice: invoice.string)
}
} label: {
RoundedRectangle(cornerRadius: 20, style: .circular)
.foregroundColor(colorScheme == .light ? .black : .white)
.overlay {
Text("Pay", comment: "Button to pay a Lightning invoice.")
.fontWeight(.medium)
.foregroundColor(colorScheme == .light ? .white : .black)
}
}
.onTapGesture {
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
print("pay button tap")
}
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.secondary.opacity(0.1))
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("", systemImage: "bolt.fill")
.foregroundColor(.orange)
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
Spacer()
CopyButton
}
Divider()
Text(invoice.description_string)
Text(invoice.amount.amount_sats_str())
.font(.title)
PayButton
.frame(height: 50)
.zIndex(10.0)
}
.padding(30)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
}
}
}
func open_with_wallet(wallet: Wallet.Model, invoice: String) {
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
@@ -106,12 +28,68 @@ func open_with_wallet(wallet: Wallet.Model, invoice: String) {
}
}
struct InvoiceView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) private var openURL
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@ObservedObject var user_settings = UserSettingsStore()
var PayButton: some View {
Button {
if user_settings.show_wallet_selector {
showing_select_wallet = true
} else {
open_with_wallet(wallet: user_settings.default_wallet.model, invoice: invoice.string)
}
} label: {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(colorScheme == .light ? .black : .white)
.overlay {
Text("Pay", comment: "Button to pay a Lightning invoice.")
.fontWeight(.medium)
.foregroundColor(colorScheme == .light ? .white : .black)
}
}
//.buttonStyle(.bordered)
.onTapGesture {
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
}
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.secondary.opacity(0.1))
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("", systemImage: "bolt.fill")
.foregroundColor(.orange)
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
}
Divider()
Text(invoice.description)
Text(invoice.amount.amount_sats_str())
.font(.title)
PayButton
.frame(height: 50)
.zIndex(10.0)
}
.padding(30)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: invoice.string).environmentObject(user_settings)
}
}
}
let test_invoice = Invoice(description: .description("this is a description"), amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
let test_invoice = Invoice(description: "this is a description", amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
struct InvoiceView_Previews: PreviewProvider {
static var previews: some View {
InvoiceView(our_pubkey: "", invoice: test_invoice)
.frame(width: 300, height: 200)
InvoiceView(invoice: test_invoice)
.frame(width: 200, height: 200)
}
}

View File

@@ -8,7 +8,6 @@
import SwiftUI
struct InvoicesView: View {
let our_pubkey: String
var invoices: [Invoice]
@State var open_sheet: Bool = false
@@ -17,7 +16,7 @@ struct InvoicesView: View {
var body: some View {
TabView {
ForEach(invoices, id: \.string) { invoice in
InvoiceView(our_pubkey: our_pubkey, invoice: invoice)
InvoiceView(invoice: invoice)
.tabItem {
Text(invoice.string)
}
@@ -31,7 +30,7 @@ struct InvoicesView: View {
struct InvoicesView_Previews: PreviewProvider {
static var previews: some View {
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
InvoicesView(invoices: [Invoice.init(description: "description", amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
.frame(width: 300)
}
}

View File

@@ -1,96 +0,0 @@
//
// SelectableText.swift
// damus
//
// Created by Oleg Abalonski on 2/16/23.
//
import UIKit
import SwiftUI
struct SelectableText: View {
let attributedString: AttributedString
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
var body: some View {
GeometryReader { geo in
TextViewRepresentable(
attributedString: attributedString,
textColor: UIColor.label,
font: UIFont.preferredFont(forTextStyle: .title2),
fixedWidth: selectedTextWidth,
height: $selectedTextHeight
)
.onAppear {
self.selectedTextWidth = geo.size.width
}
.onChange(of: geo.size) { newSize in
self.selectedTextWidth = newSize.width
}
}
.frame(height: selectedTextHeight)
}
}
fileprivate struct TextViewRepresentable: UIViewRepresentable {
let attributedString: AttributedString
let textColor: UIColor
let font: UIFont
let fixedWidth: CGFloat
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
let view = UITextView()
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
view.textContainer.lineFragmentPadding = 0
view.textContainerInset = .zero
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
let mutableAttributedString = createNSAttributedString()
uiView.attributedText = mutableAttributedString
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
DispatchQueue.main.async {
height = newHeight
}
}
func createNSAttributedString() -> NSMutableAttributedString {
let mutableAttributedString = NSMutableAttributedString(attributedString)
let myAttribute = [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: textColor
]
mutableAttributedString.addAttributes(
myAttribute,
range: NSRange.init(location: 0, length: mutableAttributedString.length)
)
return mutableAttributedString
}
}
fileprivate extension NSAttributedString {
func height(containerWidth: CGFloat) -> CGFloat {
let rect = self.boundingRect(
with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
)
return ceil(rect.size.height)
}
}

View File

@@ -1,145 +0,0 @@
//
// TranslateButton.swift
// damus
//
// Created by William Casarin on 2023-02-02.
//
import SwiftUI
import NaturalLanguage
struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
@State var checkingTranslationStatus: Bool = false
@State var currentLanguage: String = "en"
@State var noteLanguage: String? = nil
@State var translated_note: String? = nil
@State var show_translated_note: Bool = false
@State var translated_artifacts: NoteArtifacts? = nil
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
}
.translate_button_style()
}
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group {
Button(NSLocalizedString("Translated from \(lang)", comment: "Button to indicate that the note has been translated from a different language.")) {
show_translated_note = false
}
.translate_button_style()
SelectableText(attributedString: artifacts.content)
}
}
func CheckingStatus(lang: String) -> some View {
return Button(NSLocalizedString("Translating from \(lang)...", comment: "Button to indicate that the note is in the process of being translated from a different language.")) {
show_translated_note = false
}
.translate_button_style()
}
func MainContent(note_lang: String) -> some View {
return Group {
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let lang = languageName, show_translated_note {
if checkingTranslationStatus {
CheckingStatus(lang: lang)
} else if let artifacts = translated_artifacts {
Translated(lang: lang, artifacts: artifacts)
}
} else {
TranslateButton
}
}
}
var body: some View {
Group {
if let note_lang = noteLanguage, noteLanguage != currentLanguage {
MainContent(note_lang: note_lang)
} else {
Text("")
}
}
.task {
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
return
}
checkingTranslationStatus = true
if #available(iOS 16, *) {
currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
} else {
currentLanguage = Locale.current.languageCode ?? "en"
}
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = event.blocks(damus_state.keypair.privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
noteLanguage = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue ?? currentLanguage
if let lang = noteLanguage, noteLanguage != currentLanguage {
// If the detected dominant language is a variant, remove the variant component and just take the language part as translation services typically only supports the variant-less language.
if #available(iOS 16, *) {
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
} else {
noteLanguage = NSLocale(localeIdentifier: lang).languageCode
}
}
guard let note_lang = noteLanguage else {
noteLanguage = currentLanguage
translated_note = nil
checkingTranslationStatus = false
return
}
if note_lang != currentLanguage {
do {
// If the note language is different from our language, send a translation request.
let translator = Translator(damus_state.settings)
let originalContent = event.get_content(damus_state.keypair.privkey)
translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage)
if originalContent == translated_note {
// If the translation is the same as the original, don't bother showing it.
noteLanguage = currentLanguage
translated_note = nil
}
} catch {
// If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack.
noteLanguage = currentLanguage
translated_note = nil
}
}
if let translated = translated_note {
// Render translated note.
let translatedBlocks = event.get_blocks(content: translated)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
checkingTranslationStatus = false
}
}
}
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
TranslateView(damus_state: ds, event: test_event)
}
}

View File

@@ -1,130 +0,0 @@
//
// ZapButton.swift
// damus
//
// Created by William Casarin on 2023-01-17.
//
import SwiftUI
struct ZapButton: View {
let damus_state: DamusState
let event: NostrEvent
let lnurl: String
@ObservedObject var bar: ActionBarModel
@State var zapping: Bool = false
@State var invoice: String = ""
@State var slider_value: Double = 0.0
@State var slider_visible: Bool = false
@State var showing_select_wallet: Bool = false
func send_zap() {
guard let privkey = damus_state.keypair.privkey else {
return
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
// TODO: gather comment?
let content = ""
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target)
zapping = true
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
if mpayreq == nil {
mpayreq = await fetch_static_payreq(lnurl)
}
guard let payreq = mpayreq else {
// TODO: show error
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else {
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
zapping = false
if should_show_wallet_selector(damus_state.pubkey) {
self.invoice = inv
self.showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
}
}
}
//damus_state.pool.send(.event(zapreq))
}
var zap_img: String {
if bar.zapped {
return "bolt.fill"
}
if !zapping {
return "bolt"
}
return "bolt.horizontal.fill"
}
var zap_color: Color? {
if bar.zapped {
return Color.orange
}
if !zapping {
return nil
}
return Color.yellow
}
var body: some View {
HStack(spacing: 4) {
EventActionButton(img: zap_img, col: zap_color) {
if bar.zapped {
//notify(.delete, bar.our_tip)
} else if !zapping {
send_zap()
}
}
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
Text(String("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")"))
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
}
}
}
struct ZapButton_Previews: PreviewProvider {
static var previews: some View {
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, our_like: nil, our_boost: nil, our_zap: nil)
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
}
}

View File

@@ -7,14 +7,15 @@
import SwiftUI
import Starscream
import Kingfisher
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://nostr.fmt.wiz.biz",
"wss://relay.nostr.bg",
"wss://nostr.oxtr.dev",
"wss://relay.snort.social",
"wss://offchain.pub",
"wss://nos.lol",
"wss://relay.current.fyi",
"wss://brb.io",
]
@@ -27,16 +28,12 @@ enum Sheets: Identifiable {
case post
case report(ReportTarget)
case reply(NostrEvent)
case event(NostrEvent)
case filter
var id: String {
switch self {
case .report: return "report"
case .post: return "post"
case .reply(let ev): return "reply-" + ev.id
case .event(let ev): return "event-" + ev.id
case .filter: return "filter"
}
}
}
@@ -76,7 +73,6 @@ struct ContentView: View {
@State var damus_state: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false
@State var is_deleted_account: Bool = false
@State var is_profile_open: Bool = false
@State var event: NostrEvent? = nil
@State var active_profile: String? = nil
@@ -92,6 +88,7 @@ struct ContentView: View {
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
@StateObject var user_settings = UserSettingsStore()
// connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
@@ -114,7 +111,7 @@ struct ContentView: View {
.tabViewStyle(.page(indexDisplayMode: .never))
if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
PostButtonContainer(userSettings: user_settings) {
self.active_sheet = .post
}
}
@@ -122,10 +119,9 @@ struct ContentView: View {
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: {
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
})
FiltersView
//.frame(maxWidth: 275)
.padding()
Divider()
.frame(height: 1)
}
@@ -141,33 +137,13 @@ struct ContentView: View {
}
}
func popToRoot() {
profile_open = false
thread_open = false
search_open = false
isSideBarOpened = false
}
var timelineNavItem: some View {
VStack {
switch selected_timeline {
case .home:
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
case .dms:
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
.bold()
case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
var FiltersView: some View {
VStack{
Picker(NSLocalizedString("Filter State", comment: "Filter state for seeing either only posts, or posts & replies."), selection: $filter_state) {
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
}
.pickerStyle(.segmented)
}
}
@@ -190,10 +166,9 @@ struct ContentView: View {
PostingTimelineView
case .notifications:
VStack(spacing: 0) {
Divider()
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
}
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
.navigationTitle(NSLocalizedString("Notifications", comment: "Navigation title for notifications."))
case .dms:
DirectMessagesView(damus_state: damus_state!)
.environmentObject(home.dms)
@@ -205,9 +180,24 @@ struct ContentView: View {
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Global", comment: "Navigation bar title for Global view where posts from all connected relay servers appear."), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .principal) {
timelineNavItem
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
switch selected_timeline {
case .home:
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
case .dms:
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
.bold()
case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
}
}
}
.ignoresSafeArea(.keyboard)
@@ -216,7 +206,7 @@ struct ContentView: View {
var MaybeSearchView: some View {
Group {
if let search = self.active_search {
SearchView(appstate: damus_state!, search: SearchModel(contacts: damus_state!.contacts, pool: damus_state!.pool, search: search))
SearchView(appstate: damus_state!, search: SearchModel(pool: damus_state!.pool, search: search))
} else {
EmptyView()
}
@@ -264,7 +254,7 @@ struct ContentView: View {
if let damus = self.damus_state {
NavigationView {
ZStack {
TabView { // Prevents navbar appearance change on scroll
VStack {
MainContent(damus: damus)
.toolbar() {
ToolbarItem(placement: .navigationBarLeading) {
@@ -272,43 +262,29 @@ struct ContentView: View {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
.disabled(isSideBarOpened)
}
ToolbarItem(placement: .navigationBarTrailing) {
HStack(alignment: .center) {
if home.signal.signal != home.signal.max_signal {
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
}
// maybe expand this to other timelines in the future
if selected_timeline == .search {
Button(action: {
//isFilterVisible.toggle()
self.active_sheet = .filter
}) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label("Filter", systemImage: "line.3.horizontal.decrease")
.foregroundColor(.gray)
//.contentShape(Rectangle())
}
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Color.clear
.overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened)
)
}
.overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
)
.navigationBarHidden(isSideBarOpened ? true: false) // Would prefer a different way of doing this.
}
.navigationViewStyle(.stack)
@@ -318,6 +294,7 @@ struct ContentView: View {
}
.onAppear() {
self.connect()
//KingfisherManager.shared.cache.clearDiskCache()
setup_notifications()
}
.sheet(item: $active_sheet) { item in
@@ -325,20 +302,9 @@ struct ContentView: View {
case .report(let target):
MaybeReportView(target: target)
case .post:
PostView(replying_to: nil, references: [], damus_state: damus_state!)
PostView(replying_to: nil, references: [])
case .reply(let event):
ReplyView(replying_to: event, damus: damus_state!)
case .event:
EventDetailView()
case .filter:
let timeline = selected_timeline ?? .home
if #available(iOS 16.0, *) {
RelayFilterView(state: damus_state!, timeline: timeline)
.presentationDetents([.height(550)])
.presentationDragIndicator(.visible)
} else {
RelayFilterView(state: damus_state!, timeline: timeline)
}
}
}
.onOpenURL { url in
@@ -382,9 +348,6 @@ struct ContentView: View {
}
.onReceive(handle_notify(.like)) { like in
}
.onReceive(handle_notify(.deleted_account)) { notif in
self.is_deleted_account = true
}
.onReceive(handle_notify(.report)) { notif in
let target = notif.object as! ReportTarget
self.active_sheet = .report(target)
@@ -457,8 +420,6 @@ struct ContentView: View {
let post_res = obj.object as! NostrPostResult
switch post_res {
case .post(let post):
//let post = tup.0
//let to_relays = tup.1
print("post \(post.content)")
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
self.damus_state?.pool.send(.event(new_ev))
@@ -473,13 +434,7 @@ struct ContentView: View {
.onReceive(handle_notify(.new_mutes)) { notif in
home.filter_muted()
}
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
is_deleted_account = false
notify(.logout, ())
}
}
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate the user has been blocked"), isPresented: $user_blocked_confirm, actions: {
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate "), isPresented: $user_blocked_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) {
user_blocked_confirm = false
}
@@ -563,7 +518,6 @@ struct ContentView: View {
}
func switch_timeline(_ timeline: Timeline) {
self.popToRoot()
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
if timeline == self.selected_timeline {
@@ -589,33 +543,21 @@ struct ContentView: View {
func connect() {
let pool = RelayPool()
let metadatas = RelayMetadatas()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in BOOTSTRAP_RELAYS {
if let url = URL(string: relay) {
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
}
add_relay(pool, relay)
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: UserSettingsStore(),
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts()
self.damus_state = DamusState(pool: pool, keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache()
)
home.damus_state = self.damus_state!

View File

@@ -24,6 +24,7 @@
<string>zeusln</string>
<string>zebedee</string>
<string>lightning</string>
<string>squarecash</string>
<string>phoenix</string>
<string>lnlink</string>
<string>strike</string>

View File

@@ -11,43 +11,30 @@ import Foundation
class ActionBarModel: ObservableObject {
@Published var our_like: NostrEvent?
@Published var our_boost: NostrEvent?
@Published var our_zap: Zap?
@Published var our_tip: NostrEvent?
@Published var likes: Int
@Published var boosts: Int
@Published var zaps: Int
@Published var zap_total: Int64
@Published var tips: Int64
static func empty() -> ActionBarModel {
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
return ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
}
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?) {
init(likes: Int, boosts: Int, tips: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_tip: NostrEvent?) {
self.likes = likes
self.boosts = boosts
self.zaps = zaps
self.zap_total = zap_total
self.tips = tips
self.our_like = our_like
self.our_boost = our_boost
self.our_zap = our_zap
}
func update(damus: DamusState, evid: String) {
self.likes = damus.likes.counts[evid] ?? 0
self.boosts = damus.boosts.counts[evid] ?? 0
self.zaps = damus.zaps.event_counts[evid] ?? 0
self.zap_total = damus.zaps.event_totals[evid] ?? 0
self.our_like = damus.likes.our_events[evid]
self.our_boost = damus.boosts.our_events[evid]
self.our_zap = damus.zaps.our_zaps[evid]?.first
self.objectWillChange.send()
self.our_tip = our_tip
}
var is_empty: Bool {
return likes == 0 && boosts == 0 && zaps == 0
return likes == 0 && boosts == 0 && tips == 0
}
var zapped: Bool {
return our_zap != nil
var tipped: Bool {
return our_tip != nil
}
var liked: Bool {

View File

@@ -18,23 +18,17 @@ struct DamusState {
let profiles: Profiles
let dms: DirectMessagesModel
let previews: PreviewCache
let zaps: Zaps
let lnurls: LNUrls
let settings: UserSettingsStore
let relay_filters: RelayFilters
let relay_metadata: RelayMetadatas
let drafts: Drafts
var pubkey: String {
return keypair.pubkey
}
var is_privkey_user: Bool {
keypair.privkey != nil
}
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts())
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache())
}
}

View File

@@ -1,35 +0,0 @@
//
// DeepLPlan.swift
// damus
//
// Created by Terry Yiu on 2/3/23.
//
import Foundation
enum DeepLPlan: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
var id: String { self.tag }
var tag: String
var displayName: String
var url: String
}
case free
case pro
var model: Model {
switch self {
case .free:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Free", comment: "Dropdown option for selecting Free plan for DeepL translation service."), url: "https://api-free.deepl.com")
case .pro:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Pro", comment: "Dropdown option for selecting Pro plan for DeepL translation service."), url: "https://api.deepl.com")
}
}
static var allModels: [Model] {
return Self.allCases.map { $0.model }
}
}

View File

@@ -13,8 +13,6 @@ class DirectMessageModel: ObservableObject {
is_request = determine_is_request()
}
}
@Published var draft: String
var is_request: Bool
var our_pubkey: String
@@ -33,13 +31,11 @@ class DirectMessageModel: ObservableObject {
self.events = events
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
}
init(our_pubkey: String) {
self.events = []
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
}
}

View File

@@ -1,13 +0,0 @@
//
// DraftsModel.swift
// damus
//
// Created by Terry Yiu on 2/12/23.
//
import Foundation
class Drafts: ObservableObject {
@Published var post: String = ""
@Published var replies: [NostrEvent: String] = [:]
}

View File

@@ -51,7 +51,11 @@ class FollowersModel: ObservableObject {
if has_contact.contains(ev.pubkey) {
return
}
process_contact_event(state: damus_state, ev: ev)
process_contact_event(
pool: damus_state.pool,
contacts: damus_state.contacts,
pubkey: damus_state.pubkey, ev: ev
)
contacts?.append(ev.pubkey)
has_contact.insert(ev.pubkey)
}
@@ -82,7 +86,7 @@ class FollowersModel: ObservableObject {
if ev.known_kind == .contacts {
handle_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):

View File

@@ -60,7 +60,7 @@ class FollowingModel {
switch nev {
case .event(_, let ev):
if ev.kind == 0 {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
print("followingmodel notice: \(msg)")

View File

@@ -38,9 +38,6 @@ class HomeModel: ObservableObject {
var channels: [String: NostrEvent] = [:]
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
var done_init: Bool = false
var incoming_dms: [NostrEvent] = []
let dm_debouncer = Debouncer(interval: 0.5)
var should_debounce_dms = true
let home_subid = UUID().description
let contacts_subid = UUID().description
@@ -59,25 +56,16 @@ class HomeModel: ObservableObject {
init() {
self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
}
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
}
var pool: RelayPool {
return damus_state.pool
}
func setup_debouncer() {
// turn off debouncer after initial load
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
}
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
if !has_event.keys.contains(sub_id) {
@@ -124,62 +112,9 @@ class HomeModel: ObservableObject {
handle_channel_create(ev)
case .channel_meta:
handle_channel_meta(ev)
case .zap:
handle_zap_event(ev)
}
}
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_pubkey: String, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
return
}
damus_state.zaps.add_zap(zap: zap)
guard zap.target.pubkey == our_pubkey else {
return
}
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
return
}
handle_last_event(ev: ev, timeline: .notifications)
return
}
func handle_zap_event(_ ev: NostrEvent) {
// These are zap notifications
guard let ptag = event_tag(ev, name: "p") else {
return
}
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper)
return
}
guard let profile = damus_state.profiles.lookup(id: ptag) else {
return
}
guard let lnurl = profile.lnurl else {
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
return
}
DispatchQueue.main.async {
self.damus_state.profiles.zappers[ptag] = zapper
self.handle_zap_event_with_zapper(ev, our_pubkey: self.damus_state.pubkey, zapper: zapper)
}
}
}
func handle_channel_create(_ ev: NostrEvent) {
guard ev.is_valid else {
return
@@ -206,7 +141,7 @@ class HomeModel: ObservableObject {
}
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
process_contact_event(state: self.damus_state, ev: ev)
process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev)
if sub_id == init_subid {
pool.send(.unsubscribe(init_subid), to: [relay_id])
@@ -242,7 +177,6 @@ class HomeModel: ObservableObject {
case .success(let n):
let boosted = Counted(event: ev, id: e, total: n)
notify(.boosted, boosted)
notify(.update_stats, e)
}
}
@@ -260,7 +194,6 @@ class HomeModel: ObservableObject {
case .success(let n):
let liked = Counted(event: ev, id: e.ref_id, total: n)
notify(.liked, liked)
notify(.update_stats, e.ref_id)
}
}
@@ -301,7 +234,7 @@ class HomeModel: ObservableObject {
switch ev {
case .event(let sub_id, let ev):
// globally handle likes
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
if !always_process {
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
return
@@ -315,8 +248,7 @@ class HomeModel: ObservableObject {
case .eose(let sub_id):
if sub_id == dms_subid {
var dms = dms.dms.flatMap { $0.1.events }
dms.append(contentsOf: incoming_dms)
let dms = dms.dms.flatMap { $0.1.events }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
} else if sub_id == notifications_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications, damus_state: damus_state)
@@ -385,7 +317,6 @@ class HomeModel: ObservableObject {
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
NostrKind.zap.rawValue,
])
notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 100
@@ -441,7 +372,7 @@ class HomeModel: ObservableObject {
}
func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(profiles: damus_state.profiles, ev: ev)
}
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
@@ -471,46 +402,29 @@ class HomeModel: ObservableObject {
}
}
func insert_home_event(_ ev: NostrEvent) {
func insert_home_event(_ ev: NostrEvent) -> Bool {
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
if ok {
handle_last_event(ev: ev, timeline: .home)
}
return ok
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
if should_hide_event(contacts: damus_state.contacts, ev: ev) {
return
}
if sub_id == home_subid {
insert_home_event(ev)
let _ = insert_home_event(ev)
} else if sub_id == notifications_subid {
handle_notification(ev: ev)
}
}
func handle_dm(_ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
if !should_debounce_dms {
self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
}
self.incoming_dms = []
return
}
incoming_dms.append(ev)
dm_debouncer.debounce {
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
}
self.incoming_dms = []
if let notifs = handle_incoming_dm(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
self.new_events = notifs
}
}
}
@@ -616,17 +530,10 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
print("-----")
}
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
func process_metadata_event(profiles: Profiles, ev: NostrEvent) {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
DispatchQueue.main.async {
notify(.deleted_account, ())
}
return
}
var old_nip05: String? = nil
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
@@ -656,14 +563,14 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
// load pfps asap
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if URL(string: picture) != nil {
if let _ = URL(string: picture) {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
}
let banner = tprof.profile.banner ?? ""
if URL(string: banner) != nil {
if let _ = URL(string: banner) {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
@@ -676,31 +583,31 @@ func robohash(_ pk: String) -> String {
return "https://robohash.org/" + pk
}
func load_our_stuff(state: DamusState, ev: NostrEvent) {
guard ev.pubkey == state.pubkey else {
func load_our_stuff(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
guard ev.pubkey == pubkey else {
return
}
// only use new stuff
if let current_ev = state.contacts.event {
if let current_ev = contacts.event {
guard ev.created_at > current_ev.created_at else {
return
}
}
let m_old_ev = state.contacts.event
state.contacts.event = ev
let m_old_ev = contacts.event
contacts.event = ev
load_our_contacts(contacts: state.contacts, our_pubkey: state.pubkey, m_old_ev: m_old_ev, ev: ev)
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
load_our_contacts(contacts: contacts, our_pubkey: pubkey, m_old_ev: m_old_ev, ev: ev)
load_our_relays(contacts: contacts, our_pubkey: pubkey, pool: pool, m_old_ev: m_old_ev, ev: ev)
}
func process_contact_event(state: DamusState, ev: NostrEvent) {
load_our_stuff(state: state, ev: ev)
add_contact_if_friend(contacts: state.contacts, ev: ev)
func process_contact_event(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
load_our_stuff(pool: pool, contacts: contacts, pubkey: pubkey, ev: ev)
add_contact_if_friend(contacts: contacts, ev: ev)
}
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_old_ev: NostrEvent?, ev: NostrEvent) {
let bootstrap_dict: [String: RelayInfo] = [:]
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? BOOTSTRAP_RELAYS.reduce(into: bootstrap_dict) { (d, r) in
d[r] = .rw
@@ -724,15 +631,14 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let diff = old.symmetricDifference(new)
let new_relay_filters = load_relay_filters(state.pubkey) == nil
for d in diff {
changed = true
if new.contains(d) {
if let url = URL(string: d) {
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
try? pool.add_relay(url, info: decoded[d] ?? .rw)
}
} else {
state.pool.remove_relay(d)
pool.remove_relay(d)
}
}
@@ -741,61 +647,9 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
}
}
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: URL, info: RelayInfo, new_relay_filters: Bool) {
try? pool.add_relay(url, info: info)
let relay_id = url.absoluteString
guard metadatas.lookup(relay_id: relay_id) == nil else {
return
}
Task.detached(priority: .background) {
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
return
}
DispatchQueue.main.async {
metadatas.insert(relay_id: relay_id, metadata: meta)
// if this is the first time adding filters, we should filter non-paid relays
if new_relay_filters && !meta.is_paid {
relay_filters.insert(timeline: .search, relay_id: relay_id)
}
}
}
}
func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
var urlString = relay_id.replacingOccurrences(of: "wss://", with: "https://")
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
guard let url = URL(string: urlString) else {
return nil
}
var request = URLRequest(url: url)
request.setValue("application/nostr+json", forHTTPHeaderField: "Accept")
var res: (Data, URLResponse)? = nil
res = try await URLSession.shared.data(for: request)
guard let data = res?.0 else {
return nil
}
let nip11 = try JSONDecoder().decode(RelayMetadata.self, from: data)
return nip11
}
func process_relay_metadata() {
}
@discardableResult
func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
func handle_incoming_dm(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? {
var inserted = false
var found = false
let ours = ev.pubkey == our_pubkey
var i = 0
@@ -822,34 +676,15 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
}
if !found {
inserted = true
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
dms.dms.append((the_pk, model))
inserted = true
}
var new_bits: NewEventsBits? = nil
if inserted {
new_bits = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
}
return (inserted, new_bits)
}
@discardableResult
func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, evs: [NostrEvent]) -> NewEventsBits? {
var inserted = false
var new_events: NewEventsBits? = nil
for ev in evs {
let res = handle_incoming_dm(ev: ev, our_pubkey: our_pubkey, dms: dms, prev_events: prev_events)
inserted = res.0 || inserted
if let new = res.1 {
new_events = new
}
}
if inserted {
new_events = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
return a.1.events.last!.created_at > b.1.events.last!.created_at
}
@@ -886,10 +721,9 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
}
func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
func should_hide_event(contacts: Contacts, ev: NostrEvent) -> Bool {
if contacts.is_muted(ev.pubkey) {
return false
return true
}
return ev.should_show_event
return !ev.should_show_event
}

View File

@@ -0,0 +1,108 @@
//
// KFImageModel.swift
// damus
//
// Created by Oleg Abalonski on 1/11/23.
//
import Foundation
import Kingfisher
import SVGKit
class KFImageModel: ObservableObject {
let url: URL?
let fallbackUrl: URL?
let processor: ImageProcessor
let serializer: CacheSerializer
@Published var refreshID = ""
init(url: URL?, fallbackUrl: URL?, maxByteSize: Int, downsampleSize: CGSize) {
self.url = url
self.fallbackUrl = fallbackUrl
self.processor = CustomImageProcessor(maxSize: maxByteSize, downsampleSize: downsampleSize)
self.serializer = CustomCacheSerializer(maxSize: maxByteSize, downsampleSize: downsampleSize)
}
func refresh() -> Void {
DispatchQueue.main.async {
self.refreshID = UUID().uuidString
}
}
func cache(_ image: UIImage, forKey key: String) -> Void {
KingfisherManager.shared.cache.store(image, forKey: key, processorIdentifier: processor.identifier) { _ in
self.refresh()
}
}
func downloadFailed() -> Void {
guard let url = url, let fallbackUrl = fallbackUrl else { return }
DispatchQueue.global(qos: .background).async {
KingfisherManager.shared.downloader.downloadImage(with: fallbackUrl) { result in
var fallbackImage: UIImage {
switch result {
case .success(let imageLoadingResult):
return imageLoadingResult.image
case .failure(let error):
print(error)
return UIImage()
}
}
self.cache(fallbackImage, forKey: url.absoluteString)
}
}
}
}
struct CustomImageProcessor: ImageProcessor {
let maxSize: Int
let downsampleSize: CGSize
let identifier = "com.damus.customimageprocessor"
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(_):
// This case will never run
return DefaultImageProcessor.default.process(item: item, options: options)
case .data(let data):
// Handle large image size
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
// Handle SVG image
if let svgImage = SVGKImage(data: data), let image = svgImage.uiImage {
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)
}
}
}
struct CustomCacheSerializer: CacheSerializer {
let maxSize: Int
let downsampleSize: CGSize
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
return DefaultCacheSerializer.default.data(with: image, original: original)
}
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return DefaultCacheSerializer.default.image(with: data, options: options)
}
}

View File

@@ -1,41 +0,0 @@
//
// LibreTranslateServer.swift
// damus
//
// Created by Terry Yiu on 1/21/23.
//
import Foundation
enum LibreTranslateServer: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
var id: String { self.tag }
var tag: String
var displayName: String
var url: String?
}
case argosopentech
case terraprint
case vern
case custom
var model: Model {
switch self {
case .argosopentech:
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
case .terraprint:
return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co")
case .vern:
return .init(tag: self.rawValue, displayName: "lt.vern.cc", url: "https://lt.vern.cc")
case .custom:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil)
}
}
static var allModels: [Model] {
return Self.allCases.map { $0.model }
}
}

View File

@@ -7,10 +7,6 @@
import Foundation
enum CountResult {
case already_counted
case success(Int)
}
class EventCounter {
var counts: [String: Int] = [:]
@@ -18,6 +14,11 @@ class EventCounter {
var our_events: [String: NostrEvent] = [:]
var our_pubkey: String
enum CountResult {
case already_counted
case success(Int)
}
init (our_pubkey: String) {
self.our_pubkey = our_pubkey
}

View File

@@ -32,30 +32,13 @@ struct IdBlock: Identifiable {
let block: Block
}
typealias Invoice = LightningInvoice<Amount>
typealias ZapInvoice = LightningInvoice<Int64>
enum InvoiceDescription {
case description(String)
case description_hash(Data)
}
struct LightningInvoice<T> {
let description: InvoiceDescription
let amount: T
struct Invoice {
let description: String
let amount: Amount
let string: String
let expiry: UInt64
let payment_hash: Data
let created_at: UInt64
var description_string: String {
switch description {
case .description(let string):
return string
case .description_hash:
return ""
}
}
}
enum Block {
@@ -206,76 +189,20 @@ enum Amount: Equatable {
case .any:
return NSLocalizedString("Any", comment: "Any amount of sats")
case .specific(let amt):
return format_msats(amt)
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
let sats = NSNumber(value: (Double(amt) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
}
}
}
func format_actions_abbrev(_ actions: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
if actions >= 1_000_000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if actions >= 1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return "\(actions)"
}
let actions = NSNumber(value: actions)
return formatter.string(from: actions) ?? "\(actions)"
}
func format_msats_abbrev(_ msats: Int64) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
let sats = NSNumber(value: (Double(msats) / 1000.0))
if msats >= 1_000_000*1000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if msats >= 1000*1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return sats.stringValue
}
return formatter.string(from: sats) ?? sats.stringValue
}
func format_msats(_ msat: Int64) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {
guard let invstr = strblock_to_string(b.invstr) else {
return nil
@@ -285,8 +212,9 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return nil
}
guard let description = convert_invoice_description(b11: b11) else {
return nil
var description = ""
if b11.description != nil {
description = String(cString: b11.description)
}
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
@@ -297,18 +225,6 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
}
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
if let desc = b11.description {
return .description(String(cString: desc))
}
if var deschash = maybe_pointee(b11.description_hash) {
return .description_hash(Data(bytes: &deschash, count: 32))
}
return nil
}
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
{
let ind = Int(ind)

View File

@@ -20,24 +20,6 @@ class ProfileModel: ObservableObject, Equatable {
var sub_id = UUID().description
var prof_subid = UUID().description
func follows(pubkey: String) -> Bool {
guard let contacts = self.contacts else {
return false
}
for tag in contacts.tags {
guard tag.count >= 2 && tag[0] == "p" else {
continue
}
if tag[1] == pubkey {
return true
}
}
return false
}
func get_follow_target() -> FollowTarget {
if let contacts = contacts {
return .contact(contacts)
@@ -88,7 +70,7 @@ class ProfileModel: ObservableObject, Equatable {
}
func handle_profile_contact_event(_ ev: NostrEvent) {
process_contact_event(state: damus, ev: ev)
process_contact_event(pool: damus.pool, contacts: damus.contacts, pubkey: damus.pubkey, ev: ev)
// only use new stuff
if let current_ev = self.contacts {
@@ -111,11 +93,11 @@ class ProfileModel: ObservableObject, Equatable {
return
}
if ev.is_textlike || ev.known_kind == .boost {
insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at})
let _ = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at})
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
process_metadata_event(profiles: damus.profiles, ev: ev)
}
seen_event.insert(ev.id)
}

View File

@@ -31,13 +31,12 @@ class SearchHomeModel: ObservableObject {
}
func filter_muted() {
events = events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
events = events.filter { !should_hide_event(contacts: damus_state.contacts, ev: $0) }
}
func subscribe() {
loading = true
let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters)
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event)
}
func unsubscribe(to: String? = nil) {
@@ -55,13 +54,13 @@ class SearchHomeModel: ObservableObject {
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
return
}
if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if ev.is_textlike && !should_hide_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if seen_pubkey.contains(ev.pubkey) {
return
}
seen_pubkey.insert(ev.pubkey)
insert_uniq_sorted_event(events: &events, new_ev: ev) {
let _ = insert_uniq_sorted_event(events: &events, new_ev: ev) {
$0.created_at > $1.created_at
}
}
@@ -130,7 +129,7 @@ func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent
}
if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(profiles: damus_state.profiles, ev: ev)
}
}

View File

@@ -15,20 +15,14 @@ class SearchModel: ObservableObject {
let pool: RelayPool
var search: NostrFilter
let contacts: Contacts
let sub_id = UUID().description
let limit: UInt32 = 500
init(contacts: Contacts, pool: RelayPool, search: NostrFilter) {
self.contacts = contacts
init(pool: RelayPool, search: NostrFilter) {
self.pool = pool
self.search = search
}
func filter_muted() {
self.events = self.events.filter { should_show_event(contacts: contacts, ev: $0) }
}
func subscribe() {
// since 1 month
search.limit = self.limit
@@ -53,10 +47,6 @@ class SearchModel: ObservableObject {
return
}
guard should_show_event(contacts: contacts, ev: ev) else {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at } ) {
objectWillChange.send()
}
@@ -98,21 +88,6 @@ func event_matches_hashtag(_ ev: NostrEvent, hashtags: [String]) -> Bool {
return false
}
func tag_is_hashtag(_ tag: [String]) -> Bool {
// "hashtag" is deprecated, will remove in the future
return tag.count >= 2 && (tag[0] == "hashtag" || tag[0] == "t")
}
func has_hashtag(_ tags: [[String]], hashtag: String) -> Bool {
for tag in tags {
if tag_is_hashtag(tag) && tag[1] == hashtag {
return true
}
}
return false
}
func event_matches_filter(_ ev: NostrEvent, filter: NostrFilter) -> Bool {
if let hashtags = filter.hashtag {
return event_matches_hashtag(ev, hashtags: hashtags)

View File

@@ -190,7 +190,7 @@ class ThreadModel: ObservableObject {
}
if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(profiles: damus_state.profiles, ev: ev)
} else if ev.is_textlike {
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {

View File

@@ -1,37 +0,0 @@
//
// TranslationService.swift
// damus
//
// Created by Terry Yiu on 2/3/23.
//
import Foundation
enum TranslationService: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
var id: String { self.tag }
var tag: String
var displayName: String
}
case none
case libretranslate
case deepl
var model: Model {
switch self {
case .none:
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no 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:
return .init(tag: self.rawValue, displayName: NSLocalizedString("DeepL (Proprietary, Higher Accuracy)", comment: "Dropdown option for selecting DeepL as the translation service."))
}
}
static var allModels: [Model] {
return Self.allCases.map { $0.model }
}
}

View File

@@ -6,76 +6,6 @@
//
import Foundation
import Vault
func should_show_wallet_selector(_ pubkey: String) -> Bool {
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
}
func pk_setting_key(_ pubkey: String, key: String) -> String {
return "\(pubkey)_\(key)"
}
func default_zap_setting_key(pubkey: String) -> String {
return pk_setting_key(pubkey, key: "default_zap_amount")
}
func set_default_zap_amount(pubkey: String, amount: Int) {
let key = default_zap_setting_key(pubkey: pubkey)
UserDefaults.standard.setValue(amount, forKey: key)
}
func get_default_zap_amount(pubkey: String) -> Int? {
let key = default_zap_setting_key(pubkey: pubkey)
let amt = UserDefaults.standard.integer(forKey: key)
if amt == 0 {
return nil
}
return amt
}
func get_default_wallet(_ pubkey: String) -> Wallet {
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
{
return default_wallet
} else {
return .system_default_wallet
}
}
private func get_translation_service(_ pubkey: String) -> TranslationService? {
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
return nil
}
return TranslationService(rawValue: translation_service)
}
private func get_deepl_plan(_ pubkey: String) -> DeepLPlan? {
guard let server_name = UserDefaults.standard.string(forKey: "deepl_plan") else {
return nil
}
return DeepLPlan(rawValue: server_name)
}
private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
return nil
}
return LibreTranslateServer(rawValue: server_name)
}
private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
if let url = server.model.url {
return url
}
return UserDefaults.standard.object(forKey: "libretranslate_url") as? String
}
class UserSettingsStore: ObservableObject {
@Published var default_wallet: Wallet {
@@ -96,154 +26,16 @@ class UserSettingsStore: ObservableObject {
}
}
@Published var translation_service: TranslationService {
didSet {
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
}
}
@Published var deepl_plan: DeepLPlan {
didSet {
UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
}
}
@Published var deepl_api_key: String {
didSet {
do {
if deepl_api_key == "" {
try clearDeepLApiKey()
} else {
try saveDeepLApiKey(deepl_api_key)
}
} catch {
// No-op.
}
}
}
@Published var libretranslate_server: LibreTranslateServer {
didSet {
if oldValue == libretranslate_server {
return
}
UserDefaults.standard.set(libretranslate_server.rawValue, forKey: "libretranslate_server")
libretranslate_api_key = ""
if libretranslate_server == .custom {
libretranslate_url = ""
} else {
libretranslate_url = libretranslate_server.model.url!
}
}
}
@Published var libretranslate_url: String {
didSet {
UserDefaults.standard.set(libretranslate_url, forKey: "libretranslate_url")
}
}
@Published var libretranslate_api_key: String {
didSet {
do {
if libretranslate_api_key == "" {
try clearLibreTranslateApiKey()
} else {
try saveLibreTranslateApiKey(libretranslate_api_key)
}
} catch {
// No-op.
}
}
}
init() {
// TODO: pubkey-scoped settings
let pubkey = ""
self.default_wallet = get_default_wallet(pubkey)
show_wallet_selector = should_show_wallet_selector(pubkey)
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
{
self.default_wallet = default_wallet
} else {
default_wallet = .system_default_wallet
}
show_wallet_selector = UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
// Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
if let translation_service = get_translation_service(pubkey) {
self.translation_service = translation_service
} else {
self.translation_service = .none
}
if let libretranslate_server = get_libretranslate_server(pubkey) {
self.libretranslate_server = libretranslate_server
self.libretranslate_url = get_libretranslate_url(pubkey, server: libretranslate_server) ?? ""
} else {
// Choose a random server to distribute load.
libretranslate_server = .allCases.filter { $0 != .custom }.randomElement()!
libretranslate_url = ""
}
do {
libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
} catch {
libretranslate_api_key = ""
}
if let deepl_plan = get_deepl_plan(pubkey) {
self.deepl_plan = deepl_plan
} else {
self.deepl_plan = .free
}
do {
deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
} catch {
deepl_api_key = ""
}
}
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
private func clearLibreTranslateApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
private func saveDeepLApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusDeepLKeychainConfiguration())
}
private func clearDeepLApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
}
func can_translate(_ pubkey: String) -> Bool {
switch translation_service {
case .none:
return false
case .libretranslate:
return URLComponents(string: libretranslate_url) != nil
case .deepl:
return deepl_api_key != ""
}
}
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
var serviceName = "damus"
var accessGroup: String? = nil
var accountName = "libretranslate_apikey"
}
struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
var serviceName = "damus"
var accessGroup: String? = nil
var accountName = "deepl_apikey"
}

View File

@@ -45,7 +45,7 @@ enum Wallet: String, CaseIterable, Identifiable {
return .init(index: 0, tag: "strike", displayName: NSLocalizedString("Strike", comment: "Dropdown option label for Lightning wallet, Strike."), link: "strike:",
appStoreLink: "https://apps.apple.com/us/app/strike-bitcoin-payments/id1488724463", image: "strike")
case .cashapp:
return .init(index: 1, tag: "cashapp", displayName: NSLocalizedString("Cash App", comment: "Dropdown option label for Lightning wallet, Cash App."), link: "https://cash.app/launch/lightning/",
return .init(index: 1, tag: "cashapp", displayName: NSLocalizedString("Cash App", comment: "Dropdown option label for Lightning wallet, Cash App."), link: "squarecash://",
appStoreLink: "https://apps.apple.com/us/app/cash-app/id711923939", image: "cashapp")
case .muun:
return .init(index: 2, tag: "muun", displayName: NSLocalizedString("Muun", comment: "Dropdown option label for Lightning wallet, Muun."), link: "muun:", appStoreLink: "https://apps.apple.com/us/app/muun-wallet/id1482037683", image: "muun")
@@ -53,7 +53,7 @@ enum Wallet: String, CaseIterable, Identifiable {
return .init(index: 3, tag: "bluewallet", displayName: NSLocalizedString("Blue Wallet", comment: "Dropdown option label for Lightning wallet, Blue Wallet."), link: "bluewallet:lightning:",
appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
case .walletofsatoshi:
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet of Satoshi."), link: "walletofsatoshi:lightning:",
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet Of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet Of Satoshi."), link: "walletofsatoshi:lightning:",
appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
case .zebedee:
return .init(index: 5, tag: "zebedee", displayName: NSLocalizedString("Zebedee", comment: "Dropdown option label for Lightning wallet, Zebedee."), link: "zebedee:lightning:",

View File

@@ -1,77 +0,0 @@
//
// ZapsModel.swift
// damus
//
// Created by William Casarin on 2023-02-10.
//
import Foundation
class ZapsModel: ObservableObject {
let state: DamusState
let target: ZapTarget
var zaps: [Zap]
let zaps_subid = UUID().description
init(state: DamusState, target: ZapTarget) {
self.state = state
self.target = target
self.zaps = []
}
func subscribe() {
var filter = NostrFilter.filter_kinds([9735])
switch target {
case .profile(let profile_id):
filter.pubkeys = [profile_id]
case .note(let note_target):
filter.referenced_ids = [note_target.note_id]
}
state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: zaps_subid)
}
func handle_event(relay_id: String, conn_ev: NostrConnectionEvent) {
guard case .nostr_event(let resp) = conn_ev else {
return
}
guard resp.subid == zaps_subid else {
return
}
guard case .event(_, let ev) = resp else {
return
}
guard ev.kind == 9735 else {
return
}
if let zap = state.zaps.zaps[ev.id] {
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
} else {
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
return
}
state.zaps.add_zap(zap: zap)
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
}
}
}

View File

@@ -24,22 +24,18 @@ struct Profile: Codable {
}
private func str(_ str: String) -> String? {
return get_val(str)
}
private func get_val<T>(_ v: String) -> T? {
guard let val = self.value[v] else{
guard let val = self.value[str] else{
return nil
}
guard let s = val.value as? T else {
guard let s = val.value as? String else {
return nil
}
return s
}
private mutating func set_val<T>(_ key: String, _ val: T?) {
private mutating func set_str(_ key: String, _ val: String?) {
if val == nil {
self.value.removeValue(forKey: key)
return
@@ -48,15 +44,6 @@ struct Profile: Codable {
self.value[key] = AnyCodable.init(val)
}
private mutating func set_str(_ key: String, _ val: String?) {
set_val(key, val)
}
var deleted: Bool? {
get { return get_val("deleted"); }
set(s) { set_val("deleted", s) }
}
var display_name: String? {
get { return str("display_name"); }
set(s) { set_str("display_name", s) }
@@ -102,7 +89,7 @@ struct Profile: Codable {
}
var lnurl: String? {
guard let addr = lud16 ?? lud06 else {
guard let addr = lud06 ?? lud16 else {
return nil;
}
@@ -110,10 +97,6 @@ struct Profile: Codable {
return lnaddress_to_lnurl(addr);
}
if !addr.lowercased().hasPrefix("lnurl") {
return nil
}
return addr;
}
@@ -126,10 +109,6 @@ struct Profile: Codable {
return make_ln_url(self.lnurl)
}
init() {
self.value = [:]
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.value = try container.decode([String: AnyCodable].self)

View File

@@ -11,8 +11,6 @@ import secp256k1
import secp256k1_implementation
import CryptoKit
enum ValidationResult: Decodable {
case ok
case bad_id
@@ -29,7 +27,7 @@ struct KeyEvent {
let relay_url: String
}
struct ReferencedId: Identifiable, Hashable, Equatable {
struct ReferencedId: Identifiable, Hashable {
let ref_id: String
let relay_id: String?
let key: String
@@ -81,7 +79,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
}
var too_big: Bool {
return self.content.count > 16000
return self.content.count > 32000
}
var should_show_event: Bool {
@@ -105,15 +103,11 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
if let bs = _blocks {
return bs
}
let blocks = get_blocks(content: self.get_content(privkey))
let blocks = parse_mentions(content: self.get_content(privkey), tags: self.tags)
self._blocks = blocks
return blocks
}
func get_blocks(content: String) -> [Block] {
return parse_mentions(content: content, tags: self.tags)
}
lazy var inner_event: NostrEvent? = {
// don't try to deserialize an inner event if we know there won't be one
if self.known_kind == .boost {
@@ -370,14 +364,9 @@ func decode_nostr_event(txt: String) -> NostrResponse? {
func encode_json<T: Encodable>(_ val: T) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
}
func decode_nostr_event_json(json: String) -> NostrEvent? {
return decode_json(json)
}
func decode_json<T: Decodable>(_ val: String) -> T? {
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
}
@@ -578,26 +567,6 @@ func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> Nost
return ev
}
func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
switch target {
case .profile(let pk):
return [["p", pk]]
case .note(let note_target):
return [["e", note_target.note_id], ["p", note_target.author]]
}
}
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent {
var tags = zap_target_to_tags(target)
var relay_tag = ["relays"]
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
tags.append(relay_tag)
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: privkey, ev: ev)
return ev
}
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []

View File

@@ -7,7 +7,7 @@
import Foundation
struct NostrFilter: Codable, Equatable {
struct NostrFilter: Codable {
var ids: [String]?
var kinds: [Int]?
var referenced_ids: [String]?

View File

@@ -20,5 +20,4 @@ enum NostrKind: Int {
case channel_meta = 41
case chat = 42
case list = 30000
case zap = 9735
}

View File

@@ -8,7 +8,7 @@
import Foundation
enum NostrLink: Equatable {
enum NostrLink {
case ref(ReferencedId)
case filter(NostrFilter)
}
@@ -101,24 +101,6 @@ func decode_universal_link(_ s: String) -> NostrLink? {
return nil
}
func decode_nostr_bech32_uri(_ s: String) -> NostrLink? {
guard let obj = Bech32Object.parse(s) else {
return nil
}
switch obj {
case .nsec(let privkey):
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
case .npub(let pubkey):
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
case .note(let id):
return .ref(ReferencedId(ref_id: id, relay_id: nil, key: "e"))
}
}
func decode_nostr_uri(_ s: String) -> NostrLink? {
if s.starts(with: "https://damus.io/") {
return decode_universal_link(s)
@@ -140,15 +122,5 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
return .filter(NostrFilter.filter_hashtag([parts[1].lowercased()]))
}
if let rid = tag_to_refid(parts) {
return .ref(rid)
}
guard parts.count == 1 else {
return nil
}
let part = parts[0]
return decode_nostr_bech32_uri(part)
return tag_to_refid(parts).map { .ref($0) }
}

View File

@@ -12,20 +12,11 @@ import UIKit
class Profiles {
var profiles: [String: TimestampedProfile] = [:]
var validated: [String: NIP05] = [:]
var zappers: [String: String] = [:]
func is_validated(_ pk: String) -> NIP05? {
return validated[pk]
}
func lookup_zapper(pubkey: String) -> String? {
if let zapper = zappers[pubkey] {
return zapper
}
return nil
}
func add(id: String, profile: TimestampedProfile) {
profiles[id] = profile
}

View File

@@ -72,7 +72,7 @@ func char_to_hex(_ c: UInt8) -> UInt8?
return nil;
}
@discardableResult
func hex_decode(_ str: String) -> [UInt8]?
{
if str.count == 0 {

View File

@@ -7,16 +7,16 @@
import Foundation
public struct RelayInfo: Codable {
struct RelayInfo: Codable {
let read: Bool
let write: Bool
static let rw = RelayInfo(read: true, write: true)
}
public struct RelayDescriptor: Codable {
public let url: URL
public let info: RelayInfo
struct RelayDescriptor: Codable {
let url: URL
let info: RelayInfo
}
enum RelayFlags: Int {
@@ -24,30 +24,6 @@ enum RelayFlags: Int {
case broken = 1
}
struct Limitations: Codable {
let payment_required: Bool?
static var empty: Limitations {
Limitations(payment_required: nil)
}
}
struct RelayMetadata: Codable {
let name: String?
let description: String?
let pubkey: String?
let contact: String?
let supported_nips: [Int]?
let software: String?
let version: String?
let limitation: Limitations?
let payments_url: String?
var is_paid: Bool {
return limitation?.payment_required ?? false
}
}
class Relay: Identifiable {
let descriptor: RelayDescriptor
let connection: RelayConnection

View File

@@ -89,20 +89,9 @@ class RelayConnection: WebSocketDelegate {
self.isConnected = false
case .text(let txt):
if txt.count > 2000 {
DispatchQueue.global(qos: .default).async {
if let ev = decode_nostr_event(txt: txt) {
DispatchQueue.main.async {
self.handleEvent(.nostr_event(ev))
}
return
}
}
} else {
if let ev = decode_nostr_event(txt: txt) {
handleEvent(.nostr_event(ev))
return
}
if let ev = decode_nostr_event(txt: txt) {
handleEvent(.nostr_event(ev))
return
}
print("decode failed for \(txt)")
@@ -129,9 +118,10 @@ func make_nostr_req(_ req: NostrRequest) -> String? {
}
func make_nostr_push_event(ev: NostrEvent) -> String? {
guard let event = encode_json(ev) else {
return nil
}
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
let event_data = try! encoder.encode(ev)
let event = String(decoding: event_data, as: UTF8.self)
let encoded = "[\"EVENT\",\(event)]"
print(encoded)
return encoded

View File

@@ -42,8 +42,6 @@ class RelayPool {
var relays: [Relay] = []
var handlers: [RelayHandler] = []
var request_queue: [QueuedRequest] = []
var seen: Set<String> = Set()
var counts: [String: UInt64] = [:]
var descriptors: [RelayDescriptor] {
relays.map { $0.descriptor }
@@ -151,9 +149,9 @@ class RelayPool {
self.send(.unsubscribe(sub_id), to: to)
}
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> (), to: [String]? = nil) {
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> ()) {
register_handler(sub_id: sub_id, handler: handler)
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
send(.subscribe(.init(filters: filters, sub_id: sub_id)))
}
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [String]?, handler: @escaping (String, NostrConnectionEvent) -> ()) {
@@ -195,13 +193,27 @@ class RelayPool {
relay.connection.send(req)
}
}
func get_relays(_ ids: [String]) -> [Relay] {
relays.filter { ids.contains($0.id) }
var relays: [Relay] = []
for id in ids {
if let relay = get_relay(id) {
relays.append(relay)
}
}
return relays
}
func get_relay(_ id: String) -> Relay? {
relays.first(where: { $0.id == id })
for relay in relays {
if relay.id == id {
return relay
}
}
return nil
}
func record_last_pong(relay_id: String, event: NostrConnectionEvent) {
@@ -229,25 +241,8 @@ class RelayPool {
}
}
func record_seen(relay_id: String, event: NostrConnectionEvent) {
if case .nostr_event(let ev) = event {
if case .event(_, let nev) = ev {
let k = relay_id + nev.id
if !seen.contains(k) {
seen.insert(k)
if counts[relay_id] == nil {
counts[relay_id] = 1
} else {
counts[relay_id] = (counts[relay_id] ?? 0) + 1
}
}
}
}
}
func handle_event(relay_id: String, event: NostrConnectionEvent) {
record_last_pong(relay_id: relay_id, event: event)
record_seen(relay_id: relay_id, event: event)
// run req queue when we reconnect
if case .ws_event(let ws) = event {

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