Compare commits

...

139 Commits

Author SHA1 Message Date
tyiu 2c3ac64091 Add export and import translation scripts 2023-01-29 12:28:29 -05:00
William Casarin 7bcc345038 Fix build 2023-01-28 15:54:52 -08:00
William Casarin bf0f879d66 revert dubious change 2023-01-28 15:53:04 -08:00
William Casarin 3af9131afe autocomplete: add space after replacing occurances
This is more intuitive
2023-01-28 15:50:42 -08:00
Swift b6b6d033a8 User tagging and autocompletion
Co-authored-by: William Casarin <jb55@jb55.com>
Changelog-Added: User tagging and autocompletion in posts
Closes: #347
Fixes: #411, #63
2023-01-28 15:43:45 -08:00
William Casarin 819d7496b2 v1.0.0-12 changelog 2023-01-28 10:54:49 -08:00
William Casarin 4c58e73e18 v1.0.0-12 2023-01-28 10:52:34 -08:00
William Casarin 6e38707aaa Merge remote-tracking branches 'ar' and 'pt_PT' 2023-01-28 09:55:02 -08:00
William Casarin 0f08612b79 Added arabic and portugese translations
Changelog-Added: Added arabic and portugese translations
2023-01-28 09:42:32 -08:00
transifex-integration[bot] ef89c4b33b Apply translations in pt_PT
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pt_PT' language.
2023-01-28 17:41:01 +00:00
transifex-integration[bot] 5c9bc02ac6 Apply translations in ar
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'ar' language.
2023-01-28 17:40:50 +00:00
Joel Klabo b57d2a3a6e Use AttributedString in NoteContentView
Changelog-Changed: Remove markdown link support from posts
Closes: #398
2023-01-28 09:38:38 -08:00
OlegAba 0e8c94b668 Replace SVGKit package with CoreSVG
Changelog-Fixed: Fixed crash on some SVG profile pictures
Closes: #416
2023-01-28 09:38:22 -08:00
transifex-integration[bot] 3e6c8c47a7 Apply translations in pt_PT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pt_PT' language.
2023-01-28 17:00:11 +00:00
ericholguin e4beb872a5 Add QRCode view
Changelog-Added: Add QRCode view for sharing your pubkey
Closes: #418
2023-01-28 08:36:53 -08:00
William Casarin 552bd9cae5 Implement NIP-21 URI handling
Changelog-Added: Added nostr: uri handling
2023-01-28 08:31:11 -08:00
transifex-integration[bot] 059a16a8dc Apply translations in ar
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'ar' language.
2023-01-28 11:15:51 +00:00
William Casarin b6ea17a0eb Merge remote-tracking branches 'tyiu/tyiu/string-comments' and 'tyiu/tyiu/remove-it-CH'
Changelog-Fixed: Localization fixes
Changelog-Fixed: Don't allow blocking yourself
2023-01-27 12:49:24 -08:00
William Casarin a9e9f0dc8f Hide muted users from global
Changelog-Fixed: Hide muted users from global
2023-01-27 12:16:41 -08:00
William Casarin 5edb7df5c4 Mute events in threads
Changlog-Added: Mute events in threads
2023-01-27 12:11:57 -08:00
William Casarin d559dd3a13 Allow non-string values in profiles
Profiles were not loading from other clients because some fields were
not strings

Changelog-Fixed: Fixed profiles sometimes not loading from other clients
2023-01-27 10:19:29 -08:00
William Casarin b9c2473a2d reporting: don't use spam for every report
Changelog-Fixed: Fixed bug where `spam` was always the report type
2023-01-27 10:18:48 -08:00
William Casarin 196081cd38 mutelists: #d must be an array
Not sure how this was working before
2023-01-27 09:36:04 -08:00
tyiu 3e02cc6889 Prevent blocking or reporting yourself 2023-01-27 00:56:32 -05:00
tyiu 51f94cf135 Add missing comments to localized strings, reorder buttons, mark destructive buttons 2023-01-27 00:56:25 -05:00
tyiu a20fa08030 Remove it-CH locale as there is no difference with it-IT yet 2023-01-26 10:01:42 -05:00
William Casarin 203203a706 v1.0.0-11 changelog 2023-01-25 16:13:12 -08:00
William Casarin 92239eae69 v1.0.0-11 2023-01-25 16:12:19 -08:00
OlegAba 5de745fb19 Add double tap gesture and fix bugs
Changlog-Added: Image double-tap gesture
Closes: #397
2023-01-25 16:10:18 -08:00
Steven Briscoe 1baae90beb Fix to allow relays using ws://
Changelog-Fixed: allow ws:// relays again
2023-01-25 16:05:02 -08:00
Joel Klabo 2b832120ec Disable UI Tests 2023-01-25 16:04:15 -08:00
William Casarin 255668c17a Merge remote-tracking branch translation/ar 2023-01-25 16:03:29 -08:00
tyiu c046c7cf45 Add Reposts view
Changelog-Added: Reposts view
Closes: #376
2023-01-25 16:00:00 -08:00
William Casarin 5daaec35a8 Bump pfp/banner animated fize size limit to 5MiB/20MiB
Changelog-Changed: Bump pfp/banner animated fize size limit to 5MiB/20MiB
Closes: #384
2023-01-25 15:57:37 -08:00
William Casarin abfbc8c9aa Merge branch 'translations'
Changelog-Added: Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV
2023-01-25 15:53:19 -08:00
William Casarin 44b1136b86 Merge remote-tracking branch 'it_IT' 2023-01-25 15:52:08 -08:00
William Casarin dc28456122 Merge remote-tracking branch 'it_CH' 2023-01-25 15:50:59 -08:00
William Casarin 0dd804f61c Merge remote-tracking branches 'fr_FR', 'de_DE', 'de_AT' and 'lv_LV' 2023-01-25 15:49:12 -08:00
William Casarin a3e7abc85d Update kieran's relay addr 2023-01-25 15:46:59 -08:00
Ricardo Arturo Cabral Mejía d61d7df91b Remove wlvs.space relay, add eden.nostr.land
Phasing out nostr-relay.wlvs.space in favor of eden.nostr.land. Eden
relay is load balanced by geographical proximity and can handle more
clients.

Changelog-Changed: Updated default boostrap relays
2023-01-25 15:46:34 -08:00
William Casarin 5e3ce4e454 v1.0.0-10 changelog 2023-01-25 15:40:04 -08:00
William Casarin 59abc7b608 v1.0.0-10 2023-01-25 15:36:28 -08:00
William Casarin 74d8d57542 Add EULA step to account creation 2023-01-25 15:34:33 -08:00
William Casarin 214e45a98b Add muting and mutelists
- Filter muted posts from feed on mute
- List muted users in sidebar

Changelog-Added: Added ability to block users
2023-01-25 12:50:04 -08:00
William Casarin 2a8b9f75c1 Initial NIP-51 Mute List Implementation 2023-01-25 09:53:59 -08:00
William Casarin 7d323b65e4 Move share and report actions into a ... button 2023-01-25 08:41:05 -08:00
William Casarin b69116e685 Move share button
To avoid the weird hit detection issues
2023-01-25 08:24:40 -08:00
William Casarin 561e2cd3ad refactor: add profile_button_style view extension 2023-01-25 08:24:35 -08:00
William Casarin ad87a62486 [appstore] Report Content
This view provides a way to report content (nudity, illegal, spam) to
relays. Clients can use this information to filter or warn if they
choose to.

This is needed for the appstore release

Changelog-Added: Added a way to report content
2023-01-25 08:11:21 -08:00
transifex-integration[bot] 5793db4053 Apply translations in ar
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'ar' language.
2023-01-25 08:03:22 +00:00
tyiu e736f8f837 Import localization for fr_FR 2023-01-24 21:35:12 -05:00
transifex-integration[bot] 81c1993156 Apply translations in fr_FR
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'fr_FR' language.
2023-01-24 13:40:24 +00:00
tyiu 4d97dbcacf Import localizations for it_CH 2023-01-23 16:01:32 -05:00
tyiu af72cf4e06 Import localization for it_IT 2023-01-23 16:00:57 -05:00
tyiu 55ba3f8c1b Import localization for de_DE 2023-01-23 16:00:12 -05:00
tyiu d7ab33e731 Import localization for de-AT 2023-01-23 15:59:25 -05:00
tyiu 1203b1d7fc Import localization for lv_LV 2023-01-23 15:58:07 -05:00
transifex-integration[bot] a62f3e2737 Apply translations in lv_LV
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'lv_LV' language.
2023-01-23 20:47:32 +00:00
Swift 209a1c3213 Create stretchable profile cover header
Closes: #250
Changelog-Added: Stretchable profile cover header
2023-01-23 12:33:30 -08:00
William Casarin 675903b768 SelectedEventView: use EventProfile 2023-01-23 12:30:29 -08:00
William Casarin 92035e17d3 refactor: move reply_desc to ReplyDescription 2023-01-23 12:20:02 -08:00
William Casarin 8df5bf04ae refactor: Break EventView into 3 separate views
SelectedEventView
EmbeddedEventView
EventView
2023-01-23 12:13:58 -08:00
transifex-integration[bot] cf79fd9491 Apply translations in de_AT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_AT' language.
2023-01-23 19:27:44 +00:00
transifex-integration[bot] 56d43f1ad1 Apply translations in de_DE
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_DE' language.
2023-01-23 19:27:40 +00:00
William Casarin 7e1daf7816 refactor: move Highlight to its own file 2023-01-23 10:58:39 -08:00
William Casarin 0ead583bda refactor: move BuilderEventView to it's own file 2023-01-23 10:58:39 -08:00
William Casarin dd44bd779b EventView: hide extra previews
Getting too busy
2023-01-23 10:58:39 -08:00
William Casarin c31374fc0a build 9 2023-01-23 10:28:36 -08:00
transifex-integration[bot] 984a1b916d Apply translations in it_CH
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'it_CH' language.
2023-01-23 15:36:11 +00:00
transifex-integration[bot] b8cefb9392 Apply translations in it_IT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'it_IT' language.
2023-01-23 15:26:15 +00:00
William Casarin f3730630b5 Merge tyiu/string-fixes, transifex/de_AT, transifex/de_DE
More translation stuff from Terry
2023-01-22 19:36:51 -08:00
tyiu d5f2a17249 Import localization for de-AT 2023-01-22 21:57:47 -05:00
tyiu 4526ed01fe Import localization for de-DE 2023-01-22 21:56:35 -05:00
transifex-integration[bot] a590fb099d Apply translations in de_AT
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_AT' language.
2023-01-23 02:54:31 +00:00
transifex-integration[bot] 135814737c Apply translations in de_DE
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_DE' language.
2023-01-23 02:54:20 +00:00
tyiu e2e60639d9 Remove unused language mapping from transifex.yml 2023-01-22 21:53:30 -05:00
tyiu 83909c8fc9 Change use of DM to be consistent and add missing localization comments 2023-01-22 21:50:28 -05:00
William Casarin 3e4914462b EventDetailBar disappeared. Let's fix that before (8) 2023-01-22 10:37:33 -08:00
William Casarin 7a11433a98 v1.0.0-8 changelog 2023-01-22 10:28:51 -08:00
William Casarin 03e1c1903f v1.0.0-8 2023-01-22 10:27:35 -08:00
William Casarin acdee6a326 Show Website on profiles
Changelog-Added: Show website on profiles
2023-01-22 10:25:12 -08:00
William Casarin f5e03f145c Fix build 2023-01-22 10:10:54 -08:00
Joel Klabo 2a9ddd10c8 Choose Participants on a Thread Reply
Closes: #345
Changelog-Added: Add the ability to choose participants when replying
2023-01-22 10:10:13 -08:00
tyiu 5e9580377d Fix localized string for privacy access description for photos
Closes: #359
2023-01-22 10:00:09 -08:00
William Casarin 9d2ff2fe65 Fix commas and emojis getting included in hashtags
Changelog-Fixed: Fix commands and emojis getting included in hashtags
2023-01-22 09:57:00 -08:00
Joel Klabo 13ea42a2e2 Don't Parse URL with Only Whitespace 2023-01-22 09:49:41 -08:00
William Casarin d9e22ce7bf Add translations for de_AT, de_DE, tr_TR, fr_FR
Changelog-Added: Translations for de_AT, de_DE, tr_TR, fr_FR
2023-01-22 09:43:51 -08:00
William Casarin 2335a65b78 Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_fr_FR' 2023-01-22 09:42:57 -08:00
William Casarin 566cd141ce Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_tr_TR' 2023-01-22 09:42:06 -08:00
William Casarin 55f7f8c072 Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_de_DE' 2023-01-22 09:40:46 -08:00
William Casarin 4b4addd215 Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_de_AT' 2023-01-22 09:39:51 -08:00
Joel Klabo 255a0c55ba Update CI Specify Xcode Version
Closes: #360
2023-01-22 09:26:29 -08:00
Thomas Rademaker ced6e2488f Fix duplicate post buttons when swiping tabs
Move the postButtonContainer view to avoid having 2 buttons on screen when swiping tabs

Closes: #366
Changelog-Fixed: Fix duplicate post buttons when swiping tabs
2023-01-22 09:25:16 -08:00
Thomas Rademaker 77c2abc524 fix broken test 2023-01-22 09:25:16 -08:00
Joel Klabo e4ad15ced1 Center PFP in Zoom View
Closes: #357
Changlog-Fixed: Center profile picture in zoom view
2023-01-22 09:23:11 -08:00
Joel Klabo 904a6e960a Cleanup X Padding and Size 2023-01-22 09:23:11 -08:00
tyiu da8a82954a Import localization for fr-FR 2023-01-21 20:59:57 -05:00
tyiu 383f45fe96 Import localization for de-DE 2023-01-21 20:58:21 -05:00
tyiu 9dc0f3baf6 Import localization for de-AT 2023-01-21 20:57:34 -05:00
tyiu abc857582f Import localization for tr-TR 2023-01-21 20:54:33 -05:00
transifex-integration[bot] 4816b57dcd Apply translations in de_AT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_AT' language.
2023-01-21 23:34:52 +00:00
transifex-integration[bot] 06b1953b49 Apply translations in de_DE
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'de_DE' language.
2023-01-21 23:33:33 +00:00
William Casarin d658d1d987 DM Message Requests
Put strangers in a different tab

Changelog-Added: Add DM Message Requests
2023-01-21 11:13:44 -08:00
William Casarin e14cd99c85 Add first_eref_mention helper to refactor embedded builder 2023-01-21 10:29:40 -08:00
William Casarin 0258ef792f Show embedded note references
This reverts commit 3f3b78f9bc.

Changelog-Fixed: Show embedded note references
2023-01-21 09:57:29 -08:00
transifex-integration[bot] 52524e00a2 Apply translations in tr_TR
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'tr_TR' language.
2023-01-21 09:10:29 +00:00
transifex-integration[bot] 342883fbb0 Apply translations in fr_FR
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'fr_FR' language.
2023-01-20 22:00:04 +00:00
William Casarin 1e6505abe3 v1.0.0-7 changelog 2023-01-20 09:35:51 -08:00
OlegAba 98c24147e8 Drastically improve image viewer
Changelog-Added: Drastically improved image viewer
Closes: #349
2023-01-20 09:33:22 -08:00
Joel Klabo d1e7de5dcb Add Preview with 999 Everything 2023-01-20 09:23:42 -08:00
Joel Klabo 11e0a87f06 Fix ... when too many likes/reposts
Closes: #351
Changelog-Fixed: Fix ... when too many likes/reposts
2023-01-20 09:23:14 -08:00
Swift 1154cec719 Avoid showing reboost alert for pubkey user
Closes: #337
Changelog-Fixed: Don't show report alert if logged in as a pubkey
2023-01-19 10:10:07 -08:00
Joel Klabo 1ecfb0487e Swipe to Dismiss Images
Closes: #343
2023-01-18 13:36:57 -08:00
Swift 8ffa8446b6 Image Pinch Zooming
Changelog-Added: Added pinch to zoom on images
2023-01-18 10:10:22 -08:00
Ben Weeks fb1f99e728 Fixed Jack's issue with homepage gap at the top.
Closes: #328
Changelog-Fixed: Fix padding issue at top of home timeline
2023-01-18 09:59:26 -08:00
Zach Hendel 031408dec3 Makes both name and @username clickable to go to profile
Changelog-Changed: Makes both name and username clickable in sidebar to go to profile
Closes: #329
2023-01-18 09:54:18 -08:00
Zach Hendel 16b6d029fa Icons for hardclick menu event view
changes copy user id icon to person
changes copy note id icon to note.text
changes copy note json icon to magnifying glass

Closes: #330
2023-01-18 09:48:58 -08:00
Brandon Holm 3e093e8572 Added Blixt Wallet TestFlight link
Closes: #331
2023-01-18 09:48:49 -08:00
John Bethancourt fa11af4b1d Prevent absurdly large sidebar on Mac or iPad
Closes: #338
Changelog-Fixed: Fix absurdly large sidebar on Mac/iPad
2023-01-18 09:39:35 -08:00
William Casarin 91159d70ca Merge remote-tracking branch 'github/translations_damus-localizations-en-us-xcloc-localized-contents-en-us-xliff--master_es_419'
Changelog-Added: Add Latin American Spanish translations
2023-01-18 09:30:53 -08:00
OlegAba a57d654f32 Fix tab views moving after selecting from search result
Closes: #339
Changelog-Fixed: Fix tab views moving after selecting from search result
2023-01-18 09:29:30 -08:00
OlegAba 0313480685 Change navigation title to bold
Closes: #341
2023-01-18 09:29:04 -08:00
OlegAba e07b31e0a1 Consistent follow/unfollow button width
Changelog-Fixed: Make follow/unfollow button a consistent width
2023-01-18 09:28:29 -08:00
tyiu 538a0ae5ea Import es-419 localization files 2023-01-16 20:44:41 -05:00
tyiu 0f82db2440 Delete unused exported es-419 localization 2023-01-16 20:44:11 -05:00
transifex-integration[bot] 92ae2c7754 Apply translations in es_419
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'es_419' language.
2023-01-16 16:59:06 +00:00
William Casarin 00c819140b Don't include garbage in notifications
Check to make sure we have our pubkey on events coming into
notifications. Sometimes relays are buggy.

Changelog-Fixed: Don't add events to notifications from buggy relays
2023-01-15 12:53:37 -08:00
OlegAba cbc3c46c9d Fix image crash and support SVG profile pictures
Closes: #310
Changelog-Fixed: Fixed some crashes with large images
Changelog-Added: Added SVG profile picture support
2023-01-14 19:30:54 -08:00
radixrat 4b5c34b4e2 Click pfp in side menu should open profile as well
Closes: #323
Changelog-Changed: Clicking pfp in sidebar opens profile as well
2023-01-14 18:49:36 -08:00
ericholguin 9eb39f7e0a Don't blur images if your friend boosted it
Closes: #322
Changelog-Changed: Don't blur images if your friend boosted it
2023-01-14 18:47:45 -08:00
tyiu 173b22b772 localization: fix string bugs
Closes: #321
2023-01-14 18:44:11 -08:00
William Casarin 18c7cba53c Minor refactor 2023-01-14 17:24:03 -08:00
William Casarin 9a40fd595d Add some DM sorting tests
They didn't help me fix the problem, but maybe they are still useful
somehow
2023-01-14 17:23:35 -08:00
William Casarin a71c35a6b0 Fix DM sorting bug
Changelog-Fixed: Fix DM sorting on incoming messages
2023-01-14 17:21:44 -08:00
William Casarin d69d3cc74e create_dm: allow created_at argument
This is mainly used by tests
2023-01-14 16:28:18 -08:00
William Casarin 6e220ac4c1 dms: extract incoming DM handling into pure functions
So that it will be easier to test
2023-01-14 09:48:31 -08:00
Ben Weeks aba758b143 Fixes damus-io/damus#246 ChangeLog-Changed: Resolved issue with image URLs in uppercase (@npub1jutptdc2m8kgjmudtws095qk2tcale0eemvp4j2xnjnl4nh6669slrf04x) 2023-01-06 00:39:57 +00:00
Ben Weeks b7c7b0b3bf Merge branch 'damus-io:master' into beautify-image-zoom 2023-01-06 00:25:15 +00:00
Ben Weeks 2d10d4592b Fixes damus-io/damus#254 Changes background to Color("DamusDarkGrey") ChangeLog-Changed: Allowed pinch and zoom on profile picture (scoder1747) ChangeLog-Changed: Allowed pinch and zoom to post pictures (@npub1jutptdc2m8kgjmudtws095qk2tcale0eemvp4j2xnjnl4nh6669slrf04x) 2023-01-06 00:16:48 +00:00
123 changed files with 18857 additions and 1020 deletions
+22 -8
View File
@@ -1,4 +1,6 @@
name: Test
name: Run Test Suite
run-name: Testing ${{ github.ref }} by @${{ github.actor }}
on:
push:
branches:
@@ -6,12 +8,24 @@ on:
pull_request:
branches:
- "*"
jobs:
test:
name: Run Tests
runs-on: macos-latest
run_tests:
runs-on: macos-12
strategy:
matrix:
include:
- xcode: "14.2"
ios: "16.2"
name: Test iOS (${{ matrix.ios }})
steps:
- 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]}
- 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]}
+106 -1
View File
@@ -1,3 +1,109 @@
## [1.0.0-12] - 2023-01-28
### Added
- Added arabic and portugese translations (William Casarin)
- 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 (William Casarin)
- 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 (William Casarin)
- 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 (William Casarin)
- Added SVG profile picture support (OlegAba)
### Changed
- Makes both name and username clickable in sidebar to go to profile (Zach Hendel)
- Clicking pfp in sidebar opens profile as well (radixrat)
- Don't blur images if your friend boosted it (ericholguin)
### Fixed
- Fix ... when too many likes/reposts (Joel Klabo)
- Don't show report alert if logged in as a pubkey (Swift)
- Fix padding issue at top of home timeline (Ben Weeks)
- Fix absurdly large sidebar on Mac/iPad (John Bethancourt)
- Fix tab views moving after selecting from search result (OlegAba)
- Make follow/unfollow button a consistent width (OlegAba)
- Don't add events to notifications from buggy relays (William Casarin)
- Fixed some crashes with large images (OlegAba)
- Fix DM sorting on incoming messages (William Casarin)
- Fix text getting truncated next to link previews (William Casarin)
[1.0.0-7]: https://github.com/damus-io/damus/releases/tag/v1.0.0-7
## [1.0.0-6] - 2023-01-13
### Added
@@ -384,4 +490,3 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
+34 -4
View File
@@ -91,15 +91,41 @@ damus implements the following [Nostr Implementation Possibilities][nips]
## Contributing
Contributors welcome! [Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on github as well.
Contributors welcome!
### Code
[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
## git log bot
### Translations
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
Translators welcome! Join the [Transifex][transifex] project.
### Awards
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
There may be nostr badges awarded for contributors in the future... :)
@@ -107,3 +133,7 @@ First contributors:
1. @randymcmillan
2. @jcarucci27
### git log bot
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
@@ -1,12 +0,0 @@
{
"developmentRegion" : "en-US",
"project" : "damus.xcodeproj",
"targetLocale" : "es-419",
"toolInfo" : {
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "14.2"
},
"version" : "1.0"
}
+24 -3
View File
@@ -22,6 +22,10 @@ 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;
@@ -29,18 +33,35 @@ static void make_cursor(struct cursor *c, const u8 *content, size_t len)
c->p = content;
}
static int consume_until_whitespace(struct cursor *cur, int or_end) {
static int consume_until_boundary(struct cursor *cur) {
char c;
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c))
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)
return 1;
cur->p++;
consumedAtLeastOne = true;
}
return or_end;
}
@@ -145,7 +166,7 @@ static int parse_hashtag(struct cursor *cur, struct block *block) {
return 0;
}
consume_until_whitespace(cur, 1);
consume_until_boundary(cur);
block->type = BLOCK_HASHTAG;
block->block.str.start = (const char*)(start + 1);
+255 -3
View File
@@ -12,6 +12,11 @@
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 */; };
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
@@ -122,6 +127,16 @@
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */; };
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 */; };
@@ -143,16 +158,37 @@
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 */; };
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 */; };
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 */; };
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; };
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 */
@@ -177,9 +213,32 @@
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>"; };
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>"; };
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>"; };
3AEABD20297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEABD21297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AEABD22297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "de-DE"; path = "de-DE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AEB8005297CCEA900713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "tr-TR"; path = "tr-TR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
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>"; };
@@ -321,6 +380,16 @@
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsView.swift; sourceTree = "<group>"; };
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>"; };
@@ -345,15 +414,36 @@
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>"; };
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>"; };
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; };
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 */
@@ -393,6 +483,14 @@
path = "Empty Views";
sourceTree = "<group>";
};
3AA24800297E3DAE0090C62D /* Reposts */ = {
isa = PBXGroup;
children = (
3AA24801297E3DC20090C62D /* RepostView.swift */,
);
path = Reposts;
sourceTree = "<group>";
};
4C06670728FDE62900038D2A /* damus-c */ = {
isa = PBXGroup;
children = (
@@ -450,6 +548,7 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */,
@@ -480,6 +579,9 @@
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -487,6 +589,10 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CF0ABF42985CD4200D66079 /* Posting */,
4CF0ABDF2981A83000D66079 /* Muting */,
4CC7AAEE297F11B300430951 /* Events */,
3AA24800297E3DAE0090C62D /* Reposts */,
4CB88394296F7F8100DC99E7 /* Reactions */,
4CB88387296AF97C00DC99E7 /* ActionBar */,
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
@@ -501,8 +607,8 @@
4C216F33286F5ACD00040376 /* DMView.swift */,
E990020E2955F837003BBC5A /* EditMetadataView.swift */,
3169CAE4294E699400EE4006 /* Empty Views */,
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */,
4C75EFB82804A2740006080F /* EventView.swift */,
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */,
4C3AC79E2833115300E1F516 /* FollowButtonView.swift */,
4C3AC79C2833036D00E1F516 /* FollowingView.swift */,
4C90BD17283A9EE5008EE7EF /* LoginView.swift */,
@@ -521,6 +627,7 @@
4C06670028FC7C5900038D2A /* RelayView.swift */,
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
@@ -534,6 +641,11 @@
647D9A8C2968520300A295DE /* SideMenuView.swift */,
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>";
@@ -561,6 +673,7 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4C3A1D322960DB0500558C0F /* Markdown.swift */,
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
@@ -575,6 +688,9 @@
4C3A1D3629637E0500558C0F /* PreviewCache.swift */,
64FBD06E296255C400D9D3B2 /* Theme.swift */,
4CB8838529656C8B00DC99E7 /* NIP05.swift */,
4CF0ABD72981980C00D66079 /* Lists.swift */,
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -596,6 +712,21 @@
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 = (
@@ -607,6 +738,10 @@
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */,
4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */,
4CB8838C296F710400DC99E7 /* Reposted.swift */,
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */,
4CC7AAEC297F0B9E00430951 /* Highlight.swift */,
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -636,12 +771,15 @@
4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup;
children = (
F7F0BA23297892AE009531F3 /* Modifiers */,
4C4A3A5A288A1B2200453788 /* damus.entitlements */,
4CE4F9DF285287A000C00DD9 /* Components */,
4C7FF7D628233637009601DB /* Util */,
4C0A3F8D280F63FF000448DE /* Models */,
4C75EFAB28049CC80006080F /* Nostr */,
4C75EFA72804823E0006080F /* Info.plist */,
3ACB685D297633BC00C46468 /* Localizable.strings */,
3ACB685A297633BC00C46468 /* InfoPlist.strings */,
4C75EFA227FA576C0006080F /* Views */,
4CE6DEE627F7A08100C66700 /* damusApp.swift */,
4CE6DEE827F7A08100C66700 /* ContentView.swift */,
@@ -670,6 +808,8 @@
4CE6DEF727F7A08200C66700 /* damusTests.swift */,
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */,
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
4CB88399297322D200DC99E7 /* DMTests.swift */,
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -691,6 +831,40 @@
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 = (
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */,
);
path = Modifiers;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -785,6 +959,12 @@
Base,
"es-419",
"en-US",
"de-AT",
"de-DE",
"tr-TR",
"fr-FR",
"lv-LV",
"it-IT",
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -809,7 +989,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */,
);
@@ -843,12 +1025,15 @@
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 */,
@@ -856,8 +1041,10 @@
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 */,
@@ -865,12 +1052,16 @@
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 */,
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
@@ -884,19 +1075,27 @@
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 */,
@@ -917,20 +1116,29 @@
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 */,
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 */,
@@ -946,9 +1154,11 @@
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 */,
@@ -956,13 +1166,17 @@
E990020F2955F837003BBC5A /* EditMetadataView.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 */,
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 */,
@@ -977,9 +1191,11 @@
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1013,10 +1229,44 @@
children = (
3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */,
3A5EA111297CCF6C00569477 /* de-AT */,
3AEABD22297CCFA8003F2975 /* de-DE */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A929C22297F2CF80090925E /* it-IT */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
};
3ACB685A297633BC00C46468 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
3ACB685B297633BC00C46468 /* es-419 */,
3A5EA10F297CCF6C00569477 /* de-AT */,
3AEABD20297CCFA8003F2975 /* de-DE */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A929C20297F2CF80090925E /* it-IT */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
3ACB685D297633BC00C46468 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
3ACB685E297633BC00C46468 /* es-419 */,
3A5EA110297CCF6C00569477 /* de-AT */,
3AEABD21297CCFA8003F2975 /* de-DE */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A929C21297F2CF80090925E /* it-IT */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
@@ -1148,7 +1398,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1156,6 +1406,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -1188,7 +1439,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1196,6 +1447,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -0,0 +1,98 @@
<?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>
+39
View File
@@ -0,0 +1,39 @@
//
// 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
}
}
}
+143 -42
View File
@@ -12,14 +12,14 @@ import Kingfisher
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [URL]
let activityItems: [URL?]
let callback: Callback? = nil
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
activityItems: activityItems as [Any],
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
@@ -32,7 +32,7 @@ struct ShareSheet: UIViewControllerRepresentable {
}
struct ImageContextMenuModifier: ViewModifier {
let url: URL
let url: URL?
let image: UIImage?
@Binding var showShareSheet: Bool
@@ -64,8 +64,21 @@ struct ImageContextMenuModifier: ViewModifier {
}
}
struct ImageViewer: View {
let urls: [URL]
private struct ImageContainerView: View {
@ObservedObject var imageModel: KFImageModel
@State private var image: UIImage?
@State private var showShareSheet = false
init(url: URL?) {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 2000000, // 2 MB
downsampleSize: CGSize(width: 400, height: 400)
)
}
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
@@ -75,45 +88,131 @@ struct ImageViewer: View {
return image
}
}
@State private var image: UIImage?
@State private var showShareSheet = false
func onShared(completed: Bool) -> Void {
if (completed) {
showShareSheet = false
var body: some View {
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.configure { view in
view.framePreloadCount = 1
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image))
.onFailure { _ in
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
.clipped()
.modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [imageModel.url])
}
// TODO: Update ImageCarousel with serializer and processor
// .serialize(by: imageModel.serializer)
// .setProcessor(imageModel.processor)
}
}
struct ImageView: View {
let urls: [URL?]
@Environment(\.presentationMode) var presentationMode
@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 {
Text(urls[selectedIndex]?.lastPathComponent ?? "")
.bold()
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
})
}
.padding()
Divider()
.ignoresSafeArea()
}
.background(.regularMaterial)
}
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
Capsule()
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
.frame(width: 7, height: 7)
}
}
.padding()
.background(.regularMaterial)
.clipShape(Capsule())
}
var body: some View {
TabView {
ForEach(urls, id: \.absoluteString) { url in
VStack{
Text(url.lastPathComponent)
KFAnimatedImage(url)
.configure { view in
view.framePreloadCount = 3
}
.cacheOriginalImage()
.imageModifier(ImageHandler(handler: $image))
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.aspectRatio(contentMode: .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [url])
}
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
.padding(.top, safeAreaInsets?.top)
.padding(.bottom, safeAreaInsets?.bottom)
}
.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
})
.gesture(TapGesture(count: 1).onEnded {
showMenu.toggle()
})
.overlay(
VStack {
if showMenu {
navBarView
Spacer()
if (urls.count > 1) {
tabViewIndicator
}
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, safeAreaInsets?.bottom)
)
}
.tabViewStyle(PageTabViewStyle())
}
}
@@ -130,13 +229,15 @@ struct ImageCarousel: View {
.foregroundColor(Color.clear)
.overlay {
KFAnimatedImage(url)
.configure { view in
view.framePreloadCount = 3
}
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.configure { view in
view.framePreloadCount = 3
}
.aspectRatio(contentMode: .fit)
.tabItem {
Text(url.absoluteString)
@@ -151,8 +252,8 @@ struct ImageCarousel: View {
}
}
.cornerRadius(10)
.sheet(isPresented: $open_sheet) {
ImageViewer(urls: urls)
.fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls)
}
.frame(height: 200)
.onTapGesture {
@@ -164,6 +265,6 @@ struct ImageCarousel: View {
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!])
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
}
}
+42
View File
@@ -0,0 +1,42 @@
//
// 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
@@ -0,0 +1,38 @@
//
// 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
@@ -0,0 +1,152 @@
//
// 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)
}
}
}
+149 -22
View File
@@ -11,11 +11,11 @@ import Kingfisher
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://nostr-relay.wlvs.space",
"wss://eden.nostr.land",
"wss://nostr.fmt.wiz.biz",
"wss://relay.nostr.bg",
"wss://nostr.oxtr.dev",
"wss://nostr.v0l.io",
"wss://relay.snort.social",
"wss://brb.io",
]
@@ -26,10 +26,12 @@ 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
}
@@ -79,6 +81,10 @@ 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()
@@ -93,17 +99,25 @@ struct ContentView: View {
var PostingTimelineView: some View {
VStack {
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)
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
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.safeAreaInset(edge: .top) {
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
FiltersView
//.frame(maxWidth: 275)
@@ -113,7 +127,6 @@ struct ContentView: View {
}
.background(colorScheme == .dark ? Color.black : Color.white)
}
.ignoresSafeArea(.keyboard)
}
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
@@ -121,11 +134,6 @@ 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
}
}
}
}
@@ -172,17 +180,27 @@ struct ContentView: View {
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Global", comment: "Navigation bar title for Global view where posts from all connected relay servers appear."), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .principal) {
if selected_timeline == .home {
switch selected_timeline {
case .home:
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
} else {
Text("Global")
case .dms:
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
.bold()
case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
}
}
}
.ignoresSafeArea(.keyboard)
}
var MaybeSearchView: some View {
@@ -217,6 +235,20 @@ 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 {
@@ -267,8 +299,10 @@ 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: [])
PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event):
ReplyView(replying_to: event, damus: damus_state!)
}
@@ -314,6 +348,15 @@ struct ContentView: View {
}
.onReceive(handle_notify(.like)) { like in
}
.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))
@@ -388,6 +431,90 @@ 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("User blocked", comment: "Alert message to indicate "), isPresented: $user_blocked_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) {
user_blocked_confirm = false
}
}, 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) {
-2
View File
@@ -15,8 +15,6 @@
</array>
</dict>
</array>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>&quot;Granting Damus access to your photo library allows you to save photos.</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>river</string>
+38
View File
@@ -11,13 +11,51 @@ 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())
+6 -1
View File
@@ -22,8 +22,13 @@ struct DamusState {
var pubkey: String {
return keypair.pubkey
}
var is_privkey_user: Bool {
keypair.privkey != nil
}
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(), previews: PreviewCache())
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache())
}
}
+26 -5
View File
@@ -8,13 +8,34 @@
import Foundation
class DirectMessageModel: ObservableObject {
@Published var events: [NostrEvent]
init(events: [NostrEvent]) {
self.events = events
@Published var events: [NostrEvent] {
didSet {
is_request = determine_is_request()
}
}
init() {
var is_request: Bool
var our_pubkey: String
func determine_is_request() -> Bool {
for event in events {
if event.pubkey == our_pubkey {
return false
}
}
return true
}
init(events: [NostrEvent], our_pubkey: String) {
self.events = events
self.is_request = false
self.our_pubkey = our_pubkey
}
init(our_pubkey: String) {
self.events = []
self.is_request = false
self.our_pubkey = our_pubkey
}
}
+14 -1
View File
@@ -10,13 +10,26 @@ import Foundation
class DirectMessagesModel: ObservableObject {
@Published var dms: [(String, DirectMessageModel)] = []
@Published var loading: Bool = false
let our_pubkey: String
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
}
var message_requests: [(String, DirectMessageModel)] {
return dms.filter { dm in dm.1.is_request }
}
var friend_dms: [(String, DirectMessageModel)] {
return dms.filter { dm in !dm.1.is_request }
}
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
if let dm = lookup(pubkey) {
return dm
}
let new = DirectMessageModel()
let new = DirectMessageModel(our_pubkey: our_pubkey)
dms.append((pubkey, new))
return new
}
+133 -63
View File
@@ -48,17 +48,19 @@ class HomeModel: ObservableObject {
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications: [NostrEvent] = []
@Published var dms: DirectMessagesModel = DirectMessagesModel()
@Published var dms: DirectMessagesModel
@Published var events: [NostrEvent] = []
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
init() {
self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
}
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
}
var pool: RelayPool {
@@ -96,6 +98,8 @@ 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:
@@ -122,6 +126,12 @@ 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
@@ -272,7 +282,11 @@ 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,
])
@@ -309,7 +323,7 @@ class HomeModel: ObservableObject {
var home_filters = [home_filter]
var notifications_filters = [notifications_filter]
var contacts_filters = [contacts_filter, our_contacts_filter]
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
var dms_filters = [dms_filter, our_dms_filter]
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
@@ -333,7 +347,30 @@ 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(profiles: damus_state.profiles, ev: ev)
}
@@ -347,24 +384,23 @@ class HomeModel: ObservableObject {
return m[kind]
}
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) {
let last_ev = get_last_event(timeline)
if last_ev == nil || last_ev!.created_at < ev.created_at {
save_last_event(ev, timeline: timeline)
if shouldNotify {
new_events = NewEventsBits(prev: new_events, setting: timeline)
}
}
}
func handle_notification(ev: NostrEvent) {
guard event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey) else {
return
}
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
return
}
handle_last_event(ev: ev, timeline: .notifications)
}
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) {
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
new_events = new_bits
}
}
func insert_home_event(_ ev: NostrEvent) -> Bool {
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
@@ -374,12 +410,8 @@ 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(ev) {
if should_hide_event(contacts: damus_state.contacts, ev: ev) {
return
}
@@ -391,49 +423,8 @@ class HomeModel: ObservableObject {
}
func handle_dm(_ ev: NostrEvent) {
var inserted = false
var found = false
let ours = ev.pubkey == self.damus_state.pubkey
var i = 0
var the_pk = ev.pubkey
if ours {
if let ref_pk = ev.referenced_pubkeys.first {
the_pk = ref_pk.ref_id
} else {
// self dm!?
print("TODO: handle self dm?")
}
}
for (pk, _) in dms.dms {
if pk == the_pk {
found = true
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
$0.created_at < $1.created_at
}
break
}
i += 1
}
if !found {
inserted = true
let model = DirectMessageModel(events: [ev])
dms.dms.append((the_pk, model))
}
if inserted {
handle_last_event(ev: ev, timeline: .dms, shouldNotify: !ours)
dms.dms = dms.dms.sorted { a, b in
if a.1.events.count > 0 && b.1.events.count > 0 {
return a.1.events.last!.created_at > b.1.events.last!.created_at
}
return false
}
if let notifs = handle_incoming_dm(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
self.new_events = notifs
}
}
}
@@ -656,4 +647,83 @@ func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_
}
}
func handle_incoming_dm(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? {
var inserted = false
var found = false
let ours = ev.pubkey == our_pubkey
var i = 0
var the_pk = ev.pubkey
if ours {
if let ref_pk = ev.referenced_pubkeys.first {
the_pk = ref_pk.ref_id
} else {
// self dm!?
print("TODO: handle self dm?")
}
}
for (pk, _) in dms.dms {
if pk == the_pk {
found = true
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
$0.created_at < $1.created_at
}
break
}
i += 1
}
if !found {
inserted = true
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
dms.dms.append((the_pk, model))
}
var new_events: NewEventsBits? = nil
if inserted {
new_events = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
return a.1.events.last!.created_at > b.1.events.last!.created_at
}
}
return new_events
}
/// A helper to determine if we need to notify the user of new events
func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? {
let last_ev = get_last_event(timeline)
if last_ev == nil || last_ev!.created_at < ev.created_at {
save_last_event(ev, timeline: timeline)
if shouldNotify {
return NewEventsBits(prev: new_events, setting: timeline)
}
}
return nil
}
/// Sometimes we get garbage in our notifications. Ensure we have our pubkey on this event
func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
for tag in ev.tags {
if tag.count >= 2 && tag[0] == "p" && tag[1] == our_pubkey {
return true
}
}
return false
}
func should_hide_event(contacts: Contacts, ev: NostrEvent) -> Bool {
if contacts.is_muted(ev.pubkey) {
return true
}
return !ev.should_show_event
}
+114
View File
@@ -0,0 +1,114 @@
//
// KFImageModel.swift
// damus
//
// Created by Oleg Abalonski on 1/11/23.
//
import UIKit
import Kingfisher
class KFImageModel: ObservableObject {
let url: URL?
let fallbackUrl: URL?
let processor: ImageProcessor
let serializer: CacheSerializer
@Published var refreshID = ""
init(url: URL?, fallbackUrl: URL?, maxByteSize: Int, downsampleSize: CGSize) {
self.url = url
self.fallbackUrl = fallbackUrl
self.processor = CustomImageProcessor(maxSize: maxByteSize, downsampleSize: downsampleSize)
self.serializer = CustomCacheSerializer(maxSize: maxByteSize, downsampleSize: downsampleSize)
}
func refresh() -> Void {
DispatchQueue.main.async {
self.refreshID = UUID().uuidString
}
}
func cache(_ image: UIImage, forKey key: String) -> Void {
KingfisherManager.shared.cache.store(image, forKey: key, processorIdentifier: processor.identifier) { _ in
self.refresh()
}
}
func downloadFailed() -> Void {
guard let url = url, let fallbackUrl = fallbackUrl else { return }
DispatchQueue.global(qos: .background).async {
KingfisherManager.shared.downloader.downloadImage(with: fallbackUrl) { result in
var fallbackImage: UIImage {
switch result {
case .success(let imageLoadingResult):
return imageLoadingResult.image
case .failure(let error):
print(error)
return UIImage()
}
}
self.cache(fallbackImage, forKey: url.absoluteString)
}
}
}
}
struct CustomImageProcessor: ImageProcessor {
let maxSize: Int
let downsampleSize: CGSize
let identifier = "com.damus.customimageprocessor"
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(_):
// This case will never run
return DefaultImageProcessor.default.process(item: item, options: options)
case .data(let data):
// Handle large image size
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
// Handle SVG image
if let 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)
}
return DefaultImageProcessor.default.process(item: item, options: options)
}
}
}
struct CustomCacheSerializer: CacheSerializer {
let maxSize: Int
let downsampleSize: CGSize
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
return DefaultCacheSerializer.default.data(with: image, original: original)
}
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return DefaultCacheSerializer.default.image(with: data, options: options)
}
}
+1 -5
View File
@@ -198,11 +198,7 @@ enum Amount: Equatable {
let sats = NSNumber(value: (Double(amt) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
if formattedSats == numberFormatter.string(from: 1) {
return NSLocalizedString("\(formattedSats) sat", comment: "Amount of 1 sat.")
}
return NSLocalizedString("\(formattedSats) sats", comment: "Amount of sats.")
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
}
}
}
+18
View File
@@ -0,0 +1,18 @@
//
// ListModel.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import Foundation
/*
class MutelistModel: ObservableObject {
let contacts: Contacts
@Published var users: [String]
}
*/
+59
View File
@@ -0,0 +1,59 @@
//
// 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
@@ -0,0 +1,77 @@
//
// 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
}
}
}
+5 -1
View File
@@ -30,6 +30,10 @@ 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)
@@ -50,7 +54,7 @@ class SearchHomeModel: ObservableObject {
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
return
}
if ev.is_textlike && ev.should_show_event && !ev.is_reply(nil) {
if ev.is_textlike && !should_hide_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if seen_pubkey.contains(ev.pubkey) {
return
}
+1 -1
View File
@@ -75,7 +75,7 @@ enum Wallet: String, CaseIterable, Identifiable {
appStoreLink: "https://apps.apple.com/sv/app/bitcoin-beach-wallet/id1531383905", image: "bbw")
case .blixtwallet:
return .init(index: 11, tag: "blixtwallet", displayName: NSLocalizedString("Blixt Wallet", comment: "Dropdown option label for Lightning wallet, Blixt Wallet"), link: "blixtwallet:lightning:",
appStoreLink: nil, image: "blixt-wallet")
appStoreLink: "https://testflight.apple.com/join/EXvGhRzS", image: "blixt-wallet")
case .river:
return .init(index: 12, tag: "river", displayName: NSLocalizedString("River", comment: "Dropdown option label for Lightning wallet, River"), link: "river://",
appStoreLink: "https://apps.apple.com/us/app/river-buy-mine-bitcoin/id1536176542", image: "river")
+39
View File
@@ -0,0 +1,39 @@
//
// SwipeToDismiss.swift
// damus
//
// Created by Joel Klabo on 1/18/23.
//
import SwiftUI
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)
.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
}
}
.onEnded { _ in
if abs(offset.height) > 100 {
onDismiss()
} else {
offset = .zero
}
}
)
}
}
+45 -20
View File
@@ -8,7 +8,7 @@
import Foundation
struct Profile: Codable {
var value: [String: String]
var value: [String: AnyCodable]
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
self.value = [:]
@@ -23,44 +23,69 @@ struct Profile: Codable {
self.nip05 = nip05
}
private func str(_ str: String) -> String? {
guard let val = self.value[str] else{
return nil
}
guard let s = val.value as? String else {
return nil
}
return s
}
private mutating func set_str(_ key: String, _ val: String?) {
if val == nil {
self.value.removeValue(forKey: key)
return
}
self.value[key] = AnyCodable.init(val)
}
var display_name: String? {
get { return value["display_name"]; }
set(s) { value["display_name"] = s }
get { return str("display_name"); }
set(s) { set_str("display_name", s) }
}
var name: String? {
get { return value["name"]; }
set(s) { value["name"] = s }
get { return str("name"); }
set(s) { set_str("name", s) }
}
var about: String? {
get { return value["about"]; }
set(s) { value["about"] = s }
get { return str("about"); }
set(s) { set_str("about", s) }
}
var picture: String? {
get { return value["picture"]; }
set(s) { value["picture"] = s }
get { return str("picture"); }
set(s) { set_str("picture", s) }
}
var banner: String? {
get { return value["banner"]; }
set(s) { value["banner"] = s }
get { return str("banner"); }
set(s) { set_str("banner", s) }
}
var website: String? {
get { return value["website"]; }
set(s) { value["website"] = s }
get { return str("website"); }
set(s) { set_str("website", s) }
}
var lud06: String? {
get { return value["lud06"]; }
set(s) { value["lud06"] = s }
get { return str("lud06"); }
set(s) { set_str("lud06", s) }
}
var lud16: String? {
get { return value["lud16"]; }
set(s) { value["lud16"] = s }
get { return str("lud16"); }
set(s) { set_str("lud16", s) }
}
var website_url: URL? {
return self.website.flatMap { URL(string: $0) }
}
var lnurl: String? {
@@ -76,8 +101,8 @@ struct Profile: Codable {
}
var nip05: String? {
get { return value["nip05"]; }
set(s) { value["nip05"] = s }
get { return str("nip05"); }
set(s) { set_str("nip05", s) }
}
var lightning_uri: URL? {
@@ -86,7 +111,7 @@ struct Profile: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.value = try container.decode([String: String].self)
self.value = try container.decode([String: AnyCodable].self)
}
func encode(to encoder: Encoder) throws {
+44 -1
View File
@@ -27,7 +27,7 @@ struct KeyEvent {
let relay_url: String
}
struct ReferencedId: Identifiable, Hashable {
struct ReferencedId: Identifiable, Hashable, Equatable {
let ref_id: String
let relay_id: String?
let key: String
@@ -789,3 +789,46 @@ 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 {
return false
}
guard case .event = mention.type else {
return false
}
if mention.ref.key != "e" {
return false
}
return true
}
/// MARK: - Preview
if let firstBlock = blocks.first, case .mention(let mention) = firstBlock, mention.ref.key == "e" {
return 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"
}
}
}
}
+3 -1
View File
@@ -7,7 +7,7 @@
import Foundation
struct NostrFilter: Codable {
struct NostrFilter: Codable, Equatable {
var ids: [String]?
var kinds: [Int]?
var referenced_ids: [String]?
@@ -17,6 +17,7 @@ struct NostrFilter: Codable {
var limit: UInt32?
var authors: [String]?
var hashtag: [String]? = nil
var parameter: [String]? = nil
private enum CodingKeys : String, CodingKey {
case ids
@@ -24,6 +25,7 @@ struct NostrFilter: Codable {
case referenced_ids = "#e"
case pubkeys = "#p"
case hashtag = "#t"
case parameter = "#d"
case since
case until
case authors
+1
View File
@@ -19,4 +19,5 @@ enum NostrKind: Int {
case channel_create = 40
case channel_meta = 41
case chat = 42
case list = 30000
}
+30 -2
View File
@@ -8,7 +8,7 @@
import Foundation
enum NostrLink {
enum NostrLink: Equatable {
case ref(ReferencedId)
case filter(NostrFilter)
}
@@ -101,6 +101,24 @@ 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)
@@ -122,5 +140,15 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
return .filter(NostrFilter.filter_hashtag([parts[1].lowercased()]))
}
return tag_to_refid(parts).map { .ref($0) }
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)
}
+147
View File
@@ -0,0 +1,147 @@
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
@@ -0,0 +1,188 @@
#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
@@ -0,0 +1,291 @@
#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
@@ -0,0 +1,31 @@
//
// 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
@@ -0,0 +1,101 @@
//
// 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,12 +12,25 @@ 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
@@ -0,0 +1,91 @@
//
// 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
}
+12 -72
View File
@@ -11,150 +11,90 @@ 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")
}
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
+20 -38
View File
@@ -33,62 +33,41 @@ struct EventActionBar: View {
EventActionButton(img: "bubble.left", col: nil) {
notify(.reply, event)
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
HStack(alignment: .bottom) {
Spacer()
ZStack {
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
if bar.boosted {
notify(.delete, bar.our_boost)
} else {
} else if damus_state.is_privkey_user {
self.confirm_boost = true
}
}.overlay {
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.offset(x: 22)
.font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray)
}
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.offset(x: 18)
.font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray)
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
HStack(alignment: .bottom) {
Spacer()
ZStack {
LikeButton(liked: bar.liked) {
if bar.liked {
notify(.delete, bar.our_like)
} else {
send_like()
}
}.overlay {
Text("\(bar.likes > 0 ? "\(bar.likes)" : "")")
.offset(x: 22)
.font(.footnote.weight(.medium))
.foregroundColor(bar.liked ? Color.accentColor : Color.gray)
}
Text("\(bar.likes > 0 ? "\(bar.likes)" : "")")
.offset(x: 22)
.font(.footnote.weight(.medium))
.foregroundColor(bar.liked ? Color.accentColor : Color.gray)
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
Spacer()
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
show_share_sheet = true
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
/*
HStack(alignment: .bottom) {
Text("\(bar.tips > 0 ? "\(bar.tips)" : "")")
.font(.footnote)
.foregroundColor(bar.tipped ? Color.orange : Color.gray)
EventActionButton(img: bar.tipped ? "bitcoinsign.circle.fill" : "bitcoinsign.circle", col: bar.tipped ? Color.orange : nil) {
if bar.tipped {
//notify(.delete, bar.our_tip)
} else {
//notify(.boost, event)
}
}
}
*/
}
.sheet(isPresented: $show_share_sheet) {
if let note_id = bech32_note_id(event.id) {
@@ -177,8 +156,9 @@ struct EventActionBar_Previews: PreviewProvider {
let ev = NostrEvent(content: "hi", pubkey: pk)
let bar = ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
let likedbar = ActionBarModel(likes: 10, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, tips: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_tip: nil)
let likedbar = ActionBarModel(likes: 10, boosts: 10, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
let likedbar_ours = ActionBarModel(likes: 100, boosts: 100, tips: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_tip: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, tips: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_tip: nil)
VStack(spacing: 50) {
EventActionBar(damus_state: ds, event: ev, bar: bar)
@@ -186,6 +166,8 @@ struct EventActionBar_Previews: PreviewProvider {
EventActionBar(damus_state: ds, event: ev, bar: likedbar)
EventActionBar(damus_state: ds, event: ev, bar: likedbar_ours)
EventActionBar(damus_state: ds, event: ev, bar: maxed_bar)
}
.padding(20)
}
+4 -1
View File
@@ -15,7 +15,10 @@ struct EventDetailBar: View {
var body: some View {
HStack {
if bar.boosts > 0 {
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'.")
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())
}
if bar.likes > 0 {
+22 -8
View File
@@ -9,29 +9,43 @@ import SwiftUI
import Kingfisher
struct InnerBannerImageView: View {
let url: URL?
let pubkey: String
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
@ObservedObject var imageModel: KFImageModel
init(url: URL?) {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 20_971_520, // 20 MiB
downsampleSize: CGSize(width: 750, height: 250)
)
}
var body: some View {
ZStack {
Color(uiColor: .systemBackground)
if (url != nil) {
KFAnimatedImage(url)
if (imageModel.url != nil) {
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.appendProcessor(LargeImageProcessor.shared)
.serialize(by: imageModel.serializer)
.setProcessor(imageModel.processor)
.configure { view in
view.framePreloadCount = 1
}
.placeholder { _ in
Image("profile-banner").resizable()
Color(uiColor: .secondarySystemBackground)
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.onFailureImage(defaultImage)
.id(imageModel.refreshID)
} else {
Image("profile-banner").resizable()
Image(uiImage: defaultImage).resizable()
}
}
}
@@ -50,7 +64,7 @@ struct BannerImageView: View {
}
var body: some View {
InnerBannerImageView(url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles), pubkey: pubkey)
InnerBannerImageView(url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles))
.onReceive(handle_notify(.profile_updated)) { notif in
let updated = notif.object as! ProfileUpdate
+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, pubkey: ev.pubkey, privkey: damus.keypair.privkey)
.event_context_menu(ev, keypair: damus.keypair)
.onTapGesture {
if thread.initial_event.id == ev.id {
//dismiss()
+4 -4
View File
@@ -91,7 +91,7 @@ struct ConfigView: View {
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
HStack {
if show_privkey == false {
SecureField(NSLocalizedString("PrivateKey", comment: "Title of the secure field that holds the user's private key."), text: $privkey)
SecureField(NSLocalizedString("Private Key", comment: "Title of the secure field that holds the user's private key."), text: $privkey)
.disabled(true)
} else {
Text(sec)
@@ -139,10 +139,10 @@ struct ConfigView: View {
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
.navigationBarTitleDisplayMode(.large)
.alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user.")) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) {
confirm_logout = false
}
Button(NSLocalizedString("Logout", comment: "Button for logging out the user.")) {
Button(NSLocalizedString("Logout", comment: "Button for logging out the user."), role: .destructive) {
notify(.logout, ())
}
} message: {
@@ -154,7 +154,7 @@ struct ConfigView: View {
return
}
if relay.starts(with: "wss://") == false {
if relay.starts(with: "wss://") == false && relay.starts(with: "ws://") == false {
relay = "wss://" + relay
}
+2
View File
@@ -11,6 +11,7 @@ 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)
@@ -75,6 +76,7 @@ struct CreateAccountView: View {
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
EmptyView()
}
DamusWhiteButton(NSLocalizedString("Create", comment: "Button to create account.")) {
self.is_done = true
}
+7 -5
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, pubkey: ev.pubkey, privkey: damus_state.keypair.privkey)
.event_context_menu(ev, keypair: damus_state.keypair)
}
EndBlock(height: 80)
}
@@ -149,7 +149,7 @@ struct DMChatView: View {
.opacity(((dms.events.count == 0) ? 1.0 : 0.0))
.foregroundColor(.gray)
}
.navigationTitle(NSLocalizedString("DM", comment: "Navigation title for DM view, which is the English abbreviation for Direct Message."))
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message."))
.toolbar { Header }
}
}
@@ -158,7 +158,7 @@ struct DMChatView_Previews: PreviewProvider {
static var previews: some View {
let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: [])
let model = DirectMessageModel(events: [ev])
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey")
DMChatView(damus_state: test_damus_state(), pubkey: "pubkey")
.environmentObject(model)
@@ -166,7 +166,7 @@ struct DMChatView_Previews: PreviewProvider {
}
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair) -> NostrEvent?
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
{
guard let privkey = keypair.privkey else {
return nil
@@ -181,7 +181,9 @@ func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keyp
return nil
}
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv)
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags)
let created = created_at ?? Int64(Date().timeIntervalSince1970)
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags, createdAt: created)
ev.calculate_id()
ev.sign(privkey: privkey)
return ev
+42 -10
View File
@@ -7,15 +7,26 @@
import SwiftUI
enum DMType: Hashable {
case rando
case friend
}
struct DirectMessagesView: View {
let damus_state: DamusState
@State var dm_type: DMType = .friend
@State var open_dm: Bool = false
@State var pubkey: String = ""
@State var active_model: DirectMessageModel = DirectMessageModel()
@EnvironmentObject var model: DirectMessagesModel
@State var active_model: DirectMessageModel
var MainContent: some View {
init(damus_state: DamusState) {
self.damus_state = damus_state
self._active_model = State(initialValue: DirectMessageModel(our_pubkey: damus_state.pubkey))
}
func MainContent(requests: Bool) -> some View {
ScrollView {
let chat = DMChatView(damus_state: damus_state, pubkey: pubkey)
.environmentObject(active_model)
@@ -26,20 +37,19 @@ struct DirectMessagesView: View {
if model.dms.isEmpty, !model.loading {
EmptyTimelineView()
} else {
ForEach(model.dms, id: \.0) { tup in
let dms = requests ? model.message_requests : model.friend_dms
ForEach(dms, id: \.0) { tup in
MaybeEvent(tup)
}
}
}
.padding(.horizontal)
.padding(.top)
}
}
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View {
Group {
if let ev = tup.1.events.last {
EventView(damus: damus_state, event: ev, pubkey: tup.0, show_friend_icon: true)
EventView(damus: damus_state, event: ev, pubkey: tup.0)
.onTapGesture {
pubkey = tup.0
active_model = tup.1
@@ -52,8 +62,29 @@ struct DirectMessagesView: View {
}
var body: some View {
MainContent
.navigationTitle(NSLocalizedString("Encrypted DMs", comment: "Navigation title for view of encrypted DMs, where DM is an 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. DM is the English abbreviation for Direct Message."), selection: $dm_type) {
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.")
.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.")
.tag(DMType.rando)
}
.pickerStyle(.segmented)
TabView(selection: $dm_type) {
MainContent(requests: false)
.tag(DMType.friend)
MainContent(requests: true)
.tag(DMType.rando)
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.padding(.horizontal)
.padding(.top)
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message."))
}
}
@@ -63,8 +94,9 @@ struct DirectMessagesView_Previews: PreviewProvider {
pubkey: "pubkey",
kind: 4,
tags: [])
let model = DirectMessageModel(events: [ev])
DirectMessagesView(damus_state: test_damus_state())
let ds = test_damus_state()
let model = DirectMessageModel(events: [ev], our_pubkey: ds.pubkey)
DirectMessagesView(damus_state: ds)
.environmentObject(model)
}
}
+96
View File
@@ -0,0 +1,96 @@
//
// EULAView.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import SwiftUI
struct EULAView: View {
@Environment(\.dismiss) var dismiss
@State var creating_account = 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()
NavigationLink(destination: CreateAccountView(), isActive: $creating_account) {
EmptyView()
}
DamusWhiteButton(NSLocalizedString("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.")) {
creating_account = 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()
}
}
+2 -2
View File
@@ -71,8 +71,8 @@ struct EventDetailView: View {
}
toggle_thread_view()
}
case .event(let ev, let highlight):
EventView(event: ev, highlight: highlight, has_action_bar: true, damus: damus, show_friend_icon: true)
case .event(let ev, _):
EventView(damus: damus, event: ev, has_action_bar: true)
.onTapGesture {
if thread.initial_event.id == ev.id {
toggle_thread_view()
+45 -236
View File
@@ -8,38 +8,9 @@
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
}
@@ -49,117 +20,40 @@ 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)
}.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
@EnvironmentObject var action_bar: ActionBarModel
init(event: NostrEvent, highlight: Highlight, has_action_bar: Bool, damus: DamusState, show_friend_icon: Bool, size: EventViewKind = .normal) {
init(damus: DamusState, event: NostrEvent, has_action_bar: Bool) {
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
}
init(damus: DamusState, event: NostrEvent, show_friend_icon: Bool, size: EventViewKind = .normal) {
init(damus: DamusState, event: NostrEvent) {
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
}
init(damus: DamusState, event: NostrEvent, pubkey: String, show_friend_icon: Bool, size: EventViewKind = .normal, embedded: Bool = false) {
init(damus: DamusState, event: NostrEvent, pubkey: String) {
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
}
var body: some View {
@@ -175,7 +69,7 @@ struct EventView: View {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
}
.buttonStyle(PlainButtonStyle())
TextEvent(inner_ev, pubkey: inner_ev.pubkey)
TextEvent(inner_ev, pubkey: inner_ev.pubkey, booster_pubkey: event.pubkey)
.padding([.top], 1)
}
} else {
@@ -185,80 +79,44 @@ struct EventView: View {
}
}
func TextEvent(_ event: NostrEvent, pubkey: String) -> some View {
let content = event.get_content(damus.keypair.privkey)
func TextEvent(_ event: NostrEvent, pubkey: String, booster_pubkey: String? = nil) -> some View {
return HStack(alignment: .top) {
let profile = damus.profiles.lookup(id: pubkey)
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)
}
Spacer()
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)
}
Spacer()
}
VStack(alignment: .leading) {
HStack(alignment: .center) {
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)
}
}
}
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
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)
Text("\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
}
let should_show_img = should_show_images(contacts: damus.contacts, ev: event, our_pubkey: damus.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)
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 {
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)
}
Rectangle().frame(height: 2).opacity(0)
let bar = make_actionbar_model(ev: event, damus: damus)
if size == .selected && !bar.is_empty {
EventDetailBar(state: damus, target: event.id, bar: bar)
Divider()
.padding([.bottom], 4)
}
EventActionBar(damus_state: damus, event: event, bar: bar)
.padding([.top], 4)
}
Divider()
@@ -271,18 +129,21 @@ struct EventView: View {
.id(event.id)
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
.padding([.bottom], 2)
.event_context_menu(event, pubkey: pubkey, privkey: damus.keypair.privkey)
.event_context_menu(event, keypair: damus.keypair)
}
}
// blame the porn bots for this code
func should_show_images(contacts: Contacts, ev: NostrEvent, our_pubkey: String) -> Bool {
func should_show_images(contacts: Contacts, ev: NostrEvent, our_pubkey: String, booster_pubkey: String? = nil) -> Bool {
if ev.pubkey == our_pubkey {
return true
}
if contacts.is_in_friendosphere(ev.pubkey) {
return true
}
if let boost_key = booster_pubkey, contacts.is_in_friendosphere(boost_key) {
return true
}
return false
}
@@ -310,37 +171,9 @@ extension View {
}
}
func event_context_menu(_ event: NostrEvent, pubkey: String, privkey: String?) -> some View {
func event_context_menu(_ event: NostrEvent, keypair: Keypair) -> some View {
return self.contextMenu {
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: "tag")
}
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: "tag")
}
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: "note")
}
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")
}
EventMenuContext(event: event, keypair: keypair)
}
}
@@ -360,33 +193,6 @@ 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]
@@ -409,22 +215,25 @@ 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(),
show_friend_icon: true,
size: .selected
event: test_event,
has_action_bar: true
)
}
.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
@@ -0,0 +1,81 @@
//
// 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")
}
}
@@ -0,0 +1,35 @@
//
// 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)
}
}
struct EmbeddedEventView_Previews: PreviewProvider {
static var previews: some View {
EmbeddedEventView(damus_state: test_damus_state(), event: test_event)
.padding()
}
}
+35
View File
@@ -0,0 +1,35 @@
//
// 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)
}
}
+101
View File
@@ -0,0 +1,101 @@
//
// EventMenu.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import SwiftUI
struct EventMenuContext: View {
let event: NostrEvent
let keypair: Keypair
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 = keypair.pubkey_bech32
} 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 != event.pubkey && keypair.privkey != nil {
Button(role: .destructive) {
let target: ReportTarget = .note(ReportNoteTarget(pubkey: event.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, event.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
@@ -0,0 +1,51 @@
//
// 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
@@ -0,0 +1,112 @@
//
// 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")
Spacer()
Button(shown ? "Hide" : "Show") {
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
@@ -0,0 +1,55 @@
//
// 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)
}
@@ -0,0 +1,62 @@
//
// 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)
}
}
}
struct SelectedEventView_Previews: PreviewProvider {
static var previews: some View {
SelectedEventView(damus: test_damus_state(), event: test_event)
.padding()
}
}
+1 -2
View File
@@ -19,8 +19,7 @@ struct FollowButtonView: View {
follow_state = perform_follow_btn_action(follow_state, target: target)
} label: {
Text(follow_btn_txt(follow_state))
.frame(height: 30)
.padding(.horizontal, 25)
.frame(width: 105, height: 30)
//.padding(.vertical, 10)
.font(.caption.weight(.bold))
.foregroundColor(follow_state == .unfollows ? filledTextColor() : borderColor())
+1 -20
View File
@@ -15,26 +15,7 @@ struct FollowUserView: View {
var body: some View {
HStack {
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())
UserView(damus_state: damus_state, pubkey: target.pubkey)
FollowButtonView(target: target, follow_state: damus_state.contacts.follow_state(target.pubkey))
}
+20
View File
@@ -0,0 +1,20 @@
//
// ImageView.swift
// damus
//
// Created by user232838 on 1/5/23.
//
import SwiftUI
struct ImageView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct ImageView_Previews: PreviewProvider {
static var previews: some View {
ImageView()
}
}
@@ -0,0 +1,33 @@
//
// MagnificationGestureView.swift
// damus
//
// Created by user232838 on 1/5/23.
//
import SwiftUI
struct MagnificationGestureView: View {
@GestureState var magnifyBy = 1.0
var magnification: some Gesture {
MagnificationGesture()
.updating($magnifyBy) { currentState, gestureState, transaction in
gestureState = currentState
}
}
var body: some View {
Circle()
.frame(width: 100, height: 100)
.scaleEffect(magnifyBy)
.gesture(magnification)
}
}
struct MagnificationGestureView_Previews: PreviewProvider {
static var previews: some View {
MagnificationGestureView()
}
}
+67
View File
@@ -0,0 +1,67 @@
//
// 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"])
}
}
+32 -17
View File
@@ -9,13 +9,13 @@ import SwiftUI
import LinkPresentation
struct NoteArtifacts {
let content: String
let content: AttributedString
let images: [URL]
let invoices: [Invoice]
let links: [URL]
static func just_content(_ content: String) -> NoteArtifacts {
NoteArtifacts(content: content, images: [], invoices: [], links: [])
NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
}
}
@@ -24,19 +24,18 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
var invoices: [Invoice] = []
var img_urls: [URL] = []
var link_urls: [URL] = []
let txt = blocks.reduce("") { str, block in
let txt: AttributedString = blocks.reduce("") { str, block in
switch block {
case .mention(let m):
return str + mention_str(m, profiles: profiles)
case .text(let txt):
return str + txt
return str + AttributedString(stringLiteral: 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
@@ -44,7 +43,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
return str
} else {
link_urls.append(url)
return str + url.absoluteString
return str + url_str(url)
}
}
}
@@ -53,7 +52,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
}
func is_image_url(_ url: URL) -> Bool {
let str = url.lastPathComponent
let str = url.lastPathComponent.lowercased()
return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif")
}
@@ -72,7 +71,7 @@ struct NoteContentView: View {
func MainContent() -> some View {
return VStack(alignment: .leading) {
Text(Markdown.parse(content: artifacts.content))
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
@@ -91,8 +90,8 @@ struct NoteContentView: View {
InvoicesView(invoices: artifacts.invoices)
}
if show_images, self.preview != nil {
self.preview
if let preview = self.preview, show_images {
preview
} else {
ForEach(artifacts.links, id:\.self) { link in
if let url = link {
@@ -163,20 +162,36 @@ struct NoteContentView: View {
}
}
func hashtag_str(_ htag: String) -> String {
return "[#\(htag)](nostr:t:\(htag))"
}
func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "nostr:t:\(htag)")
attributedString.foregroundColor = .purple
return attributedString
}
func mention_str(_ m: Mention, profiles: Profiles) -> String {
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 {
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)
return "[@\(disp)](nostr:\(encode_pubkey_uri(m.ref)))"
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "nostr:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
return "[@\(abbrev_pubkey(bevid))](nostr:\(encode_event_id_uri(m.ref)))"
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "nostr:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
}
}
@@ -185,7 +200,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: content, images: [], invoices: [], links: [])
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, previews: PreviewCache(), show_images: true, artifacts: artifacts, size: .normal)
}
}
+79
View File
@@ -0,0 +1,79 @@
//
// 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()
}
}
+32 -2
View File
@@ -16,10 +16,11 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
struct PostView: View {
@State var post: String = ""
let replying_to: NostrEvent?
@FocusState var focus: Bool
let replying_to: NostrEvent?
let references: [ReferencedId]
let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode
@@ -74,6 +75,7 @@ struct PostView: View {
TextEditor(text: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
if post.isEmpty {
Text(POST_PLACEHOLDER)
.padding(.top, 8)
@@ -82,6 +84,14 @@ 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) {
@@ -92,3 +102,23 @@ 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
@@ -0,0 +1,87 @@
//
// 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)
}
}
+19 -65
View File
@@ -33,13 +33,23 @@ func pfp_line_width(_ h: Highlight) -> CGFloat {
}
struct InnerProfilePicView: View {
let url: URL?
let fallbackUrl: URL?
let pubkey: String
let size: CGFloat
let highlight: Highlight
@State private var refreshID = UUID().uuidString
@ObservedObject var imageModel: KFImageModel
init(url: URL?, fallbackUrl: URL?, pubkey: String, size: CGFloat, highlight: Highlight) {
self.pubkey = pubkey
self.size = size
self.highlight = highlight
self.imageModel = KFImageModel(
url: url,
fallbackUrl: fallbackUrl,
maxByteSize: 5_242_880, // 5Mib
downsampleSize: CGSize(width: 200, height: 200)
)
}
var PlaceholderColor: Color {
return id_to_color(pubkey)
@@ -57,10 +67,12 @@ struct InnerProfilePicView: View {
ZStack {
Color(uiColor: .systemBackground)
KFAnimatedImage(url)
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.appendProcessor(LargeImageProcessor.shared)
.serialize(by: imageModel.serializer)
.setProcessor(imageModel.processor)
.cacheOriginalImage()
.configure { view in
view.framePreloadCount = 1
}
@@ -71,42 +83,13 @@ struct InnerProfilePicView: View {
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.onFailure { _ in
setFallbackImage()
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
}
.frame(width: size, height: size)
.clipShape(Circle())
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
.id(refreshID)
}
func refreshView() -> Void {
refreshID = UUID().uuidString
}
func setFallbackImage() -> Void {
guard let url = url, let fallbackUrl = fallbackUrl else { return }
KingfisherManager.shared.downloader.downloadImage(with: fallbackUrl) { result in
func fallbackImage() -> UIImage {
switch result {
case .success(let imageLoadingResult):
return imageLoadingResult.image
case .failure(let error):
print(error)
return UIImage()
}
}
// Kingfisher ID format for caching when using a custom processor
let processorIdentifier = "|>" + LargeImageProcessor.shared.identifier
KingfisherManager.shared.cache.store(fallbackImage(), forKey: url.absoluteString, processorIdentifier: processorIdentifier) { _ in
refreshView()
}
}
}
}
@@ -142,35 +125,6 @@ struct ProfilePicView: View {
}
}
struct LargeImageProcessor: ImageProcessor {
static let shared = LargeImageProcessor()
let identifier = "com.damus.largeimageprocessor"
let maxSize: Int = 1000000
let downsampleSize = CGSize(width: 200, height: 200)
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(let image):
guard let data = image.kf.data(format: .unknown) else {
return nil
}
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return image
case .data(let data):
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return KFCrossPlatformImage(data: data)
}
}
}
func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> URL {
let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
if let url = URL(string: pic) {
+81 -22
View File
@@ -114,11 +114,13 @@ 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
@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/
@@ -146,9 +148,7 @@ struct ProfileView: View {
}
}) {
Image(systemName: "bolt.circle")
.symbolRenderingMode(.palette)
.foregroundStyle(colorScheme == .dark ? .white : .black, colorScheme == .dark ? .white : .black)
.font(.system(size: 32).weight(.thin))
.profile_button_style(scheme: colorScheme)
.contextMenu {
Button {
UIPasteboard.general.string = profile.lnurl ?? ""
@@ -167,15 +167,21 @@ 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)
}
}
var ShareButton: some View {
Button(action: {
show_share_sheet = true
}) {
Image(systemName: "square.and.arrow.up.circle.fill")
.symbolRenderingMode(.palette)
.font(.system(size: 32))
.padding()
.foregroundStyle(.white, .black, .black.opacity(0.8))
Image(systemName: "square.and.arrow.up.circle")
.profile_button_style(scheme: colorScheme)
}
}
@@ -185,24 +191,48 @@ struct ProfileView: View {
.environmentObject(dm_model)
return NavigationLink(destination: dmview) {
Image(systemName: "bubble.left.circle")
.symbolRenderingMode(.palette)
.font(.system(size: 32).weight(.thin))
.foregroundStyle(colorScheme == .dark ? .white : .black, colorScheme == .dark ? .white : .black)
.profile_button_style(scheme: colorScheme)
}
}
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 { geo in
GeometryReader { geometry in
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles)
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width, height: BANNER_HEIGHT)
.frame(width: geometry.size.width, height: self.getHeightForHeaderImage(geometry))
.clipped()
ShareButton
.offset(x: geo.size.width - 80.0, y: 50.0 )
.offset(x: 0, y: self.getOffsetForHeaderImage(geometry))
}.frame(height: BANNER_HEIGHT)
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 8.0) {
let data = damus_state.profiles.lookup(id: profile.pubkey)
let pfp_size: CGFloat = 90.0
@@ -211,14 +241,14 @@ struct ProfileView: View {
.onTapGesture {
is_zoomed.toggle()
}
.sheet(isPresented: $is_zoomed) {
ProfilePicView(pubkey: profile.pubkey, size: zoom_size, highlight: .none, profiles: damus_state.profiles)
}
.fullScreenCover(isPresented: $is_zoomed) {
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
Group {
ActionSheetButton
if let profile = data {
if let lnurl = profile.lnurl, lnurl != "" {
@@ -251,6 +281,10 @@ struct ProfileView: View {
Text(ProfileView.markdown.process(data?.about ?? ""))
.font(.subheadline)
if let url = data?.website_url {
WebsiteLink(url: url)
}
Divider()
HStack {
@@ -340,6 +374,23 @@ struct ProfileView: View {
}
}
}
.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)
}
}
}
.ignoresSafeArea()
}
}
@@ -356,7 +407,7 @@ struct ProfileView_Previews: PreviewProvider {
func test_damus_state() -> DamusState {
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel(), previews: PreviewCache())
let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: pubkey), previews: PreviewCache())
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io")
let tsprof = TimestampedProfile(profile: prof, timestamp: 0)
@@ -440,3 +491,11 @@ 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)
}
}
+97
View File
@@ -0,0 +1,97 @@
//
// ProfileZoomView.swift
// damus
//
// Created by scoder1747 on 12/27/22.
//
import SwiftUI
struct ProfileZoomView: View {
@Environment(\.presentationMode) var presentationMode
let pubkey: String
let profiles: Profiles
@GestureState private var scaleState: CGFloat = 1
@GestureState private var offsetState = CGSize.zero
@State private var offset = CGSize.zero
@State private var scale: CGFloat = 1
func resetStatus(){
self.offset = CGSize.zero
self.scale = 1
}
var zoomGesture: some Gesture {
MagnificationGesture()
.updating($scaleState) { currentState, gestureState, _ in
gestureState = currentState
}
.onEnded { value in
scale *= value
}
}
var dragGesture: some Gesture {
DragGesture()
.updating($offsetState) { currentState, gestureState, _ in
gestureState = currentState.translation
}.onEnded { value in
offset.height += value.translation.height
offset.width += value.translation.width
}
}
var doubleTapGesture : some Gesture {
TapGesture(count: 2).onEnded { value in
resetStatus()
}
}
var body: some View {
ZStack(alignment: .topLeading) {
Color("DamusDarkGrey") // Or Color("DamusBlack")
.edgesIgnoringSafeArea(.all)
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.subheadline)
.padding(.leading, 20)
}
.zIndex(1)
VStack(alignment: .center) {
Spacer()
ProfilePicView(pubkey: pubkey, size: 200.0, highlight: .none, profiles: profiles)
.padding(100)
.scaledToFit()
.scaleEffect(self.scale * scaleState)
.offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
.gesture(SimultaneousGesture(zoomGesture, dragGesture))
.gesture(doubleTapGesture)
.modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
Spacer()
}
}
}
}
struct ProfileZoomView_Previews: PreviewProvider {
static let pubkey = "ca48854ac6555fed8e439ebb4fa2d928410e0eef13fa41164ec45aaaa132d846"
static var previews: some View {
ProfileZoomView(
pubkey: pubkey,
profiles: make_preview_profiles(pubkey))
}
}
+90
View File
@@ -0,0 +1,90 @@
//
// 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())
}
}
+20 -6
View File
@@ -18,11 +18,16 @@ 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 = all_referenced_pubkeys(replying_to)
let names = references.pRefs
.map { pubkey in
let pk = pubkey.ref_id
let prof = damus.profiles.lookup(id: pk)
@@ -33,13 +38,22 @@ struct ReplyView: View {
.foregroundColor(.gray)
.font(.footnote)
}
ScrollView {
EventView(event: replying_to, highlight: .none, has_action_bar: false, damus: damus, show_friend_icon: true)
.onTapGesture {
participantsShown.toggle()
}
PostView(replying_to: replying_to, references: gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to))
.sheet(isPresented: $participantsShown) {
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
}
ScrollView {
EventView(damus: damus, event: replying_to, has_action_bar: false)
}
PostView(replying_to: replying_to, references: references, damus_state: damus)
}
.onAppear {
references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)
originalReferences = references
}
.padding()
}
@@ -47,6 +61,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())
ReplyView(replying_to: NostrEvent(content: "hi", pubkey: "pubkey"), damus: test_damus_state(), references: [])
}
}
+115
View File
@@ -0,0 +1,115 @@
//
// 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
@@ -0,0 +1,24 @@
//
// 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
@@ -0,0 +1,38 @@
//
// 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,6 +87,9 @@ 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()
+1 -1
View File
@@ -42,7 +42,7 @@ struct SetupView: View {
DamusGradient()
VStack(alignment: .center) {
NavigationLink(destination: CreateAccountView(), tag: .create_account, selection: $state ) {
NavigationLink(destination: EULAView(), tag: .create_account, selection: $state ) {
EmptyView()
}
NavigationLink(destination: LoginView(), tag: .login, selection: $state ) {
+60 -52
View File
@@ -14,9 +14,11 @@ struct SideMenuView: View {
@State var confirm_logout: Bool = false
@StateObject var user_settings = UserSettingsStore()
@State private var showQRCode = false
@Environment(\.colorScheme) var colorScheme
var sideBarWidth = UIScreen.main.bounds.size.width * 0.65
var sideBarWidth = min(UIScreen.main.bounds.size.width * 0.65, 400.0)
func fillColor() -> Color {
colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
@@ -50,46 +52,31 @@ struct SideMenuView: View {
VStack(alignment: .leading, spacing: 20) {
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
if let picture = damus_state.profiles.lookup(id: damus_state.pubkey)?.picture {
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, picture: picture)
} else {
Image(systemName: "person.fill")
}
VStack(alignment: .leading) {
if let display_name = profile?.display_name {
Text(display_name)
.foregroundColor(textColor())
.font(.title)
}
if let name = profile?.name {
Text("@" + name)
.foregroundColor(Color("DamusMediumGrey"))
.font(.body)
}
}
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)
let followers = FollowersModel(damus_state: damus_state, target: damus_state.pubkey)
let profile_model = ProfileModel(pubkey: damus_state.pubkey, damus: damus_state)
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
if let picture = damus_state.profiles.lookup(id: damus_state.pubkey)?.picture {
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, picture: picture)
} else {
Image(systemName: "person.fill")
}
VStack(alignment: .leading) {
if let display_name = profile?.display_name {
Text(display_name)
.foregroundColor(textColor())
.font(.title)
}
if let name = profile?.name {
Text("@" + name)
.foregroundColor(Color("DamusMediumGrey"))
.font(.body)
}
}
}
Divider()
.padding(.trailing,40)
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
Label(NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person")
@@ -122,6 +109,12 @@ 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: ConfigView(state: damus_state).environmentObject(user_settings)) {
Label(NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear")
.font(.title2)
@@ -133,18 +126,33 @@ struct SideMenuView: View {
Spacer()
Button(action: {
//ConfigView(state: damus_state)
if damus_state.keypair.privkey == nil {
notify(.logout, ())
} else {
confirm_logout = true
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)
}
}, 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)
@@ -157,10 +165,10 @@ struct SideMenuView: View {
isSidebarVisible.toggle()
}
.alert("Logout", isPresented: $confirm_logout) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user.")) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) {
confirm_logout = false
}
Button(NSLocalizedString("Logout", comment: "Button for logging out the user.")) {
Button(NSLocalizedString("Logout", comment: "Button for logging out the user."), role: .destructive) {
notify(.logout, ())
}
} message: {
+18 -31
View File
@@ -255,22 +255,13 @@ struct ThreadV2View: View {
// MARK: - Parents events view
VStack {
ForEach(thread.parentEvents, id: \.id) { event in
EventView(
event: event,
highlight: .none,
has_action_bar: true,
damus: damus,
show_friend_icon: true, // TODO: change it
size: .small
MutedEventView(damus_state: damus,
event: event,
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: false
)
.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
@@ -285,29 +276,25 @@ struct ThreadV2View: View {
})
// MARK: - Actual event view
EventView(
MutedEventView(
damus_state: damus,
event: thread.current,
highlight: .none,
has_action_bar: true,
damus: damus,
show_friend_icon: true, // TODO: change it
size: .selected
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: true
).id("main")
// MARK: - Responses of the actual event view
ForEach(thread.childEvents, id: \.id) { event in
EventView(
MutedEventView(
damus_state: damus,
event: event,
highlight: .none,
has_action_bar: true,
damus: damus,
show_friend_icon: true, // TODO: change it
size: .small
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: false
)
.onTapGesture {
nav_target = event.id
navigating = true
}
}
}.padding()
}.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))
+2 -5
View File
@@ -39,11 +39,7 @@ struct InnerTimelineView: View {
EmptyTimelineView()
} else {
ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in
//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)
EventView(damus: damus, event: ev, has_action_bar: true)
.onTapGesture {
nav_target = ev
navigating = true
@@ -52,6 +48,7 @@ struct InnerTimelineView: View {
}
}
.padding(.horizontal)
.padding(.top,10)
}
}
+9
View File
@@ -0,0 +1,9 @@
/* 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.";
+503
View File
@@ -0,0 +1,503 @@
/* 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 nip05 identifier. It should look like an email." = "'%@' ist eine ungültige nip05 Kennzeichnung. Diese sollte wie eine Emailadresse 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)) Follower";
/* 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 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'. */
"%@ %@" = "%@ %@";
/* 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 reposts.
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";
/* Label to indicate the public ID of the account. */
"Account ID" = "Konto ID";
/* 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";
/* 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";
/* 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";
/* 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. */
"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";
/* 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 ID" = "Benutzer ID kopieren";
/* Button to create account. */
"Create" = "Erstellen";
/* Button to create an account. */
"Create Account" = "Konto erstellen";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Erfinder von Bitcoin. Absolute Legende(n).";
/* 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 to delete a relay server that the user connects to. */
"Delete" = "Löschen";
/* Button to dismiss a text field alert. */
"Dismiss" = "Schließen";
/* Label to prompt display name entry. */
"Display Name" = "Profilname";
/* 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. */
"DM Type" = "PN Typ";
/* 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: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filter Einstellung";
/* Button to follow a user. */
"Follow" = "Folgen";
/* Label describing followers of a user. */
"Followers" = "Follower";
/* 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" = "Folgt";
/* 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" = "Weltweit";
/* 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 %@";
/* 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";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Ungültiger Schlüssel";
/* 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!";
/* 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 logout the user. */
"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";
/* No search results. */
"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...";
/* 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";
/* 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";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relay";
/* Sidebar menu label for Relay servers view */
"Relays" = "Relays";
/* 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 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";
/* 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";
/* Section title for resetting the user */
"Reset" = "Zurücksetzen";
/* 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...";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Einstellungen";
/* Button to share an image. */
"Share" = "Teilen";
/* Toggle to show or hide user's secret account login key. */
"Show" = "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";
/* 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";
/* 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";
/* 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, %@!";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://ein.relay.at";
/* 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";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";
+154
View File
@@ -0,0 +1,154 @@
<?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>Follower</string>
<key>other</key>
<string>Follower</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
@@ -0,0 +1,9 @@
/* 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.";
+503
View File
@@ -0,0 +1,503 @@
/* 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 nip05 identifier. It should look like an email." = "'%@' ist eine ungültige nip05 Kennzeichnung. Diese sollte wie eine Emailadresse 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)) Follower";
/* 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 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'. */
"%@ %@" = "%@ %@";
/* 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." = "Für das Erstellen eines Accounts ist keine Telefonnumer, E-Mail-Adresse und kein Name notwendig. Lege direkt los!";
/* 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üsselte private Nachrichten. Halte „Big Tech“ aus deinen Direktnachrichten 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 reposts.
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";
/* Label to indicate the public ID of the account. */
"Account ID" = "Konto-ID";
/* 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";
/* 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" = "Banner Bild";
/* 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 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";
/* 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";
/* 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. */
"Cancel" = "Abbrechen";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "Unterhaltung";
/* 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" = "Rechnung 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";
/* 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 ID" = "Benutzer-ID kopieren";
/* Button to create account. */
"Create" = "Erstellen";
/* Button to create an account. */
"Create Account" = "Konto erstellen";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Erfinder von Bitcoin. Absolute Legende(n).";
/* 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 to delete a relay server that the user connects to. */
"Delete" = "Löschen";
/* Button to dismiss a text field alert. */
"Dismiss" = "Schließen";
/* Label to prompt display name entry. */
"Display Name" = "Profilname";
/* 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. */
"DM Type" = "PN Art";
/* 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: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filter Einstellung";
/* Button to follow a user. */
"Follow" = "Folgen";
/* Label describing followers of a user. */
"Followers" = "Follower";
/* 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" = "Folgt";
/* 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" = "Weltweit";
/* 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 %@";
/* 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.de/bild.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Ungültiger Schlüssel";
/* 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!";
/* 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 logout the user. */
"Logout" = "Abmelden";
/* 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 dein nsec Kontoschlüssel gesichert ist bevor du dich abmeldest 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";
/* No search results. */
"none" = "keine";
/* 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...";
/* 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";
/* 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";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relay";
/* Sidebar menu label for Relay servers view */
"Relays" = "Relays";
/* 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 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";
/* 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";
/* Section title for resetting the user */
"Reset" = "Zurücksetzen";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Wiederholung";
/* 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 speichern";
/* 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 eine 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 die Unterhaltung zu beginnen...";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Einstellungen";
/* Button to share an image. */
"Share" = "Teilen";
/* Toggle to show or hide user's secret account login key. */
"Show" = "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";
/* 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 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 veralteter nostr-Schlüssel. Wir sind und unsicher 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";
/* 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";
/* 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, %@!";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://irgendein.relay.de";
/* 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";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";
+154
View File
@@ -0,0 +1,154 @@
<?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>%d andere Notiz</string>
<key>other</key>
<string>%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>Follower</string>
<key>other</key>
<string>Follower</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; %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>Mal geteilt</string>
<key>other</key>
<string>Mal geteilt</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>
+19 -15
View File
@@ -12,10 +12,8 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>0 other notes</string>
<key>one</key>
<string>1 other note</string>
<string>%d other note</string>
<key>other</key>
<string>%d other notes</string>
</dict>
@@ -30,8 +28,6 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Followers</string>
<key>one</key>
<string>Follower</string>
<key>other</key>
@@ -48,8 +44,6 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Reactions</string>
<key>one</key>
<string>Reaction</string>
<key>other</key>
@@ -66,8 +60,6 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Relays</string>
<key>one</key>
<string>Relay</string>
<key>other</key>
@@ -87,7 +79,7 @@
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<string> &amp; %d other</string>
<key>other</key>
<string> &amp; %d others</string>
</dict>
@@ -105,7 +97,7 @@
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<string> &amp; %d other</string>
<key>other</key>
<string> &amp; %d others</string>
</dict>
@@ -120,14 +112,28 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Reposts</string>
<key>one</key>
<string>Repost</string>
<key>other</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>one</key>
<string>%2$@ sat</string>
<key>other</key>
<string>%2$@ sats</string>
</dict>
</dict>
<key>tips_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -138,8 +144,6 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Tips</string>
<key>one</key>
<string>Tip</string>
<key>other</key>
@@ -1,6 +1,9 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "\"Granting Damus access to your photo library allows you to save photos.";
"NSPhotoLibraryAddUsageDescription" = "Si le concedes acceso a Damus a tu fototeca, podrás guardar fotos.";
+493
View File
@@ -0,0 +1,493 @@
/* 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" = "'%@' en '%@' se usarán con fines de verificación";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'%@' es un identificador nip05 no válido. Debería de tener la apariencia de un correo electrónico.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "Seguidores de (Profile.displayName(profile: profile, pubkey: whos))";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) sigue a";
/* 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 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'. */
"%@ %@" = "%@ %@";
/* 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." = "%@. No se requiere un número de teléfono, correo electrónico ni nombre para crear una cuenta. Comienza de inmediato sin fricciones.";
/* 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" = "%@. Mensajes privados con cifrado de un extremo a otro. Mantén a los gigantes tecnológicos fuera de tus mensajes directos.";
/* 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." = "%@. Deja propinas en las publicaciones de tus amigos y acumula sats con Bitcoin⚡️, la moneda nativa de internet.";
/* Number of reposts.
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" = "Información";
/* Label for About Me section of user profile form. */
"About Me" = "Acerca de mí";
/* Placeholder text for About Me description. */
"Absolute Boss" = "Jefe supremo";
/* Label to indicate the public ID of the account. */
"Account ID" = "Identificador de cuenta";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Agregar";
/* Label for section for adding a relay server. */
"Add Relay" = "Agregar relé";
/* Any amount of sats */
"Any" = "Cualquiera";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "¿Seguro quieres volver a publicar esto?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Imagen del banner";
/* 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." = "Antes de empezar, tendrás que guardar la información de tu cuenta. De lo contrario, no podrás iniciar sesión en el futuro si llegas a desinstalar 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" = "Propinas con Bitcoin Lightning";
/* Dropdown option label for Lightning wallet, Blixt Wallet */
"Blixt Wallet" = "Blixt Wallet";
/* 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" = "Transmitir";
/* 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. */
"Cancel" = "Cancelar";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "Chat";
/* Button for clearing cached data. */
"Clear" = "Borrar";
/* Section title for clearing cached data. */
"Clear Cache" = "Borrar caché";
/* Label indicating that a user's key was copied. */
"Copied" = "Copiada";
/* Button to copy a relay server address. */
"Copy" = "Copiar";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "Copiar identificador de cuenta";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "Copiar imagen";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "Copiar URL de imagen";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Copiar factura";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "Copiar LNURL";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "Copiar identificador de nota";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Copiar JSON de nota";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Copiar texto";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Copiar identificador de usuario";
/* Button to create account. */
"Create" = "Crear";
/* Button to create an account. */
"Create Account" = "Crear cuenta";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Creador(es) de Bitcoin. Toda una leyenda.";
/* 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" = "Billetera predeterminada";
/* Button to delete a relay server that the user connects to. */
"Delete" = "Borrar";
/* Button to dismiss a text field alert. */
"Dismiss" = "Descartar";
/* Label to prompt display name entry. */
"Display Name" = "Mostrar nombre";
/* Navigation title for DM view, which is the English abbreviation for Direct Message. */
"DM" = "MD";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "Listo";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "Gana dinero";
/* Button to edit user's profile. */
"Edit" = "Editar";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "Cifrada";
/* Navigation title for view of encrypted DMs, where DM is an English abbreviation for Direct Message. */
"Encrypted DMs" = "MD cifrados";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "Ingresa la clave de tu cuenta para iniciar sesión:";
/* Error message indicating why saving keys failed. */
"Error: %@" = "Error: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Estado del filtro";
/* Button to follow a user. */
"Follow" = "Seguir";
/* Label describing followers of a user. */
"Followers" = "Seguidores";
/* 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" = "Siguiendo";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "Siguiendo...";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "Sigue";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Global";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "Ir a la publicación %@";
/* Navigation link to go to profile. */
"Goto profile %@" = "Ir al perfil %@";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Inicio";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://ejemplo.com/foto.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Clave inválida";
/* 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" = "Zurdo";
/* Button to complete account creation and start using the app. */
"Let's go!" = "¡Vamos!";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Dirección de Lightning o LNURL";
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "Factura de Lightning";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "LNLink";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "Predeterminada del sistema";
/* Button to log into account.
Button to log into an account. */
"Login" = "Iniciar sesión";
/* Alert for logging out the user.
Button for logging out the user.
Button to logout the user. */
"Logout" = "Cerrar sesión";
/* 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" = "Asegúrate de que tu clave de cuenta nsec esté guardada antes de cerrar sesión o perderás el acceso a esta cuenta";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "Verificación NIP-05";
/* No search results. */
"none" = "ninguno";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Nada para ver aquí. ¡Vuelve a consultar luego!";
/* Navigation title for notifications. */
"Notifications" = "Notificaciones";
/* String indicating that a given timestamp just occurred */
"now" = "ahora";
/* 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...";
/* Label indicating that a form input is optional. */
"optional" = "opcional";
/* Button to pay a Lightning invoice. */
"Pay" = "Pagar";
/* Navigation bar title for view to pay Lightning invoice. */
"Pay the Lightning invoice" = "Paga la factura de Lightning";
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Button to post a note. */
"Post" = "Publicar";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Publicaciones";
/* Label for filter for seeing posts and replies (instead of only posts). */
"Posts & Replies" = "Publicaciones y respuestas";
/* 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" = "Privada";
/* 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. */
"Private Key" = "Clave privada";
/* Title of the secure field that holds the user's private key. */
"PrivateKey" = "ClavePrivada";
/* Sidebar menu label for Profile view. */
"Profile" = "Perfil";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "Foto del perfil";
/* Section title for the user's public account ID. */
"Public Account ID" = "Identificador de cuenta pública";
/* Label indicating that the text is a user's public account key. */
"Public key" = "Clave pública";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "Clave pública";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "¿Clave pública?";
/* Navigation bar title for Reactions view. */
"Reactions" = "Reacciones";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Relés recomendados";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relé";
/* Sidebar menu label for Relay servers view */
"Relays" = "Relés";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "Respuesta a sí mismo";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "Respondiendo a %1$@ y %2$@";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Respondiendo a:";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Republicar";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Republicada";
/* Section title for resetting the user */
"Reset" = "Reiniciar";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Reintentar";
/* 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" = "Guardar";
/* Context menu option to save an image. */
"Save Image" = "Guardar imagen";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Buscar hashtag: #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "Búsqueda...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "Clave de inicio de sesión de cuenta secreta";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "Seleccionar una billetera de Lightning";
/* Prompt selection of user's default wallet */
"Select default wallet" = "Seleccionar billetera predeterminada";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Envía un mensaje para empezar la conversación...";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Configuración";
/* Button to share an image. */
"Share" = "Compartir";
/* Toggle to show or hide user's secret account login key. */
"Show" = "Mostrar";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Mostrar selector de billetera";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "Cerrar sesión";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* 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." = "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.";
/* 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." = "Esta es una clave de nostr antigua. No sabemos con seguridad si es una clave pública o privada. Activa el siguiente botón si es una clave pública.";
/* 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." = "Este es tu identificador de cuenta, que puedes compartir con tus amigos para que te sigan. Haz clic para copiarlo.";
/* 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!" = "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!";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Hilo";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Ingresa tu publicación aquí...";
/* 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" = "Dejar de seguir";
/* 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" = "Dejando de seguir";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "Dejando de seguir...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Deja de seguir";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Nombre de usuario";
/* Sidebar menu label for Wallet view. */
"Wallet" = "Billetera";
/* Dropdown option label for Lightning wallet, Wallet Of Satoshi. */
"Wallet Of Satoshi" = "Wallet Of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "Selección de billetera";
/* Label for Website section of user profile form. */
"Website" = "Sitio web";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "Te damos la bienvenida a la red social que %@ controlas.";
/* Text to welcome user. */
"Welcome, %@!" = "¡Te damos la bienvenida, %@!";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://algún.relé.com";
/* 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" = "tú";
/* Label for Your Name section of user profile form. */
"Your Name" = "Tu nombre";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";
+136 -42
View File
@@ -2,58 +2,152 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<key>other</key>
<string> &amp; %d others</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<key>other</key>
<string> &amp; %d others</string>
</dict>
</dict>
<key>collapsed_event_view_other_notes</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>0 other notes</string>
<key>one</key>
<string>1 other note</string>
<string>%d otra nota</string>
<key>other</key>
<string>%d other notes</string>
<string>%d otras notas</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>Seguidor</string>
<key>other</key>
<string>Seguidores</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>Reacción</string>
<key>other</key>
<string>Reacciones</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>Relé</string>
<key>other</key>
<string>Relés</string>
</dict>
</dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Respondiendo a %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string> y %d otro</string>
<key>other</key>
<string> y %d otros</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Respondiendo a %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string> y %d otro</string>
<key>other</key>
<string> y %d otros</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>Republicación</string>
<key>other</key>
<string>Republicaciones</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>Propina</string>
<key>other</key>
<string>Propinas</string>
</dict>
</dict>
</dict>
+9
View File
@@ -0,0 +1,9 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Donner accès à Damus à vos photos vous permet d'enregistrer des images";
+503
View File
@@ -0,0 +1,503 @@
/* 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" = "'%@' à '@' sera utilisé pour la vérification";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'@' est un identifiant nip05 invalide. Cela devrait ressembler à une adresse e-mail.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "Abonnés de (Profile.displayName(profile: profile, pubkey: whos))";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) suit";
/* 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 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'. */
"%@ %@" = "%@ %@";
/* 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." = "%@. La création d'un compte ne nécessite pas de numéro de téléphone, d'e-mail ou de nom. Commencez tout de suite sans aucune 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" = "%@. Messagerie privée cryptée de bout en bout. Gardez Big Tech hors de vos 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." = "%@. Donnez un pourboire aux publications de vos amis et empilez les sats avec Bitcoin⚡️, la monnaie native d'Internet.";
/* Number of reposts.
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" = "À propos de";
/* Label for About Me section of user profile form. */
"About Me" = "À Propos de Moi";
/* Placeholder text for About Me description. */
"Absolute Boss" = "Patron Absolu";
/* Label to indicate the public ID of the account. */
"Account ID" = "Identifiant de Compte";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Ajouter";
/* Button label to re-add all original participants as profiles to reply to in a note */
"Add all" = "Tout ajouter";
/* Label for section for adding a relay server. */
"Add Relay" = "Ajouter un Relais";
/* Any amount of sats */
"Any" = "N'importe Lequel";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Êtes-vous sûr de vouloir republier ceci ?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Image Bannière";
/* 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." = "Avant de commencer, vous devrez enregistrer les informations de votre compte, sinon vous ne pourrez plus vous connecter à l'avenir si vous désinstallez 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" = "Pourboires de Bitcoin Lightning";
/* Dropdown option label for Lightning wallet, Blixt Wallet */
"Blixt Wallet" = "Blixt Wallet";
/* 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" = "Diffuser";
/* 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. */
"Cancel" = "Annuler";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "Chatter";
/* Button for clearing cached data. */
"Clear" = "Vider";
/* Section title for clearing cached data. */
"Clear Cache" = "Vider le Cache";
/* Label indicating that a user's key was copied. */
"Copied" = "Copié";
/* Button to copy a relay server address. */
"Copy" = "Copier";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "Copier l'Identifiant de Compte";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "Copier l'Image";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "Copier l'URL de l'Image";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Copier la Facture";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "Copier le LNURL";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "Copier l'Identifiant de la Note";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Copier le JSON de la Note";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Copier le Texte";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Copier l'Identifiant de l'Utilisateur";
/* Button to create account. */
"Create" = "Créer";
/* Button to create an account. */
"Create Account" = "Créer un Compte";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Créateur(s) de Bitcoin. Légende absolue.";
/* 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" = "Portefeuille par défaut";
/* Button to delete a relay server that the user connects to. */
"Delete" = "Effacer";
/* Button to dismiss a text field alert. */
"Dismiss" = "Rejeter";
/* Label to prompt display name entry. */
"Display Name" = "Afficher Nom";
/* 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. */
"DM Type" = "Type de message privé";
/* 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" = "Messages privés";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "Fini";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "Gagnes de l'argent";
/* Button to edit user's profile. */
"Edit" = "Modifier";
/* Text indicating that the view is used for editing which participants are replied to in a note. */
"Edit participants" = "Modifier les participants";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "Crypté";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "Entrez votre clé de compte pour vous connecter:";
/* Error message indicating why saving keys failed. */
"Error: %@" = "Erreur: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "État du filtre";
/* Button to follow a user. */
"Follow" = "S'abonner";
/* Label describing followers of a user. */
"Followers" = "Abonnés";
/* 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" = "Abonnements";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "Abonnements...";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "Suit";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Global";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "Aller au post %@";
/* Navigation link to go to profile. */
"Goto profile %@" = "Aller au profil %@";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Accueil";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://exemple.com/pic.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Clé non valide";
/* 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" = "Gaucher";
/* Button to complete account creation and start using the app. */
"Let's go!" = "Allons-y!";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Adresse Lightning ou LNURL";
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "Facture Lightning";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "LNLink";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "Valeur locale par défaut";
/* Button to log into account.
Button to log into an account. */
"Login" = "Se connecter";
/* Alert for logging out the user.
Button for logging out the user.
Button to logout the user. */
"Logout" = "Se déconnecter";
/* 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" = "Assurez-vous que votre clé de compte nsec est enregistrée avant de vous déconnecter ou vous perdrez l'accès à ce compte";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "Vérification NIP-05";
/* No search results. */
"none" = "aucun";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Rien à voir ici. Revenez plus tard!";
/* Navigation title for notifications. */
"Notifications" = "Notifications";
/* String indicating that a given timestamp just occurred */
"now" = "maintenant";
/* 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...";
/* Label indicating that a form input is optional. */
"optional" = "optionnel";
/* Button to pay a Lightning invoice. */
"Pay" = "Payer";
/* Navigation bar title for view to pay Lightning invoice. */
"Pay the Lightning invoice" = "Payer la facture Lightning";
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Button to post a note. */
"Post" = "Publication";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Publications";
/* Label for filter for seeing posts and replies (instead of only posts). */
"Posts & Replies" = "Publications & Réponses";
/* 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" = "Privé";
/* Title of the secure field that holds the user's private key. */
"Private Key" = "Clé Privée";
/* Sidebar menu label for Profile view. */
"Profile" = "Profil";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "Image de profil";
/* Section title for the user's public account ID. */
"Public Account ID" = "Identifiant publique de compte";
/* Label indicating that the text is a user's public account key. */
"Public key" = "Clé Publique";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "Clé Publique";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "Clé Publique?";
/* Navigation bar title for Reactions view. */
"Reactions" = "Réactions";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Relais Recommandés";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relais";
/* Sidebar menu label for Relay servers view */
"Relays" = "Relais";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Tout supprimer";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "Réponse à soi-même";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "Répondre à %1$@ & %2$@";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Répondre à:";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Republier";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "A republié";
/* 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" = "Demandes";
/* Section title for resetting the user */
"Reset" = "Réinitialiser";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Retenter";
/* 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" = "Enregistrer";
/* Context menu option to save an image. */
"Save Image" = "Enregistrer Image";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Rechercher hashtag: #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "Rechercher...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "Clé secrète de connexion au compte";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "Sélectionnez un portefeuille Lightning";
/* Prompt selection of user's default wallet */
"Select default wallet" = "Sélectionnez le portefeuille par défaut";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Envoyez un message pour démarrer la conversation...";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Paramètres";
/* Button to share an image. */
"Share" = "Partager";
/* Toggle to show or hide user's secret account login key. */
"Show" = "Afficher";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Afficher le sélecteur de portefeuille";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "Se déconnecter";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* 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." = "Il s'agit d'une clé publique, vous ne pourrez pas publier de messages ou interagir de quelque manière que ce soit. Ceci est utilisé pour visualiser les comptes de leur point de vue.";
/* 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." = "Il s'agit d'une clé Nostr à l'ancienne. Nous ne savons pas s'il s'agit d'une clé publique ou d'une clé privée. Veuillez basculer le bouton ci-dessous s'il s'agit d'une clé publique.";
/* 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." = "Ceci est votre identifiant de compte, vous pouvez le donner à vos amis afin qu'ils puissent vous suivre. Cliquez pour copier.";
/* 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!" = "Il s'agit de votre clé de compte secrète. Vous en aurez besoin pour accéder à votre compte. Ne le partagez avec personne ! Enregistrez-le dans un gestionnaire de mots de passe et gardez-le en sécurité!";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Fil de discussion";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Tapez votre message ici...";
/* 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" = "Se désabonner";
/* 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" = "Ne plus suivre";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "Ne plus suivre...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Se désabonne de";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Nom d'utilisateur";
/* Sidebar menu label for Wallet view. */
"Wallet" = "Portefeuille";
/* Dropdown option label for Lightning wallet, Wallet Of Satoshi. */
"Wallet Of Satoshi" = "Wallet Of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "Sélecteur de portefeuille";
/* Label for Website section of user profile form. */
"Website" = "Site Internet";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "Bienvenue sur le réseau social %@ contrôle.";
/* Text to welcome user. */
"Welcome, %@!" = "Bienvenue, %@!";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://un.relais.com";
/* 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" = "vous";
/* Label for Your Name section of user profile form. */
"Your Name" = "Votre Nom";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";
+154
View File
@@ -0,0 +1,154 @@
<?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>%d autre note</string>
<key>other</key>
<string>%d autres 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>one</key>
<string>Abonné</string>
<key>other</key>
<string>Abonnés</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>Réaction</string>
<key>other</key>
<string>Réactions</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>Relais</string>
<key>other</key>
<string>Relais</string>
</dict>
</dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Réponse à %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>&amp; %d autre</string>
<key>other</key>
<string>&amp; %d autres</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Répondre à %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>&amp; %d autre</string>
<key>other</key>
<string>&amp; %d autres</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>Republication</string>
<key>other</key>
<string>Republications</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>Pourboire</string>
<key>other</key>
<string>Pourboires</string>
</dict>
</dict>
</dict>
</plist>
+9
View File
@@ -0,0 +1,9 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Dai il permesso a Damus di accedere alle tue Foto per salvare immagini";
+503
View File
@@ -0,0 +1,503 @@
/* 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" = "'%@' at '%@' sarà usato per la verifica";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'%@' non è un identificatore NIP05 valido. Dovrebbe essere simile ad un indirizzo email.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "Seguaci di (Profile.displayName(profile: profile, pubkey: whos))'";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) segui già";
/* 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 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'. */
"%@ %@" = "%@ %@";
/* 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." = "%@. Per creare un account non hai bisogno di un numero di telefono, un indirizzo email o del tuo nome. Inizia ora senza impegni.";
/* 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" = "%@. I messaggi sono criptati utilizzando la crittografia end-to-end. Mantieni i colossi della tecnologia lontani dai tuoi messaggi";
/* 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." = "%@. Paga i tuoi amici e accumula sats con Bitcoin⚡️, la moneta di internet.";
/* Number of reposts.
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" = "Informazioni";
/* Label for About Me section of user profile form. */
"About Me" = "Io";
/* Placeholder text for About Me description. */
"Absolute Boss" = "Capo supremo";
/* Label to indicate the public ID of the account. */
"Account ID" = "ID dell'account";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Aggiungi";
/* Button label to re-add all original participants as profiles to reply to in a note */
"Add all" = "Aggiungi tutto";
/* Label for section for adding a relay server. */
"Add Relay" = "Aggiungi relè";
/* Any amount of sats */
"Any" = "Qualsiasi";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Sei sicuro di voler segnalare questo post?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Immagine banner";
/* 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." = "Prima di iniziare, dovrai salvare le informazioni del tuo account altrimenti non sarai in grado di accedere in futuro se dovessi disinstallare 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" = "Mancia con Bitcoin Lightning";
/* Dropdown option label for Lightning wallet, Blixt Wallet */
"Blixt Wallet" = "Blixt Wallet";
/* 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" = "Trasmetti";
/* 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. */
"Cancel" = "Annulla";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "Chat";
/* Button for clearing cached data. */
"Clear" = "Cancella";
/* Section title for clearing cached data. */
"Clear Cache" = "Cancella cache";
/* Label indicating that a user's key was copied. */
"Copied" = "Copiato";
/* Button to copy a relay server address. */
"Copy" = "Copia";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "Copia ID dell'Account";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "Copia Immagine";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "Copia URL dell'Immagine";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Copia fattura";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "Copia LNURL";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "Copia ID della Nota";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Copia JSON della Nota";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Copia Testo";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Copia ID dell'Utente";
/* Button to create account. */
"Create" = "Crea";
/* Button to create an account. */
"Create Account" = "Crea Account";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Creatore/i di Bitcoin. Leggenda assoluta";
/* 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" = "Portafoglio Principale";
/* Button to delete a relay server that the user connects to. */
"Delete" = "Cancella";
/* Button to dismiss a text field alert. */
"Dismiss" = "Lascia stare";
/* Label to prompt display name entry. */
"Display Name" = "Nome visualizzato";
/* 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. */
"DM Type" = "Tipo DM";
/* 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" = "DM";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "Finito";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "Guadagna Soldi";
/* Button to edit user's profile. */
"Edit" = "Modifica";
/* Text indicating that the view is used for editing which participants are replied to in a note. */
"Edit participants" = "Modifica partecipanti";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "Criptato";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "Inserisci la chiave del tuo account per accedere";
/* Error message indicating why saving keys failed. */
"Error: %@" = "Errore: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filtra";
/* Button to follow a user. */
"Follow" = "Segui";
/* Label describing followers of a user. */
"Followers" = "Seguaci";
/* 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" = "Seguiti";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "Segui già...";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "Segui";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Globale";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "Vai al post %@";
/* Navigation link to go to profile. */
"Goto profile %@" = "Vai al profilo %@";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Home";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://esempio.com/foto.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Chiave non valida";
/* 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" = "Mancino";
/* Button to complete account creation and start using the app. */
"Let's go!" = "Andiamo!";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Indirizzo Lightning o LNURL";
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "Fattura Lightning";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "LNLink";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "Predefinito";
/* Button to log into account.
Button to log into an account. */
"Login" = "Entra";
/* Alert for logging out the user.
Button for logging out the user.
Button to logout the user. */
"Logout" = "Esci";
/* 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" = "Assicurati di aver salvato la chiave privata (nSEC) prima di uscire o perderai l'accesso a questo account";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "Verifica NIP-05";
/* No search results. */
"none" = "Nessun risultato";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Niente da vedere qui. Controlla dopo!";
/* Navigation title for notifications. */
"Notifications" = "Notifiche";
/* String indicating that a given timestamp just occurred */
"now" = "ora";
/* 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...";
/* Label indicating that a form input is optional. */
"optional" = "opzione1";
/* Button to pay a Lightning invoice. */
"Pay" = "Paga";
/* Navigation bar title for view to pay Lightning invoice. */
"Pay the Lightning invoice" = "Paga la fattura Lightning";
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Button to post a note. */
"Post" = "Post";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Post";
/* Label for filter for seeing posts and replies (instead of only posts). */
"Posts & Replies" = "Post & Risposte";
/* 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" = "Privato";
/* Title of the secure field that holds the user's private key. */
"Private Key" = "Chiave Privata";
/* Sidebar menu label for Profile view. */
"Profile" = "Profilo1";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "Foto Profilo";
/* Section title for the user's public account ID. */
"Public Account ID" = "ID Pubblico dell'Account";
/* Label indicating that the text is a user's public account key. */
"Public key" = "Chiave Pubblica";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "Chiave Pubblica";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "È la chiave pubblica?";
/* Navigation bar title for Reactions view. */
"Reactions" = "Reazioni";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Relè consigliati";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relè";
/* Sidebar menu label for Relay servers view */
"Relays" = "Relè";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Rimuovi tutto";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "Rispondi a te stesso";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "Rispondi a %1$@ e %2$@";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Rispondi a:";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Reposta";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Repostato";
/* 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" = "Richiesta";
/* Section title for resetting the user */
"Reset" = "Ricomincia";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Riprova";
/* 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" = "Salva";
/* Context menu option to save an image. */
"Save Image" = "Salva Immagine";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Cerca hashtag: #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "Cerca...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "Chiave login segreta dell'Account";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "Seleziona un portafoglio Lightning";
/* Prompt selection of user's default wallet */
"Select default wallet" = "Seleziona un wallet predefinito";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Invia un messaggio e inizia la conversazione...";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Impostazioni";
/* Button to share an image. */
"Share" = "Condividi";
/* Toggle to show or hide user's secret account login key. */
"Show" = "Mostra";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Mostra wallet disponibili";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "Esci";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* 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." = "Questa è una chiave pubblica, non potrai postare o interagire in alcun modo. Puoi utilizzarla solo per vedere gli account";
/* 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." = "Questa è una chiave di vecchio tipo. Non siamo sicuri se si tratti di una chiave pubblica o privata. Utilizza il pulsante sottostante se si tratta di una chiave pubblica.";
/* 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." = "Questo è l'ID del tuo account. Condividilo con i tuoi amici per farti seguire. Clicca per copiare";
/* 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!" = "Questa è la tua chiave privata. Ti serve ad accedere al tuo account. Non condividerla con nessuno! Salvala in un gestore password e tienila al sicuro";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Thread";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Scrivi il tuo post qui...";
/* 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" = "Smetti di seguire";
/* 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" = "Smetti di seguire";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "Togliendo il segui...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Smetti di seguire";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Nome utente";
/* Sidebar menu label for Wallet view. */
"Wallet" = "Portafoglio";
/* Dropdown option label for Lightning wallet, Wallet Of Satoshi. */
"Wallet Of Satoshi" = "Wallet Of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "Seleziona un portafoglio";
/* Label for Website section of user profile form. */
"Website" = "Sito web";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "Benvenuto nel social network %@ controlla.";
/* Text to welcome user. */
"Welcome, %@!" = "Benvenuto, %@!";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://un.relè.com";
/* 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" = "tu";
/* Label for Your Name section of user profile form. */
"Your Name" = "Nome";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";
@@ -4,39 +4,35 @@
<dict>
<key>collapsed_event_view_other_notes</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>0 other notes</string>
<key>one</key>
<string>1 other note</string>
<string>%d other note</string>
<key>other</key>
<string>%d other notes</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
<key>FOLLOWERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Followers</string>
<key>one</key>
<string>Follower</string>
<key>other</key>
<string>Followers</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
</dict>
<key>reactions_count</key>
<dict>
@@ -48,8 +44,6 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Reactions</string>
<key>one</key>
<string>Reaction</string>
<key>other</key>
@@ -66,8 +60,6 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Relays</string>
<key>one</key>
<string>Relay</string>
<key>other</key>
@@ -84,12 +76,12 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<string> &amp; %d other</string>
<key>other</key>
<string> &amp; %d others</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
@@ -102,12 +94,12 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string> &amp; 1 other</string>
<string> &amp; %d other</string>
<key>other</key>
<string> &amp; %d others</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>reposts_count</key>
@@ -120,14 +112,28 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Reposts</string>
<key>one</key>
<string>Repost</string>
<key>other</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>one</key>
<string>%2$@ sat</string>
<key>other</key>
<string>%2$@ sats</string>
</dict>
</dict>
<key>tips_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -138,8 +144,6 @@
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Tips</string>
<key>one</key>
<string>Tip</string>
<key>other</key>
+9
View File
@@ -0,0 +1,9 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Piešķir Damus piekļuvi saviem fotoattēliem, lai varat saglabāt attēlus.";
+503
View File
@@ -0,0 +1,503 @@
/* 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" = "'%@' pie '%@' tiks izmantota priekš verifikācijas.";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'%@' ir nederīgs nip05 identifikators. Tam vajadzētu būt kā e-pasta adresei.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "(Profils.ParādāmaisVārds(profils: profils, kopAtslēga: kurš))'am Sekotāji";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(kurš) seko";
/* 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 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'. */
"%@ %@" = "%@ %@";
/* 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." = "Lai izveidotu kontu, nav nepieciešams tālruņa numurs, e-pasts vai vārds. Sāciet uzreiz bez aizkaves.";
/* 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" = "%@. Pilnīga šifrēta privātā ziņojumapmaiņa. Esi privātajā iesūtnē bez lielo tekfirmu klātbūtnes.";
/* 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." = "%@. Sponsorē draugu ziņas un krājiet Satiņus ar BitMonētu⚡️, interneta digitālo valūtu.";
/* Number of reposts.
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" = "Par";
/* Label for About Me section of user profile form. */
"About Me" = "Par Mani";
/* Placeholder text for About Me description. */
"Absolute Boss" = "Pilnīgs Priekšnieks";
/* Label to indicate the public ID of the account. */
"Account ID" = "Konta ID";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Pievieno";
/* Button label to re-add all original participants as profiles to reply to in a note */
"Add all" = "Pievienot Visus";
/* Label for section for adding a relay server. */
"Add Relay" = "Pievienot Releju";
/* Any amount of sats */
"Any" = "Jebkurš";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Vai tiešām vēlies šo ziņu pārpublicēt?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Karoga Attēls";
/* 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." = "Pirms sākam darbu, jums ir jāsaglabā sava konta informācija, pretējā gadījumā jūs nevarēsiet pieteikties nākamjā reizē, ja kādreiz atinstalēsiet Damus.";
/* Dropdown option label for Lightning wallet, Bitcoin Beach. */
"Bitcoin Beach" = "Bitmonētas Pludmale";
/* Label for Bitcoin Lightning Tips section of user profile form. */
"Bitcoin Lightning Tips" = "Bitmonētas Zibens Naudiņa";
/* Dropdown option label for Lightning wallet, Blixt Wallet */
"Blixt Wallet" = "Blixt Maciņš";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Maciņš";
/* 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" = "Pārraidīt";
/* 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. */
"Cancel" = "Atcelt";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "Čatiņš";
/* Button for clearing cached data. */
"Clear" = "Notīrīt";
/* Section title for clearing cached data. */
"Clear Cache" = "Notīrīt Kešatmiņu";
/* Label indicating that a user's key was copied. */
"Copied" = "Nokopēts";
/* Button to copy a relay server address. */
"Copy" = "Kopēt";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "Nokopēt konta ID";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "Nokopēt Attēlu";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "Nokopēt Attēla Adresi";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Nokopēt Pavadzīmi";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "Nokopēt Zibens Adresi";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "Nokopēt Ziņas ID";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Nokopēt Ziņas JSON";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Nokopēt Tekstu";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Nokopēt Lietotāja ID";
/* Button to create account. */
"Create" = "Izveidot";
/* Button to create an account. */
"Create Account" = "Izveidot Kontu";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Bitmonētas Radītājs(i). Pilnīgas Leģendas.";
/* 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" = "Noklusējuma Maciņš";
/* Button to delete a relay server that the user connects to. */
"Delete" = "Izdzēst";
/* Button to dismiss a text field alert. */
"Dismiss" = "Atmest";
/* Label to prompt display name entry. */
"Display Name" = "Parādāmais Vārds";
/* 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. */
"DM Type" = "Privātās Iesūtnes Tips";
/* 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" = "Privātās Ziņas";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "Pabeigts";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "Pelni Naudu";
/* Button to edit user's profile. */
"Edit" = "Rediģēt";
/* Text indicating that the view is used for editing which participants are replied to in a note. */
"Edit participants" = "Rediģēt dalībniekus";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "Šifrēts";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "Ievadi sava konta atslēgu, lai pieteiktos:";
/* Error message indicating why saving keys failed. */
"Error: %@" = "Kļūda: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filtrēt Štatu";
/* Button to follow a user. */
"Follow" = "Sekot";
/* Label describing followers of a user. */
"Followers" = "Sekotāji";
/* 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" = "Sekoju";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "Sekoju..";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "Seko";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Globāls";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "Ej uz ierakstu %@";
/* Navigation link to go to profile. */
"Goto profile %@" = "Ej uz profilu %@";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Sākums";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://paraugs.lv/att.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Nepareiza atslēga";
/* 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" = "Kreilis";
/* Button to complete account creation and start using the app. */
"Let's go!" = "Uz priekšu!";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Zibens Adrese vai ZibSaite";
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "Zibens Pavadzīme";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "ZibSaite";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "Vietējais Noklusējums";
/* Button to log into account.
Button to log into an account. */
"Login" = "Pieteikties";
/* Alert for logging out the user.
Button for logging out the user.
Button to logout the user. */
"Logout" = "Iziet";
/* 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" = "Pirms iziešanas pārliecinieties, vai jūsu nsec konta atslēga ir saglabāta, pretējā gadījumā jūs zaudēsiet piekļuvi šim kontam";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "NIP-05 Verifikācija";
/* No search results. */
"none" = "neviens";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Nav ko redzēt te. Ienāc šeit vēlāk.";
/* Navigation title for notifications. */
"Notifications" = "Paziņojumi";
/* String indicating that a given timestamp just occurred */
"now" = "tagad";
/* 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...";
/* Label indicating that a form input is optional. */
"optional" = "pēc izvēles";
/* Button to pay a Lightning invoice. */
"Pay" = "Maksāt";
/* Navigation bar title for view to pay Lightning invoice. */
"Pay the Lightning invoice" = "Samaksāt Zibens Pavadzīmi";
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Button to post a note. */
"Post" = "Publicēt";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Ieraksti";
/* Label for filter for seeing posts and replies (instead of only posts). */
"Posts & Replies" = "Ieraksti & Atbildes";
/* 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" = "Privāts";
/* Title of the secure field that holds the user's private key. */
"Private Key" = "Privātā Atslēga";
/* Sidebar menu label for Profile view. */
"Profile" = "Profils";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "Profila Attēls";
/* Section title for the user's public account ID. */
"Public Account ID" = "Publiskais Konta ID";
/* Label indicating that the text is a user's public account key. */
"Public key" = "Publiskā atslēga";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "Publiskā Atslēga";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "Vai Publiskā Atslēga?";
/* Navigation bar title for Reactions view. */
"Reactions" = "Reakcijas";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Ieteicamie Releji";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relejs";
/* Sidebar menu label for Relay servers view */
"Relays" = "Releji";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Noņemt visu";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "Atbildēt sev";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "Atbildot %1$@ & %2$@";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Atbildot";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Pārpublicēt";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Pārpublicēts";
/* 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" = "Pieprasījumi";
/* Section title for resetting the user */
"Reset" = "Atiestatīt";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Mēģiniet vēlreiz";
/* Dropdown option label for Lightning wallet, River */
"River" = "Plūsma";
/* Example username of Bitcoin creator(s), Satoshi Nakamoto. */
"satoshi" = "satoshi";
/* Name of Bitcoin creator(s). */
"Satoshi Nakamoto" = "Satoshi Nakamoto";
/* Button for saving profile. */
"Save" = "Saglabāt";
/* Context menu option to save an image. */
"Save Image" = "Saglabāt Attēlu";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Meklēt Režģbirku #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "Meklēt...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "Konta Slepenā Pietiekšanās Atslēga";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "Atlasīt Zibens maciņu";
/* Prompt selection of user's default wallet */
"Select default wallet" = "Atlasīt noklusējuma maciņu";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Sūti ziņu, lai sāktu sarunu...";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Uzstādījumi";
/* Button to share an image. */
"Share" = "Dalīties";
/* Toggle to show or hide user's secret account login key. */
"Show" = "Parādīt";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Parādīt maciņu atlasītāju";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "Atteikties";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* 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." = "Šī ir publiskā atslēga, jūs nevarēsit rakstīt ziņas vai kādā citā veidā darboties. To izmanto, lai tikai skatītu kontus no to skatpunkta.";
/* 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." = "Šī ir vecā parauga nostr atslēga. Mēs neesam pārliecināti, vai tā ir kopAtslēga vai slepenā atslēga. Lūdzu, pārslēdziet ar tālāk esošo pogu, ja šī ir publiskā atslēga.";
/* 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." = "Šis ir jūsu konta ID. Varat to iedot draugiem, lai viņi varētu jums sekot. Noklikšķiniet, lai nokopētu.";
/* 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!" = "Šī ir jūsu konta slepenā atslēga. Tā ir nepieciešama, lai piekļūtu savam kontam. Nedalies ar to nevienam! Saglabājiet to paroļu pārvaldniekā un glabājiet to drošībā!";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Plūsma";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Rakstiet savu ziņu šeit...";
/* 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" = "Atsekot";
/* 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" = "Atsekoju";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "Atsekoju...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Atsekos";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Lietotājvārds";
/* Sidebar menu label for Wallet view. */
"Wallet" = "Maciņš";
/* Dropdown option label for Lightning wallet, Wallet Of Satoshi. */
"Wallet Of Satoshi" = "Wallet Of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "Maciņu Atlasītājs";
/* Label for Website section of user profile form. */
"Website" = "Mājaslapa";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "Esi sveicināts sociālo tīklu %@ vadībā.";
/* Text to welcome user. */
"Welcome, %@!" = "Laipni lūgts, %@!";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://cits.relejs.lv";
/* 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" = "jūs";
/* Label for Your Name section of user profile form. */
"Your Name" = "Tavs Vārds";
/* 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