Compare commits

..

1 Commits

Author SHA1 Message Date
tyiu a72f964111 Fix localized string for privacy access description for photos 2023-01-21 16:01:31 -05:00
140 changed files with 912 additions and 27442 deletions
+8 -22
View File
@@ -1,6 +1,4 @@
name: Run Test Suite
run-name: Testing ${{ github.ref }} by @${{ github.actor }}
name: Test
on:
push:
branches:
@@ -8,24 +6,12 @@ on:
pull_request:
branches:
- "*"
jobs:
run_tests:
runs-on: macos-12
strategy:
matrix:
include:
- xcode: "14.2"
ios: "16.2"
name: Test iOS (${{ matrix.ios }})
test:
name: Run Tests
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode }}
- name: Run Tests
run: xcodebuild test -scheme damus -project damus.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=${{ matrix.ios }}' | xcpretty && exit ${PIPESTATUS[0]}
- name: Checkout repository
uses: actions/checkout@v1
- name: Running Tests
run: xcodebuild test -scheme damus -project damus.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' | xcpretty && exit ${PIPESTATUS[0]}
+1 -99
View File
@@ -1,107 +1,10 @@
## [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)
- Added ability to block users (William Casarin)
- Added a way to report content (William Casarin)
- Stretchable profile cover header (Swift)
### Changed
- Bump pfp/banner animated fize size limit to 5MiB/20MiB (William Casarin)
- Updated default boostrap relays (Ricardo Arturo Cabral Mejía)
### Fixed
- allow ws:// relays again (Steven Briscoe)
[1.0.0-11]: https://github.com/damus-io/damus/releases/tag/v1.0.0-11
## [1.0.0-8] - 2023-01-22
### Added
- 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)
- Add DM Message Requests (William Casarin)
### Fixed
- Fix commands and emojis getting included in hashtags (William Casarin)
- Fix duplicate post buttons when swiping tabs (Thomas Rademaker)
- Show embedded note references (William Casarin)
[1.0.0-8]: https://github.com/damus-io/damus/releases/tag/v1.0.0-8
## [1.0.0-7] - 2023-01-20
### Added
- 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)
@@ -515,4 +418,3 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
+4 -34
View File
@@ -91,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... :)
@@ -133,7 +107,3 @@ First contributors:
1. @randymcmillan
2. @jcarucci27
### git log bot
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
@@ -41,14 +41,9 @@ Abbreviated version of a nostr public key.</note>
<trans-unit id="%@ %@" xml:space="preserve">
<source>%@ %@</source>
<target>%@ %@</target>
<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'.
<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 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>
<target>%@ has been blocked</target>
<note>Alert message that informs a user was blocked.</note>
</trans-unit>
<trans-unit id="%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." xml:space="preserve">
<source>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</source>
<target>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</target>
@@ -67,7 +62,7 @@ Sentence composed of 2 variables to describe how many profiles a user is followi
<trans-unit id="%lld" xml:space="preserve">
<source>%lld</source>
<target>%lld</target>
<note>Number of tip payments on a post.
<note>Number of reposts.
Number of profiles a user is following.</note>
</trans-unit>
<trans-unit id="%lld/%lld" xml:space="preserve">
@@ -80,9 +75,9 @@ Number of profiles a user is following.</note>
<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">
@@ -105,11 +100,6 @@ Number of profiles a user is following.</note>
<target>@</target>
<note>Prefix character to username.</note>
</trans-unit>
<trans-unit id="API Key (optional)" xml:space="preserve">
<source>API Key (optional)</source>
<target>API Key (optional)</target>
<note>Example URL to LibreTranslate server</note>
</trans-unit>
<trans-unit id="About" xml:space="preserve">
<source>About</source>
<target>About</target>
@@ -125,21 +115,11 @@ Number of profiles a user is following.</note>
<target>Absolute Boss</target>
<note>Placeholder text for About Me description.</note>
</trans-unit>
<trans-unit id="Accept" xml:space="preserve">
<source>Accept</source>
<target>Accept</target>
<note>Button to accept the end user license agreement before being allowed into the app.</note>
</trans-unit>
<trans-unit id="Account ID" xml:space="preserve">
<source>Account ID</source>
<target>Account ID</target>
<note>Label to indicate the public ID of the account.</note>
</trans-unit>
<trans-unit id="Actions" xml:space="preserve">
<source>Actions</source>
<target>Actions</target>
<note>Title for confirmation dialog to either share, report, or block a profile.</note>
</trans-unit>
<trans-unit id="Add" xml:space="preserve">
<source>Add</source>
<target>Add</target>
@@ -151,11 +131,6 @@ Number of profiles a user is following.</note>
<target>Add Relay</target>
<note>Label for section for adding a relay server.</note>
</trans-unit>
<trans-unit id="Add all" xml:space="preserve">
<source>Add all</source>
<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="Any" xml:space="preserve">
<source>Any</source>
<target>Any</target>
@@ -191,33 +166,6 @@ Number of profiles a user is following.</note>
<target>Blixt Wallet</target>
<note>Dropdown option label for Lightning wallet, Blixt Wallet</note>
</trans-unit>
<trans-unit id="Block" xml:space="preserve">
<source>Block</source>
<target>Block</target>
<note>Alert button to block a user.
Button to block a profile.
Context menu option for blocking users.</note>
</trans-unit>
<trans-unit id="Block %@?" xml:space="preserve">
<source>Block %@?</source>
<target>Block %@?</target>
<note>Alert message prompt to ask if a user should be blocked.</note>
</trans-unit>
<trans-unit id="Block User" xml:space="preserve">
<source>Block User</source>
<target>Block User</target>
<note>Title of alert for blocking a user.</note>
</trans-unit>
<trans-unit id="Blocked" xml:space="preserve">
<source>Blocked</source>
<target>Blocked</target>
<note>Sidebar menu label for Profile view.</note>
</trans-unit>
<trans-unit id="Blocked Users" xml:space="preserve">
<source>Blocked Users</source>
<target>Blocked Users</target>
<note>Navigation title of view to see list of blocked users.</note>
</trans-unit>
<trans-unit id="Blue Wallet" xml:space="preserve">
<source>Blue Wallet</source>
<target>Blue Wallet</target>
@@ -236,9 +184,7 @@ Number of profiles a user is following.</note>
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Cancel</target>
<note>Alert button to cancel out of alert for blocking a user.
Button to cancel out of alert that creates a new mutelist.
Button to cancel out of posting a note.
<note>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 out of logging out the user.</note>
@@ -304,19 +250,14 @@ Number of profiles a user is following.</note>
<target>Copy Note JSON</target>
<note>Context menu option for copying the JSON text from the note.</note>
</trans-unit>
<trans-unit id="Copy Report ID" xml:space="preserve">
<source>Copy Report ID</source>
<target>Copy Report ID</target>
<note>Button to copy report ID.</note>
</trans-unit>
<trans-unit id="Copy Text" xml:space="preserve">
<source>Copy Text</source>
<target>Copy Text</target>
<note>Context menu option for copying the text from an note.</note>
</trans-unit>
<trans-unit id="Copy User Pubkey" xml:space="preserve">
<source>Copy User Pubkey</source>
<target>Copy User Pubkey</target>
<trans-unit id="Copy User ID" xml:space="preserve">
<source>Copy User ID</source>
<target>Copy User ID</target>
<note>Context menu option for copying the ID of the user who created the note.</note>
</trans-unit>
<trans-unit id="Copy invoice" xml:space="preserve">
@@ -324,11 +265,6 @@ Number of profiles a user is following.</note>
<target>Copy invoice</target>
<note>Title of section for copying a Lightning invoice identifier.</note>
</trans-unit>
<trans-unit id="Could not find user to block..." xml:space="preserve">
<source>Could not find user to block...</source>
<target>Could not find user to block...</target>
<note>Alert message to indicate that the blocked user could not be found.</note>
</trans-unit>
<trans-unit id="Create" xml:space="preserve">
<source>Create</source>
<target>Create</target>
@@ -339,31 +275,25 @@ Number of profiles a user is following.</note>
<target>Create Account</target>
<note>Button to create an account.</note>
</trans-unit>
<trans-unit id="Create new mutelist" xml:space="preserve">
<source>Create new mutelist</source>
<target>Create new mutelist</target>
<note>Title of alert prompting the user to create a new mutelist.</note>
</trans-unit>
<trans-unit id="Creator(s) of Bitcoin. Absolute legend." xml:space="preserve">
<source>Creator(s) of Bitcoin. Absolute legend.</source>
<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" xml:space="preserve">
<source>DM</source>
<target>DM</target>
<note>Navigation title for DM view, which is the English abbreviation for Direct Message.</note>
</trans-unit>
<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>
<note>DM selector for seeing either DMs or message requests, which are messages that have not been responded to yet.</note>
</trans-unit>
<trans-unit id="DMs" xml:space="preserve">
<source>DMs</source>
<target>DMs</target>
<note>Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message.</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Damus" xml:space="preserve">
<source>Damus</source>
@@ -378,8 +308,7 @@ Number of profiles a user is following.</note>
<trans-unit id="Delete" xml:space="preserve">
<source>Delete</source>
<target>Delete</target>
<note>Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist.</note>
<note>Button to delete a relay server that the user connects to.</note>
</trans-unit>
<trans-unit id="Dismiss" xml:space="preserve">
<source>Dismiss</source>
@@ -396,11 +325,6 @@ Number of profiles a user is following.</note>
<target>Done</target>
<note>Button to dismiss wallet selection view for paying Lightning invoice.</note>
</trans-unit>
<trans-unit id="EULA" xml:space="preserve">
<source>EULA</source>
<target>EULA</target>
<note>Label indicating that the below text is the EULA, an acronym for End User License Agreement.</note>
</trans-unit>
<trans-unit id="Earn Money" xml:space="preserve">
<source>Earn Money</source>
<target>Earn Money</target>
@@ -411,16 +335,16 @@ Number of profiles a user is following.</note>
<target>Edit</target>
<note>Button to edit user's profile.</note>
</trans-unit>
<trans-unit id="Edit participants" xml:space="preserve">
<source>Edit participants</source>
<target>Edit participants</target>
<note>Text indicating that the view is used for editing which participants are replied to in a note.</note>
</trans-unit>
<trans-unit id="Encrypted" xml:space="preserve">
<source>Encrypted</source>
<target>Encrypted</target>
<note>Heading indicating that this application keeps private messaging end-to-end encrypted.</note>
</trans-unit>
<trans-unit id="Encrypted DMs" xml:space="preserve">
<source>Encrypted DMs</source>
<target>Encrypted DMs</target>
<note>Navigation title for view of encrypted DMs, where DM is an English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Enter your account key to login:" xml:space="preserve">
<source>Enter your account key to login:</source>
<target>Enter your account key to login:</target>
@@ -477,36 +401,16 @@ 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 LibreTranslate server API key.</note>
</trans-unit>
<trans-unit id="Home" xml:space="preserve">
<source>Home</source>
<target>Home</target>
<note>Navigation bar title for Home view where posts and replies appear from those who the user is following.</note>
</trans-unit>
<trans-unit id="Illegal content" xml:space="preserve">
<source>Illegal content</source>
<target>Illegal content</target>
<note>Button for user to report that the account or content has illegal content.</note>
</trans-unit>
<trans-unit id="Invalid key" xml:space="preserve">
<source>Invalid key</source>
<target>Invalid key</target>
<note>Error message indicating that an invalid account key was entered for login.</note>
</trans-unit>
<trans-unit id="It's spam" xml:space="preserve">
<source>It's spam</source>
<target>It's spam</target>
<note>Button for user to report that the account or content has spam.</note>
</trans-unit>
<trans-unit id="LNLink" xml:space="preserve">
<source>LNLink</source>
<target>LNLink</target>
@@ -522,11 +426,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 Translations" xml:space="preserve">
<source>LibreTranslate Translations</source>
<target>LibreTranslate Translations</target>
<note>Section title for selecting the server that hosts the LibreTranslate machine translation API.</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>
@@ -570,16 +469,6 @@ 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 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 server.</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>
@@ -590,11 +479,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Notifications</target>
<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="Pay" xml:space="preserve">
<source>Pay</source>
<target>Pay</target>
@@ -615,11 +499,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<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>
@@ -680,11 +559,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Recommended Relays</target>
<note>Section title for recommend relay servers that could be added as part of configuration</note>
</trans-unit>
<trans-unit id="Reject" xml:space="preserve">
<source>Reject</source>
<target>Reject</target>
<note>Button to reject the end user license agreement, which disallows the user from being let into the app.</note>
</trans-unit>
<trans-unit id="Relay" xml:space="preserve">
<source>Relay</source>
<target>Relay</target>
@@ -695,16 +569,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Relays</target>
<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>
<target>Relays have been notified and clients will be able to use this information to filter content. Thank you!</target>
<note>Description of what was done as a result of sending a report to relay servers.</note>
</trans-unit>
<trans-unit id="Remove all" xml:space="preserve">
<source>Remove all</source>
<target>Remove all</target>
<note>Button label to remove all participants from a note reply.</note>
</trans-unit>
<trans-unit id="Reply to self" xml:space="preserve">
<source>Reply to self</source>
<target>Reply to self</target>
@@ -720,22 +584,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Replying to:</target>
<note>Indicating that the user is replying to the following listed people.</note>
</trans-unit>
<trans-unit id="Report" xml:space="preserve">
<source>Report</source>
<target>Report</target>
<note>Button to report a profile.
Context menu option for reporting content.</note>
</trans-unit>
<trans-unit id="Report ID:" xml:space="preserve">
<source>Report ID:</source>
<target>Report ID:</target>
<note>Label indicating that the text underneath is the identifier of the report that was sent to relay servers.</note>
</trans-unit>
<trans-unit id="Report sent!" xml:space="preserve">
<source>Report sent!</source>
<target>Report sent!</target>
<note>Message indicating that a report was successfully sent to relay servers.</note>
</trans-unit>
<trans-unit id="Repost" xml:space="preserve">
<source>Repost</source>
<target>Repost</target>
@@ -747,15 +595,10 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Reposted</target>
<note>Text indicating that the post was reposted (i.e. re-shared).</note>
</trans-unit>
<trans-unit id="Reposts" xml:space="preserve">
<source>Reposts</source>
<target>Reposts</target>
<note>Navigation bar title for Reposts view.</note>
</trans-unit>
<trans-unit id="Requests" xml:space="preserve">
<source>Requests</source>
<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>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reset" xml:space="preserve">
<source>Reset</source>
@@ -817,11 +660,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="Settings" xml:space="preserve">
<source>Settings</source>
<target>Settings</target>
@@ -831,19 +669,12 @@ 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 an image.
Button to share the link to a profile.</note>
<note>Button to share an image.</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 hide 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>
@@ -860,16 +691,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Strike</target>
<note>Dropdown option label for Lightning wallet, Strike.</note>
</trans-unit>
<trans-unit id="Thanks!" xml:space="preserve">
<source>Thanks!</source>
<target>Thanks!</target>
<note>Button to close out of alert that informs that the action to block a user was successful.</note>
</trans-unit>
<trans-unit id="They are impersonating someone" xml:space="preserve">
<source>They are impersonating someone</source>
<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 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>
@@ -896,26 +717,11 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Navigation bar title for note thread.
Navigation bar title for threaded event detail view.</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 (languageName!)" xml:space="preserve">
<source>Translated from (languageName!)</source>
<target>Translated from (languageName!)</target>
<note>Button to indicate that the note has been translated from a different language.</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>
@@ -936,16 +742,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Unfollows</target>
<note>Text to indicate that the button next to it is in a state that will unfollow a profile when tapped.</note>
</trans-unit>
<trans-unit id="User blocked" xml:space="preserve">
<source>User blocked</source>
<target>User blocked</target>
<note>Alert message to indicate</note>
</trans-unit>
<trans-unit id="User has been blocked" xml:space="preserve">
<source>User has been blocked</source>
<target>User has been blocked</target>
<note>Alert message that informs a user was blocked.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>Username</target>
@@ -957,16 +753,16 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<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>
@@ -982,26 +778,11 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<target>Welcome, %@!</target>
<note>Text to welcome user.</note>
</trans-unit>
<trans-unit id="What do you want to report?" xml:space="preserve">
<source>What do you want to report?</source>
<target>What do you want to report?</target>
<note>Header text to prompt user what issue they want to report.</note>
</trans-unit>
<trans-unit id="Yes, Overwrite" xml:space="preserve">
<source>Yes, Overwrite</source>
<target>Yes, Overwrite</target>
<note>Text of button that confirms to overwrite the existing mutelist.</note>
</trans-unit>
<trans-unit id="Your Name" xml:space="preserve">
<source>Your Name</source>
<target>Your Name</target>
<note>Label for Your Name section of user profile form.</note>
</trans-unit>
<trans-unit id="Your report will be sent to the relays you are connected to" xml:space="preserve">
<source>Your report will be sent to the relays you are connected to</source>
<target>Your report will be sent to the relays you are connected to</target>
<note>Footer text to inform user what will happen when the report is submitted.</note>
</trans-unit>
<trans-unit id="Zebedee" xml:space="preserve">
<source>Zebedee</source>
<target>Zebedee</target>
@@ -17,8 +17,8 @@
<note>Bundle name</note>
</trans-unit>
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
<source>Granting Damus access to your photos allows you to save images.</source>
<target>Si le concedes acceso a Damus a tus fotos, podrás guardar imágenes.</target>
<source>&quot;Granting Damus access to your photo library allows you to save photos.</source>
<target>Si le concedes acceso a Damus a tu fototeca, podrás guardar fotos.</target>
<note>Privacy - Photo Library Additions Usage Description</note>
</trans-unit>
@@ -45,15 +45,9 @@ Abbreviated version of a nostr public key.</note>
<source>%@ %@</source>
<target>%@ %@</target>
<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'.
<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 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>
<target>Se bloqueó a %@</target>
<note>Alert message that informs a user was blocked.</note>
</trans-unit>
<trans-unit id="%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." xml:space="preserve">
<source>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</source>
<target>%@. No se requiere un número de teléfono, correo electrónico ni nombre para crear una cuenta. Comienza de inmediato sin fricciones.</target>
@@ -76,7 +70,7 @@ Sentence composed of 2 variables to describe how many profiles a user is followi
<source>%lld</source>
<target>%lld</target>
<note>Number of tip payments on a post.
<note>Number of reposts.
Number of profiles a user is following.</note>
</trans-unit>
<trans-unit id="%lld/%lld" xml:space="preserve">
@@ -87,13 +81,13 @@ Number of profiles a user is following.</note>
</trans-unit>
<trans-unit id="'%@' at '%@' will be used for verification" xml:space="preserve">
<source>'%@' at '%@' will be used for verification</source>
<target>'%@' en '%@' se usará con fines de verificación</target>
<target>'%@' en '%@' se usarán con fines de verificación</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>'%@' es un identificador NIP-05 no válido. Debería de tener la apariencia de un correo electrónico.</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>'%@' es un identificador nip05 no válido. Debería de tener la apariencia de un correo electrónico.</target>
<note>Description of why the nip05 identifier is invalid.</note>
</trans-unit>
@@ -121,12 +115,6 @@ Number of profiles a user is following.</note>
<note>Prefix character to username.</note>
</trans-unit>
<trans-unit id="API Key (optional)" xml:space="preserve">
<source>API Key (optional)</source>
<target>Clave de API (opcional)</target>
<note>Example URL to LibreTranslate server</note>
</trans-unit>
<trans-unit id="About" xml:space="preserve">
<source>About</source>
<target>Información</target>
@@ -145,24 +133,12 @@ Number of profiles a user is following.</note>
<note>Placeholder text for About Me description.</note>
</trans-unit>
<trans-unit id="Accept" xml:space="preserve">
<source>Accept</source>
<target>Aceptar</target>
<note>Button to accept the end user license agreement before being allowed into the app.</note>
</trans-unit>
<trans-unit id="Account ID" xml:space="preserve">
<source>Account ID</source>
<target>Identificador de cuenta</target>
<note>Label to indicate the public ID of the account.</note>
</trans-unit>
<trans-unit id="Actions" xml:space="preserve">
<source>Actions</source>
<target>Acciones</target>
<note>Title for confirmation dialog to either share, report, or block a profile.</note>
</trans-unit>
<trans-unit id="Add" xml:space="preserve">
<source>Add</source>
<target>Agregar</target>
@@ -176,12 +152,6 @@ Number of profiles a user is following.</note>
<note>Label for section for adding a relay server.</note>
</trans-unit>
<trans-unit id="Add all" xml:space="preserve">
<source>Add all</source>
<target>Agregar todo</target>
<note>Button label to re-add all original participants as profiles to reply to in a note</note>
</trans-unit>
<trans-unit id="Any" xml:space="preserve">
<source>Any</source>
<target>Cualquiera</target>
@@ -224,38 +194,6 @@ Number of profiles a user is following.</note>
<note>Dropdown option label for Lightning wallet, Blixt Wallet</note>
</trans-unit>
<trans-unit id="Block" xml:space="preserve">
<source>Block</source>
<target>Bloquear</target>
<note>Alert button to block a user.
Button to block a profile.
Context menu option for blocking users.</note>
</trans-unit>
<trans-unit id="Block %@?" xml:space="preserve">
<source>Block %@?</source>
<target>¿Bloquear a %@?</target>
<note>Alert message prompt to ask if a user should be blocked.</note>
</trans-unit>
<trans-unit id="Block User" xml:space="preserve">
<source>Block User</source>
<target>Bloquear usuario</target>
<note>Title of alert for blocking a user.</note>
</trans-unit>
<trans-unit id="Blocked" xml:space="preserve">
<source>Blocked</source>
<target>Bloqueado</target>
<note>Sidebar menu label for Profile view.</note>
</trans-unit>
<trans-unit id="Blocked Users" xml:space="preserve">
<source>Blocked Users</source>
<target>Usuarios bloqueados</target>
<note>Navigation title of view to see list of blocked users.</note>
</trans-unit>
<trans-unit id="Blue Wallet" xml:space="preserve">
<source>Blue Wallet</source>
<target>Blue Wallet</target>
@@ -278,9 +216,7 @@ Number of profiles a user is following.</note>
<source>Cancel</source>
<target>Cancelar</target>
<note>Alert button to cancel out of alert for blocking a user.
Button to cancel out of alert that creates a new mutelist.
Button to cancel out of posting a note.
<note>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 out of logging out the user.</note>
@@ -358,21 +294,15 @@ Number of profiles a user is following.</note>
<note>Context menu option for copying the JSON text from the note.</note>
</trans-unit>
<trans-unit id="Copy Report ID" xml:space="preserve">
<source>Copy Report ID</source>
<target>Copiar identificador de reporte</target>
<note>Button to copy report ID.</note>
</trans-unit>
<trans-unit id="Copy Text" xml:space="preserve">
<source>Copy Text</source>
<target>Copiar texto</target>
<note>Context menu option for copying the text from an note.</note>
</trans-unit>
<trans-unit id="Copy User Pubkey" xml:space="preserve">
<source>Copy User Pubkey</source>
<target>Copiar clave pública de usuario</target>
<trans-unit id="Copy User ID" xml:space="preserve">
<source>Copy User ID</source>
<target>Copiar identificador de usuario</target>
<note>Context menu option for copying the ID of the user who created the note.</note>
</trans-unit>
@@ -382,12 +312,6 @@ Number of profiles a user is following.</note>
<note>Title of section for copying a Lightning invoice identifier.</note>
</trans-unit>
<trans-unit id="Could not find user to block..." xml:space="preserve">
<source>Could not find user to block...</source>
<target>No se pudo encontrar al usuario para bloquearlo...</target>
<note>Alert message to indicate that the blocked user could not be found.</note>
</trans-unit>
<trans-unit id="Create" xml:space="preserve">
<source>Create</source>
<target>Crear</target>
@@ -400,36 +324,17 @@ Number of profiles a user is following.</note>
<note>Button to create an account.</note>
</trans-unit>
<trans-unit id="Create new mutelist" xml:space="preserve">
<source>Create new mutelist</source>
<target>Crear nueva lista de silenciados</target>
<note>Title of alert prompting the user to create a new mutelist.</note>
</trans-unit>
<trans-unit id="Creator(s) of Bitcoin. Absolute legend." xml:space="preserve">
<source>Creator(s) of Bitcoin. Absolute legend.</source>
<target>Creador(es) de Bitcoin. Toda una leyenda.</target>
<note>Example description about Bitcoin creator(s), Satoshi Nakamoto.</note>
</trans-unit>
<trans-unit id="Custom" xml:space="preserve">
<source>Custom</source>
<target>Personalizado</target>
<trans-unit id="DM" xml:space="preserve">
<source>DM</source>
<target>MD</target>
<note>Dropdown option for selecting a custom translation server.</note>
</trans-unit>
<trans-unit id="DM Type" xml:space="preserve">
<source>DM Type</source>
<target>Tipo de mensaje directo</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>
<target>Mensajes directos</target>
<note>Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message.</note>
<note>Navigation title for DM view, which is the English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Damus" xml:space="preserve">
<source>Damus</source>
@@ -447,8 +352,7 @@ Number of profiles a user is following.</note>
<source>Delete</source>
<target>Borrar</target>
<note>Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist.</note>
<note>Button to delete a relay server that the user connects to.</note>
</trans-unit>
<trans-unit id="Dismiss" xml:space="preserve">
<source>Dismiss</source>
@@ -468,12 +372,6 @@ Number of profiles a user is following.</note>
<note>Button to dismiss wallet selection view for paying Lightning invoice.</note>
</trans-unit>
<trans-unit id="EULA" xml:space="preserve">
<source>EULA</source>
<target>CLUF</target>
<note>Label indicating that the below text is the EULA, an acronym for End User License Agreement.</note>
</trans-unit>
<trans-unit id="Earn Money" xml:space="preserve">
<source>Earn Money</source>
<target>Gana dinero</target>
@@ -486,18 +384,18 @@ Number of profiles a user is following.</note>
<note>Button to edit user's profile.</note>
</trans-unit>
<trans-unit id="Edit participants" xml:space="preserve">
<source>Edit participants</source>
<target>Editar participantes</target>
<note>Text indicating that the view is used for editing which participants are replied to in a note.</note>
</trans-unit>
<trans-unit id="Encrypted" xml:space="preserve">
<source>Encrypted</source>
<target>Cifrada</target>
<note>Heading indicating that this application keeps private messaging end-to-end encrypted.</note>
</trans-unit>
<trans-unit id="Encrypted DMs" xml:space="preserve">
<source>Encrypted DMs</source>
<target>MD cifrados</target>
<note>Navigation title for view of encrypted DMs, where DM is an English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Enter your account key to login:" xml:space="preserve">
<source>Enter your account key to login:</source>
<target>Ingresa la clave de tu cuenta para iniciar sesión:</target>
@@ -565,42 +463,18 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Navigation link to go to profile.</note>
</trans-unit>
<trans-unit id="Hide" xml:space="preserve">
<source>Hide</source>
<target>Ocultar</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>Ocultar clave de API</target>
<note>Button to hide the LibreTranslate server API key.</note>
</trans-unit>
<trans-unit id="Home" xml:space="preserve">
<source>Home</source>
<target>Inicio</target>
<note>Navigation bar title for Home view where posts and replies appear from those who the user is following.</note>
</trans-unit>
<trans-unit id="Illegal content" xml:space="preserve">
<source>Illegal content</source>
<target>Contenido ilegal</target>
<note>Button for user to report that the account or content has illegal content.</note>
</trans-unit>
<trans-unit id="Invalid key" xml:space="preserve">
<source>Invalid key</source>
<target>Clave inválida</target>
<note>Error message indicating that an invalid account key was entered for login.</note>
</trans-unit>
<trans-unit id="It's spam" xml:space="preserve">
<source>It's spam</source>
<target>Es spam</target>
<note>Button for user to report that the account or content has spam.</note>
</trans-unit>
<trans-unit id="LNLink" xml:space="preserve">
<source>LNLink</source>
<target>LNLink</target>
@@ -619,12 +493,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Button to complete account creation and start using the app.</note>
</trans-unit>
<trans-unit id="LibreTranslate Translations" xml:space="preserve">
<source>LibreTranslate Translations</source>
<target>Traducciones de LibreTranslate</target>
<note>Section title for selecting the server that hosts the LibreTranslate machine translation API.</note>
</trans-unit>
<trans-unit id="Lightning Address or LNURL" xml:space="preserve">
<source>Lightning Address or LNURL</source>
<target>Dirección de Lightning o LNURL</target>
@@ -676,18 +544,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Label for NIP-05 Verification section of user profile form.</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 se encontró una lista de bloqueo. ¿Crear una nueva? Esto sobrescribirá las listas de bloqueo anteriores.</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>Ninguno</target>
<note>Dropdown option for selecting no translation server.</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>Nada para ver aquí. ¡Vuelve a consultar luego!</target>
@@ -700,12 +556,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<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>Desnudos o contenido explícito</target>
<note>Button for user to report that the account or content has nudity or explicit content.</note>
</trans-unit>
<trans-unit id="Pay" xml:space="preserve">
<source>Pay</source>
<target>Pagar</target>
@@ -730,12 +580,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<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>Publicación de un usuario que bloqueaste</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>Publicaciones</target>
@@ -758,6 +602,12 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<source>Private Key</source>
<target>Clave privada</target>
<note>Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account.</note>
</trans-unit>
<trans-unit id="PrivateKey" xml:space="preserve">
<source>PrivateKey</source>
<target>ClavePrivada</target>
<note>Title of the secure field that holds the user's private key.</note>
</trans-unit>
<trans-unit id="Profile" xml:space="preserve">
@@ -808,12 +658,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Section title for recommend relay servers that could be added as part of configuration</note>
</trans-unit>
<trans-unit id="Reject" xml:space="preserve">
<source>Reject</source>
<target>Rechazar</target>
<note>Button to reject the end user license agreement, which disallows the user from being let into the app.</note>
</trans-unit>
<trans-unit id="Relay" xml:space="preserve">
<source>Relay</source>
<target>Relé</target>
@@ -826,18 +670,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<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>
<target>Se notificó a los relés, por lo que los clientes podrán usar esta información para filtrar contenido. ¡Gracias!</target>
<note>Description of what was done as a result of sending a report to relay servers.</note>
</trans-unit>
<trans-unit id="Remove all" xml:space="preserve">
<source>Remove all</source>
<target>Eliminar todo</target>
<note>Button label to remove all participants from a note reply.</note>
</trans-unit>
<trans-unit id="Reply to self" xml:space="preserve">
<source>Reply to self</source>
<target>Respuesta a sí mismo</target>
@@ -856,25 +688,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Indicating that the user is replying to the following listed people.</note>
</trans-unit>
<trans-unit id="Report" xml:space="preserve">
<source>Report</source>
<target>Reportar</target>
<note>Button to report a profile.
Context menu option for reporting content.</note>
</trans-unit>
<trans-unit id="Report ID:" xml:space="preserve">
<source>Report ID:</source>
<target>Identificador de reporte:</target>
<note>Label indicating that the text underneath is the identifier of the report that was sent to relay servers.</note>
</trans-unit>
<trans-unit id="Report sent!" xml:space="preserve">
<source>Report sent!</source>
<target>¡Reporte enviado!</target>
<note>Message indicating that a report was successfully sent to relay servers.</note>
</trans-unit>
<trans-unit id="Repost" xml:space="preserve">
<source>Repost</source>
<target>Republicar</target>
@@ -888,18 +701,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Text indicating that the post was reposted (i.e. re-shared).</note>
</trans-unit>
<trans-unit id="Reposts" xml:space="preserve">
<source>Reposts</source>
<target>Republicaciones</target>
<note>Navigation bar title for Reposts view.</note>
</trans-unit>
<trans-unit id="Requests" xml:space="preserve">
<source>Requests</source>
<target>Solicitudes</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>Reiniciar</target>
@@ -944,7 +745,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="Search..." xml:space="preserve">
<source>Search...</source>
<target>Buscar...</target>
<target>Búsqueda...</target>
<note>Placeholder text to prompt entry of search query.</note>
</trans-unit>
@@ -972,12 +773,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<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>Servidor</target>
<note>Prompt selection of LibreTranslate server to perform machine translations on notes</note>
</trans-unit>
<trans-unit id="Settings" xml:space="preserve">
<source>Settings</source>
<target>Configuración</target>
@@ -989,21 +784,13 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<source>Share</source>
<target>Compartir</target>
<note>Button to share an image.
Button to share the link to a profile.</note>
<note>Button to share an image.</note>
</trans-unit>
<trans-unit id="Show" xml:space="preserve">
<source>Show</source>
<target>Mostrar</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>Mostrar clave de API</target>
<note>Button to hide 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>
@@ -1023,18 +810,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Dropdown option label for Lightning wallet, Strike.</note>
</trans-unit>
<trans-unit id="Thanks!" xml:space="preserve">
<source>Thanks!</source>
<target>¡Gracias!</target>
<note>Button to close out of alert that informs that the action to block a user was successful.</note>
</trans-unit>
<trans-unit id="They are impersonating someone" xml:space="preserve">
<source>They are impersonating someone</source>
<target>Está suplantando a alguien</target>
<note>Button for user to report that the account is impersonating someone.</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>Esta es una clave pública, por lo que no podrás hacer publicaciones ni interactuar de ningún modo. Se usa para ver cuentas desde su perspectiva.</target>
@@ -1055,7 +830,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" xml:space="preserve">
<source>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</source>
<target>Esta es tu clave de cuenta secreta, que necesitas para acceder a tu cuenta. ¡No la compartas con nadie! Guárdala en un administrador de contraseñas y protégela.</target>
<target>Esta es tu clave de cuenta secreta, que necesitas para acceder a tu cuenta. ¡No la compartas con nadie! Guárdala en un administrador de contraseñas y protégela!</target>
<note>Label to describe that a private key is the user's secret account key and what they should do with it.</note>
</trans-unit>
@@ -1066,30 +841,12 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Navigation bar title for note thread.
Navigation bar title for threaded event detail view.</note>
</trans-unit>
<trans-unit id="Translate Note" xml:space="preserve">
<source>Translate Note</source>
<target>Traducir nota</target>
<note>Button to translate note from different language.</note>
</trans-unit>
<trans-unit id="Translated from (languageName!)" xml:space="preserve">
<source>Translated from (languageName!)</source>
<target>Traducida del (languageName!)</target>
<note>Button to indicate that the note has been translated from a different language.</note>
</trans-unit>
<trans-unit id="Type your post here..." xml:space="preserve">
<source>Type your post here...</source>
<target>Escribe tu publicación aquí...</target>
<target>Ingresa tu publicación aquí...</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>Dejar de seguir</target>
@@ -1114,18 +871,6 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Text to indicate that the button next to it is in a state that will unfollow a profile when tapped.</note>
</trans-unit>
<trans-unit id="User blocked" xml:space="preserve">
<source>User blocked</source>
<target>Usuario bloqueado</target>
<note>Alert message to indicate</note>
</trans-unit>
<trans-unit id="User has been blocked" xml:space="preserve">
<source>User has been blocked</source>
<target>Se bloqueó al usuario</target>
<note>Alert message that informs a user was blocked.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>Nombre de usuario</target>
@@ -1139,18 +884,18 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<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>Selección de billetera</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>Sitio web</target>
@@ -1169,30 +914,12 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Text to welcome user.</note>
</trans-unit>
<trans-unit id="What do you want to report?" xml:space="preserve">
<source>What do you want to report?</source>
<target>¿Qué quieres reportar?</target>
<note>Header text to prompt user what issue they want to report.</note>
</trans-unit>
<trans-unit id="Yes, Overwrite" xml:space="preserve">
<source>Yes, Overwrite</source>
<target>Sí, sobrescribir</target>
<note>Text of button that confirms to overwrite the existing mutelist.</note>
</trans-unit>
<trans-unit id="Your Name" xml:space="preserve">
<source>Your Name</source>
<target>Tu nombre</target>
<note>Label for Your Name section of user profile form.</note>
</trans-unit>
<trans-unit id="Your report will be sent to the relays you are connected to" xml:space="preserve">
<source>Your report will be sent to the relays you are connected to</source>
<target>El reporte se enviará a los relés con los que tengas conexión</target>
<note>Footer text to inform user what will happen when the report is submitted.</note>
</trans-unit>
<trans-unit id="Zebedee" xml:space="preserve">
<source>Zebedee</source>
<target>Zebedee</target>
@@ -1207,13 +934,13 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</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>
<target>evento_colapsado_ver_otras_notas</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>
<target>recuento_de_seguidores</target>
<note>Part of a larger sentence to describe how many people are following a user. (Key in .stringsdict)</note>
</trans-unit>
@@ -1261,13 +988,13 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="reactions_count" translate="no" xml:space="preserve">
<source>reactions_count</source>
<target>reactions_count</target>
<target>recuento_de_reacciones</target>
<note>Part of a larger sentence to describe how many reactions there are on a post. (Key in .stringsdict)</note>
</trans-unit>
<trans-unit id="relays_count" translate="no" xml:space="preserve">
<source>relays_count</source>
<target>relays_count</target>
<target>recuento_de_relés</target>
<note>Part of a larger sentence to describe how many relay servers a user is connected. (Key in .stringsdict)</note>
</trans-unit>
@@ -1315,7 +1042,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="wss://some.relay.com" xml:space="preserve">
<source>wss://some.relay.com</source>
<target>wss://algun.rele.com</target>
<target>wss://algún.relé.com</target>
<note>Placeholder example for relay server address.</note>
</trans-unit>
+2 -23
View File
@@ -22,10 +22,6 @@ static inline int is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
static inline int is_boundary(char c) {
return !isalnum(c);
}
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
{
c->start = content;
@@ -33,33 +29,16 @@ static void make_cursor(struct cursor *c, const u8 *content, size_t len)
c->p = content;
}
static int consume_until_boundary(struct cursor *cur) {
char c;
while (cur->p < cur->end) {
c = *cur->p;
if (is_boundary(c))
return 1;
cur->p++;
}
return 1;
}
static int consume_until_whitespace(struct cursor *cur, int or_end) {
char c;
bool consumedAtLeastOne = false;
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c) && consumedAtLeastOne)
if (is_whitespace(c))
return 1;
cur->p++;
consumedAtLeastOne = true;
}
return or_end;
@@ -166,7 +145,7 @@ static int parse_hashtag(struct cursor *cur, struct block *block) {
return 0;
}
consume_until_boundary(cur);
consume_until_whitespace(cur, 1);
block->type = BLOCK_HASHTAG;
block->block.str.start = (const char*)(start + 1);
+29 -258
View File
@@ -12,13 +12,9 @@
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
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 */; };
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 */; };
@@ -117,8 +113,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 */; };
@@ -131,15 +125,6 @@
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 */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.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 */; };
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */; };
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */; };
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; };
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
@@ -161,30 +146,14 @@
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */; };
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */; };
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */; };
4CF0ABD42980996B00D66079 /* Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD32980996B00D66079 /* Report.swift */; };
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD529817F5B00D66079 /* ReportView.swift */; };
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABD72981980C00D66079 /* Lists.swift */; };
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABDB2981A19E00D66079 /* ListTests.swift */; };
4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABDD2981A69500D66079 /* MutelistModel.swift */; };
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE02981A83900D66079 /* MutelistView.swift */; };
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE22981BC7D00D66079 /* UserView.swift */; };
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE42981EE0C00D66079 /* EULAView.swift */; };
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE6298444FC00D66079 /* MutedEventView.swift */; };
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 */; };
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 */; };
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 */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.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 */; };
@@ -192,7 +161,6 @@
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.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 */
/* Begin PBXContainerItemProxy section */
@@ -217,45 +185,11 @@
3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTimelineView.swift; sourceTree = "<group>"; };
3169CAEC294FCCFC00EE4006 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = damus/Util/Constants.swift; sourceTree = SOURCE_ROOT; };
31D2E846295218AF006D67F8 /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = "<group>"; };
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>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
3AF0BC09298C1F66008E2AB8 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AF0BC0A298C1F66008E2AB8 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/Localizable.strings; sourceTree = "<group>"; };
3AF0BC0B298C1F66008E2AB8 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = zh; path = zh.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>"; };
@@ -386,8 +320,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>"; };
@@ -400,15 +332,6 @@
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>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.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>"; };
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedEventView.swift; sourceTree = "<group>"; };
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescription.swift; sourceTree = "<group>"; };
4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; };
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
@@ -433,29 +356,11 @@
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileName.swift; sourceTree = "<group>"; };
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowView.swift; sourceTree = "<group>"; };
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventActionBar.swift; sourceTree = "<group>"; };
4CF0ABD32980996B00D66079 /* Report.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Report.swift; sourceTree = "<group>"; };
4CF0ABD529817F5B00D66079 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
4CF0ABD72981980C00D66079 /* Lists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lists.swift; sourceTree = "<group>"; };
4CF0ABDB2981A19E00D66079 /* ListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTests.swift; sourceTree = "<group>"; };
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistModel.swift; sourceTree = "<group>"; };
4CF0ABE02981A83900D66079 /* MutelistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistView.swift; sourceTree = "<group>"; };
4CF0ABE22981BC7D00D66079 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = "<group>"; };
4CF0ABE42981EE0C00D66079 /* EULAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EULAView.swift; sourceTree = "<group>"; };
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedEventView.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.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>"; };
@@ -463,7 +368,6 @@
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@@ -471,6 +375,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 */,
@@ -503,14 +409,6 @@
path = "Empty Views";
sourceTree = "<group>";
};
3AA24800297E3DAE0090C62D /* Reposts */ = {
isa = PBXGroup;
children = (
3AA24801297E3DC20090C62D /* RepostView.swift */,
);
path = Reposts;
sourceTree = "<group>";
};
4C06670728FDE62900038D2A /* damus-c */ = {
isa = PBXGroup;
children = (
@@ -568,7 +466,6 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */,
@@ -600,9 +497,6 @@
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -610,11 +504,6 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CAAD8AE29888A9B00060CEA /* Relays */,
4CF0ABF42985CD4200D66079 /* Posting */,
4CF0ABDF2981A83000D66079 /* Muting */,
4CC7AAEE297F11B300430951 /* Events */,
3AA24800297E3DAE0090C62D /* Reposts */,
4CB88394296F7F8100DC99E7 /* Reactions */,
4CB88387296AF97C00DC99E7 /* ActionBar */,
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
@@ -645,9 +534,10 @@
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 */,
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
@@ -662,10 +552,6 @@
9609F057296E220800069BF3 /* BannerImageView.swift */,
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */,
6439E013296790CF0020672B /* ProfileZoomView.swift */,
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */,
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -693,7 +579,6 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4C3A1D322960DB0500558C0F /* Markdown.swift */,
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
@@ -708,24 +593,10 @@
4C3A1D3629637E0500558C0F /* PreviewCache.swift */,
64FBD06E296255C400D9D3B2 /* Theme.swift */,
4CB8838529656C8B00DC99E7 /* NIP05.swift */,
4CF0ABD72981980C00D66079 /* Lists.swift */,
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */,
);
path = Util;
sourceTree = "<group>";
};
4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup;
children = (
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
4C06670028FC7C5900038D2A /* RelayView.swift */,
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
);
path = Relays;
sourceTree = "<group>";
};
4CB88387296AF97C00DC99E7 /* ActionBar */ = {
isa = PBXGroup;
children = (
@@ -743,21 +614,6 @@
path = Reactions;
sourceTree = "<group>";
};
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */,
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */,
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */,
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */,
4CC7AAF9297F64AC00430951 /* EventMenu.swift */,
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
);
path = Events;
sourceTree = "<group>";
};
4CE4F9DF285287A000C00DD9 /* Components */ = {
isa = PBXGroup;
children = (
@@ -769,11 +625,6 @@
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */,
4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */,
4CB8838C296F710400DC99E7 /* Reposted.swift */,
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */,
4CC7AAEC297F0B9E00430951 /* Highlight.swift */,
5C513FB9297F72980072348F /* CustomPicker.swift */,
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -841,7 +692,6 @@
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */,
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
4CB88399297322D200DC99E7 /* DMTests.swift */,
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -863,32 +713,6 @@
name = Frameworks;
sourceTree = "<group>";
};
4CF0ABDF2981A83000D66079 /* Muting */ = {
isa = PBXGroup;
children = (
4CF0ABE02981A83900D66079 /* MutelistView.swift */,
);
path = Muting;
sourceTree = "<group>";
};
4CF0ABEA29844B2F00D66079 /* AnyCodable */ = {
isa = PBXGroup;
children = (
4CF0ABE829844AF100D66079 /* AnyCodable.swift */,
4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */,
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */,
);
path = AnyCodable;
sourceTree = "<group>";
};
4CF0ABF42985CD4200D66079 /* Posting */ = {
isa = PBXGroup;
children = (
4CF0ABF52985CD5500D66079 /* UserSearch.swift */,
);
path = Posting;
sourceTree = "<group>";
};
F7F0BA23297892AE009531F3 /* Modifiers */ = {
isa = PBXGroup;
children = (
@@ -918,6 +742,8 @@
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
6C7DE41E2955169800E66263 /* Vault */,
7C45AE6C297352F90031D7BC /* SVGKit */,
7C45AE6E297352F90031D7BC /* SVGKitSwift */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -991,16 +817,6 @@
Base,
"es-419",
"en-US",
"de-AT",
"tr-TR",
"fr-FR",
"lv-LV",
"it-IT",
de,
"pt-PT",
"pl-PL",
zh,
ar,
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1008,6 +824,7 @@
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */,
7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -1061,15 +878,12 @@
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 */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
@@ -1077,10 +891,8 @@
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
@@ -1088,10 +900,7 @@
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
4C477C9E282C3A4800033AA3 /* TipCounter.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 */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
@@ -1111,27 +920,20 @@
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
4C3EA67F28FFC01D00C48A62 /* InvoiceView.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 */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.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 */,
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
@@ -1145,7 +947,6 @@
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 */,
@@ -1153,30 +954,21 @@
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c 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 */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
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 */,
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
@@ -1192,31 +984,23 @@
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.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 */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
@@ -1235,7 +1019,6 @@
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1269,16 +1052,6 @@
children = (
3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */,
3A5EA111297CCF6C00569477 /* de-AT */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A929C22297F2CF80090925E /* it-IT */,
3AB5B86C2986D8A3006599D2 /* de */,
3AF6336A29884C6B0005672A /* pt-PT */,
3A93342B29884CA600D6A8F3 /* pl-PL */,
3AF0BC0B298C1F66008E2AB8 /* zh */,
3AC524F0298C000B00693EBF /* ar */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1287,16 +1060,6 @@
isa = PBXVariantGroup;
children = (
3ACB685B297633BC00C46468 /* es-419 */,
3A5EA10F297CCF6C00569477 /* de-AT */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A929C20297F2CF80090925E /* it-IT */,
3AB5B86A2986D8A3006599D2 /* de */,
3AF6336829884C6B0005672A /* pt-PT */,
3A93342929884CA600D6A8F3 /* pl-PL */,
3AF0BC09298C1F66008E2AB8 /* zh */,
3AC524EE298C000B00693EBF /* ar */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1305,16 +1068,6 @@
isa = PBXVariantGroup;
children = (
3ACB685E297633BC00C46468 /* es-419 */,
3A5EA110297CCF6C00569477 /* de-AT */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A929C21297F2CF80090925E /* it-IT */,
3AB5B86B2986D8A3006599D2 /* de */,
3AF6336929884C6B0005672A /* pt-PT */,
3A93342A29884CA600D6A8F3 /* pl-PL */,
3AF0BC0A298C1F66008E2AB8 /* zh */,
3AC524EF298C000B00693EBF /* ar */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1450,7 +1203,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1491,7 +1244,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1673,6 +1426,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 */
@@ -1696,6 +1457,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 */;
@@ -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",
@@ -1,98 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
BuildableName = "damus.app"
BlueprintName = "damus"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEF227F7A08200C66700"
BuildableName = "damusTests.xctest"
BlueprintName = "damusTests"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"
BuildableName = "damusUITests.xctest"
BlueprintName = "damusUITests"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
BuildableName = "damus.app"
BlueprintName = "damus"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
BuildableName = "damus.app"
BlueprintName = "damus"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
-60
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")
}
}
-39
View File
@@ -1,39 +0,0 @@
//
// Highlight.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import Foundation
import SwiftUI
enum Highlight {
case none
case main
case reply
case custom(Color, Float)
var is_main: Bool {
if case .main = self {
return true
}
return false
}
var is_none: Bool {
if case .none = self {
return true
}
return false
}
var is_replied_to: Bool {
switch self {
case .reply: return true
default: return false
}
}
}
+64 -17
View File
@@ -115,6 +115,56 @@ private struct ImageContainerView: View {
// TODO: Update ImageCarousel with serializer and processor
// .serialize(by: imageModel.serializer)
// .setProcessor(imageModel.processor)
}
}
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .clear
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}
@@ -127,14 +177,6 @@ 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 {
@@ -180,24 +222,22 @@ struct ImageView: View {
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
.padding(.top, safeAreaInsets?.top)
.padding(.bottom, safeAreaInsets?.bottom)
}
.ignoresSafeArea()
.tag(index)
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
.ignoresSafeArea()
.tag(index)
}
}
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.gesture(TapGesture(count: 2).onEnded {
// Prevents menu from hiding on double tap
.onChange(of: selectedIndex, perform: { _ in
showMenu = true
})
.gesture(TapGesture(count: 1).onEnded {
.onTapGesture {
showMenu.toggle()
})
}
.overlay(
VStack {
if showMenu {
@@ -210,7 +250,14 @@ struct ImageView: View {
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, safeAreaInsets?.bottom)
.padding(
.bottom,
UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets.bottom
)
)
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ struct InvoiceView: View {
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@EnvironmentObject var user_settings: UserSettingsStore
@ObservedObject var user_settings = UserSettingsStore()
var PayButton: some View {
Button {
-42
View File
@@ -1,42 +0,0 @@
//
// UserView.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import SwiftUI
struct UserView: View {
let damus_state: DamusState
let pubkey: String
var body: some View {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
let followers = FollowersModel(damus_state: damus_state, target: pubkey)
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(about)
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
}
.buttonStyle(PlainButtonStyle())
}
}
struct UserView_Previews: PreviewProvider {
static var previews: some View {
UserView(damus_state: test_damus_state(), pubkey: "pk")
}
}
-38
View File
@@ -1,38 +0,0 @@
//
// WebsiteLink.swift
// damus
//
// Created by William Casarin on 2023-01-22.
//
import SwiftUI
struct WebsiteLink: View {
let url: URL
@Environment(\.openURL) var openURL
var body: some View {
HStack {
Image(systemName: "link")
.foregroundColor(.gray)
.font(.footnote)
Button(action: {
openURL(url)
}, label: {
Text(link_text)
.font(.footnote)
})
}
}
var link_text: String {
url.host ?? url.absoluteString
}
}
struct WebsiteLink_Previews: PreviewProvider {
static var previews: some View {
WebsiteLink(url: URL(string: "https://jb55.com")!)
}
}
-152
View File
@@ -1,152 +0,0 @@
//
// ZoomableScrollView.swift
// damus
//
// Created by Oleg Abalonski on 1/25/23.
//
import SwiftUI
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = GesturedScrollView()
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .clear
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content, ignoreSafeArea: true))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let viewSize = hostingController.view.frame.size
guard let imageSize = scrollView.subviews[0].subviews.last?.frame.size else { return }
if scrollView.zoomScale > 1 {
let ratioW = viewSize.width / imageSize.width
let ratioH = viewSize.height / imageSize.height
let ratio = ratioW < ratioH ? ratioW:ratioH
let newWidth = imageSize.width * ratio
let newHeight = imageSize.height * ratio
let left = 0.5 * (newWidth * scrollView.zoomScale > viewSize.width ? (newWidth - viewSize.width) : (scrollView.frame.width - scrollView.contentSize.width))
let top = 0.5 * (newHeight * scrollView.zoomScale > viewSize.height ? (newHeight - viewSize.height) : (scrollView.frame.height - scrollView.contentSize.height))
scrollView.contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
} else {
scrollView.contentInset = .zero
}
}
}
}
fileprivate class GesturedScrollView: UIScrollView, UIGestureRecognizerDelegate {
let doubleTapGesture: UITapGestureRecognizer
override init(frame: CGRect) {
doubleTapGesture = UITapGestureRecognizer()
super.init(frame: frame)
doubleTapGesture.addTarget(self, action: #selector(handleDoubleTap))
doubleTapGesture.numberOfTapsRequired = 2
addGestureRecognizer(doubleTapGesture)
doubleTapGesture.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
if self.zoomScale == 1 {
let pointInView = gesture.location(in: self.subviews.first)
let newZoomScale = self.maximumZoomScale / 4.0
let scrollViewSize = self.bounds.size
let width = scrollViewSize.width / newZoomScale
let height = scrollViewSize.height / newZoomScale
let originX = pointInView.x - (width / 2.0)
let originY = pointInView.y - (height / 2.0)
let zoomRect = CGRect(x: originX, y: originY, width: width, height: height)
self.zoom(to: zoomRect, animated: true)
} else {
self.setZoomScale(self.minimumZoomScale, animated: true)
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer == doubleTapGesture
}
}
fileprivate extension UIHostingController {
convenience init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
+36 -158
View File
@@ -11,11 +11,11 @@ import Kingfisher
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://relay.snort.social",
"wss://nostr.orangepill.dev",
"wss://nos.lol",
"wss://relay.current.fyi",
"wss://nostr-relay.wlvs.space",
"wss://nostr.fmt.wiz.biz",
"wss://relay.nostr.bg",
"wss://nostr.oxtr.dev",
"wss://nostr.v0l.io",
"wss://brb.io",
]
@@ -26,12 +26,10 @@ struct TimestampedProfile {
enum Sheets: Identifiable {
case post
case report(ReportTarget)
case reply(NostrEvent)
var id: String {
switch self {
case .report: return "report"
case .post: return "post"
case .reply(let ev): return "reply-" + ev.id
}
@@ -73,7 +71,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
@@ -82,10 +79,6 @@ struct ContentView: View {
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
@State var blocking: String? = nil
@State var confirm_block: Bool = false
@State var user_blocked_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
@@ -100,30 +93,21 @@ struct ContentView: View {
var PostingTimelineView: some View {
VStack {
ZStack {
TabView(selection: $filter_state) {
contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts)
.id(FilterState.posts)
contentTimelineView(filter: FilterState.posts_and_replies.filter)
.tag(FilterState.posts_and_replies)
.id(FilterState.posts_and_replies)
}
.tabViewStyle(.page(indexDisplayMode: .never))
if privkey != nil {
PostButtonContainer(userSettings: user_settings) {
self.active_sheet = .post
}
}
TabView(selection: $filter_state) {
contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts)
.id(FilterState.posts)
contentTimelineView(filter: FilterState.posts_and_replies.filter)
.tag(FilterState.posts_and_replies)
.id(FilterState.posts_and_replies)
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.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)
}
@@ -136,6 +120,21 @@ struct ContentView: View {
if let damus = self.damus_state {
TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
}
if privkey != nil {
PostButtonContainer(userSettings: user_settings) {
self.active_sheet = .post
}
}
}
}
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)
}
}
@@ -179,7 +178,7 @@ struct ContentView: View {
.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.")
Text("DM", comment: "Toolbar label for DM view, which is the English abbreviation for Direct Message.")
.bold()
case .notifications:
Text("Notifications", comment: "Toolbar label for Notifications view.")
@@ -227,20 +226,6 @@ struct ContentView: View {
}
}
func MaybeReportView(target: ReportTarget) -> some View {
Group {
if let ds = damus_state {
if let sec = ds.keypair.privkey {
ReportView(pool: ds.pool, target: target, privkey: sec)
} else {
EmptyView()
}
} else {
EmptyView()
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state {
@@ -260,11 +245,9 @@ struct ContentView: View {
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)
}
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)
}
}
@@ -286,7 +269,6 @@ struct ContentView: View {
.padding([.bottom], 8)
}
}
.environmentObject(user_settings)
.onAppear() {
self.connect()
//KingfisherManager.shared.cache.clearDiskCache()
@@ -294,10 +276,8 @@ struct ContentView: View {
}
.sheet(item: $active_sheet) { item in
switch item {
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!)
}
@@ -343,18 +323,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)
}
.onReceive(handle_notify(.block)) { notif in
let pubkey = notif.object as! String
self.blocking = pubkey
self.confirm_block = true
}
.onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent
self.damus_state?.pool.send(.event(ev))
@@ -429,96 +397,6 @@ struct ContentView: View {
.onReceive(timer) { n in
self.damus_state?.pool.connect_to_disconnected()
}
.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: {
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
}
}, message: {
if let pubkey = self.blocking {
let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey)
Text("\(name) has been blocked", comment: "Alert message that informs a user was blocked.")
} else {
Text("User has been blocked", comment: "Alert message that informs a user was blocked.")
}
})
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of alert that creates a new mutelist.")) {
confirm_overwrite_mutelist = false
confirm_block = false
}
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
guard let ds = damus_state else {
return
}
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = blocking else {
return
}
guard let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey) else {
return
}
damus_state?.contacts.set_mutelist(mutelist)
ds.pool.send(.event(mutelist))
confirm_overwrite_mutelist = false
confirm_block = false
user_blocked_confirm = true
}
}, message: {
Text("No block list found, create a new one? This will overwrite any previous block lists.", comment: "Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists.")
})
.alert(NSLocalizedString("Block User", comment: "Title of alert for blocking a user."), isPresented: $confirm_block, actions: {
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for blocking a user."), role: .cancel) {
confirm_block = false
}
Button(NSLocalizedString("Block", comment: "Alert button to block a user."), role: .destructive) {
guard let ds = damus_state else {
return
}
if ds.contacts.mutelist == nil {
confirm_overwrite_mutelist = true
} else {
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = blocking else {
return
}
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
return
}
damus_state?.contacts.set_mutelist(ev)
ds.pool.send(.event(ev))
}
}
}, message: {
if let pubkey = blocking {
let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey)
Text("Block \(name)?", comment: "Alert message prompt to ask if a user should be blocked.")
} else {
Text("Could not find user to block...", comment: "Alert message to indicate that the blocked user could not be found.")
}
})
}
func switch_timeline(_ timeline: Timeline) {
-38
View File
@@ -11,51 +11,13 @@ import Foundation
class Contacts {
private var friends: Set<String> = Set()
private var friend_of_friends: Set<String> = Set()
private var muted: Set<String> = Set()
let our_pubkey: String
var event: NostrEvent?
var mutelist: NostrEvent?
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
}
func is_muted(_ pk: String) -> Bool {
return muted.contains(pk)
}
func set_mutelist(_ ev: NostrEvent) {
let oldlist = self.mutelist
self.mutelist = ev
let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? [])
let new = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
let diff = old.symmetricDifference(new)
var new_mutes = Array<String>()
var new_unmutes = Array<String>()
for d in diff {
if new.contains(d) {
new_mutes.append(d)
} else {
new_unmutes.append(d)
}
}
// TODO: set local mutelist here
self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
if new_mutes.count > 0 {
notify(.new_mutes, new_mutes)
}
if new_unmutes.count > 0 {
notify(.new_unmutes, new_unmutes)
}
}
func get_friendosphere() -> [String] {
var fs = get_friend_list()
fs.append(contentsOf: get_friend_of_friend_list())
+1 -1
View File
@@ -22,7 +22,7 @@ struct DamusState {
var pubkey: String {
return keypair.pubkey
}
var is_privkey_user: Bool {
keypair.privkey != nil
}
+1 -1
View File
@@ -86,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):
+1 -1
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)")
+10 -56
View File
@@ -98,8 +98,6 @@ class HomeModel: ObservableObject {
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
case .metadata:
handle_metadata_event(ev)
case .list:
handle_list_event(ev)
case .boost:
handle_boost_event(sub_id: sub_id, ev)
case .like:
@@ -126,12 +124,6 @@ class HomeModel: ObservableObject {
func handle_channel_meta(_ ev: NostrEvent) {
}
func filter_muted() {
self.events = events.filter { !damus_state.contacts.is_muted($0.pubkey) }
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
self.notifications = notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
}
func handle_delete_event(_ ev: NostrEvent) {
guard ev.is_valid else {
return
@@ -282,11 +274,7 @@ class HomeModel: ObservableObject {
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
our_contacts_filter.authors = [damus_state.pubkey]
var our_blocklist_filter = NostrFilter.filter_kinds([30000])
our_blocklist_filter.parameter = ["mute"]
our_blocklist_filter.authors = [damus_state.pubkey]
var dms_filter = NostrFilter.filter_kinds([
NostrKind.dm.rawValue,
])
@@ -323,7 +311,7 @@ class HomeModel: ObservableObject {
var home_filters = [home_filter]
var notifications_filters = [notifications_filter]
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
var contacts_filters = [contacts_filter, our_contacts_filter]
var dms_filters = [dms_filter, our_dms_filter]
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
@@ -347,32 +335,9 @@ class HomeModel: ObservableObject {
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
}
}
func handle_list_event(_ ev: NostrEvent) {
// we only care about our lists
guard ev.pubkey == damus_state.pubkey else {
return
}
if let mutelist = damus_state.contacts.mutelist {
if ev.created_at <= mutelist.created_at {
return
}
}
guard let name = get_referenced_ids(tags: ev.tags, key: "d").first else {
return
}
guard name.ref_id == "mute" else {
return
}
damus_state.contacts.set_mutelist(ev)
}
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? {
@@ -410,8 +375,12 @@ class HomeModel: ObservableObject {
return ok
}
func should_hide_event(_ ev: NostrEvent) -> Bool {
return !ev.should_show_event
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
if should_hide_event(contacts: damus_state.contacts, ev: ev) {
if should_hide_event(ev) {
return
}
@@ -530,17 +499,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) {
@@ -726,11 +688,3 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
return false
}
func should_hide_event(contacts: Contacts, ev: NostrEvent) -> Bool {
if contacts.is_muted(ev.pubkey) {
return true
}
return !ev.should_show_event
}
+4 -10
View File
@@ -5,8 +5,9 @@
// Created by Oleg Abalonski on 1/11/23.
//
import UIKit
import Foundation
import Kingfisher
import SVGKit
class KFImageModel: ObservableObject {
@@ -79,15 +80,8 @@ struct CustomImageProcessor: ImageProcessor {
}
// Handle SVG image
if let dataString = String(data: data, encoding: .utf8),
let svg = SVG(dataString) {
let render = UIGraphicsImageRenderer(size: svg.size)
let image = render.image { context in
svg.draw(in: context.cgContext)
}
return image.kf.scaled(to: options.scaleFactor)
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)
-44
View File
@@ -1,44 +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 none
case argosopentech
case terraprint
case vern
case custom
var model: Model {
switch self {
case .none:
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation server."), url: nil)
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 }
}
}
-18
View File
@@ -1,18 +0,0 @@
//
// ListModel.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import Foundation
/*
class MutelistModel: ObservableObject {
let contacts: Contacts
@Published var users: [String]
}
*/
+1 -1
View File
@@ -97,7 +97,7 @@ class ProfileModel: ObservableObject, Equatable {
} 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)
}
-59
View File
@@ -1,59 +0,0 @@
//
// Report.swift
// damus
//
// Created by William Casarin on 2023-01-24.
//
import Foundation
enum ReportType: String {
case explicit
case illegal
case spam
case impersonation
}
struct ReportNoteTarget {
let pubkey: String
let note_id: String
}
enum ReportTarget {
case user(String)
case note(ReportNoteTarget)
}
struct Report {
let type: ReportType
let target: ReportTarget
let message: String
}
func create_report_tags(target: ReportTarget, type: ReportType) -> [[String]] {
var tags: [[String]]
switch target {
case .user(let pubkey):
tags = [["p", pubkey]]
case .note(let notet):
tags = [["e", notet.note_id], ["p", notet.pubkey]]
}
tags.append(["report", type.rawValue])
return tags
}
func create_report_event(privkey: String, report: Report) -> NostrEvent? {
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
let kind = 1984
let tags = create_report_tags(target: report.target, type: report.type)
let ev = NostrEvent(content: report.message, pubkey: pubkey, kind: kind, tags: tags)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: privkey, ev: ev)
return ev
}
-77
View File
@@ -1,77 +0,0 @@
//
// RepostsModel.swift
// damus
//
// Created by Terry Yiu on 1/22/23.
//
import Foundation
class RepostsModel: ObservableObject {
let state: DamusState
let target: String
let sub_id: String
let profiles_id: String
@Published var reposts: [NostrEvent]
init (state: DamusState, target: String) {
self.state = state
self.target = target
self.sub_id = UUID().description
self.profiles_id = UUID().description
self.reposts = []
}
func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([NostrKind.boost.rawValue])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
let filter = get_filter()
let filters = [filter]
self.state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_nostr_event)
}
func unsubscribe() {
self.state.pool.unsubscribe(sub_id: sub_id)
}
func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == NostrKind.boost.rawValue else {
return
}
guard let reposted_event = last_etag(tags: ev.tags) else {
return
}
guard reposted_event == self.target else {
return
}
if insert_uniq_sorted_event(events: &self.reposts, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: reposts, damus_state: state)
break
}
}
}
+2 -6
View File
@@ -30,10 +30,6 @@ class SearchHomeModel: ObservableObject {
return filter
}
func filter_muted() {
events = events.filter { !should_hide_event(contacts: damus_state.contacts, ev: $0) }
}
func subscribe() {
loading = true
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event)
@@ -54,7 +50,7 @@ class SearchHomeModel: ObservableObject {
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
return
}
if ev.is_textlike && !should_hide_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if ev.is_textlike && ev.should_show_event && !ev.is_reply(nil) {
if seen_pubkey.contains(ev.pubkey) {
return
}
@@ -129,7 +125,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)
}
}
+1 -1
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 {
-74
View File
@@ -6,7 +6,6 @@
//
import Foundation
import Vault
class UserSettingsStore: ObservableObject {
@Published var default_wallet: Wallet {
@@ -27,44 +26,6 @@ class UserSettingsStore: ObservableObject {
}
}
@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_server == .none {
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() {
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
@@ -76,40 +37,5 @@ class UserSettingsStore: ObservableObject {
show_wallet_selector = UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
if let translationServerName = UserDefaults.standard.string(forKey: "libretranslate_server"),
let translationServer = LibreTranslateServer(rawValue: translationServerName) {
self.libretranslate_server = translationServer
libretranslate_url = translationServer.model.url ?? UserDefaults.standard.object(forKey: "libretranslate_url") as? String ?? ""
} else {
// Note from @tyiu:
// Default server 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).
libretranslate_server = .none
libretranslate_url = ""
}
do {
libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
} catch {
libretranslate_api_key = ""
}
}
func saveLibreTranslateApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
func clearLibreTranslateApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
var serviceName = "damus"
var accessGroup: String? = nil
var accountName = "libretranslate_apikey"
}
+1 -1
View File
@@ -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:",
+2 -6
View File
@@ -11,17 +11,13 @@ struct SwipeToDismissModifier: ViewModifier {
let minDistance: CGFloat?
var onDismiss: () -> Void
@State private var offset: CGSize = .zero
@GestureState private var viewOffset: CGSize = .zero
func body(content: Content) -> some View {
content
.offset(y: viewOffset.height)
.animation(.interactiveSpring(), value: viewOffset)
.offset(y: offset.height)
.animation(.interactiveSpring(), value: offset)
.simultaneousGesture(
DragGesture(minimumDistance: minDistance ?? 10)
.updating($viewOffset, body: { value, gestureState, transaction in
gestureState = CGSize(width: value.location.x - value.startLocation.x, height: value.location.y - value.startLocation.y)
})
.onChanged { gesture in
if gesture.translation.width < 50 {
offset = gesture.translation
+20 -62
View File
@@ -8,7 +8,7 @@
import Foundation
struct Profile: Codable {
var value: [String: AnyCodable]
var value: [String: String]
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
self.value = [:]
@@ -23,82 +23,44 @@ struct Profile: Codable {
self.nip05 = nip05
}
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{
return nil
}
guard let s = val.value as? T else {
return nil
}
return s
}
private mutating func set_val<T>(_ key: String, _ val: T?) {
if val == nil {
self.value.removeValue(forKey: key)
return
}
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) }
get { return value["display_name"]; }
set(s) { value["display_name"] = s }
}
var name: String? {
get { return str("name"); }
set(s) { set_str("name", s) }
get { return value["name"]; }
set(s) { value["name"] = s }
}
var about: String? {
get { return str("about"); }
set(s) { set_str("about", s) }
get { return value["about"]; }
set(s) { value["about"] = s }
}
var picture: String? {
get { return str("picture"); }
set(s) { set_str("picture", s) }
get { return value["picture"]; }
set(s) { value["picture"] = s }
}
var banner: String? {
get { return str("banner"); }
set(s) { set_str("banner", s) }
get { return value["banner"]; }
set(s) { value["banner"] = s }
}
var website: String? {
get { return str("website"); }
set(s) { set_str("website", s) }
get { return value["website"]; }
set(s) { value["website"] = s }
}
var lud06: String? {
get { return str("lud06"); }
set(s) { set_str("lud06", s) }
get { return value["lud06"]; }
set(s) { value["lud06"] = s }
}
var lud16: String? {
get { return str("lud16"); }
set(s) { set_str("lud16", s) }
}
var website_url: URL? {
return self.website.flatMap { URL(string: $0) }
get { return value["lud16"]; }
set(s) { value["lud16"] = s }
}
var lnurl: String? {
@@ -114,21 +76,17 @@ struct Profile: Codable {
}
var nip05: String? {
get { return str("nip05"); }
set(s) { set_str("nip05", s) }
get { return value["nip05"]; }
set(s) { value["nip05"] = s }
}
var lightning_uri: URL? {
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)
self.value = try container.decode([String: String].self)
}
func encode(to encoder: Encoder) throws {
+3 -24
View File
@@ -27,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
@@ -103,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 {
@@ -794,6 +790,7 @@ func inner_event_or_self(ev: NostrEvent) -> NostrEvent {
return inner_ev
}
func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
let blocks = ev.blocks(privkey).filter { block in
guard case .mention(let mention) = block else {
@@ -818,21 +815,3 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
return nil
}
extension [ReferencedId] {
var pRefs: [ReferencedId] {
get {
self.filter { ref in
ref.key == "p"
}
}
}
var eRefs: [ReferencedId] {
get {
self.filter { ref in
ref.key == "e"
}
}
}
}
+1 -3
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]?
@@ -17,7 +17,6 @@ struct NostrFilter: Codable, Equatable {
var limit: UInt32?
var authors: [String]?
var hashtag: [String]? = nil
var parameter: [String]? = nil
private enum CodingKeys : String, CodingKey {
case ids
@@ -25,7 +24,6 @@ struct NostrFilter: Codable, Equatable {
case referenced_ids = "#e"
case pubkeys = "#p"
case hashtag = "#t"
case parameter = "#d"
case since
case until
case authors
-1
View File
@@ -19,5 +19,4 @@ enum NostrKind: Int {
case channel_create = 40
case channel_meta = 41
case chat = 42
case list = 30000
}
+2 -30
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) }
}
-22
View File
@@ -1,22 +0,0 @@
//
// AccountDeletion.swift
// damus
//
// Created by William Casarin on 2023-01-30.
//
import Foundation
func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent {
var profile = Profile()
profile.deleted = true
profile.about = "account deleted"
profile.name = "nobody"
let content = encode_json(profile)!
let ev = NostrEvent(content: content, pubkey: keypair.pubkey, kind: 0)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: keypair.privkey, ev: ev)
return ev
}
-147
View File
@@ -1,147 +0,0 @@
import Foundation
/**
A type-erased `Codable` value.
The `AnyCodable` type forwards encoding and decoding responsibilities
to an underlying value, hiding its specific underlying type.
You can encode or decode mixed-type values in dictionaries
and other collections that require `Encodable` or `Decodable` conformance
by declaring their contained type to be `AnyCodable`.
- SeeAlso: `AnyEncodable`
- SeeAlso: `AnyDecodable`
*/
@frozen public struct AnyCodable: Codable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
}
extension AnyCodable: _AnyEncodable, _AnyDecodable {}
extension AnyCodable: Equatable {
public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool {
switch (lhs.value, rhs.value) {
case is (Void, Void):
return true
case let (lhs as Bool, rhs as Bool):
return lhs == rhs
case let (lhs as Int, rhs as Int):
return lhs == rhs
case let (lhs as Int8, rhs as Int8):
return lhs == rhs
case let (lhs as Int16, rhs as Int16):
return lhs == rhs
case let (lhs as Int32, rhs as Int32):
return lhs == rhs
case let (lhs as Int64, rhs as Int64):
return lhs == rhs
case let (lhs as UInt, rhs as UInt):
return lhs == rhs
case let (lhs as UInt8, rhs as UInt8):
return lhs == rhs
case let (lhs as UInt16, rhs as UInt16):
return lhs == rhs
case let (lhs as UInt32, rhs as UInt32):
return lhs == rhs
case let (lhs as UInt64, rhs as UInt64):
return lhs == rhs
case let (lhs as Float, rhs as Float):
return lhs == rhs
case let (lhs as Double, rhs as Double):
return lhs == rhs
case let (lhs as String, rhs as String):
return lhs == rhs
case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]):
return lhs == rhs
case let (lhs as [AnyCodable], rhs as [AnyCodable]):
return lhs == rhs
case let (lhs as [String: Any], rhs as [String: Any]):
return NSDictionary(dictionary: lhs) == NSDictionary(dictionary: rhs)
case let (lhs as [Any], rhs as [Any]):
return NSArray(array: lhs) == NSArray(array: rhs)
case is (NSNull, NSNull):
return true
default:
return false
}
}
}
extension AnyCodable: CustomStringConvertible {
public var description: String {
switch value {
case is Void:
return String(describing: nil as Any?)
case let value as CustomStringConvertible:
return value.description
default:
return String(describing: value)
}
}
}
extension AnyCodable: CustomDebugStringConvertible {
public var debugDescription: String {
switch value {
case let value as CustomDebugStringConvertible:
return "AnyCodable(\(value.debugDescription))"
default:
return "AnyCodable(\(description))"
}
}
}
extension AnyCodable: ExpressibleByNilLiteral {}
extension AnyCodable: ExpressibleByBooleanLiteral {}
extension AnyCodable: ExpressibleByIntegerLiteral {}
extension AnyCodable: ExpressibleByFloatLiteral {}
extension AnyCodable: ExpressibleByStringLiteral {}
extension AnyCodable: ExpressibleByStringInterpolation {}
extension AnyCodable: ExpressibleByArrayLiteral {}
extension AnyCodable: ExpressibleByDictionaryLiteral {}
extension AnyCodable: Hashable {
public func hash(into hasher: inout Hasher) {
switch value {
case let value as Bool:
hasher.combine(value)
case let value as Int:
hasher.combine(value)
case let value as Int8:
hasher.combine(value)
case let value as Int16:
hasher.combine(value)
case let value as Int32:
hasher.combine(value)
case let value as Int64:
hasher.combine(value)
case let value as UInt:
hasher.combine(value)
case let value as UInt8:
hasher.combine(value)
case let value as UInt16:
hasher.combine(value)
case let value as UInt32:
hasher.combine(value)
case let value as UInt64:
hasher.combine(value)
case let value as Float:
hasher.combine(value)
case let value as Double:
hasher.combine(value)
case let value as String:
hasher.combine(value)
case let value as [String: AnyCodable]:
hasher.combine(value)
case let value as [AnyCodable]:
hasher.combine(value)
default:
break
}
}
}
-188
View File
@@ -1,188 +0,0 @@
#if canImport(Foundation)
import Foundation
#endif
/**
A type-erased `Decodable` value.
The `AnyDecodable` type forwards decoding responsibilities
to an underlying value, hiding its specific underlying type.
You can decode mixed-type values in dictionaries
and other collections that require `Decodable` conformance
by declaring their contained type to be `AnyDecodable`:
let json = """
{
"boolean": true,
"integer": 42,
"double": 3.141592653589793,
"string": "string",
"array": [1, 2, 3],
"nested": {
"a": "alpha",
"b": "bravo",
"c": "charlie"
},
"null": null
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let dictionary = try! decoder.decode([String: AnyDecodable].self, from: json)
*/
@frozen public struct AnyDecodable: Decodable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
}
@usableFromInline
protocol _AnyDecodable {
var value: Any { get }
init<T>(_ value: T?)
}
extension AnyDecodable: _AnyDecodable {}
extension _AnyDecodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
#if canImport(Foundation)
self.init(NSNull())
#else
self.init(Optional<Self>.none)
#endif
} else if let bool = try? container.decode(Bool.self) {
self.init(bool)
} else if let int = try? container.decode(Int.self) {
self.init(int)
} else if let uint = try? container.decode(UInt.self) {
self.init(uint)
} else if let double = try? container.decode(Double.self) {
self.init(double)
} else if let string = try? container.decode(String.self) {
self.init(string)
} else if let array = try? container.decode([AnyDecodable].self) {
self.init(array.map { $0.value })
} else if let dictionary = try? container.decode([String: AnyDecodable].self) {
self.init(dictionary.mapValues { $0.value })
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyDecodable value cannot be decoded")
}
}
}
extension AnyDecodable: Equatable {
public static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool {
switch (lhs.value, rhs.value) {
#if canImport(Foundation)
case is (NSNull, NSNull), is (Void, Void):
return true
#endif
case let (lhs as Bool, rhs as Bool):
return lhs == rhs
case let (lhs as Int, rhs as Int):
return lhs == rhs
case let (lhs as Int8, rhs as Int8):
return lhs == rhs
case let (lhs as Int16, rhs as Int16):
return lhs == rhs
case let (lhs as Int32, rhs as Int32):
return lhs == rhs
case let (lhs as Int64, rhs as Int64):
return lhs == rhs
case let (lhs as UInt, rhs as UInt):
return lhs == rhs
case let (lhs as UInt8, rhs as UInt8):
return lhs == rhs
case let (lhs as UInt16, rhs as UInt16):
return lhs == rhs
case let (lhs as UInt32, rhs as UInt32):
return lhs == rhs
case let (lhs as UInt64, rhs as UInt64):
return lhs == rhs
case let (lhs as Float, rhs as Float):
return lhs == rhs
case let (lhs as Double, rhs as Double):
return lhs == rhs
case let (lhs as String, rhs as String):
return lhs == rhs
case let (lhs as [String: AnyDecodable], rhs as [String: AnyDecodable]):
return lhs == rhs
case let (lhs as [AnyDecodable], rhs as [AnyDecodable]):
return lhs == rhs
default:
return false
}
}
}
extension AnyDecodable: CustomStringConvertible {
public var description: String {
switch value {
case is Void:
return String(describing: nil as Any?)
case let value as CustomStringConvertible:
return value.description
default:
return String(describing: value)
}
}
}
extension AnyDecodable: CustomDebugStringConvertible {
public var debugDescription: String {
switch value {
case let value as CustomDebugStringConvertible:
return "AnyDecodable(\(value.debugDescription))"
default:
return "AnyDecodable(\(description))"
}
}
}
extension AnyDecodable: Hashable {
public func hash(into hasher: inout Hasher) {
switch value {
case let value as Bool:
hasher.combine(value)
case let value as Int:
hasher.combine(value)
case let value as Int8:
hasher.combine(value)
case let value as Int16:
hasher.combine(value)
case let value as Int32:
hasher.combine(value)
case let value as Int64:
hasher.combine(value)
case let value as UInt:
hasher.combine(value)
case let value as UInt8:
hasher.combine(value)
case let value as UInt16:
hasher.combine(value)
case let value as UInt32:
hasher.combine(value)
case let value as UInt64:
hasher.combine(value)
case let value as Float:
hasher.combine(value)
case let value as Double:
hasher.combine(value)
case let value as String:
hasher.combine(value)
case let value as [String: AnyDecodable]:
hasher.combine(value)
case let value as [AnyDecodable]:
hasher.combine(value)
default:
break
}
}
}
-291
View File
@@ -1,291 +0,0 @@
#if canImport(Foundation)
import Foundation
#endif
/**
A type-erased `Encodable` value.
The `AnyEncodable` type forwards encoding responsibilities
to an underlying value, hiding its specific underlying type.
You can encode mixed-type values in dictionaries
and other collections that require `Encodable` conformance
by declaring their contained type to be `AnyEncodable`:
let dictionary: [String: AnyEncodable] = [
"boolean": true,
"integer": 42,
"double": 3.141592653589793,
"string": "string",
"array": [1, 2, 3],
"nested": [
"a": "alpha",
"b": "bravo",
"c": "charlie"
],
"null": nil
]
let encoder = JSONEncoder()
let json = try! encoder.encode(dictionary)
*/
@frozen public struct AnyEncodable: Encodable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
}
@usableFromInline
protocol _AnyEncodable {
var value: Any { get }
init<T>(_ value: T?)
}
extension AnyEncodable: _AnyEncodable {}
// MARK: - Encodable
extension _AnyEncodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch value {
#if canImport(Foundation)
case is NSNull:
try container.encodeNil()
#endif
case is Void:
try container.encodeNil()
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let int8 as Int8:
try container.encode(int8)
case let int16 as Int16:
try container.encode(int16)
case let int32 as Int32:
try container.encode(int32)
case let int64 as Int64:
try container.encode(int64)
case let uint as UInt:
try container.encode(uint)
case let uint8 as UInt8:
try container.encode(uint8)
case let uint16 as UInt16:
try container.encode(uint16)
case let uint32 as UInt32:
try container.encode(uint32)
case let uint64 as UInt64:
try container.encode(uint64)
case let float as Float:
try container.encode(float)
case let double as Double:
try container.encode(double)
case let string as String:
try container.encode(string)
#if canImport(Foundation)
case let number as NSNumber:
try encode(nsnumber: number, into: &container)
case let date as Date:
try container.encode(date)
case let url as URL:
try container.encode(url)
#endif
case let array as [Any?]:
try container.encode(array.map { AnyEncodable($0) })
case let dictionary as [String: Any?]:
try container.encode(dictionary.mapValues { AnyEncodable($0) })
case let encodable as Encodable:
try encodable.encode(to: encoder)
default:
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyEncodable value cannot be encoded")
throw EncodingError.invalidValue(value, context)
}
}
#if canImport(Foundation)
private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws {
switch Character(Unicode.Scalar(UInt8(nsnumber.objCType.pointee))) {
case "B":
try container.encode(nsnumber.boolValue)
case "c":
try container.encode(nsnumber.int8Value)
case "s":
try container.encode(nsnumber.int16Value)
case "i", "l":
try container.encode(nsnumber.int32Value)
case "q":
try container.encode(nsnumber.int64Value)
case "C":
try container.encode(nsnumber.uint8Value)
case "S":
try container.encode(nsnumber.uint16Value)
case "I", "L":
try container.encode(nsnumber.uint32Value)
case "Q":
try container.encode(nsnumber.uint64Value)
case "f":
try container.encode(nsnumber.floatValue)
case "d":
try container.encode(nsnumber.doubleValue)
default:
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "NSNumber cannot be encoded because its type is not handled")
throw EncodingError.invalidValue(nsnumber, context)
}
}
#endif
}
extension AnyEncodable: Equatable {
public static func == (lhs: AnyEncodable, rhs: AnyEncodable) -> Bool {
switch (lhs.value, rhs.value) {
case is (Void, Void):
return true
case let (lhs as Bool, rhs as Bool):
return lhs == rhs
case let (lhs as Int, rhs as Int):
return lhs == rhs
case let (lhs as Int8, rhs as Int8):
return lhs == rhs
case let (lhs as Int16, rhs as Int16):
return lhs == rhs
case let (lhs as Int32, rhs as Int32):
return lhs == rhs
case let (lhs as Int64, rhs as Int64):
return lhs == rhs
case let (lhs as UInt, rhs as UInt):
return lhs == rhs
case let (lhs as UInt8, rhs as UInt8):
return lhs == rhs
case let (lhs as UInt16, rhs as UInt16):
return lhs == rhs
case let (lhs as UInt32, rhs as UInt32):
return lhs == rhs
case let (lhs as UInt64, rhs as UInt64):
return lhs == rhs
case let (lhs as Float, rhs as Float):
return lhs == rhs
case let (lhs as Double, rhs as Double):
return lhs == rhs
case let (lhs as String, rhs as String):
return lhs == rhs
case let (lhs as [String: AnyEncodable], rhs as [String: AnyEncodable]):
return lhs == rhs
case let (lhs as [AnyEncodable], rhs as [AnyEncodable]):
return lhs == rhs
default:
return false
}
}
}
extension AnyEncodable: CustomStringConvertible {
public var description: String {
switch value {
case is Void:
return String(describing: nil as Any?)
case let value as CustomStringConvertible:
return value.description
default:
return String(describing: value)
}
}
}
extension AnyEncodable: CustomDebugStringConvertible {
public var debugDescription: String {
switch value {
case let value as CustomDebugStringConvertible:
return "AnyEncodable(\(value.debugDescription))"
default:
return "AnyEncodable(\(description))"
}
}
}
extension AnyEncodable: ExpressibleByNilLiteral {}
extension AnyEncodable: ExpressibleByBooleanLiteral {}
extension AnyEncodable: ExpressibleByIntegerLiteral {}
extension AnyEncodable: ExpressibleByFloatLiteral {}
extension AnyEncodable: ExpressibleByStringLiteral {}
extension AnyEncodable: ExpressibleByStringInterpolation {}
extension AnyEncodable: ExpressibleByArrayLiteral {}
extension AnyEncodable: ExpressibleByDictionaryLiteral {}
extension _AnyEncodable {
public init(nilLiteral _: ()) {
self.init(nil as Any?)
}
public init(booleanLiteral value: Bool) {
self.init(value)
}
public init(integerLiteral value: Int) {
self.init(value)
}
public init(floatLiteral value: Double) {
self.init(value)
}
public init(extendedGraphemeClusterLiteral value: String) {
self.init(value)
}
public init(stringLiteral value: String) {
self.init(value)
}
public init(arrayLiteral elements: Any...) {
self.init(elements)
}
public init(dictionaryLiteral elements: (AnyHashable, Any)...) {
self.init([AnyHashable: Any](elements, uniquingKeysWith: { first, _ in first }))
}
}
extension AnyEncodable: Hashable {
public func hash(into hasher: inout Hasher) {
switch value {
case let value as Bool:
hasher.combine(value)
case let value as Int:
hasher.combine(value)
case let value as Int8:
hasher.combine(value)
case let value as Int16:
hasher.combine(value)
case let value as Int32:
hasher.combine(value)
case let value as Int64:
hasher.combine(value)
case let value as UInt:
hasher.combine(value)
case let value as UInt8:
hasher.combine(value)
case let value as UInt16:
hasher.combine(value)
case let value as UInt32:
hasher.combine(value)
case let value as UInt64:
hasher.combine(value)
case let value as Float:
hasher.combine(value)
case let value as Double:
hasher.combine(value)
case let value as String:
hasher.combine(value)
case let value as [String: AnyEncodable]:
hasher.combine(value)
case let value as [AnyEncodable]:
hasher.combine(value)
default:
break
}
}
}
-31
View File
@@ -1,31 +0,0 @@
//
// Bech32Object.swift
// damus
//
// Created by William Casarin on 2023-01-28.
//
import Foundation
enum Bech32Object {
case nsec(String)
case npub(String)
case note(String)
static func parse(_ str: String) -> Bech32Object? {
guard let decoded = try? bech32_decode(str) else {
return nil
}
if decoded.hrp == "npub" {
return .npub(hex_encode(decoded.data))
} else if decoded.hrp == "nsec" {
return .nsec(hex_encode(decoded.data))
} else if decoded.hrp == "note" {
return .note(hex_encode(decoded.data))
}
return nil
}
}
-101
View File
@@ -1,101 +0,0 @@
//
// CoreSVG.swift
// damus
//
// Created by Oleg Abalonski on 1/27/23.
// Ref: https://gist.github.com/ollieatkinson/eb87a82fcb5500d5561fed8b0900a9f7
import Darwin
import Foundation
import UIKit
@objc
class CGSVGDocument: NSObject { }
var CGSVGDocumentRetain: (@convention(c) (CGSVGDocument?) -> Unmanaged<CGSVGDocument>?) = load("CGSVGDocumentRetain")
var CGSVGDocumentRelease: (@convention(c) (CGSVGDocument?) -> Void) = load("CGSVGDocumentRelease")
var CGSVGDocumentCreateFromData: (@convention(c) (CFData?, CFDictionary?) -> Unmanaged<CGSVGDocument>?) = load("CGSVGDocumentCreateFromData")
var CGContextDrawSVGDocument: (@convention(c) (CGContext?, CGSVGDocument?) -> Void) = load("CGContextDrawSVGDocument")
var CGSVGDocumentGetCanvasSize: (@convention(c) (CGSVGDocument?) -> CGSize) = load("CGSVGDocumentGetCanvasSize")
typealias ImageWithCGSVGDocument = @convention(c) (AnyObject, Selector, CGSVGDocument) -> UIImage
var ImageWithCGSVGDocumentSEL: Selector = NSSelectorFromString("_imageWithCGSVGDocument:")
let CoreSVG = dlopen("/System/Library/PrivateFrameworks/CoreSVG.framework/CoreSVG", RTLD_NOW)
func load<T>(_ name: String) -> T {
unsafeBitCast(dlsym(CoreSVG, name), to: T.self)
}
public class SVG {
deinit { CGSVGDocumentRelease(document) }
let document: CGSVGDocument
public convenience init?(_ value: String) {
guard let data = value.data(using: .utf8) else { return nil }
self.init(data)
}
public init?(_ data: Data) {
guard let document = CGSVGDocumentCreateFromData(data as CFData, nil)?.takeUnretainedValue() else { return nil }
guard CGSVGDocumentGetCanvasSize(document) != .zero else { return nil }
self.document = document
}
public var size: CGSize {
CGSVGDocumentGetCanvasSize(document)
}
public func image() -> UIImage? {
let ImageWithCGSVGDocument = unsafeBitCast(UIImage.self.method(for: ImageWithCGSVGDocumentSEL), to: ImageWithCGSVGDocument.self)
let image = ImageWithCGSVGDocument(UIImage.self, ImageWithCGSVGDocumentSEL, document)
return image
}
public func draw(in context: CGContext) {
draw(in: context, size: size)
}
public func draw(in context: CGContext, size target: CGSize) {
var target = target
let ratio = (
x: target.width / size.width,
y: target.height / size.height
)
let rect = (
document: CGRect(origin: .zero, size: size), ()
)
let scale: (x: CGFloat, y: CGFloat)
if target.width <= 0 {
scale = (ratio.y, ratio.y)
target.width = size.width * scale.x
} else if target.height <= 0 {
scale = (ratio.x, ratio.x)
target.width = size.width * scale.y
} else {
let min = min(ratio.x, ratio.y)
scale = (min, min)
target.width = size.width * scale.x
target.height = size.height * scale.y
}
let transform = (
scale: CGAffineTransform(scaleX: scale.x, y: scale.y),
aspect: CGAffineTransform(translationX: (target.width / scale.x - rect.document.width) / 2, y: (target.height / scale.y - rect.document.height) / 2)
)
context.translateBy(x: 0, y: target.height)
context.scaleBy(x: 1, y: -1)
context.concatenate(transform.scale)
context.concatenate(transform.aspect)
CGContextDrawSVGDocument(context, document)
}
}
-13
View File
@@ -12,25 +12,12 @@ import Vault
let PUBKEY_HRP = "npub"
let PRIVKEY_HRP = "nsec"
struct FullKeypair {
let pubkey: String
let privkey: String
}
struct Keypair {
let pubkey: String
let privkey: String?
let pubkey_bech32: String
let privkey_bech32: String?
func to_full() -> FullKeypair? {
guard let privkey = self.privkey else {
return nil
}
return FullKeypair(pubkey: pubkey, privkey: privkey)
}
init(pubkey: String, privkey: String?) {
self.pubkey = pubkey
self.privkey = privkey
-91
View File
@@ -1,91 +0,0 @@
//
// Mute.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import Foundation
func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: String) -> NostrEvent? {
return create_or_update_list_event(keypair: keypair, mprev: mprev, to_add: to_add, list_name: "mute", list_type: "p")
}
func remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent, to_remove: String) -> NostrEvent? {
return remove_from_list_event(keypair: keypair, prev: prev, to_remove: to_remove, tag_type: "p")
}
func create_or_update_list_event(keypair: FullKeypair, mprev: NostrEvent?, to_add: String, list_name: String, list_type: String) -> NostrEvent? {
let pubkey = keypair.pubkey
if let prev = mprev {
if let okprev = ensure_list_name(list: prev, name: list_name), prev.pubkey == keypair.pubkey {
return add_to_list_event(keypair: keypair, prev: okprev, to_add: to_add, tag_type: list_type)
}
}
let tags = [["d", list_name], [list_type, to_add]]
let ev = NostrEvent(content: "", pubkey: pubkey, kind: 30000, tags: tags)
ev.tags = tags
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: keypair.privkey, ev: ev)
return ev
}
func remove_from_list_event(keypair: FullKeypair, prev: NostrEvent, to_remove: String, tag_type: String) -> NostrEvent? {
var exists = false
for tag in prev.tags {
if tag.count >= 2 && tag[0] == tag_type && tag[1] == to_remove {
exists = true
}
}
// make sure we actually have the pubkey to remove
guard exists else {
return nil
}
let new_tags = prev.tags.filter { tag in
!(tag.count >= 2 && tag[0] == tag_type && tag[1] == to_remove)
}
let ev = NostrEvent(content: prev.content, pubkey: keypair.pubkey, kind: 30000, tags: new_tags)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: keypair.privkey, ev: ev)
return ev
}
func add_to_list_event(keypair: FullKeypair, prev: NostrEvent, to_add: String, tag_type: String) -> NostrEvent? {
for tag in prev.tags {
// we are already muting this user
if tag.count >= 2 && tag[0] == tag_type && tag[1] == to_add {
return nil
}
}
let new = NostrEvent(content: prev.content, pubkey: keypair.pubkey, kind: 30000, tags: prev.tags)
new.tags.append([tag_type, to_add])
new.id = calculate_event_id(ev: new)
new.sig = sign_event(privkey: keypair.privkey, ev: new)
return new
}
func ensure_list_name(list: NostrEvent, name: String) -> NostrEvent? {
for tag in list.tags {
if tag.count >= 2 && tag[0] == "d" {
if tag[1] != name {
return nil
} else {
return list
}
}
}
list.tags.insert(["d", name], at: 0)
return list
}
+72 -15
View File
@@ -11,93 +11,150 @@ extension Notification.Name {
static var thread_focus: Notification.Name {
return Notification.Name("thread focus")
}
}
extension Notification.Name {
static var relays_changed: Notification.Name {
return Notification.Name("relays_changed")
}
}
extension Notification.Name {
static var select_event: Notification.Name {
return Notification.Name("select_event")
}
}
extension Notification.Name {
static var select_quote: Notification.Name {
return Notification.Name("select quote")
}
}
extension Notification.Name {
static var reply: Notification.Name {
return Notification.Name("reply")
}
}
extension Notification.Name {
static var profile_updated: Notification.Name {
return Notification.Name("profile_updated")
}
}
extension Notification.Name {
static var switched_timeline: Notification.Name {
return Notification.Name("switched_timeline")
}
}
extension Notification.Name {
static var liked: Notification.Name {
return Notification.Name("liked")
}
}
extension Notification.Name {
static var open_profile: Notification.Name {
return Notification.Name("open_profile")
}
}
extension Notification.Name {
static var scroll_to_top: Notification.Name {
return Notification.Name("scroll_to_to")
}
}
extension Notification.Name {
static var broadcast_event: Notification.Name {
return Notification.Name("broadcast event")
}
}
extension Notification.Name {
static var open_thread: Notification.Name {
return Notification.Name("open thread")
}
}
extension Notification.Name {
static var notice: Notification.Name {
return Notification.Name("notice")
}
}
extension Notification.Name {
static var like: Notification.Name {
return Notification.Name("like note")
}
}
extension Notification.Name {
static var delete: Notification.Name {
return Notification.Name("delete note")
}
}
extension Notification.Name {
static var post: Notification.Name {
return Notification.Name("send post")
}
}
extension Notification.Name {
static var boost: Notification.Name {
return Notification.Name("boost")
}
}
extension Notification.Name {
static var boosted: Notification.Name {
return Notification.Name("boosted")
}
}
extension Notification.Name {
static var follow: Notification.Name {
return Notification.Name("follow")
}
}
extension Notification.Name {
static var unfollow: Notification.Name {
return Notification.Name("unfollow")
}
}
extension Notification.Name {
static var login: Notification.Name {
return Notification.Name("login")
}
}
extension Notification.Name {
static var logout: Notification.Name {
return Notification.Name("logout")
}
}
extension Notification.Name {
static var followed: Notification.Name {
return Notification.Name("followed")
}
}
extension Notification.Name {
static var chatroom_meta: Notification.Name {
return Notification.Name("chatroom_meta")
}
}
extension Notification.Name {
static var unfollowed: Notification.Name {
return Notification.Name("unfollowed")
}
static var report: Notification.Name {
return Notification.Name("report")
}
static var block: Notification.Name {
return Notification.Name("block")
}
static var new_mutes: Notification.Name {
return Notification.Name("new_mutes")
}
static var new_unmutes: Notification.Name {
return Notification.Name("new_unmutes")
}
static var deleted_account: Notification.Name {
return Notification.Name("deleted_account")
}
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
+1 -4
View File
@@ -15,10 +15,7 @@ struct EventDetailBar: View {
var body: some View {
HStack {
if bar.boosts > 0 {
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
Text("\(Text("\(bar.boosts)", comment: "Number of reposts.").font(.body.bold())) \(Text(String(format: NSLocalizedString("reposts_count", comment: "Part of a larger sentence to describe how many reposts there are."), bar.boosts)).foregroundColor(.gray))", comment: "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'.")
}
.buttonStyle(PlainButtonStyle())
Text("\(Text("\(bar.boosts)", comment: "Number of reposts.").font(.body.bold())) \(Text(String(format: NSLocalizedString("reposts_count", comment: "Part of a larger sentence to describe how many reposts there are."), bar.boosts)).foregroundColor(.gray))", comment: "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'.")
}
if bar.likes > 0 {
+1 -1
View File
@@ -18,7 +18,7 @@ struct InnerBannerImageView: View {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 20_971_520, // 20 MiB
maxByteSize: 5000000,
downsampleSize: CGSize(width: 750, height: 250)
)
}
+1 -1
View File
@@ -24,7 +24,7 @@ struct ChatroomView: View {
next_ev: ind == count-1 ? nil : thread.events[ind+1],
damus_state: damus
)
.event_context_menu(ev, keypair: damus.keypair, target_pubkey: ev.pubkey)
.event_context_menu(ev, pubkey: ev.pubkey, privkey: damus.keypair.privkey)
.onTapGesture {
if thread.initial_event.id == ev.id {
//dismiss()
+94 -72
View File
@@ -5,29 +5,30 @@
// Created by William Casarin on 2022-06-09.
//
import AVFoundation
import Kingfisher
import SwiftUI
import Kingfisher
struct ConfigView: View {
let state: DamusState
@Environment(\.dismiss) var dismiss
@State var show_add_relay: Bool = false
@State var confirm_logout: Bool = false
@State var confirm_delete_account: Bool = false
@State var new_relay: String = ""
@State var show_privkey: Bool = false
@State var show_libretranslate_api_key: Bool = false
@State var privkey: String
@State var privkey_copied: Bool = false
@State var pubkey_copied: Bool = false
@State var delete_text: String = ""
@State var relays: [RelayDescriptor]
@EnvironmentObject var user_settings: UserSettingsStore
let generator = UIImpactFeedbackGenerator(style: .light)
init(state: DamusState) {
self.state = state
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
_relays = State(initialValue: state.pool.descriptors)
}
// TODO: (jb55) could be more general but not gonna worry about it atm
func CopyButton(is_pk: Bool) -> some View {
return Button(action: {
@@ -40,19 +41,52 @@ struct ConfigView: View {
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
}
}
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { (xs, x) in
if let _ = state.pool.get_relay(x) {
} else {
xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
}
}
}
var body: some View {
ZStack(alignment: .leading) {
Form {
Section {
List(Array(relays), id: \.url) { relay in
RelayView(state: state, relay: relay.url.absoluteString)
}
} header: {
HStack {
Text("Relays", comment: "Header text for relay server list for configuration.")
Spacer()
Button(action: { show_add_relay = true }) {
Image(systemName: "plus")
.foregroundColor(.accentColor)
}
}
}
if recommended.count > 0 {
Section(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration")) {
List(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.absoluteString)
}
}
}
Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
HStack {
Text(state.keypair.pubkey_bech32)
CopyButton(is_pk: true)
}
.clipShape(RoundedRectangle(cornerRadius: 5))
}
if let sec = state.keypair.privkey_bech32 {
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
HStack {
@@ -63,14 +97,14 @@ struct ConfigView: View {
Text(sec)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
CopyButton(is_pk: false)
}
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
}
}
Section(NSLocalizedString("Wallet Selector", comment: "Section title for selection of wallet.")) {
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $user_settings.show_wallet_selector).toggleStyle(.switch)
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"),
@@ -82,39 +116,6 @@ struct ConfigView: View {
}
}
Section(NSLocalizedString("LibreTranslate Translations", comment: "Section title for selecting the server that hosts the LibreTranslate machine translation API.")) {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $user_settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if user_settings.libretranslate_server != .none {
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $user_settings.libretranslate_url)
.disableAutocorrection(true)
.disabled(user_settings.libretranslate_server != .custom)
.autocapitalization(UITextAutocapitalizationType.none)
HStack {
if show_libretranslate_api_key {
TextField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $user_settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
show_libretranslate_api_key = false
}
} else {
SecureField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $user_settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
Button(NSLocalizedString("Show API Key", comment: "Button to hide the LibreTranslate server API key.")) {
show_libretranslate_api_key = true
}
}
}
}
}
Section(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen")) {
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $user_settings.left_handed)
.toggleStyle(.switch)
@@ -128,49 +129,70 @@ struct ConfigView: View {
}
}
if state.is_privkey_user {
Section(NSLocalizedString("Delete", comment: "Section title for deleting the user")) {
Button(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), role: .destructive) {
confirm_delete_account = true
}
Section(NSLocalizedString("Reset", comment: "Section title for resetting the user")) {
Button(NSLocalizedString("Logout", comment: "Button to logout the user.")) {
confirm_logout = true
}
}
}
}
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
.navigationBarTitleDisplayMode(.large)
.alert(NSLocalizedString("Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
TextField("Type DELETE to delete", text: $delete_text)
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
confirm_delete_account = false
}
Button(NSLocalizedString("Delete", comment: "Button for deleting the users account."), role: .destructive) {
guard let full_kp = state.keypair.to_full() else {
return
}
guard delete_text == "DELETE" else {
return
}
let ev = created_deleted_account_profile(keypair: full_kp)
state.pool.send(.event(ev))
notify(.logout, ())
}
}
.alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user.")) {
confirm_logout = false
}
Button(NSLocalizedString("Logout", comment: "Button for logging out the user."), role: .destructive) {
Button(NSLocalizedString("Logout", comment: "Button for logging out the user.")) {
notify(.logout, ())
}
} message: {
Text("Make sure your nsec account key is saved before you logout or you will lose access to this account", comment: "Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.")
}
.sheet(isPresented: $show_add_relay) {
AddRelayView(show_add_relay: $show_add_relay, relay: $new_relay) { m_relay in
guard var relay = m_relay else {
return
}
if relay.starts(with: "wss://") == false {
relay = "wss://" + relay
}
guard let url = URL(string: relay) else {
return
}
guard let ev = state.contacts.event else {
return
}
guard let privkey = state.keypair.privkey else {
return
}
let info = RelayInfo.rw
guard (try? state.pool.add_relay(url, info: info)) != nil else {
return
}
state.pool.connect(to: [relay])
guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.descriptors, relay: relay, info: info) else {
return
}
process_contact_event(pool: state.pool, contacts: state.contacts, pubkey: state.pubkey, ev: ev)
state.pool.send(.event(new_ev))
}
}
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.onReceive(handle_notify(.relays_changed)) { _ in
self.relays = state.pool.descriptors
}
}
}
-2
View File
@@ -11,7 +11,6 @@ struct CreateAccountView: View {
@StateObject var account: CreateAccountModel = CreateAccountModel()
@State var is_light: Bool = false
@State var is_done: Bool = false
@State var reading_eula: Bool = false
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
return VStack(alignment: .leading, spacing: 10.0, content: content)
@@ -76,7 +75,6 @@ struct CreateAccountView: View {
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
EmptyView()
}
DamusWhiteButton(NSLocalizedString("Create", comment: "Button to create account.")) {
self.is_done = true
}
+22 -18
View File
@@ -19,7 +19,7 @@ struct DMChatView: View {
VStack(alignment: .leading) {
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
DMView(event: dms.events[ind], damus_state: damus_state)
.event_context_menu(ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey)
.event_context_menu(ev, pubkey: ev.pubkey, privkey: damus_state.keypair.privkey)
}
EndBlock(height: 80)
}
@@ -63,8 +63,6 @@ struct DMChatView: View {
)
.padding(16)
.foregroundColor(Color.primary)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
.fixedSize(horizontal: false, vertical: true)
}
@Environment(\.colorScheme) var colorScheme
@@ -99,15 +97,22 @@ struct DMChatView: View {
}
}
}
.fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
Text(message).opacity(0).padding(.all, 8)
.fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
}
.fixedSize(horizontal: false, vertical: true)
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
.frame(height: 50 + 20 * CGFloat(text_lines))
}
var text_lines: Int {
var lines = 1
for c in message {
if lines > 4 {
return lines
}
if c.isNewline {
lines += 1
}
}
return lines
}
func send_message() {
@@ -137,15 +142,14 @@ struct DMChatView: View {
Footer
}
Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.")
.lineLimit(nil)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
.opacity(((dms.events.count == 0) ? 1.0 : 0.0))
.foregroundColor(.gray)
.lineLimit(nil)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
.opacity(((dms.events.count == 0) ? 1.0 : 0.0))
.foregroundColor(.gray)
}
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message."))
.navigationTitle(NSLocalizedString("DM", comment: "Navigation title for DM view, which is the English abbreviation for Direct Message."))
.toolbar { Header }
}
}
+11 -10
View File
@@ -49,7 +49,7 @@ struct DirectMessagesView: View {
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View {
Group {
if let ev = tup.1.events.last {
EventView(damus: damus_state, event: ev, pubkey: tup.0)
EventView(damus: damus_state, event: ev, pubkey: tup.0, show_friend_icon: true)
.onTapGesture {
pubkey = tup.0
active_model = tup.1
@@ -62,16 +62,16 @@ struct DirectMessagesView: View {
}
var body: some View {
VStack(spacing: 0) {
CustomPicker(selection: $dm_type, content: {
Text("DMs", comment: "Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.")
VStack {
Picker(NSLocalizedString("DM Type", comment: "DM selector for seeing either DMs or message requests, which are messages that have not been responded to yet."), selection: $dm_type) {
Text("DMs")
.tag(DMType.friend)
Text("Requests", comment: "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.")
Text("Requests")
.tag(DMType.rando)
})
Divider()
.frame(height: 1)
}
.pickerStyle(.segmented)
TabView(selection: $dm_type) {
MainContent(requests: false)
@@ -83,7 +83,8 @@ struct DirectMessagesView: View {
.tabViewStyle(.page(indexDisplayMode: .never))
}
.padding(.horizontal)
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message."))
.padding(.top)
.navigationTitle(NSLocalizedString("Encrypted DMs", comment: "Navigation title for view of encrypted DMs, where DM is an English abbreviation for Direct Message."))
}
}
-103
View File
@@ -1,103 +0,0 @@
//
// EULAView.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import SwiftUI
struct EULAView: View {
var state: SetupState?
@Environment(\.dismiss) var dismiss
@State var accepted = false
var body: some View {
ZStack {
DamusGradient()
ScrollView {
Text("EULA", comment: "Label indicating that the below text is the EULA, an acronym for End User License Agreement.")
.font(.title.bold())
.foregroundColor(.white)
Text(Markdown.parse(content: """
End User License Agreement
## Introduction
This End User License Agreement ("EULA") is a legal agreement between you and Damus Nostr Inc. for the use of our mobile application Damus. By installing, accessing, or using our application, you agree to be bound by the terms and conditions of this EULA.
## Prohibited Content and Conduct
You agree not to use our application to create, upload, post, send, or store any content that:
* Is illegal, infringing, or fraudulent
* Is defamatory, libelous, or threatening
* Is pornographic, obscene, or offensive
* Is discriminatory or promotes hate speech
* Is harmful to minors
* Is intended to harass or bully others
* Is intended to impersonate others
## You also agree not to engage in any conduct that:
* Harasses or bullies others
* Impersonates others
* Is intended to intimidate or threaten others
* Is intended to promote or incite violence
## Consequences of Violation
Any violation of this EULA, including the prohibited content and conduct outlined above, may result in the termination of your access to our application.
## Disclaimer of Warranties and Limitation of Liability
Our application is provided "as is" and "as available" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. We do not guarantee that our application will be uninterrupted or error-free. In no event shall Damus Nostr Inc. be liable for any damages whatsoever, including but not limited to direct, indirect, special, incidental, or consequential damages, arising out of or in connection with the use or inability to use our application.
## Changes to EULA
We reserve the right to update or modify this EULA at any time and without prior notice. Your continued use of our application following any changes to this EULA will be deemed to be your acceptance of such changes.
## Contact Information
If you have any questions about this EULA, please contact us at damus@jb55.com
## Acceptance of Terms
By using our Application, you signify your acceptance of this EULA. If you do not agree to this EULA, you may not use our Application.
"""))
.padding()
if state == .create_account {
NavigationLink(destination: CreateAccountView(), isActive: $accepted) {
EmptyView()
}
} else {
NavigationLink(destination: LoginView(), isActive: $accepted) {
EmptyView()
}
}
DamusWhiteButton(NSLocalizedString("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.")) {
accepted = true
}
DamusWhiteButton(NSLocalizedString("Reject", comment: "Button to reject the end user license agreement, which disallows the user from being let into the app.")) {
dismiss()
}
}
.padding()
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
.foregroundColor(.white)
}
}
struct EULAView_Previews: PreviewProvider {
static var previews: some View {
EULAView()
}
}
+1 -1
View File
@@ -196,7 +196,7 @@ struct EditMetadataView: View {
if let parts = nip05_parts {
Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.")
} else {
Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
Text("'\(nip05)' is an invalid nip05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
}
})
+2 -2
View File
@@ -71,8 +71,8 @@ struct EventDetailView: View {
}
toggle_thread_view()
}
case .event(let ev, _):
EventView(damus: damus, event: ev, has_action_bar: true)
case .event(let ev, let highlight):
EventView(event: ev, highlight: highlight, has_action_bar: true, damus: damus, show_friend_icon: true)
.onTapGesture {
if thread.initial_event.id == ev.id {
toggle_thread_view()
+249 -49
View File
@@ -8,9 +8,38 @@
import Foundation
import SwiftUI
enum Highlight {
case none
case main
case reply
case custom(Color, Float)
var is_main: Bool {
if case .main = self {
return true
}
return false
}
var is_none: Bool {
if case .none = self {
return true
}
return false
}
var is_replied_to: Bool {
switch self {
case .reply: return true
default: return false
}
}
}
enum EventViewKind {
case small
case normal
case big
case selected
}
@@ -20,40 +49,121 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
return .body
case .normal:
return .body
case .big:
return .headline
case .selected:
return .custom("selected", size: 21.0)
}
}
struct BuilderEventView: View {
let damus: DamusState
let event_id: String
@State var event: NostrEvent?
@State var subscription_uuid: String = UUID().description
func unsubscribe() {
damus.pool.unsubscribe(sub_id: subscription_uuid)
}
func subscribe(filters: [NostrFilter]) {
damus.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
damus.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
}
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nostr_response) = ev else {
return
}
guard case .event(let id, let nostr_event) = nostr_response else {
return
}
// Is current event
if id == subscription_uuid {
if event != nil {
return
}
event = nostr_event
unsubscribe()
}
}
func load() {
subscribe(filters: [
NostrFilter(
ids: [self.event_id],
limit: 1
)
])
}
var body: some View {
VStack {
if let event = event {
let ev = event.inner_event ?? event
NavigationLink(destination: BuildThreadV2View(damus: damus, event_id: ev.id)) {
EventView(damus: damus, event: event, show_friend_icon: true, size: .small, embedded: true)
}.buttonStyle(.plain)
} else {
ProgressView().padding()
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.border(Color.gray.opacity(0.2), width: 1)
.cornerRadius(2)
.onAppear {
self.load()
}
}
}
struct EventView: View {
let event: NostrEvent
let highlight: Highlight
let has_action_bar: Bool
let damus: DamusState
let pubkey: String
let show_friend_icon: Bool
let size: EventViewKind
let embedded: Bool
@EnvironmentObject var action_bar: ActionBarModel
init(damus: DamusState, event: NostrEvent, has_action_bar: Bool) {
init(event: NostrEvent, highlight: Highlight, has_action_bar: Bool, damus: DamusState, show_friend_icon: Bool, size: EventViewKind = .normal, embedded: Bool = false) {
self.event = event
self.highlight = highlight
self.has_action_bar = has_action_bar
self.damus = damus
self.pubkey = event.pubkey
self.show_friend_icon = show_friend_icon
self.size = size
self.embedded = embedded
}
init(damus: DamusState, event: NostrEvent) {
init(damus: DamusState, event: NostrEvent, show_friend_icon: Bool, size: EventViewKind = .normal, embedded: Bool = false) {
self.event = event
self.highlight = .none
self.has_action_bar = false
self.damus = damus
self.pubkey = event.pubkey
self.show_friend_icon = show_friend_icon
self.size = size
self.embedded = embedded
}
init(damus: DamusState, event: NostrEvent, pubkey: String) {
init(damus: DamusState, event: NostrEvent, pubkey: String, show_friend_icon: Bool, size: EventViewKind = .normal, embedded: Bool = false) {
self.event = event
self.highlight = .none
self.has_action_bar = false
self.damus = damus
self.pubkey = pubkey
self.show_friend_icon = show_friend_icon
self.size = size
self.embedded = embedded
}
var body: some View {
@@ -80,47 +190,85 @@ struct EventView: View {
}
func TextEvent(_ event: NostrEvent, pubkey: String, booster_pubkey: String? = nil) -> some View {
let content = event.get_content(damus.keypair.privkey)
return HStack(alignment: .top) {
let profile = damus.profiles.lookup(id: pubkey)
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles)
if size != .selected {
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
if !embedded {
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, profiles: damus.profiles)
}
}
Spacer()
}
Spacer()
}
VStack(alignment: .leading) {
HStack(alignment: .center) {
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
if size == .selected {
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, profiles: damus.profiles)
}
}
}
Text("\(format_relative_time(event.created_at))")
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: show_friend_icon, size: size)
if size != .selected {
Text("\(format_relative_time(event.created_at))")
.font(eventviewsize_to_font(size))
.foregroundColor(.gray)
}
}
if event.is_reply(damus.keypair.privkey) {
Text("\(reply_desc(profiles: damus.profiles, event: event))")
.font(.footnote)
.foregroundColor(.gray)
Spacer()
}
EventBody(damus_state: damus, event: event, size: .normal)
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
}
if has_action_bar {
Rectangle().frame(height: 2).opacity(0)
let bar = make_actionbar_model(ev: event, damus: damus)
EventActionBar(damus_state: damus, event: event, bar: bar)
.padding([.top], 4)
.frame(maxWidth: .infinity, alignment: .leading)
}
Divider()
.padding([.top], 4)
let should_show_img = should_show_images(contacts: damus.contacts, ev: event, our_pubkey: damus.pubkey, booster_pubkey: booster_pubkey)
NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, previews: damus.previews, show_images: should_show_img, artifacts: .just_content(content), size: self.size)
.frame(maxWidth: .infinity, alignment: .leading)
.allowsHitTesting(!embedded)
if !embedded {
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
}
if has_action_bar {
if size == .selected {
Text("\(format_date(event.created_at))")
.padding(.top, 10)
.font(.footnote)
.foregroundColor(.gray)
Divider()
.padding([.bottom], 4)
} else {
Rectangle().frame(height: 2).opacity(0)
}
let bar = make_actionbar_model(ev: event, damus: damus)
EventActionBar(damus_state: damus, event: event, bar: bar)
}
Divider()
.padding([.top], 4)
}
}
.padding([.leading], 2)
}
@@ -129,7 +277,7 @@ struct EventView: View {
.id(event.id)
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
.padding([.bottom], 2)
.event_context_menu(event, keypair: damus.keypair, target_pubkey: pubkey)
.event_context_menu(event, pubkey: pubkey, privkey: damus.keypair.privkey)
}
}
@@ -171,9 +319,37 @@ extension View {
}
}
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String) -> some View {
func event_context_menu(_ event: NostrEvent, pubkey: String, privkey: String?) -> some View {
return self.contextMenu {
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey)
Button {
UIPasteboard.general.string = event.get_content(privkey)
} label: {
Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
}
Button {
UIPasteboard.general.string = bech32_pubkey(pubkey) ?? pubkey
} label: {
Label(NSLocalizedString("Copy User ID", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
}
Button {
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
} label: {
Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
}
Button {
UIPasteboard.general.string = event_to_json(ev: event)
} label: {
Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "j.square.on.square")
}
Button {
NotificationCenter.default.post(name: .broadcast_event, object: event)
} label: {
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
}
}
}
@@ -193,6 +369,33 @@ func format_date(_ created_at: Int64) -> String {
}
func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
let desc = make_reply_description(event.tags)
let pubkeys = desc.pubkeys
let n = desc.others
if desc.pubkeys.count == 0 {
return NSLocalizedString("Reply to self", comment: "Label to indicate that the user is replying to themself.")
}
let names: [String] = pubkeys.map {
let prof = profiles.lookup(id: $0)
return Profile.displayName(profile: prof, pubkey: $0)
}
if names.count == 2 {
if n > 2 {
let othersCount = n - pubkeys.count
return String(format: NSLocalizedString("replying_to_two_and_others", comment: "Label to indicate that the user is replying to 2 users and others."), names[0], names[1], othersCount)
}
return String(format: NSLocalizedString("Replying to %@ & %@", comment: "Label to indicate that the user is replying to 2 users."), names[0], names[1])
}
let othersCount = n - pubkeys.count
return String(format: NSLocalizedString("replying_to_one_and_others", comment: "Label to indicate that the user is replying to 1 user and others."), names[0], othersCount)
}
func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
let likes = damus.likes.counts[ev.id]
@@ -215,25 +418,22 @@ func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
struct EventView_Previews: PreviewProvider {
static var previews: some View {
VStack {
/*
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .small)
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .normal)
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .big)
*/
EventView(
event: NostrEvent(
content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool",
pubkey: "pk",
createdAt: Int64(Date().timeIntervalSince1970 - 100)
),
highlight: .none,
has_action_bar: true,
damus: test_damus_state(),
event: test_event,
has_action_bar: true
show_friend_icon: true,
size: .selected
)
}
.padding()
}
}
let test_event =
NostrEvent(
content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jpg cool",
pubkey: "pk",
createdAt: Int64(Date().timeIntervalSince1970 - 100)
)
-81
View File
@@ -1,81 +0,0 @@
//
// BuilderEventView.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import SwiftUI
struct BuilderEventView: View {
let damus: DamusState
let event_id: String
@State var event: NostrEvent?
@State var subscription_uuid: String = UUID().description
func unsubscribe() {
damus.pool.unsubscribe(sub_id: subscription_uuid)
}
func subscribe(filters: [NostrFilter]) {
damus.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
damus.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
}
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nostr_response) = ev else {
return
}
guard case .event(let id, let nostr_event) = nostr_response else {
return
}
// Is current event
if id == subscription_uuid {
if event != nil {
return
}
event = nostr_event
unsubscribe()
}
}
func load() {
subscribe(filters: [
NostrFilter(
ids: [self.event_id],
limit: 1
)
])
}
var body: some View {
VStack {
if let event = event {
let ev = event.inner_event ?? event
NavigationLink(destination: BuildThreadV2View(damus: damus, event_id: ev.id)) {
EmbeddedEventView(damus_state: damus, event: event)
.padding(8)
}.buttonStyle(.plain)
} else {
ProgressView().padding()
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(8)
.border(Color.gray.opacity(0.2), width: 1)
.onAppear {
self.load()
}
}
}
struct BuilderEventView_Previews: PreviewProvider {
static var previews: some View {
BuilderEventView(damus: test_damus_state(), event_id: "536bee9e83c818e3b82c101935128ae27a0d4290039aaf253efe5f09232c1962")
}
}
@@ -1,35 +0,0 @@
//
// EmbeddedEventView.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import SwiftUI
struct EmbeddedEventView: View {
let damus_state: DamusState
let event: NostrEvent
var pubkey: String {
event.pubkey
}
var body: some View {
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
EventProfile(damus_state: damus_state, pubkey: pubkey, profile: profile, size: .small)
EventBody(damus_state: damus_state, event: event, size: .small)
}
.event_context_menu(event, keypair: damus_state.keypair, target_pubkey: pubkey)
}
}
struct EmbeddedEventView_Previews: PreviewProvider {
static var previews: some View {
EmbeddedEventView(damus_state: test_damus_state(), event: test_event)
.padding()
}
}
-35
View File
@@ -1,35 +0,0 @@
//
// EventBody.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import SwiftUI
struct EventBody: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
var content: String {
event.get_content(damus_state.keypair.privkey)
}
var body: some View {
if event_is_reply(event, privkey: damus_state.keypair.privkey) {
ReplyDescription(event: event, profiles: damus_state.profiles)
}
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil)
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_img, artifacts: .just_content(content), size: size)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct EventBody_Previews: PreviewProvider {
static var previews: some View {
EventBody(damus_state: test_damus_state(), event: test_event, size: .normal)
}
}
-102
View File
@@ -1,102 +0,0 @@
//
// EventMenu.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import SwiftUI
struct EventMenuContext: View {
let event: NostrEvent
let keypair: Keypair
let target_pubkey: String
var body: some View {
Button {
UIPasteboard.general.string = event.get_content(keypair.privkey)
} label: {
Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
}
Button {
UIPasteboard.general.string = bech32_pubkey(target_pubkey)
} label: {
Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
}
Button {
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
} label: {
Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
}
Button {
UIPasteboard.general.string = event_to_json(ev: event)
} label: {
Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "square.on.square")
}
Button {
NotificationCenter.default.post(name: .broadcast_event, object: event)
} label: {
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
Button(role: .destructive) {
let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))
notify(.report, target)
} label: {
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
}
Button(role: .destructive) {
notify(.block, target_pubkey)
} label: {
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
}
}
}
}
/*
struct EventMenu: UIViewRepresentable {
typealias UIViewType = UIButton
let saveAction = UIAction(title: "") { action in }
let saveMenu = UIMenu(title: "", children: [
UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
//code action for menu item
},
UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
//code action for menu item
},
UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
//code action for menu item
},
])
func makeUIView(context: Context) -> UIButton {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
button.showsMenuAsPrimaryAction = true
button.menu = saveMenu
return button
}
func updateUIView(_ uiView: UIButton, context: Context) {
uiView.setImage(UIImage(systemName: "plus"), for: .normal)
}
}
struct EventMenu_Previews: PreviewProvider {
static var previews: some View {
EventMenu(event: test_event, privkey: nil, pubkey: test_event.pubkey)
}
}
*/
-51
View File
@@ -1,51 +0,0 @@
//
// EventProfile.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import SwiftUI
func eventview_pfp_size(_ size: EventViewKind) -> CGFloat {
switch size {
case .small:
return PFP_SIZE * 0.5
case .normal:
return PFP_SIZE
case .selected:
return PFP_SIZE
}
}
struct EventProfile: View {
let damus_state: DamusState
let pubkey: String
let profile: Profile?
let size: EventViewKind
var pfp_size: CGFloat {
eventview_pfp_size(size)
}
var body: some View {
HStack(alignment: .center) {
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: FollowersModel(damus_state: damus_state, target: pubkey))
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles)
}
}
EventProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: true, size: size)
}
}
}
struct EventProfile_Previews: PreviewProvider {
static var previews: some View {
EventProfile(damus_state: test_damus_state(), pubkey: "pk", profile: nil, size: .normal)
}
}
-112
View File
@@ -1,112 +0,0 @@
//
// MutedEventView.swift
// damus
//
// Created by William Casarin on 2023-01-27.
//
import SwiftUI
struct MutedEventView: View {
let damus_state: DamusState
let event: NostrEvent
let scroller: ScrollViewProxy?
let selected: Bool
@Binding var nav_target: String?
@Binding var navigating: Bool
@State var shown: Bool
@Environment(\.colorScheme) var colorScheme
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, nav_target: Binding<String?>, navigating: Binding<Bool>, selected: Bool) {
self.damus_state = damus_state
self.event = event
self.scroller = scroller
self.selected = selected
self._nav_target = nav_target
self._navigating = navigating
self._shown = State(initialValue: !should_hide_event(contacts: damus_state.contacts, ev: event))
}
var should_mute: Bool {
return should_hide_event(contacts: damus_state.contacts, ev: event)
}
var FillColor: Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
var MutedBox: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(FillColor)
HStack {
Text("Post from a user you've blocked", comment: "Text to indicate that what is being shown is a post from a user who has been blocked.")
Spacer()
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a post from a user who has been blocked.") : NSLocalizedString("Show", comment: "Button to show a post from a user who has been blocked.")) {
shown.toggle()
}
}
.padding(10)
}
}
var Event: some View {
Group {
if selected {
SelectedEventView(damus: damus_state, event: event)
} else {
EventView(damus: damus_state, event: event, has_action_bar: true)
.onTapGesture {
nav_target = event.id
navigating = true
}
.onAppear {
// TODO: find another solution to prevent layout shifting and layout blocking on large responses
scroller?.scrollTo("main", anchor: .bottom)
}
}
}
}
var body: some View {
Group {
if should_mute {
MutedBox
}
if shown {
Event
}
}
.onReceive(handle_notify(.new_mutes)) { notif in
guard let mutes = notif.object as? [String] else {
return
}
if mutes.contains(event.pubkey) {
shown = false
}
}
.onReceive(handle_notify(.new_unmutes)) { notif in
guard let unmutes = notif.object as? [String] else {
return
}
if unmutes.contains(event.pubkey) {
shown = true
}
}
}
}
struct MutedEventView_Previews: PreviewProvider {
@State static var nav_target: String? = nil
@State static var navigating: Bool = false
static var previews: some View {
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, nav_target: $nav_target, navigating: $navigating, selected: false)
.frame(width: .infinity, height: 50)
}
}
-55
View File
@@ -1,55 +0,0 @@
//
// ReplyDescription.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import SwiftUI
// jb55 - TODO: this could be a lot better
struct ReplyDescription: View {
let event: NostrEvent
let profiles: Profiles
var body: some View {
Text("\(reply_desc(profiles: profiles, event: event))")
.font(.footnote)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct ReplyDescription_Previews: PreviewProvider {
static var previews: some View {
ReplyDescription(event: test_event, profiles: test_damus_state().profiles)
}
}
func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
let desc = make_reply_description(event.tags)
let pubkeys = desc.pubkeys
let n = desc.others
if desc.pubkeys.count == 0 {
return NSLocalizedString("Reply to self", comment: "Label to indicate that the user is replying to themself.")
}
let names: [String] = pubkeys.map {
let prof = profiles.lookup(id: $0)
return Profile.displayName(profile: prof, pubkey: $0)
}
if names.count == 2 {
if n > 2 {
let othersCount = n - pubkeys.count
return String(format: NSLocalizedString("replying_to_two_and_others", comment: "Label to indicate that the user is replying to 2 users and others."), names[0], names[1], othersCount)
}
return String(format: NSLocalizedString("Replying to %@ & %@", comment: "Label to indicate that the user is replying to 2 users."), names[0], names[1])
}
let othersCount = n - pubkeys.count
return String(format: NSLocalizedString("replying_to_one_and_others", comment: "Label to indicate that the user is replying to 1 user and others."), names[0], othersCount)
}
@@ -1,62 +0,0 @@
//
// SelectedEventView.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import SwiftUI
struct SelectedEventView: View {
let damus: DamusState
let event: NostrEvent
var pubkey: String {
event.pubkey
}
var body: some View {
HStack(alignment: .top) {
let profile = damus.profiles.lookup(id: pubkey)
VStack(alignment: .leading) {
EventProfile(damus_state: damus, pubkey: pubkey, profile: profile, size: .normal)
EventBody(damus_state: damus, event: event, size: .selected)
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
}
Text("\(format_date(event.created_at))")
.padding(.top, 10)
.font(.footnote)
.foregroundColor(.gray)
Divider()
.padding([.bottom], 4)
let bar = make_actionbar_model(ev: event, damus: damus)
if !bar.is_empty {
EventDetailBar(state: damus, target: event.id, bar: bar)
Divider()
}
EventActionBar(damus_state: damus, event: event, bar: bar)
.padding([.top], 4)
Divider()
.padding([.top], 4)
}
.padding([.leading], 2)
.event_context_menu(event, keypair: damus.keypair, target_pubkey: event.pubkey)
}
}
}
struct SelectedEventView_Previews: PreviewProvider {
static var previews: some View {
SelectedEventView(damus: test_damus_state(), event: test_event)
.padding()
}
}
+20 -1
View File
@@ -15,7 +15,26 @@ struct FollowUserView: View {
var body: some View {
HStack {
UserView(damus_state: damus_state, pubkey: target.pubkey)
let pmodel = ProfileModel(pubkey: target.pubkey, damus: damus_state)
let followers = FollowersModel(damus_state: damus_state, target: target.pubkey)
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
NavigationLink(destination: pv) {
ProfilePicView(pubkey: target.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: target.pubkey)
ProfileName(pubkey: target.pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(FollowUserView.markdown.process(about))
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
}
.buttonStyle(PlainButtonStyle())
FollowButtonView(target: target, follow_state: damus_state.contacts.follow_state(target.pubkey))
}
-67
View File
@@ -1,67 +0,0 @@
//
// MutelistView.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import SwiftUI
struct MutelistView: View {
let damus_state: DamusState
@State var users: [String]
func RemoveAction(pubkey: String) -> some View {
Button {
guard let mutelist = damus_state.contacts.mutelist else {
return
}
guard let keypair = damus_state.keypair.to_full() else {
return
}
guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: pubkey) else {
return
}
damus_state.contacts.set_mutelist(new_ev)
damus_state.pool.send(.event(new_ev))
users = get_mutelist_users(new_ev)
} label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash")
}
.tint(.red)
}
var body: some View {
List(users, id: \.self) { pubkey in
UserView(damus_state: damus_state, pubkey: pubkey)
.id(pubkey)
.swipeActions {
RemoveAction(pubkey: pubkey)
}
}
.navigationTitle(NSLocalizedString("Blocked Users", comment: "Navigation title of view to see list of blocked users."))
}
}
func get_mutelist_users(_ mlist: NostrEvent?) -> [String] {
guard let mutelist = mlist else {
return []
}
return mutelist.tags.reduce(into: Array<String>()) { pks, tag in
if tag.count >= 2 && tag[0] == "p" {
pks.append(tag[1])
}
}
}
struct MutelistView_Previews: PreviewProvider {
static var previews: some View {
MutelistView(damus_state: test_damus_state(), users: [test_event.pubkey, test_event.pubkey+"hi"])
}
}
+16 -212
View File
@@ -7,44 +7,36 @@
import SwiftUI
import LinkPresentation
import NaturalLanguage
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct NoteArtifacts {
let content: AttributedString
let content: String
let images: [URL]
let invoices: [Invoice]
let links: [URL]
static func just_content(_ content: String) -> NoteArtifacts {
NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
NoteArtifacts(content: content, images: [], invoices: [], links: [])
}
}
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
let blocks = ev.blocks(privkey)
return render_blocks(blocks: blocks, profiles: profiles, privkey: privkey)
}
func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> NoteArtifacts {
var invoices: [Invoice] = []
var img_urls: [URL] = []
var link_urls: [URL] = []
let txt: AttributedString = blocks.reduce("") { str, block in
let txt = blocks.reduce("") { str, block in
switch block {
case .mention(let m):
return str + mention_str(m, profiles: profiles)
case .text(let txt):
return str + AttributedString(stringLiteral: txt)
return str + txt
case .hashtag(let htag):
return str + hashtag_str(htag)
case .invoice(let invoice):
invoices.append(invoice)
return str
case .url(let url):
// Handle Image URLs
if is_image_url(url) {
// Append Image
@@ -52,11 +44,11 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
return str
} else {
link_urls.append(url)
return str + url_str(url)
return str + url.absoluteString
}
}
}
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
}
@@ -73,59 +65,17 @@ struct NoteContentView: View {
let show_images: Bool
@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
@State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable? = nil
let size: EventViewKind
@EnvironmentObject var user_settings: UserSettingsStore
var TranslateButton: some View {
Group {
let languageName = Locale.current.localizedString(forLanguageCode: noteLanguage!)
if show_translated_note {
if checkingTranslationStatus {
Button(NSLocalizedString("Translating from \(languageName!)...", 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()
} else if translated_artifacts != nil {
Button(NSLocalizedString("Translated from \(languageName!)", comment: "Button to indicate that the note has been translated from a different language.")) {
show_translated_note = false
}
.translate_button_style()
Text(translated_artifacts!.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
}
} else {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
}
.translate_button_style()
}
}
}
func MainContent() -> some View {
return VStack(alignment: .leading) {
Text(artifacts.content)
Text(Markdown.parse(content: artifacts.content))
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
if size == .selected && noteLanguage != nil && noteLanguage != currentLanguage {
TranslateButton
}
if show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
@@ -193,54 +143,10 @@ struct NoteContentView: View {
previews.store(evid: self.event.id, preview: view)
self.preview = view
}
if size == .selected && noteLanguage == nil && !checkingTranslationStatus && user_settings.libretranslate_url != "" {
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.
noteLanguage = NLLanguageRecognizer.dominantLanguage(for: event.content)?.rawValue ?? currentLanguage
if noteLanguage != currentLanguage {
// If the detected dominant language is a variant, remove the variant component and just take the language part as LibreTranslate typically only supports the variant-less language.
if #available(iOS 16, *) {
noteLanguage = Locale.LanguageCode(stringLiteral: noteLanguage!).identifier(.alpha2)
} else {
noteLanguage = Locale.canonicalLanguageIdentifier(from: noteLanguage!)
}
}
if noteLanguage == nil {
noteLanguage = currentLanguage
translated_note = nil
} else if noteLanguage != currentLanguage {
do {
// If the note language is different from our language, send a translation request.
let translator = Translator(user_settings.libretranslate_url, apiKey: user_settings.libretranslate_api_key)
translated_note = try await translator.translate(event.content, from: noteLanguage!, to: currentLanguage)
} 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 translated_note != nil {
// Render translated note.
let blocks = event.get_blocks(content: translated_note!)
translated_artifacts = render_blocks(blocks: blocks, profiles: profiles, privkey: privkey)
}
checkingTranslationStatus = false
}
}
}
func getMetaData(for url: URL) async -> LPLinkMetadata? {
// iOS 15 is crashing for some reason
guard #available(iOS 16, *) else {
@@ -257,112 +163,20 @@ struct NoteContentView: View {
}
}
func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "nostr:t:\(htag)")
attributedString.foregroundColor = .purple
return attributedString
}
func hashtag_str(_ htag: String) -> String {
return "[#\(htag)](nostr:t:\(htag))"
}
func url_str(_ url: URL) -> AttributedString {
var attributedString = AttributedString(stringLiteral: url.absoluteString)
attributedString.link = url
attributedString.foregroundColor = .purple
return attributedString
}
func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
func mention_str(_ m: Mention, profiles: Profiles) -> String {
switch m.type {
case .pubkey:
let pk = m.ref.ref_id
let profile = profiles.lookup(id: pk)
let disp = Profile.displayName(profile: profile, pubkey: pk)
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "nostr:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
return "[@\(disp)](nostr:\(encode_pubkey_uri(m.ref)))"
case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "nostr:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
}
}
public struct Translator {
private let url: String
private let apiKey: String?
private let session = URLSession.shared
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
public init(_ url: String, apiKey: String? = nil) {
self.url = url
self.apiKey = apiKey
}
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String {
let url = try makeURL(path: "/translate")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
struct RequestBody: Encodable {
let q: String
let source: String
let target: String
let api_key: String?
}
let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: apiKey)
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
return response.translatedText
}
private func makeURL(path: String) throws -> URL {
guard var components = URLComponents(string: url) else {
throw URLError(.badURL)
}
components.path = path
guard let url = components.url else {
throw URLError(.badURL)
}
return url
}
private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output {
let data = try await session.data(for: request)
let result = try decoder.decode(Output.self, from: data)
return result
}
}
private extension URLSession {
func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask?
let onCancel = { task?.cancel() }
return try await withTaskCancellationHandler(
operation: {
try await withCheckedThrowingContinuation { continuation in
task = dataTask(with: request) { data, _, error in
guard let data = data else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: data)
}
task?.resume()
}
},
onCancel: { onCancel() }
)
return "[@\(abbrev_pubkey(bevid))](nostr:\(encode_event_id_uri(m.ref)))"
}
}
@@ -371,17 +185,7 @@ struct NoteContentView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
let artifacts = NoteArtifacts(content: content, images: [], invoices: [], links: [])
NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, previews: PreviewCache(), show_images: true, artifacts: artifacts, size: .normal)
}
}
extension View {
func translate_button_style() -> some View {
return self
.font(.footnote)
.contentShape(Rectangle())
.padding([.top, .bottom], 10)
}
}
-79
View File
@@ -1,79 +0,0 @@
//
// ParicipantsView.swift
// damus
//
// Created by Joel Klabo on 1/18/23.
//
import SwiftUI
struct ParticipantsView: View {
let damus_state: DamusState
@Binding var references: [ReferencedId]
@Binding var originalReferences: [ReferencedId]
var body: some View {
VStack {
Text("Edit participants", comment: "Text indicating that the view is used for editing which participants are replied to in a note.")
HStack {
Spacer()
Button {
// Remove all "p" refs, keep "e" refs
references = originalReferences.eRefs
} label: {
Text("Remove all", comment: "Button label to remove all participants from a note reply.")
}
.buttonStyle(.borderedProminent)
Spacer()
Button {
references = originalReferences
} label: {
Text("Add all", comment: "Button label to re-add all original participants as profiles to reply to in a note")
}
.buttonStyle(.borderedProminent)
Spacer()
}
VStack {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(FollowUserView.markdown.process(about))
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? .purple : .gray)
}
.onTapGesture {
if references.contains(participant) {
references = references.filter {
$0 != participant
}
} else {
if references.contains(participant) {
// Don't add it twice
} else {
references.append(participant)
}
}
}
}
}
Spacer()
}
.padding()
}
}
+2 -32
View File
@@ -16,11 +16,10 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
struct PostView: View {
@State var post: String = ""
@FocusState var focus: Bool
let replying_to: NostrEvent?
@FocusState var focus: Bool
let references: [ReferencedId]
let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode
@@ -75,7 +74,6 @@ struct PostView: View {
TextEditor(text: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
if post.isEmpty {
Text(POST_PLACEHOLDER)
.padding(.top, 8)
@@ -84,14 +82,6 @@ struct PostView: View {
.allowsHitTesting(false)
}
}
// This if-block observes @ for tagging
if let searching = get_searching_string(post) {
VStack {
Spacer()
UserSearch(damus_state: damus_state, search: searching, post: $post)
}.zIndex(1)
}
}
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -102,23 +92,3 @@ struct PostView: View {
}
}
func get_searching_string(_ post: String) -> String? {
guard let last_word = post.components(separatedBy: .whitespacesAndNewlines).last else {
return nil
}
guard last_word.count >= 2 else {
return nil
}
guard last_word.first! == "@" else {
return nil
}
// don't include @npub... strings
guard last_word.count != 64 else {
return nil
}
return String(last_word.dropFirst())
}
-87
View File
@@ -1,87 +0,0 @@
//
// UserAutocompletion.swift
// damus
//
// Created by William Casarin on 2023-01-28.
//
import SwiftUI
struct SearchedUser: Identifiable {
let petname: String?
let profile: Profile?
let pubkey: String
var id: String {
return pubkey
}
}
struct UserSearch: View {
let damus_state: DamusState
let search: String
@Binding var post: String
var users: [SearchedUser] {
guard let contacts = damus_state.contacts.event else {
return []
}
return search_users(profiles: damus_state.profiles, tags: contacts.tags, search: search)
}
var body: some View {
ScrollView {
LazyVStack {
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.onTapGesture {
guard let pk = bech32_pubkey(user.pubkey) else {
return
}
post = post.replacingOccurrences(of: "@"+search, with: "@"+pk+" ")
}
}
}
}
}
}
struct UserSearch_Previews: PreviewProvider {
static let search: String = "jb55"
@State static var post: String = "some @jb55"
static var previews: some View {
UserSearch(damus_state: test_damus_state(), search: search, post: $post)
}
}
func search_users(profiles: Profiles, tags: [[String]], search: String) -> [SearchedUser] {
var seen_user = Set<String>()
return tags.reduce(into: Array<SearchedUser>()) { arr, tag in
guard tag.count >= 2 && tag[0] == "p" else {
return
}
let pubkey = tag[1]
guard !seen_user.contains(pubkey) else {
return
}
seen_user.insert(pubkey)
var petname: String? = nil
if tag.count >= 4 {
petname = tag[3]
}
let profile = profiles.lookup(id: pubkey)
guard ((petname?.hasPrefix(search) ?? false) || (profile?.name?.hasPrefix(search) ?? false)) else {
return
}
let searched_user = SearchedUser(petname: petname, profile: profile, pubkey: pubkey)
arr.append(searched_user)
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ struct InnerProfilePicView: View {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: fallbackUrl,
maxByteSize: 5_242_880, // 5Mib
maxByteSize: 1000000,
downsampleSize: CGSize(width: 200, height: 200)
)
}
+19 -82
View File
@@ -91,8 +91,6 @@ struct EditButton: View {
RoundedRectangle(cornerRadius: 24)
.stroke(borderColor(), lineWidth: 1)
}
.minimumScaleFactor(0.5)
.lineLimit(1)
}
}
@@ -116,13 +114,11 @@ struct ProfileView: View {
@State var showing_select_wallet: Bool = false
@State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false
@State var action_sheet_presented: Bool = false
@EnvironmentObject var user_settings: UserSettingsStore
@StateObject var user_settings = UserSettingsStore()
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) var openURL
// We just want to have a white "< Home" text here, however,
// setting the initialiser is causing issues, and it's late.
// Ref: https://blog.techchee.com/navigation-bar-title-style-color-and-custom-back-button-in-swiftui/
@@ -150,7 +146,9 @@ struct ProfileView: View {
}
}) {
Image(systemName: "bolt.circle")
.profile_button_style(scheme: colorScheme)
.symbolRenderingMode(.palette)
.foregroundStyle(colorScheme == .dark ? .white : .black, colorScheme == .dark ? .white : .black)
.font(.system(size: 32).weight(.thin))
.contextMenu {
Button {
UIPasteboard.general.string = profile.lnurl ?? ""
@@ -169,39 +167,15 @@ struct ProfileView: View {
static let markdown = Markdown()
var ActionSheetButton: some View {
Button(action: {
action_sheet_presented = true
}) {
Image(systemName: "ellipsis.circle")
.profile_button_style(scheme: colorScheme)
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
let target: ReportTarget = .user(profile.pubkey)
notify(.report, target)
}
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.block, profile.pubkey)
}
}
}
}
var ShareButton: some View {
Button(action: {
show_share_sheet = true
}) {
Image(systemName: "square.and.arrow.up.circle")
.profile_button_style(scheme: colorScheme)
Image(systemName: "square.and.arrow.up.circle.fill")
.symbolRenderingMode(.palette)
.font(.system(size: 32))
.padding()
.foregroundStyle(.white, .black, .black.opacity(0.8))
}
}
@@ -211,48 +185,24 @@ struct ProfileView: View {
.environmentObject(dm_model)
return NavigationLink(destination: dmview) {
Image(systemName: "bubble.left.circle")
.profile_button_style(scheme: colorScheme)
.symbolRenderingMode(.palette)
.font(.system(size: 32).weight(.thin))
.foregroundStyle(colorScheme == .dark ? .white : .black, colorScheme == .dark ? .white : .black)
}
}
private func getScrollOffset(_ geometry: GeometryProxy) -> CGFloat {
geometry.frame(in: .global).minY
}
private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
let imageHeight = 150.0
if offset > 0 {
return imageHeight + offset
}
return imageHeight
}
private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat {
let offset = getScrollOffset(geometry)
// Image was pulled down
if offset > 0 {
return -offset
}
return 0
}
var TopSection: some View {
ZStack(alignment: .top) {
GeometryReader { geometry in
GeometryReader { geo in
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles)
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: self.getHeightForHeaderImage(geometry))
.frame(width: geo.size.width, height: BANNER_HEIGHT)
.clipped()
.offset(x: 0, y: self.getOffsetForHeaderImage(geometry))
ShareButton
.offset(x: geo.size.width - 80.0, y: 50.0 )
}.frame(height: BANNER_HEIGHT)
VStack(alignment: .leading, spacing: 8.0) {
VStack(alignment: .leading) {
let data = damus_state.profiles.lookup(id: profile.pubkey)
let pfp_size: CGFloat = 90.0
@@ -268,7 +218,6 @@ struct ProfileView: View {
Spacer()
Group {
ActionSheetButton
if let profile = data {
if let lnurl = profile.lnurl, lnurl != "" {
@@ -301,10 +250,6 @@ struct ProfileView: View {
Text(ProfileView.markdown.process(data?.about ?? ""))
.font(.subheadline)
if let url = data?.website_url {
WebsiteLink(url: url)
}
Divider()
HStack {
@@ -494,11 +439,3 @@ struct KeyView: View {
}
}
}
extension View {
func profile_button_style(scheme: ColorScheme) -> some View {
self.symbolRenderingMode(.palette)
.font(.system(size: 32).weight(.thin))
.foregroundStyle(scheme == .dark ? .white : .black, scheme == .dark ? .white : .black)
}
}
+12 -11
View File
@@ -54,20 +54,23 @@ struct ProfileZoomView: View {
Color("DamusDarkGrey") // Or Color("DamusBlack")
.edgesIgnoringSafeArea(.all)
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.subheadline)
.padding(.leading, 20)
HStack() {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.largeTitle)
.frame(width: 40, height: 40)
.padding(20)
}
}
.zIndex(1)
VStack(alignment: .center) {
Spacer()
.frame(height: 120)
ProfilePicView(pubkey: pubkey, size: 200.0, highlight: .none, profiles: profiles)
.padding(100)
.scaledToFit()
@@ -78,8 +81,6 @@ struct ProfileZoomView: View {
.modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
Spacer()
}
}
-90
View File
@@ -1,90 +0,0 @@
//
// QRCodeView.swift
// damus
//
// Created by eric on 1/27/23.
//
import SwiftUI
import CoreImage.CIFilterBuiltins
struct QRCodeView: View {
let damus_state: DamusState
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
var maybe_key: String? {
guard let key = bech32_pubkey(damus_state.pubkey) else {
return nil
}
return key
}
var body: some View {
ZStack(alignment: .topLeading) {
DamusGradient()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.subheadline)
.padding(.leading, 20)
}
.zIndex(1)
VStack(alignment: .center) {
Spacer()
if let key = maybe_key {
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.padding()
Text(key)
.font(.headline)
.foregroundColor(Color(.white))
.padding()
}
Spacer()
}
}
.modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
}
func generateQRCode(pubkey: String) -> UIImage {
let data = pubkey.data(using: String.Encoding.ascii)
let qrFilter = CIFilter(name: "CIQRCodeGenerator")
qrFilter?.setValue(data, forKey: "inputMessage")
let qrImage = qrFilter?.outputImage
let colorInvertFilter = CIFilter(name: "CIColorInvert")
colorInvertFilter?.setValue(qrImage, forKey: "inputImage")
let outputInvertedImage = colorInvertFilter?.outputImage
let maskToAlphaFilter = CIFilter(name: "CIMaskToAlpha")
maskToAlphaFilter?.setValue(outputInvertedImage, forKey: "inputImage")
let outputCIImage = maskToAlphaFilter?.outputImage
let context = CIContext()
let cgImage = context.createCGImage(outputCIImage!, from: outputCIImage!.extent)!
return UIImage(cgImage: cgImage)
}
}
struct QRCodeView_Previews: PreviewProvider {
static var previews: some View {
QRCodeView(damus_state: test_damus_state())
}
}
-109
View File
@@ -1,109 +0,0 @@
//
// RelayConfigView.swift
// damus
//
// Created by William Casarin on 2023-01-30.
//
import SwiftUI
struct RelayConfigView: View {
let state: DamusState
@State var new_relay: String = ""
@State var show_add_relay: Bool = false
@State var relays: [RelayDescriptor]
init(state: DamusState) {
self.state = state
_relays = State(initialValue: state.pool.descriptors)
}
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { (xs, x) in
if let _ = state.pool.get_relay(x) {
} else {
xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
}
}
}
var body: some View {
MainContent
.onReceive(handle_notify(.relays_changed)) { _ in
self.relays = state.pool.descriptors
}
.sheet(isPresented: $show_add_relay) {
AddRelayView(show_add_relay: $show_add_relay, relay: $new_relay) { m_relay in
guard var relay = m_relay else {
return
}
if relay.starts(with: "wss://") == false && relay.starts(with: "ws://") == false {
relay = "wss://" + relay
}
guard let url = URL(string: relay) else {
return
}
guard let ev = state.contacts.event else {
return
}
guard let privkey = state.keypair.privkey else {
return
}
let info = RelayInfo.rw
guard (try? state.pool.add_relay(url, info: info)) != nil else {
return
}
state.pool.connect(to: [relay])
guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.descriptors, relay: relay, info: info) else {
return
}
process_contact_event(pool: state.pool, contacts: state.contacts, pubkey: state.pubkey, ev: ev)
state.pool.send(.event(new_ev))
}
}
}
var MainContent: some View {
Form {
Section {
List(Array(relays), id: \.url) { relay in
RelayView(state: state, relay: relay.url.absoluteString)
}
} header: {
HStack {
Text("Relays", comment: "Header text for relay server list for configuration.")
Spacer()
Button(action: { show_add_relay = true }) {
Image(systemName: "plus")
.foregroundColor(.accentColor)
}
}
}
if recommended.count > 0 {
Section(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration")) {
List(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.absoluteString)
}
}
}
}
}
}
struct RelayConfigView_Previews: PreviewProvider {
static var previews: some View {
RelayConfigView(state: test_damus_state())
}
}
+5 -19
View File
@@ -18,16 +18,11 @@ struct ReplyView: View {
let replying_to: NostrEvent
let damus: DamusState
@State var originalReferences: [ReferencedId] = []
@State var references: [ReferencedId] = []
@State var participantsShown: Bool = false
var body: some View {
VStack {
Text("Replying to:", comment: "Indicating that the user is replying to the following listed people.")
HStack(alignment: .top) {
let names = references.pRefs
let names = all_referenced_pubkeys(replying_to)
.map { pubkey in
let pk = pubkey.ref_id
let prof = damus.profiles.lookup(id: pk)
@@ -38,22 +33,13 @@ struct ReplyView: View {
.foregroundColor(.gray)
.font(.footnote)
}
.onTapGesture {
participantsShown.toggle()
}
.sheet(isPresented: $participantsShown) {
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
}
ScrollView {
EventView(damus: damus, event: replying_to, has_action_bar: false)
EventView(event: replying_to, highlight: .none, has_action_bar: false, damus: damus, show_friend_icon: true)
}
PostView(replying_to: replying_to, references: references, damus_state: damus)
}
.onAppear {
references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)
originalReferences = references
PostView(replying_to: replying_to, references: gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to))
}
.padding()
}
@@ -61,6 +47,6 @@ struct ReplyView: View {
struct ReplyView_Previews: PreviewProvider {
static var previews: some View {
ReplyView(replying_to: NostrEvent(content: "hi", pubkey: "pubkey"), damus: test_damus_state(), references: [])
ReplyView(replying_to: NostrEvent(content: "hi", pubkey: "pubkey"), damus: test_damus_state())
}
}
-115
View File
@@ -1,115 +0,0 @@
//
// ReportView.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import SwiftUI
struct ReportView: View {
let pool: RelayPool
let target: ReportTarget
let privkey: String
@State var report_sent: Bool = false
@State var report_id: String = ""
var body: some View {
if report_sent {
Success
} else {
MainForm
}
}
var Success: some View {
VStack(alignment: .center, spacing: 20) {
Text("Report sent!", comment: "Message indicating that a report was successfully sent to relay servers.")
.font(.headline)
Text("Relays have been notified and clients will be able to use this information to filter content. Thank you!", comment: "Description of what was done as a result of sending a report to relay servers.")
Text("Report ID:", comment: "Label indicating that the text underneath is the identifier of the report that was sent to relay servers.")
Text(report_id)
Button(NSLocalizedString("Copy Report ID", comment: "Button to copy report ID.")) {
UIPasteboard.general.string = report_id
let g = UIImpactFeedbackGenerator(style: .medium)
g.impactOccurred()
}
}
.padding()
}
func do_send_report(type: ReportType) {
guard let ev = send_report(privkey: privkey, pool: pool, target: target, type: type) else {
return
}
guard let note_id = bech32_note_id(ev.id) else {
return
}
report_sent = true
report_id = note_id
}
var MainForm: some View {
VStack {
Text("Report", comment: "Label indicating that the current view is for the user to report content.")
.font(.headline)
.padding()
Form {
Section(content: {
Button(NSLocalizedString("It's spam", comment: "Button for user to report that the account or content has spam.")) {
do_send_report(type: .spam)
}
Button(NSLocalizedString("Nudity or explicit content", comment: "Button for user to report that the account or content has nudity or explicit content.")) {
do_send_report(type: .explicit)
}
Button(NSLocalizedString("Illegal content", comment: "Button for user to report that the account or content has illegal content.")) {
do_send_report(type: .illegal)
}
if case .user = target {
Button(NSLocalizedString("They are impersonating someone", comment: "Button for user to report that the account is impersonating someone.")) {
do_send_report(type: .impersonation)
}
}
}, header: {
Text("What do you want to report?", comment: "Header text to prompt user what issue they want to report.")
}, footer: {
Text("Your report will be sent to the relays you are connected to", comment: "Footer text to inform user what will happen when the report is submitted.")
})
}
}
}
}
func send_report(privkey: String, pool: RelayPool, target: ReportTarget, type: ReportType) -> NostrEvent? {
let report = Report(type: type, target: target, message: "")
guard let ev = create_report_event(privkey: privkey, report: report) else {
return nil
}
pool.send(.event(ev))
return ev
}
struct ReportView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
VStack {
ReportView(pool: ds.pool, target: ReportTarget.user(""), privkey: "")
ReportView(pool: ds.pool, target: ReportTarget.user(""), privkey: "", report_sent: true, report_id: "report_id")
}
}
}
-24
View File
@@ -1,24 +0,0 @@
//
// RepostView.swift
// damus
//
// Created by Terry Yiu on 1/22/23.
//
import SwiftUI
struct RepostView: View {
let damus_state: DamusState
let repost: NostrEvent
var body: some View {
FollowUserView(target: .pubkey(repost.pubkey), damus_state: damus_state)
}
}
struct RepostView_Previews: PreviewProvider {
static var previews: some View {
RepostView(damus_state: test_damus_state(), repost: NostrEvent(id: "", content: "", pubkey: ""))
}
}
-38
View File
@@ -1,38 +0,0 @@
//
// RepostsView.swift
// damus
//
// Created by Terry Yiu on 1/22/23.
//
import SwiftUI
struct RepostsView: View {
let damus_state: DamusState
@StateObject var model: RepostsModel
var body: some View {
ScrollView {
LazyVStack {
ForEach(model.reposts, id: \.id) { ev in
RepostView(damus_state: damus_state, repost: ev)
}
}
.padding()
}
.navigationBarTitle(NSLocalizedString("Reposts", comment: "Navigation bar title for Reposts view."))
.onAppear {
model.subscribe()
}
.onDisappear {
model.unsubscribe()
}
}
}
struct RepostsView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
RepostsView(damus_state: state, model: RepostsModel(state: state, target: "pubkey"))
}
}
-3
View File
@@ -87,9 +87,6 @@ struct SearchHomeView: View {
.onChange(of: search) { s in
print("search change 1")
}
.onReceive(handle_notify(.new_mutes)) { _ in
self.model.filter_muted()
}
.onAppear {
if model.events.isEmpty {
model.subscribe()
+2 -2
View File
@@ -42,10 +42,10 @@ struct SetupView: View {
DamusGradient()
VStack(alignment: .center) {
NavigationLink(destination: EULAView(state: state), tag: .create_account, selection: $state ) {
NavigationLink(destination: CreateAccountView(), tag: .create_account, selection: $state ) {
EmptyView()
}
NavigationLink(destination: EULAView(state: state), tag: .login, selection: $state ) {
NavigationLink(destination: LoginView(), tag: .login, selection: $state ) {
EmptyView()
}
+30 -43
View File
@@ -12,9 +12,7 @@ struct SideMenuView: View {
@Binding var isSidebarVisible: Bool
@State var confirm_logout: Bool = false
@EnvironmentObject var user_settings: UserSettingsStore
@State private var showQRCode = false
@StateObject var user_settings = UserSettingsStore()
@Environment(\.colorScheme) var colorScheme
@@ -77,6 +75,22 @@ struct SideMenuView: View {
Divider()
.padding(.trailing,40)
/*
HStack(alignment: .bottom) {
Text("69,420")
.foregroundColor(.accentColor)
.font(.largeTitle)
Text("SATS")
.font(.caption)
.padding(.bottom,6)
}
Divider()
.padding(.trailing,40)
*/
// THERE IS A LIMIT OF 10 NAVIGATIONLINKS!!! (Consider some in other views)
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
Label(NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person")
@@ -109,18 +123,6 @@ struct SideMenuView: View {
})
*/
NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) {
Label(NSLocalizedString("Blocked", comment: "Sidebar menu label for Profile view."), systemImage: "exclamationmark.octagon")
.font(.title2)
.foregroundColor(textColor())
}
NavigationLink(destination: RelayConfigView(state: damus_state)) {
Label(NSLocalizedString("Relays", comment: "Sidebar menu label for Relays view."), systemImage: "network")
.font(.title2)
.foregroundColor(textColor())
}
NavigationLink(destination: ConfigView(state: damus_state).environmentObject(user_settings)) {
Label(NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear")
.font(.title2)
@@ -132,33 +134,18 @@ struct SideMenuView: View {
Spacer()
HStack(alignment: .center) {
Button(action: {
//ConfigView(state: damus_state)
if damus_state.keypair.privkey == nil {
notify(.logout, ())
} else {
confirm_logout = true
}
}, label: {
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
.font(.title3)
.foregroundColor(textColor())
})
Spacer()
Button(action: {
showQRCode.toggle()
}, label: {
Label(NSLocalizedString("", comment: "Sidebar menu label for accessing QRCode view"), systemImage: "qrcode")
.font(.title)
.foregroundColor(textColor())
.padding(.trailing, 20)
}).fullScreenCover(isPresented: $showQRCode) {
QRCodeView(damus_state: damus_state)
Button(action: {
//ConfigView(state: damus_state)
if damus_state.keypair.privkey == nil {
notify(.logout, ())
} else {
confirm_logout = true
}
}
}, label: {
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
.font(.title3)
.foregroundColor(textColor())
})
}
.padding(.top, 60)
.padding(.bottom, 40)
@@ -171,10 +158,10 @@ struct SideMenuView: View {
isSidebarVisible.toggle()
}
.alert("Logout", isPresented: $confirm_logout) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user.")) {
confirm_logout = false
}
Button(NSLocalizedString("Logout", comment: "Button for logging out the user."), role: .destructive) {
Button(NSLocalizedString("Logout", comment: "Button for logging out the user.")) {
notify(.logout, ())
}
} message: {
+31 -18
View File
@@ -255,13 +255,22 @@ struct ThreadV2View: View {
// MARK: - Parents events view
VStack {
ForEach(thread.parentEvents, id: \.id) { event in
MutedEventView(damus_state: damus,
event: event,
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: false
EventView(
event: event,
highlight: .none,
has_action_bar: true,
damus: damus,
show_friend_icon: true, // TODO: change it
size: .small
)
.onTapGesture {
nav_target = event.id
navigating = true
}
.onAppear {
// TODO: find another solution to prevent layout shifting and layout blocking on large responses
reader.scrollTo("main", anchor: .bottom)
}
}
}.background(GeometryReader { geometry in
// get the height and width of the EventView view
@@ -276,25 +285,29 @@ struct ThreadV2View: View {
})
// MARK: - Actual event view
MutedEventView(
damus_state: damus,
EventView(
event: thread.current,
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: true
highlight: .none,
has_action_bar: true,
damus: damus,
show_friend_icon: true, // TODO: change it
size: .selected
).id("main")
// MARK: - Responses of the actual event view
ForEach(thread.childEvents, id: \.id) { event in
MutedEventView(
damus_state: damus,
EventView(
event: event,
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: false
highlight: .none,
has_action_bar: true,
damus: damus,
show_friend_icon: true, // TODO: change it
size: .small
)
.onTapGesture {
nav_target = event.id
navigating = true
}
}
}.padding()
}.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))
+5 -1
View File
@@ -39,7 +39,11 @@ struct InnerTimelineView: View {
EmptyTimelineView()
} else {
ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in
EventView(damus: damus, event: ev, has_action_bar: true)
//let tm = ThreadModel(event: inner_event_or_self(ev: ev), damus_state: damus)
//let is_chatroom = should_show_chatroom(ev)
//let tv = ThreadView(thread: tm, damus: damus, is_chatroom: is_chatroom)
EventView(event: ev, highlight: .none, has_action_bar: true, damus: damus, show_friend_icon: show_friend_icon)
.onTapGesture {
nav_target = ev
navigating = true
-9
View File
@@ -1,9 +0,0 @@
/* Bundle display name */
"CFBundleDisplayName" = "دامُس";
/* Bundle name */
"CFBundleName" = "دامُس";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "السماح لدامُس بالوصول إلى الصور يتيح لك حفظ الصور";
-629
View File
@@ -1,629 +0,0 @@
/* Blank space to separate profile picture from profile editor form. */
" " = "61b6edf1108e6f396680a33b02486a70_tr";
/* Description of how the nip05 identifier would be used for verification. */
"'%@' at '%@' will be used for verification" = "سيتم التحقق من '%@' @ '%@'";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid NIP-05 identifier. It should look like an email." = "'%@' عنوان NIP-05 غير صالح. من المفترض أن يشابه صيغة الايميل مثل المثال الموضح.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "متابعي (Profile.displayName(profile: profile, pubkey: whos))";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) يتابع";
/* Prefix character to username. */
"@" = "@";
/* Amount of time that has passed since reply quote event occurred.
Abbreviated version of a nostr public key. */
"%@" = "%@";
/* 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'. */
"%@ %@" = "%@ %@";
/* Alert message that informs a user was blocked. */
"%@ has been blocked" = "تم حظر %@";
/* Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string. */
"%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." = "انشاء حسابك لايتطلب رقم جوال أو بريد الكتروني أو معلومات شخصية. احصل على حسابك الخاص في ثواني.";
/* Explanation of what is done to keep private data encrypted. There is a heading that precedes this explanation which is a variable to this string. */
"%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" = "محادثات خاصة مشفرة كليا. ";
/* 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. */
"%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet." = "%@. بسهولة مطلقة، أرسل و استقبل برقيات البتكوين ⚡️عملة الانترنت العالمية.";
/* Number of tip payments on a post.
Number of profiles a user is following. */
"%lld" = "%lld";
/* Fraction of how many of the user's relay servers that are operational. */
"%lld/%lld" = "%lld/%lld";
/* Placeholder for event mention. */
"< e >" = "< e >";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "النبذة التعريفية";
/* Label for About Me section of user profile form. */
"About Me" = "النبذة التعريفية";
/* Placeholder text for About Me description. */
"Absolute Boss" = "مدير كبير";
/* Button to accept the end user license agreement before being allowed into the app. */
"Accept" = "موافق";
/* Label to indicate the public ID of the account. */
"Account ID" = "معرف الحساب";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "خيارات";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "اضافة";
/* Button label to re-add all original participants as profiles to reply to in a note */
"Add all" = "اضافة الجميع";
/* Label for section for adding a relay server. */
"Add Relay" = "اضافة موصّل";
/* Any amount of sats */
"Any" = "كم";
/* Example URL to LibreTranslate server */
"API Key (optional)" = "مفتاح API (اختياري)";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "هل أنت متأكد من اعادة النشر؟";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "صورة الخلفية";
/* Reminder to user that they should save their account information. */
"Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus." = "قبل البدء، يجب عليك حفظ معلومات حسابك لتتمكن من الوصول إليه مستقبلا في حالة حذف دامُس أو تغيير جهازك.";
/* Dropdown option label for Lightning wallet, Bitcoin Beach. */
"Bitcoin Beach" = "Bitcoin Beach";
/* Label for Bitcoin Lightning Tips section of user profile form. */
"Bitcoin Lightning Tips" = "اكراميات البتكوين";
/* Dropdown option label for Lightning wallet, Blixt Wallet */
"Blixt Wallet" = "Blixt Wallet";
/* Alert button to block a user.
Button to block a profile.
Context menu option for blocking users. */
"Block" = "حظر";
/* Alert message prompt to ask if a user should be blocked. */
"Block %@?" = "حظر %@؟";
/* Title of alert for blocking a user. */
"Block User" = "حظر المستخدم";
/* Sidebar menu label for Profile view. */
"Blocked" = "قائمة الحظر";
/* Navigation title of view to see list of blocked users. */
"Blocked Users" = "المحظورون";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
/* Dropdown option label for Lightning wallet, Breez. */
"Breez" = "Breez";
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "بث";
/* Alert button to cancel out of alert for blocking a user.
Button to cancel out of alert that creates a new mutelist.
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. */
"Cancel" = "الغاء";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "المحادثة";
/* Button for clearing cached data. */
"Clear" = "مسح";
/* Section title for clearing cached data. */
"Clear Cache" = "مسح البيانات المؤقتة";
/* Label indicating that a user's key was copied. */
"Copied" = "تم النسخ";
/* Button to copy a relay server address. */
"Copy" = "نسخ";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "نسخ عنوان الحساب";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "نسخ الصورة";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "نسخ رابط الصورة";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "نسخ البرقية";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "نسخ LNURL";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "نسخ معرف المنشور";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "نسخ المنشور بصيغة JSON";
/* Button to copy report ID. */
"Copy Report ID" = "نسخ معرف البلاغ";
/* Context menu option for copying the text from an note. */
"Copy Text" = "نسخ النص";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User Pubkey" = "نسخ معرف الحساب";
/* Alert message to indicate that the blocked user could not be found. */
"Could not find user to block..." = "لم يتم العثور حساب لحظره";
/* Button to create account. */
"Create" = "انشاء";
/* Button to create an account. */
"Create Account" = "انشاء حساب";
/* Title of alert prompting the user to create a new mutelist. */
"Create new mutelist" = "أنشئ قائمة حظر جديدة";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "مبتكر البتكوين. اسطورة لن تتكرر.";
/* Dropdown option for selecting a custom translation server. */
"Custom" = "مخصص";
/* Name of the app, shown on the first screen when user is not logged in. */
"Damus" = "دامُس";
/* Button to pay a Lightning invoice with the user's default Lightning wallet. */
"Default Wallet" = "المحفظة الافتراضية";
/* 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 */
"Delete" = "حذف";
/* Button to dismiss a text field alert. */
"Dismiss" = "اغلاق";
/* Label to prompt display name entry. */
"Display Name" = "الاسم";
/* Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message. */
"DMs" = "الرسائل الخاصة";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "انهاء";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "اكسب المال.";
/* Button to edit user's profile. */
"Edit" = "تحرير";
/* Text indicating that the view is used for editing which participants are replied to in a note. */
"Edit participants" = "تحرير المشاركين";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "مشفر";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "أدخل مفتاح حسابك لتسجيل الدخول:";
/* Error message indicating why saving keys failed. */
"Error: %@" = "خطأ: %@";
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "اتفاقية الاستخدام";
/* Button to follow a user. */
"Follow" = "متابعة";
/* Label describing followers of a user. */
"Followers" = "المتابعون";
/* Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.
Part of a larger sentence to describe how many profiles a user is following. */
"Following" = "المتابَعين";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "يتابع...";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "تابع";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "عام";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "عرض المنشور %@";
/* Navigation link to go to profile. */
"Goto profile %@" = "عرض الحساب %@";
/* Button to hide a post from a user who has been blocked. */
"Hide" = "اخفاء";
/* Button to hide the LibreTranslate server API key. */
"Hide API Key" = "اخفاء مفتاح API";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "الرئيسية";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://example.com/pic.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Button for user to report that the account or content has illegal content. */
"Illegal content" = "محتوى غير قانوني";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "المفتاح غير صالح";
/* Button for user to report that the account or content has spam. */
"It's spam" = "سبام";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
/* Moves the post button to the left side of the screen */
"Left Handed" = "تفضيل استخدام اليد اليسرى";
/* Button to complete account creation and start using the app. */
"Let's go!" = "هيا بنا!";
/* Section title for selecting the server that hosts the LibreTranslate machine translation API. */
"LibreTranslate Translations" = "ترجمة LibreTranslate";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "عنوان البرق أو LNURL";
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "برقية";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "LNLink";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "الاختيار";
/* Button to log into account.
Button to log into an account. */
"Login" = "الدخول";
/* 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. */
"Logout" = "الخروج";
/* Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out. */
"Make sure your nsec account key is saved before you logout or you will lose access to this account" = "تأكد من حفظ مفتاح حسابك السري قبل الخروج حتى لا تفقد امكانية الدخول الى حسابك.";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "تحقق NIP-05";
/* Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists. */
"No block list found, create a new one? This will overwrite any previous block lists." = "لم نعثر على قائمة حظر. هل تريد انشاء قائمة جديدة؟ سيتم استبدال أي قوائم سابقة ان وجدت";
/* No search results. */
"none" = "لا شيء";
/* Dropdown option for selecting no translation server. */
"None" = "لا اختيار";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "لا جديد في هذه اللحظة. يرجى المعاودة لاحقا!";
/* Navigation title for notifications. */
"Notifications" = "التنبيهات";
/* String indicating that a given timestamp just occurred */
"now" = "الان";
/* Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key. */
"nsec1..." = "nsec1...";
/* Button for user to report that the account or content has nudity or explicit content. */
"Nudity or explicit content" = "عري أو محتوى فاضح";
/* Label indicating that a form input is optional. */
"optional" = "غير الزامي";
/* Button to pay a Lightning invoice. */
"Pay" = "ادفع";
/* Navigation bar title for view to pay Lightning invoice. */
"Pay the Lightning invoice" = "ادفع البرقية";
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Button to post a note. */
"Post" = "انشر";
/* Text to indicate that what is being shown is a post from a user who has been blocked. */
"Post from a user you've blocked" = "منشور لمستخدم محظور";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "المنشورات";
/* Label for filter for seeing posts and replies (instead of only posts). */
"Posts & Replies" = "المنشورات والردود";
/* Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading. */
"Private" = "خصوصية";
/* Title of the secure field that holds the user's private key. */
"Private Key" = "المفتاح السري";
/* Sidebar menu label for Profile view. */
"Profile" = "الملف الشخصي";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "صورة الحساب";
/* Section title for the user's public account ID. */
"Public Account ID" = "معرف الحساب";
/* Label indicating that the text is a user's public account key. */
"Public key" = "المفتاح العام";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "المفتاح العام";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "مفتاح عام؟";
/* Navigation bar title for Reactions view. */
"Reactions" = "التفاعل";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "موصّلات موصى بها";
/* Button to reject the end user license agreement, which disallows the user from being let into the app. */
"Reject" = "رفض";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "موصّل";
/* Sidebar menu label for Relay servers view
Sidebar menu label for Relays view. */
"Relays" = "موصّلات";
/* Description of what was done as a result of sending a report to relay servers. */
"Relays have been notified and clients will be able to use this information to filter content. Thank you!" = "تم ابلاغ الموصّلات وسيتم الاستفادة من هذا البلاغ لتصفية المحتوى. شكرا لك!";
/* Button label to remove all participants from a note reply. */
"Remove all" = "حذف المشاركين";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "رد على منشوره السابق";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "رد على %1$@ & %2$@";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "رد على:";
/* Button to report a profile.
Context menu option for reporting content. */
"Report" = "ابلاغ";
/* Label indicating that the text underneath is the identifier of the report that was sent to relay servers. */
"Report ID:" = "معرف البلاغ";
/* Message indicating that a report was successfully sent to relay servers. */
"Report sent!" = "تم الابلاغ!";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "إعادة نشر";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "منشور مُعاد";
/* Navigation bar title for Reposts view. */
"Reposts" = "اعادات النشر";
/* 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. */
"Requests" = "طلبات";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "اعادة المحاولة";
/* Dropdown option label for Lightning wallet, River */
"River" = "River";
/* Example username of Bitcoin creator(s), Satoshi Nakamoto. */
"satoshi" = "ساتوشي";
/* Name of Bitcoin creator(s). */
"Satoshi Nakamoto" = "ساتوشي ناكاموتو";
/* Button for saving profile. */
"Save" = "حفظ";
/* Context menu option to save an image. */
"Save Image" = "حفظ الصورة";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "البحث عن وسم: #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "بحث...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "المفتاح السري للحساب";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "اختر محفظة البرق";
/* Prompt selection of user's default wallet */
"Select default wallet" = "المحفظة الافتراضية";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "أرسل رسالة لبدء المحادثة...";
/* Prompt selection of LibreTranslate server to perform machine translations on notes */
"Server" = "سيرفر";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "الاعدادات";
/* Button to share an image.
Button to share the link to a profile. */
"Share" = "مشاركة";
/* Button to show a post from a user who has been blocked.
Toggle to show or hide user's secret account login key. */
"Show" = "عرض";
/* Button to hide the LibreTranslate server API key. */
"Show API Key" = "عرض مفتاح API";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "هل تريد اختيار المحفظة عند كل عملية دفع؟";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "تسجيل الخروج";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* Button to close out of alert that informs that the action to block a user was successful. */
"Thanks!" = "شكرا!";
/* Button for user to report that the account is impersonating someone. */
"They are impersonating someone" = "انتحال صفة شخص آخر";
/* Warning that the inputted account key is a public key and the result of what happens because of it. */
"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." = "هذا مفتاح عام. لن تستطيع النشر أو التفاعل بهذا الحساب بأي طريقة. تستطيع فقط مشاهدة المحتوى العام من منظور صاحب الحساب.";
/* Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key. */
"This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key." = "صيغة المفتاح قديمة. لا نستطيع التحديد إذا ما كان المفتاح خاصا أو عاما. الرجاء تفعيل الخانة بالأسفل إذا كان المفتاح عاما.";
/* Label to describe that a public key is the user's account ID and what they can do with it. */
"This is your account ID, you can give this to your friends so that they can follow you. Click to copy." = "هذا معرف حسابك. بإمكانك إرساله لأصدقائك حتى يتمكنوا من متابعتك. اضغط للنسخ.";
/* Label to describe that a private key is the user's secret account key and what they should do with it. */
"This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" = "هذا مفتاح الحساب السري. تحتاجه للدخول إلى حسابك. لا تشاركه مع أي شخص! احتفظ به في مكان آمن مثل برنامج إدارة كلمات المرور السرية. ";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "منشور";
/* Button to translate note from different language. */
"Translate Note" = "ترجم المنشور";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (languageName!)" = "مُترجَم من (languageName!)";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "اكتب المنشور هنا...";
/* Non-breaking space character to fill in blank space next to event action button icons. */
"u{00A0}" = "u{00A0}";
/* Button to unfollow a user. */
"Unfollow" = "الغاء المتابعة";
/* Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile. */
"Unfollowing" = "يلغي المتابعة";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "يلغي المتابعة...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "ألغى متابعة";
/* Example URL to LibreTranslate server */
"URL" = "رابط";
/* Alert message to indicate the user has been blocked */
"User blocked" = "الحساب محظور";
/* Alert message that informs a user was blocked. */
"User has been blocked" = "تم الحظر";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "اسم المستخدم";
/* Sidebar menu label for Wallet view. */
"Wallet" = "المحفظة";
/* Dropdown option label for Lightning wallet, Wallet of Satoshi. */
"Wallet of Satoshi" = "Wallet of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "تفضيلات المحفظة";
/* Label for Website section of user profile form. */
"Website" = "موقع الكتروني";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "مرحبا بك في شبكتك الاجتماعية!";
/* Text to welcome user. */
"Welcome, %@!" = "مرحبا، %@!";
/* Header text to prompt user what issue they want to report. */
"What do you want to report?" = "عن ماذا تريد الابلاغ";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://some.relay.com";
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "نعم، استبدل";
/* 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. */
"you" = "أنت";
/* Label for Your Name section of user profile form. */
"Your Name" = "الاسم";
/* Footer text to inform user what will happen when the report is submitted. */
"Your report will be sent to the relays you are connected to" = "سيتم ارسال بلاغك للموصّلات المتصلة بحسابك";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";
-222
View File
@@ -1,222 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>collapsed_event_view_other_notes</key>
<dict>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>%d other notes</string>
<key>many</key>
<string>%d other notes</string>
<key>one</key>
<string>%dمنشور اضافي</string>
<key>other</key>
<string>%dمنشورات اضافية</string>
<key>two</key>
<string>%d other notes</string>
<key>zero</key>
<string>%d other notes</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
</dict>
<key>followers_count</key>
<dict>
<key>FOLLOWERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>Followers</string>
<key>many</key>
<string>Followers</string>
<key>one</key>
<string>متابع</string>
<key>other</key>
<string>المتابعون</string>
<key>two</key>
<string>Followers</string>
<key>zero</key>
<string>Followers</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
</dict>
<key>reactions_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTIONS@</string>
<key>REACTIONS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>Reactions</string>
<key>many</key>
<string>Reactions</string>
<key>one</key>
<string>تفاعل</string>
<key>other</key>
<string>تفاعل</string>
<key>two</key>
<string>Reactions</string>
<key>zero</key>
<string>Reactions</string>
</dict>
</dict>
<key>relays_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@RELAYS@</string>
<key>RELAYS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>Relays</string>
<key>many</key>
<string>Relays</string>
<key>one</key>
<string> موصّل</string>
<key>other</key>
<string>موصّلات</string>
<key>two</key>
<string>Relays</string>
<key>zero</key>
<string>Relays</string>
</dict>
</dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>رد على %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string> &amp; %d others</string>
<key>many</key>
<string> &amp; %d others</string>
<key>one</key>
<string>&amp; %dآخر</string>
<key>other</key>
<string>&amp; %dآخرين</string>
<key>two</key>
<string> &amp; %d others</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>رد على%@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string> &amp; %d others</string>
<key>many</key>
<string> &amp; %d others</string>
<key>one</key>
<string>&amp; %dآخر</string>
<key>other</key>
<string>&amp; %dآخرين</string>
<key>two</key>
<string> &amp; %d others</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTS@</string>
<key>REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>Reposts</string>
<key>many</key>
<string>Reposts</string>
<key>one</key>
<string>اعادة نشر</string>
<key>other</key>
<string>اعادات نشر</string>
<key>two</key>
<string>Reposts</string>
<key>zero</key>
<string>Reposts</string>
</dict>
</dict>
<key>sats_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@SATS@</string>
<key>SATS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>few</key>
<string>%2$@ sats</string>
<key>many</key>
<string>%2$@ sats</string>
<key>one</key>
<string>%2$@ ساتوشي</string>
<key>other</key>
<string>%2$@ ساتوشي</string>
<key>two</key>
<string>%2$@ sats</string>
<key>zero</key>
<string>%2$@ sats</string>
</dict>
</dict>
<key>tips_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@TIPS@</string>
<key>TIPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>few</key>
<string>Tips</string>
<key>many</key>
<string>Tips</string>
<key>one</key>
<string>اكرامية</string>
<key>other</key>
<string>اكراميات</string>
<key>two</key>
<string>Tips</string>
<key>zero</key>
<string>Tips</string>
</dict>
</dict>
</dict>
</plist>
-9
View File
@@ -1,9 +0,0 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Damus Zugriff auf deine Fotos zu gewähren erlaubt dir Bilder zu sichern.";
-629
View File
@@ -1,629 +0,0 @@
/* Blank space to separate profile picture from profile editor form. */
" " = "61b6edf1108e6f396680a33b02486a70_tr";
/* Description of how the nip05 identifier would be used for verification. */
"'%@' at '%@' will be used for verification" = "'%@' bei '%@' wird zur Verifizierung benutzt werden.";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid NIP-05 identifier. It should look like an email." = "%@' ist kein gülter NIP-05 identifier. Dieser sollte wie eine email aussehen. ";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "(Profile.displayName(profile: profile, pubkey: whos)) Gefolgte";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) folgt";
/* Prefix character to username. */
"@" = "@";
/* Amount of time that has passed since reply quote event occurred.
Abbreviated version of a nostr public key. */
"%@" = "%@";
/* 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'. */
"%@ %@" = "%@ %@";
/* Alert message that informs a user was blocked. */
"%@ has been blocked" = "%@ wurde blockiert";
/* Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string. */
"%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." = "%@. Ein Konto zu erstellen benötigt keine Telefonnummer, Emailadresse oder Namen. Fang jetzt gleich ganz reibungslos an.";
/* Explanation of what is done to keep private data encrypted. There is a heading that precedes this explanation which is a variable to this string. */
"%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" = "%@. End-zu-End verschlüsselter privater Nachrichtenaustausch. Halte Tech-Riesen aus deinen PNs heraus";
/* 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. */
"%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet." = "%@. Belohne Beiträge deiner Freunde und sammle Sats mit Bitcoin⚡️, der eigenen Währung des Internets.";
/* Number of tip payments on a post.
Number of profiles a user is following. */
"%lld" = "%lld";
/* Fraction of how many of the user's relay servers that are operational. */
"%lld/%lld" = "%lld/%lld";
/* Placeholder for event mention. */
"< e >" = "< e >";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "Über";
/* Label for About Me section of user profile form. */
"About Me" = "Über mich";
/* Placeholder text for About Me description. */
"Absolute Boss" = "Absoluter Macher";
/* Button to accept the end user license agreement before being allowed into the app. */
"Accept" = "Zustimmen";
/* Label to indicate the public ID of the account. */
"Account ID" = "Konto ID";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "Handlungen";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Hinzufügen";
/* Button label to re-add all original participants as profiles to reply to in a note */
"Add all" = "Alle hinzufügen";
/* Label for section for adding a relay server. */
"Add Relay" = "Relay hinzufügen";
/* Any amount of sats */
"Any" = "beliebig";
/* Example URL to LibreTranslate server */
"API Key (optional)" = "API Schlüssel (optional)";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Bist du sicher dass Du den Beitrag auf deinem Profil teilen möchtest?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Bannerbild";
/* Reminder to user that they should save their account information. */
"Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus." = "Bevor wir anfangen, Du wirst deine Kontodaten sichern müssen, sonst wirst du dich in der Zukunft nicht mehr anmelden können wenn du Damus jemals deinstallierst.";
/* Dropdown option label for Lightning wallet, Bitcoin Beach. */
"Bitcoin Beach" = "Bitcoin Beach";
/* Label for Bitcoin Lightning Tips section of user profile form. */
"Bitcoin Lightning Tips" = "Bitcoin Lightning Zahlungen";
/* Dropdown option label for Lightning wallet, Blixt Wallet */
"Blixt Wallet" = "Blixt Wallet";
/* Alert button to block a user.
Button to block a profile.
Context menu option for blocking users. */
"Block" = "Blockieren";
/* Alert message prompt to ask if a user should be blocked. */
"Block %@?" = "%@ blockieren?";
/* Title of alert for blocking a user. */
"Block User" = "Benutzer blockieren";
/* Sidebar menu label for Profile view. */
"Blocked" = "Blockiert";
/* Navigation title of view to see list of blocked users. */
"Blocked Users" = "Blockierte Benutzer";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
/* Dropdown option label for Lightning wallet, Breez. */
"Breez" = "Breez";
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "Senden";
/* Alert button to cancel out of alert for blocking a user.
Button to cancel out of alert that creates a new mutelist.
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. */
"Cancel" = "Abbrechen";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "Unterhaltungen";
/* Button for clearing cached data. */
"Clear" = "Löschen";
/* Section title for clearing cached data. */
"Clear Cache" = "Zwischenspeicher löschen";
/* Label indicating that a user's key was copied. */
"Copied" = "Kopiert";
/* Button to copy a relay server address. */
"Copy" = "Kopieren";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "Konto ID kopieren";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "Bild kopieren";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "Bild URL kopieren";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Zahlungsdaten kopieren";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "LNURL kopieren";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "Notiz ID kopieren";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Notiz JSON kopieren";
/* Button to copy report ID. */
"Copy Report ID" = "Meldungs-ID kopieren";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Text kopieren";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User Pubkey" = "Öffentlichen Schlüssel des Benutzers kopieren";
/* Alert message to indicate that the blocked user could not be found. */
"Could not find user to block..." = "Der zu blockierende Benutzer konnte nicht gefunden werden...";
/* Button to create account. */
"Create" = "Erstellen";
/* Button to create an account. */
"Create Account" = "Konto erstellen";
/* Title of alert prompting the user to create a new mutelist. */
"Create new mutelist" = "Neue Stummschaltungsliste";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Erfinder von Bitcoin. Absolute Legende(n).";
/* Dropdown option for selecting a custom translation server. */
"Custom" = "Auswahl";
/* Name of the app, shown on the first screen when user is not logged in. */
"Damus" = "Damus";
/* Button to pay a Lightning invoice with the user's default Lightning wallet. */
"Default Wallet" = "Voreingestelltes Wallet";
/* 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 */
"Delete" = "Löschen";
/* Button to dismiss a text field alert. */
"Dismiss" = "Schließen";
/* Label to prompt display name entry. */
"Display Name" = "Profilname";
/* Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message. */
"DMs" = "PNs";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "Fertig";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "Verdiene Geld";
/* Button to edit user's profile. */
"Edit" = "Bearbeiten";
/* Text indicating that the view is used for editing which participants are replied to in a note. */
"Edit participants" = "Teilnehmer editieren";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "Verschlüsselt";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "Gib deinen Kontoschlüssel ein um dich anzumelden:";
/* Error message indicating why saving keys failed. */
"Error: %@" = "Fehler: %@";
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "Endbenutzer-Lizenzvereinbarung";
/* Button to follow a user. */
"Follow" = "Folgen";
/* Label describing followers of a user. */
"Followers" = "Gefolgte:r";
/* Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.
Part of a larger sentence to describe how many profiles a user is following. */
"Following" = "Gefolgt";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "Folge…";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "Folgt";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Allgemein";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "Gehe zum Beitrag %@";
/* Navigation link to go to profile. */
"Goto profile %@" = "Gehe zum Profil %@";
/* Button to hide a post from a user who has been blocked. */
"Hide" = "Verstecken";
/* Button to hide the LibreTranslate server API key. */
"Hide API Key" = "API Schlüssel verstecken";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Heim";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://beispiel.at/bild.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Button for user to report that the account or content has illegal content. */
"Illegal content" = "Illegaler Inhalt";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Ungültiger Schlüssel";
/* Button for user to report that the account or content has spam. */
"It's spam" = "Es ist Spam";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
/* Moves the post button to the left side of the screen */
"Left Handed" = "Linkshändig";
/* Button to complete account creation and start using the app. */
"Let's go!" = "Lass uns loslegen!";
/* Section title for selecting the server that hosts the LibreTranslate machine translation API. */
"LibreTranslate Translations" = "LibreTranslate Übersetzungen";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Lightning-Adresse oder LNURL";
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "Lightning-Rechnung";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "LNLink";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "System-Standard";
/* Button to log into account.
Button to log into an account. */
"Login" = "Einloggen";
/* 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. */
"Logout" = "Ausloggen";
/* Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out. */
"Make sure your nsec account key is saved before you logout or you will lose access to this account" = "Stelle sicher, dass du deinen nsec-Schlüssel gespeichert hast, sonst wirst du den Zugang zu deinem Konto verlieren.";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "NIP-05-Verifizierung";
/* Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists. */
"No block list found, create a new one? This will overwrite any previous block lists." = "Es wurde keine Blockier-Liste gefunden, soll eine neue erzeugt werden? Dies wird eine frühere Blockier-Liste überschreiben.";
/* No search results. */
"none" = "keine";
/* Dropdown option for selecting no translation server. */
"None" = "Keine";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Hier gibt es nichts zu sehen. Komm später wieder vorbei!";
/* Navigation title for notifications. */
"Notifications" = "Benachrichtigungen";
/* String indicating that a given timestamp just occurred */
"now" = "jetzt";
/* Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key. */
"nsec1..." = "nsec1...";
/* Button for user to report that the account or content has nudity or explicit content. */
"Nudity or explicit content" = "Nacktheit oder anstößige Inhalte";
/* Label indicating that a form input is optional. */
"optional" = "optional";
/* Button to pay a Lightning invoice. */
"Pay" = "Bezahlen";
/* Navigation bar title for view to pay Lightning invoice. */
"Pay the Lightning invoice" = "Bezahle die Lightning-Rechnung";
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Button to post a note. */
"Post" = "Veröffentlichen";
/* Text to indicate that what is being shown is a post from a user who has been blocked. */
"Post from a user you've blocked" = "Nachricht von einem/e User/in den/die Du geblockt hast";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Beiträge";
/* Label for filter for seeing posts and replies (instead of only posts). */
"Posts & Replies" = "Beiträge & Antworten";
/* Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading. */
"Private" = "Privat";
/* Title of the secure field that holds the user's private key. */
"Private Key" = "Privater Schlüssel";
/* Sidebar menu label for Profile view. */
"Profile" = "Profil";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "Profilbild";
/* Section title for the user's public account ID. */
"Public Account ID" = "Öffentliche Konto ID";
/* Label indicating that the text is a user's public account key. */
"Public key" = "Öffentlicher Schlüssel";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "Öffentlicher Schlüssel";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "Öffentlicher Schlüssel?";
/* Navigation bar title for Reactions view. */
"Reactions" = "Reaktionen";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Empfohlene Relays";
/* Button to reject the end user license agreement, which disallows the user from being let into the app. */
"Reject" = "Ablehnen";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relay";
/* Sidebar menu label for Relay servers view
Sidebar menu label for Relays view. */
"Relays" = "Relays";
/* Description of what was done as a result of sending a report to relay servers. */
"Relays have been notified and clients will be able to use this information to filter content. Thank you!" = "Relays wurden benachrichtigt und Anwendungen können diese Information nutzen, um Inhalte zu filtern. Vielen Dank!";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Alle entfernen";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "Antwort an sich selbst";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "Antwort an %1$@ & %2$@";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Antwort an:";
/* Button to report a profile.
Context menu option for reporting content. */
"Report" = "Melden";
/* Label indicating that the text underneath is the identifier of the report that was sent to relay servers. */
"Report ID:" = "Meldungs-ID";
/* Message indicating that a report was successfully sent to relay servers. */
"Report sent!" = "Meldung versandt!";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Selbst teilen";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Selbst geteilt";
/* Navigation bar title for Reposts view. */
"Reposts" = "Geteilte Beiträge";
/* 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. */
"Requests" = "Anfragen";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Erneut versuchen";
/* Dropdown option label for Lightning wallet, River */
"River" = "River";
/* Example username of Bitcoin creator(s), Satoshi Nakamoto. */
"satoshi" = "satoshi";
/* Name of Bitcoin creator(s). */
"Satoshi Nakamoto" = "Satoshi Nakamoto";
/* Button for saving profile. */
"Save" = "Speichern";
/* Context menu option to save an image. */
"Save Image" = "Bild sichern";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Hashtag suchen: #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "Suchen...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "Geheimer Konto Anmeldeschlüssel";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "Wähle ein Lightning Wallet";
/* Prompt selection of user's default wallet */
"Select default wallet" = "Wähle das voreingestellte Wallet";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Sende eine Nachricht um eine Unterhaltung zu beginnen...";
/* Prompt selection of LibreTranslate server to perform machine translations on notes */
"Server" = "Server";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Einstellungen";
/* Button to share an image.
Button to share the link to a profile. */
"Share" = "Teilen";
/* Button to show a post from a user who has been blocked.
Toggle to show or hide user's secret account login key. */
"Show" = "Anzeigen";
/* Button to hide the LibreTranslate server API key. */
"Show API Key" = "API Schlüssel anzeigen";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Wallet-Auswahl zeigen";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "Abmelden";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* Button to close out of alert that informs that the action to block a user was successful. */
"Thanks!" = "Danke!";
/* Button for user to report that the account is impersonating someone. */
"They are impersonating someone" = "Sie gibt sich für jemand anderen aus";
/* Warning that the inputted account key is a public key and the result of what happens because of it. */
"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." = "Dies ist ein öffentlicher Schlüssel, mit dem Sie keine Beiträge verfassen oder in irgendeiner Weise interagieren können. Er wird verwendet, um Konten aus deren Perspektive zu betrachten.";
/* Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key. */
"This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key." = "Dies ist ein Nostr-Schlüssel im alten Format. Es ist nicht eindeutig, ob es ein öffentlicher oder privater Schlüssel ist. Bitte aktiviere die Schaltfläche unten, wenn es ein öffentlicher Schlüssel ist.";
/* Label to describe that a public key is the user's account ID and what they can do with it. */
"This is your account ID, you can give this to your friends so that they can follow you. Click to copy." = "Dies ist deine Konto-ID, die du an deine Freunde weitergeben kannst, damit sie dir folgen können. Zum Kopieren anklicken.";
/* Label to describe that a private key is the user's secret account key and what they should do with it. */
"This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" = "Dies ist dein geheimer, privater Schlüssel. Du benötigst ihn, um auf dein Konto zuzugreifen. Gib den privaten Schlüssel an niemanden weiter! Speichere ihn in einem Passwort-Manager und bewahre ihn sicher auf!";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Thema";
/* Button to translate note from different language. */
"Translate Note" = "Note übersetzen";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (languageName!)" = "Übersetzt aus (languageName!)";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Schreibe deinen Beitrag hier...";
/* Non-breaking space character to fill in blank space next to event action button icons. */
"u{00A0}" = "u{00A0}";
/* Button to unfollow a user. */
"Unfollow" = "Entfolgen";
/* Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile. */
"Unfollowing" = "Entfolgen...";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "Entfolgen...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Entfolgen";
/* Example URL to LibreTranslate server */
"URL" = "URL";
/* Alert message to indicate the user has been blocked */
"User blocked" = "Benutzer blockiert";
/* Alert message that informs a user was blocked. */
"User has been blocked" = "Der Benutzer wurde blockiert";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Benutzername";
/* Sidebar menu label for Wallet view. */
"Wallet" = "Wallet";
/* Dropdown option label for Lightning wallet, Wallet of Satoshi. */
"Wallet of Satoshi" = "Wallet of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "Wallet-Auswahl";
/* Label for Website section of user profile form. */
"Website" = "Website";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "Willkommen in dem sozialen Netzwerk das %@ kontrollierst.";
/* Text to welcome user. */
"Welcome, %@!" = "Willkommen, %@!";
/* Header text to prompt user what issue they want to report. */
"What do you want to report?" = "Was möchtest du melden?";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://ein.relay.at";
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "Ja, überschreiben";
/* 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. */
"you" = "Du";
/* Label for Your Name section of user profile form. */
"Your Name" = "Dein Name";
/* Footer text to inform user what will happen when the report is submitted. */
"Your report will be sent to the relays you are connected to" = "Die Meldung wird an Relays versendet, mit denen du verbunden bist";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";
-154
View File
@@ -1,154 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>collapsed_event_view_other_notes</key>
<dict>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>1%d andere Notiz</string>
<key>other</key>
<string>1%d andere Notizen</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
</dict>
<key>followers_count</key>
<dict>
<key>FOLLOWERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Gefolgte:r</string>
<key>other</key>
<string>Gefolgte</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
</dict>
<key>reactions_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTIONS@</string>
<key>REACTIONS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Reaktion</string>
<key>other</key>
<string>Reaktionen</string>
</dict>
</dict>
<key>relays_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@RELAYS@</string>
<key>RELAYS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Relay</string>
<key>other</key>
<string>Relays</string>
</dict>
</dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Antwort an %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>&amp; 1%d andere</string>
<key>other</key>
<string>&amp; %d andere</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Antwort an %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>&amp; %d andere</string>
<key>other</key>
<string>&amp; %d andere</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTS@</string>
<key>REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>geteilter Beitrag</string>
<key>other</key>
<string>geteilte Beiträge</string>
</dict>
</dict>
<key>sats_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@SATS@</string>
<key>SATS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>%2$@ sat</string>
<key>other</key>
<string>%2$@ sats</string>
</dict>
</dict>
<key>tips_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@TIPS@</string>
<key>TIPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Trinkgeld</string>
<key>other</key>
<string>Trinkgelder</string>
</dict>
</dict>
</dict>
</plist>
-9
View File
@@ -1,9 +0,0 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Zum Speichern von Bildern braucht Damus Zugriff auf deine Fotos";
-629
View File
@@ -1,629 +0,0 @@
/* Blank space to separate profile picture from profile editor form. */
" " = "61b6edf1108e6f396680a33b02486a70_tr";
/* Description of how the nip05 identifier would be used for verification. */
"'%@' at '%@' will be used for verification" = "'%@' bei '%@' wird zur Verifizierung benutzt werden.";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid NIP-05 identifier. It should look like an email." = "%@' ist kein gülter NIP-05 identifier. Dieser sollte wie eine email aussehen. ";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "(Profile.displayName(profile: profile, pubkey: whos)) Gefolgte";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) folgt";
/* Prefix character to username. */
"@" = "@";
/* Amount of time that has passed since reply quote event occurred.
Abbreviated version of a nostr public key. */
"%@" = "%@";
/* 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'. */
"%@ %@" = "%@ %@";
/* Alert message that informs a user was blocked. */
"%@ has been blocked" = "%@ wurde blockiert";
/* Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string. */
"%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." = "%@. Du brauchst für ein Konto keine Telefonnummer, E-Mail-Adresse oder Namen. Fang ganz reibungslos einfach an.";
/* Explanation of what is done to keep private data encrypted. There is a heading that precedes this explanation which is a variable to this string. */
"%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" = "%@. Ende-zu-Ende verschlüsselter privater Nachrichtenaustausch. Halte Big Tech aus deinen PNs heraus";
/* 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. */
"%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet." = "%@. Belohne Beiträge deiner Freunde und sammle Sats mit Bitcoin⚡️, der eigenen Währung des Internets.";
/* Number of tip payments on a post.
Number of profiles a user is following. */
"%lld" = "%lld";
/* Fraction of how many of the user's relay servers that are operational. */
"%lld/%lld" = "%lld/%lld";
/* Placeholder for event mention. */
"< e >" = "< e >";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "Über";
/* Label for About Me section of user profile form. */
"About Me" = "Über mich";
/* Placeholder text for About Me description. */
"Absolute Boss" = "Absoluter Macher";
/* Button to accept the end user license agreement before being allowed into the app. */
"Accept" = "Zustimmen";
/* Label to indicate the public ID of the account. */
"Account ID" = "Konto-ID";
/* Title for confirmation dialog to either share, report, or block a profile. */
"Actions" = "Aktionen";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Hinzufügen";
/* Button label to re-add all original participants as profiles to reply to in a note */
"Add all" = "Alle hinzufügen";
/* Label for section for adding a relay server. */
"Add Relay" = "Relay hinzufügen";
/* Any amount of sats */
"Any" = "Beliebig";
/* Example URL to LibreTranslate server */
"API Key (optional)" = "API Schlüssel (optional)";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Bist du sicher dass Du den Beitrag teilen möchtest?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Bannerbild";
/* Reminder to user that they should save their account information. */
"Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus." = "Bevor wir anfangen, musst du deine Kontodaten sichern, sonst kannst du dich in Zukunft nicht mehr anmelden, wenn du Damus einmal deinstallierst.";
/* Dropdown option label for Lightning wallet, Bitcoin Beach. */
"Bitcoin Beach" = "Bitcoin Beach";
/* Label for Bitcoin Lightning Tips section of user profile form. */
"Bitcoin Lightning Tips" = "Bitcoin Lightning Zahlungen";
/* Dropdown option label for Lightning wallet, Blixt Wallet */
"Blixt Wallet" = "Blixt Wallet";
/* Alert button to block a user.
Button to block a profile.
Context menu option for blocking users. */
"Block" = "Blockieren";
/* Alert message prompt to ask if a user should be blocked. */
"Block %@?" = "%@ blockieren?";
/* Title of alert for blocking a user. */
"Block User" = "Benutzer blockieren";
/* Sidebar menu label for Profile view. */
"Blocked" = "Blockiert";
/* Navigation title of view to see list of blocked users. */
"Blocked Users" = "Blockierte Benutzer";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
/* Dropdown option label for Lightning wallet, Breez. */
"Breez" = "Breez";
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "Senden";
/* Alert button to cancel out of alert for blocking a user.
Button to cancel out of alert that creates a new mutelist.
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. */
"Cancel" = "Abbrechen";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "Unterhaltungen";
/* Button for clearing cached data. */
"Clear" = "Löschen";
/* Section title for clearing cached data. */
"Clear Cache" = "Zwischenspeicher löschen";
/* Label indicating that a user's key was copied. */
"Copied" = "Kopiert";
/* Button to copy a relay server address. */
"Copy" = "Kopieren";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "Konto-ID kopieren";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "Bild kopieren";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "Bild-URL kopieren";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Zahlungsdaten kopieren";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "LNURL kopieren";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "Notiz-ID kopieren";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Notiz-JSON kopieren";
/* Button to copy report ID. */
"Copy Report ID" = "Meldungs-ID kopieren";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Text kopieren";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User Pubkey" = "Öffentlichen Schlüssel des Benutzers kopieren";
/* Alert message to indicate that the blocked user could not be found. */
"Could not find user to block..." = "Der zu blockierende Benutzer konnte nicht gefunden werden...";
/* Button to create account. */
"Create" = "Erstellen";
/* Button to create an account. */
"Create Account" = "Konto erstellen";
/* Title of alert prompting the user to create a new mutelist. */
"Create new mutelist" = "Neue Stummschaltungsliste";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Erfinder von Bitcoin. Absolute Legende(n).";
/* Dropdown option for selecting a custom translation server. */
"Custom" = "Anpassen";
/* Name of the app, shown on the first screen when user is not logged in. */
"Damus" = "Damus";
/* Button to pay a Lightning invoice with the user's default Lightning wallet. */
"Default Wallet" = "Voreingestellte Wallet";
/* 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 */
"Delete" = "Löschen";
/* Button to dismiss a text field alert. */
"Dismiss" = "Schließen";
/* Label to prompt display name entry. */
"Display Name" = "Profilname";
/* Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message. */
"DMs" = "PNs";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "Fertig";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "Verdiene Geld";
/* Button to edit user's profile. */
"Edit" = "Bearbeiten";
/* Text indicating that the view is used for editing which participants are replied to in a note. */
"Edit participants" = "Teilnehmer editieren";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "Verschlüsselt";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "Gib deinen Kontoschlüssel ein um dich anzumelden:";
/* Error message indicating why saving keys failed. */
"Error: %@" = "Fehler: %@";
/* Label indicating that the below text is the EULA, an acronym for End User License Agreement. */
"EULA" = "Endbenutzer-Lizenzvereinbarung";
/* Button to follow a user. */
"Follow" = "Folgen";
/* Label describing followers of a user. */
"Followers" = "Gefolgte:r";
/* Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.
Part of a larger sentence to describe how many profiles a user is following. */
"Following" = "Gefolgt";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "Folge…";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "Folgt";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Allgemein";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "Gehe zum Beitrag %@";
/* Navigation link to go to profile. */
"Goto profile %@" = "Gehe zum Profil %@";
/* Button to hide a post from a user who has been blocked. */
"Hide" = "Verstecken";
/* Button to hide the LibreTranslate server API key. */
"Hide API Key" = "API Schlüssel verstecken";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Heim";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://example.com/pic.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Button for user to report that the account or content has illegal content. */
"Illegal content" = "Illegaler Inhalt";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Ungültiger Schlüssel";
/* Button for user to report that the account or content has spam. */
"It's spam" = "Das ist Spam";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
/* Moves the post button to the left side of the screen */
"Left Handed" = "Linkshändig";
/* Button to complete account creation and start using the app. */
"Let's go!" = "Los gehts!";
/* Section title for selecting the server that hosts the LibreTranslate machine translation API. */
"LibreTranslate Translations" = "LibreTranslate Übersetzungen";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Lightning-Adresse oder LNURL";
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "Lightning-Rechnung";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "LNLink";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "System-Standard";
/* Button to log into account.
Button to log into an account. */
"Login" = "Anmelden";
/* 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. */
"Logout" = "Ausloggen";
/* Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out. */
"Make sure your nsec account key is saved before you logout or you will lose access to this account" = "Sorge dafür dass dein nsec Kontoschlüssel vor dem Abmelden gesichert ist, oder du wirst den Zugang zu diesem Konto verlieren";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "NIP-05-Verifizierung";
/* Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists. */
"No block list found, create a new one? This will overwrite any previous block lists." = "Es wurde keine Blockier-Liste gefunden, soll eine neue erzeugt werden? Dies überschreibt eventuelle frühere Blockier-Listen.";
/* No search results. */
"none" = "keine";
/* Dropdown option for selecting no translation server. */
"None" = "Kein";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Hier gibts nichts zu sehen. Schau später wieder vorbei!";
/* Navigation title for notifications. */
"Notifications" = "Benachrichtigungen";
/* String indicating that a given timestamp just occurred */
"now" = "jetzt";
/* Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key. */
"nsec1..." = "nsec1...";
/* Button for user to report that the account or content has nudity or explicit content. */
"Nudity or explicit content" = "Nacktheit oder anstößige Inhalte";
/* Label indicating that a form input is optional. */
"optional" = "optional";
/* Button to pay a Lightning invoice. */
"Pay" = "Bezahlen";
/* Navigation bar title for view to pay Lightning invoice. */
"Pay the Lightning invoice" = "Bezahle die Lightning-Rechnung";
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Button to post a note. */
"Post" = "Teilen";
/* Text to indicate that what is being shown is a post from a user who has been blocked. */
"Post from a user you've blocked" = "Nachricht von einem/e User/in den/die Du geblockt hast.";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Beiträge";
/* Label for filter for seeing posts and replies (instead of only posts). */
"Posts & Replies" = "Beiträge & Antworten";
/* Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading. */
"Private" = "Privatsphäre";
/* Title of the secure field that holds the user's private key. */
"Private Key" = "Privater Schlüssel";
/* Sidebar menu label for Profile view. */
"Profile" = "Profil";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "Profilbild";
/* Section title for the user's public account ID. */
"Public Account ID" = "Öffentliche Konto ID";
/* Label indicating that the text is a user's public account key. */
"Public key" = "Öffentlicher Schlüssel";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "Öffentlicher Schlüssel";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "Öffentlicher Schlüssel?";
/* Navigation bar title for Reactions view. */
"Reactions" = "Reaktionen";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Empfohlene Relays";
/* Button to reject the end user license agreement, which disallows the user from being let into the app. */
"Reject" = "Ablehnen";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relay";
/* Sidebar menu label for Relay servers view
Sidebar menu label for Relays view. */
"Relays" = "Relays";
/* Description of what was done as a result of sending a report to relay servers. */
"Relays have been notified and clients will be able to use this information to filter content. Thank you!" = "Relays wurden benachrichtigt und Anwendungen können diese Information nutzen, um Inhalte zu filtern. Vielen Dank!";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Alle entfernen";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "Antwort an dich selbst";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "%1$@ & %2$@ antworten";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Du antwortest:";
/* Button to report a profile.
Context menu option for reporting content. */
"Report" = "Melden";
/* Label indicating that the text underneath is the identifier of the report that was sent to relay servers. */
"Report ID:" = "Meldungs-ID";
/* Message indicating that a report was successfully sent to relay servers. */
"Report sent!" = "Meldung versandt!";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Teilen";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Geteilt";
/* Navigation bar title for Reposts view. */
"Reposts" = "Geteilte Beiträge";
/* 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. */
"Requests" = "Anfragen";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Erneut versuchen";
/* Dropdown option label for Lightning wallet, River */
"River" = "River";
/* Example username of Bitcoin creator(s), Satoshi Nakamoto. */
"satoshi" = "satoshi";
/* Name of Bitcoin creator(s). */
"Satoshi Nakamoto" = "Satoshi Nakamoto";
/* Button for saving profile. */
"Save" = "Speichern";
/* Context menu option to save an image. */
"Save Image" = "Bild sichern";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Hashtag suchen: #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "Suchen...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "Geheimer Konto-Anmeldeschlüssel";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "Wähle ein Lightning Wallet";
/* Prompt selection of user's default wallet */
"Select default wallet" = "Wähle ein voreingestelltes Wallet";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Sende eine Nachricht um eine Unterhaltung zu beginnen...";
/* Prompt selection of LibreTranslate server to perform machine translations on notes */
"Server" = "Server";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Einstellungen";
/* Button to share an image.
Button to share the link to a profile. */
"Share" = "Teilen";
/* Button to show a post from a user who has been blocked.
Toggle to show or hide user's secret account login key. */
"Show" = "Anzeigen";
/* Button to hide the LibreTranslate server API key. */
"Show API Key" = "API Schlüssel anzeigen";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Wallet-Auswahl anzeigen";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "Abmelden";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* Button to close out of alert that informs that the action to block a user was successful. */
"Thanks!" = "Danke!";
/* Button for user to report that the account is impersonating someone. */
"They are impersonating someone" = "Die geben sich für jemand anderen aus";
/* Warning that the inputted account key is a public key and the result of what happens because of it. */
"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." = "Dies ist ein öffentlicher Schlüssel, Du wirst keine Beiträge teilen oder oder auf irgendeine Weise interagieren können. Dies wird genutzt um andere Kontos aus deren Perspektive zu sehen.";
/* Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key. */
"This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key." = "Dies ist ein nostr-Schlüsse im veralteten Format. Wir sind nicht sicher ob es ein öffentlicher Schlüssel oder ein privater Schlüssel ist. Bitte betätige die untenstehende Schaltfläche wenn es ein öffentlicher Schlüssel ist.";
/* Label to describe that a public key is the user's account ID and what they can do with it. */
"This is your account ID, you can give this to your friends so that they can follow you. Click to copy." = "Dies ist deine Konto-ID, die du an deine Freunde weitergeben kannst, damit sie dir folgen können. Zum Kopieren anklicken.";
/* Label to describe that a private key is the user's secret account key and what they should do with it. */
"This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" = "Dies ist dein geheimer, privater Schlüssel. Du benötigst ihn, um auf dein Konto zuzugreifen. Gib den privaten Schlüssel an niemanden weiter! Speichere ihn in einem Passwort-Manager und bewahre ihn sicher auf!";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Thema";
/* Button to translate note from different language. */
"Translate Note" = "Notiz übersetzen";
/* Button to indicate that the note has been translated from a different language. */
"Translated from (languageName!)" = "Übersetzt aus (languageName!)";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Schreibe deinen Beitrag hier...";
/* Non-breaking space character to fill in blank space next to event action button icons. */
"u{00A0}" = "u{00A0}";
/* Button to unfollow a user. */
"Unfollow" = "Entfolgen";
/* Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile. */
"Unfollowing" = "Entfolge...";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "Entfolgen...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Entfolgen";
/* Example URL to LibreTranslate server */
"URL" = "URL";
/* Alert message to indicate the user has been blocked */
"User blocked" = "Benutzer blockiert";
/* Alert message that informs a user was blocked. */
"User has been blocked" = "Der Benutzer wurde blockiert";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Benutzername";
/* Sidebar menu label for Wallet view. */
"Wallet" = "Wallet";
/* Dropdown option label for Lightning wallet, Wallet of Satoshi. */
"Wallet of Satoshi" = "Wallet of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "Wallet-Auswahl";
/* Label for Website section of user profile form. */
"Website" = "Website";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "Willkommen in dem sozialen Netzwerk das %@ kontrollierst.";
/* Text to welcome user. */
"Welcome, %@!" = "Willkommen, %@!";
/* Header text to prompt user what issue they want to report. */
"What do you want to report?" = "Was möchtest du melden?";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://some.relay.com";
/* Text of button that confirms to overwrite the existing mutelist. */
"Yes, Overwrite" = "Ja, überschreiben";
/* 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. */
"you" = "Du";
/* Label for Your Name section of user profile form. */
"Your Name" = "Dein Name";
/* Footer text to inform user what will happen when the report is submitted. */
"Your report will be sent to the relays you are connected to" = "Die Meldung wird an Relays versendet, mit denen du verbunden bist";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";

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