Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
cd9e8bf9b1
|
|||
| 331d7e9792 | |||
| d21613a765 | |||
| 7780120504 | |||
| 1696e0365e | |||
| 006f8d79e0 | |||
| 135432e03c | |||
| 1fd4d4d950 | |||
| 7d406fd75f | |||
| 0902548336 | |||
| 09547529ad | |||
| 6bd7e7563c | |||
| 5ec77bf8d2 | |||
| 33368c3ac4 | |||
| 99d282ee20 | |||
| a9009049c9 | |||
| e64abca1f0 | |||
| e90408027b | |||
| 58a74af25b | |||
| 0a33f4ca1c | |||
| 960ed8158c | |||
|
0cff4dc194
|
|||
| 03822418c7 | |||
|
de510423f6
|
|||
| 264fbac16c | |||
|
2cd508c4c2
|
|||
|
5e0b4583c0
|
|||
|
4d2a670c72
|
|||
|
73d17ac708
|
|||
|
c2e955faa5
|
|||
|
58d95a0c15
|
|||
|
d86a6a9e16
|
|||
|
1269c00485
|
|||
| 98183cb4a8 | |||
| 537100d923 | |||
| ca3c65496a | |||
| 9b2fb867b4 | |||
| 52f6dff4e9 | |||
| 94811b3737 | |||
| 921b5a2a31 | |||
| 116825b556 | |||
| e40cc9a50a | |||
| 43f6053429 | |||
| 1e8d8120ac | |||
| dfb681cc02 | |||
| 889c584487 | |||
| 72f00fb413 | |||
| d6694fac40 | |||
| d4068f8d52 | |||
| 7d410bff34 | |||
| b25e2ff6c0 | |||
| eddff1a579 | |||
| 387e1bcf22 | |||
| 4da002e1b4 | |||
| 139a2455a5 | |||
| e058f7e8e1 | |||
| ec3f0b3c5d | |||
| 20b1697e40 | |||
| 159f00e466 | |||
| 57635b3c17 | |||
|
900094fae4
|
|||
|
4fbc9882ce
|
|||
| e1578c0337 | |||
| 9fa11118d3 | |||
|
3aac4e2f7f
|
|||
|
133c237105
|
|||
|
f59d267863
|
|||
| 78b4035d51 | |||
| dcc4b7b5e4 | |||
| 1af12e5e81 | |||
| 2eeeb081fd | |||
| 7affc5ae4b | |||
| f283519a0d | |||
| 3317f23618 | |||
| 2ed17a2509 | |||
| 08ca484d54 | |||
| 2feaa207d7 | |||
| bb6a09179e | |||
| 49f64e7f49 | |||
|
a65a6966ac
|
|||
|
6f15746b8a
|
|||
| 13066a8fa2 | |||
| c647daf9b9 | |||
| 7bcc345038 | |||
| bf0f879d66 | |||
| 3af9131afe | |||
| b6b6d033a8 | |||
| 819d7496b2 | |||
| 4c58e73e18 | |||
| 6e38707aaa | |||
| 0f08612b79 | |||
| ef89c4b33b | |||
| 5c9bc02ac6 | |||
| b57d2a3a6e | |||
| 0e8c94b668 | |||
| 3e6c8c47a7 | |||
| e4beb872a5 | |||
| 552bd9cae5 | |||
| 059a16a8dc | |||
| b6ea17a0eb | |||
| a9e9f0dc8f | |||
| 5edb7df5c4 | |||
| d559dd3a13 | |||
| b9c2473a2d | |||
| 196081cd38 | |||
|
3e02cc6889
|
|||
|
51f94cf135
|
|||
|
a20fa08030
|
|||
| 203203a706 | |||
| 92239eae69 | |||
| 5de745fb19 | |||
| 1baae90beb | |||
| 2b832120ec | |||
| 255668c17a | |||
| c046c7cf45 | |||
| 5daaec35a8 | |||
| abfbc8c9aa | |||
| 44b1136b86 | |||
| dc28456122 | |||
| 0dd804f61c | |||
| a3e7abc85d | |||
| d61d7df91b | |||
| 5e3ce4e454 | |||
| 59abc7b608 | |||
| 74d8d57542 | |||
| 214e45a98b | |||
| 2a8b9f75c1 | |||
| 7d323b65e4 | |||
| b69116e685 | |||
| 561e2cd3ad | |||
| ad87a62486 | |||
| 5793db4053 | |||
|
e736f8f837
|
|||
| 81c1993156 | |||
|
4d97dbcacf
|
|||
|
af72cf4e06
|
|||
|
55ba3f8c1b
|
|||
|
d7ab33e731
|
|||
|
1203b1d7fc
|
|||
| a62f3e2737 | |||
| 209a1c3213 | |||
| 675903b768 | |||
| 92035e17d3 | |||
| 8df5bf04ae | |||
| cf79fd9491 | |||
| 56d43f1ad1 | |||
| 7e1daf7816 | |||
| 0ead583bda | |||
| dd44bd779b | |||
| c31374fc0a | |||
| 984a1b916d | |||
| b8cefb9392 | |||
| f3730630b5 | |||
|
d5f2a17249
|
|||
|
4526ed01fe
|
|||
| a590fb099d | |||
| 135814737c | |||
|
e2e60639d9
|
|||
|
83909c8fc9
|
|||
| 3e4914462b | |||
| 7a11433a98 | |||
| 03e1c1903f | |||
| acdee6a326 | |||
| f5e03f145c | |||
| 2a9ddd10c8 | |||
| 5e9580377d | |||
| 9d2ff2fe65 | |||
| 13ea42a2e2 | |||
| d9e22ce7bf | |||
| 2335a65b78 | |||
| 566cd141ce | |||
| 55f7f8c072 | |||
| 4b4addd215 | |||
| 255a0c55ba | |||
| ced6e2488f | |||
| 77c2abc524 | |||
| e4ad15ced1 | |||
| 904a6e960a | |||
|
da8a82954a
|
|||
|
383f45fe96
|
|||
|
9dc0f3baf6
|
|||
|
abc857582f
|
|||
| 4816b57dcd | |||
| 06b1953b49 | |||
| d658d1d987 | |||
| e14cd99c85 | |||
| 0258ef792f | |||
| 52524e00a2 | |||
| 342883fbb0 | |||
| 1e6505abe3 | |||
| 98c24147e8 | |||
| d1e7de5dcb | |||
| 11e0a87f06 | |||
| 1154cec719 | |||
| 1ecfb0487e | |||
| 8ffa8446b6 | |||
| fb1f99e728 | |||
| 031408dec3 | |||
| 16b6d029fa | |||
| 3e093e8572 | |||
| fa11af4b1d | |||
| 91159d70ca | |||
| a57d654f32 | |||
| 0313480685 | |||
| e07b31e0a1 | |||
|
538a0ae5ea
|
|||
|
0f82db2440
|
|||
| 92ae2c7754 | |||
| 00c819140b | |||
| cbc3c46c9d | |||
| 4b5c34b4e2 | |||
| 9eb39f7e0a | |||
| 173b22b772 | |||
| 18c7cba53c | |||
| 9a40fd595d | |||
| a71c35a6b0 | |||
| d69d3cc74e | |||
| 6e220ac4c1 | |||
| bcb40a6ec7 | |||
| 2f1063b49f | |||
| 73110952e5 | |||
| d01e7c0595 | |||
| aba758b143 | |||
| b7c7b0b3bf | |||
| 2d10d4592b |
@@ -1,4 +1,6 @@
|
|||||||
name: Test
|
name: Run Test Suite
|
||||||
|
run-name: Testing ${{ github.ref }} by @${{ github.actor }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -6,12 +8,24 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "*"
|
- "*"
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
run_tests:
|
||||||
name: Run Tests
|
runs-on: macos-12
|
||||||
runs-on: macos-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- xcode: "14.2"
|
||||||
|
ios: "16.2"
|
||||||
|
|
||||||
|
name: Test iOS (${{ matrix.ios }})
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v1
|
||||||
- name: Running Tests
|
- name: Select Xcode
|
||||||
run: xcodebuild test -scheme damus -project damus.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' | xcpretty && exit ${PIPESTATUS[0]}
|
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]}
|
||||||
+131
@@ -1,3 +1,134 @@
|
|||||||
|
## [1.0.0-13] - 2023-01-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- LibreTranslate note translations (Terry Yiu)
|
||||||
|
- Added support for account deletion (William Casarin)
|
||||||
|
- User tagging and autocompletion in posts (Swift)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Remove redundant logout button from settings (Jonathan Milligan)
|
||||||
|
- Moved relay config to its own sidebar entry (William Casarin)
|
||||||
|
- New stylized tabs (ericholguin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix hidden profile action sheet when clicking ... (William Casarin)
|
||||||
|
- Fixed height of DM input (Terry Yiu)
|
||||||
|
- Fixed bug where copying pubkey from context menu only copied your own pubkey (Terry Yiu)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-13]: https://github.com/damus-io/damus/releases/tag/v1.0.0-13
|
||||||
|
## [1.0.0-12] - 2023-01-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added Arabic and Portuguese translations (Barodane, Antonio Chagas)
|
||||||
|
- Add QRCode view for sharing your pubkey (ericholguin)
|
||||||
|
- Added nostr: uri handling (William Casarin)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Remove markdown link support from posts (Joel Klabo)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed crash on some SVG profile pictures (OlegAba)
|
||||||
|
- Localization fixes
|
||||||
|
- Don't allow blocking yourself (Terry)
|
||||||
|
- Hide muted users from global (William Casarin)
|
||||||
|
- Fixed profiles sometimes not loading from other clients (William Casarin)
|
||||||
|
- Fixed bug where `spam` was always the report type (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-12]: https://github.com/damus-io/damus/releases/tag/v1.0.0-12
|
||||||
|
|
||||||
|
## [1.0.0-11] - 2023-01-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Reposts view (Terry Yiu)
|
||||||
|
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (Nicolò Carcagnì, Solobalbo, Gregor, Peter Gerstbach, SYX)
|
||||||
|
- Added ability to block users (William Casarin)
|
||||||
|
- Added a way to report content (William Casarin)
|
||||||
|
- Stretchable profile cover header (Swift)
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump pfp/banner animated fize size limit to 5MiB/20MiB (William Casarin)
|
||||||
|
- Updated default boostrap relays (Ricardo Arturo Cabral Mejía)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- allow ws:// relays again (Steven Briscoe)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-11]: https://github.com/damus-io/damus/releases/tag/v1.0.0-11
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0-8] - 2023-01-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Show website on profiles (William Casarin)
|
||||||
|
- Add the ability to choose participants when replying (Joel Klabo)
|
||||||
|
- Translations for de_AT, de_DE, tr_TR, fr_FR (Gregor, Peter Gerstbach, Taylan Benli, Solobalbo)
|
||||||
|
- Add DM Message Requests (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix commands and emojis getting included in hashtags (William Casarin)
|
||||||
|
- Fix duplicate post buttons when swiping tabs (Thomas Rademaker)
|
||||||
|
- Show embedded note references (William Casarin)
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0-8]: https://github.com/damus-io/damus/releases/tag/v1.0.0-8
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0-7] - 2023-01-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Drastically improved image viewer (OlegAba)
|
||||||
|
- Added pinch to zoom on images (Swift)
|
||||||
|
- Add Latin American Spanish translations (Nicolás Valencia)
|
||||||
|
- 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
|
## [1.0.0-6] - 2023-01-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -91,15 +91,41 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
|||||||
|
|
||||||
## Contributing
|
## 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-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... :)
|
There may be nostr badges awarded for contributors in the future... :)
|
||||||
|
|
||||||
@@ -107,3 +133,7 @@ First contributors:
|
|||||||
|
|
||||||
1. @randymcmillan
|
1. @randymcmillan
|
||||||
2. @jcarucci27
|
2. @jcarucci27
|
||||||
|
|
||||||
|
### git log bot
|
||||||
|
|
||||||
|
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
|
||||||
|
|||||||
-60
@@ -1,60 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>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> & 1 other</string>
|
|
||||||
<key>other</key>
|
|
||||||
<string> & %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> & 1 other</string>
|
|
||||||
<key>other</key>
|
|
||||||
<string> & %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>
|
|
||||||
<key>other</key>
|
|
||||||
<string>%d other notes</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
BIN
Binary file not shown.
-60
@@ -1,60 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>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> & 1 other</string>
|
|
||||||
<key>other</key>
|
|
||||||
<string> & %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> & 1 other</string>
|
|
||||||
<key>other</key>
|
|
||||||
<string> & %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>
|
|
||||||
<key>other</key>
|
|
||||||
<string>%d other notes</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -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
@@ -22,6 +22,10 @@ static inline int is_whitespace(char c) {
|
|||||||
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
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)
|
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
||||||
{
|
{
|
||||||
c->start = content;
|
c->start = content;
|
||||||
@@ -29,18 +33,35 @@ static void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
|||||||
c->p = content;
|
c->p = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int consume_until_whitespace(struct cursor *cur, int or_end) {
|
static int consume_until_boundary(struct cursor *cur) {
|
||||||
char c;
|
char c;
|
||||||
|
|
||||||
while (cur->p < cur->end) {
|
while (cur->p < cur->end) {
|
||||||
c = *cur->p;
|
c = *cur->p;
|
||||||
|
|
||||||
if (is_whitespace(c))
|
if (is_boundary(c))
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
cur->p++;
|
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;
|
return or_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +166,7 @@ static int parse_hashtag(struct cursor *cur, struct block *block) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
consume_until_whitespace(cur, 1);
|
consume_until_boundary(cur);
|
||||||
|
|
||||||
block->type = BLOCK_HASHTAG;
|
block->type = BLOCK_HASHTAG;
|
||||||
block->block.str.start = (const char*)(start + 1);
|
block->block.str.start = (const char*)(start + 1);
|
||||||
|
|||||||
@@ -12,7 +12,16 @@
|
|||||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
|
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
|
||||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
||||||
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
|
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 */; };
|
||||||
|
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
|
||||||
|
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
|
||||||
|
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
|
||||||
|
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
|
||||||
|
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
||||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
||||||
|
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
|
||||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
||||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
||||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
||||||
@@ -22,7 +31,6 @@
|
|||||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
|
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
|
||||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
|
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
|
||||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
|
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
|
||||||
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; };
|
|
||||||
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
|
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
|
||||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
|
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
|
||||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||||
@@ -80,6 +88,7 @@
|
|||||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */; };
|
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */; };
|
||||||
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; };
|
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; };
|
||||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
|
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
|
||||||
|
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
|
||||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
|
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
|
||||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
|
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
|
||||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
|
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
|
||||||
@@ -111,6 +120,8 @@
|
|||||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
|
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
|
||||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
|
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
|
||||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
||||||
|
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
|
||||||
|
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
|
||||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
|
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
|
||||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
|
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
|
||||||
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */; };
|
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */; };
|
||||||
@@ -122,6 +133,23 @@
|
|||||||
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */; };
|
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */; };
|
||||||
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88392296F798300DC99E7 /* ReactionsModel.swift */; };
|
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88392296F798300DC99E7 /* ReactionsModel.swift */; };
|
||||||
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */; };
|
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */; };
|
||||||
|
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88399297322D200DC99E7 /* DMTests.swift */; };
|
||||||
|
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */; };
|
||||||
|
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A72975FC1800DC99E7 /* Zaps.swift */; };
|
||||||
|
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A9297612FF00DC99E7 /* ZapTests.swift */; };
|
||||||
|
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
|
||||||
|
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
|
||||||
|
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
|
||||||
|
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
|
||||||
|
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
|
||||||
|
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
|
||||||
|
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
|
||||||
|
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; };
|
||||||
|
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 */; };
|
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||||
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
|
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
|
||||||
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
|
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
|
||||||
@@ -143,16 +171,38 @@
|
|||||||
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */; };
|
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */; };
|
||||||
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */; };
|
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */; };
|
||||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2B01280B39E800AB5EEF /* EventActionBar.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 */; };
|
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
|
||||||
|
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||||
|
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||||
|
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
|
||||||
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
|
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
|
||||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
|
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
|
||||||
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
|
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 */; };
|
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
|
||||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
||||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
||||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
|
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
|
||||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
|
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
|
||||||
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -177,9 +227,48 @@
|
|||||||
3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTimelineView.swift; sourceTree = "<group>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
|
3A5EA10F297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-AT"; path = "de-AT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
|
3A5EA110297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-AT"; path = "de-AT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
|
3A5EA111297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "de-AT"; path = "de-AT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
|
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
|
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
|
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
|
3A93342929884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
|
3A93342A29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
|
3A93342B29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pl-PL"; path = "pl-PL.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
|
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
|
||||||
|
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
|
||||||
|
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
|
||||||
|
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
|
||||||
|
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
|
||||||
|
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
|
3AB5B86B2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
3AB5B86C2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
|
3AB72AB8298ECF30004BB58C /* Translator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translator.swift; sourceTree = "<group>"; };
|
||||||
|
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
|
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
|
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
|
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
|
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
|
||||||
|
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTranslateServer.swift; sourceTree = "<group>"; };
|
||||||
|
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
|
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
|
3AEB8005297CCEA900713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "tr-TR"; path = "tr-TR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
|
3AF0BC09298C1F66008E2AB8 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
|
3AF0BC0A298C1F66008E2AB8 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
3AF0BC0B298C1F66008E2AB8 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = zh; path = zh.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
|
3AF6336829884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
|
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
|
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
@@ -191,7 +280,6 @@
|
|||||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
|
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
|
||||||
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
||||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
|
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
|
||||||
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
|
|
||||||
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
||||||
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
|
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
|
||||||
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
||||||
@@ -278,6 +366,7 @@
|
|||||||
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceTests.swift; sourceTree = "<group>"; };
|
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceTests.swift; sourceTree = "<group>"; };
|
||||||
4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; };
|
4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; };
|
||||||
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; };
|
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; };
|
||||||
|
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
|
||||||
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
|
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
|
||||||
4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; };
|
4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; };
|
||||||
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
|
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
|
||||||
@@ -310,6 +399,8 @@
|
|||||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
|
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
|
||||||
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; };
|
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; };
|
||||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||||
|
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
|
||||||
|
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
|
||||||
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
|
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
|
||||||
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; };
|
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; };
|
||||||
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = "<group>"; };
|
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = "<group>"; };
|
||||||
@@ -321,6 +412,23 @@
|
|||||||
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsView.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
|
||||||
|
4CB88399297322D200DC99E7 /* DMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMTests.swift; sourceTree = "<group>"; };
|
||||||
|
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrlPayRequest.swift; sourceTree = "<group>"; };
|
||||||
|
4CB883A72975FC1800DC99E7 /* Zaps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zaps.swift; sourceTree = "<group>"; };
|
||||||
|
4CB883A9297612FF00DC99E7 /* ZapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapTests.swift; sourceTree = "<group>"; };
|
||||||
|
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
|
||||||
|
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
|
||||||
|
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
|
||||||
|
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
|
||||||
|
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
|
||||||
|
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
|
||||||
|
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
|
||||||
|
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
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>"; };
|
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>"; };
|
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
|
||||||
@@ -345,15 +453,37 @@
|
|||||||
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileName.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
|
||||||
|
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
|
||||||
|
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
||||||
|
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
|
||||||
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -393,6 +523,14 @@
|
|||||||
path = "Empty Views";
|
path = "Empty Views";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
3AA24800297E3DAE0090C62D /* Reposts */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3AA24801297E3DC20090C62D /* RepostView.swift */,
|
||||||
|
);
|
||||||
|
path = Reposts;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4C06670728FDE62900038D2A /* damus-c */ = {
|
4C06670728FDE62900038D2A /* damus-c */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -450,6 +588,7 @@
|
|||||||
4C0A3F8D280F63FF000448DE /* Models */ = {
|
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
|
||||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
|
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
|
||||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
|
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
|
||||||
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */,
|
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */,
|
||||||
@@ -480,6 +619,12 @@
|
|||||||
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
|
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
|
||||||
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
|
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
|
||||||
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
|
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
|
||||||
|
7C45AE70297353390031D7BC /* KFImageModel.swift */,
|
||||||
|
4CF0ABD32980996B00D66079 /* Report.swift */,
|
||||||
|
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
|
||||||
|
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
|
||||||
|
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
|
||||||
|
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -487,6 +632,11 @@
|
|||||||
4C75EFA227FA576C0006080F /* Views */ = {
|
4C75EFA227FA576C0006080F /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4CAAD8AE29888A9B00060CEA /* Relays */,
|
||||||
|
4CF0ABF42985CD4200D66079 /* Posting */,
|
||||||
|
4CF0ABDF2981A83000D66079 /* Muting */,
|
||||||
|
4CC7AAEE297F11B300430951 /* Events */,
|
||||||
|
3AA24800297E3DAE0090C62D /* Reposts */,
|
||||||
4CB88394296F7F8100DC99E7 /* Reactions */,
|
4CB88394296F7F8100DC99E7 /* Reactions */,
|
||||||
4CB88387296AF97C00DC99E7 /* ActionBar */,
|
4CB88387296AF97C00DC99E7 /* ActionBar */,
|
||||||
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
|
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
|
||||||
@@ -501,8 +651,8 @@
|
|||||||
4C216F33286F5ACD00040376 /* DMView.swift */,
|
4C216F33286F5ACD00040376 /* DMView.swift */,
|
||||||
E990020E2955F837003BBC5A /* EditMetadataView.swift */,
|
E990020E2955F837003BBC5A /* EditMetadataView.swift */,
|
||||||
3169CAE4294E699400EE4006 /* Empty Views */,
|
3169CAE4294E699400EE4006 /* Empty Views */,
|
||||||
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */,
|
|
||||||
4C75EFB82804A2740006080F /* EventView.swift */,
|
4C75EFB82804A2740006080F /* EventView.swift */,
|
||||||
|
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */,
|
||||||
4C3AC79E2833115300E1F516 /* FollowButtonView.swift */,
|
4C3AC79E2833115300E1F516 /* FollowButtonView.swift */,
|
||||||
4C3AC79C2833036D00E1F516 /* FollowingView.swift */,
|
4C3AC79C2833036D00E1F516 /* FollowingView.swift */,
|
||||||
4C90BD17283A9EE5008EE7EF /* LoginView.swift */,
|
4C90BD17283A9EE5008EE7EF /* LoginView.swift */,
|
||||||
@@ -517,10 +667,8 @@
|
|||||||
4C8682862814DE470026224F /* ProfileView.swift */,
|
4C8682862814DE470026224F /* ProfileView.swift */,
|
||||||
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
|
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
|
||||||
4C363A8B28236B92006E126D /* PubkeyView.swift */,
|
4C363A8B28236B92006E126D /* PubkeyView.swift */,
|
||||||
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
|
|
||||||
4C06670028FC7C5900038D2A /* RelayView.swift */,
|
|
||||||
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */,
|
|
||||||
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
|
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
|
||||||
|
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
|
||||||
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
|
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
|
||||||
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
|
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
|
||||||
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
|
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
|
||||||
@@ -534,6 +682,11 @@
|
|||||||
647D9A8C2968520300A295DE /* SideMenuView.swift */,
|
647D9A8C2968520300A295DE /* SideMenuView.swift */,
|
||||||
9609F057296E220800069BF3 /* BannerImageView.swift */,
|
9609F057296E220800069BF3 /* BannerImageView.swift */,
|
||||||
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */,
|
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */,
|
||||||
|
6439E013296790CF0020672B /* ProfileZoomView.swift */,
|
||||||
|
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
|
||||||
|
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
|
||||||
|
3AA247FE297E3D900090C62D /* RepostsView.swift */,
|
||||||
|
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -561,6 +714,8 @@
|
|||||||
4C7FF7D628233637009601DB /* Util */ = {
|
4C7FF7D628233637009601DB /* Util */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
||||||
|
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
|
||||||
4C3A1D322960DB0500558C0F /* Markdown.swift */,
|
4C3A1D322960DB0500558C0F /* Markdown.swift */,
|
||||||
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
||||||
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
|
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
|
||||||
@@ -575,10 +730,28 @@
|
|||||||
4C3A1D3629637E0500558C0F /* PreviewCache.swift */,
|
4C3A1D3629637E0500558C0F /* PreviewCache.swift */,
|
||||||
64FBD06E296255C400D9D3B2 /* Theme.swift */,
|
64FBD06E296255C400D9D3B2 /* Theme.swift */,
|
||||||
4CB8838529656C8B00DC99E7 /* NIP05.swift */,
|
4CB8838529656C8B00DC99E7 /* NIP05.swift */,
|
||||||
|
4CF0ABD72981980C00D66079 /* Lists.swift */,
|
||||||
|
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
|
||||||
|
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
|
||||||
|
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */,
|
||||||
|
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */,
|
||||||
|
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
|
||||||
|
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
|
||||||
|
3AB72AB8298ECF30004BB58C /* Translator.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4CAAD8AE29888A9B00060CEA /* Relays */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
|
||||||
|
4C06670028FC7C5900038D2A /* RelayView.swift */,
|
||||||
|
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
|
||||||
|
);
|
||||||
|
path = Relays;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4CB88387296AF97C00DC99E7 /* ActionBar */ = {
|
4CB88387296AF97C00DC99E7 /* ActionBar */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -596,6 +769,21 @@
|
|||||||
path = Reactions;
|
path = Reactions;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
4CE4F9DF285287A000C00DD9 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -607,6 +795,13 @@
|
|||||||
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */,
|
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */,
|
||||||
4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */,
|
4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */,
|
||||||
4CB8838C296F710400DC99E7 /* Reposted.swift */,
|
4CB8838C296F710400DC99E7 /* Reposted.swift */,
|
||||||
|
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */,
|
||||||
|
4CC7AAEC297F0B9E00430951 /* Highlight.swift */,
|
||||||
|
5C513FB9297F72980072348F /* CustomPicker.swift */,
|
||||||
|
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
|
||||||
|
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
|
||||||
|
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
|
||||||
|
4C42812B298C848200DBF26F /* TranslateView.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -636,12 +831,15 @@
|
|||||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
F7F0BA23297892AE009531F3 /* Modifiers */,
|
||||||
4C4A3A5A288A1B2200453788 /* damus.entitlements */,
|
4C4A3A5A288A1B2200453788 /* damus.entitlements */,
|
||||||
4CE4F9DF285287A000C00DD9 /* Components */,
|
4CE4F9DF285287A000C00DD9 /* Components */,
|
||||||
4C7FF7D628233637009601DB /* Util */,
|
4C7FF7D628233637009601DB /* Util */,
|
||||||
4C0A3F8D280F63FF000448DE /* Models */,
|
4C0A3F8D280F63FF000448DE /* Models */,
|
||||||
4C75EFAB28049CC80006080F /* Nostr */,
|
4C75EFAB28049CC80006080F /* Nostr */,
|
||||||
4C75EFA72804823E0006080F /* Info.plist */,
|
4C75EFA72804823E0006080F /* Info.plist */,
|
||||||
|
3ACB685D297633BC00C46468 /* Localizable.strings */,
|
||||||
|
3ACB685A297633BC00C46468 /* InfoPlist.strings */,
|
||||||
4C75EFA227FA576C0006080F /* Views */,
|
4C75EFA227FA576C0006080F /* Views */,
|
||||||
4CE6DEE627F7A08100C66700 /* damusApp.swift */,
|
4CE6DEE627F7A08100C66700 /* damusApp.swift */,
|
||||||
4CE6DEE827F7A08100C66700 /* ContentView.swift */,
|
4CE6DEE827F7A08100C66700 /* ContentView.swift */,
|
||||||
@@ -670,6 +868,10 @@
|
|||||||
4CE6DEF727F7A08200C66700 /* damusTests.swift */,
|
4CE6DEF727F7A08200C66700 /* damusTests.swift */,
|
||||||
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */,
|
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */,
|
||||||
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
|
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
|
||||||
|
4CB88399297322D200DC99E7 /* DMTests.swift */,
|
||||||
|
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
|
||||||
|
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
|
||||||
|
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
|
||||||
);
|
);
|
||||||
path = damusTests;
|
path = damusTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -691,6 +893,40 @@
|
|||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
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 */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -785,6 +1021,16 @@
|
|||||||
Base,
|
Base,
|
||||||
"es-419",
|
"es-419",
|
||||||
"en-US",
|
"en-US",
|
||||||
|
"de-AT",
|
||||||
|
"tr-TR",
|
||||||
|
"fr-FR",
|
||||||
|
"lv-LV",
|
||||||
|
"it-IT",
|
||||||
|
de,
|
||||||
|
"pt-PT",
|
||||||
|
"pl-PL",
|
||||||
|
zh,
|
||||||
|
ar,
|
||||||
);
|
);
|
||||||
mainGroup = 4CE6DEDA27F7A08100C66700;
|
mainGroup = 4CE6DEDA27F7A08100C66700;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
@@ -809,7 +1055,9 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
|
||||||
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
|
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
|
||||||
|
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
|
||||||
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
|
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
|
||||||
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */,
|
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */,
|
||||||
);
|
);
|
||||||
@@ -843,12 +1091,15 @@
|
|||||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
||||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
||||||
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */,
|
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */,
|
||||||
|
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
|
||||||
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
||||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
||||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
|
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
|
||||||
|
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
|
||||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
||||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
||||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||||
|
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
|
||||||
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
||||||
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
|
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
|
||||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||||
@@ -856,26 +1107,36 @@
|
|||||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
||||||
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
|
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
|
||||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
|
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
|
||||||
|
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
|
||||||
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
|
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
|
||||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||||
|
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
|
||||||
|
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
|
||||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
||||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
|
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
|
||||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
|
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
|
||||||
|
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
|
||||||
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
|
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
|
||||||
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
||||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
||||||
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
|
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
|
||||||
|
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
|
||||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
||||||
|
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
|
||||||
|
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
|
||||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
|
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
|
||||||
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
|
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
|
||||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||||
|
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
|
||||||
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
|
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
|
||||||
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
|
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
|
||||||
|
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
|
||||||
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
||||||
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
|
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
|
||||||
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
||||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||||
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
||||||
|
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
|
||||||
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
||||||
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
||||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
||||||
@@ -884,19 +1145,29 @@
|
|||||||
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
|
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
|
||||||
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */,
|
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */,
|
||||||
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
|
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
|
||||||
|
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
|
||||||
|
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
|
||||||
|
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
|
||||||
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
|
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
|
||||||
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
|
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
|
||||||
|
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
|
||||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
|
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
|
||||||
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
|
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
|
||||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
|
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
|
||||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
||||||
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
|
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
|
||||||
|
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
|
||||||
|
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
|
||||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
|
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
|
||||||
|
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
|
||||||
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
|
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
|
||||||
|
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
|
||||||
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
||||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
||||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
|
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
|
||||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
|
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
|
||||||
|
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
|
||||||
|
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
|
||||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
||||||
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
|
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
|
||||||
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
|
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
|
||||||
@@ -910,27 +1181,39 @@
|
|||||||
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
|
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
|
||||||
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
||||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
||||||
|
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
|
||||||
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
||||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||||
|
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||||
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
||||||
|
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
|
||||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
|
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
|
||||||
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
|
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
|
||||||
|
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */,
|
||||||
|
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */,
|
||||||
|
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */,
|
||||||
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */,
|
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */,
|
||||||
|
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
|
||||||
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
|
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
|
||||||
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
|
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
|
||||||
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
|
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
|
||||||
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
|
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
|
||||||
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
|
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
|
||||||
|
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
|
||||||
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
|
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
|
||||||
|
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
|
||||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||||
|
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||||
|
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
|
||||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
||||||
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
||||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||||
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
|
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
|
||||||
|
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
|
||||||
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
|
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
|
||||||
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
|
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
|
||||||
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
|
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
|
||||||
@@ -942,27 +1225,36 @@
|
|||||||
4C06670E28FDEAA000038D2A /* utf8.c in Sources */,
|
4C06670E28FDEAA000038D2A /* utf8.c in Sources */,
|
||||||
4C3EA66D28FF782800C48A62 /* amount.c in Sources */,
|
4C3EA66D28FF782800C48A62 /* amount.c in Sources */,
|
||||||
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
|
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
|
||||||
|
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */,
|
||||||
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
|
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
|
||||||
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
|
|
||||||
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
|
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
|
||||||
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
||||||
|
4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */,
|
||||||
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
|
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
|
||||||
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
|
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
|
||||||
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
|
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
|
||||||
|
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
||||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||||
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
|
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
|
||||||
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
|
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
|
||||||
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
|
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
|
||||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
|
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
|
||||||
|
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
|
||||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
||||||
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
|
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
|
||||||
|
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
||||||
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
|
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
|
||||||
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
||||||
|
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
|
||||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
||||||
|
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
|
||||||
|
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
|
||||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
|
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
|
||||||
|
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
||||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||||
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
||||||
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
|
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
|
||||||
|
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
|
||||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
|
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
|
||||||
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
|
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
|
||||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
||||||
@@ -977,9 +1269,13 @@
|
|||||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
|
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
|
||||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||||
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
||||||
|
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||||
|
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
|
||||||
|
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
|
||||||
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
|
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
|
||||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
|
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
|
||||||
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
|
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
|
||||||
|
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -1013,10 +1309,56 @@
|
|||||||
children = (
|
children = (
|
||||||
3A5C4575296A879E0032D398 /* es-419 */,
|
3A5C4575296A879E0032D398 /* es-419 */,
|
||||||
3A2B8B0A296A8982009CC16D /* en-US */,
|
3A2B8B0A296A8982009CC16D /* en-US */,
|
||||||
|
3A5EA111297CCF6C00569477 /* de-AT */,
|
||||||
|
3AEB8005297CCEA900713A25 /* tr-TR */,
|
||||||
|
3A4F3322297CCFEE004B5F72 /* fr-FR */,
|
||||||
|
3A185A06297F2C3800F4BDC0 /* lv-LV */,
|
||||||
|
3A929C22297F2CF80090925E /* it-IT */,
|
||||||
|
3AB5B86C2986D8A3006599D2 /* de */,
|
||||||
|
3AF6336A29884C6B0005672A /* pt-PT */,
|
||||||
|
3A93342B29884CA600D6A8F3 /* pl-PL */,
|
||||||
|
3AF0BC0B298C1F66008E2AB8 /* zh */,
|
||||||
|
3AC524F0298C000B00693EBF /* ar */,
|
||||||
);
|
);
|
||||||
name = Localizable.stringsdict;
|
name = Localizable.stringsdict;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
3ACB685A297633BC00C46468 /* InfoPlist.strings */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
3ACB685B297633BC00C46468 /* es-419 */,
|
||||||
|
3A5EA10F297CCF6C00569477 /* de-AT */,
|
||||||
|
3AEB8003297CCEA800713A25 /* tr-TR */,
|
||||||
|
3A4F3320297CCFEE004B5F72 /* fr-FR */,
|
||||||
|
3A185A04297F2C3800F4BDC0 /* lv-LV */,
|
||||||
|
3A929C20297F2CF80090925E /* it-IT */,
|
||||||
|
3AB5B86A2986D8A3006599D2 /* de */,
|
||||||
|
3AF6336829884C6B0005672A /* pt-PT */,
|
||||||
|
3A93342929884CA600D6A8F3 /* pl-PL */,
|
||||||
|
3AF0BC09298C1F66008E2AB8 /* zh */,
|
||||||
|
3AC524EE298C000B00693EBF /* ar */,
|
||||||
|
);
|
||||||
|
name = InfoPlist.strings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
3ACB685D297633BC00C46468 /* Localizable.strings */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
3ACB685E297633BC00C46468 /* es-419 */,
|
||||||
|
3A5EA110297CCF6C00569477 /* de-AT */,
|
||||||
|
3AEB8004297CCEA800713A25 /* tr-TR */,
|
||||||
|
3A4F3321297CCFEE004B5F72 /* fr-FR */,
|
||||||
|
3A185A05297F2C3800F4BDC0 /* lv-LV */,
|
||||||
|
3A929C21297F2CF80090925E /* it-IT */,
|
||||||
|
3AB5B86B2986D8A3006599D2 /* de */,
|
||||||
|
3AF6336929884C6B0005672A /* pt-PT */,
|
||||||
|
3A93342A29884CA600D6A8F3 /* pl-PL */,
|
||||||
|
3AF0BC0A298C1F66008E2AB8 /* zh */,
|
||||||
|
3AC524EF298C000B00693EBF /* ar */,
|
||||||
|
);
|
||||||
|
name = Localizable.strings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
@@ -1148,7 +1490,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1156,6 +1498,7 @@
|
|||||||
INFOPLIST_FILE = damus/Info.plist;
|
INFOPLIST_FILE = damus/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
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_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@@ -1188,7 +1531,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1196,6 +1539,7 @@
|
|||||||
INFOPLIST_FILE = damus/Info.plist;
|
INFOPLIST_FILE = damus/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
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_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = 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>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// CustomPicker.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Eric Holguin on 1/22/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
|
||||||
|
Color("DamusPurple"),
|
||||||
|
Color("DamusBlue")
|
||||||
|
]), startPoint: .leading, endPoint: .trailing)
|
||||||
|
|
||||||
|
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
@Namespace var picker
|
||||||
|
@Binding var selection: SelectionValue
|
||||||
|
@ViewBuilder let content: Content
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
let contentMirror = Mirror(reflecting: content)
|
||||||
|
let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count
|
||||||
|
HStack {
|
||||||
|
ForEach(0..<blocksCount, id: \.self) { index in
|
||||||
|
let tupleBlock = contentMirror.descendant("value", ".\(index)")
|
||||||
|
let text = Mirror(reflecting: tupleBlock!).descendant("content") as! Text
|
||||||
|
let tag = Mirror(reflecting: tupleBlock!).descendant("modifier", "value", "tagged") as! SelectionValue
|
||||||
|
|
||||||
|
Button {
|
||||||
|
withAnimation(.spring()) {
|
||||||
|
selection = tag
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
text
|
||||||
|
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
|
||||||
|
.font(.system(size: 14, weight: .heavy))
|
||||||
|
}
|
||||||
|
.background(
|
||||||
|
Group {
|
||||||
|
if tag == selection {
|
||||||
|
Rectangle().fill(RECTANGLE_GRADIENT).frame(height: 2.5)
|
||||||
|
.matchedGeometryEffect(id: "selector", in: picker)
|
||||||
|
.cornerRadius(2.5)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
alignment: .bottom
|
||||||
|
)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.accentColor(tag == selection ? textColor() : .gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textColor() -> Color {
|
||||||
|
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,14 +12,14 @@ import Kingfisher
|
|||||||
struct ShareSheet: UIViewControllerRepresentable {
|
struct ShareSheet: UIViewControllerRepresentable {
|
||||||
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
|
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
|
||||||
|
|
||||||
let activityItems: [URL]
|
let activityItems: [URL?]
|
||||||
let callback: Callback? = nil
|
let callback: Callback? = nil
|
||||||
let applicationActivities: [UIActivity]? = nil
|
let applicationActivities: [UIActivity]? = nil
|
||||||
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
|
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> UIActivityViewController {
|
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||||||
let controller = UIActivityViewController(
|
let controller = UIActivityViewController(
|
||||||
activityItems: activityItems,
|
activityItems: activityItems as [Any],
|
||||||
applicationActivities: applicationActivities)
|
applicationActivities: applicationActivities)
|
||||||
controller.excludedActivityTypes = excludedActivityTypes
|
controller.excludedActivityTypes = excludedActivityTypes
|
||||||
controller.completionWithItemsHandler = callback
|
controller.completionWithItemsHandler = callback
|
||||||
@@ -32,7 +32,7 @@ struct ShareSheet: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ImageContextMenuModifier: ViewModifier {
|
struct ImageContextMenuModifier: ViewModifier {
|
||||||
let url: URL
|
let url: URL?
|
||||||
let image: UIImage?
|
let image: UIImage?
|
||||||
@Binding var showShareSheet: Bool
|
@Binding var showShareSheet: Bool
|
||||||
|
|
||||||
@@ -64,8 +64,21 @@ struct ImageContextMenuModifier: ViewModifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImageViewer: View {
|
private struct ImageContainerView: View {
|
||||||
let urls: [URL]
|
|
||||||
|
@ObservedObject var imageModel: KFImageModel
|
||||||
|
|
||||||
|
@State private var image: UIImage?
|
||||||
|
@State private var showShareSheet = false
|
||||||
|
|
||||||
|
init(url: URL?) {
|
||||||
|
self.imageModel = KFImageModel(
|
||||||
|
url: url,
|
||||||
|
fallbackUrl: nil,
|
||||||
|
maxByteSize: 2000000, // 2 MB
|
||||||
|
downsampleSize: CGSize(width: 400, height: 400)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private struct ImageHandler: ImageModifier {
|
private struct ImageHandler: ImageModifier {
|
||||||
@Binding var handler: UIImage?
|
@Binding var handler: UIImage?
|
||||||
@@ -75,45 +88,131 @@ struct ImageViewer: View {
|
|||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var image: UIImage?
|
|
||||||
@State private var showShareSheet = false
|
|
||||||
|
|
||||||
func onShared(completed: Bool) -> Void {
|
var body: some View {
|
||||||
if (completed) {
|
|
||||||
showShareSheet = false
|
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 {
|
var body: some View {
|
||||||
TabView {
|
ZStack {
|
||||||
ForEach(urls, id: \.absoluteString) { url in
|
Color(.systemBackground)
|
||||||
VStack{
|
.ignoresSafeArea()
|
||||||
Text(url.lastPathComponent)
|
|
||||||
|
TabView(selection: $selectedIndex) {
|
||||||
KFAnimatedImage(url)
|
ForEach(urls.indices, id: \.self) { index in
|
||||||
.configure { view in
|
ZoomableScrollView {
|
||||||
view.framePreloadCount = 3
|
ImageContainerView(url: urls[index])
|
||||||
}
|
.aspectRatio(contentMode: .fit)
|
||||||
.cacheOriginalImage()
|
.padding(.top, safeAreaInsets?.top)
|
||||||
.imageModifier(ImageHandler(handler: $image))
|
.padding(.bottom, safeAreaInsets?.bottom)
|
||||||
.loadDiskFileSynchronously()
|
}
|
||||||
.scaleFactor(UIScreen.main.scale)
|
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
|
||||||
.fade(duration: 0.1)
|
presentationMode.wrappedValue.dismiss()
|
||||||
.aspectRatio(contentMode: .fit)
|
}))
|
||||||
.tabItem {
|
.ignoresSafeArea()
|
||||||
Text(url.absoluteString)
|
.tag(index)
|
||||||
}
|
|
||||||
.id(url.absoluteString)
|
|
||||||
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
|
|
||||||
.sheet(isPresented: $showShareSheet) {
|
|
||||||
ShareSheet(activityItems: [url])
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.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)
|
.foregroundColor(Color.clear)
|
||||||
.overlay {
|
.overlay {
|
||||||
KFAnimatedImage(url)
|
KFAnimatedImage(url)
|
||||||
.configure { view in
|
.callbackQueue(.dispatch(.global(qos: .background)))
|
||||||
view.framePreloadCount = 3
|
.processingQueue(.dispatch(.global(qos: .background)))
|
||||||
}
|
|
||||||
.cacheOriginalImage()
|
.cacheOriginalImage()
|
||||||
.loadDiskFileSynchronously()
|
.loadDiskFileSynchronously()
|
||||||
.scaleFactor(UIScreen.main.scale)
|
.scaleFactor(UIScreen.main.scale)
|
||||||
.fade(duration: 0.1)
|
.fade(duration: 0.1)
|
||||||
|
.configure { view in
|
||||||
|
view.framePreloadCount = 3
|
||||||
|
}
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Text(url.absoluteString)
|
Text(url.absoluteString)
|
||||||
@@ -151,8 +252,8 @@ struct ImageCarousel: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.sheet(isPresented: $open_sheet) {
|
.fullScreenCover(isPresented: $open_sheet) {
|
||||||
ImageViewer(urls: urls)
|
ImageView(urls: urls)
|
||||||
}
|
}
|
||||||
.frame(height: 200)
|
.frame(height: 200)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
@@ -164,6 +265,6 @@ struct ImageCarousel: View {
|
|||||||
|
|
||||||
struct ImageCarousel_Previews: PreviewProvider {
|
struct ImageCarousel_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
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")!])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,17 +31,16 @@ func open_with_wallet(wallet: Wallet.Model, invoice: String) {
|
|||||||
struct InvoiceView: View {
|
struct InvoiceView: View {
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
|
let our_pubkey: String
|
||||||
let invoice: Invoice
|
let invoice: Invoice
|
||||||
@State var showing_select_wallet: Bool = false
|
@State var showing_select_wallet: Bool = false
|
||||||
@ObservedObject var user_settings = UserSettingsStore()
|
|
||||||
|
|
||||||
var PayButton: some View {
|
var PayButton: some View {
|
||||||
Button {
|
Button {
|
||||||
if user_settings.show_wallet_selector {
|
if should_show_wallet_selector(our_pubkey) {
|
||||||
showing_select_wallet = true
|
showing_select_wallet = true
|
||||||
} else {
|
} else {
|
||||||
open_with_wallet(wallet: user_settings.default_wallet.model, invoice: invoice.string)
|
open_with_wallet(wallet: get_default_wallet(our_pubkey).model, invoice: invoice.string)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
RoundedRectangle(cornerRadius: 20)
|
RoundedRectangle(cornerRadius: 20)
|
||||||
@@ -70,7 +69,7 @@ struct InvoiceView: View {
|
|||||||
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
|
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Text(invoice.description)
|
Text(invoice.description_string)
|
||||||
Text(invoice.amount.amount_sats_str())
|
Text(invoice.amount.amount_sats_str())
|
||||||
.font(.title)
|
.font(.title)
|
||||||
PayButton
|
PayButton
|
||||||
@@ -80,16 +79,16 @@ struct InvoiceView: View {
|
|||||||
.padding(30)
|
.padding(30)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: invoice.string).environmentObject(user_settings)
|
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let test_invoice = Invoice(description: "this is a description", amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
|
let test_invoice = Invoice(description: .description("this is a description"), amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
|
||||||
|
|
||||||
struct InvoiceView_Previews: PreviewProvider {
|
struct InvoiceView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
InvoiceView(invoice: test_invoice)
|
InvoiceView(our_pubkey: "", invoice: test_invoice)
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct InvoicesView: View {
|
struct InvoicesView: View {
|
||||||
|
let our_pubkey: String
|
||||||
var invoices: [Invoice]
|
var invoices: [Invoice]
|
||||||
|
|
||||||
@State var open_sheet: Bool = false
|
@State var open_sheet: Bool = false
|
||||||
@@ -16,7 +17,7 @@ struct InvoicesView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
TabView {
|
||||||
ForEach(invoices, id: \.string) { invoice in
|
ForEach(invoices, id: \.string) { invoice in
|
||||||
InvoiceView(invoice: invoice)
|
InvoiceView(our_pubkey: our_pubkey, invoice: invoice)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Text(invoice.string)
|
Text(invoice.string)
|
||||||
}
|
}
|
||||||
@@ -30,7 +31,7 @@ struct InvoicesView: View {
|
|||||||
|
|
||||||
struct InvoicesView_Previews: PreviewProvider {
|
struct InvoicesView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
InvoicesView(invoices: [Invoice.init(description: "description", amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
|
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
|
||||||
.frame(width: 300)
|
.frame(width: 300)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
//
|
||||||
|
// TranslateButton.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-02-02.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import NaturalLanguage
|
||||||
|
|
||||||
|
struct TranslateView: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let event: NostrEvent
|
||||||
|
let size: EventViewKind
|
||||||
|
|
||||||
|
@State var checkingTranslationStatus: Bool = false
|
||||||
|
@State var currentLanguage: String = "en"
|
||||||
|
@State var noteLanguage: String? = nil
|
||||||
|
@State var translated_note: String? = nil
|
||||||
|
@State var show_translated_note: Bool = false
|
||||||
|
@State var translated_artifacts: NoteArtifacts? = nil
|
||||||
|
|
||||||
|
var TranslateButton: some View {
|
||||||
|
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
||||||
|
show_translated_note = true
|
||||||
|
}
|
||||||
|
.translate_button_style()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
|
||||||
|
return Group {
|
||||||
|
Button(NSLocalizedString("Translated from \(lang)", comment: "Button to indicate that the note has been translated from a different language.")) {
|
||||||
|
show_translated_note = false
|
||||||
|
}
|
||||||
|
.translate_button_style()
|
||||||
|
|
||||||
|
Text(artifacts.content)
|
||||||
|
.font(eventviewsize_to_font(size))
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckingStatus(lang: String) -> some View {
|
||||||
|
return Button(NSLocalizedString("Translating from \(lang)...", comment: "Button to indicate that the note is in the process of being translated from a different language.")) {
|
||||||
|
show_translated_note = false
|
||||||
|
}
|
||||||
|
.translate_button_style()
|
||||||
|
}
|
||||||
|
|
||||||
|
func MainContent(note_lang: String) -> some View {
|
||||||
|
return Group {
|
||||||
|
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
|
||||||
|
if let lang = languageName, show_translated_note {
|
||||||
|
if checkingTranslationStatus {
|
||||||
|
CheckingStatus(lang: lang)
|
||||||
|
} else if let artifacts = translated_artifacts {
|
||||||
|
Translated(lang: lang, artifacts: artifacts)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TranslateButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if let note_lang = noteLanguage, noteLanguage != currentLanguage {
|
||||||
|
MainContent(note_lang: note_lang)
|
||||||
|
} else {
|
||||||
|
Text("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkingTranslationStatus = true
|
||||||
|
|
||||||
|
if #available(iOS 16, *) {
|
||||||
|
currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
|
||||||
|
} else {
|
||||||
|
currentLanguage = Locale.current.languageCode ?? "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in.
|
||||||
|
let content = event.get_content(damus_state.keypair.privkey)
|
||||||
|
noteLanguage = NLLanguageRecognizer.dominantLanguage(for: content)?.rawValue ?? currentLanguage
|
||||||
|
|
||||||
|
if let lang = noteLanguage, noteLanguage != currentLanguage {
|
||||||
|
// If the detected dominant language is a variant, remove the variant component and just take the language part as translation services typically only supports the variant-less language.
|
||||||
|
if #available(iOS 16, *) {
|
||||||
|
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
|
||||||
|
} else {
|
||||||
|
noteLanguage = Locale.canonicalLanguageIdentifier(from: lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let note_lang = noteLanguage else {
|
||||||
|
noteLanguage = currentLanguage
|
||||||
|
translated_note = nil
|
||||||
|
checkingTranslationStatus = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if note_lang != currentLanguage {
|
||||||
|
do {
|
||||||
|
// If the note language is different from our language, send a translation request.
|
||||||
|
let translator = Translator(damus_state.settings)
|
||||||
|
translated_note = try await translator.translate(content, from: note_lang, to: currentLanguage)
|
||||||
|
} catch {
|
||||||
|
// If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack.
|
||||||
|
noteLanguage = currentLanguage
|
||||||
|
translated_note = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let translated = translated_note {
|
||||||
|
// Render translated note.
|
||||||
|
let blocks = event.get_blocks(content: translated)
|
||||||
|
translated_artifacts = render_blocks(blocks: blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkingTranslationStatus = false
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TranslateView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let ds = test_damus_state()
|
||||||
|
TranslateView(damus_state: ds, event: test_event, size: .selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")!)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
//
|
||||||
|
// ZapButton.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ZapButton: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let event: NostrEvent
|
||||||
|
let lnurl: String
|
||||||
|
|
||||||
|
@ObservedObject var bar: ActionBarModel
|
||||||
|
|
||||||
|
@State var zapping: Bool = false
|
||||||
|
@State var invoice: String = ""
|
||||||
|
@State var slider_value: Double = 0.0
|
||||||
|
@State var slider_visible: Bool = false
|
||||||
|
@State var showing_select_wallet: Bool = false
|
||||||
|
|
||||||
|
func send_zap() {
|
||||||
|
guard let privkey = damus_state.keypair.privkey else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only take the first 10 because reasons
|
||||||
|
let relays = Array(damus_state.pool.descriptors.prefix(10))
|
||||||
|
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||||
|
// TODO: gather comment?
|
||||||
|
let content = ""
|
||||||
|
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target)
|
||||||
|
|
||||||
|
zapping = true
|
||||||
|
|
||||||
|
Task {
|
||||||
|
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
||||||
|
if mpayreq == nil {
|
||||||
|
mpayreq = await fetch_static_payreq(lnurl)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let payreq = mpayreq else {
|
||||||
|
// TODO: show error
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
zapping = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, amount: 1000000) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
zapping = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
zapping = false
|
||||||
|
|
||||||
|
if should_show_wallet_selector(damus_state.pubkey) {
|
||||||
|
self.invoice = inv
|
||||||
|
self.showing_select_wallet = true
|
||||||
|
} else {
|
||||||
|
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//damus_state.pool.send(.event(zapreq))
|
||||||
|
}
|
||||||
|
|
||||||
|
var zap_img: String {
|
||||||
|
if bar.zapped {
|
||||||
|
return "bolt.fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !zapping {
|
||||||
|
return "bolt"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "bolt.horizontal.fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
var zap_color: Color? {
|
||||||
|
if bar.zapped {
|
||||||
|
return Color.orange
|
||||||
|
}
|
||||||
|
|
||||||
|
if !zapping {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Color.yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
EventActionButton(img: zap_img, col: zap_color) {
|
||||||
|
if bar.zapped {
|
||||||
|
//notify(.delete, bar.our_tip)
|
||||||
|
} else if !zapping {
|
||||||
|
send_zap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||||
|
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
struct ZapButton_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let bar = ActionBarModel.empty()
|
||||||
|
ZapButton(damus_state: test_damus_state(), event: NostrEvent(content: "hi", pubkey: "pk"), bar: bar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+176
-48
@@ -11,11 +11,11 @@ import Kingfisher
|
|||||||
|
|
||||||
var BOOTSTRAP_RELAYS = [
|
var BOOTSTRAP_RELAYS = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://nostr-relay.wlvs.space",
|
"wss://eden.nostr.land",
|
||||||
"wss://nostr.fmt.wiz.biz",
|
"wss://relay.snort.social",
|
||||||
"wss://relay.nostr.bg",
|
"wss://nostr.orangepill.dev",
|
||||||
"wss://nostr.oxtr.dev",
|
"wss://nos.lol",
|
||||||
"wss://nostr.v0l.io",
|
"wss://relay.current.fyi",
|
||||||
"wss://brb.io",
|
"wss://brb.io",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -26,10 +26,12 @@ struct TimestampedProfile {
|
|||||||
|
|
||||||
enum Sheets: Identifiable {
|
enum Sheets: Identifiable {
|
||||||
case post
|
case post
|
||||||
|
case report(ReportTarget)
|
||||||
case reply(NostrEvent)
|
case reply(NostrEvent)
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .report: return "report"
|
||||||
case .post: return "post"
|
case .post: return "post"
|
||||||
case .reply(let ev): return "reply-" + ev.id
|
case .reply(let ev): return "reply-" + ev.id
|
||||||
}
|
}
|
||||||
@@ -71,6 +73,7 @@ struct ContentView: View {
|
|||||||
@State var damus_state: DamusState? = nil
|
@State var damus_state: DamusState? = nil
|
||||||
@State var selected_timeline: Timeline? = .home
|
@State var selected_timeline: Timeline? = .home
|
||||||
@State var is_thread_open: Bool = false
|
@State var is_thread_open: Bool = false
|
||||||
|
@State var is_deleted_account: Bool = false
|
||||||
@State var is_profile_open: Bool = false
|
@State var is_profile_open: Bool = false
|
||||||
@State var event: NostrEvent? = nil
|
@State var event: NostrEvent? = nil
|
||||||
@State var active_profile: String? = nil
|
@State var active_profile: String? = nil
|
||||||
@@ -79,10 +82,13 @@ struct ContentView: View {
|
|||||||
@State var profile_open: Bool = false
|
@State var profile_open: Bool = false
|
||||||
@State var thread_open: Bool = false
|
@State var thread_open: Bool = false
|
||||||
@State var search_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 var filter_state : FilterState = .posts_and_replies
|
||||||
@State private var isSideBarOpened = false
|
@State private var isSideBarOpened = false
|
||||||
@StateObject var home: HomeModel = HomeModel()
|
@StateObject var home: HomeModel = HomeModel()
|
||||||
@StateObject var user_settings = UserSettingsStore()
|
|
||||||
|
|
||||||
// connect retry timer
|
// connect retry timer
|
||||||
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
|
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
|
||||||
@@ -93,27 +99,35 @@ struct ContentView: View {
|
|||||||
|
|
||||||
var PostingTimelineView: some View {
|
var PostingTimelineView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
TabView(selection: $filter_state) {
|
ZStack {
|
||||||
contentTimelineView(filter: FilterState.posts.filter)
|
TabView(selection: $filter_state) {
|
||||||
.tag(FilterState.posts)
|
contentTimelineView(filter: FilterState.posts.filter)
|
||||||
.id(FilterState.posts)
|
.tag(FilterState.posts)
|
||||||
contentTimelineView(filter: FilterState.posts_and_replies.filter)
|
.id(FilterState.posts)
|
||||||
.tag(FilterState.posts_and_replies)
|
contentTimelineView(filter: FilterState.posts_and_replies.filter)
|
||||||
.id(FilterState.posts_and_replies)
|
.tag(FilterState.posts_and_replies)
|
||||||
|
.id(FilterState.posts_and_replies)
|
||||||
|
}
|
||||||
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
|
|
||||||
|
if privkey != nil {
|
||||||
|
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
|
||||||
|
self.active_sheet = .post
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
|
||||||
}
|
}
|
||||||
.safeAreaInset(edge: .top) {
|
.safeAreaInset(edge: .top, spacing: 0) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
FiltersView
|
CustomPicker(selection: $filter_state, content: {
|
||||||
//.frame(maxWidth: 275)
|
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
|
||||||
.padding()
|
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
|
||||||
|
})
|
||||||
Divider()
|
Divider()
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
}
|
}
|
||||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(.keyboard)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
||||||
@@ -121,21 +135,6 @@ struct ContentView: View {
|
|||||||
if let damus = self.damus_state {
|
if let damus = self.damus_state {
|
||||||
TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
|
TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
|
||||||
}
|
}
|
||||||
if privkey != nil {
|
|
||||||
PostButtonContainer(userSettings: user_settings) {
|
|
||||||
self.active_sheet = .post
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var FiltersView: some View {
|
|
||||||
VStack{
|
|
||||||
Picker(NSLocalizedString("Filter State", comment: "Filter state for seeing either only posts, or posts & replies."), selection: $filter_state) {
|
|
||||||
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
|
|
||||||
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,17 +171,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)
|
.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 {
|
.toolbar {
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
if selected_timeline == .home {
|
switch selected_timeline {
|
||||||
|
case .home:
|
||||||
Image("damus-home")
|
Image("damus-home")
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width:30,height:30)
|
.frame(width:30,height:30)
|
||||||
.shadow(color: Color("DamusPurple"), radius: 2)
|
.shadow(color: Color("DamusPurple"), radius: 2)
|
||||||
} else {
|
case .dms:
|
||||||
Text("Global")
|
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 {
|
var MaybeSearchView: some View {
|
||||||
@@ -217,6 +226,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 {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
if let damus = self.damus_state {
|
if let damus = self.damus_state {
|
||||||
@@ -229,20 +252,18 @@ struct ContentView: View {
|
|||||||
Button {
|
Button {
|
||||||
isSideBarOpened.toggle()
|
isSideBarOpened.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
if let picture = damus_state?.profiles.lookup(id: pubkey)?.picture {
|
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
|
||||||
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, picture: picture)
|
|
||||||
} else {
|
|
||||||
Image(systemName: "person.fill")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
if home.signal.signal != home.signal.max_signal {
|
if home.signal.signal != home.signal.max_signal {
|
||||||
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
|
||||||
.font(.callout)
|
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
||||||
.foregroundColor(.gray)
|
.font(.callout)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -271,8 +292,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.sheet(item: $active_sheet) { item in
|
.sheet(item: $active_sheet) { item in
|
||||||
switch item {
|
switch item {
|
||||||
|
case .report(let target):
|
||||||
|
MaybeReportView(target: target)
|
||||||
case .post:
|
case .post:
|
||||||
PostView(replying_to: nil, references: [])
|
PostView(replying_to: nil, references: [], damus_state: damus_state!)
|
||||||
case .reply(let event):
|
case .reply(let event):
|
||||||
ReplyView(replying_to: event, damus: damus_state!)
|
ReplyView(replying_to: event, damus: damus_state!)
|
||||||
}
|
}
|
||||||
@@ -318,6 +341,18 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.onReceive(handle_notify(.like)) { like in
|
.onReceive(handle_notify(.like)) { like in
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.deleted_account)) { notif in
|
||||||
|
self.is_deleted_account = true
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.report)) { notif in
|
||||||
|
let target = notif.object as! ReportTarget
|
||||||
|
self.active_sheet = .report(target)
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.block)) { notif in
|
||||||
|
let pubkey = notif.object as! String
|
||||||
|
self.blocking = pubkey
|
||||||
|
self.confirm_block = true
|
||||||
|
}
|
||||||
.onReceive(handle_notify(.broadcast_event)) { obj in
|
.onReceive(handle_notify(.broadcast_event)) { obj in
|
||||||
let ev = obj.object as! NostrEvent
|
let ev = obj.object as! NostrEvent
|
||||||
self.damus_state?.pool.send(.event(ev))
|
self.damus_state?.pool.send(.event(ev))
|
||||||
@@ -392,6 +427,96 @@ struct ContentView: View {
|
|||||||
.onReceive(timer) { n in
|
.onReceive(timer) { n in
|
||||||
self.damus_state?.pool.connect_to_disconnected()
|
self.damus_state?.pool.connect_to_disconnected()
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||||
|
home.filter_muted()
|
||||||
|
}
|
||||||
|
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
|
||||||
|
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
|
||||||
|
is_deleted_account = false
|
||||||
|
notify(.logout, ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate the user has been blocked"), isPresented: $user_blocked_confirm, actions: {
|
||||||
|
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) {
|
||||||
|
user_blocked_confirm = false
|
||||||
|
}
|
||||||
|
}, message: {
|
||||||
|
if let pubkey = self.blocking {
|
||||||
|
let profile = damus_state!.profiles.lookup(id: pubkey)
|
||||||
|
let name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||||
|
Text("\(name) has been blocked", comment: "Alert message that informs a user was blocked.")
|
||||||
|
} else {
|
||||||
|
Text("User has been blocked", comment: "Alert message that informs a user was blocked.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
|
||||||
|
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of alert that creates a new mutelist.")) {
|
||||||
|
confirm_overwrite_mutelist = false
|
||||||
|
confirm_block = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
|
||||||
|
guard let ds = damus_state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let keypair = ds.keypair.to_full() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let pubkey = blocking else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
damus_state?.contacts.set_mutelist(mutelist)
|
||||||
|
ds.pool.send(.event(mutelist))
|
||||||
|
|
||||||
|
confirm_overwrite_mutelist = false
|
||||||
|
confirm_block = false
|
||||||
|
user_blocked_confirm = true
|
||||||
|
}
|
||||||
|
}, message: {
|
||||||
|
Text("No block list found, create a new one? This will overwrite any previous block lists.", comment: "Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists.")
|
||||||
|
})
|
||||||
|
.alert(NSLocalizedString("Block User", comment: "Title of alert for blocking a user."), isPresented: $confirm_block, actions: {
|
||||||
|
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for blocking a user."), role: .cancel) {
|
||||||
|
confirm_block = false
|
||||||
|
}
|
||||||
|
Button(NSLocalizedString("Block", comment: "Alert button to block a user."), role: .destructive) {
|
||||||
|
guard let ds = damus_state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds.contacts.mutelist == nil {
|
||||||
|
confirm_overwrite_mutelist = true
|
||||||
|
} else {
|
||||||
|
guard let keypair = ds.keypair.to_full() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let pubkey = blocking else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
damus_state?.contacts.set_mutelist(ev)
|
||||||
|
ds.pool.send(.event(ev))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, message: {
|
||||||
|
if let pubkey = blocking {
|
||||||
|
let profile = damus_state?.profiles.lookup(id: pubkey)
|
||||||
|
let name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||||
|
Text("Block \(name)?", comment: "Alert message prompt to ask if a user should be blocked.")
|
||||||
|
} else {
|
||||||
|
Text("Could not find user to block...", comment: "Alert message to indicate that the blocked user could not be found.")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func switch_timeline(_ timeline: Timeline) {
|
func switch_timeline(_ timeline: Timeline) {
|
||||||
@@ -434,7 +559,10 @@ struct ContentView: View {
|
|||||||
tips: TipCounter(our_pubkey: pubkey),
|
tips: TipCounter(our_pubkey: pubkey),
|
||||||
profiles: Profiles(),
|
profiles: Profiles(),
|
||||||
dms: home.dms,
|
dms: home.dms,
|
||||||
previews: PreviewCache()
|
previews: PreviewCache(),
|
||||||
|
zaps: Zaps(our_pubkey: pubkey),
|
||||||
|
lnurls: LNUrls(),
|
||||||
|
settings: UserSettingsStore()
|
||||||
)
|
)
|
||||||
home.damus_state = self.damus_state!
|
home.damus_state = self.damus_state!
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
||||||
<string>"Granting Damus access to your photo library allows you to save photos.</string>
|
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>river</string>
|
<string>river</string>
|
||||||
|
|||||||
@@ -11,30 +11,32 @@ import Foundation
|
|||||||
class ActionBarModel: ObservableObject {
|
class ActionBarModel: ObservableObject {
|
||||||
@Published var our_like: NostrEvent?
|
@Published var our_like: NostrEvent?
|
||||||
@Published var our_boost: NostrEvent?
|
@Published var our_boost: NostrEvent?
|
||||||
@Published var our_tip: NostrEvent?
|
@Published var our_zap: Zap?
|
||||||
@Published var likes: Int
|
@Published var likes: Int
|
||||||
@Published var boosts: Int
|
@Published var boosts: Int
|
||||||
@Published var tips: Int64
|
@Published var zaps: Int
|
||||||
|
@Published var zap_total: Int64
|
||||||
|
|
||||||
static func empty() -> ActionBarModel {
|
static func empty() -> ActionBarModel {
|
||||||
return ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
|
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(likes: Int, boosts: Int, tips: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_tip: NostrEvent?) {
|
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?) {
|
||||||
self.likes = likes
|
self.likes = likes
|
||||||
self.boosts = boosts
|
self.boosts = boosts
|
||||||
self.tips = tips
|
self.zaps = zaps
|
||||||
|
self.zap_total = zap_total
|
||||||
self.our_like = our_like
|
self.our_like = our_like
|
||||||
self.our_boost = our_boost
|
self.our_boost = our_boost
|
||||||
self.our_tip = our_tip
|
self.our_zap = our_zap
|
||||||
}
|
}
|
||||||
|
|
||||||
var is_empty: Bool {
|
var is_empty: Bool {
|
||||||
return likes == 0 && boosts == 0 && tips == 0
|
return likes == 0 && boosts == 0 && zaps == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var tipped: Bool {
|
var zapped: Bool {
|
||||||
return our_tip != nil
|
return our_zap != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var liked: Bool {
|
var liked: Bool {
|
||||||
|
|||||||
@@ -11,13 +11,51 @@ import Foundation
|
|||||||
class Contacts {
|
class Contacts {
|
||||||
private var friends: Set<String> = Set()
|
private var friends: Set<String> = Set()
|
||||||
private var friend_of_friends: Set<String> = Set()
|
private var friend_of_friends: Set<String> = Set()
|
||||||
|
private var muted: Set<String> = Set()
|
||||||
|
|
||||||
let our_pubkey: String
|
let our_pubkey: String
|
||||||
var event: NostrEvent?
|
var event: NostrEvent?
|
||||||
|
var mutelist: NostrEvent?
|
||||||
|
|
||||||
init(our_pubkey: String) {
|
init(our_pubkey: String) {
|
||||||
self.our_pubkey = our_pubkey
|
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] {
|
func get_friendosphere() -> [String] {
|
||||||
var fs = get_friend_list()
|
var fs = get_friend_list()
|
||||||
fs.append(contentsOf: get_friend_of_friend_list())
|
fs.append(contentsOf: get_friend_of_friend_list())
|
||||||
|
|||||||
@@ -18,12 +18,20 @@ struct DamusState {
|
|||||||
let profiles: Profiles
|
let profiles: Profiles
|
||||||
let dms: DirectMessagesModel
|
let dms: DirectMessagesModel
|
||||||
let previews: PreviewCache
|
let previews: PreviewCache
|
||||||
|
let zaps: Zaps
|
||||||
|
let lnurls: LNUrls
|
||||||
|
let settings: UserSettingsStore
|
||||||
|
|
||||||
var pubkey: String {
|
var pubkey: String {
|
||||||
return keypair.pubkey
|
return keypair.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var is_privkey_user: Bool {
|
||||||
|
keypair.privkey != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static var empty: DamusState {
|
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(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// DeepLPlan.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 2/3/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum DeepLPlan: String, CaseIterable, Identifiable {
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
struct Model: Identifiable, Hashable {
|
||||||
|
var id: String { self.tag }
|
||||||
|
var tag: String
|
||||||
|
var displayName: String
|
||||||
|
var url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
case free
|
||||||
|
case pro
|
||||||
|
|
||||||
|
var model: Model {
|
||||||
|
switch self {
|
||||||
|
case .free:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Free", comment: "Dropdown option for selecting Free plan for DeepL translation service."), url: "https://api-free.deepl.com")
|
||||||
|
case .pro:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Pro", comment: "Dropdown option for selecting Pro plan for DeepL translation service."), url: "https://api.deepl.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allModels: [Model] {
|
||||||
|
return Self.allCases.map { $0.model }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,34 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class DirectMessageModel: ObservableObject {
|
class DirectMessageModel: ObservableObject {
|
||||||
@Published var events: [NostrEvent]
|
@Published var events: [NostrEvent] {
|
||||||
|
didSet {
|
||||||
init(events: [NostrEvent]) {
|
is_request = determine_is_request()
|
||||||
self.events = events
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.events = []
|
||||||
|
self.is_request = false
|
||||||
|
self.our_pubkey = our_pubkey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,26 @@ import Foundation
|
|||||||
class DirectMessagesModel: ObservableObject {
|
class DirectMessagesModel: ObservableObject {
|
||||||
@Published var dms: [(String, DirectMessageModel)] = []
|
@Published var dms: [(String, DirectMessageModel)] = []
|
||||||
@Published var loading: Bool = false
|
@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 {
|
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
|
||||||
if let dm = lookup(pubkey) {
|
if let dm = lookup(pubkey) {
|
||||||
return dm
|
return dm
|
||||||
}
|
}
|
||||||
|
|
||||||
let new = DirectMessageModel()
|
let new = DirectMessageModel(our_pubkey: our_pubkey)
|
||||||
dms.append((pubkey, new))
|
dms.append((pubkey, new))
|
||||||
return new
|
return new
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ class FollowersModel: ObservableObject {
|
|||||||
let sub_id: String = UUID().description
|
let sub_id: String = UUID().description
|
||||||
let profiles_id: String = UUID().description
|
let profiles_id: String = UUID().description
|
||||||
|
|
||||||
var count_display: String {
|
var count: Int? {
|
||||||
guard let contacts = self.contacts else {
|
guard let contacts = self.contacts else {
|
||||||
return "?"
|
return nil
|
||||||
}
|
}
|
||||||
return "\(contacts.count)";
|
return contacts.count
|
||||||
}
|
}
|
||||||
|
|
||||||
init(damus_state: DamusState, target: String) {
|
init(damus_state: DamusState, target: String) {
|
||||||
@@ -86,7 +86,7 @@ class FollowersModel: ObservableObject {
|
|||||||
if ev.known_kind == .contacts {
|
if ev.known_kind == .contacts {
|
||||||
handle_contact_event(ev)
|
handle_contact_event(ev)
|
||||||
} else if ev.known_kind == .metadata {
|
} else if ev.known_kind == .metadata {
|
||||||
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .notice(let msg):
|
case .notice(let msg):
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class FollowingModel {
|
|||||||
switch nev {
|
switch nev {
|
||||||
case .event(_, let ev):
|
case .event(_, let ev):
|
||||||
if ev.kind == 0 {
|
if ev.kind == 0 {
|
||||||
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||||
}
|
}
|
||||||
case .notice(let msg):
|
case .notice(let msg):
|
||||||
print("followingmodel notice: \(msg)")
|
print("followingmodel notice: \(msg)")
|
||||||
|
|||||||
+195
-65
@@ -48,17 +48,19 @@ class HomeModel: ObservableObject {
|
|||||||
|
|
||||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||||
@Published var notifications: [NostrEvent] = []
|
@Published var notifications: [NostrEvent] = []
|
||||||
@Published var dms: DirectMessagesModel = DirectMessagesModel()
|
@Published var dms: DirectMessagesModel
|
||||||
@Published var events: [NostrEvent] = []
|
@Published var events: [NostrEvent] = []
|
||||||
@Published var loading: Bool = false
|
@Published var loading: Bool = false
|
||||||
@Published var signal: SignalModel = SignalModel()
|
@Published var signal: SignalModel = SignalModel()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.damus_state = DamusState.empty
|
self.damus_state = DamusState.empty
|
||||||
|
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(damus_state: DamusState) {
|
init(damus_state: DamusState) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
|
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
var pool: RelayPool {
|
var pool: RelayPool {
|
||||||
@@ -96,6 +98,8 @@ class HomeModel: ObservableObject {
|
|||||||
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
||||||
case .metadata:
|
case .metadata:
|
||||||
handle_metadata_event(ev)
|
handle_metadata_event(ev)
|
||||||
|
case .list:
|
||||||
|
handle_list_event(ev)
|
||||||
case .boost:
|
case .boost:
|
||||||
handle_boost_event(sub_id: sub_id, ev)
|
handle_boost_event(sub_id: sub_id, ev)
|
||||||
case .like:
|
case .like:
|
||||||
@@ -108,9 +112,61 @@ class HomeModel: ObservableObject {
|
|||||||
handle_channel_create(ev)
|
handle_channel_create(ev)
|
||||||
case .channel_meta:
|
case .channel_meta:
|
||||||
handle_channel_meta(ev)
|
handle_channel_meta(ev)
|
||||||
|
case .zap:
|
||||||
|
handle_zap_event(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handle_zap_event_with_zapper(_ ev: NostrEvent, zapper: String) {
|
||||||
|
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
damus_state.zaps.add_zap(zap: zap)
|
||||||
|
|
||||||
|
if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_last_event(ev: ev, timeline: .notifications)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_zap_event(_ ev: NostrEvent) {
|
||||||
|
// These are zap notifications
|
||||||
|
guard let ptag = event_tag(ev, name: "p") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard ptag == damus_state.pubkey else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: damus_state.pubkey) {
|
||||||
|
handle_zap_event_with_zapper(ev, zapper: local_zapper)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let lnurl = profile.lnurl else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.handle_zap_event_with_zapper(ev, zapper: zapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func handle_channel_create(_ ev: NostrEvent) {
|
func handle_channel_create(_ ev: NostrEvent) {
|
||||||
guard ev.is_valid else {
|
guard ev.is_valid else {
|
||||||
return
|
return
|
||||||
@@ -122,6 +178,12 @@ class HomeModel: ObservableObject {
|
|||||||
func handle_channel_meta(_ ev: NostrEvent) {
|
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) {
|
func handle_delete_event(_ ev: NostrEvent) {
|
||||||
guard ev.is_valid else {
|
guard ev.is_valid else {
|
||||||
return
|
return
|
||||||
@@ -272,7 +334,11 @@ class HomeModel: ObservableObject {
|
|||||||
|
|
||||||
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
|
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
|
||||||
our_contacts_filter.authors = [damus_state.pubkey]
|
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([
|
var dms_filter = NostrFilter.filter_kinds([
|
||||||
NostrKind.dm.rawValue,
|
NostrKind.dm.rawValue,
|
||||||
])
|
])
|
||||||
@@ -303,13 +369,14 @@ class HomeModel: ObservableObject {
|
|||||||
NostrKind.chat.rawValue,
|
NostrKind.chat.rawValue,
|
||||||
NostrKind.like.rawValue,
|
NostrKind.like.rawValue,
|
||||||
NostrKind.boost.rawValue,
|
NostrKind.boost.rawValue,
|
||||||
|
NostrKind.zap.rawValue,
|
||||||
])
|
])
|
||||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||||
notifications_filter.limit = 100
|
notifications_filter.limit = 100
|
||||||
|
|
||||||
var home_filters = [home_filter]
|
var home_filters = [home_filter]
|
||||||
var notifications_filters = [notifications_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]
|
var dms_filters = [dms_filter, our_dms_filter]
|
||||||
|
|
||||||
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
||||||
@@ -333,9 +400,32 @@ class HomeModel: ObservableObject {
|
|||||||
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
|
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) {
|
func handle_metadata_event(_ ev: NostrEvent) {
|
||||||
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
||||||
@@ -347,24 +437,23 @@ class HomeModel: ObservableObject {
|
|||||||
return m[kind]
|
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) {
|
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: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
|
if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_last_event(ev: ev, timeline: .notifications)
|
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 {
|
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 })
|
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
|
||||||
@@ -374,12 +463,8 @@ class HomeModel: ObservableObject {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func should_hide_event(_ ev: NostrEvent) -> Bool {
|
|
||||||
return !ev.should_show_event
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,49 +476,8 @@ class HomeModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle_dm(_ ev: NostrEvent) {
|
func handle_dm(_ ev: NostrEvent) {
|
||||||
|
if let notifs = handle_incoming_dm(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
|
||||||
var inserted = false
|
self.new_events = notifs
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,10 +583,17 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
|
|||||||
print("-----")
|
print("-----")
|
||||||
}
|
}
|
||||||
|
|
||||||
func process_metadata_event(profiles: Profiles, ev: NostrEvent) {
|
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
|
||||||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
notify(.deleted_account, ())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var old_nip05: String? = nil
|
var old_nip05: String? = nil
|
||||||
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||||
@@ -656,4 +707,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// LibreTranslateServer.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 1/21/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum LibreTranslateServer: String, CaseIterable, Identifiable {
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
struct Model: Identifiable, Hashable {
|
||||||
|
var id: String { self.tag }
|
||||||
|
var tag: String
|
||||||
|
var displayName: String
|
||||||
|
var url: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
case argosopentech
|
||||||
|
case terraprint
|
||||||
|
case vern
|
||||||
|
case custom
|
||||||
|
|
||||||
|
var model: Model {
|
||||||
|
switch self {
|
||||||
|
case .argosopentech:
|
||||||
|
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
|
||||||
|
case .terraprint:
|
||||||
|
return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co")
|
||||||
|
case .vern:
|
||||||
|
return .init(tag: self.rawValue, displayName: "lt.vern.cc", url: "https://lt.vern.cc")
|
||||||
|
case .custom:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allModels: [Model] {
|
||||||
|
return Self.allCases.map { $0.model }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
enum CountResult {
|
||||||
|
case already_counted
|
||||||
|
case success(Int)
|
||||||
|
}
|
||||||
|
|
||||||
class EventCounter {
|
class EventCounter {
|
||||||
var counts: [String: Int] = [:]
|
var counts: [String: Int] = [:]
|
||||||
@@ -14,11 +18,6 @@ class EventCounter {
|
|||||||
var our_events: [String: NostrEvent] = [:]
|
var our_events: [String: NostrEvent] = [:]
|
||||||
var our_pubkey: String
|
var our_pubkey: String
|
||||||
|
|
||||||
enum CountResult {
|
|
||||||
case already_counted
|
|
||||||
case success(Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
init (our_pubkey: String) {
|
init (our_pubkey: String) {
|
||||||
self.our_pubkey = our_pubkey
|
self.our_pubkey = our_pubkey
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-20
@@ -32,13 +32,30 @@ struct IdBlock: Identifiable {
|
|||||||
let block: Block
|
let block: Block
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Invoice {
|
typealias Invoice = LightningInvoice<Amount>
|
||||||
let description: String
|
typealias ZapInvoice = LightningInvoice<Int64>
|
||||||
let amount: Amount
|
|
||||||
|
enum InvoiceDescription {
|
||||||
|
case description(String)
|
||||||
|
case description_hash(Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LightningInvoice<T> {
|
||||||
|
let description: InvoiceDescription
|
||||||
|
let amount: T
|
||||||
let string: String
|
let string: String
|
||||||
let expiry: UInt64
|
let expiry: UInt64
|
||||||
let payment_hash: Data
|
let payment_hash: Data
|
||||||
let created_at: UInt64
|
let created_at: UInt64
|
||||||
|
|
||||||
|
var description_string: String {
|
||||||
|
switch description {
|
||||||
|
case .description(let string):
|
||||||
|
return string
|
||||||
|
case .description_hash:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Block {
|
enum Block {
|
||||||
@@ -189,24 +206,50 @@ enum Amount: Equatable {
|
|||||||
case .any:
|
case .any:
|
||||||
return NSLocalizedString("Any", comment: "Any amount of sats")
|
return NSLocalizedString("Any", comment: "Any amount of sats")
|
||||||
case .specific(let amt):
|
case .specific(let amt):
|
||||||
let numberFormatter = NumberFormatter()
|
return format_msats(amt)
|
||||||
numberFormatter.numberStyle = .decimal
|
|
||||||
numberFormatter.minimumFractionDigits = 0
|
|
||||||
numberFormatter.maximumFractionDigits = 3
|
|
||||||
numberFormatter.roundingMode = .down
|
|
||||||
|
|
||||||
let sats = NSNumber(value: (Double(amt) / 1000.0))
|
|
||||||
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
|
|
||||||
|
|
||||||
if formattedSats == numberFormatter.string(from: 1) {
|
|
||||||
return NSLocalizedString("\(formattedSats) sat", comment: "Amount of 1 sat.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return NSLocalizedString("\(formattedSats) sats", comment: "Amount of sats.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func format_msats_abbrev(_ msats: Int64) -> String {
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .decimal
|
||||||
|
formatter.positiveSuffix = "m"
|
||||||
|
formatter.positivePrefix = ""
|
||||||
|
formatter.minimumFractionDigits = 0
|
||||||
|
formatter.maximumFractionDigits = 3
|
||||||
|
formatter.roundingMode = .down
|
||||||
|
formatter.roundingIncrement = 0.1
|
||||||
|
formatter.multiplier = 1
|
||||||
|
|
||||||
|
let sats = NSNumber(value: (Double(msats) / 1000.0))
|
||||||
|
|
||||||
|
if msats >= 1_000_000*1000 {
|
||||||
|
formatter.positiveSuffix = "m"
|
||||||
|
formatter.multiplier = 0.000001
|
||||||
|
} else if msats >= 1000*1000 {
|
||||||
|
formatter.positiveSuffix = "k"
|
||||||
|
formatter.multiplier = 0.001
|
||||||
|
} else {
|
||||||
|
return sats.stringValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter.string(from: sats) ?? sats.stringValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func format_msats(_ msat: Int64) -> String {
|
||||||
|
let numberFormatter = NumberFormatter()
|
||||||
|
numberFormatter.numberStyle = .decimal
|
||||||
|
numberFormatter.minimumFractionDigits = 0
|
||||||
|
numberFormatter.maximumFractionDigits = 3
|
||||||
|
numberFormatter.roundingMode = .down
|
||||||
|
|
||||||
|
let sats = NSNumber(value: (Double(msat) / 1000.0))
|
||||||
|
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
|
||||||
|
|
||||||
|
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
|
||||||
|
}
|
||||||
|
|
||||||
func convert_invoice_block(_ b: invoice_block) -> Block? {
|
func convert_invoice_block(_ b: invoice_block) -> Block? {
|
||||||
guard let invstr = strblock_to_string(b.invstr) else {
|
guard let invstr = strblock_to_string(b.invstr) else {
|
||||||
return nil
|
return nil
|
||||||
@@ -216,9 +259,8 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var description = ""
|
guard let description = convert_invoice_description(b11: b11) else {
|
||||||
if b11.description != nil {
|
return nil
|
||||||
description = String(cString: b11.description)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
|
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
|
||||||
@@ -229,6 +271,18 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
|
|||||||
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
|
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||||
|
if let desc = b11.description {
|
||||||
|
return .description(String(cString: desc))
|
||||||
|
}
|
||||||
|
|
||||||
|
if var deschash = maybe_pointee(b11.description_hash) {
|
||||||
|
return .description_hash(Data(bytes: &deschash, count: 32))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
|
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
|
||||||
{
|
{
|
||||||
let ind = Int(ind)
|
let ind = Int(ind)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -97,7 +97,7 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
} else if ev.known_kind == .contacts {
|
} else if ev.known_kind == .contacts {
|
||||||
handle_profile_contact_event(ev)
|
handle_profile_contact_event(ev)
|
||||||
} else if ev.known_kind == .metadata {
|
} else if ev.known_kind == .metadata {
|
||||||
process_metadata_event(profiles: damus.profiles, ev: ev)
|
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||||
}
|
}
|
||||||
seen_event.insert(ev.id)
|
seen_event.insert(ev.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,10 @@ class SearchHomeModel: ObservableObject {
|
|||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filter_muted() {
|
||||||
|
events = events.filter { !should_hide_event(contacts: damus_state.contacts, ev: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
func subscribe() {
|
func subscribe() {
|
||||||
loading = true
|
loading = true
|
||||||
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event)
|
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 {
|
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
|
||||||
return
|
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) {
|
if seen_pubkey.contains(ev.pubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -125,7 +129,7 @@ func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ev.known_kind == .metadata {
|
if ev.known_kind == .metadata {
|
||||||
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class ThreadModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ev.known_kind == .metadata {
|
if ev.known_kind == .metadata {
|
||||||
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||||
} else if ev.is_textlike {
|
} else if ev.is_textlike {
|
||||||
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
|
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
|
||||||
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {
|
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// TranslationService.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 2/3/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum TranslationService: String, CaseIterable, Identifiable {
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
struct Model: Identifiable, Hashable {
|
||||||
|
var id: String { self.tag }
|
||||||
|
var tag: String
|
||||||
|
var displayName: String
|
||||||
|
}
|
||||||
|
|
||||||
|
case none
|
||||||
|
case libretranslate
|
||||||
|
case deepl
|
||||||
|
|
||||||
|
var model: Model {
|
||||||
|
switch self {
|
||||||
|
case .none:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation service."))
|
||||||
|
case .libretranslate:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service."))
|
||||||
|
case .deepl:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("DeepL (Proprietary, Higher Accuracy)", comment: "Dropdown option for selecting DeepL as the translation service."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allModels: [Model] {
|
||||||
|
return Self.allCases.map { $0.model }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,53 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Vault
|
||||||
|
|
||||||
|
func should_show_wallet_selector(_ pubkey: String) -> Bool {
|
||||||
|
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_default_wallet(_ pubkey: String) -> Wallet {
|
||||||
|
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
|
||||||
|
let default_wallet = Wallet(rawValue: defaultWalletName)
|
||||||
|
{
|
||||||
|
return default_wallet
|
||||||
|
} else {
|
||||||
|
return .system_default_wallet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func get_translation_service(_ pubkey: String) -> TranslationService? {
|
||||||
|
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return TranslationService(rawValue: translation_service)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func get_deepl_plan(_ pubkey: String) -> DeepLPlan? {
|
||||||
|
guard let server_name = UserDefaults.standard.string(forKey: "deepl_plan") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeepLPlan(rawValue: server_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
|
||||||
|
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return LibreTranslateServer(rawValue: server_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
|
||||||
|
if let url = server.model.url {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserDefaults.standard.object(forKey: "libretranslate_url") as? String
|
||||||
|
}
|
||||||
|
|
||||||
class UserSettingsStore: ObservableObject {
|
class UserSettingsStore: ObservableObject {
|
||||||
@Published var default_wallet: Wallet {
|
@Published var default_wallet: Wallet {
|
||||||
@@ -26,16 +73,154 @@ class UserSettingsStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
@Published var translation_service: TranslationService {
|
||||||
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
|
didSet {
|
||||||
let default_wallet = Wallet(rawValue: defaultWalletName)
|
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
|
||||||
{
|
|
||||||
self.default_wallet = default_wallet
|
|
||||||
} else {
|
|
||||||
default_wallet = .system_default_wallet
|
|
||||||
}
|
}
|
||||||
show_wallet_selector = UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
|
}
|
||||||
|
|
||||||
|
@Published var deepl_plan: DeepLPlan {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var deepl_api_key: String {
|
||||||
|
didSet {
|
||||||
|
do {
|
||||||
|
if deepl_api_key == "" {
|
||||||
|
try clearDeepLApiKey()
|
||||||
|
} else {
|
||||||
|
try saveDeepLApiKey(deepl_api_key)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No-op.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var libretranslate_server: LibreTranslateServer {
|
||||||
|
didSet {
|
||||||
|
if oldValue == libretranslate_server {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDefaults.standard.set(libretranslate_server.rawValue, forKey: "libretranslate_server")
|
||||||
|
|
||||||
|
libretranslate_api_key = ""
|
||||||
|
|
||||||
|
if libretranslate_server == .custom {
|
||||||
|
libretranslate_url = ""
|
||||||
|
} else {
|
||||||
|
libretranslate_url = libretranslate_server.model.url!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var libretranslate_url: String {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(libretranslate_url, forKey: "libretranslate_url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var libretranslate_api_key: String {
|
||||||
|
didSet {
|
||||||
|
do {
|
||||||
|
if libretranslate_api_key == "" {
|
||||||
|
try clearLibreTranslateApiKey()
|
||||||
|
} else {
|
||||||
|
try saveLibreTranslateApiKey(libretranslate_api_key)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No-op.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// TODO: pubkey-scoped settings
|
||||||
|
let pubkey = ""
|
||||||
|
self.default_wallet = get_default_wallet(pubkey)
|
||||||
|
show_wallet_selector = should_show_wallet_selector(pubkey)
|
||||||
|
|
||||||
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
||||||
|
|
||||||
|
// Note from @tyiu:
|
||||||
|
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
|
||||||
|
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
|
||||||
|
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
|
||||||
|
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
|
||||||
|
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
|
||||||
|
if let translation_service = get_translation_service(pubkey) {
|
||||||
|
self.translation_service = translation_service
|
||||||
|
} else {
|
||||||
|
self.translation_service = .none
|
||||||
|
}
|
||||||
|
|
||||||
|
if let libretranslate_server = get_libretranslate_server(pubkey) {
|
||||||
|
self.libretranslate_server = libretranslate_server
|
||||||
|
self.libretranslate_url = get_libretranslate_url(pubkey, server: libretranslate_server) ?? ""
|
||||||
|
} else {
|
||||||
|
// Choose a random server to distribute load.
|
||||||
|
libretranslate_server = .allCases.filter { $0 != .custom }.randomElement()!
|
||||||
|
libretranslate_url = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||||
|
} catch {
|
||||||
|
libretranslate_api_key = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if let deepl_plan = get_deepl_plan(pubkey) {
|
||||||
|
self.deepl_plan = deepl_plan
|
||||||
|
} else {
|
||||||
|
self.deepl_plan = .free
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||||
|
} catch {
|
||||||
|
deepl_api_key = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
|
||||||
|
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearLibreTranslateApiKey() throws {
|
||||||
|
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveDeepLApiKey(_ apiKey: String) throws {
|
||||||
|
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearDeepLApiKey() throws {
|
||||||
|
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||||
|
}
|
||||||
|
|
||||||
|
func can_translate(_ pubkey: String) -> Bool {
|
||||||
|
switch translation_service {
|
||||||
|
case .none:
|
||||||
|
return false
|
||||||
|
case .libretranslate:
|
||||||
|
return URLComponents(string: libretranslate_url) != nil
|
||||||
|
case .deepl:
|
||||||
|
return deepl_api_key != ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
|
||||||
|
var serviceName = "damus"
|
||||||
|
var accessGroup: String? = nil
|
||||||
|
var accountName = "libretranslate_apikey"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
|
||||||
|
var serviceName = "damus"
|
||||||
|
var accessGroup: String? = nil
|
||||||
|
var accountName = "deepl_apikey"
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ enum Wallet: String, CaseIterable, Identifiable {
|
|||||||
return .init(index: 3, tag: "bluewallet", displayName: NSLocalizedString("Blue Wallet", comment: "Dropdown option label for Lightning wallet, Blue Wallet."), link: "bluewallet:lightning:",
|
return .init(index: 3, tag: "bluewallet", displayName: NSLocalizedString("Blue Wallet", comment: "Dropdown option label for Lightning wallet, Blue Wallet."), link: "bluewallet:lightning:",
|
||||||
appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
|
appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
|
||||||
case .walletofsatoshi:
|
case .walletofsatoshi:
|
||||||
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet Of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet Of Satoshi."), link: "walletofsatoshi:lightning:",
|
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet of Satoshi."), link: "walletofsatoshi:lightning:",
|
||||||
appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
|
appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
|
||||||
case .zebedee:
|
case .zebedee:
|
||||||
return .init(index: 5, tag: "zebedee", displayName: NSLocalizedString("Zebedee", comment: "Dropdown option label for Lightning wallet, Zebedee."), link: "zebedee:lightning:",
|
return .init(index: 5, tag: "zebedee", displayName: NSLocalizedString("Zebedee", comment: "Dropdown option label for Lightning wallet, Zebedee."), link: "zebedee:lightning:",
|
||||||
@@ -75,7 +75,7 @@ enum Wallet: String, CaseIterable, Identifiable {
|
|||||||
appStoreLink: "https://apps.apple.com/sv/app/bitcoin-beach-wallet/id1531383905", image: "bbw")
|
appStoreLink: "https://apps.apple.com/sv/app/bitcoin-beach-wallet/id1531383905", image: "bbw")
|
||||||
case .blixtwallet:
|
case .blixtwallet:
|
||||||
return .init(index: 11, tag: "blixtwallet", displayName: NSLocalizedString("Blixt Wallet", comment: "Dropdown option label for Lightning wallet, Blixt Wallet"), link: "blixtwallet:lightning:",
|
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:
|
case .river:
|
||||||
return .init(index: 12, tag: "river", displayName: NSLocalizedString("River", comment: "Dropdown option label for Lightning wallet, River"), link: "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")
|
appStoreLink: "https://apps.apple.com/us/app/river-buy-mine-bitcoin/id1536176542", image: "river")
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+66
-20
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Profile: Codable {
|
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?) {
|
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
|
||||||
self.value = [:]
|
self.value = [:]
|
||||||
@@ -23,44 +23,82 @@ struct Profile: Codable {
|
|||||||
self.nip05 = nip05
|
self.nip05 = nip05
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func str(_ str: String) -> String? {
|
||||||
|
return get_val(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func get_val<T>(_ v: String) -> T? {
|
||||||
|
guard let val = self.value[v] else{
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let s = val.value as? T else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
private mutating func set_val<T>(_ key: String, _ val: T?) {
|
||||||
|
if val == nil {
|
||||||
|
self.value.removeValue(forKey: key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.value[key] = AnyCodable.init(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
private mutating func set_str(_ key: String, _ val: String?) {
|
||||||
|
set_val(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted: Bool? {
|
||||||
|
get { return get_val("deleted"); }
|
||||||
|
set(s) { set_val("deleted", s) }
|
||||||
|
}
|
||||||
|
|
||||||
var display_name: String? {
|
var display_name: String? {
|
||||||
get { return value["display_name"]; }
|
get { return str("display_name"); }
|
||||||
set(s) { value["display_name"] = s }
|
set(s) { set_str("display_name", s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var name: String? {
|
var name: String? {
|
||||||
get { return value["name"]; }
|
get { return str("name"); }
|
||||||
set(s) { value["name"] = s }
|
set(s) { set_str("name", s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var about: String? {
|
var about: String? {
|
||||||
get { return value["about"]; }
|
get { return str("about"); }
|
||||||
set(s) { value["about"] = s }
|
set(s) { set_str("about", s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var picture: String? {
|
var picture: String? {
|
||||||
get { return value["picture"]; }
|
get { return str("picture"); }
|
||||||
set(s) { value["picture"] = s }
|
set(s) { set_str("picture", s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var banner: String? {
|
var banner: String? {
|
||||||
get { return value["banner"]; }
|
get { return str("banner"); }
|
||||||
set(s) { value["banner"] = s }
|
set(s) { set_str("banner", s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var website: String? {
|
var website: String? {
|
||||||
get { return value["website"]; }
|
get { return str("website"); }
|
||||||
set(s) { value["website"] = s }
|
set(s) { set_str("website", s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var lud06: String? {
|
var lud06: String? {
|
||||||
get { return value["lud06"]; }
|
get { return str("lud06"); }
|
||||||
set(s) { value["lud06"] = s }
|
set(s) { set_str("lud06", s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var lud16: String? {
|
var lud16: String? {
|
||||||
get { return value["lud16"]; }
|
get { return str("lud16"); }
|
||||||
set(s) { value["lud16"] = s }
|
set(s) { set_str("lud16", s) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var website_url: URL? {
|
||||||
|
return self.website.flatMap { URL(string: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var lnurl: String? {
|
var lnurl: String? {
|
||||||
@@ -72,21 +110,29 @@ struct Profile: Codable {
|
|||||||
return lnaddress_to_lnurl(addr);
|
return lnaddress_to_lnurl(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !addr.lowercased().hasPrefix("lnurl") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nip05: String? {
|
var nip05: String? {
|
||||||
get { return value["nip05"]; }
|
get { return str("nip05"); }
|
||||||
set(s) { value["nip05"] = s }
|
set(s) { set_str("nip05", s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var lightning_uri: URL? {
|
var lightning_uri: URL? {
|
||||||
return make_ln_url(self.lnurl)
|
return make_ln_url(self.lnurl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.value = [:]
|
||||||
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.singleValueContainer()
|
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 {
|
func encode(to encoder: Encoder) throws {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import secp256k1
|
|||||||
import secp256k1_implementation
|
import secp256k1_implementation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
enum ValidationResult: Decodable {
|
enum ValidationResult: Decodable {
|
||||||
case ok
|
case ok
|
||||||
case bad_id
|
case bad_id
|
||||||
@@ -27,7 +29,7 @@ struct KeyEvent {
|
|||||||
let relay_url: String
|
let relay_url: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ReferencedId: Identifiable, Hashable {
|
struct ReferencedId: Identifiable, Hashable, Equatable {
|
||||||
let ref_id: String
|
let ref_id: String
|
||||||
let relay_id: String?
|
let relay_id: String?
|
||||||
let key: String
|
let key: String
|
||||||
@@ -79,7 +81,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
|||||||
}
|
}
|
||||||
|
|
||||||
var too_big: Bool {
|
var too_big: Bool {
|
||||||
return self.content.count > 32000
|
return self.content.count > 16000
|
||||||
}
|
}
|
||||||
|
|
||||||
var should_show_event: Bool {
|
var should_show_event: Bool {
|
||||||
@@ -103,11 +105,15 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
|||||||
if let bs = _blocks {
|
if let bs = _blocks {
|
||||||
return bs
|
return bs
|
||||||
}
|
}
|
||||||
let blocks = parse_mentions(content: self.get_content(privkey), tags: self.tags)
|
let blocks = get_blocks(content: self.get_content(privkey))
|
||||||
self._blocks = blocks
|
self._blocks = blocks
|
||||||
return blocks
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func get_blocks(content: String) -> [Block] {
|
||||||
|
return parse_mentions(content: content, tags: self.tags)
|
||||||
|
}
|
||||||
|
|
||||||
lazy var inner_event: NostrEvent? = {
|
lazy var inner_event: NostrEvent? = {
|
||||||
// don't try to deserialize an inner event if we know there won't be one
|
// don't try to deserialize an inner event if we know there won't be one
|
||||||
if self.known_kind == .boost {
|
if self.known_kind == .boost {
|
||||||
@@ -367,6 +373,10 @@ func encode_json<T: Encodable>(_ val: T) -> String? {
|
|||||||
return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
|
return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decode_nostr_event_json(json: String) -> NostrEvent? {
|
||||||
|
return decode_json(json)
|
||||||
|
}
|
||||||
|
|
||||||
func decode_json<T: Decodable>(_ val: String) -> T? {
|
func decode_json<T: Decodable>(_ val: String) -> T? {
|
||||||
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
|
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
|
||||||
}
|
}
|
||||||
@@ -567,6 +577,26 @@ func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> Nost
|
|||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
|
||||||
|
switch target {
|
||||||
|
case .profile(let pk):
|
||||||
|
return [["p", pk]]
|
||||||
|
case .note(let note_target):
|
||||||
|
return [["e", note_target.note_id], ["p", note_target.author]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent {
|
||||||
|
var tags = zap_target_to_tags(target)
|
||||||
|
var relay_tag = ["relays"]
|
||||||
|
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
||||||
|
tags.append(relay_tag)
|
||||||
|
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags)
|
||||||
|
ev.id = calculate_event_id(ev: ev)
|
||||||
|
ev.sig = sign_event(privkey: privkey, ev: ev)
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
||||||
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
|
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
|
||||||
|
|
||||||
@@ -789,3 +819,46 @@ func inner_event_or_self(ev: NostrEvent) -> NostrEvent {
|
|||||||
|
|
||||||
return inner_ev
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct NostrFilter: Codable {
|
struct NostrFilter: Codable, Equatable {
|
||||||
var ids: [String]?
|
var ids: [String]?
|
||||||
var kinds: [Int]?
|
var kinds: [Int]?
|
||||||
var referenced_ids: [String]?
|
var referenced_ids: [String]?
|
||||||
@@ -17,6 +17,7 @@ struct NostrFilter: Codable {
|
|||||||
var limit: UInt32?
|
var limit: UInt32?
|
||||||
var authors: [String]?
|
var authors: [String]?
|
||||||
var hashtag: [String]? = nil
|
var hashtag: [String]? = nil
|
||||||
|
var parameter: [String]? = nil
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case ids
|
case ids
|
||||||
@@ -24,6 +25,7 @@ struct NostrFilter: Codable {
|
|||||||
case referenced_ids = "#e"
|
case referenced_ids = "#e"
|
||||||
case pubkeys = "#p"
|
case pubkeys = "#p"
|
||||||
case hashtag = "#t"
|
case hashtag = "#t"
|
||||||
|
case parameter = "#d"
|
||||||
case since
|
case since
|
||||||
case until
|
case until
|
||||||
case authors
|
case authors
|
||||||
|
|||||||
@@ -19,4 +19,6 @@ enum NostrKind: Int {
|
|||||||
case channel_create = 40
|
case channel_create = 40
|
||||||
case channel_meta = 41
|
case channel_meta = 41
|
||||||
case chat = 42
|
case chat = 42
|
||||||
|
case list = 30000
|
||||||
|
case zap = 9735
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
enum NostrLink {
|
enum NostrLink: Equatable {
|
||||||
case ref(ReferencedId)
|
case ref(ReferencedId)
|
||||||
case filter(NostrFilter)
|
case filter(NostrFilter)
|
||||||
}
|
}
|
||||||
@@ -101,6 +101,24 @@ func decode_universal_link(_ s: String) -> NostrLink? {
|
|||||||
return nil
|
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? {
|
func decode_nostr_uri(_ s: String) -> NostrLink? {
|
||||||
if s.starts(with: "https://damus.io/") {
|
if s.starts(with: "https://damus.io/") {
|
||||||
return decode_universal_link(s)
|
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 .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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,20 @@ import UIKit
|
|||||||
class Profiles {
|
class Profiles {
|
||||||
var profiles: [String: TimestampedProfile] = [:]
|
var profiles: [String: TimestampedProfile] = [:]
|
||||||
var validated: [String: NIP05] = [:]
|
var validated: [String: NIP05] = [:]
|
||||||
|
var zappers: [String: String] = [:]
|
||||||
|
|
||||||
func is_validated(_ pk: String) -> NIP05? {
|
func is_validated(_ pk: String) -> NIP05? {
|
||||||
return validated[pk]
|
return validated[pk]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lookup_zapper(pubkey: String) -> String? {
|
||||||
|
if let zapper = zappers[pubkey] {
|
||||||
|
return zapper
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func add(id: String, profile: TimestampedProfile) {
|
func add(id: String, profile: TimestampedProfile) {
|
||||||
profiles[id] = profile
|
profiles[id] = profile
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,16 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct RelayInfo: Codable {
|
public struct RelayInfo: Codable {
|
||||||
let read: Bool
|
let read: Bool
|
||||||
let write: Bool
|
let write: Bool
|
||||||
|
|
||||||
static let rw = RelayInfo(read: true, write: true)
|
static let rw = RelayInfo(read: true, write: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RelayDescriptor: Codable {
|
public struct RelayDescriptor: Codable {
|
||||||
let url: URL
|
public let url: URL
|
||||||
let info: RelayInfo
|
public let info: RelayInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RelayFlags: Int {
|
enum RelayFlags: Int {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// AccountDeletion.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent {
|
||||||
|
var profile = Profile()
|
||||||
|
profile.deleted = true
|
||||||
|
profile.about = "account deleted"
|
||||||
|
profile.name = "nobody"
|
||||||
|
|
||||||
|
let content = encode_json(profile)!
|
||||||
|
let ev = NostrEvent(content: content, pubkey: keypair.pubkey, kind: 0)
|
||||||
|
ev.id = calculate_event_id(ev: ev)
|
||||||
|
ev.sig = sign_event(privkey: keypair.privkey, ev: ev)
|
||||||
|
return ev
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,26 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||||
|
var i: Int = 0
|
||||||
|
|
||||||
|
for zap in zaps {
|
||||||
|
// don't insert duplicate events
|
||||||
|
if new_zap.event.id == zap.event.id {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_zap.invoice.amount > zap.invoice.amount {
|
||||||
|
zaps.insert(new_zap, at: i)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
zaps.append(new_zap)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool {
|
func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool {
|
||||||
var i: Int = 0
|
var i: Int = 0
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,25 @@ import Vault
|
|||||||
let PUBKEY_HRP = "npub"
|
let PUBKEY_HRP = "npub"
|
||||||
let PRIVKEY_HRP = "nsec"
|
let PRIVKEY_HRP = "nsec"
|
||||||
|
|
||||||
|
struct FullKeypair {
|
||||||
|
let pubkey: String
|
||||||
|
let privkey: String
|
||||||
|
}
|
||||||
|
|
||||||
struct Keypair {
|
struct Keypair {
|
||||||
let pubkey: String
|
let pubkey: String
|
||||||
let privkey: String?
|
let privkey: String?
|
||||||
let pubkey_bech32: String
|
let pubkey_bech32: String
|
||||||
let privkey_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?) {
|
init(pubkey: String, privkey: String?) {
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.privkey = privkey
|
self.privkey = privkey
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// LNUrl.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct LNUrlPayRequest: Decodable {
|
||||||
|
let allowsNostr: Bool?
|
||||||
|
let nostrPubkey: String?
|
||||||
|
|
||||||
|
let minSendable: Int64?
|
||||||
|
let maxSendable: Int64?
|
||||||
|
let status: String?
|
||||||
|
let callback: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct LNUrlPayResponse: Decodable {
|
||||||
|
let pr: String
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// LNUrls.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class LNUrls {
|
||||||
|
var endpoints: [String: LNUrlPayRequest]
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.endpoints = [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookup(_ id: String) -> LNUrlPayRequest? {
|
||||||
|
return self.endpoints[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -11,150 +11,93 @@ extension Notification.Name {
|
|||||||
static var thread_focus: Notification.Name {
|
static var thread_focus: Notification.Name {
|
||||||
return Notification.Name("thread focus")
|
return Notification.Name("thread focus")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var relays_changed: Notification.Name {
|
static var relays_changed: Notification.Name {
|
||||||
return Notification.Name("relays_changed")
|
return Notification.Name("relays_changed")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var select_event: Notification.Name {
|
static var select_event: Notification.Name {
|
||||||
return Notification.Name("select_event")
|
return Notification.Name("select_event")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var select_quote: Notification.Name {
|
static var select_quote: Notification.Name {
|
||||||
return Notification.Name("select quote")
|
return Notification.Name("select quote")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var reply: Notification.Name {
|
static var reply: Notification.Name {
|
||||||
return Notification.Name("reply")
|
return Notification.Name("reply")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var profile_updated: Notification.Name {
|
static var profile_updated: Notification.Name {
|
||||||
return Notification.Name("profile_updated")
|
return Notification.Name("profile_updated")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var switched_timeline: Notification.Name {
|
static var switched_timeline: Notification.Name {
|
||||||
return Notification.Name("switched_timeline")
|
return Notification.Name("switched_timeline")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var liked: Notification.Name {
|
static var liked: Notification.Name {
|
||||||
return Notification.Name("liked")
|
return Notification.Name("liked")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var open_profile: Notification.Name {
|
static var open_profile: Notification.Name {
|
||||||
return Notification.Name("open_profile")
|
return Notification.Name("open_profile")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var scroll_to_top: Notification.Name {
|
static var scroll_to_top: Notification.Name {
|
||||||
return Notification.Name("scroll_to_to")
|
return Notification.Name("scroll_to_to")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var broadcast_event: Notification.Name {
|
static var broadcast_event: Notification.Name {
|
||||||
return Notification.Name("broadcast event")
|
return Notification.Name("broadcast event")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var open_thread: Notification.Name {
|
static var open_thread: Notification.Name {
|
||||||
return Notification.Name("open thread")
|
return Notification.Name("open thread")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var notice: Notification.Name {
|
static var notice: Notification.Name {
|
||||||
return Notification.Name("notice")
|
return Notification.Name("notice")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var like: Notification.Name {
|
static var like: Notification.Name {
|
||||||
return Notification.Name("like note")
|
return Notification.Name("like note")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var delete: Notification.Name {
|
static var delete: Notification.Name {
|
||||||
return Notification.Name("delete note")
|
return Notification.Name("delete note")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var post: Notification.Name {
|
static var post: Notification.Name {
|
||||||
return Notification.Name("send post")
|
return Notification.Name("send post")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var boost: Notification.Name {
|
static var boost: Notification.Name {
|
||||||
return Notification.Name("boost")
|
return Notification.Name("boost")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var boosted: Notification.Name {
|
static var boosted: Notification.Name {
|
||||||
return Notification.Name("boosted")
|
return Notification.Name("boosted")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var follow: Notification.Name {
|
static var follow: Notification.Name {
|
||||||
return Notification.Name("follow")
|
return Notification.Name("follow")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var unfollow: Notification.Name {
|
static var unfollow: Notification.Name {
|
||||||
return Notification.Name("unfollow")
|
return Notification.Name("unfollow")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var login: Notification.Name {
|
static var login: Notification.Name {
|
||||||
return Notification.Name("login")
|
return Notification.Name("login")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var logout: Notification.Name {
|
static var logout: Notification.Name {
|
||||||
return Notification.Name("logout")
|
return Notification.Name("logout")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var followed: Notification.Name {
|
static var followed: Notification.Name {
|
||||||
return Notification.Name("followed")
|
return Notification.Name("followed")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var chatroom_meta: Notification.Name {
|
static var chatroom_meta: Notification.Name {
|
||||||
return Notification.Name("chatroom_meta")
|
return Notification.Name("chatroom_meta")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static var unfollowed: Notification.Name {
|
static var unfollowed: Notification.Name {
|
||||||
return Notification.Name("unfollowed")
|
return Notification.Name("unfollowed")
|
||||||
}
|
}
|
||||||
|
static var report: Notification.Name {
|
||||||
|
return Notification.Name("report")
|
||||||
|
}
|
||||||
|
static var block: Notification.Name {
|
||||||
|
return Notification.Name("block")
|
||||||
|
}
|
||||||
|
static var new_mutes: Notification.Name {
|
||||||
|
return Notification.Name("new_mutes")
|
||||||
|
}
|
||||||
|
static var new_unmutes: Notification.Name {
|
||||||
|
return Notification.Name("new_unmutes")
|
||||||
|
}
|
||||||
|
static var deleted_account: Notification.Name {
|
||||||
|
return Notification.Name("deleted_account")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
//
|
||||||
|
// Translator.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 2/4/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public struct Translator {
|
||||||
|
private let userSettingsStore: UserSettingsStore
|
||||||
|
private let session = URLSession.shared
|
||||||
|
private let encoder = JSONEncoder()
|
||||||
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
init(_ userSettingsStore: UserSettingsStore) {
|
||||||
|
self.userSettingsStore = userSettingsStore
|
||||||
|
}
|
||||||
|
|
||||||
|
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||||
|
switch userSettingsStore.translation_service {
|
||||||
|
case .libretranslate:
|
||||||
|
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
|
||||||
|
case .deepl:
|
||||||
|
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
|
||||||
|
case .none:
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||||
|
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
struct RequestBody: Encodable {
|
||||||
|
let q: String
|
||||||
|
let source: String
|
||||||
|
let target: String
|
||||||
|
let api_key: String?
|
||||||
|
}
|
||||||
|
let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: userSettingsStore.libretranslate_api_key)
|
||||||
|
request.httpBody = try encoder.encode(body)
|
||||||
|
|
||||||
|
struct Response: Decodable {
|
||||||
|
let translatedText: String
|
||||||
|
}
|
||||||
|
let response: Response = try await decodedData(for: request)
|
||||||
|
return response.translatedText
|
||||||
|
}
|
||||||
|
|
||||||
|
private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||||
|
if userSettingsStore.deepl_api_key == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = try makeURL(userSettingsStore.deepl_plan.model.url, path: "/v2/translate")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("DeepL-Auth-Key \(userSettingsStore.deepl_api_key)", forHTTPHeaderField: "Authorization")
|
||||||
|
|
||||||
|
struct RequestBody: Encodable {
|
||||||
|
let text: [String]
|
||||||
|
let source_lang: String
|
||||||
|
let target_lang: String
|
||||||
|
}
|
||||||
|
let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased())
|
||||||
|
request.httpBody = try encoder.encode(body)
|
||||||
|
|
||||||
|
struct Response: Decodable {
|
||||||
|
let translations: [DeepLTranslations]
|
||||||
|
}
|
||||||
|
struct DeepLTranslations: Decodable {
|
||||||
|
let detected_source_language: String
|
||||||
|
let text: String
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: Response = try await decodedData(for: request)
|
||||||
|
return response.translations.map { $0.text }.joined(separator: " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
|
||||||
|
guard var components = URLComponents(string: baseUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
components.path = path
|
||||||
|
guard let url = components.url else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output {
|
||||||
|
let data = try await session.data(for: request)
|
||||||
|
let result = try decoder.decode(Output.self, from: data)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension URLSession {
|
||||||
|
func data(for request: URLRequest) async throws -> Data {
|
||||||
|
var task: URLSessionDataTask?
|
||||||
|
let onCancel = { task?.cancel() }
|
||||||
|
return try await withTaskCancellationHandler(
|
||||||
|
operation: {
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
task = dataTask(with: request) { data, _, error in
|
||||||
|
guard let data = data else {
|
||||||
|
let error = error ?? URLError(.badServerResponse)
|
||||||
|
return continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
continuation.resume(returning: data)
|
||||||
|
}
|
||||||
|
task?.resume()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: { onCancel() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
//
|
||||||
|
// Zap.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ZapSource {
|
||||||
|
case author(String)
|
||||||
|
// TODO: anonymous
|
||||||
|
//case anonymous
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct NoteZapTarget: Equatable {
|
||||||
|
public let note_id: String
|
||||||
|
public let author: String
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ZapTarget: Equatable {
|
||||||
|
case profile(String)
|
||||||
|
case note(NoteZapTarget)
|
||||||
|
|
||||||
|
public static func note(id: String, author: String) -> ZapTarget {
|
||||||
|
return .note(NoteZapTarget(note_id: id, author: author))
|
||||||
|
}
|
||||||
|
|
||||||
|
var pubkey: String {
|
||||||
|
switch self {
|
||||||
|
case .profile(let pk):
|
||||||
|
return pk
|
||||||
|
case .note(let note_target):
|
||||||
|
return note_target.author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .note(let note_target):
|
||||||
|
return note_target.note_id
|
||||||
|
case .profile(let pk):
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ZapRequest {
|
||||||
|
let ev: NostrEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Zap {
|
||||||
|
public let event: NostrEvent
|
||||||
|
public let invoice: ZapInvoice
|
||||||
|
public let zapper: String /// zap authorizer
|
||||||
|
public let target: ZapTarget
|
||||||
|
public let request: ZapRequest
|
||||||
|
|
||||||
|
public static func from_zap_event(zap_ev: NostrEvent, zapper: String) -> Zap? {
|
||||||
|
/// Make sure that we only create a zap event if it is authorized by the profile or event
|
||||||
|
guard zapper == zap_ev.pubkey else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let bolt11_str = event_tag(zap_ev, name: "bolt11") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let bolt11 = decode_bolt11(bolt11_str) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
/// Any amount invoices are not allowed
|
||||||
|
guard let zap_invoice = invoice_to_zap_invoice(bolt11) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Some endpoints don't have this, let's skip the check for now. We're mostly trusting the zapper anyways
|
||||||
|
/*
|
||||||
|
guard let preimage = event_tag(zap_ev, name: "preimage") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard preimage_matches_invoice(preimage, inv: zap_invoice) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let zap_req = decode_nostr_event_json(desc) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let target = determine_zap_target(zap_req) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the description from either the invoice, or tags, depending on the type of invoice
|
||||||
|
func get_zap_description(_ ev: NostrEvent, inv_desc: InvoiceDescription) -> String? {
|
||||||
|
switch inv_desc {
|
||||||
|
case .description(let string):
|
||||||
|
return string
|
||||||
|
case .description_hash(let deschash):
|
||||||
|
guard let desc = event_tag(ev, name: "description") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let data = desc.data(using: .utf8) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard sha256(data) == deschash else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func invoice_to_zap_invoice(_ invoice: Invoice) -> ZapInvoice? {
|
||||||
|
guard case .specific(let amt) = invoice.amount else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ZapInvoice(description: invoice.description, amount: amt, string: invoice.string, expiry: invoice.expiry, payment_hash: invoice.payment_hash, created_at: invoice.created_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preimage_matches_invoice<T>(_ preimage: String, inv: LightningInvoice<T>) -> Bool {
|
||||||
|
guard let raw_preimage = hex_decode(preimage) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let hashed = sha256(Data(raw_preimage))
|
||||||
|
|
||||||
|
return inv.payment_hash == hashed
|
||||||
|
}
|
||||||
|
|
||||||
|
func determine_zap_target(_ ev: NostrEvent) -> ZapTarget? {
|
||||||
|
guard let ptag = event_tag(ev, name: "p") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let etag = event_tag(ev, name: "e") {
|
||||||
|
return ZapTarget.note(id: etag, author: ptag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .profile(ptag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode_bolt11(_ s: String) -> Invoice? {
|
||||||
|
var bs = blocks()
|
||||||
|
bs.num_blocks = 0
|
||||||
|
blocks_init(&bs)
|
||||||
|
|
||||||
|
let bytes = s.utf8CString
|
||||||
|
let _ = bytes.withUnsafeBufferPointer { p in
|
||||||
|
damus_parse_content(&bs, p.baseAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard bs.num_blocks == 1 else {
|
||||||
|
blocks_free(&bs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = bs.blocks[0]
|
||||||
|
|
||||||
|
guard let converted = convert_block(block, tags: []) else {
|
||||||
|
blocks_free(&bs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard case .invoice(let invoice) = converted else {
|
||||||
|
blocks_free(&bs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks_free(&bs)
|
||||||
|
return invoice
|
||||||
|
}
|
||||||
|
|
||||||
|
func event_tag(_ ev: NostrEvent, name: String) -> String? {
|
||||||
|
for tag in ev.tags {
|
||||||
|
if tag.count >= 2 && tag[0] == name {
|
||||||
|
return tag[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode_nostr_event_json(_ desc: String) -> NostrEvent? {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
guard let dat = desc.data(using: .utf8) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let ev = try? decoder.decode(NostrEvent.self, from: dat) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode_zap_request(_ desc: String) -> ZapRequest? {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
guard let jsonData = desc.data(using: .utf8) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let jsonArray = try? JSONSerialization.jsonObject(with: jsonData) as? [[Any]] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for array in jsonArray {
|
||||||
|
guard array.count == 2 else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let mkey = array.first.flatMap { $0 as? String }
|
||||||
|
if let key = mkey, key == "application/nostr" {
|
||||||
|
guard let dat = try? JSONSerialization.data(withJSONObject: array[1], options: []) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let zap_req = try? decoder.decode(NostrEvent.self, from: dat) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard zap_req.kind == 9734 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure the signature on the zap request is correct
|
||||||
|
guard case .ok = validate_event(ev: zap_req) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ZapRequest(ev: zap_req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func fetch_zapper_from_lnurl(_ lnurl: String) async -> String? {
|
||||||
|
guard let endpoint = await fetch_static_payreq(lnurl) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let allows = endpoint.allowsNostr, allows else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let key = endpoint.nostrPubkey, key.count == 64 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint.nostrPubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode_lnurl(_ lnurl: String) -> URL? {
|
||||||
|
guard let decoded = try? bech32_decode(lnurl) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard decoded.hrp == "lnurl" else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let url = URL(string: String(decoding: decoded.data, as: UTF8.self)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
|
||||||
|
guard let url = decode_lnurl(lnurl) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let ret = try? await URLSession.shared.data(from: url) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = String(decoding: ret.0, as: UTF8.self)
|
||||||
|
|
||||||
|
guard let endpoint: LNUrlPayRequest = decode_json(json_str) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, amount: Int64) async -> String? {
|
||||||
|
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let zappable = payreq.allowsNostr ?? false
|
||||||
|
|
||||||
|
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
|
||||||
|
|
||||||
|
if zappable {
|
||||||
|
if let json = encode_json(zapreq) {
|
||||||
|
query.append(URLQueryItem(name: "nostr", value: json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base_url.queryItems = query
|
||||||
|
|
||||||
|
guard let url = base_url.url else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
print("url \(url)")
|
||||||
|
|
||||||
|
guard let ret = try? await URLSession.shared.data(from: url) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = String(decoding: ret.0, as: UTF8.self)
|
||||||
|
guard let result: LNUrlPayResponse = decode_json(json_str) else {
|
||||||
|
print("fetch_zap_invoice error: \(json_str)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.pr
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
//
|
||||||
|
// Zaps.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class Zaps {
|
||||||
|
var zaps: [String: Zap]
|
||||||
|
let our_pubkey: String
|
||||||
|
var our_zaps: [String: [Zap]]
|
||||||
|
|
||||||
|
var event_counts: [String: Int]
|
||||||
|
var event_totals: [String: Int64]
|
||||||
|
|
||||||
|
init(our_pubkey: String) {
|
||||||
|
self.zaps = [:]
|
||||||
|
self.our_pubkey = our_pubkey
|
||||||
|
self.our_zaps = [:]
|
||||||
|
self.event_counts = [:]
|
||||||
|
self.event_totals = [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func add_zap(zap: Zap) {
|
||||||
|
if zaps[zap.event.id] != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.zaps[zap.event.id] = zap
|
||||||
|
|
||||||
|
// record our zaps for an event
|
||||||
|
if zap.request.ev.pubkey == our_pubkey {
|
||||||
|
switch zap.target {
|
||||||
|
case .note(let note_target):
|
||||||
|
if our_zaps[note_target.note_id] == nil {
|
||||||
|
our_zaps[note_target.note_id] = [zap]
|
||||||
|
} else {
|
||||||
|
let _ = insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
|
||||||
|
}
|
||||||
|
case .profile(_):
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't count tips to self. lame.
|
||||||
|
guard zap.request.ev.pubkey != zap.target.pubkey else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = zap.target.id
|
||||||
|
if event_counts[id] == nil {
|
||||||
|
event_counts[id] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if event_totals[id] == nil {
|
||||||
|
event_totals[id] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
event_counts[id] = event_counts[id]! + 1
|
||||||
|
event_totals[id] = event_totals[id]! + zap.invoice.amount
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ struct EventActionBar: View {
|
|||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
|
||||||
@State var sheet: ActionBarSheet? = nil
|
@State var sheet: ActionBarSheet? = nil
|
||||||
@State var confirm_boost: Bool = false
|
@State var confirm_boost: Bool = false
|
||||||
@State var show_share_sheet: Bool = false
|
@State var show_share_sheet: Bool = false
|
||||||
@@ -33,62 +34,47 @@ struct EventActionBar: View {
|
|||||||
EventActionButton(img: "bubble.left", col: nil) {
|
EventActionButton(img: "bubble.left", col: nil) {
|
||||||
notify(.reply, event)
|
notify(.reply, event)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
}
|
||||||
|
Spacer()
|
||||||
HStack(alignment: .bottom) {
|
ZStack {
|
||||||
|
|
||||||
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
|
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
|
||||||
if bar.boosted {
|
if bar.boosted {
|
||||||
notify(.delete, bar.our_boost)
|
notify(.delete, bar.our_boost)
|
||||||
} else {
|
} else if damus_state.is_privkey_user {
|
||||||
self.confirm_boost = true
|
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)
|
Spacer()
|
||||||
|
ZStack {
|
||||||
HStack(alignment: .bottom) {
|
|
||||||
LikeButton(liked: bar.liked) {
|
LikeButton(liked: bar.liked) {
|
||||||
if bar.liked {
|
if bar.liked {
|
||||||
notify(.delete, bar.our_like)
|
notify(.delete, bar.our_like)
|
||||||
} else {
|
} else {
|
||||||
send_like()
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if let lnurl = damus_state.profiles.lookup(id: event.pubkey)?.lnurl {
|
||||||
|
Spacer()
|
||||||
|
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
|
||||||
|
|
||||||
|
Spacer()
|
||||||
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
|
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
|
||||||
show_share_sheet = true
|
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) {
|
.sheet(isPresented: $show_share_sheet) {
|
||||||
if let note_id = bech32_note_id(event.id) {
|
if let note_id = bech32_note_id(event.id) {
|
||||||
@@ -98,7 +84,7 @@ struct EventActionBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $confirm_boost) {
|
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $confirm_boost) {
|
||||||
Button("Cancel") {
|
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
|
||||||
confirm_boost = false
|
confirm_boost = false
|
||||||
}
|
}
|
||||||
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
|
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
|
||||||
@@ -149,7 +135,7 @@ struct EventActionBar: View {
|
|||||||
|
|
||||||
func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View {
|
func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View {
|
||||||
Button(action: action) {
|
Button(action: action) {
|
||||||
Label(" ", systemImage: img)
|
Label(NSLocalizedString("\u{00A0}", comment: "Non-breaking space character to fill in blank space next to event action button icons."), systemImage: img)
|
||||||
.font(.footnote.weight(.medium))
|
.font(.footnote.weight(.medium))
|
||||||
.foregroundColor(col == nil ? Color.gray : col!)
|
.foregroundColor(col == nil ? Color.gray : col!)
|
||||||
}
|
}
|
||||||
@@ -176,9 +162,11 @@ struct EventActionBar_Previews: PreviewProvider {
|
|||||||
let ds = test_damus_state()
|
let ds = test_damus_state()
|
||||||
let ev = NostrEvent(content: "hi", pubkey: pk)
|
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 bar = ActionBarModel.empty()
|
||||||
let likedbar = ActionBarModel(likes: 10, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
|
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: 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_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil)
|
||||||
|
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
|
||||||
|
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil)
|
||||||
|
|
||||||
VStack(spacing: 50) {
|
VStack(spacing: 50) {
|
||||||
EventActionBar(damus_state: ds, event: ev, bar: bar)
|
EventActionBar(damus_state: ds, event: ev, bar: bar)
|
||||||
@@ -186,6 +174,10 @@ struct EventActionBar_Previews: PreviewProvider {
|
|||||||
EventActionBar(damus_state: ds, event: ev, bar: likedbar)
|
EventActionBar(damus_state: ds, event: ev, bar: likedbar)
|
||||||
|
|
||||||
EventActionBar(damus_state: ds, event: ev, bar: likedbar_ours)
|
EventActionBar(damus_state: ds, event: ev, bar: likedbar_ours)
|
||||||
|
|
||||||
|
EventActionBar(damus_state: ds, event: ev, bar: maxed_bar)
|
||||||
|
|
||||||
|
EventActionBar(damus_state: ds, event: ev, bar: zapbar)
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,27 +15,21 @@ struct EventDetailBar: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
if bar.boosts > 0 {
|
if bar.boosts > 0 {
|
||||||
Text("\(bar.boosts)")
|
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
|
||||||
.font(.body.bold())
|
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'.")
|
||||||
Text("Reposts")
|
}
|
||||||
.foregroundColor(.gray)
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
if bar.likes > 0 {
|
if bar.likes > 0 {
|
||||||
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
|
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
|
||||||
Text("\(bar.likes)")
|
Text("\(Text("\(bar.likes)", comment: "Number of reactions on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("reactions_count", comment: "Part of a larger sentence to describe how many reactions there are on a post."), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
|
||||||
.font(.body.bold())
|
|
||||||
Text("Reactions")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
if bar.tips > 0 {
|
if bar.zaps > 0 {
|
||||||
Text("\(bar.tips)")
|
Text("\(Text("\(bar.zaps)", comment: "Number of zap payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("zaps_count", comment: "Part of a larger sentence to describe how many zap payments there are on a post."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
|
||||||
.font(.body.bold())
|
|
||||||
Text("Tips")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,29 +9,43 @@ import SwiftUI
|
|||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
|
||||||
struct InnerBannerImageView: View {
|
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 {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color(uiColor: .systemBackground)
|
Color(uiColor: .systemBackground)
|
||||||
|
|
||||||
if (url != nil) {
|
if (imageModel.url != nil) {
|
||||||
KFAnimatedImage(url)
|
KFAnimatedImage(imageModel.url)
|
||||||
.callbackQueue(.dispatch(.global(qos: .background)))
|
.callbackQueue(.dispatch(.global(qos: .background)))
|
||||||
.processingQueue(.dispatch(.global(qos: .background)))
|
.processingQueue(.dispatch(.global(qos: .background)))
|
||||||
.appendProcessor(LargeImageProcessor.shared)
|
.serialize(by: imageModel.serializer)
|
||||||
|
.setProcessor(imageModel.processor)
|
||||||
.configure { view in
|
.configure { view in
|
||||||
view.framePreloadCount = 1
|
view.framePreloadCount = 1
|
||||||
}
|
}
|
||||||
.placeholder { _ in
|
.placeholder { _ in
|
||||||
Image("profile-banner").resizable()
|
Color(uiColor: .secondarySystemBackground)
|
||||||
}
|
}
|
||||||
.scaleFactor(UIScreen.main.scale)
|
.scaleFactor(UIScreen.main.scale)
|
||||||
.loadDiskFileSynchronously()
|
.loadDiskFileSynchronously()
|
||||||
.fade(duration: 0.1)
|
.fade(duration: 0.1)
|
||||||
|
.onFailureImage(defaultImage)
|
||||||
|
.id(imageModel.refreshID)
|
||||||
} else {
|
} else {
|
||||||
Image("profile-banner").resizable()
|
Image(uiImage: defaultImage).resizable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +64,7 @@ struct BannerImageView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some 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
|
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||||
let updated = notif.object as! ProfileUpdate
|
let updated = notif.object as! ProfileUpdate
|
||||||
|
|
||||||
|
|||||||
@@ -96,17 +96,24 @@ struct ChatView: View {
|
|||||||
|
|
||||||
if let ref_id = thread.replies.lookup(event.id) {
|
if let ref_id = thread.replies.lookup(event.id) {
|
||||||
if !is_reply_to_prev() {
|
if !is_reply_to_prev() {
|
||||||
ReplyQuoteView(privkey: damus_state.keypair.privkey, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)
|
/*
|
||||||
|
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)
|
||||||
.frame(maxHeight: expand_reply ? nil : 100)
|
.frame(maxHeight: expand_reply ? nil : 100)
|
||||||
.environmentObject(thread)
|
.environmentObject(thread)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
expand_reply = !expand_reply
|
expand_reply = !expand_reply
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
ReplyDescription
|
ReplyDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey), artifacts: .just_content(event.content), size: .normal)
|
let show_images = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||||
|
NoteContentView(damus_state: damus_state,
|
||||||
|
event: event,
|
||||||
|
show_images: show_images,
|
||||||
|
artifacts: .just_content(event.content),
|
||||||
|
size: .normal)
|
||||||
|
|
||||||
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
||||||
let bar = make_actionbar_model(ev: event, damus: damus_state)
|
let bar = make_actionbar_model(ev: event, damus: damus_state)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct ChatroomView: View {
|
|||||||
next_ev: ind == count-1 ? nil : thread.events[ind+1],
|
next_ev: ind == count-1 ? nil : thread.events[ind+1],
|
||||||
damus_state: damus
|
damus_state: damus
|
||||||
)
|
)
|
||||||
.event_context_menu(ev, pubkey: ev.pubkey, privkey: damus.keypair.privkey)
|
.event_context_menu(ev, keypair: damus.keypair, target_pubkey: ev.pubkey)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if thread.initial_event.id == ev.id {
|
if thread.initial_event.id == ev.id {
|
||||||
//dismiss()
|
//dismiss()
|
||||||
|
|||||||
+165
-96
@@ -5,30 +5,31 @@
|
|||||||
// Created by William Casarin on 2022-06-09.
|
// Created by William Casarin on 2022-06-09.
|
||||||
//
|
//
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import SwiftUI
|
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct ConfigView: View {
|
struct ConfigView: View {
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@State var show_add_relay: Bool = false
|
|
||||||
@State var confirm_logout: Bool = false
|
@State var confirm_logout: Bool = false
|
||||||
@State var new_relay: String = ""
|
@State var confirm_delete_account: Bool = false
|
||||||
@State var show_privkey: Bool = false
|
@State var show_privkey: Bool = false
|
||||||
|
@State var show_api_key: Bool = false
|
||||||
@State var privkey: String
|
@State var privkey: String
|
||||||
@State var privkey_copied: Bool = false
|
@State var privkey_copied: Bool = false
|
||||||
@State var pubkey_copied: Bool = false
|
@State var pubkey_copied: Bool = false
|
||||||
@State var relays: [RelayDescriptor]
|
@State var delete_text: String = ""
|
||||||
@EnvironmentObject var user_settings: UserSettingsStore
|
|
||||||
|
@ObservedObject var settings: UserSettingsStore
|
||||||
|
|
||||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
|
||||||
init(state: DamusState) {
|
init(state: DamusState) {
|
||||||
self.state = state
|
self.state = state
|
||||||
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
|
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
|
||||||
_relays = State(initialValue: state.pool.descriptors)
|
_settings = ObservedObject(initialValue: state.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: (jb55) could be more general but not gonna worry about it atm
|
// TODO: (jb55) could be more general but not gonna worry about it atm
|
||||||
func CopyButton(is_pk: Bool) -> some View {
|
func CopyButton(is_pk: Bool) -> some View {
|
||||||
return Button(action: {
|
return Button(action: {
|
||||||
@@ -41,74 +42,41 @@ struct ConfigView: View {
|
|||||||
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
|
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var recommended: [RelayDescriptor] {
|
|
||||||
let rs: [RelayDescriptor] = []
|
|
||||||
return BOOTSTRAP_RELAYS.reduce(into: rs) { (xs, x) in
|
|
||||||
if let _ = state.pool.get_relay(x) {
|
|
||||||
} else {
|
|
||||||
xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
|
||||||
List(Array(relays), id: \.url) { relay in
|
|
||||||
RelayView(state: state, relay: relay.url.absoluteString)
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
HStack {
|
|
||||||
Text("Relays", comment: "Header text for relay server list for configuration.")
|
|
||||||
Spacer()
|
|
||||||
Button(action: { show_add_relay = true }) {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if recommended.count > 0 {
|
|
||||||
Section(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration")) {
|
|
||||||
List(recommended, id: \.url) { r in
|
|
||||||
RecommendedRelayView(damus: state, relay: r.url.absoluteString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
|
Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(state.keypair.pubkey_bech32)
|
Text(state.keypair.pubkey_bech32)
|
||||||
|
|
||||||
CopyButton(is_pk: true)
|
CopyButton(is_pk: true)
|
||||||
}
|
}
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let sec = state.keypair.privkey_bech32 {
|
if let sec = state.keypair.privkey_bech32 {
|
||||||
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
|
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
|
||||||
HStack {
|
HStack {
|
||||||
if show_privkey == false {
|
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)
|
.disabled(true)
|
||||||
} else {
|
} else {
|
||||||
Text(sec)
|
Text(sec)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyButton(is_pk: false)
|
CopyButton(is_pk: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
|
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(NSLocalizedString("Wallet Selector", comment: "Section title for selection of wallet.")) {
|
Section(NSLocalizedString("Wallet Selector", comment: "Section title for selection of wallet.")) {
|
||||||
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $user_settings.show_wallet_selector).toggleStyle(.switch)
|
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch)
|
||||||
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"),
|
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"),
|
||||||
selection: $user_settings.default_wallet) {
|
selection: $settings.default_wallet) {
|
||||||
ForEach(Wallet.allCases, id: \.self) { wallet in
|
ForEach(Wallet.allCases, id: \.self) { wallet in
|
||||||
Text(wallet.model.displayName)
|
Text(wallet.model.displayName)
|
||||||
.tag(wallet.model.tag)
|
.tag(wallet.model.tag)
|
||||||
@@ -116,8 +84,55 @@ struct ConfigView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
|
||||||
|
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
|
||||||
|
ForEach(TranslationService.allCases, id: \.self) { server in
|
||||||
|
Text(server.model.displayName)
|
||||||
|
.tag(server.model.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.translation_service == .libretranslate {
|
||||||
|
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
|
||||||
|
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
|
||||||
|
Text(server.model.displayName)
|
||||||
|
.tag(server.model.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.libretranslate_server == .custom {
|
||||||
|
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureField(NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server."), text: $settings.libretranslate_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.disabled(settings.translation_service != .libretranslate)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.translation_service == .deepl {
|
||||||
|
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
|
||||||
|
ForEach(DeepLPlan.allCases, id: \.self) { server in
|
||||||
|
Text(server.model.displayName)
|
||||||
|
.tag(server.model.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.deepl_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.disabled(settings.translation_service != .deepl)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
|
||||||
|
if settings.deepl_api_key == "" {
|
||||||
|
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen")) {
|
Section(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen")) {
|
||||||
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $user_settings.left_handed)
|
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,69 +144,123 @@ struct ConfigView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(NSLocalizedString("Reset", comment: "Section title for resetting the user")) {
|
if state.is_privkey_user {
|
||||||
Button(NSLocalizedString("Logout", comment: "Button to logout the user.")) {
|
Section(NSLocalizedString("Delete", comment: "Section title for deleting the user")) {
|
||||||
confirm_logout = true
|
Button(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), role: .destructive) {
|
||||||
|
confirm_delete_account = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
|
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.alert(NSLocalizedString("Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
|
||||||
|
TextField("Type DELETE to delete", text: $delete_text)
|
||||||
|
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
|
||||||
|
confirm_delete_account = false
|
||||||
|
}
|
||||||
|
Button(NSLocalizedString("Delete", comment: "Button for deleting the users account."), role: .destructive) {
|
||||||
|
guard let full_kp = state.keypair.to_full() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard delete_text == "DELETE" else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let ev = created_deleted_account_profile(keypair: full_kp)
|
||||||
|
state.pool.send(.event(ev))
|
||||||
|
notify(.logout, ())
|
||||||
|
}
|
||||||
|
}
|
||||||
.alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) {
|
.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
|
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, ())
|
notify(.logout, ())
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text("Make sure your nsec account key is saved before you logout or you will lose access to this account", comment: "Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.")
|
Text("Make sure your nsec account key is saved before you logout or you will lose access to this account", comment: "Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.")
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $show_add_relay) {
|
|
||||||
AddRelayView(show_add_relay: $show_add_relay, relay: $new_relay) { m_relay in
|
|
||||||
guard var relay = m_relay else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if relay.starts(with: "wss://") == false {
|
|
||||||
relay = "wss://" + relay
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let url = URL(string: relay) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let ev = state.contacts.event else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let privkey = state.keypair.privkey else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = RelayInfo.rw
|
|
||||||
|
|
||||||
guard (try? state.pool.add_relay(url, info: info)) != nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state.pool.connect(to: [relay])
|
|
||||||
|
|
||||||
guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.descriptors, relay: relay, info: info) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
process_contact_event(pool: state.pool, contacts: state.contacts, pubkey: state.pubkey, ev: ev)
|
|
||||||
|
|
||||||
state.pool.send(.event(new_ev))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.relays_changed)) { _ in
|
}
|
||||||
self.relays = state.pool.descriptors
|
|
||||||
|
var libretranslate_view: some View {
|
||||||
|
VStack {
|
||||||
|
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
|
||||||
|
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
|
||||||
|
Text(server.model.displayName)
|
||||||
|
.tag(server.model.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.disabled(settings.libretranslate_server != .custom)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
HStack {
|
||||||
|
let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.")
|
||||||
|
if show_api_key {
|
||||||
|
TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
if settings.libretranslate_api_key != "" {
|
||||||
|
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
|
||||||
|
show_api_key = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
if settings.libretranslate_api_key != "" {
|
||||||
|
Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) {
|
||||||
|
show_api_key = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var deepl_view: some View {
|
||||||
|
VStack {
|
||||||
|
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
|
||||||
|
ForEach(DeepLPlan.allCases, id: \.self) { server in
|
||||||
|
Text(server.model.displayName)
|
||||||
|
.tag(server.model.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.")
|
||||||
|
if show_api_key {
|
||||||
|
TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
if settings.deepl_api_key != "" {
|
||||||
|
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) {
|
||||||
|
show_api_key = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
if settings.deepl_api_key != "" {
|
||||||
|
Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) {
|
||||||
|
show_api_key = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if settings.deepl_api_key == "" {
|
||||||
|
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ struct CreateAccountView: View {
|
|||||||
@StateObject var account: CreateAccountModel = CreateAccountModel()
|
@StateObject var account: CreateAccountModel = CreateAccountModel()
|
||||||
@State var is_light: Bool = false
|
@State var is_light: Bool = false
|
||||||
@State var is_done: Bool = false
|
@State var is_done: Bool = false
|
||||||
|
@State var reading_eula: Bool = false
|
||||||
|
|
||||||
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
|
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
|
||||||
return VStack(alignment: .leading, spacing: 10.0, content: content)
|
return VStack(alignment: .leading, spacing: 10.0, content: content)
|
||||||
@@ -75,6 +76,7 @@ struct CreateAccountView: View {
|
|||||||
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
|
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
DamusWhiteButton(NSLocalizedString("Create", comment: "Button to create account.")) {
|
DamusWhiteButton(NSLocalizedString("Create", comment: "Button to create account.")) {
|
||||||
self.is_done = true
|
self.is_done = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct DMChatView: View {
|
|||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
|
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
|
||||||
DMView(event: dms.events[ind], damus_state: damus_state)
|
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, target_pubkey: ev.pubkey)
|
||||||
}
|
}
|
||||||
EndBlock(height: 80)
|
EndBlock(height: 80)
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,8 @@ struct DMChatView: View {
|
|||||||
)
|
)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.foregroundColor(Color.primary)
|
.foregroundColor(Color.primary)
|
||||||
|
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@@ -97,22 +99,15 @@ struct DMChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.frame(height: 50 + 20 * CGFloat(text_lines))
|
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
|
||||||
}
|
|
||||||
|
|
||||||
var text_lines: Int {
|
Text(message).opacity(0).padding(.all, 8)
|
||||||
var lines = 1
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
for c in message {
|
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
|
||||||
if lines > 4 {
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
if c.isNewline {
|
|
||||||
lines += 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
return lines
|
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func send_message() {
|
func send_message() {
|
||||||
@@ -142,14 +137,15 @@ struct DMChatView: View {
|
|||||||
|
|
||||||
Footer
|
Footer
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.")
|
Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.")
|
||||||
.lineLimit(nil)
|
.lineLimit(nil)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
.opacity(((dms.events.count == 0) ? 1.0 : 0.0))
|
.opacity(((dms.events.count == 0) ? 1.0 : 0.0))
|
||||||
.foregroundColor(.gray)
|
.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 }
|
.toolbar { Header }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +154,7 @@ struct DMChatView_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: [])
|
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")
|
DMChatView(damus_state: test_damus_state(), pubkey: "pubkey")
|
||||||
.environmentObject(model)
|
.environmentObject(model)
|
||||||
@@ -166,7 +162,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 {
|
guard let privkey = keypair.privkey else {
|
||||||
return nil
|
return nil
|
||||||
@@ -181,7 +177,9 @@ func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keyp
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv)
|
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.calculate_id()
|
||||||
ev.sign(privkey: privkey)
|
ev.sign(privkey: privkey)
|
||||||
return ev
|
return ev
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct DMView: View {
|
|||||||
|
|
||||||
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||||
|
|
||||||
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal)
|
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal)
|
||||||
.foregroundColor(is_ours ? Color.white : Color.primary)
|
.foregroundColor(is_ours ? Color.white : Color.primary)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
|
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
|
||||||
|
|||||||
@@ -7,15 +7,26 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
enum DMType: Hashable {
|
||||||
|
case rando
|
||||||
|
case friend
|
||||||
|
}
|
||||||
|
|
||||||
struct DirectMessagesView: View {
|
struct DirectMessagesView: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
|
|
||||||
|
@State var dm_type: DMType = .friend
|
||||||
@State var open_dm: Bool = false
|
@State var open_dm: Bool = false
|
||||||
@State var pubkey: String = ""
|
@State var pubkey: String = ""
|
||||||
@State var active_model: DirectMessageModel = DirectMessageModel()
|
|
||||||
@EnvironmentObject var model: DirectMessagesModel
|
@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 {
|
ScrollView {
|
||||||
let chat = DMChatView(damus_state: damus_state, pubkey: pubkey)
|
let chat = DMChatView(damus_state: damus_state, pubkey: pubkey)
|
||||||
.environmentObject(active_model)
|
.environmentObject(active_model)
|
||||||
@@ -26,20 +37,19 @@ struct DirectMessagesView: View {
|
|||||||
if model.dms.isEmpty, !model.loading {
|
if model.dms.isEmpty, !model.loading {
|
||||||
EmptyTimelineView()
|
EmptyTimelineView()
|
||||||
} else {
|
} 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)
|
MaybeEvent(tup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.top)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View {
|
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if let ev = tup.1.events.last {
|
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 {
|
.onTapGesture {
|
||||||
pubkey = tup.0
|
pubkey = tup.0
|
||||||
active_model = tup.1
|
active_model = tup.1
|
||||||
@@ -52,8 +62,28 @@ struct DirectMessagesView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
MainContent
|
VStack(spacing: 0) {
|
||||||
.navigationTitle(NSLocalizedString("Encrypted DMs", comment: "Navigation title for view of encrypted DMs, where DM is an English abbreviation for Direct Message."))
|
CustomPicker(selection: $dm_type, content: {
|
||||||
|
Text("DMs", comment: "Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.")
|
||||||
|
.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)
|
||||||
|
})
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.frame(height: 1)
|
||||||
|
|
||||||
|
TabView(selection: $dm_type) {
|
||||||
|
MainContent(requests: false)
|
||||||
|
.tag(DMType.friend)
|
||||||
|
|
||||||
|
MainContent(requests: true)
|
||||||
|
.tag(DMType.rando)
|
||||||
|
}
|
||||||
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +93,9 @@ struct DirectMessagesView_Previews: PreviewProvider {
|
|||||||
pubkey: "pubkey",
|
pubkey: "pubkey",
|
||||||
kind: 4,
|
kind: 4,
|
||||||
tags: [])
|
tags: [])
|
||||||
let model = DirectMessageModel(events: [ev])
|
let ds = test_damus_state()
|
||||||
DirectMessagesView(damus_state: test_damus_state())
|
let model = DirectMessageModel(events: [ev], our_pubkey: ds.pubkey)
|
||||||
|
DirectMessagesView(damus_state: ds)
|
||||||
.environmentObject(model)
|
.environmentObject(model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
//
|
||||||
|
// EULAView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EULAView: View {
|
||||||
|
var state: SetupState?
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@State var accepted = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
DamusGradient()
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
Text("EULA", comment: "Label indicating that the below text is the EULA, an acronym for End User License Agreement.")
|
||||||
|
.font(.title.bold())
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
Text(Markdown.parse(content: """
|
||||||
|
End User License Agreement
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This End User License Agreement ("EULA") is a legal agreement between you and Damus Nostr Inc. for the use of our mobile application Damus. By installing, accessing, or using our application, you agree to be bound by the terms and conditions of this EULA.
|
||||||
|
|
||||||
|
## Prohibited Content and Conduct
|
||||||
|
|
||||||
|
You agree not to use our application to create, upload, post, send, or store any content that:
|
||||||
|
|
||||||
|
* Is illegal, infringing, or fraudulent
|
||||||
|
* Is defamatory, libelous, or threatening
|
||||||
|
* Is pornographic, obscene, or offensive
|
||||||
|
* Is discriminatory or promotes hate speech
|
||||||
|
* Is harmful to minors
|
||||||
|
* Is intended to harass or bully others
|
||||||
|
* Is intended to impersonate others
|
||||||
|
|
||||||
|
## You also agree not to engage in any conduct that:
|
||||||
|
|
||||||
|
* Harasses or bullies others
|
||||||
|
* Impersonates others
|
||||||
|
* Is intended to intimidate or threaten others
|
||||||
|
* Is intended to promote or incite violence
|
||||||
|
|
||||||
|
## Consequences of Violation
|
||||||
|
|
||||||
|
Any violation of this EULA, including the prohibited content and conduct outlined above, may result in the termination of your access to our application.
|
||||||
|
|
||||||
|
## Disclaimer of Warranties and Limitation of Liability
|
||||||
|
|
||||||
|
Our application is provided "as is" and "as available" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. We do not guarantee that our application will be uninterrupted or error-free. In no event shall Damus Nostr Inc. be liable for any damages whatsoever, including but not limited to direct, indirect, special, incidental, or consequential damages, arising out of or in connection with the use or inability to use our application.
|
||||||
|
|
||||||
|
## Changes to EULA
|
||||||
|
|
||||||
|
We reserve the right to update or modify this EULA at any time and without prior notice. Your continued use of our application following any changes to this EULA will be deemed to be your acceptance of such changes.
|
||||||
|
|
||||||
|
## Contact Information
|
||||||
|
|
||||||
|
If you have any questions about this EULA, please contact us at damus@jb55.com
|
||||||
|
|
||||||
|
## Acceptance of Terms
|
||||||
|
|
||||||
|
By using our Application, you signify your acceptance of this EULA. If you do not agree to this EULA, you may not use our Application.
|
||||||
|
|
||||||
|
"""))
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
if state == .create_account {
|
||||||
|
NavigationLink(destination: CreateAccountView(), isActive: $accepted) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NavigationLink(destination: LoginView(), isActive: $accepted) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DamusWhiteButton(NSLocalizedString("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.")) {
|
||||||
|
accepted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
DamusWhiteButton(NSLocalizedString("Reject", comment: "Button to reject the end user license agreement, which disallows the user from being let into the app.")) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.navigationBarItems(leading: BackNav())
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EULAView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
EULAView()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -196,7 +196,7 @@ struct EditMetadataView: View {
|
|||||||
if let parts = nip05_parts {
|
if let parts = nip05_parts {
|
||||||
Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.")
|
Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.")
|
||||||
} else {
|
} else {
|
||||||
Text("'\(nip05)' is an invalid nip05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
|
Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ struct EventDetailView: View {
|
|||||||
}
|
}
|
||||||
toggle_thread_view()
|
toggle_thread_view()
|
||||||
}
|
}
|
||||||
case .event(let ev, let highlight):
|
case .event(let ev, _):
|
||||||
EventView(event: ev, highlight: highlight, has_action_bar: true, damus: damus, show_friend_icon: true)
|
EventView(damus: damus, event: ev, has_action_bar: true)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if thread.initial_event.id == ev.id {
|
if thread.initial_event.id == ev.id {
|
||||||
toggle_thread_view()
|
toggle_thread_view()
|
||||||
|
|||||||
+67
-243
@@ -8,38 +8,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
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 {
|
enum EventViewKind {
|
||||||
case small
|
case small
|
||||||
case normal
|
case normal
|
||||||
case big
|
|
||||||
case selected
|
case selected
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,117 +20,40 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
|
|||||||
return .body
|
return .body
|
||||||
case .normal:
|
case .normal:
|
||||||
return .body
|
return .body
|
||||||
case .big:
|
|
||||||
return .headline
|
|
||||||
case .selected:
|
case .selected:
|
||||||
return .custom("selected", size: 21.0)
|
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 {
|
struct EventView: View {
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let highlight: Highlight
|
|
||||||
let has_action_bar: Bool
|
let has_action_bar: Bool
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
let pubkey: String
|
let pubkey: String
|
||||||
let show_friend_icon: Bool
|
|
||||||
let size: EventViewKind
|
|
||||||
|
|
||||||
@EnvironmentObject var action_bar: ActionBarModel
|
@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.event = event
|
||||||
self.highlight = highlight
|
|
||||||
self.has_action_bar = has_action_bar
|
self.has_action_bar = has_action_bar
|
||||||
self.damus = damus
|
self.damus = damus
|
||||||
self.pubkey = event.pubkey
|
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.event = event
|
||||||
self.highlight = .none
|
|
||||||
self.has_action_bar = false
|
self.has_action_bar = false
|
||||||
self.damus = damus
|
self.damus = damus
|
||||||
self.pubkey = event.pubkey
|
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.event = event
|
||||||
self.highlight = .none
|
|
||||||
self.has_action_bar = false
|
self.has_action_bar = false
|
||||||
self.damus = damus
|
self.damus = damus
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.show_friend_icon = show_friend_icon
|
|
||||||
self.size = size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -175,94 +69,71 @@ struct EventView: View {
|
|||||||
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
|
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
TextEvent(inner_ev, pubkey: inner_ev.pubkey)
|
TextEvent(inner_ev, pubkey: inner_ev.pubkey, booster_pubkey: event.pubkey)
|
||||||
.padding([.top], 1)
|
.padding([.top], 1)
|
||||||
}
|
}
|
||||||
|
} else if event.known_kind == .zap {
|
||||||
|
if let zap = damus.zaps.zaps[event.id] {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("⚡️ \(format_msats(zap.invoice.amount))")
|
||||||
|
.font(.headline)
|
||||||
|
.padding([.top], 2)
|
||||||
|
|
||||||
|
TextEvent(zap.request.ev, pubkey: zap.request.ev.pubkey, booster_pubkey: nil)
|
||||||
|
.padding([.top], 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
TextEvent(event, pubkey: pubkey)
|
TextEvent(event, pubkey: pubkey)
|
||||||
.padding([.top], 6)
|
.padding([.top], 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.padding([.top], 4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TextEvent(_ event: NostrEvent, pubkey: String) -> some View {
|
func TextEvent(_ event: NostrEvent, pubkey: String, booster_pubkey: String? = nil) -> some View {
|
||||||
let content = event.get_content(damus.keypair.privkey)
|
|
||||||
|
|
||||||
return HStack(alignment: .top) {
|
return HStack(alignment: .top) {
|
||||||
let profile = damus.profiles.lookup(id: pubkey)
|
let profile = damus.profiles.lookup(id: pubkey)
|
||||||
|
|
||||||
if size != .selected {
|
VStack {
|
||||||
VStack {
|
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
|
||||||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
|
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
|
||||||
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
|
|
||||||
|
NavigationLink(destination: pv) {
|
||||||
NavigationLink(destination: pv) {
|
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles)
|
||||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, profiles: damus.profiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
if size == .selected {
|
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
|
||||||
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: show_friend_icon, size: size)
|
Text("\(format_relative_time(event.created_at))")
|
||||||
if size != .selected {
|
|
||||||
Text("\(format_relative_time(event.created_at))")
|
|
||||||
.font(eventviewsize_to_font(size))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.is_reply(damus.keypair.privkey) {
|
|
||||||
Text("\(reply_desc(profiles: damus.profiles, event: event))")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.gray)
|
.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)
|
EventBody(damus_state: damus, event: event, size: .normal)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
|
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 has_action_bar {
|
||||||
if size == .selected {
|
Rectangle().frame(height: 2).opacity(0)
|
||||||
Text("\(format_date(event.created_at))")
|
|
||||||
.padding(.top, 10)
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.padding([.bottom], 4)
|
|
||||||
} else {
|
|
||||||
Rectangle().frame(height: 2).opacity(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
let bar = make_actionbar_model(ev: event, damus: damus)
|
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)
|
EventActionBar(damus_state: damus, event: event, bar: bar)
|
||||||
|
.padding([.top], 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
|
||||||
.padding([.top], 4)
|
|
||||||
}
|
}
|
||||||
.padding([.leading], 2)
|
.padding([.leading], 2)
|
||||||
}
|
}
|
||||||
@@ -271,18 +142,21 @@ struct EventView: View {
|
|||||||
.id(event.id)
|
.id(event.id)
|
||||||
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
||||||
.padding([.bottom], 2)
|
.padding([.bottom], 2)
|
||||||
.event_context_menu(event, pubkey: pubkey, privkey: damus.keypair.privkey)
|
.event_context_menu(event, keypair: damus.keypair, target_pubkey: pubkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// blame the porn bots for this code
|
// 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 {
|
if ev.pubkey == our_pubkey {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if contacts.is_in_friendosphere(ev.pubkey) {
|
if contacts.is_in_friendosphere(ev.pubkey) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if let boost_key = booster_pubkey, contacts.is_in_friendosphere(boost_key) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,37 +184,9 @@ extension View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func event_context_menu(_ event: NostrEvent, pubkey: String, privkey: String?) -> some View {
|
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String) -> some View {
|
||||||
return self.contextMenu {
|
return self.contextMenu {
|
||||||
Button {
|
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -360,48 +206,23 @@ 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 {
|
func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
|
||||||
let likes = damus.likes.counts[ev.id]
|
let likes = damus.likes.counts[ev.id]
|
||||||
let boosts = damus.boosts.counts[ev.id]
|
let boosts = damus.boosts.counts[ev.id]
|
||||||
let tips = damus.tips.tips[ev.id]
|
let zaps = damus.zaps.event_counts[ev.id]
|
||||||
|
let zap_total = damus.zaps.event_totals[ev.id]
|
||||||
let our_like = damus.likes.our_events[ev.id]
|
let our_like = damus.likes.our_events[ev.id]
|
||||||
let our_boost = damus.boosts.our_events[ev.id]
|
let our_boost = damus.boosts.our_events[ev.id]
|
||||||
let our_tip = damus.tips.our_tips[ev.id]
|
let our_zap = damus.zaps.our_zaps[ev.id]
|
||||||
|
|
||||||
return ActionBarModel(likes: likes ?? 0,
|
return ActionBarModel(likes: likes ?? 0,
|
||||||
boosts: boosts ?? 0,
|
boosts: boosts ?? 0,
|
||||||
tips: tips ?? 0,
|
zaps: zaps ?? 0,
|
||||||
|
zap_total: zap_total ?? 0,
|
||||||
our_like: our_like,
|
our_like: our_like,
|
||||||
our_boost: our_boost,
|
our_boost: our_boost,
|
||||||
our_tip: our_tip
|
our_zap: our_zap?.first
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,22 +230,25 @@ func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
|
|||||||
struct EventView_Previews: PreviewProvider {
|
struct EventView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
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: .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: .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(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(
|
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(),
|
damus: test_damus_state(),
|
||||||
show_friend_icon: true,
|
event: test_event,
|
||||||
size: .selected
|
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)
|
||||||
|
)
|
||||||
|
|||||||
@@ -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, target_pubkey: pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EmbeddedEventView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
EmbeddedEventView(damus_state: test_damus_state(), event: test_event)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(damus_state: damus_state, event: event, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
//
|
||||||
|
// EventMenu.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-01-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EventMenuContext: View {
|
||||||
|
let event: NostrEvent
|
||||||
|
let keypair: Keypair
|
||||||
|
let target_pubkey: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = event.get_content(keypair.privkey)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = bech32_pubkey(target_pubkey)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = event_to_json(ev: event)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "square.on.square")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||||
|
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))
|
||||||
|
notify(.report, target)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(role: .destructive) {
|
||||||
|
notify(.block, target_pubkey)
|
||||||
|
} label: {
|
||||||
|
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
struct EventMenu: UIViewRepresentable {
|
||||||
|
|
||||||
|
typealias UIViewType = UIButton
|
||||||
|
|
||||||
|
let saveAction = UIAction(title: "") { action in }
|
||||||
|
let saveMenu = UIMenu(title: "", children: [
|
||||||
|
UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
|
||||||
|
//code action for menu item
|
||||||
|
},
|
||||||
|
UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
|
||||||
|
//code action for menu item
|
||||||
|
},
|
||||||
|
UIAction(title: "First Menu Item", image: UIImage(systemName: "nameOfSFSymbol")) { action in
|
||||||
|
//code action for menu item
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UIButton {
|
||||||
|
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
|
||||||
|
button.showsMenuAsPrimaryAction = true
|
||||||
|
button.menu = saveMenu
|
||||||
|
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UIButton, context: Context) {
|
||||||
|
uiView.setImage(UIImage(systemName: "plus"), for: .normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EventMenu_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
EventMenu(event: test_event, privkey: nil, pubkey: test_event.pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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", comment: "Text to indicate that what is being shown is a post from a user who has been blocked.")
|
||||||
|
Spacer()
|
||||||
|
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a post from a user who has been blocked.") : NSLocalizedString("Show", comment: "Button to show a post from a user who has been blocked.")) {
|
||||||
|
shown.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Event: some View {
|
||||||
|
Group {
|
||||||
|
if selected {
|
||||||
|
SelectedEventView(damus: damus_state, event: event)
|
||||||
|
} else {
|
||||||
|
EventView(damus: damus_state, event: event, has_action_bar: true)
|
||||||
|
.onTapGesture {
|
||||||
|
nav_target = event.id
|
||||||
|
navigating = true
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
// TODO: find another solution to prevent layout shifting and layout blocking on large responses
|
||||||
|
scroller?.scrollTo("main", anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if should_mute {
|
||||||
|
MutedBox
|
||||||
|
}
|
||||||
|
if shown {
|
||||||
|
Event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||||
|
guard let mutes = notif.object as? [String] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mutes.contains(event.pubkey) {
|
||||||
|
shown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.new_unmutes)) { notif in
|
||||||
|
guard let unmutes = notif.object as? [String] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmutes.contains(event.pubkey) {
|
||||||
|
shown = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MutedEventView_Previews: PreviewProvider {
|
||||||
|
@State static var nav_target: String? = nil
|
||||||
|
@State static var navigating: Bool = false
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
|
||||||
|
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, nav_target: $nav_target, navigating: $navigating, selected: false)
|
||||||
|
.frame(width: .infinity, height: 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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, target_pubkey: event.pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SelectedEventView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SelectedEventView(damus: test_damus_state(), event: test_event)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,7 @@ struct FollowButtonView: View {
|
|||||||
follow_state = perform_follow_btn_action(follow_state, target: target)
|
follow_state = perform_follow_btn_action(follow_state, target: target)
|
||||||
} label: {
|
} label: {
|
||||||
Text(follow_btn_txt(follow_state))
|
Text(follow_btn_txt(follow_state))
|
||||||
.frame(height: 30)
|
.frame(width: 105, height: 30)
|
||||||
.padding(.horizontal, 25)
|
|
||||||
//.padding(.vertical, 10)
|
//.padding(.vertical, 10)
|
||||||
.font(.caption.weight(.bold))
|
.font(.caption.weight(.bold))
|
||||||
.foregroundColor(follow_state == .unfollows ? filledTextColor() : borderColor())
|
.foregroundColor(follow_state == .unfollows ? filledTextColor() : borderColor())
|
||||||
|
|||||||
@@ -15,26 +15,7 @@ struct FollowUserView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
let pmodel = ProfileModel(pubkey: target.pubkey, damus: damus_state)
|
UserView(damus_state: damus_state, pubkey: target.pubkey)
|
||||||
let followers = FollowersModel(damus_state: damus_state, target: target.pubkey)
|
|
||||||
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
|
|
||||||
|
|
||||||
NavigationLink(destination: pv) {
|
|
||||||
ProfilePicView(pubkey: target.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
let profile = damus_state.profiles.lookup(id: target.pubkey)
|
|
||||||
ProfileName(pubkey: target.pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
|
|
||||||
if let about = profile?.about {
|
|
||||||
Text(FollowUserView.markdown.process(about))
|
|
||||||
.lineLimit(3)
|
|
||||||
.font(.footnote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
|
|
||||||
FollowButtonView(target: target, follow_state: damus_state.contacts.follow_state(target.pubkey))
|
FollowButtonView(target: target, follow_state: damus_state.contacts.follow_state(target.pubkey))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,73 +7,28 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
|
import NaturalLanguage
|
||||||
struct NoteArtifacts {
|
|
||||||
let content: String
|
|
||||||
let images: [URL]
|
|
||||||
let invoices: [Invoice]
|
|
||||||
let links: [URL]
|
|
||||||
|
|
||||||
static func just_content(_ content: String) -> NoteArtifacts {
|
|
||||||
NoteArtifacts(content: content, images: [], invoices: [], links: [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
|
|
||||||
let blocks = ev.blocks(privkey)
|
|
||||||
var invoices: [Invoice] = []
|
|
||||||
var img_urls: [URL] = []
|
|
||||||
var link_urls: [URL] = []
|
|
||||||
let txt = blocks.reduce("") { str, block in
|
|
||||||
switch block {
|
|
||||||
case .mention(let m):
|
|
||||||
return str + mention_str(m, profiles: profiles)
|
|
||||||
case .text(let txt):
|
|
||||||
return str + 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
|
|
||||||
img_urls.append(url)
|
|
||||||
return str
|
|
||||||
} else {
|
|
||||||
link_urls.append(url)
|
|
||||||
return str + url.absoluteString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func is_image_url(_ url: URL) -> Bool {
|
|
||||||
let str = url.lastPathComponent
|
|
||||||
return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif")
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NoteContentView: View {
|
struct NoteContentView: View {
|
||||||
let privkey: String?
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let profiles: Profiles
|
|
||||||
let previews: PreviewCache
|
|
||||||
|
|
||||||
let show_images: Bool
|
let show_images: Bool
|
||||||
|
|
||||||
@State var artifacts: NoteArtifacts
|
@State var artifacts: NoteArtifacts
|
||||||
|
|
||||||
@State var preview: LinkViewRepresentable? = nil
|
|
||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
|
|
||||||
|
@State var preview: LinkViewRepresentable? = nil
|
||||||
|
|
||||||
func MainContent() -> some View {
|
func MainContent() -> some View {
|
||||||
return VStack(alignment: .leading) {
|
return VStack(alignment: .leading) {
|
||||||
Text(Markdown.parse(content: artifacts.content))
|
Text(artifacts.content)
|
||||||
.font(eventviewsize_to_font(size))
|
.font(eventviewsize_to_font(size))
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
|
if size == .selected {
|
||||||
|
TranslateView(damus_state: damus_state, event: event, size: size)
|
||||||
|
}
|
||||||
|
|
||||||
if show_images && artifacts.images.count > 0 {
|
if show_images && artifacts.images.count > 0 {
|
||||||
ImageCarousel(urls: artifacts.images)
|
ImageCarousel(urls: artifacts.images)
|
||||||
@@ -87,11 +42,11 @@ struct NoteContentView: View {
|
|||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
if artifacts.invoices.count > 0 {
|
if artifacts.invoices.count > 0 {
|
||||||
InvoicesView(invoices: artifacts.invoices)
|
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
|
||||||
}
|
}
|
||||||
|
|
||||||
if show_images, self.preview != nil {
|
if let preview = self.preview, show_images {
|
||||||
self.preview
|
preview
|
||||||
} else {
|
} else {
|
||||||
ForEach(artifacts.links, id:\.self) { link in
|
ForEach(artifacts.links, id:\.self) { link in
|
||||||
if let url = link {
|
if let url = link {
|
||||||
@@ -106,16 +61,16 @@ struct NoteContentView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
MainContent()
|
MainContent()
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: privkey)
|
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||||
let profile = notif.object as! ProfileUpdate
|
let profile = notif.object as! ProfileUpdate
|
||||||
let blocks = event.blocks(privkey)
|
let blocks = event.blocks(damus_state.keypair.privkey)
|
||||||
for block in blocks {
|
for block in blocks {
|
||||||
switch block {
|
switch block {
|
||||||
case .mention(let m):
|
case .mention(let m):
|
||||||
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
|
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
|
||||||
self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: privkey)
|
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||||
}
|
}
|
||||||
case .text: return
|
case .text: return
|
||||||
case .hashtag: return
|
case .hashtag: return
|
||||||
@@ -125,7 +80,7 @@ struct NoteContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
if let preview = previews.lookup(self.event.id) {
|
if let preview = damus_state.previews.lookup(self.event.id) {
|
||||||
switch preview {
|
switch preview {
|
||||||
case .value(let view):
|
case .value(let view):
|
||||||
self.preview = view
|
self.preview = view
|
||||||
@@ -139,13 +94,13 @@ struct NoteContentView: View {
|
|||||||
let meta = await getMetaData(for: artifacts.links.first!)
|
let meta = await getMetaData(for: artifacts.links.first!)
|
||||||
|
|
||||||
let view = meta.map { LinkViewRepresentable(meta: .linkmeta($0)) }
|
let view = meta.map { LinkViewRepresentable(meta: .linkmeta($0)) }
|
||||||
previews.store(evid: self.event.id, preview: view)
|
damus_state.previews.store(evid: self.event.id, preview: view)
|
||||||
self.preview = view
|
self.preview = view
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func getMetaData(for url: URL) async -> LPLinkMetadata? {
|
func getMetaData(for url: URL) async -> LPLinkMetadata? {
|
||||||
// iOS 15 is crashing for some reason
|
// iOS 15 is crashing for some reason
|
||||||
guard #available(iOS 16, *) else {
|
guard #available(iOS 16, *) else {
|
||||||
@@ -162,29 +117,107 @@ struct NoteContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashtag_str(_ htag: String) -> String {
|
func hashtag_str(_ htag: String) -> AttributedString {
|
||||||
return "[#\(htag)](nostr:t:\(htag))"
|
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 {
|
switch m.type {
|
||||||
case .pubkey:
|
case .pubkey:
|
||||||
let pk = m.ref.ref_id
|
let pk = m.ref.ref_id
|
||||||
let profile = profiles.lookup(id: pk)
|
let profile = profiles.lookup(id: pk)
|
||||||
let disp = Profile.displayName(profile: profile, pubkey: 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:
|
case .event:
|
||||||
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct NoteContentView_Previews: PreviewProvider {
|
struct NoteContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let state = test_damus_state()
|
let state = test_damus_state()
|
||||||
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
|
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)
|
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, artifacts: artifacts, size: .normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func translate_button_style() -> some View {
|
||||||
|
return self
|
||||||
|
.font(.footnote)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.padding([.top, .bottom], 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NoteArtifacts {
|
||||||
|
let content: AttributedString
|
||||||
|
let images: [URL]
|
||||||
|
let invoices: [Invoice]
|
||||||
|
let links: [URL]
|
||||||
|
|
||||||
|
static func just_content(_ content: String) -> NoteArtifacts {
|
||||||
|
NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
|
||||||
|
let blocks = ev.blocks(privkey)
|
||||||
|
return render_blocks(blocks: blocks, profiles: profiles, privkey: privkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> NoteArtifacts {
|
||||||
|
var invoices: [Invoice] = []
|
||||||
|
var img_urls: [URL] = []
|
||||||
|
var link_urls: [URL] = []
|
||||||
|
let txt: AttributedString = blocks.reduce("") { str, block in
|
||||||
|
switch block {
|
||||||
|
case .mention(let m):
|
||||||
|
return str + mention_str(m, profiles: profiles)
|
||||||
|
case .text(let 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
|
||||||
|
img_urls.append(url)
|
||||||
|
return str
|
||||||
|
} else {
|
||||||
|
link_urls.append(url)
|
||||||
|
return str + url_str(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_image_url(_ url: URL) -> Bool {
|
||||||
|
let str = url.lastPathComponent.lowercased()
|
||||||
|
return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,8 +34,7 @@ func PostButton(action: @escaping () -> ()) -> some View {
|
|||||||
.keyboardShortcut("n", modifiers: [.command, .shift])
|
.keyboardShortcut("n", modifiers: [.command, .shift])
|
||||||
}
|
}
|
||||||
|
|
||||||
func PostButtonContainer(userSettings: UserSettingsStore, action: @escaping () -> Void) -> some View {
|
func PostButtonContainer(is_left_handed: Bool, action: @escaping () -> Void) -> some View {
|
||||||
let is_left_handed = userSettings.left_handed.self
|
|
||||||
return VStack {
|
return VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
|
|||||||
|
|
||||||
struct PostView: View {
|
struct PostView: View {
|
||||||
@State var post: String = ""
|
@State var post: String = ""
|
||||||
|
|
||||||
let replying_to: NostrEvent?
|
|
||||||
@FocusState var focus: Bool
|
@FocusState var focus: Bool
|
||||||
|
|
||||||
|
let replying_to: NostrEvent?
|
||||||
let references: [ReferencedId]
|
let references: [ReferencedId]
|
||||||
|
let damus_state: DamusState
|
||||||
|
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ struct PostView: View {
|
|||||||
TextEditor(text: $post)
|
TextEditor(text: $post)
|
||||||
.focused($focus)
|
.focused($focus)
|
||||||
.textInputAutocapitalization(.sentences)
|
.textInputAutocapitalization(.sentences)
|
||||||
|
|
||||||
if post.isEmpty {
|
if post.isEmpty {
|
||||||
Text(POST_PLACEHOLDER)
|
Text(POST_PLACEHOLDER)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
@@ -82,6 +84,14 @@ struct PostView: View {
|
|||||||
.allowsHitTesting(false)
|
.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() {
|
.onAppear() {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
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())
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,13 +33,23 @@ func pfp_line_width(_ h: Highlight) -> CGFloat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct InnerProfilePicView: View {
|
struct InnerProfilePicView: View {
|
||||||
let url: URL?
|
|
||||||
let fallbackUrl: URL?
|
|
||||||
let pubkey: String
|
let pubkey: String
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
let highlight: Highlight
|
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 {
|
var PlaceholderColor: Color {
|
||||||
return id_to_color(pubkey)
|
return id_to_color(pubkey)
|
||||||
@@ -57,10 +67,12 @@ struct InnerProfilePicView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
Color(uiColor: .systemBackground)
|
Color(uiColor: .systemBackground)
|
||||||
|
|
||||||
KFAnimatedImage(url)
|
KFAnimatedImage(imageModel.url)
|
||||||
.callbackQueue(.dispatch(.global(qos: .background)))
|
.callbackQueue(.dispatch(.global(qos: .background)))
|
||||||
.processingQueue(.dispatch(.global(qos: .background)))
|
.processingQueue(.dispatch(.global(qos: .background)))
|
||||||
.appendProcessor(LargeImageProcessor.shared)
|
.serialize(by: imageModel.serializer)
|
||||||
|
.setProcessor(imageModel.processor)
|
||||||
|
.cacheOriginalImage()
|
||||||
.configure { view in
|
.configure { view in
|
||||||
view.framePreloadCount = 1
|
view.framePreloadCount = 1
|
||||||
}
|
}
|
||||||
@@ -71,42 +83,13 @@ struct InnerProfilePicView: View {
|
|||||||
.loadDiskFileSynchronously()
|
.loadDiskFileSynchronously()
|
||||||
.fade(duration: 0.1)
|
.fade(duration: 0.1)
|
||||||
.onFailure { _ in
|
.onFailure { _ in
|
||||||
setFallbackImage()
|
imageModel.downloadFailed()
|
||||||
}
|
}
|
||||||
|
.id(imageModel.refreshID)
|
||||||
}
|
}
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
.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 {
|
func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> URL {
|
||||||
let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
|
let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
|
||||||
if let url = URL(string: pic) {
|
if let url = URL(string: pic) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user