Compare commits
177 Commits
tyiu/trans
...
tyiu/selec
| Author | SHA1 | Date | |
|---|---|---|---|
|
d10e5a87de
|
|||
|
|
59211bb4fd | ||
|
|
6d634763c5 | ||
|
|
49cf56f4c2 | ||
|
|
98c7bf5afc | ||
| 70a7239cfd | |||
|
|
0e83632896 | ||
|
|
f0df4aa218 | ||
|
|
bb9fc6f905 | ||
| f69e0c660a | |||
|
|
9089246b6b | ||
|
|
237c939639 | ||
|
|
4f86361b63 | ||
|
|
209ad71ff3 | ||
|
|
c728850524 | ||
| bc638f79f6 | |||
|
|
47a6f7ff38 | ||
|
|
6653798d27 | ||
|
|
e9ea96ffb6 | ||
|
59ccde9c38
|
|||
|
f9be7b166c
|
|||
|
|
4f2bacfaab | ||
|
|
5a8b29b5cc | ||
|
|
679c0ac424 | ||
|
|
48050f5e69 | ||
|
|
b5c967e161 | ||
|
|
715d4aa35d | ||
|
|
4b54278378 | ||
|
|
6e700e5726 | ||
|
|
18ad113198 | ||
|
|
7415671900 | ||
|
|
d5b6d935e8 | ||
| 543fd67f35 | |||
|
|
c24689d3ef | ||
| bf7120dc08 | |||
|
|
dbe938ad9b | ||
|
|
289d55b918 | ||
|
|
771fa845e3 | ||
|
|
5f1545b86a | ||
|
|
fe444228e6 | ||
|
|
10596ddb09 | ||
|
|
989684cd37 | ||
|
|
9ab03034a2 | ||
| c602c754f8 | |||
|
c5db887ab1
|
|||
|
|
0563ec8bf8 | ||
|
|
29c4170833 | ||
|
|
3c629621eb | ||
|
|
09f12845c0 | ||
|
|
b882a96206 | ||
|
|
fb82cc0531 | ||
|
|
90cd48ead7 | ||
|
|
f71b67f036 | ||
|
4406e44424
|
|||
|
|
ae6608cf7d | ||
|
|
04759107a2 | ||
|
|
552402f2b5 | ||
| 852609ee30 | |||
|
|
1e44d97a97 | ||
| 567303e680 | |||
| 7d1bac4028 | |||
|
|
eae844e081 | ||
|
|
140b0e4fc4 | ||
| 0b476faff7 | |||
|
|
53ec89551b | ||
|
|
638052492d | ||
|
|
45f8c37498 | ||
|
|
f96ad99790 | ||
|
|
1f79c20973 | ||
| e8b23daa3d | |||
|
|
a2eb77a5e9 | ||
|
|
29a8206586 | ||
|
|
07676a1f95 | ||
|
|
79ca3b2262 | ||
|
|
ba8425dedb | ||
|
|
4faf63f29d | ||
|
|
84ad0e03d0 | ||
|
|
7d3d23def3 | ||
|
|
ac1a5d237e | ||
|
|
cfcd799d63 | ||
|
|
351b32308f | ||
|
|
5a4299edaa | ||
|
|
99b619e011 | ||
|
|
d5ee9e4780 | ||
|
|
ced5b4974f | ||
| 9be55b08fd | |||
|
|
ac5f39a922 | ||
|
|
0e9691ae7a | ||
| 1441d339a7 | |||
|
|
2517132041 | ||
|
|
71acb16387 | ||
|
|
9e2e8595e8 | ||
|
|
1a2e9464af | ||
|
|
63dd39c7e4 | ||
|
|
40be9885c5 | ||
|
|
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 |
79
CHANGELOG.md
79
CHANGELOG.md
@@ -1,8 +1,78 @@
|
||||
## [1.0.0-15] - 2023-02-10
|
||||
|
||||
### Added
|
||||
|
||||
- Relay Filtering (William Casarin)
|
||||
- Japanese translations (Terry Yiu)
|
||||
- Add password autofill on account login and creation (Terry Yiu)
|
||||
- Show if relay is paid (William Casarin)
|
||||
- Add "Follows You" indicator on profile (William Casarin)
|
||||
- Add screen to select individual relays when posting/broadcasting (Andrii Sievrikov)
|
||||
- Relay Detail View (Joel Klabo)
|
||||
- Warn when attempting to post an nsec key (Terry Yiu)
|
||||
- DeepL translation integration (Terry Yiu)
|
||||
- Use local authentication (faceid) to access private key (Andrii Sievrikov)
|
||||
- Add accessibility labels to action bar (Bryan Montz)
|
||||
- Copy invoice button (Joel Klabo)
|
||||
- Receive Lightning Zaps (William Casarin)
|
||||
- Allow text selection in bio (Suhail Saqan)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Show "Follow Back" button on profile page (William Casarin)
|
||||
- When on your profile page, open relay view instead for your own relays (Terry Yiu)
|
||||
- Updated QR code view, include profile image, etc (ericholguin)
|
||||
- Clicking relay numbers now goes to relay config (radixrat)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Load zaps, likes and reposts when you open a thread (William Casarin)
|
||||
- Fix bug where sidebar navigation fails to pop when switching timelines (William Casarin)
|
||||
- Use lnaddress before lnurl for tip addresses to avoid Anigma scamming (William Casarin)
|
||||
- Fix sidebar navigation bugs (OlegAba)
|
||||
- Fix issue where navigation fails pop to root when switching timelines (William Casarin)
|
||||
- Make @ mentions case insensitive (William Casarin)
|
||||
- Fix some lnurls not getting decoded properly (William Casarin)
|
||||
- Hide incoming DMs from blocked users (William Casarin)
|
||||
- Hide blocked users from search results (William Casarin)
|
||||
- Fix Cash App invoice payments (Rob Seward)
|
||||
- DM Padding (OlegAba)
|
||||
- Check for broken lnurls (William Casarin)
|
||||
|
||||
|
||||
|
||||
[1.0.0-15]: https://github.com/damus-io/damus/releases/tag/v1.0.0-15
|
||||
## [1.0.0-13] - 2023-01-30
|
||||
|
||||
### Added
|
||||
|
||||
- LibreTranslate note translations (Terry Yiu)
|
||||
- Added support for account deletion (William Casarin)
|
||||
- User tagging and autocompletion in posts (Swift)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove redundant logout button from settings (Jonathan Milligan)
|
||||
- Moved relay config to its own sidebar entry (William Casarin)
|
||||
- New stylized tabs (ericholguin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix hidden profile action sheet when clicking ... (William Casarin)
|
||||
- Fixed height of DM input (Terry Yiu)
|
||||
- Fixed bug where copying pubkey from context menu only copied your own pubkey (Terry Yiu)
|
||||
|
||||
|
||||
|
||||
[1.0.0-13]: https://github.com/damus-io/damus/releases/tag/v1.0.0-13
|
||||
## [1.0.0-12] - 2023-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- Added arabic and portugese translations (William Casarin)
|
||||
- Added Arabic and Portuguese translations (Barodane, Antonio Chagas)
|
||||
- Add QRCode view for sharing your pubkey (ericholguin)
|
||||
- Added nostr: uri handling (William Casarin)
|
||||
|
||||
@@ -29,7 +99,7 @@
|
||||
### Added
|
||||
|
||||
- Reposts view (Terry Yiu)
|
||||
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (William Casarin)
|
||||
- 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)
|
||||
@@ -56,7 +126,7 @@
|
||||
|
||||
- Show website on profiles (William Casarin)
|
||||
- Add the ability to choose participants when replying (Joel Klabo)
|
||||
- Translations for de_AT, de_DE, tr_TR, fr_FR (William Casarin)
|
||||
- Translations for de_AT, de_DE, tr_TR, fr_FR (Gregor, Peter Gerstbach, Taylan Benli, Solobalbo)
|
||||
- Add DM Message Requests (William Casarin)
|
||||
|
||||
|
||||
@@ -76,7 +146,7 @@
|
||||
|
||||
- Drastically improved image viewer (OlegAba)
|
||||
- Added pinch to zoom on images (Swift)
|
||||
- Add Latin American Spanish translations (William Casarin)
|
||||
- Add Latin American Spanish translations (Nicolás Valencia)
|
||||
- Added SVG profile picture support (OlegAba)
|
||||
|
||||
|
||||
@@ -489,4 +559,3 @@
|
||||
|
||||
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
|
||||
|
||||
|
||||
|
||||
43
README.md
43
README.md
@@ -1,3 +1,4 @@
|
||||
[](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml)
|
||||
|
||||
# damus
|
||||
|
||||
@@ -25,7 +26,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
||||
## Getting Started on Damus
|
||||
|
||||
### Damus iOS
|
||||
1) Get the Damus app on TestFlight: https://testflight.apple.com/join/CLwjLxWl
|
||||
1) Get the Damus app on the iOS App Store: https://apps.apple.com/ca/app/damus/id1628663131
|
||||
|
||||
#### ⚙️ Settings (gear icon, top right)
|
||||
- Relays: You can add more relays to send your notes to by tapping the "+".
|
||||
@@ -48,7 +49,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
||||
4. Add @ direcly followed by the pubkey (e.g., `@npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s`)
|
||||
- You can also long-press a Note to grab their User ID aka pubkey or Note ID to link directly to a Note.
|
||||
- Currently you can't delete your Notes in the iOS app
|
||||
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `(https://i.ibb.co/2SHZbwm/alpha60.jpg)`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
|
||||
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `https://i.ibb.co/2SHZbwm/alpha60.jpg`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
|
||||
- Engaging with Notes
|
||||
- 💬 Replying to a Note: Tap the chat icon underneath the note. This will show up in the users’ notifications and in your 🏠 Personal and 🔍 Global Feeds
|
||||
- ♺ Reposts: Tap the repost icon which will show up in your 🏠 Personal and 🔍 Global Feeds
|
||||
@@ -91,15 +92,41 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributors welcome! [Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on github as well.
|
||||
Contributors welcome!
|
||||
|
||||
### Code
|
||||
|
||||
[Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on GitHub as well.
|
||||
|
||||
[git-send-email]: http://git-send-email.io
|
||||
|
||||
## git log bot
|
||||
### Translations
|
||||
|
||||
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
|
||||
Translators welcome! Join the [Transifex][transifex] project.
|
||||
|
||||
### Awards
|
||||
All user-facing strings must have a comment in order to provide context to translators. If a SwiftUI component has a `comment` parameter, use that. Otherwise, wrap your string with `NSLocalizedString` with the `comment` field populated.
|
||||
|
||||
[transifex]: https://explore.transifex.com/damus/damus-ios/
|
||||
|
||||
#### Export Source Translations
|
||||
|
||||
If user-facing strings have been added or changed, please export them for translation as part of your pull request or commit by running:
|
||||
|
||||
```zsh
|
||||
./devtools/export-source-translation.sh
|
||||
```
|
||||
|
||||
This command will export source translations to `translations/en-US.xcloc/Localized Contents/en-US.xliff`, which the Transifex integration will read from the `master` branch and allow translators to translate those strings.
|
||||
|
||||
#### Import Translations
|
||||
|
||||
Once 100% of strings have been translated for a given locale, Transifex will open up a pull request with the `translations/<locale>.xliff` file changed. Currently, it must be manually imported into the project before merging the pull request by running:
|
||||
|
||||
```zsh
|
||||
./devtools/import-translation.sh <locale_code_in_snake_case>
|
||||
```
|
||||
|
||||
### Awards
|
||||
|
||||
There may be nostr badges awarded for contributors in the future... :)
|
||||
|
||||
@@ -107,3 +134,7 @@ First contributors:
|
||||
|
||||
1. @randymcmillan
|
||||
2. @jcarucci27
|
||||
|
||||
### git log bot
|
||||
|
||||
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
|
||||
|
||||
@@ -26,6 +26,10 @@ static inline int is_boundary(char c) {
|
||||
return !isalnum(c);
|
||||
}
|
||||
|
||||
static inline int is_invalid_url_ending(char c) {
|
||||
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
|
||||
}
|
||||
|
||||
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
||||
{
|
||||
c->start = content;
|
||||
@@ -55,8 +59,8 @@ static int consume_until_whitespace(struct cursor *cur, int or_end) {
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_whitespace(c) && consumedAtLeastOne)
|
||||
return 1;
|
||||
if (is_whitespace(c))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
consumedAtLeastOne = true;
|
||||
@@ -221,6 +225,9 @@ static int parse_url(struct cursor *cur, struct block *block) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// strip any unwanted characters
|
||||
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
|
||||
|
||||
block->type = BLOCK_URL;
|
||||
block->block.str.start = (const char *)start;
|
||||
block->block.str.end = (const char *)cur->p;
|
||||
|
||||
@@ -15,9 +15,14 @@
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
|
||||
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
|
||||
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
|
||||
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
|
||||
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
|
||||
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
||||
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
|
||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
||||
@@ -27,8 +32,6 @@
|
||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
|
||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
|
||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
|
||||
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; };
|
||||
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
|
||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
||||
@@ -69,6 +72,8 @@
|
||||
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */; };
|
||||
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDB281DCE6100B3DE84 /* Liked.swift */; };
|
||||
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */; };
|
||||
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */; };
|
||||
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3D52B7298DB5C6001C5831 /* TextEvent.swift */; };
|
||||
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA63C28FF52D600C48A62 /* bolt11.c */; };
|
||||
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64028FF553900C48A62 /* hash_u5.c */; };
|
||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA64328FF558100C48A62 /* sha256.c */; };
|
||||
@@ -85,6 +90,7 @@
|
||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */; };
|
||||
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; };
|
||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
|
||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
|
||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
|
||||
@@ -116,6 +122,8 @@
|
||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
|
||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
|
||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
||||
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
|
||||
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
|
||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
|
||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
|
||||
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */; };
|
||||
@@ -128,7 +136,16 @@
|
||||
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88392296F798300DC99E7 /* ReactionsModel.swift */; };
|
||||
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */; };
|
||||
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88399297322D200DC99E7 /* DMTests.swift */; };
|
||||
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */; };
|
||||
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A72975FC1800DC99E7 /* Zaps.swift */; };
|
||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A9297612FF00DC99E7 /* ZapTests.swift */; };
|
||||
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
|
||||
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
|
||||
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
|
||||
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
|
||||
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
|
||||
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
|
||||
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
|
||||
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
|
||||
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; };
|
||||
@@ -151,6 +168,14 @@
|
||||
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; };
|
||||
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; };
|
||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; };
|
||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
|
||||
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
|
||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
|
||||
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */; };
|
||||
4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; };
|
||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; };
|
||||
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
|
||||
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8795A2996C47A00F758CC /* ZapsModel.swift */; };
|
||||
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
|
||||
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; };
|
||||
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */; };
|
||||
@@ -173,8 +198,10 @@
|
||||
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
|
||||
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
|
||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
|
||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
|
||||
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
|
||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
|
||||
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
|
||||
@@ -187,6 +214,8 @@
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
|
||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
|
||||
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; };
|
||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -216,29 +245,52 @@
|
||||
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5EA10F297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-AT"; path = "de-AT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A5EA110297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-AT"; path = "de-AT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A5EA111297CCF6C00569477 /* de-AT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "de-AT"; path = "de-AT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1F298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A93342929884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A93342A29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A93342B29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pl-PL"; path = "pl-PL.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
|
||||
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
|
||||
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
|
||||
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
|
||||
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
|
||||
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AB5B86B2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AB5B86C2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AB72AB8298ECF30004BB58C /* Translator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translator.swift; sourceTree = "<group>"; };
|
||||
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
|
||||
3AEABD20297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AEABD21297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AEABD22297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "de-DE"; path = "de-DE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
3AF6336829884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
|
||||
4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; };
|
||||
4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
@@ -250,8 +302,6 @@
|
||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
|
||||
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
|
||||
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
|
||||
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
||||
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
|
||||
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
||||
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
|
||||
@@ -292,6 +342,8 @@
|
||||
4C3BEFD9281DCA1400B3DE84 /* LikeCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeCounter.swift; sourceTree = "<group>"; };
|
||||
4C3BEFDB281DCE6100B3DE84 /* Liked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Liked.swift; sourceTree = "<group>"; };
|
||||
4C3BEFDF281DE1ED00B3DE84 /* DamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusState.swift; sourceTree = "<group>"; };
|
||||
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapEvent.swift; sourceTree = "<group>"; };
|
||||
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEvent.swift; sourceTree = "<group>"; };
|
||||
4C3EA63B28FF52D600C48A62 /* bolt11.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bolt11.h; sourceTree = "<group>"; };
|
||||
4C3EA63C28FF52D600C48A62 /* bolt11.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = bolt11.c; sourceTree = "<group>"; };
|
||||
4C3EA63E28FF54BD00C48A62 /* short_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = short_types.h; sourceTree = "<group>"; };
|
||||
@@ -337,6 +389,7 @@
|
||||
4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceTests.swift; sourceTree = "<group>"; };
|
||||
4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; };
|
||||
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; };
|
||||
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
|
||||
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
|
||||
4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; };
|
||||
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
|
||||
@@ -369,6 +422,8 @@
|
||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
|
||||
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; };
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
|
||||
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
|
||||
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
|
||||
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; };
|
||||
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = "<group>"; };
|
||||
@@ -381,7 +436,16 @@
|
||||
4CB88392296F798300DC99E7 /* ReactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsModel.swift; sourceTree = "<group>"; };
|
||||
4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
|
||||
4CB88399297322D200DC99E7 /* DMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMTests.swift; sourceTree = "<group>"; };
|
||||
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrlPayRequest.swift; sourceTree = "<group>"; };
|
||||
4CB883A72975FC1800DC99E7 /* Zaps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zaps.swift; sourceTree = "<group>"; };
|
||||
4CB883A9297612FF00DC99E7 /* ZapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapTests.swift; sourceTree = "<group>"; };
|
||||
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
|
||||
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
|
||||
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
|
||||
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
|
||||
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
|
||||
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
|
||||
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
|
||||
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
|
||||
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; };
|
||||
@@ -406,6 +470,14 @@
|
||||
4CE6DF0127F7A08200C66700 /* damusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITests.swift; sourceTree = "<group>"; };
|
||||
4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConnection.swift; sourceTree = "<group>"; };
|
||||
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
|
||||
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; };
|
||||
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatus.swift; sourceTree = "<group>"; };
|
||||
4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; };
|
||||
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; };
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; };
|
||||
4CE8795A2996C47A00F758CC /* ZapsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsModel.swift; sourceTree = "<group>"; };
|
||||
4CEE2AE72804F57C00AB5EEF /* libsecp256k1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libsecp256k1.a; sourceTree = "<group>"; };
|
||||
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRequest.swift; sourceTree = "<group>"; };
|
||||
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = "<group>"; };
|
||||
@@ -429,8 +501,10 @@
|
||||
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
|
||||
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
|
||||
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
|
||||
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
|
||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
||||
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
|
||||
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
|
||||
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
|
||||
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
||||
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
|
||||
@@ -442,6 +516,8 @@
|
||||
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
|
||||
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
|
||||
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; };
|
||||
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
|
||||
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
|
||||
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
|
||||
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -582,6 +658,11 @@
|
||||
7C45AE70297353390031D7BC /* KFImageModel.swift */,
|
||||
4CF0ABD32980996B00D66079 /* Report.swift */,
|
||||
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
|
||||
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
|
||||
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
|
||||
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
|
||||
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -589,6 +670,9 @@
|
||||
4C75EFA227FA576C0006080F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879562996C44A00F758CC /* Zaps */,
|
||||
4CB9D4A52992D01900A9A7E4 /* Profile */,
|
||||
4CAAD8AE29888A9B00060CEA /* Relays */,
|
||||
4CF0ABF42985CD4200D66079 /* Posting */,
|
||||
4CF0ABDF2981A83000D66079 /* Muting */,
|
||||
4CC7AAEE297F11B300430951 /* Events */,
|
||||
@@ -623,9 +707,6 @@
|
||||
4C8682862814DE470026224F /* ProfileView.swift */,
|
||||
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
|
||||
4C363A8B28236B92006E126D /* PubkeyView.swift */,
|
||||
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
|
||||
4C06670028FC7C5900038D2A /* RelayView.swift */,
|
||||
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */,
|
||||
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
|
||||
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
|
||||
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
|
||||
@@ -635,7 +716,6 @@
|
||||
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */,
|
||||
4C3AC7A02835A81400E1F516 /* SetupView.swift */,
|
||||
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */,
|
||||
4C0A3F96280F8E02000448DE /* ThreadView.swift */,
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
|
||||
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */,
|
||||
647D9A8C2968520300A295DE /* SideMenuView.swift */,
|
||||
@@ -646,6 +726,7 @@
|
||||
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
|
||||
3AA247FE297E3D900090C62D /* RepostsView.swift */,
|
||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
|
||||
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -673,8 +754,11 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879492995B58700F758CC /* Relays */,
|
||||
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
||||
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
|
||||
4C3A1D322960DB0500558C0F /* Markdown.swift */,
|
||||
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */,
|
||||
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
||||
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
|
||||
4C363A8328233689006E126D /* Parser.swift */,
|
||||
@@ -691,10 +775,30 @@
|
||||
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;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CAAD8AE29888A9B00060CEA /* Relays */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879532996BA0000F758CC /* Detail */,
|
||||
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */,
|
||||
4C06670028FC7C5900038D2A /* RelayView.swift */,
|
||||
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
|
||||
F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
|
||||
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
|
||||
4CE879512996B68900F758CC /* RelayType.swift */,
|
||||
);
|
||||
path = Relays;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CB88387296AF97C00DC99E7 /* ActionBar */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -712,6 +816,15 @@
|
||||
path = Reactions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CB9D4A52992D01900A9A7E4 /* Profile */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
|
||||
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CC7AAEE297F11B300430951 /* Events */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -723,6 +836,8 @@
|
||||
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */,
|
||||
4CC7AAF9297F64AC00430951 /* EventMenu.swift */,
|
||||
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
|
||||
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */,
|
||||
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */,
|
||||
);
|
||||
path = Events;
|
||||
sourceTree = "<group>";
|
||||
@@ -740,8 +855,11 @@
|
||||
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;
|
||||
sourceTree = "<group>";
|
||||
@@ -810,6 +928,8 @@
|
||||
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */,
|
||||
4CB88399297322D200DC99E7 /* DMTests.swift */,
|
||||
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
|
||||
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
|
||||
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -823,6 +943,31 @@
|
||||
path = damusUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CE879492995B58700F758CC /* Relays */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE8794729941DA700F758CC /* RelayFilters.swift */,
|
||||
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */,
|
||||
);
|
||||
path = Relays;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CE879532996BA0000F758CC /* Detail */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */,
|
||||
);
|
||||
path = Detail;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CE879562996C44A00F758CC /* Zaps */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */,
|
||||
);
|
||||
path = Zaps;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CEE2AE62804F57B00AB5EEF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -959,12 +1104,18 @@
|
||||
Base,
|
||||
"es-419",
|
||||
"en-US",
|
||||
"de-AT",
|
||||
"de-DE",
|
||||
"tr-TR",
|
||||
"fr-FR",
|
||||
"lv-LV",
|
||||
"it-IT",
|
||||
de,
|
||||
"pt-PT",
|
||||
"pl-PL",
|
||||
ar,
|
||||
nl,
|
||||
"zh-CN",
|
||||
"el-GR",
|
||||
ja,
|
||||
);
|
||||
mainGroup = 4CE6DEDA27F7A08100C66700;
|
||||
packageReferences = (
|
||||
@@ -1030,6 +1181,7 @@
|
||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
|
||||
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
|
||||
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
|
||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||
@@ -1045,20 +1197,25 @@
|
||||
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
|
||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
|
||||
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
|
||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
|
||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
|
||||
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
|
||||
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
|
||||
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
||||
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
|
||||
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
|
||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
||||
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
|
||||
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
|
||||
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
|
||||
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
|
||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
|
||||
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
|
||||
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
|
||||
@@ -1067,6 +1224,7 @@
|
||||
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
||||
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
|
||||
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
||||
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
||||
@@ -1077,12 +1235,15 @@
|
||||
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
|
||||
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
|
||||
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
|
||||
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
|
||||
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
|
||||
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
|
||||
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */,
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
||||
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
|
||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
|
||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
|
||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
||||
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
|
||||
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
|
||||
@@ -1090,6 +1251,7 @@
|
||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
|
||||
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
|
||||
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
|
||||
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
|
||||
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
|
||||
@@ -1098,29 +1260,35 @@
|
||||
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
|
||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
||||
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
|
||||
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
|
||||
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
|
||||
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
|
||||
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
|
||||
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
|
||||
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
|
||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
|
||||
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
|
||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
|
||||
4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
|
||||
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
|
||||
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
|
||||
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
||||
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
|
||||
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
|
||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
|
||||
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
|
||||
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */,
|
||||
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */,
|
||||
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */,
|
||||
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */,
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
|
||||
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
|
||||
@@ -1150,27 +1318,35 @@
|
||||
4C06670E28FDEAA000038D2A /* utf8.c in Sources */,
|
||||
4C3EA66D28FF782800C48A62 /* amount.c in Sources */,
|
||||
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
|
||||
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */,
|
||||
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
|
||||
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */,
|
||||
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */,
|
||||
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
|
||||
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
||||
4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */,
|
||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
|
||||
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
|
||||
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
|
||||
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
|
||||
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
|
||||
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
|
||||
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
|
||||
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
|
||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
|
||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
||||
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
|
||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
||||
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
||||
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
|
||||
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
||||
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
|
||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
|
||||
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
|
||||
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */,
|
||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
|
||||
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||
@@ -1179,7 +1355,9 @@
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
|
||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
|
||||
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
|
||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1191,7 +1369,9 @@
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
|
||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
|
||||
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
|
||||
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
|
||||
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
|
||||
@@ -1229,12 +1409,18 @@
|
||||
children = (
|
||||
3A5C4575296A879E0032D398 /* es-419 */,
|
||||
3A2B8B0A296A8982009CC16D /* en-US */,
|
||||
3A5EA111297CCF6C00569477 /* de-AT */,
|
||||
3AEABD22297CCFA8003F2975 /* de-DE */,
|
||||
3AEB8005297CCEA900713A25 /* tr-TR */,
|
||||
3A4F3322297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A06297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C22297F2CF80090925E /* it-IT */,
|
||||
3AB5B86C2986D8A3006599D2 /* de */,
|
||||
3AF6336A29884C6B0005672A /* pt-PT */,
|
||||
3A93342B29884CA600D6A8F3 /* pl-PL */,
|
||||
3AC524F0298C000B00693EBF /* ar */,
|
||||
3A96D41C298DA94500388A2A /* nl */,
|
||||
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
|
||||
3A25EF152992DA5D008ABE69 /* el-GR */,
|
||||
3A66D929299472FA008B44F4 /* ja */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
@@ -1243,12 +1429,18 @@
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3ACB685B297633BC00C46468 /* es-419 */,
|
||||
3A5EA10F297CCF6C00569477 /* de-AT */,
|
||||
3AEABD20297CCFA8003F2975 /* de-DE */,
|
||||
3AEB8003297CCEA800713A25 /* tr-TR */,
|
||||
3A4F3320297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A04297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C20297F2CF80090925E /* it-IT */,
|
||||
3AB5B86A2986D8A3006599D2 /* de */,
|
||||
3AF6336829884C6B0005672A /* pt-PT */,
|
||||
3A93342929884CA600D6A8F3 /* pl-PL */,
|
||||
3AC524EE298C000B00693EBF /* ar */,
|
||||
3A96D41A298DA94500388A2A /* nl */,
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
|
||||
3A25EF132992DA5D008ABE69 /* el-GR */,
|
||||
3A66D927299472FA008B44F4 /* ja */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1257,12 +1449,18 @@
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3ACB685E297633BC00C46468 /* es-419 */,
|
||||
3A5EA110297CCF6C00569477 /* de-AT */,
|
||||
3AEABD21297CCFA8003F2975 /* de-DE */,
|
||||
3AEB8004297CCEA800713A25 /* tr-TR */,
|
||||
3A4F3321297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A05297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C21297F2CF80090925E /* it-IT */,
|
||||
3AB5B86B2986D8A3006599D2 /* de */,
|
||||
3AF6336929884C6B0005672A /* pt-PT */,
|
||||
3A93342A29884CA600D6A8F3 /* pl-PL */,
|
||||
3AC524EF298C000B00693EBF /* ar */,
|
||||
3A96D41B298DA94500388A2A /* nl */,
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
|
||||
3A25EF142992DA5D008ABE69 /* el-GR */,
|
||||
3A66D928299472FA008B44F4 /* ja */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1398,7 +1596,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1406,6 +1604,7 @@
|
||||
INFOPLIST_FILE = damus/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -1420,7 +1619,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -1439,7 +1638,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1447,6 +1646,7 @@
|
||||
INFOPLIST_FILE = damus/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -1461,7 +1661,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher",
|
||||
"state" : {
|
||||
"revision" : "017f94ccfdacabb1ae7f45b75b4217b24c06e6ac",
|
||||
"version" : "7.4.0"
|
||||
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
|
||||
"version" : "7.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
60
damus/Components/CustomPicker.swift
Normal file
60
damus/Components/CustomPicker.swift
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,84 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InvoiceView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.openURL) private var openURL
|
||||
let our_pubkey: String
|
||||
let invoice: Invoice
|
||||
@State var showing_select_wallet: Bool = false
|
||||
@State var copied = false
|
||||
|
||||
var CopyButton: some View {
|
||||
Button {
|
||||
copied = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
copied = false
|
||||
}
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
UIPasteboard.general.string = invoice.string
|
||||
} label: {
|
||||
if !copied {
|
||||
Image(systemName: "doc.on.clipboard")
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundColor(Color("DamusGreen"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var PayButton: some View {
|
||||
Button {
|
||||
if should_show_wallet_selector(our_pubkey) {
|
||||
showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: get_default_wallet(our_pubkey).model, invoice: invoice.string)
|
||||
}
|
||||
} label: {
|
||||
RoundedRectangle(cornerRadius: 20, style: .circular)
|
||||
.foregroundColor(colorScheme == .light ? .black : .white)
|
||||
.overlay {
|
||||
Text("Pay", comment: "Button to pay a Lightning invoice.")
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(colorScheme == .light ? .white : .black)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
|
||||
print("pay button tap")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.foregroundColor(.secondary.opacity(0.1))
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Label("", systemImage: "bolt.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
|
||||
Spacer()
|
||||
CopyButton
|
||||
}
|
||||
Divider()
|
||||
Text(invoice.description_string)
|
||||
Text(invoice.amount.amount_sats_str())
|
||||
.font(.title)
|
||||
PayButton
|
||||
.frame(height: 50)
|
||||
.zIndex(10.0)
|
||||
}
|
||||
.padding(30)
|
||||
}
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func open_with_wallet(wallet: Wallet.Model, invoice: String) {
|
||||
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url)
|
||||
@@ -28,68 +106,12 @@ func open_with_wallet(wallet: Wallet.Model, invoice: String) {
|
||||
}
|
||||
}
|
||||
|
||||
struct InvoiceView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
let invoice: Invoice
|
||||
@State var showing_select_wallet: Bool = false
|
||||
@ObservedObject var user_settings = UserSettingsStore()
|
||||
|
||||
var PayButton: some View {
|
||||
Button {
|
||||
if user_settings.show_wallet_selector {
|
||||
showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: user_settings.default_wallet.model, invoice: invoice.string)
|
||||
}
|
||||
} label: {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.foregroundColor(colorScheme == .light ? .black : .white)
|
||||
.overlay {
|
||||
Text("Pay", comment: "Button to pay a Lightning invoice.")
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(colorScheme == .light ? .white : .black)
|
||||
}
|
||||
}
|
||||
//.buttonStyle(.bordered)
|
||||
.onTapGesture {
|
||||
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.foregroundColor(.secondary.opacity(0.1))
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Label("", systemImage: "bolt.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
|
||||
}
|
||||
Divider()
|
||||
Text(invoice.description)
|
||||
Text(invoice.amount.amount_sats_str())
|
||||
.font(.title)
|
||||
PayButton
|
||||
.frame(height: 50)
|
||||
.zIndex(10.0)
|
||||
}
|
||||
.padding(30)
|
||||
}
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: invoice.string).environmentObject(user_settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let test_invoice = Invoice(description: "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 {
|
||||
static var previews: some View {
|
||||
InvoiceView(invoice: test_invoice)
|
||||
.frame(width: 200, height: 200)
|
||||
InvoiceView(our_pubkey: "", invoice: test_invoice)
|
||||
.frame(width: 300, height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct InvoicesView: View {
|
||||
let our_pubkey: String
|
||||
var invoices: [Invoice]
|
||||
|
||||
@State var open_sheet: Bool = false
|
||||
@@ -16,7 +17,7 @@ struct InvoicesView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
ForEach(invoices, id: \.string) { invoice in
|
||||
InvoiceView(invoice: invoice)
|
||||
InvoiceView(our_pubkey: our_pubkey, invoice: invoice)
|
||||
.tabItem {
|
||||
Text(invoice.string)
|
||||
}
|
||||
@@ -30,7 +31,7 @@ struct InvoicesView: View {
|
||||
|
||||
struct InvoicesView_Previews: PreviewProvider {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
148
damus/Components/TranslateView.swift
Normal file
148
damus/Components/TranslateView.swift
Normal file
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// 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
|
||||
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
|
||||
let originalBlocks = event.blocks(damus_state.keypair.privkey)
|
||||
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
|
||||
|
||||
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
|
||||
let languageRecognizer = NLLanguageRecognizer()
|
||||
languageRecognizer.processString(originalOnlyText)
|
||||
noteLanguage = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue ?? currentLanguage
|
||||
|
||||
if let lang = noteLanguage, noteLanguage != currentLanguage {
|
||||
// If the detected dominant language is a variant, remove the variant component and just take the language part as translation services typically only supports the variant-less language.
|
||||
if #available(iOS 16, *) {
|
||||
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
|
||||
} else {
|
||||
noteLanguage = NSLocale(localeIdentifier: lang).languageCode
|
||||
}
|
||||
}
|
||||
|
||||
guard let note_lang = noteLanguage else {
|
||||
noteLanguage = currentLanguage
|
||||
translated_note = nil
|
||||
checkingTranslationStatus = false
|
||||
return
|
||||
}
|
||||
|
||||
if note_lang != currentLanguage {
|
||||
do {
|
||||
// If the note language is different from our language, send a translation request.
|
||||
let translator = Translator(damus_state.settings)
|
||||
let originalContent = event.get_content(damus_state.keypair.privkey)
|
||||
translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage)
|
||||
|
||||
if originalContent == translated_note {
|
||||
// If the translation is the same as the original, don't bother showing it.
|
||||
noteLanguage = currentLanguage
|
||||
translated_note = nil
|
||||
}
|
||||
} catch {
|
||||
// If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack.
|
||||
noteLanguage = currentLanguage
|
||||
translated_note = nil
|
||||
}
|
||||
}
|
||||
|
||||
if let translated = translated_note {
|
||||
// Render translated note.
|
||||
let translatedBlocks = event.get_blocks(content: translated)
|
||||
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
}
|
||||
|
||||
checkingTranslationStatus = false
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TranslateView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ds = test_damus_state()
|
||||
TranslateView(damus_state: ds, event: test_event, size: .selected)
|
||||
}
|
||||
}
|
||||
131
damus/Components/ZapButton.swift
Normal file
131
damus/Components/ZapButton.swift
Normal file
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// ZapButton.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-01-17.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ZapButton: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let lnurl: String
|
||||
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
@State var zapping: Bool = false
|
||||
@State var invoice: String = ""
|
||||
@State var slider_value: Double = 0.0
|
||||
@State var slider_visible: Bool = false
|
||||
@State var showing_select_wallet: Bool = false
|
||||
|
||||
func send_zap() {
|
||||
guard let privkey = damus_state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
// Only take the first 10 because reasons
|
||||
let relays = Array(damus_state.pool.descriptors.prefix(10))
|
||||
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||
// TODO: gather comment?
|
||||
let content = ""
|
||||
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target)
|
||||
|
||||
zapping = true
|
||||
|
||||
Task {
|
||||
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
||||
if mpayreq == nil {
|
||||
mpayreq = await fetch_static_payreq(lnurl)
|
||||
}
|
||||
|
||||
guard let payreq = mpayreq else {
|
||||
// TODO: show error
|
||||
DispatchQueue.main.async {
|
||||
zapping = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||
}
|
||||
|
||||
let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else {
|
||||
DispatchQueue.main.async {
|
||||
zapping = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
zapping = false
|
||||
|
||||
if should_show_wallet_selector(damus_state.pubkey) {
|
||||
self.invoice = inv
|
||||
self.showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//damus_state.pool.send(.event(zapreq))
|
||||
}
|
||||
|
||||
var zap_img: String {
|
||||
if bar.zapped {
|
||||
return "bolt.fill"
|
||||
}
|
||||
|
||||
if !zapping {
|
||||
return "bolt"
|
||||
}
|
||||
|
||||
return "bolt.horizontal.fill"
|
||||
}
|
||||
|
||||
var zap_color: Color? {
|
||||
if bar.zapped {
|
||||
return Color.orange
|
||||
}
|
||||
|
||||
if !zapping {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Color.yellow
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
EventActionButton(img: zap_img, col: zap_color) {
|
||||
if bar.zapped {
|
||||
//notify(.delete, bar.our_tip)
|
||||
} else if !zapping {
|
||||
send_zap()
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||
|
||||
Text("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")")
|
||||
.offset(x: 22)
|
||||
.font(.footnote)
|
||||
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
|
||||
}
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct ZapButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, our_like: nil, our_boost: nil, our_zap: nil)
|
||||
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,14 @@
|
||||
|
||||
import SwiftUI
|
||||
import Starscream
|
||||
import Kingfisher
|
||||
|
||||
var BOOTSTRAP_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://eden.nostr.land",
|
||||
"wss://nostr.fmt.wiz.biz",
|
||||
"wss://relay.nostr.bg",
|
||||
"wss://nostr.oxtr.dev",
|
||||
"wss://relay.snort.social",
|
||||
"wss://offchain.pub",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.current.fyi",
|
||||
"wss://brb.io",
|
||||
]
|
||||
|
||||
@@ -28,12 +27,16 @@ enum Sheets: Identifiable {
|
||||
case post
|
||||
case report(ReportTarget)
|
||||
case reply(NostrEvent)
|
||||
case event(NostrEvent)
|
||||
case filter
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .report: return "report"
|
||||
case .post: return "post"
|
||||
case .reply(let ev): return "reply-" + ev.id
|
||||
case .event(let ev): return "event-" + ev.id
|
||||
case .filter: return "filter"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +76,7 @@ struct ContentView: View {
|
||||
@State var damus_state: DamusState? = nil
|
||||
@State var selected_timeline: Timeline? = .home
|
||||
@State var is_thread_open: Bool = false
|
||||
@State var is_deleted_account: Bool = false
|
||||
@State var is_profile_open: Bool = false
|
||||
@State var event: NostrEvent? = nil
|
||||
@State var active_profile: String? = nil
|
||||
@@ -88,7 +92,6 @@ struct ContentView: View {
|
||||
@State var filter_state : FilterState = .posts_and_replies
|
||||
@State private var isSideBarOpened = false
|
||||
@StateObject var home: HomeModel = HomeModel()
|
||||
@StateObject var user_settings = UserSettingsStore()
|
||||
|
||||
// connect retry timer
|
||||
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
|
||||
@@ -111,7 +114,7 @@ struct ContentView: View {
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
|
||||
if privkey != nil {
|
||||
PostButtonContainer(userSettings: user_settings) {
|
||||
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
|
||||
self.active_sheet = .post
|
||||
}
|
||||
}
|
||||
@@ -119,9 +122,10 @@ struct ContentView: View {
|
||||
}
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
FiltersView
|
||||
//.frame(maxWidth: 275)
|
||||
.padding()
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
|
||||
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
|
||||
})
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
@@ -137,13 +141,33 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
func popToRoot() {
|
||||
profile_open = false
|
||||
thread_open = false
|
||||
search_open = false
|
||||
isSideBarOpened = false
|
||||
}
|
||||
|
||||
var timelineNavItem: some View {
|
||||
VStack {
|
||||
switch selected_timeline {
|
||||
case .home:
|
||||
Image("damus-home")
|
||||
.resizable()
|
||||
.frame(width:30,height:30)
|
||||
.shadow(color: Color("DamusPurple"), radius: 2)
|
||||
case .dms:
|
||||
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||
.bold()
|
||||
case .notifications:
|
||||
Text("Notifications", comment: "Toolbar label for Notifications view.")
|
||||
.bold()
|
||||
case .search:
|
||||
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
|
||||
.bold()
|
||||
case .none:
|
||||
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,9 +190,10 @@ struct ContentView: View {
|
||||
PostingTimelineView
|
||||
|
||||
case .notifications:
|
||||
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
|
||||
.navigationTitle(NSLocalizedString("Notifications", comment: "Navigation title for notifications."))
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
|
||||
}
|
||||
case .dms:
|
||||
DirectMessagesView(damus_state: damus_state!)
|
||||
.environmentObject(home.dms)
|
||||
@@ -180,24 +205,9 @@ struct ContentView: View {
|
||||
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Global", comment: "Navigation bar title for Global view where posts from all connected relay servers appear."), displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
switch selected_timeline {
|
||||
case .home:
|
||||
Image("damus-home")
|
||||
.resizable()
|
||||
.frame(width:30,height:30)
|
||||
.shadow(color: Color("DamusPurple"), radius: 2)
|
||||
case .dms:
|
||||
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||
.bold()
|
||||
case .notifications:
|
||||
Text("Notifications", comment: "Toolbar label for Notifications view.")
|
||||
.bold()
|
||||
case .search:
|
||||
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
|
||||
.bold()
|
||||
case .none:
|
||||
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
|
||||
}
|
||||
timelineNavItem
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.keyboard)
|
||||
@@ -206,7 +216,7 @@ struct ContentView: View {
|
||||
var MaybeSearchView: some View {
|
||||
Group {
|
||||
if let search = self.active_search {
|
||||
SearchView(appstate: damus_state!, search: SearchModel(pool: damus_state!.pool, search: search))
|
||||
SearchView(appstate: damus_state!, search: SearchModel(contacts: damus_state!.contacts, pool: damus_state!.pool, search: search))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -254,7 +264,7 @@ struct ContentView: View {
|
||||
if let damus = self.damus_state {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
VStack {
|
||||
TabView { // Prevents navbar appearance change on scroll
|
||||
MainContent(damus: damus)
|
||||
.toolbar() {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
@@ -262,29 +272,43 @@ struct ContentView: View {
|
||||
isSideBarOpened.toggle()
|
||||
} label: {
|
||||
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
.disabled(isSideBarOpened)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack(alignment: .center) {
|
||||
if home.signal.signal != home.signal.max_signal {
|
||||
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
|
||||
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
// maybe expand this to other timelines in the future
|
||||
if selected_timeline == .search {
|
||||
Button(action: {
|
||||
//isFilterVisible.toggle()
|
||||
self.active_sheet = .filter
|
||||
}) {
|
||||
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
|
||||
Label("Filter", systemImage: "line.3.horizontal.decrease")
|
||||
.foregroundColor(.gray)
|
||||
//.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Color.clear
|
||||
.overlay(
|
||||
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened)
|
||||
)
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
}
|
||||
.navigationBarHidden(isSideBarOpened ? true: false) // Would prefer a different way of doing this.
|
||||
.overlay(
|
||||
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
||||
)
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
@@ -294,7 +318,6 @@ struct ContentView: View {
|
||||
}
|
||||
.onAppear() {
|
||||
self.connect()
|
||||
//KingfisherManager.shared.cache.clearDiskCache()
|
||||
setup_notifications()
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
@@ -305,6 +328,17 @@ struct ContentView: View {
|
||||
PostView(replying_to: nil, references: [], damus_state: damus_state!)
|
||||
case .reply(let event):
|
||||
ReplyView(replying_to: event, damus: damus_state!)
|
||||
case .event(let event):
|
||||
EventDetailView()
|
||||
case .filter:
|
||||
let timeline = selected_timeline ?? .home
|
||||
if #available(iOS 16.0, *) {
|
||||
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||
.presentationDetents([.height(550)])
|
||||
.presentationDragIndicator(.visible)
|
||||
} else {
|
||||
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
@@ -348,6 +382,9 @@ struct ContentView: View {
|
||||
}
|
||||
.onReceive(handle_notify(.like)) { like in
|
||||
}
|
||||
.onReceive(handle_notify(.deleted_account)) { notif in
|
||||
self.is_deleted_account = true
|
||||
}
|
||||
.onReceive(handle_notify(.report)) { notif in
|
||||
let target = notif.object as! ReportTarget
|
||||
self.active_sheet = .report(target)
|
||||
@@ -420,6 +457,8 @@ struct ContentView: View {
|
||||
let post_res = obj.object as! NostrPostResult
|
||||
switch post_res {
|
||||
case .post(let post):
|
||||
//let post = tup.0
|
||||
//let to_relays = tup.1
|
||||
print("post \(post.content)")
|
||||
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
|
||||
self.damus_state?.pool.send(.event(new_ev))
|
||||
@@ -434,7 +473,13 @@ struct ContentView: View {
|
||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||
home.filter_muted()
|
||||
}
|
||||
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate "), isPresented: $user_blocked_confirm, actions: {
|
||||
.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
|
||||
}
|
||||
@@ -518,6 +563,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func switch_timeline(_ timeline: Timeline) {
|
||||
self.popToRoot()
|
||||
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
|
||||
|
||||
if timeline == self.selected_timeline {
|
||||
@@ -543,21 +589,33 @@ struct ContentView: View {
|
||||
|
||||
func connect() {
|
||||
let pool = RelayPool()
|
||||
let metadatas = RelayMetadatas()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in BOOTSTRAP_RELAYS {
|
||||
add_relay(pool, relay)
|
||||
if let url = URL(string: relay) {
|
||||
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
|
||||
}
|
||||
}
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
self.damus_state = DamusState(pool: pool, keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
tips: TipCounter(our_pubkey: pubkey),
|
||||
profiles: Profiles(),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache()
|
||||
self.damus_state = DamusState(pool: pool,
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
tips: TipCounter(our_pubkey: pubkey),
|
||||
profiles: Profiles(),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
lnurls: LNUrls(),
|
||||
settings: UserSettingsStore(),
|
||||
relay_filters: relay_filters,
|
||||
relay_metadata: metadatas,
|
||||
drafts: Drafts()
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
<string>zeusln</string>
|
||||
<string>zebedee</string>
|
||||
<string>lightning</string>
|
||||
<string>squarecash</string>
|
||||
<string>phoenix</string>
|
||||
<string>lnlink</string>
|
||||
<string>strike</string>
|
||||
|
||||
@@ -11,30 +11,43 @@ import Foundation
|
||||
class ActionBarModel: ObservableObject {
|
||||
@Published var our_like: NostrEvent?
|
||||
@Published var our_boost: NostrEvent?
|
||||
@Published var our_tip: NostrEvent?
|
||||
@Published var our_zap: Zap?
|
||||
@Published var likes: Int
|
||||
@Published var boosts: Int
|
||||
@Published var tips: Int64
|
||||
@Published var zaps: Int
|
||||
@Published var zap_total: Int64
|
||||
|
||||
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.boosts = boosts
|
||||
self.tips = tips
|
||||
self.zaps = zaps
|
||||
self.zap_total = zap_total
|
||||
self.our_like = our_like
|
||||
self.our_boost = our_boost
|
||||
self.our_tip = our_tip
|
||||
self.our_zap = our_zap
|
||||
}
|
||||
|
||||
func update(damus: DamusState, evid: String) {
|
||||
self.likes = damus.likes.counts[evid] ?? 0
|
||||
self.boosts = damus.boosts.counts[evid] ?? 0
|
||||
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
||||
self.zap_total = damus.zaps.event_totals[evid] ?? 0
|
||||
self.our_like = damus.likes.our_events[evid]
|
||||
self.our_boost = damus.boosts.our_events[evid]
|
||||
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
|
||||
var is_empty: Bool {
|
||||
return likes == 0 && boosts == 0 && tips == 0
|
||||
return likes == 0 && boosts == 0 && zaps == 0
|
||||
}
|
||||
|
||||
var tipped: Bool {
|
||||
return our_tip != nil
|
||||
var zapped: Bool {
|
||||
return our_zap != nil
|
||||
}
|
||||
|
||||
var liked: Bool {
|
||||
|
||||
@@ -18,17 +18,23 @@ struct DamusState {
|
||||
let profiles: Profiles
|
||||
let dms: DirectMessagesModel
|
||||
let previews: PreviewCache
|
||||
let zaps: Zaps
|
||||
let lnurls: LNUrls
|
||||
let settings: UserSettingsStore
|
||||
let relay_filters: RelayFilters
|
||||
let relay_metadata: RelayMetadatas
|
||||
let drafts: Drafts
|
||||
|
||||
var pubkey: String {
|
||||
return keypair.pubkey
|
||||
}
|
||||
|
||||
|
||||
var is_privkey_user: Bool {
|
||||
keypair.privkey != nil
|
||||
}
|
||||
|
||||
|
||||
static var empty: DamusState {
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache())
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts())
|
||||
}
|
||||
}
|
||||
|
||||
35
damus/Models/DeepLPlan.swift
Normal file
35
damus/Models/DeepLPlan.swift
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ class DirectMessageModel: ObservableObject {
|
||||
is_request = determine_is_request()
|
||||
}
|
||||
}
|
||||
|
||||
@Published var draft: String
|
||||
|
||||
var is_request: Bool
|
||||
var our_pubkey: String
|
||||
@@ -31,11 +33,13 @@ class DirectMessageModel: ObservableObject {
|
||||
self.events = events
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
}
|
||||
|
||||
init(our_pubkey: String) {
|
||||
self.events = []
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
}
|
||||
}
|
||||
|
||||
13
damus/Models/DraftsModel.swift
Normal file
13
damus/Models/DraftsModel.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// DraftsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 2/12/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Drafts: ObservableObject {
|
||||
@Published var post: String = ""
|
||||
@Published var replies: [NostrEvent: String] = [:]
|
||||
}
|
||||
@@ -51,11 +51,7 @@ class FollowersModel: ObservableObject {
|
||||
if has_contact.contains(ev.pubkey) {
|
||||
return
|
||||
}
|
||||
process_contact_event(
|
||||
pool: damus_state.pool,
|
||||
contacts: damus_state.contacts,
|
||||
pubkey: damus_state.pubkey, ev: ev
|
||||
)
|
||||
process_contact_event(state: damus_state, ev: ev)
|
||||
contacts?.append(ev.pubkey)
|
||||
has_contact.insert(ev.pubkey)
|
||||
}
|
||||
@@ -86,7 +82,7 @@ class FollowersModel: ObservableObject {
|
||||
if ev.known_kind == .contacts {
|
||||
handle_contact_event(ev)
|
||||
} 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):
|
||||
|
||||
@@ -60,7 +60,7 @@ class FollowingModel {
|
||||
switch nev {
|
||||
case .event(_, let ev):
|
||||
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):
|
||||
print("followingmodel notice: \(msg)")
|
||||
|
||||
@@ -112,9 +112,62 @@ class HomeModel: ObservableObject {
|
||||
handle_channel_create(ev)
|
||||
case .channel_meta:
|
||||
handle_channel_meta(ev)
|
||||
case .zap:
|
||||
handle_zap_event(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_pubkey: String, zapper: String) {
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.zaps.add_zap(zap: zap)
|
||||
|
||||
guard zap.target.pubkey == our_pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
if !insert_uniq_sorted_event(events: ¬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
|
||||
}
|
||||
|
||||
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
|
||||
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper)
|
||||
return
|
||||
}
|
||||
|
||||
guard let profile = damus_state.profiles.lookup(id: ptag) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let lnurl = profile.lnurl else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.damus_state.profiles.zappers[ptag] = zapper
|
||||
self.handle_zap_event_with_zapper(ev, our_pubkey: self.damus_state.pubkey, zapper: zapper)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handle_channel_create(_ ev: NostrEvent) {
|
||||
guard ev.is_valid else {
|
||||
return
|
||||
@@ -141,7 +194,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||
process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev)
|
||||
process_contact_event(state: self.damus_state, ev: ev)
|
||||
|
||||
if sub_id == init_subid {
|
||||
pool.send(.unsubscribe(init_subid), to: [relay_id])
|
||||
@@ -177,6 +230,7 @@ class HomeModel: ObservableObject {
|
||||
case .success(let n):
|
||||
let boosted = Counted(event: ev, id: e, total: n)
|
||||
notify(.boosted, boosted)
|
||||
notify(.update_stats, e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +248,7 @@ class HomeModel: ObservableObject {
|
||||
case .success(let n):
|
||||
let liked = Counted(event: ev, id: e.ref_id, total: n)
|
||||
notify(.liked, liked)
|
||||
notify(.update_stats, e.ref_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +289,7 @@ class HomeModel: ObservableObject {
|
||||
switch ev {
|
||||
case .event(let sub_id, let ev):
|
||||
// globally handle likes
|
||||
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
|
||||
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
|
||||
if !always_process {
|
||||
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
|
||||
return
|
||||
@@ -317,6 +372,7 @@ class HomeModel: ObservableObject {
|
||||
NostrKind.chat.rawValue,
|
||||
NostrKind.like.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
NostrKind.zap.rawValue,
|
||||
])
|
||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||
notifications_filter.limit = 100
|
||||
@@ -372,7 +428,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
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? {
|
||||
@@ -411,7 +467,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
||||
if should_hide_event(contacts: damus_state.contacts, ev: ev) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -423,7 +479,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
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) {
|
||||
if let notifs = handle_incoming_dm(contacts: damus_state.contacts, prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
|
||||
self.new_events = notifs
|
||||
}
|
||||
}
|
||||
@@ -530,10 +586,17 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
|
||||
DispatchQueue.main.async {
|
||||
notify(.deleted_account, ())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var old_nip05: String? = nil
|
||||
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||
@@ -583,31 +646,31 @@ func robohash(_ pk: String) -> String {
|
||||
return "https://robohash.org/" + pk
|
||||
}
|
||||
|
||||
func load_our_stuff(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
|
||||
guard ev.pubkey == pubkey else {
|
||||
func load_our_stuff(state: DamusState, ev: NostrEvent) {
|
||||
guard ev.pubkey == state.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
// only use new stuff
|
||||
if let current_ev = contacts.event {
|
||||
if let current_ev = state.contacts.event {
|
||||
guard ev.created_at > current_ev.created_at else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let m_old_ev = contacts.event
|
||||
contacts.event = ev
|
||||
let m_old_ev = state.contacts.event
|
||||
state.contacts.event = ev
|
||||
|
||||
load_our_contacts(contacts: contacts, our_pubkey: pubkey, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_relays(contacts: contacts, our_pubkey: pubkey, pool: pool, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_contacts(contacts: state.contacts, our_pubkey: state.pubkey, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||
}
|
||||
|
||||
func process_contact_event(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
|
||||
load_our_stuff(pool: pool, contacts: contacts, pubkey: pubkey, ev: ev)
|
||||
add_contact_if_friend(contacts: contacts, ev: ev)
|
||||
func process_contact_event(state: DamusState, ev: NostrEvent) {
|
||||
load_our_stuff(state: state, ev: ev)
|
||||
add_contact_if_friend(contacts: state.contacts, ev: ev)
|
||||
}
|
||||
|
||||
func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
let bootstrap_dict: [String: RelayInfo] = [:]
|
||||
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? BOOTSTRAP_RELAYS.reduce(into: bootstrap_dict) { (d, r) in
|
||||
d[r] = .rw
|
||||
@@ -631,14 +694,15 @@ func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_
|
||||
|
||||
let diff = old.symmetricDifference(new)
|
||||
|
||||
let new_relay_filters = load_relay_filters(state.pubkey) == nil
|
||||
for d in diff {
|
||||
changed = true
|
||||
if new.contains(d) {
|
||||
if let url = URL(string: d) {
|
||||
try? pool.add_relay(url, info: decoded[d] ?? .rw)
|
||||
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
|
||||
}
|
||||
} else {
|
||||
pool.remove_relay(d)
|
||||
state.pool.remove_relay(d)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +711,62 @@ 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? {
|
||||
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: URL, info: RelayInfo, new_relay_filters: Bool) {
|
||||
try? pool.add_relay(url, info: info)
|
||||
|
||||
let relay_id = url.absoluteString
|
||||
guard metadatas.lookup(relay_id: relay_id) == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
Task.detached(priority: .background) {
|
||||
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
metadatas.insert(relay_id: relay_id, metadata: meta)
|
||||
|
||||
// if this is the first time adding filters, we should filter non-paid relays
|
||||
if new_relay_filters && !meta.is_paid {
|
||||
relay_filters.insert(timeline: .search, relay_id: relay_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
|
||||
var urlString = relay_id.replacingOccurrences(of: "wss://", with: "https://")
|
||||
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
|
||||
|
||||
guard let url = URL(string: urlString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("application/nostr+json", forHTTPHeaderField: "Accept")
|
||||
|
||||
var res: (Data, URLResponse)? = nil
|
||||
|
||||
res = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let data = res?.0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let nip11 = try JSONDecoder().decode(RelayMetadata.self, from: data)
|
||||
return nip11
|
||||
}
|
||||
|
||||
func process_relay_metadata() {
|
||||
}
|
||||
|
||||
func handle_incoming_dm(contacts: Contacts, prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? {
|
||||
// hide blocked users
|
||||
guard should_show_event(contacts: contacts, ev: ev) else {
|
||||
return prev_events
|
||||
}
|
||||
|
||||
var inserted = false
|
||||
var found = false
|
||||
let ours = ev.pubkey == our_pubkey
|
||||
@@ -721,9 +840,10 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
|
||||
}
|
||||
|
||||
|
||||
func should_hide_event(contacts: Contacts, ev: NostrEvent) -> Bool {
|
||||
func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
|
||||
if contacts.is_muted(ev.pubkey) {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
return !ev.should_show_event
|
||||
return ev.should_show_event
|
||||
}
|
||||
|
||||
|
||||
41
damus/Models/LibreTranslateServer.swift
Normal file
41
damus/Models/LibreTranslateServer.swift
Normal file
@@ -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
|
||||
|
||||
enum CountResult {
|
||||
case already_counted
|
||||
case success(Int)
|
||||
}
|
||||
|
||||
class EventCounter {
|
||||
var counts: [String: Int] = [:]
|
||||
@@ -14,11 +18,6 @@ class EventCounter {
|
||||
var our_events: [String: NostrEvent] = [:]
|
||||
var our_pubkey: String
|
||||
|
||||
enum CountResult {
|
||||
case already_counted
|
||||
case success(Int)
|
||||
}
|
||||
|
||||
init (our_pubkey: String) {
|
||||
self.our_pubkey = our_pubkey
|
||||
}
|
||||
|
||||
@@ -32,13 +32,30 @@ struct IdBlock: Identifiable {
|
||||
let block: Block
|
||||
}
|
||||
|
||||
struct Invoice {
|
||||
let description: String
|
||||
let amount: Amount
|
||||
typealias Invoice = LightningInvoice<Amount>
|
||||
typealias ZapInvoice = LightningInvoice<Int64>
|
||||
|
||||
enum InvoiceDescription {
|
||||
case description(String)
|
||||
case description_hash(Data)
|
||||
}
|
||||
|
||||
struct LightningInvoice<T> {
|
||||
let description: InvoiceDescription
|
||||
let amount: T
|
||||
let string: String
|
||||
let expiry: UInt64
|
||||
let payment_hash: Data
|
||||
let created_at: UInt64
|
||||
|
||||
var description_string: String {
|
||||
switch description {
|
||||
case .description(let string):
|
||||
return string
|
||||
case .description_hash:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Block {
|
||||
@@ -189,20 +206,50 @@ enum Amount: Equatable {
|
||||
case .any:
|
||||
return NSLocalizedString("Any", comment: "Any amount of sats")
|
||||
case .specific(let amt):
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = .decimal
|
||||
numberFormatter.minimumFractionDigits = 0
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
numberFormatter.roundingMode = .down
|
||||
|
||||
let sats = NSNumber(value: (Double(amt) / 1000.0))
|
||||
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
|
||||
|
||||
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
|
||||
return format_msats(amt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func format_msats_abbrev(_ msats: Int64) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.positiveSuffix = "m"
|
||||
formatter.positivePrefix = ""
|
||||
formatter.minimumFractionDigits = 0
|
||||
formatter.maximumFractionDigits = 3
|
||||
formatter.roundingMode = .down
|
||||
formatter.roundingIncrement = 0.1
|
||||
formatter.multiplier = 1
|
||||
|
||||
let sats = NSNumber(value: (Double(msats) / 1000.0))
|
||||
|
||||
if msats >= 1_000_000*1000 {
|
||||
formatter.positiveSuffix = "m"
|
||||
formatter.multiplier = 0.000001
|
||||
} else if msats >= 1000*1000 {
|
||||
formatter.positiveSuffix = "k"
|
||||
formatter.multiplier = 0.001
|
||||
} else {
|
||||
return sats.stringValue
|
||||
}
|
||||
|
||||
return formatter.string(from: sats) ?? sats.stringValue
|
||||
}
|
||||
|
||||
func format_msats(_ msat: Int64) -> String {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = .decimal
|
||||
numberFormatter.minimumFractionDigits = 0
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
numberFormatter.roundingMode = .down
|
||||
|
||||
let sats = NSNumber(value: (Double(msat) / 1000.0))
|
||||
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
|
||||
|
||||
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
|
||||
}
|
||||
|
||||
func convert_invoice_block(_ b: invoice_block) -> Block? {
|
||||
guard let invstr = strblock_to_string(b.invstr) else {
|
||||
return nil
|
||||
@@ -212,9 +259,8 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var description = ""
|
||||
if b11.description != nil {
|
||||
description = String(cString: b11.description)
|
||||
guard let description = convert_invoice_description(b11: b11) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
|
||||
@@ -225,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))
|
||||
}
|
||||
|
||||
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||
if let desc = b11.description {
|
||||
return .description(String(cString: desc))
|
||||
}
|
||||
|
||||
if var deschash = maybe_pointee(b11.description_hash) {
|
||||
return .description_hash(Data(bytes: &deschash, count: 32))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
|
||||
{
|
||||
let ind = Int(ind)
|
||||
|
||||
@@ -20,6 +20,24 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
var sub_id = UUID().description
|
||||
var prof_subid = UUID().description
|
||||
|
||||
func follows(pubkey: String) -> Bool {
|
||||
guard let contacts = self.contacts else {
|
||||
return false
|
||||
}
|
||||
|
||||
for tag in contacts.tags {
|
||||
guard tag.count >= 2 && tag[0] == "p" else {
|
||||
continue
|
||||
}
|
||||
|
||||
if tag[1] == pubkey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func get_follow_target() -> FollowTarget {
|
||||
if let contacts = contacts {
|
||||
return .contact(contacts)
|
||||
@@ -70,7 +88,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
}
|
||||
|
||||
func handle_profile_contact_event(_ ev: NostrEvent) {
|
||||
process_contact_event(pool: damus.pool, contacts: damus.contacts, pubkey: damus.pubkey, ev: ev)
|
||||
process_contact_event(state: damus, ev: ev)
|
||||
|
||||
// only use new stuff
|
||||
if let current_ev = self.contacts {
|
||||
@@ -97,7 +115,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
} else if ev.known_kind == .contacts {
|
||||
handle_profile_contact_event(ev)
|
||||
} else if ev.known_kind == .metadata {
|
||||
process_metadata_event(profiles: damus.profiles, ev: ev)
|
||||
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||
}
|
||||
seen_event.insert(ev.id)
|
||||
}
|
||||
|
||||
@@ -31,12 +31,13 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func filter_muted() {
|
||||
events = events.filter { !should_hide_event(contacts: damus_state.contacts, ev: $0) }
|
||||
events = events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
loading = true
|
||||
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event)
|
||||
let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters)
|
||||
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
|
||||
}
|
||||
|
||||
func unsubscribe(to: String? = nil) {
|
||||
@@ -54,7 +55,7 @@ class SearchHomeModel: ObservableObject {
|
||||
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
|
||||
return
|
||||
}
|
||||
if ev.is_textlike && !should_hide_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
|
||||
if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
|
||||
if seen_pubkey.contains(ev.pubkey) {
|
||||
return
|
||||
}
|
||||
@@ -129,7 +130,7 @@ func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,14 +15,20 @@ class SearchModel: ObservableObject {
|
||||
|
||||
let pool: RelayPool
|
||||
var search: NostrFilter
|
||||
let contacts: Contacts
|
||||
let sub_id = UUID().description
|
||||
let limit: UInt32 = 500
|
||||
|
||||
init(pool: RelayPool, search: NostrFilter) {
|
||||
init(contacts: Contacts, pool: RelayPool, search: NostrFilter) {
|
||||
self.contacts = contacts
|
||||
self.pool = pool
|
||||
self.search = search
|
||||
}
|
||||
|
||||
func filter_muted() {
|
||||
self.events = self.events.filter { should_show_event(contacts: contacts, ev: $0) }
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
// since 1 month
|
||||
search.limit = self.limit
|
||||
@@ -47,6 +53,10 @@ class SearchModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(contacts: contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at } ) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
@@ -88,6 +98,21 @@ func event_matches_hashtag(_ ev: NostrEvent, hashtags: [String]) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func tag_is_hashtag(_ tag: [String]) -> Bool {
|
||||
// "hashtag" is deprecated, will remove in the future
|
||||
return tag.count >= 2 && (tag[0] == "hashtag" || tag[0] == "t")
|
||||
}
|
||||
|
||||
func has_hashtag(_ tags: [[String]], hashtag: String) -> Bool {
|
||||
for tag in tags {
|
||||
if tag_is_hashtag(tag) && tag[1] == hashtag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func event_matches_filter(_ ev: NostrEvent, filter: NostrFilter) -> Bool {
|
||||
if let hashtags = filter.hashtag {
|
||||
return event_matches_hashtag(ev, hashtags: hashtags)
|
||||
|
||||
@@ -190,7 +190,7 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
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 {
|
||||
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
|
||||
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {
|
||||
|
||||
37
damus/Models/TranslationService.swift
Normal file
37
damus/Models/TranslationService.swift
Normal file
@@ -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,76 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Vault
|
||||
|
||||
func should_show_wallet_selector(_ pubkey: String) -> Bool {
|
||||
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
|
||||
}
|
||||
|
||||
func pk_setting_key(_ pubkey: String, key: String) -> String {
|
||||
return "\(pubkey)_\(key)"
|
||||
}
|
||||
|
||||
func default_zap_setting_key(pubkey: String) -> String {
|
||||
return pk_setting_key(pubkey, key: "default_zap_amount")
|
||||
}
|
||||
|
||||
func set_default_zap_amount(pubkey: String, amount: Int) {
|
||||
let key = default_zap_setting_key(pubkey: pubkey)
|
||||
UserDefaults.standard.setValue(amount, forKey: key)
|
||||
}
|
||||
|
||||
func get_default_zap_amount(pubkey: String) -> Int? {
|
||||
let key = default_zap_setting_key(pubkey: pubkey)
|
||||
let amt = UserDefaults.standard.integer(forKey: key)
|
||||
if amt == 0 {
|
||||
return nil
|
||||
}
|
||||
return amt
|
||||
}
|
||||
|
||||
|
||||
func get_default_wallet(_ pubkey: String) -> Wallet {
|
||||
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
|
||||
let default_wallet = Wallet(rawValue: defaultWalletName)
|
||||
{
|
||||
return default_wallet
|
||||
} else {
|
||||
return .system_default_wallet
|
||||
}
|
||||
}
|
||||
|
||||
private func get_translation_service(_ pubkey: String) -> TranslationService? {
|
||||
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return TranslationService(rawValue: translation_service)
|
||||
}
|
||||
|
||||
private func get_deepl_plan(_ pubkey: String) -> DeepLPlan? {
|
||||
guard let server_name = UserDefaults.standard.string(forKey: "deepl_plan") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return DeepLPlan(rawValue: server_name)
|
||||
}
|
||||
|
||||
private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
|
||||
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return LibreTranslateServer(rawValue: server_name)
|
||||
}
|
||||
|
||||
private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
|
||||
if let url = server.model.url {
|
||||
return url
|
||||
}
|
||||
|
||||
return UserDefaults.standard.object(forKey: "libretranslate_url") as? String
|
||||
}
|
||||
|
||||
class UserSettingsStore: ObservableObject {
|
||||
@Published var default_wallet: Wallet {
|
||||
@@ -26,16 +96,154 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
|
||||
let default_wallet = Wallet(rawValue: defaultWalletName)
|
||||
{
|
||||
self.default_wallet = default_wallet
|
||||
} else {
|
||||
default_wallet = .system_default_wallet
|
||||
@Published var translation_service: TranslationService {
|
||||
didSet {
|
||||
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
|
||||
}
|
||||
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
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ enum Wallet: String, CaseIterable, Identifiable {
|
||||
return .init(index: 0, tag: "strike", displayName: NSLocalizedString("Strike", comment: "Dropdown option label for Lightning wallet, Strike."), link: "strike:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/strike-bitcoin-payments/id1488724463", image: "strike")
|
||||
case .cashapp:
|
||||
return .init(index: 1, tag: "cashapp", displayName: NSLocalizedString("Cash App", comment: "Dropdown option label for Lightning wallet, Cash App."), link: "squarecash://",
|
||||
return .init(index: 1, tag: "cashapp", displayName: NSLocalizedString("Cash App", comment: "Dropdown option label for Lightning wallet, Cash App."), link: "https://cash.app/launch/lightning/",
|
||||
appStoreLink: "https://apps.apple.com/us/app/cash-app/id711923939", image: "cashapp")
|
||||
case .muun:
|
||||
return .init(index: 2, tag: "muun", displayName: NSLocalizedString("Muun", comment: "Dropdown option label for Lightning wallet, Muun."), link: "muun:", appStoreLink: "https://apps.apple.com/us/app/muun-wallet/id1482037683", image: "muun")
|
||||
@@ -53,7 +53,7 @@ enum Wallet: String, CaseIterable, Identifiable {
|
||||
return .init(index: 3, tag: "bluewallet", displayName: NSLocalizedString("Blue Wallet", comment: "Dropdown option label for Lightning wallet, Blue Wallet."), link: "bluewallet:lightning:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
|
||||
case .walletofsatoshi:
|
||||
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet Of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet Of Satoshi."), link: "walletofsatoshi:lightning:",
|
||||
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet of Satoshi."), link: "walletofsatoshi:lightning:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
|
||||
case .zebedee:
|
||||
return .init(index: 5, tag: "zebedee", displayName: NSLocalizedString("Zebedee", comment: "Dropdown option label for Lightning wallet, Zebedee."), link: "zebedee:lightning:",
|
||||
|
||||
77
damus/Models/ZapsModel.swift
Normal file
77
damus/Models/ZapsModel.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// ZapsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ZapsModel: ObservableObject {
|
||||
let state: DamusState
|
||||
let target: ZapTarget
|
||||
var zaps: [Zap]
|
||||
|
||||
let zaps_subid = UUID().description
|
||||
|
||||
init(state: DamusState, target: ZapTarget) {
|
||||
self.state = state
|
||||
self.target = target
|
||||
self.zaps = []
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var filter = NostrFilter.filter_kinds([9735])
|
||||
switch target {
|
||||
case .profile(let profile_id):
|
||||
filter.pubkeys = [profile_id]
|
||||
case .note(let note_target):
|
||||
filter.referenced_ids = [note_target.note_id]
|
||||
}
|
||||
state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
state.pool.unsubscribe(sub_id: zaps_subid)
|
||||
}
|
||||
|
||||
func handle_event(relay_id: String, conn_ev: NostrConnectionEvent) {
|
||||
guard case .nostr_event(let resp) = conn_ev else {
|
||||
return
|
||||
}
|
||||
|
||||
guard resp.subid == zaps_subid else {
|
||||
return
|
||||
}
|
||||
|
||||
guard case .event(_, let ev) = resp else {
|
||||
return
|
||||
}
|
||||
|
||||
guard ev.kind == 9735 else {
|
||||
return
|
||||
}
|
||||
|
||||
if let zap = state.zaps.zaps[ev.id] {
|
||||
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
} else {
|
||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
|
||||
return
|
||||
}
|
||||
|
||||
state.zaps.add_zap(zap: zap)
|
||||
|
||||
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -24,18 +24,22 @@ struct Profile: Codable {
|
||||
}
|
||||
|
||||
private func str(_ str: String) -> String? {
|
||||
guard let val = self.value[str] else{
|
||||
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? String else {
|
||||
guard let s = val.value as? T else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
private mutating func set_str(_ key: String, _ val: String?) {
|
||||
private mutating func set_val<T>(_ key: String, _ val: T?) {
|
||||
if val == nil {
|
||||
self.value.removeValue(forKey: key)
|
||||
return
|
||||
@@ -44,6 +48,15 @@ struct Profile: Codable {
|
||||
self.value[key] = AnyCodable.init(val)
|
||||
}
|
||||
|
||||
private mutating func set_str(_ key: String, _ val: String?) {
|
||||
set_val(key, val)
|
||||
}
|
||||
|
||||
var deleted: Bool? {
|
||||
get { return get_val("deleted"); }
|
||||
set(s) { set_val("deleted", s) }
|
||||
}
|
||||
|
||||
var display_name: String? {
|
||||
get { return str("display_name"); }
|
||||
set(s) { set_str("display_name", s) }
|
||||
@@ -89,7 +102,7 @@ struct Profile: Codable {
|
||||
}
|
||||
|
||||
var lnurl: String? {
|
||||
guard let addr = lud06 ?? lud16 else {
|
||||
guard let addr = lud16 ?? lud06 else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -97,6 +110,10 @@ struct Profile: Codable {
|
||||
return lnaddress_to_lnurl(addr);
|
||||
}
|
||||
|
||||
if !addr.lowercased().hasPrefix("lnurl") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
@@ -109,6 +126,10 @@ struct Profile: Codable {
|
||||
return make_ln_url(self.lnurl)
|
||||
}
|
||||
|
||||
init() {
|
||||
self.value = [:]
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
self.value = try container.decode([String: AnyCodable].self)
|
||||
|
||||
@@ -11,6 +11,8 @@ import secp256k1
|
||||
import secp256k1_implementation
|
||||
import CryptoKit
|
||||
|
||||
|
||||
|
||||
enum ValidationResult: Decodable {
|
||||
case ok
|
||||
case bad_id
|
||||
@@ -79,7 +81,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
}
|
||||
|
||||
var too_big: Bool {
|
||||
return self.content.count > 32000
|
||||
return self.content.count > 16000
|
||||
}
|
||||
|
||||
var should_show_event: Bool {
|
||||
@@ -103,11 +105,15 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
if let bs = _blocks {
|
||||
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
|
||||
return blocks
|
||||
}
|
||||
|
||||
func get_blocks(content: String) -> [Block] {
|
||||
return parse_mentions(content: content, tags: self.tags)
|
||||
}
|
||||
|
||||
lazy var inner_event: NostrEvent? = {
|
||||
// don't try to deserialize an inner event if we know there won't be one
|
||||
if self.known_kind == .boost {
|
||||
@@ -364,9 +370,14 @@ func decode_nostr_event(txt: String) -> NostrResponse? {
|
||||
|
||||
func encode_json<T: Encodable>(_ val: T) -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .withoutEscapingSlashes
|
||||
return (try? encoder.encode(val)).map { String(decoding: $0, as: UTF8.self) }
|
||||
}
|
||||
|
||||
func decode_nostr_event_json(json: String) -> NostrEvent? {
|
||||
return decode_json(json)
|
||||
}
|
||||
|
||||
func decode_json<T: Decodable>(_ val: String) -> T? {
|
||||
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
|
||||
}
|
||||
@@ -567,6 +578,26 @@ func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> Nost
|
||||
return ev
|
||||
}
|
||||
|
||||
func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
|
||||
switch target {
|
||||
case .profile(let pk):
|
||||
return [["p", pk]]
|
||||
case .note(let note_target):
|
||||
return [["e", note_target.note_id], ["p", note_target.author]]
|
||||
}
|
||||
}
|
||||
|
||||
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent {
|
||||
var tags = zap_target_to_tags(target)
|
||||
var relay_tag = ["relays"]
|
||||
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
||||
tags.append(relay_tag)
|
||||
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags)
|
||||
ev.id = calculate_event_id(ev: ev)
|
||||
ev.sig = sign_event(privkey: privkey, ev: ev)
|
||||
return ev
|
||||
}
|
||||
|
||||
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
||||
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@ enum NostrKind: Int {
|
||||
case channel_meta = 41
|
||||
case chat = 42
|
||||
case list = 30000
|
||||
case zap = 9735
|
||||
}
|
||||
|
||||
@@ -12,11 +12,20 @@ import UIKit
|
||||
class Profiles {
|
||||
var profiles: [String: TimestampedProfile] = [:]
|
||||
var validated: [String: NIP05] = [:]
|
||||
var zappers: [String: String] = [:]
|
||||
|
||||
func is_validated(_ pk: String) -> NIP05? {
|
||||
return validated[pk]
|
||||
}
|
||||
|
||||
func lookup_zapper(pubkey: String) -> String? {
|
||||
if let zapper = zappers[pubkey] {
|
||||
return zapper
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func add(id: String, profile: TimestampedProfile) {
|
||||
profiles[id] = profile
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RelayInfo: Codable {
|
||||
public struct RelayInfo: Codable {
|
||||
let read: Bool
|
||||
let write: Bool
|
||||
|
||||
static let rw = RelayInfo(read: true, write: true)
|
||||
}
|
||||
|
||||
struct RelayDescriptor: Codable {
|
||||
let url: URL
|
||||
let info: RelayInfo
|
||||
public struct RelayDescriptor: Codable {
|
||||
public let url: URL
|
||||
public let info: RelayInfo
|
||||
}
|
||||
|
||||
enum RelayFlags: Int {
|
||||
@@ -24,6 +24,30 @@ enum RelayFlags: Int {
|
||||
case broken = 1
|
||||
}
|
||||
|
||||
struct Limitations: Codable {
|
||||
let payment_required: Bool?
|
||||
|
||||
static var empty: Limitations {
|
||||
Limitations(payment_required: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayMetadata: Codable {
|
||||
let name: String?
|
||||
let description: String?
|
||||
let pubkey: String?
|
||||
let contact: String?
|
||||
let supported_nips: [Int]?
|
||||
let software: String?
|
||||
let version: String?
|
||||
let limitation: Limitations?
|
||||
let payments_url: String?
|
||||
|
||||
var is_paid: Bool {
|
||||
return limitation?.payment_required ?? false
|
||||
}
|
||||
}
|
||||
|
||||
class Relay: Identifiable {
|
||||
let descriptor: RelayDescriptor
|
||||
let connection: RelayConnection
|
||||
|
||||
@@ -118,10 +118,9 @@ func make_nostr_req(_ req: NostrRequest) -> String? {
|
||||
}
|
||||
|
||||
func make_nostr_push_event(ev: NostrEvent) -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .withoutEscapingSlashes
|
||||
let event_data = try! encoder.encode(ev)
|
||||
let event = String(decoding: event_data, as: UTF8.self)
|
||||
guard let event = encode_json(ev) else {
|
||||
return nil
|
||||
}
|
||||
let encoded = "[\"EVENT\",\(event)]"
|
||||
print(encoded)
|
||||
return encoded
|
||||
|
||||
@@ -42,6 +42,8 @@ class RelayPool {
|
||||
var relays: [Relay] = []
|
||||
var handlers: [RelayHandler] = []
|
||||
var request_queue: [QueuedRequest] = []
|
||||
var seen: Set<String> = Set()
|
||||
var counts: [String: UInt64] = [:]
|
||||
|
||||
var descriptors: [RelayDescriptor] {
|
||||
relays.map { $0.descriptor }
|
||||
@@ -149,9 +151,9 @@ class RelayPool {
|
||||
self.send(.unsubscribe(sub_id), to: to)
|
||||
}
|
||||
|
||||
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> ()) {
|
||||
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (String, NostrConnectionEvent) -> (), to: [String]? = nil) {
|
||||
register_handler(sub_id: sub_id, handler: handler)
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)))
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
||||
}
|
||||
|
||||
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [String]?, handler: @escaping (String, NostrConnectionEvent) -> ()) {
|
||||
@@ -241,8 +243,25 @@ class RelayPool {
|
||||
}
|
||||
}
|
||||
|
||||
func record_seen(relay_id: String, event: NostrConnectionEvent) {
|
||||
if case .nostr_event(let ev) = event {
|
||||
if case .event(_, let nev) = ev {
|
||||
let k = relay_id + nev.id
|
||||
if !seen.contains(k) {
|
||||
seen.insert(k)
|
||||
if counts[relay_id] == nil {
|
||||
counts[relay_id] = 1
|
||||
} else {
|
||||
counts[relay_id] = (counts[relay_id] ?? 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handle_event(relay_id: String, event: NostrConnectionEvent) {
|
||||
record_last_pong(relay_id: relay_id, event: event)
|
||||
record_seen(relay_id: relay_id, event: event)
|
||||
|
||||
// run req queue when we reconnect
|
||||
if case .ws_event(let ws) = event {
|
||||
|
||||
22
damus/Util/AccountDeletion.swift
Normal file
22
damus/Util/AccountDeletion.swift
Normal file
@@ -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
|
||||
}
|
||||
@@ -147,7 +147,7 @@ public func bech32_decode(_ str: String) throws -> (hrp: String, data: Data)? {
|
||||
guard let strBytes = str.data(using: .utf8) else {
|
||||
throw Bech32Error.nonUTF8String
|
||||
}
|
||||
guard strBytes.count <= 90 else {
|
||||
guard strBytes.count <= 2024 else {
|
||||
throw Bech32Error.stringLengthExceeded
|
||||
}
|
||||
var lower: Bool = false
|
||||
|
||||
@@ -29,7 +29,7 @@ public struct DismissKeyboardOnTap: ViewModifier {
|
||||
|
||||
}
|
||||
|
||||
func end_editing() {
|
||||
public func end_editing() {
|
||||
UIApplication.shared.connectedScenes
|
||||
.filter {$0.activationState == .foregroundActive}
|
||||
.map {$0 as? UIWindowScene}
|
||||
|
||||
@@ -38,6 +38,26 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
|
||||
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 {
|
||||
var i: Int = 0
|
||||
|
||||
|
||||
@@ -158,6 +158,20 @@ func get_saved_privkey() -> String? {
|
||||
return mkey.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
}
|
||||
|
||||
/**
|
||||
Detects whether a string might contain an nsec1 prefixed private key.
|
||||
It does not determine if it's the current user's private key and does not verify if it is properly encoded or has the right length.
|
||||
*/
|
||||
func contentContainsPrivateKey(_ content: String) -> Bool {
|
||||
if #available(iOS 16.0, *) {
|
||||
return content.contains(/nsec1[02-9ac-z]+/)
|
||||
} else {
|
||||
let regex = try! NSRegularExpression(pattern: "nsec1[02-9ac-z]+")
|
||||
return (regex.firstMatch(in: content, range: NSRange(location: 0, length: content.count)) != nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate func removePrivateKeyFromUserDefaults() throws {
|
||||
guard let privKey = UserDefaults.standard.string(forKey: "privkey") else { return }
|
||||
try save_privkey(privkey: privKey)
|
||||
|
||||
24
damus/Util/LNUrlPayRequest.swift
Normal file
24
damus/Util/LNUrlPayRequest.swift
Normal file
@@ -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
|
||||
}
|
||||
20
damus/Util/LNUrls.swift
Normal file
20
damus/Util/LNUrls.swift
Normal file
@@ -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]
|
||||
}
|
||||
}
|
||||
26
damus/Util/NIPURLBuilder.swift
Normal file
26
damus/Util/NIPURLBuilder.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// NIPURLBuilder.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Honk on 2/1/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct NIPURLBuilder {
|
||||
static private let baseURL = "https://github.com/nostr-protocol/nips/blob/master/"
|
||||
static func url(forNIP nip: Int) -> URL? {
|
||||
let urlString = baseURL + "\(formatNipNumber(nip: nip)).md"
|
||||
return URL(string: urlString)
|
||||
}
|
||||
|
||||
static func formatNipNumber(nip: Int) -> String {
|
||||
let formatted: String
|
||||
if nip < 10 {
|
||||
formatted = "0\(nip)"
|
||||
} else {
|
||||
formatted = "\(nip)"
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,12 @@ extension Notification.Name {
|
||||
static var new_unmutes: Notification.Name {
|
||||
return Notification.Name("new_unmutes")
|
||||
}
|
||||
static var deleted_account: Notification.Name {
|
||||
return Notification.Name("deleted_account")
|
||||
}
|
||||
static var update_stats: Notification.Name {
|
||||
return Notification.Name("update_stats")
|
||||
}
|
||||
}
|
||||
|
||||
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
||||
|
||||
94
damus/Util/Relays/RelayFilters.swift
Normal file
94
damus/Util/Relays/RelayFilters.swift
Normal file
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// RelayFilters.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-08.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RelayFilter: Hashable {
|
||||
let timeline: Timeline
|
||||
let relay_id: String
|
||||
|
||||
init(timeline: Timeline, relay_id: String, on: Bool = false) {
|
||||
self.timeline = timeline
|
||||
self.relay_id = relay_id
|
||||
}
|
||||
}
|
||||
|
||||
class RelayFilters {
|
||||
private let our_pubkey: String
|
||||
private var disabled: Set<RelayFilter>
|
||||
|
||||
func is_filtered(timeline: Timeline, relay_id: String) -> Bool {
|
||||
let filter = RelayFilter(timeline: timeline, relay_id: relay_id)
|
||||
let contains = disabled.contains(filter)
|
||||
return contains
|
||||
}
|
||||
|
||||
func remove(timeline: Timeline, relay_id: String) {
|
||||
let filter = RelayFilter(timeline: timeline, relay_id: relay_id)
|
||||
if !disabled.contains(filter) {
|
||||
return
|
||||
}
|
||||
|
||||
disabled.remove(filter)
|
||||
save_relay_filters(our_pubkey, filters: disabled)
|
||||
}
|
||||
|
||||
func insert(timeline: Timeline, relay_id: String) {
|
||||
let filter = RelayFilter(timeline: timeline, relay_id: relay_id)
|
||||
if disabled.contains(filter) {
|
||||
return
|
||||
}
|
||||
|
||||
disabled.insert(filter)
|
||||
save_relay_filters(our_pubkey, filters: disabled)
|
||||
}
|
||||
|
||||
init(our_pubkey: String) {
|
||||
self.our_pubkey = our_pubkey
|
||||
disabled = load_relay_filters(our_pubkey) ?? Set()
|
||||
}
|
||||
}
|
||||
|
||||
func save_relay_filters(_ pubkey: String, filters: Set<RelayFilter>) {
|
||||
let key = pk_setting_key(pubkey, key: "relay_filters")
|
||||
let arr = Array(filters.map { filter in "\(filter.timeline)\t\(filter.relay_id)" })
|
||||
UserDefaults.standard.set(arr, forKey: key)
|
||||
}
|
||||
|
||||
func relay_filter_setting_key(_ pubkey: String) -> String {
|
||||
return pk_setting_key(pubkey, key: "relay_filters")
|
||||
}
|
||||
|
||||
func clear_relay_filters(_ pubkey: String) {
|
||||
let key = relay_filter_setting_key(pubkey)
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
}
|
||||
|
||||
func load_relay_filters(_ pubkey: String) -> Set<RelayFilter>? {
|
||||
let key = relay_filter_setting_key(pubkey)
|
||||
guard let filters = UserDefaults.standard.stringArray(forKey: key) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return filters.reduce(into: Set()) { s, str in
|
||||
let parts = str.components(separatedBy: "\t")
|
||||
guard parts.count == 2 else {
|
||||
return
|
||||
}
|
||||
guard let timeline = Timeline.init(rawValue: parts[0]) else {
|
||||
return
|
||||
}
|
||||
let filter = RelayFilter(timeline: timeline, relay_id: parts[1])
|
||||
s.insert(filter)
|
||||
}
|
||||
}
|
||||
|
||||
func determine_to_relays(pool: RelayPool, filters: RelayFilters) -> [String] {
|
||||
return pool.descriptors
|
||||
.map { $0.url.absoluteString }
|
||||
.filter { !filters.is_filtered(timeline: .search, relay_id: $0) }
|
||||
}
|
||||
20
damus/Util/Relays/RelayMetadatas.swift
Normal file
20
damus/Util/Relays/RelayMetadatas.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// RelayMetadatas.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-09.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class RelayMetadatas {
|
||||
private var metadata: [String: RelayMetadata] = [:]
|
||||
|
||||
func lookup(relay_id: String) -> RelayMetadata? {
|
||||
return metadata[relay_id]
|
||||
}
|
||||
|
||||
func insert(relay_id: String, metadata: RelayMetadata) {
|
||||
self.metadata[relay_id] = metadata
|
||||
}
|
||||
}
|
||||
127
damus/Util/Translator.swift
Normal file
127
damus/Util/Translator.swift
Normal file
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
324
damus/Util/Zap.swift
Normal file
324
damus/Util/Zap.swift
Normal file
@@ -0,0 +1,324 @@
|
||||
//
|
||||
// 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, sats: Int) async -> String? {
|
||||
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let zappable = payreq.allowsNostr ?? false
|
||||
let amount: Int64 = Int64(sats) * 1000
|
||||
|
||||
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
|
||||
|
||||
if zappable {
|
||||
if let json = encode_json(zapreq) {
|
||||
print("zapreq json: \(json)")
|
||||
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
|
||||
}
|
||||
67
damus/Util/Zaps.swift
Normal file
67
damus/Util/Zaps.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
notify(.update_stats, zap.target.id)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -21,18 +21,44 @@ enum ActionBarSheet: Identifiable {
|
||||
struct EventActionBar: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let test_lnurl: String?
|
||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
let thread: ThreadV2?
|
||||
|
||||
// just used for previews
|
||||
@State var sheet: ActionBarSheet? = nil
|
||||
@State var confirm_boost: Bool = false
|
||||
@State var show_share_sheet: Bool = false
|
||||
@StateObject var bar: ActionBarModel
|
||||
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = nil, thread: ThreadV2? = nil) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.test_lnurl = test_lnurl
|
||||
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
|
||||
self.thread = thread
|
||||
}
|
||||
|
||||
var lnurl: String? {
|
||||
test_lnurl ?? damus_state.profiles.lookup(id: event.pubkey)?.lnurl
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if damus_state.keypair.privkey != nil {
|
||||
EventActionButton(img: "bubble.left", col: nil) {
|
||||
let self_replied = (thread != nil && thread!.childEvents.first { $0.pubkey == damus_state.pubkey } != nil)
|
||||
|
||||
EventActionButton(img: "bubble.left", col: self_replied ? Color.blue : nil) {
|
||||
notify(.reply, event)
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
if thread != nil && !thread!.childEvents.isEmpty {
|
||||
Text("\(thread!.childEvents.count)")
|
||||
.offset(x: -10)
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(self_replied ? Color.blue : Color.gray)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
ZStack {
|
||||
@@ -44,12 +70,14 @@ struct EventActionBar: View {
|
||||
self.confirm_boost = true
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
|
||||
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
||||
.offset(x: 18)
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
ZStack {
|
||||
LikeButton(liked: bar.liked) {
|
||||
if bar.liked {
|
||||
@@ -64,10 +92,17 @@ struct EventActionBar: View {
|
||||
.foregroundColor(bar.liked ? Color.accentColor : Color.gray)
|
||||
|
||||
}
|
||||
|
||||
if let lnurl = self.lnurl {
|
||||
Spacer()
|
||||
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
|
||||
show_share_sheet = true
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
|
||||
}
|
||||
.sheet(isPresented: $show_share_sheet) {
|
||||
if let note_id = bech32_note_id(event.id) {
|
||||
@@ -86,6 +121,11 @@ struct EventActionBar: View {
|
||||
} message: {
|
||||
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
|
||||
}
|
||||
.onReceive(handle_notify(.update_stats)) { n in
|
||||
let target = n.object as! String
|
||||
guard target == self.event.id else { return }
|
||||
self.bar.update(damus: self.damus_state, evid: target)
|
||||
}
|
||||
.onReceive(handle_notify(.liked)) { n in
|
||||
let liked = n.object as! Counted
|
||||
if liked.id != event.id {
|
||||
@@ -145,6 +185,7 @@ struct LikeButton: View {
|
||||
Image(liked ? "shaka-full" : "shaka-line")
|
||||
.foregroundColor(liked ? .accentColor : .gray)
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,10 +196,11 @@ struct EventActionBar_Previews: PreviewProvider {
|
||||
let ds = test_damus_state()
|
||||
let ev = NostrEvent(content: "hi", pubkey: pk)
|
||||
|
||||
let bar = ActionBarModel(likes: 0, boosts: 0, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
|
||||
let likedbar = ActionBarModel(likes: 10, boosts: 10, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
|
||||
let likedbar_ours = ActionBarModel(likes: 100, boosts: 100, tips: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_tip: nil)
|
||||
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, tips: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_tip: nil)
|
||||
let bar = ActionBarModel.empty()
|
||||
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, 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) {
|
||||
EventActionBar(damus_state: ds, event: ev, bar: bar)
|
||||
@@ -168,6 +210,8 @@ struct EventActionBar_Previews: PreviewProvider {
|
||||
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, test_lnurl: "lnurl")
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,17 @@ import SwiftUI
|
||||
struct EventDetailBar: View {
|
||||
let state: DamusState
|
||||
let target: String
|
||||
@StateObject var bar: ActionBarModel
|
||||
let target_pk: String
|
||||
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
init (state: DamusState, target: String, target_pk: String) {
|
||||
self.state = state
|
||||
self.target = target
|
||||
self.target_pk = target_pk
|
||||
self._bar = ObservedObject(wrappedValue: make_actionbar_model(ev: target, damus: state))
|
||||
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@@ -28,8 +38,12 @@ struct EventDetailBar: View {
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
if bar.tips > 0 {
|
||||
Text("\(Text("\(bar.tips)", comment: "Number of tip payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("tips_count", comment: "Part of a larger sentence to describe how many tip payments there are on a post."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many tip payments there are on a post. In source English, the first variable is the number of tip payments, and the second variable is 'Tip' or 'Tips'.")
|
||||
if bar.zaps > 0 {
|
||||
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
|
||||
NavigationLink(destination: dst) {
|
||||
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'.")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +51,6 @@ struct EventDetailBar: View {
|
||||
|
||||
struct EventDetailBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EventDetailBar(state: test_damus_state(), target: "", bar: ActionBarModel.empty())
|
||||
EventDetailBar(state: test_damus_state(), target: "", target_pk: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ struct CarouselItemView: View {
|
||||
.font(.title2)
|
||||
.foregroundColor(Color.white)
|
||||
.padding([.leading,.trailing], 50.0)
|
||||
.minimumScaleFactor(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,20 +96,27 @@ struct ChatView: View {
|
||||
|
||||
if let ref_id = thread.replies.lookup(event.id) {
|
||||
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)
|
||||
.environmentObject(thread)
|
||||
.onTapGesture {
|
||||
expand_reply = !expand_reply
|
||||
}
|
||||
*/
|
||||
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 {
|
||||
let bar = make_actionbar_model(ev: event, damus: damus_state)
|
||||
let bar = make_actionbar_model(ev: event.id, damus: damus_state)
|
||||
EventActionBar(damus_state: damus_state, event: event, bar: bar)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ struct ChatroomView: View {
|
||||
next_ev: ind == count-1 ? nil : thread.events[ind+1],
|
||||
damus_state: damus
|
||||
)
|
||||
.event_context_menu(ev, keypair: damus.keypair)
|
||||
.event_context_menu(ev, keypair: damus.keypair, target_pubkey: ev.pubkey)
|
||||
.onTapGesture {
|
||||
if thread.initial_event.id == ev.id {
|
||||
//dismiss()
|
||||
|
||||
@@ -5,119 +5,183 @@
|
||||
// Created by William Casarin on 2022-06-09.
|
||||
//
|
||||
import AVFoundation
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
import LocalAuthentication
|
||||
|
||||
struct ConfigView: View {
|
||||
let state: DamusState
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var show_add_relay: Bool = false
|
||||
@State var confirm_logout: Bool = false
|
||||
@State var new_relay: String = ""
|
||||
@State var confirm_delete_account: Bool = false
|
||||
@State var show_privkey: Bool = false
|
||||
@State var has_authenticated_locally: Bool = false
|
||||
@State var show_api_key: Bool = false
|
||||
@State var privkey: String
|
||||
@State var privkey_copied: Bool = false
|
||||
@State var pubkey_copied: Bool = false
|
||||
@State var relays: [RelayDescriptor]
|
||||
@EnvironmentObject var user_settings: UserSettingsStore
|
||||
@State var delete_text: String = ""
|
||||
@State var default_zap_amount: String
|
||||
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||
|
||||
init(state: DamusState) {
|
||||
self.state = state
|
||||
let zap_amt = get_default_zap_amount(pubkey: state.pubkey).map({ "\($0)" }) ?? "1000"
|
||||
_default_zap_amount = State(initialValue: zap_amt)
|
||||
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
|
||||
_relays = State(initialValue: state.pool.descriptors)
|
||||
_settings = ObservedObject(initialValue: state.settings)
|
||||
}
|
||||
|
||||
func authenticateLocally(completion: @escaping (Bool) -> Void) {
|
||||
// Need to authenticate only once while ConfigView is presented
|
||||
guard !has_authenticated_locally else {
|
||||
completion(true)
|
||||
return
|
||||
}
|
||||
let context = LAContext()
|
||||
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
|
||||
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: NSLocalizedString("Local authentication to access private key", comment: "Face ID usage description shown when trying to access private key")) { success, error in
|
||||
DispatchQueue.main.async {
|
||||
has_authenticated_locally = success
|
||||
completion(success)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there's no authentication set up on the device, let the user copy the key without it
|
||||
has_authenticated_locally = true
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: (jb55) could be more general but not gonna worry about it atm
|
||||
func CopyButton(is_pk: Bool) -> some View {
|
||||
return Button(action: {
|
||||
UIPasteboard.general.string = is_pk ? self.state.keypair.pubkey_bech32 : self.privkey
|
||||
self.privkey_copied = !is_pk
|
||||
self.pubkey_copied = is_pk
|
||||
generator.impactOccurred()
|
||||
let copyKey = {
|
||||
UIPasteboard.general.string = is_pk ? self.state.keypair.pubkey_bech32 : self.privkey
|
||||
self.privkey_copied = !is_pk
|
||||
self.pubkey_copied = is_pk
|
||||
generator.impactOccurred()
|
||||
}
|
||||
if has_authenticated_locally {
|
||||
copyKey()
|
||||
} else {
|
||||
authenticateLocally { success in
|
||||
if success {
|
||||
copyKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
let copied = is_pk ? self.pubkey_copied : self.privkey_copied
|
||||
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
|
||||
}
|
||||
}
|
||||
|
||||
var recommended: [RelayDescriptor] {
|
||||
let rs: [RelayDescriptor] = []
|
||||
return BOOTSTRAP_RELAYS.reduce(into: rs) { (xs, x) in
|
||||
if let _ = state.pool.get_relay(x) {
|
||||
} else {
|
||||
xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Form {
|
||||
Section {
|
||||
List(Array(relays), id: \.url) { relay in
|
||||
RelayView(state: state, relay: relay.url.absoluteString)
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text("Relays", comment: "Header text for relay server list for configuration.")
|
||||
Spacer()
|
||||
Button(action: { show_add_relay = true }) {
|
||||
Image(systemName: "plus")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if recommended.count > 0 {
|
||||
Section(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration")) {
|
||||
List(recommended, id: \.url) { r in
|
||||
RecommendedRelayView(damus: state, relay: r.url.absoluteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
|
||||
HStack {
|
||||
Text(state.keypair.pubkey_bech32)
|
||||
|
||||
|
||||
CopyButton(is_pk: true)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
}
|
||||
|
||||
|
||||
if let sec = state.keypair.privkey_bech32 {
|
||||
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
|
||||
HStack {
|
||||
if show_privkey == false {
|
||||
if show_privkey == false || !has_authenticated_locally {
|
||||
SecureField(NSLocalizedString("Private Key", comment: "Title of the secure field that holds the user's private key."), text: $privkey)
|
||||
.disabled(true)
|
||||
} else {
|
||||
Text(sec)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
}
|
||||
|
||||
|
||||
CopyButton(is_pk: false)
|
||||
}
|
||||
|
||||
|
||||
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
|
||||
.onChange(of: show_privkey) { newValue in
|
||||
if newValue {
|
||||
authenticateLocally { success in
|
||||
show_privkey = success
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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"),
|
||||
selection: $user_settings.default_wallet) {
|
||||
selection: $settings.default_wallet) {
|
||||
ForEach(Wallet.allCases, id: \.self) { wallet in
|
||||
Text(wallet.model.displayName)
|
||||
.tag(wallet.model.tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) {
|
||||
TextField("1000", text: $default_zap_amount)
|
||||
}
|
||||
|
||||
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")) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -129,15 +193,48 @@ struct ConfigView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Reset", comment: "Section title for resetting the user")) {
|
||||
Button(NSLocalizedString("Logout", comment: "Button to logout the user.")) {
|
||||
confirm_logout = true
|
||||
if state.is_privkey_user {
|
||||
Section(NSLocalizedString("Delete", comment: "Section title for deleting the user")) {
|
||||
Button(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), role: .destructive) {
|
||||
confirm_delete_account = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
|
||||
let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
|
||||
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
|
||||
Text("\(bundleShortVersion) (\(bundleVersion))", comment: "Text indicating which version of the Damus app is running. Should typically not need to be translated.")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: default_zap_amount) { val in
|
||||
guard let amt = Int(val) else {
|
||||
return
|
||||
}
|
||||
set_default_zap_amount(pubkey: state.pubkey, amount: amt)
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.alert(NSLocalizedString("Permanently Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
|
||||
TextField(NSLocalizedString("Type DELETE to delete", comment: "Text field prompt asking user to type the word DELETE to confirm that they want to proceed with deleting their account. The all caps lock DELETE word should not be translated. Everything else should."), text: $delete_text)
|
||||
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
|
||||
confirm_delete_account = false
|
||||
}
|
||||
Button(NSLocalizedString("Delete", comment: "Button for deleting the users account."), role: .destructive) {
|
||||
guard let full_kp = state.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
|
||||
guard delete_text == "DELETE" else {
|
||||
return
|
||||
}
|
||||
|
||||
let ev = created_deleted_account_profile(keypair: full_kp)
|
||||
state.pool.send(.event(ev))
|
||||
notify(.logout, ())
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) {
|
||||
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) {
|
||||
confirm_logout = false
|
||||
@@ -148,50 +245,83 @@ struct ConfigView: View {
|
||||
} message: {
|
||||
Text("Make sure your nsec account key is saved before you logout or you will lose access to this account", comment: "Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.")
|
||||
}
|
||||
.sheet(isPresented: $show_add_relay) {
|
||||
AddRelayView(show_add_relay: $show_add_relay, relay: $new_relay) { m_relay in
|
||||
guard var relay = m_relay else {
|
||||
return
|
||||
}
|
||||
|
||||
if relay.starts(with: "wss://") == false && relay.starts(with: "ws://") == false {
|
||||
relay = "wss://" + relay
|
||||
}
|
||||
|
||||
guard let url = URL(string: relay) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let ev = state.contacts.event else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let privkey = state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
let info = RelayInfo.rw
|
||||
|
||||
guard (try? state.pool.add_relay(url, info: info)) != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
state.pool.connect(to: [relay])
|
||||
|
||||
guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.descriptors, relay: relay, info: info) else {
|
||||
return
|
||||
}
|
||||
|
||||
process_contact_event(pool: state.pool, contacts: state.contacts, pubkey: state.pubkey, ev: ev)
|
||||
|
||||
state.pool.send(.event(new_ev))
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
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,7 +11,7 @@ struct DMChatView: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
@EnvironmentObject var dms: DirectMessageModel
|
||||
@State var message: String = ""
|
||||
@State var showPrivateKeyWarning: Bool = false
|
||||
|
||||
var Messages: some View {
|
||||
ScrollViewReader { scroller in
|
||||
@@ -19,7 +19,7 @@ struct DMChatView: View {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
|
||||
DMView(event: dms.events[ind], damus_state: damus_state)
|
||||
.event_context_menu(ev, keypair: damus_state.keypair)
|
||||
.event_context_menu(ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey)
|
||||
}
|
||||
EndBlock(height: 80)
|
||||
}
|
||||
@@ -51,7 +51,7 @@ struct DMChatView: View {
|
||||
}
|
||||
|
||||
var InputField: some View {
|
||||
TextEditor(text: $message)
|
||||
TextEditor(text: $dms.draft)
|
||||
.textEditorBackground {
|
||||
InputBackground()
|
||||
}
|
||||
@@ -63,6 +63,8 @@ struct DMChatView: View {
|
||||
)
|
||||
.padding(16)
|
||||
.foregroundColor(Color.primary)
|
||||
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@@ -90,34 +92,36 @@ struct DMChatView: View {
|
||||
HStack(spacing: 0) {
|
||||
InputField
|
||||
|
||||
if !message.isEmpty {
|
||||
Button(role: .none, action: send_message) {
|
||||
if !dms.draft.isEmpty {
|
||||
Button(
|
||||
role: .none,
|
||||
action: {
|
||||
showPrivateKeyWarning = contentContainsPrivateKey(dms.draft)
|
||||
|
||||
if !showPrivateKeyWarning {
|
||||
send_message()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Label("", systemImage: "arrow.right.circle")
|
||||
.font(.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 50 + 20 * CGFloat(text_lines))
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
|
||||
|
||||
var text_lines: Int {
|
||||
var lines = 1
|
||||
for c in message {
|
||||
if lines > 4 {
|
||||
return lines
|
||||
}
|
||||
if c.isNewline {
|
||||
lines += 1
|
||||
}
|
||||
Text(dms.draft).opacity(0).padding(.all, 8)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
|
||||
}
|
||||
|
||||
return lines
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(minHeight: 70, maxHeight: 150, alignment: .bottom)
|
||||
}
|
||||
|
||||
func send_message() {
|
||||
let tags = [["p", pubkey]]
|
||||
let post_blocks = parse_post_blocks(content: message)
|
||||
let post_blocks = parse_post_blocks(content: dms.draft)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
|
||||
let content = render_blocks(blocks: post_tags.blocks)
|
||||
|
||||
@@ -126,7 +130,7 @@ struct DMChatView: View {
|
||||
return
|
||||
}
|
||||
|
||||
message = ""
|
||||
dms.draft = ""
|
||||
|
||||
damus_state.pool.send(.event(dm))
|
||||
end_editing()
|
||||
@@ -142,15 +146,29 @@ struct DMChatView: View {
|
||||
|
||||
Footer
|
||||
}
|
||||
|
||||
Text("Send a message to start the conversation...", comment: "Text prompt for user to send a message to the other user.")
|
||||
.lineLimit(nil)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
.opacity(((dms.events.count == 0) ? 1.0 : 0.0))
|
||||
.foregroundColor(.gray)
|
||||
.lineLimit(nil)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
.opacity(((dms.events.count == 0) ? 1.0 : 0.0))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for DMs view, where DM is the English abbreviation for Direct Message."))
|
||||
.toolbar { Header }
|
||||
.onDisappear {
|
||||
if dms.draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
dms.draft = ""
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
|
||||
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
|
||||
showPrivateKeyWarning = false
|
||||
}
|
||||
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
|
||||
send_message()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
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)
|
||||
.padding(10)
|
||||
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
|
||||
|
||||
@@ -43,6 +43,7 @@ struct DirectMessagesView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,16 +63,16 @@ struct DirectMessagesView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Picker(NSLocalizedString("DM Type", comment: "DM selector for seeing either DMs or message requests, which are messages that have not been responded to yet. DM is the English abbreviation for Direct Message."), selection: $dm_type) {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $dm_type, content: {
|
||||
Text("DMs", comment: "Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.")
|
||||
.tag(DMType.friend)
|
||||
|
||||
Text("Requests", comment: "Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet). DM is the English abbreviation for Direct Message.")
|
||||
.tag(DMType.rando)
|
||||
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
})
|
||||
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
|
||||
TabView(selection: $dm_type) {
|
||||
MainContent(requests: false)
|
||||
@@ -82,8 +83,6 @@ struct DirectMessagesView: View {
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message."))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EULAView: View {
|
||||
var state: SetupState?
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var creating_account = false
|
||||
@State var accepted = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -69,11 +70,17 @@ By using our Application, you signify your acceptance of this EULA. If you do no
|
||||
"""))
|
||||
.padding()
|
||||
|
||||
NavigationLink(destination: CreateAccountView(), isActive: $creating_account) {
|
||||
EmptyView()
|
||||
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.")) {
|
||||
creating_account = true
|
||||
accepted = true
|
||||
}
|
||||
|
||||
DamusWhiteButton(NSLocalizedString("Reject", comment: "Button to reject the end user license agreement, which disallows the user from being let into the app.")) {
|
||||
|
||||
@@ -196,7 +196,7 @@ struct EditMetadataView: View {
|
||||
if let parts = nip05_parts {
|
||||
Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.")
|
||||
} else {
|
||||
Text("'\(nip05)' is an invalid 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.")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -7,119 +7,10 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CollapsedEvents: Identifiable {
|
||||
let count: Int
|
||||
let start: Int
|
||||
let end: Int
|
||||
|
||||
var id: String = UUID().description
|
||||
}
|
||||
|
||||
enum CollapsedEvent: Identifiable {
|
||||
case event(NostrEvent, Highlight)
|
||||
case collapsed(CollapsedEvents)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .event(let ev, _):
|
||||
return ev.id
|
||||
case .collapsed(let c):
|
||||
return c.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct EventDetailView: View {
|
||||
let sub_id = UUID().description
|
||||
let damus: DamusState
|
||||
|
||||
@StateObject var thread: ThreadModel
|
||||
@State var collapsed: Bool = true
|
||||
|
||||
func toggle_collapse_thread(scroller: ScrollViewProxy, id mid: String?, animate: Bool = true) {
|
||||
self.collapsed = !self.collapsed
|
||||
if let id = mid {
|
||||
if !self.collapsed {
|
||||
scroll_to_event(scroller: scroller, id: id, delay: 0.1, animate: animate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uncollapse_section(scroller: ScrollViewProxy, c: CollapsedEvents)
|
||||
{
|
||||
let ev = thread.events[c.start]
|
||||
print("uncollapsing section at \(c.start) '\(ev.content.prefix(12))...'")
|
||||
let start_id = ev.id
|
||||
|
||||
toggle_collapse_thread(scroller: scroller, id: start_id, animate: false)
|
||||
}
|
||||
|
||||
func CollapsedEventView(_ cev: CollapsedEvent, scroller: ScrollViewProxy) -> some View {
|
||||
Group {
|
||||
switch cev {
|
||||
case .collapsed(let c):
|
||||
Text(String(format: NSLocalizedString("collapsed_event_view_other_notes", comment: "Text to indicate that the thread was collapsed and that there are other notes to view if tapped."), c.count))
|
||||
.padding([.top,.bottom], 8)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.onTapGesture {
|
||||
//self.uncollapse_section(scroller: proxy, c: c)
|
||||
//self.toggle_collapse_thread(scroller: proxy, id: nil)
|
||||
if let ev = thread.events[safe: c.start] {
|
||||
thread.set_active_event(ev, privkey: damus.keypair.privkey)
|
||||
}
|
||||
toggle_thread_view()
|
||||
}
|
||||
case .event(let ev, _):
|
||||
EventView(damus: damus, event: ev, has_action_bar: true)
|
||||
.onTapGesture {
|
||||
if thread.initial_event.id == ev.id {
|
||||
toggle_thread_view()
|
||||
} else {
|
||||
thread.set_active_event(ev, privkey: damus.keypair.privkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
if thread.loading {
|
||||
ProgressView().progressViewStyle(.circular)
|
||||
}
|
||||
|
||||
ScrollView(.vertical) {
|
||||
LazyVStack {
|
||||
let collapsed_events = calculated_collapsed_events(
|
||||
privkey: damus.keypair.privkey,
|
||||
collapsed: self.collapsed,
|
||||
active: thread.event,
|
||||
events: thread.events
|
||||
)
|
||||
ForEach(collapsed_events, id: \.id) { cev in
|
||||
CollapsedEventView(cev, scroller: proxy)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
|
||||
EndBlock()
|
||||
}
|
||||
.onChange(of: thread.loading) { val in
|
||||
scroll_after_load(thread: thread, proxy: proxy)
|
||||
}
|
||||
.onAppear() {
|
||||
scroll_after_load(thread: thread, proxy: proxy)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))
|
||||
|
||||
}
|
||||
|
||||
func toggle_thread_view() {
|
||||
NotificationCenter.default.post(name: .toggle_thread_view, object: nil)
|
||||
Text("EventDetailView")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,167 +21,13 @@ func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct EventDetailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state()
|
||||
let tm = ThreadModel(evid: "4da698ceac09a16cdb439276fa3d13ef8f6620ffb45d11b76b3f103483c2d0b0", damus_state: state)
|
||||
EventDetailView(damus: state, thread: tm)
|
||||
EventDetailView()
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the entire reply path for the active event
|
||||
func make_reply_map(active: NostrEvent, events: [NostrEvent], privkey: String?) -> [String: ()]
|
||||
{
|
||||
let event_map: [String: Int] = zip(events,0...events.count).reduce(into: [:]) { (acc, arg1) in
|
||||
let (ev, i) = arg1
|
||||
acc[ev.id] = i
|
||||
}
|
||||
var is_reply: [String: ()] = [:]
|
||||
var i: Int = 0
|
||||
var start: Int = 0
|
||||
var iterations: Int = 0
|
||||
|
||||
if events.count == 0 {
|
||||
return is_reply
|
||||
}
|
||||
|
||||
for ev in events {
|
||||
/// does this event reply to the active event?
|
||||
let ev_refs = ev.event_refs(privkey)
|
||||
for ev_ref in ev_refs {
|
||||
if let reply = ev_ref.is_reply {
|
||||
if reply.ref_id == active.id {
|
||||
is_reply[ev.id] = ()
|
||||
start = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// does the active event reply to this event?
|
||||
let active_refs = active.event_refs(privkey)
|
||||
for active_ref in active_refs {
|
||||
if let reply = active_ref.is_reply {
|
||||
if reply.ref_id == ev.id {
|
||||
is_reply[ev.id] = ()
|
||||
start = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
i = start
|
||||
|
||||
while true {
|
||||
if iterations > 1024 {
|
||||
// infinite loop? or super large thread
|
||||
print("breaking from large reply_map... big thread??")
|
||||
break
|
||||
}
|
||||
|
||||
let ev = events[i]
|
||||
|
||||
let ref_ids = ev.referenced_ids
|
||||
if ref_ids.count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
let ref_id = ref_ids[ref_ids.count-1]
|
||||
let pubkey = ref_id.ref_id
|
||||
is_reply[pubkey] = ()
|
||||
|
||||
if let mi = event_map[pubkey] {
|
||||
i = mi
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
iterations += 1
|
||||
}
|
||||
|
||||
return is_reply
|
||||
}
|
||||
|
||||
func determine_highlight(reply_map: [String: ()], current: NostrEvent, active: NostrEvent) -> Highlight
|
||||
{
|
||||
if current.id == active.id {
|
||||
return .main
|
||||
} else if reply_map[current.id] != nil {
|
||||
return .reply
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func calculated_collapsed_events(privkey: String?, collapsed: Bool, active: NostrEvent?, events: [NostrEvent]) -> [CollapsedEvent] {
|
||||
var count: Int = 0
|
||||
|
||||
guard let active = active else {
|
||||
return []
|
||||
}
|
||||
|
||||
let reply_map = make_reply_map(active: active, events: events, privkey: privkey)
|
||||
|
||||
if !collapsed {
|
||||
return events.reduce(into: []) { acc, ev in
|
||||
let highlight = determine_highlight(reply_map: reply_map, current: ev, active: active)
|
||||
return acc.append(.event(ev, highlight))
|
||||
}
|
||||
}
|
||||
|
||||
let nevents = events.count
|
||||
var start: Int = 0
|
||||
var i: Int = 0
|
||||
|
||||
return events.reduce(into: []) { (acc, ev) in
|
||||
let highlight = determine_highlight(reply_map: reply_map, current: ev, active: active)
|
||||
|
||||
switch highlight {
|
||||
case .none:
|
||||
if i == 0 {
|
||||
start = 1
|
||||
}
|
||||
count += 1
|
||||
case .main: fallthrough
|
||||
case .custom: fallthrough
|
||||
case .reply:
|
||||
if count != 0 {
|
||||
let c = CollapsedEvents(count: count, start: start, end: i)
|
||||
acc.append(.collapsed(c))
|
||||
start = i
|
||||
count = 0
|
||||
}
|
||||
acc.append(.event(ev, highlight))
|
||||
}
|
||||
|
||||
if i == nevents-1 {
|
||||
if count != 0 {
|
||||
let c = CollapsedEvents(count: count, start: i-count, end: i)
|
||||
acc.append(.collapsed(c))
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func any_collapsed(_ evs: [CollapsedEvent]) -> Bool {
|
||||
for ev in evs {
|
||||
switch ev {
|
||||
case .collapsed:
|
||||
return true
|
||||
case .event:
|
||||
continue
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func print_event(_ ev: NostrEvent) {
|
||||
print(ev.description)
|
||||
|
||||
@@ -57,80 +57,40 @@ struct EventView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
return Group {
|
||||
if event.known_kind == .boost, let inner_ev = event.inner_event {
|
||||
VStack(alignment: .leading) {
|
||||
let prof_model = ProfileModel(pubkey: event.pubkey, damus: damus)
|
||||
let follow_model = FollowersModel(damus_state: damus, target: event.pubkey)
|
||||
let prof = damus.profiles.lookup(id: event.pubkey)
|
||||
let booster_profile = ProfileView(damus_state: damus, profile: prof_model, followers: follow_model)
|
||||
|
||||
NavigationLink(destination: booster_profile) {
|
||||
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
|
||||
VStack {
|
||||
if event.known_kind == .boost {
|
||||
if let inner_ev = event.inner_event {
|
||||
VStack(alignment: .leading) {
|
||||
let prof_model = ProfileModel(pubkey: event.pubkey, damus: damus)
|
||||
let follow_model = FollowersModel(damus_state: damus, target: event.pubkey)
|
||||
let prof = damus.profiles.lookup(id: event.pubkey)
|
||||
let booster_profile = ProfileView(damus_state: damus, profile: prof_model, followers: follow_model)
|
||||
|
||||
NavigationLink(destination: booster_profile) {
|
||||
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, has_action_bar: has_action_bar, booster_pubkey: event.pubkey)
|
||||
.padding([.top], 1)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
TextEvent(inner_ev, pubkey: inner_ev.pubkey, booster_pubkey: event.pubkey)
|
||||
.padding([.top], 1)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
} else if event.known_kind == .zap {
|
||||
if let zap = damus.zaps.zaps[event.id] {
|
||||
ZapEvent(damus: damus, zap: zap)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
} else {
|
||||
TextEvent(event, pubkey: pubkey)
|
||||
TextEvent(damus: damus, event: event, pubkey: pubkey, has_action_bar: has_action_bar, booster_pubkey: nil)
|
||||
.padding([.top], 6)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.padding([.top], 4)
|
||||
}
|
||||
}
|
||||
|
||||
func TextEvent(_ event: NostrEvent, pubkey: String, booster_pubkey: String? = nil) -> some View {
|
||||
return HStack(alignment: .top) {
|
||||
let profile = damus.profiles.lookup(id: pubkey)
|
||||
|
||||
VStack {
|
||||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
|
||||
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
|
||||
|
||||
NavigationLink(destination: pv) {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center) {
|
||||
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
|
||||
|
||||
Text("\(format_relative_time(event.created_at))")
|
||||
.foregroundColor(.gray)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
EventBody(damus_state: damus, event: event, size: .normal)
|
||||
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
BuilderEventView(damus: damus, event_id: mention.ref.id)
|
||||
}
|
||||
|
||||
if has_action_bar {
|
||||
Rectangle().frame(height: 2).opacity(0)
|
||||
|
||||
let bar = make_actionbar_model(ev: event, damus: damus)
|
||||
|
||||
EventActionBar(damus_state: damus, event: event, bar: bar)
|
||||
.padding([.top], 4)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.padding([.top], 4)
|
||||
}
|
||||
.padding([.leading], 2)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(event_validity_color(event.validity))
|
||||
.id(event.id)
|
||||
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
||||
.padding([.bottom], 2)
|
||||
.event_context_menu(event, keypair: damus.keypair)
|
||||
}
|
||||
}
|
||||
|
||||
// blame the porn bots for this code
|
||||
@@ -171,9 +131,9 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
func event_context_menu(_ event: NostrEvent, keypair: Keypair) -> some View {
|
||||
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String) -> some View {
|
||||
return self.contextMenu {
|
||||
EventMenuContext(event: event, keypair: keypair)
|
||||
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -192,22 +152,22 @@ func format_date(_ created_at: Int64) -> String {
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
|
||||
let likes = damus.likes.counts[ev.id]
|
||||
let boosts = damus.boosts.counts[ev.id]
|
||||
let tips = damus.tips.tips[ev.id]
|
||||
let our_like = damus.likes.our_events[ev.id]
|
||||
let our_boost = damus.boosts.our_events[ev.id]
|
||||
let our_tip = damus.tips.our_tips[ev.id]
|
||||
func make_actionbar_model(ev: String, damus: DamusState) -> ActionBarModel {
|
||||
let likes = damus.likes.counts[ev]
|
||||
let boosts = damus.boosts.counts[ev]
|
||||
let zaps = damus.zaps.event_counts[ev]
|
||||
let zap_total = damus.zaps.event_totals[ev]
|
||||
let our_like = damus.likes.our_events[ev]
|
||||
let our_boost = damus.boosts.our_events[ev]
|
||||
let our_zap = damus.zaps.our_zaps[ev]
|
||||
|
||||
return ActionBarModel(likes: likes ?? 0,
|
||||
boosts: boosts ?? 0,
|
||||
tips: tips ?? 0,
|
||||
zaps: zaps ?? 0,
|
||||
zap_total: zap_total ?? 0,
|
||||
our_like: our_like,
|
||||
our_boost: our_boost,
|
||||
our_tip: our_tip
|
||||
our_zap: our_zap?.first
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,23 +31,29 @@ struct BuilderEventView: View {
|
||||
return
|
||||
}
|
||||
|
||||
// Is current event
|
||||
if id == subscription_uuid {
|
||||
if event != nil {
|
||||
return
|
||||
}
|
||||
|
||||
event = nostr_event
|
||||
|
||||
unsubscribe()
|
||||
guard id == subscription_uuid else {
|
||||
return
|
||||
}
|
||||
|
||||
guard nostr_event.known_kind == .text else {
|
||||
return
|
||||
}
|
||||
|
||||
if event != nil {
|
||||
return
|
||||
}
|
||||
|
||||
event = nostr_event
|
||||
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
func load() {
|
||||
subscribe(filters: [
|
||||
NostrFilter(ids: [self.event_id], limit: 1),
|
||||
NostrFilter(
|
||||
ids: [self.event_id],
|
||||
limit: 1
|
||||
kinds: [NostrKind.zap.rawValue],
|
||||
referenced_ids: [self.event_id]
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ struct EmbeddedEventView: View {
|
||||
|
||||
EventBody(damus_state: damus_state, event: event, size: .small)
|
||||
}
|
||||
.event_context_menu(event, keypair: damus_state.keypair)
|
||||
.event_context_menu(event, keypair: damus_state.keypair, target_pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ struct EventBody: View {
|
||||
|
||||
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil)
|
||||
|
||||
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_img, artifacts: .just_content(content), size: size)
|
||||
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(content), size: size)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct EventMenuContext: View {
|
||||
let event: NostrEvent
|
||||
let keypair: Keypair
|
||||
let target_pubkey: String
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -20,7 +21,7 @@ struct EventMenuContext: View {
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = keypair.pubkey_bech32
|
||||
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")
|
||||
}
|
||||
@@ -44,16 +45,16 @@ struct EventMenuContext: View {
|
||||
}
|
||||
|
||||
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||
if keypair.pubkey != event.pubkey && keypair.privkey != nil {
|
||||
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
|
||||
Button(role: .destructive) {
|
||||
let target: ReportTarget = .note(ReportNoteTarget(pubkey: event.pubkey, note_id: event.id))
|
||||
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, event.pubkey)
|
||||
notify(.block, target_pubkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ struct MutedEventView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let scroller: ScrollViewProxy?
|
||||
let thread: ThreadV2?
|
||||
|
||||
let selected: Bool
|
||||
@Binding var nav_target: String?
|
||||
@@ -18,18 +19,19 @@ struct MutedEventView: View {
|
||||
@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) {
|
||||
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, nav_target: Binding<String?>, navigating: Binding<Bool>, selected: Bool, thread: ThreadV2?) {
|
||||
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))
|
||||
self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event))
|
||||
self.thread = thread
|
||||
}
|
||||
|
||||
var should_mute: Bool {
|
||||
return should_hide_event(contacts: damus_state.contacts, ev: event)
|
||||
return !should_show_event(contacts: damus_state.contacts, ev: event)
|
||||
}
|
||||
|
||||
var FillColor: Color {
|
||||
@@ -42,9 +44,9 @@ struct MutedEventView: View {
|
||||
.foregroundColor(FillColor)
|
||||
|
||||
HStack {
|
||||
Text("Post from a user you've blocked")
|
||||
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 ? "Hide" : "Show") {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -55,7 +57,7 @@ struct MutedEventView: View {
|
||||
var Event: some View {
|
||||
Group {
|
||||
if selected {
|
||||
SelectedEventView(damus: damus_state, event: event)
|
||||
SelectedEventView(damus: damus_state, event: event, thread: thread)
|
||||
} else {
|
||||
EventView(damus: damus_state, event: event, has_action_bar: true)
|
||||
.onTapGesture {
|
||||
@@ -106,7 +108,7 @@ struct MutedEventView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, nav_target: $nav_target, navigating: $navigating, selected: false)
|
||||
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, nav_target: $nav_target, navigating: $navigating, selected: false, thread: nil)
|
||||
.frame(width: .infinity, height: 50)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,21 @@ import SwiftUI
|
||||
struct SelectedEventView: View {
|
||||
let damus: DamusState
|
||||
let event: NostrEvent
|
||||
let thread: ThreadV2?
|
||||
|
||||
var pubkey: String {
|
||||
event.pubkey
|
||||
}
|
||||
|
||||
@StateObject var bar: ActionBarModel
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, thread: ThreadV2?) {
|
||||
self.damus = damus
|
||||
self.event = event
|
||||
self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus))
|
||||
self.thread = thread
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
let profile = damus.profiles.lookup(id: pubkey)
|
||||
@@ -35,28 +45,31 @@ struct SelectedEventView: View {
|
||||
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)
|
||||
EventDetailBar(state: damus, target: event.id, target_pk: event.pubkey)
|
||||
Divider()
|
||||
}
|
||||
|
||||
EventActionBar(damus_state: damus, event: event, bar: bar)
|
||||
|
||||
EventActionBar(damus_state: damus, event: event, thread: thread)
|
||||
.padding([.top], 4)
|
||||
|
||||
Divider()
|
||||
.padding([.top], 4)
|
||||
}
|
||||
.onReceive(handle_notify(.update_stats)) { n in
|
||||
let target = n.object as! String
|
||||
guard target == self.event.id else { return }
|
||||
self.bar.update(damus: self.damus, evid: target)
|
||||
}
|
||||
.padding([.leading], 2)
|
||||
.event_context_menu(event, keypair: damus.keypair)
|
||||
.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)
|
||||
SelectedEventView(damus: test_damus_state(), event: test_event, thread: nil)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
70
damus/Views/Events/TextEvent.swift
Normal file
70
damus/Views/Events/TextEvent.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// TextEvent.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-03.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TextEvent: View {
|
||||
let damus: DamusState
|
||||
let event: NostrEvent
|
||||
let pubkey: String
|
||||
let has_action_bar: Bool
|
||||
let booster_pubkey: String?
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
let profile = damus.profiles.lookup(id: pubkey)
|
||||
|
||||
VStack {
|
||||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
|
||||
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
|
||||
|
||||
NavigationLink(destination: pv) {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center) {
|
||||
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
|
||||
|
||||
Text("\(format_relative_time(event.created_at))")
|
||||
.foregroundColor(.gray)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
EventBody(damus_state: damus, event: event, size: .normal)
|
||||
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
BuilderEventView(damus: damus, event_id: mention.ref.id)
|
||||
}
|
||||
|
||||
if has_action_bar {
|
||||
Rectangle().frame(height: 2).opacity(0)
|
||||
|
||||
EventActionBar(damus_state: damus, event: event)
|
||||
.padding([.top], 4)
|
||||
}
|
||||
}
|
||||
.padding([.leading], 2)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(event_validity_color(event.validity))
|
||||
.id(event.id)
|
||||
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
||||
.padding([.bottom], 2)
|
||||
.event_context_menu(event, keypair: damus.keypair, target_pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
struct TextEvent_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil)
|
||||
}
|
||||
}
|
||||
33
damus/Views/Events/ZapEvent.swift
Normal file
33
damus/Views/Events/ZapEvent.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// ZapEvent.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-03.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ZapEvent: View {
|
||||
let damus: DamusState
|
||||
let zap: Zap
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
|
||||
.font(.headline)
|
||||
.padding([.top], 2)
|
||||
|
||||
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, has_action_bar: false, booster_pubkey: nil)
|
||||
.padding([.top], 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
struct ZapEvent_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ZapEvent()
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -12,13 +12,14 @@ struct FollowButtonView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
let target: FollowTarget
|
||||
let follows_you: Bool
|
||||
@State var follow_state: FollowState
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
follow_state = perform_follow_btn_action(follow_state, target: target)
|
||||
} label: {
|
||||
Text(follow_btn_txt(follow_state))
|
||||
Text(follow_btn_txt(follow_state, follows_you: follows_you))
|
||||
.frame(width: 105, height: 30)
|
||||
//.padding(.vertical, 10)
|
||||
.font(.caption.weight(.bold))
|
||||
@@ -70,16 +71,19 @@ struct FollowButtonPreviews: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Unfollows", comment: "Text to indicate that the button next to it is in a state that will unfollow a profile when tapped.")
|
||||
FollowButtonView(target: target, follow_state: .unfollows)
|
||||
FollowButtonView(target: target, follows_you: false, follow_state: .unfollows)
|
||||
|
||||
Text("Following", comment: "Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.")
|
||||
FollowButtonView(target: target, follow_state: .following)
|
||||
FollowButtonView(target: target, follows_you: false, follow_state: .following)
|
||||
|
||||
Text("Follows", comment: "Text to indicate that button next to it is in a state that will follow a profile when tapped.")
|
||||
FollowButtonView(target: target, follows_you: false, follow_state: .follows)
|
||||
|
||||
Text("Follows", comment: "Text to indicate that button next to it is in a state that will follow a profile when tapped.")
|
||||
FollowButtonView(target: target, follow_state: .follows)
|
||||
FollowButtonView(target: target, follows_you: true, follow_state: .follows)
|
||||
|
||||
Text("Unfollowing", comment: "Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile.")
|
||||
FollowButtonView(target: target, follow_state: .unfollowing)
|
||||
FollowButtonView(target: target, follows_you: false, follow_state: .unfollowing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ struct FollowUserView: View {
|
||||
HStack {
|
||||
UserView(damus_state: damus_state, pubkey: target.pubkey)
|
||||
|
||||
FollowButtonView(target: target, follow_state: damus_state.contacts.follow_state(target.pubkey))
|
||||
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +275,7 @@ struct KeyInput: View {
|
||||
.autocapitalization(.none)
|
||||
.foregroundColor(.white)
|
||||
.font(.body.monospaced())
|
||||
.textContentType(.password)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum Timeline: String, CustomStringConvertible {
|
||||
enum Timeline: String, CustomStringConvertible, Hashable {
|
||||
case home
|
||||
case notifications
|
||||
case search
|
||||
|
||||
@@ -7,74 +7,29 @@
|
||||
|
||||
import SwiftUI
|
||||
import LinkPresentation
|
||||
|
||||
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)
|
||||
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")
|
||||
}
|
||||
import NaturalLanguage
|
||||
|
||||
struct NoteContentView: View {
|
||||
let privkey: String?
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let profiles: Profiles
|
||||
let previews: PreviewCache
|
||||
|
||||
let show_images: Bool
|
||||
|
||||
|
||||
@State var artifacts: NoteArtifacts
|
||||
|
||||
@State var preview: LinkViewRepresentable? = nil
|
||||
let size: EventViewKind
|
||||
|
||||
@State var preview: LinkViewRepresentable? = nil
|
||||
|
||||
func MainContent() -> some View {
|
||||
return VStack(alignment: .leading) {
|
||||
Text(artifacts.content)
|
||||
.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 {
|
||||
ImageCarousel(urls: artifacts.images)
|
||||
} else if !show_images && artifacts.images.count > 0 {
|
||||
@@ -87,7 +42,7 @@ struct NoteContentView: View {
|
||||
.cornerRadius(10)
|
||||
}
|
||||
if artifacts.invoices.count > 0 {
|
||||
InvoicesView(invoices: artifacts.invoices)
|
||||
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
|
||||
}
|
||||
|
||||
if let preview = self.preview, show_images {
|
||||
@@ -106,16 +61,16 @@ struct NoteContentView: View {
|
||||
var body: some View {
|
||||
MainContent()
|
||||
.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
|
||||
let profile = notif.object as! ProfileUpdate
|
||||
let blocks = event.blocks(privkey)
|
||||
let blocks = event.blocks(damus_state.keypair.privkey)
|
||||
for block in blocks {
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
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 .hashtag: return
|
||||
@@ -125,7 +80,7 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
.task {
|
||||
if let preview = previews.lookup(self.event.id) {
|
||||
if let preview = damus_state.previews.lookup(self.event.id) {
|
||||
switch preview {
|
||||
case .value(let view):
|
||||
self.preview = view
|
||||
@@ -139,13 +94,13 @@ struct NoteContentView: View {
|
||||
let meta = await getMetaData(for: artifacts.links.first!)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func getMetaData(for url: URL) async -> LPLinkMetadata? {
|
||||
// iOS 15 is crashing for some reason
|
||||
guard #available(iOS 16, *) else {
|
||||
@@ -195,12 +150,74 @@ func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct NoteContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state()
|
||||
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
|
||||
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@ func PostButton(action: @escaping () -> ()) -> some View {
|
||||
.keyboardShortcut("n", modifiers: [.command, .shift])
|
||||
}
|
||||
|
||||
func PostButtonContainer(userSettings: UserSettingsStore, action: @escaping () -> Void) -> some View {
|
||||
let is_left_handed = userSettings.left_handed.self
|
||||
func PostButtonContainer(is_left_handed: Bool, action: @escaping () -> Void) -> some View {
|
||||
return VStack {
|
||||
Spacer()
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
|
||||
|
||||
struct PostView: View {
|
||||
@State var post: String = ""
|
||||
|
||||
@FocusState var focus: Bool
|
||||
@State var showPrivateKeyWarning: Bool = false
|
||||
|
||||
let replying_to: NostrEvent?
|
||||
let references: [ReferencedId]
|
||||
@@ -46,6 +48,13 @@ struct PostView: View {
|
||||
let new_post = NostrPost(content: content, references: references, kind: kind)
|
||||
|
||||
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
|
||||
|
||||
if let replying_to {
|
||||
damus_state.drafts.replies.removeValue(forKey: replying_to)
|
||||
} else {
|
||||
damus_state.drafts.post = ""
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -65,7 +74,11 @@ struct PostView: View {
|
||||
|
||||
if !is_post_empty {
|
||||
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
|
||||
self.send_post()
|
||||
showPrivateKeyWarning = contentContainsPrivateKey(self.post)
|
||||
|
||||
if !showPrivateKeyWarning {
|
||||
self.send_post()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +88,13 @@ struct PostView: View {
|
||||
TextEditor(text: $post)
|
||||
.focused($focus)
|
||||
.textInputAutocapitalization(.sentences)
|
||||
.onChange(of: post) { _ in
|
||||
if let replying_to {
|
||||
damus_state.drafts.replies[replying_to] = post
|
||||
} else {
|
||||
damus_state.drafts.post = post
|
||||
}
|
||||
}
|
||||
|
||||
if post.isEmpty {
|
||||
Text(POST_PLACEHOLDER)
|
||||
@@ -94,11 +114,37 @@ struct PostView: View {
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
if let replying_to {
|
||||
if damus_state.drafts.replies[replying_to] == nil {
|
||||
damus_state.drafts.replies[replying_to] = ""
|
||||
}
|
||||
if let p = damus_state.drafts.replies[replying_to] {
|
||||
post = p
|
||||
}
|
||||
} else {
|
||||
post = damus_state.drafts.post
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.focus = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
damus_state.drafts.replies.removeValue(forKey: replying_to)
|
||||
} else if replying_to == nil && damus_state.drafts.post.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
damus_state.drafts.post = ""
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
|
||||
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
|
||||
showPrivateKeyWarning = false
|
||||
}
|
||||
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
|
||||
self.send_post()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,10 @@ struct UserSearch_Previews: PreviewProvider {
|
||||
}
|
||||
|
||||
|
||||
func search_users(profiles: Profiles, tags: [[String]], search: String) -> [SearchedUser] {
|
||||
func search_users(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] {
|
||||
var seen_user = Set<String>()
|
||||
let search = _search.lowercased()
|
||||
|
||||
return tags.reduce(into: Array<SearchedUser>()) { arr, tag in
|
||||
guard tag.count >= 2 && tag[0] == "p" else {
|
||||
return
|
||||
@@ -77,7 +79,7 @@ func search_users(profiles: Profiles, tags: [[String]], search: String) -> [Sear
|
||||
|
||||
let profile = profiles.lookup(id: pubkey)
|
||||
|
||||
guard ((petname?.hasPrefix(search) ?? false) || (profile?.name?.hasPrefix(search) ?? false)) else {
|
||||
guard ((petname?.lowercased().hasPrefix(search) ?? false) || (profile?.name?.lowercased().hasPrefix(search) ?? false)) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
34
damus/Views/Profile/FollowsYou.swift
Normal file
34
damus/Views/Profile/FollowsYou.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// FollowsYou.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-07.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FollowsYou: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var fill_color: Color {
|
||||
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text("Follows you", comment: "Text to indicate that a user is following your profile.")
|
||||
.padding([.leading, .trailing], 6.0)
|
||||
.padding([.top, .bottom], 2.0)
|
||||
.foregroundColor(.gray)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 5.0)
|
||||
.foregroundColor(fill_color)
|
||||
}
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
struct FollowsYou_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FollowsYou()
|
||||
}
|
||||
}
|
||||
61
damus/Views/Profile/ProfileNameView.swift
Normal file
61
damus/Views/Profile/ProfileNameView.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// ProfileNameView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-07.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProfileNameView: View {
|
||||
let pubkey: String
|
||||
let profile: Profile?
|
||||
let follows_you: Bool
|
||||
let damus: DamusState
|
||||
|
||||
var spacing: CGFloat { 10.0 }
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let real_name = profile?.display_name {
|
||||
VStack(alignment: .leading) {
|
||||
Text(real_name)
|
||||
.font(.title3.weight(.bold))
|
||||
HStack(alignment: .center, spacing: spacing) {
|
||||
ProfileName(pubkey: pubkey, profile: profile, prefix: "@", damus: damus, show_friend_confirmed: true)
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
if follows_you {
|
||||
FollowsYou()
|
||||
}
|
||||
}
|
||||
KeyView(pubkey: pubkey)
|
||||
.pubkey_context_menu(bech32_pubkey: pubkey)
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center, spacing: spacing) {
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true)
|
||||
.font(.title3.weight(.bold))
|
||||
if follows_you {
|
||||
FollowsYou()
|
||||
}
|
||||
}
|
||||
KeyView(pubkey: pubkey)
|
||||
.pubkey_context_menu(bech32_pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfileNameView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
ProfileNameView(pubkey: test_event.pubkey, profile: nil, follows_you: true, damus: test_damus_state())
|
||||
|
||||
ProfileNameView(pubkey: test_event.pubkey, profile: nil, follows_you: false, damus: test_damus_state())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ enum FollowState {
|
||||
case unfollows
|
||||
}
|
||||
|
||||
func follow_btn_txt(_ fs: FollowState) -> String {
|
||||
func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> String {
|
||||
switch fs {
|
||||
case .follows:
|
||||
return NSLocalizedString("Unfollow", comment: "Button to unfollow a user.")
|
||||
@@ -28,7 +28,11 @@ func follow_btn_txt(_ fs: FollowState) -> String {
|
||||
case .unfollowing:
|
||||
return NSLocalizedString("Unfollowing...", comment: "Label to indicate that the user is in the process of unfollowing another user.")
|
||||
case .unfollows:
|
||||
return NSLocalizedString("Follow", comment: "Button to follow a user.")
|
||||
if follows_you {
|
||||
return NSLocalizedString("Follow Back", comment: "Button to follow a user back.")
|
||||
} else {
|
||||
return NSLocalizedString("Follow", comment: "Button to follow a user.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,35 +49,6 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfileNameView: View {
|
||||
let pubkey: String
|
||||
let profile: Profile?
|
||||
let damus: DamusState
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let real_name = profile?.display_name {
|
||||
VStack(alignment: .leading) {
|
||||
Text(real_name)
|
||||
.font(.title3.weight(.bold))
|
||||
ProfileName(pubkey: pubkey, profile: profile, prefix: "@", damus: damus, show_friend_confirmed: true)
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
KeyView(pubkey: pubkey)
|
||||
.pubkey_context_menu(bech32_pubkey: pubkey)
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true)
|
||||
.font(.title3.weight(.bold))
|
||||
KeyView(pubkey: pubkey)
|
||||
.pubkey_context_menu(bech32_pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EditButton: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
@@ -91,6 +66,8 @@ struct EditButton: View {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.stroke(borderColor(), lineWidth: 1)
|
||||
}
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +92,6 @@ struct ProfileView: View {
|
||||
@State var is_zoomed: Bool = false
|
||||
@State var show_share_sheet: Bool = false
|
||||
@State var action_sheet_presented: Bool = false
|
||||
@StateObject var user_settings = UserSettingsStore()
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@@ -141,10 +117,10 @@ struct ProfileView: View {
|
||||
|
||||
func LNButton(lnurl: String, profile: Profile) -> some View {
|
||||
Button(action: {
|
||||
if user_settings.show_wallet_selector {
|
||||
if damus_state.settings.show_wallet_selector {
|
||||
showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: user_settings.default_wallet.model, invoice: lnurl)
|
||||
open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl)
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "bolt.circle")
|
||||
@@ -160,8 +136,7 @@ struct ProfileView: View {
|
||||
}
|
||||
.cornerRadius(24)
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: lnurl)
|
||||
.environmentObject(user_settings)
|
||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +149,24 @@ struct ProfileView: View {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
}
|
||||
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
|
||||
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
|
||||
show_share_sheet = true
|
||||
}
|
||||
|
||||
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
|
||||
Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
|
||||
let target: ReportTarget = .user(profile.pubkey)
|
||||
notify(.report, target)
|
||||
}
|
||||
|
||||
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
|
||||
notify(.block, profile.pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var ShareButton: some View {
|
||||
@@ -221,6 +214,61 @@ struct ProfileView: View {
|
||||
return 0
|
||||
}
|
||||
|
||||
func ActionSection(profile_data: Profile?) -> some View {
|
||||
return Group {
|
||||
ActionSheetButton
|
||||
|
||||
if let profile = profile_data {
|
||||
if let lnurl = profile.lnurl, lnurl != "" {
|
||||
LNButton(lnurl: lnurl, profile: profile)
|
||||
}
|
||||
}
|
||||
|
||||
DMButton
|
||||
|
||||
if profile.pubkey != damus_state.pubkey {
|
||||
FollowButtonView(
|
||||
target: profile.get_follow_target(),
|
||||
follows_you: profile.follows(pubkey: damus_state.pubkey),
|
||||
follow_state: damus_state.contacts.follow_state(profile.pubkey)
|
||||
)
|
||||
} else if damus_state.keypair.privkey != nil {
|
||||
NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
|
||||
EditButton(damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func NameSection(profile_data: Profile?) -> some View {
|
||||
return Group {
|
||||
HStack(alignment: .center) {
|
||||
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
|
||||
.onTapGesture {
|
||||
is_zoomed.toggle()
|
||||
}
|
||||
.fullScreenCover(isPresented: $is_zoomed) {
|
||||
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
|
||||
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
|
||||
|
||||
Spacer()
|
||||
|
||||
ActionSection(profile_data: profile_data)
|
||||
.offset(y: -15.0) // Increase if set a frame
|
||||
}
|
||||
|
||||
let follows_you = profile.follows(pubkey: damus_state.pubkey)
|
||||
ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state)
|
||||
//.padding(.bottom)
|
||||
.padding(.top,-(pfp_size/2.0))
|
||||
}
|
||||
}
|
||||
|
||||
var pfp_size: CGFloat {
|
||||
return 90.0
|
||||
}
|
||||
|
||||
var TopSection: some View {
|
||||
ZStack(alignment: .top) {
|
||||
GeometryReader { geometry in
|
||||
@@ -233,55 +281,14 @@ struct ProfileView: View {
|
||||
}.frame(height: BANNER_HEIGHT)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8.0) {
|
||||
let data = damus_state.profiles.lookup(id: profile.pubkey)
|
||||
let pfp_size: CGFloat = 90.0
|
||||
let profile_data = damus_state.profiles.lookup(id: profile.pubkey)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
|
||||
.onTapGesture {
|
||||
is_zoomed.toggle()
|
||||
}
|
||||
.fullScreenCover(isPresented: $is_zoomed) {
|
||||
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
|
||||
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
|
||||
|
||||
Spacer()
|
||||
|
||||
Group {
|
||||
ActionSheetButton
|
||||
|
||||
if let profile = data {
|
||||
if let lnurl = profile.lnurl, lnurl != "" {
|
||||
LNButton(lnurl: lnurl, profile: profile)
|
||||
}
|
||||
}
|
||||
|
||||
DMButton
|
||||
|
||||
if profile.pubkey != damus_state.pubkey {
|
||||
FollowButtonView(
|
||||
target: profile.get_follow_target(),
|
||||
follow_state: damus_state.contacts.follow_state(profile.pubkey)
|
||||
)
|
||||
} else if damus_state.keypair.privkey != nil {
|
||||
NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
|
||||
EditButton(damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.offset(y: -15.0) // Increase if set a frame
|
||||
|
||||
}
|
||||
|
||||
ProfileNameView(pubkey: profile.pubkey, profile: data, damus: damus_state)
|
||||
//.padding(.bottom)
|
||||
.padding(.top,-(pfp_size/2.0))
|
||||
NameSection(profile_data: profile_data)
|
||||
|
||||
Text(ProfileView.markdown.process(data?.about ?? ""))
|
||||
.font(.subheadline)
|
||||
Text(ProfileView.markdown.process(profile_data?.about ?? ""))
|
||||
.font(.subheadline).textSelection(.enabled)
|
||||
|
||||
if let url = data?.website_url {
|
||||
if let url = profile_data?.website_url {
|
||||
WebsiteLink(url: url)
|
||||
}
|
||||
|
||||
@@ -315,10 +322,19 @@ struct ProfileView: View {
|
||||
}
|
||||
|
||||
if let relays = profile.relays {
|
||||
NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) {
|
||||
Text("\(Text("\(relays.keys.count)", comment: "Number of relay servers a user is connected.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
|
||||
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
|
||||
let relay_text = Text("\(Text("\(relays.keys.count)", comment: "Number of relay servers a user is connected.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
|
||||
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
|
||||
NavigationLink(destination: RelayConfigView(state: damus_state)) {
|
||||
relay_text
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
} else {
|
||||
NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) {
|
||||
relay_text
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,23 +390,6 @@ struct ProfileView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
|
||||
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
|
||||
show_share_sheet = true
|
||||
}
|
||||
|
||||
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
|
||||
Button(NSLocalizedString("Report", comment: "Button to report a profile."), role: .destructive) {
|
||||
let target: ReportTarget = .user(profile.pubkey)
|
||||
notify(.report, target)
|
||||
}
|
||||
|
||||
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
|
||||
notify(.block, profile.pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
@@ -407,7 +406,7 @@ struct ProfileView_Previews: PreviewProvider {
|
||||
|
||||
func test_damus_state() -> DamusState {
|
||||
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||
let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: pubkey), previews: PreviewCache())
|
||||
let damus = DamusState.empty
|
||||
|
||||
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io")
|
||||
let tsprof = TimestampedProfile(profile: prof, timestamp: 0)
|
||||
|
||||
@@ -23,19 +23,44 @@ struct QRCodeView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
DamusGradient()
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.foregroundColor(.white)
|
||||
.font(.subheadline)
|
||||
.padding(.leading, 20)
|
||||
ZStack(alignment: .center) {
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
DamusGradient()
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.foregroundColor(.white)
|
||||
.font(.subheadline)
|
||||
.padding(.leading, 20)
|
||||
}
|
||||
.zIndex(1)
|
||||
}
|
||||
.zIndex(1)
|
||||
|
||||
VStack(alignment: .center) {
|
||||
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
|
||||
|
||||
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil {
|
||||
ProfilePicView(pubkey: damus_state.pubkey, size: 90.0, highlight: .custom(Color("DamusWhite"), 4.0), profiles: damus_state.profiles)
|
||||
.padding(.top, 50)
|
||||
} else {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(Color("DamusWhite"))
|
||||
.padding(.top, 50)
|
||||
}
|
||||
|
||||
if let display_name = profile?.display_name {
|
||||
Text(display_name)
|
||||
.foregroundColor(Color("DamusWhite"))
|
||||
.font(.system(size: 24, weight: .heavy))
|
||||
}
|
||||
if let name = profile?.name {
|
||||
Text("@" + name)
|
||||
.foregroundColor(Color("DamusWhite"))
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -46,15 +71,24 @@ struct QRCodeView: View {
|
||||
.scaledToFit()
|
||||
.frame(width: 200, height: 200)
|
||||
.padding()
|
||||
|
||||
Text(key)
|
||||
.font(.headline)
|
||||
.foregroundColor(Color(.white))
|
||||
.padding()
|
||||
.cornerRadius(10)
|
||||
.overlay(RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color("DamusWhite"), lineWidth: 1))
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
|
||||
.foregroundColor(Color("DamusWhite"))
|
||||
.font(.system(size: 24, weight: .heavy))
|
||||
.padding(.top)
|
||||
|
||||
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
|
||||
.foregroundColor(Color("DamusWhite"))
|
||||
.font(.system(size: 18, weight: .ultraLight))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
44
damus/Views/RelayFilterView.swift
Normal file
44
damus/Views/RelayFilterView.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// RelayFilterView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Ben Weeks on 1/8/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RelayFilterView: View {
|
||||
let state: DamusState
|
||||
let timeline: Timeline
|
||||
//@State var relays: [RelayDescriptor]
|
||||
//@EnvironmentObject var user_settings: UserSettingsStore
|
||||
//@State var relays: [RelayDescriptor]
|
||||
|
||||
init(state: DamusState, timeline: Timeline) {
|
||||
self.state = state
|
||||
self.timeline = timeline
|
||||
|
||||
//_relays = State(initialValue: state.pool.descriptors)
|
||||
}
|
||||
|
||||
var relays: [RelayDescriptor] {
|
||||
return state.pool.descriptors
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text("To filter your \(timeline.rawValue) feed, please choose applicable relays from the list below:")
|
||||
.padding()
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 0)
|
||||
|
||||
List(Array(relays), id: \.url) { relay in
|
||||
RelayToggle(state: state, timeline: timeline, relay_id: relay.url.absoluteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayFilterView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RelayFilterView(state: test_damus_state(), timeline: .search)
|
||||
}
|
||||
}
|
||||
33
damus/Views/Relays/Detail/RelayPaidDetail.swift
Normal file
33
damus/Views/Relays/Detail/RelayPaidDetail.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// RelayPaidDetail.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-10.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RelayPaidDetail: View {
|
||||
let payments_url: String?
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
RelayType(is_paid: true)
|
||||
if let url = payments_url.flatMap({ URL(string: $0) }) {
|
||||
Button(action: {
|
||||
openURL(url)
|
||||
}, label: {
|
||||
Text("\(url)")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayPaidDetail_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RelayPaidDetail(payments_url: "https://jb55.com")
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ struct RecommendedRelayView: View {
|
||||
guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.descriptors, relay: relay, info: .rw) else {
|
||||
return
|
||||
}
|
||||
process_contact_event(pool: damus.pool, contacts: damus.contacts, pubkey: damus.pubkey, ev: ev_after_add)
|
||||
process_contact_event(state: damus, ev: ev_after_add)
|
||||
damus.pool.send(.event(ev_after_add))
|
||||
}
|
||||
}
|
||||
114
damus/Views/Relays/RelayConfigView.swift
Normal file
114
damus/Views/Relays/RelayConfigView.swift
Normal file
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// RelayConfigView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-01-30.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RelayConfigView: View {
|
||||
let state: DamusState
|
||||
@State var new_relay: String = ""
|
||||
@State var show_add_relay: Bool = false
|
||||
@State var relays: [RelayDescriptor]
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
init(state: DamusState) {
|
||||
self.state = state
|
||||
_relays = State(initialValue: state.pool.descriptors)
|
||||
}
|
||||
|
||||
var recommended: [RelayDescriptor] {
|
||||
let rs: [RelayDescriptor] = []
|
||||
return BOOTSTRAP_RELAYS.reduce(into: rs) { (xs, x) in
|
||||
if let _ = state.pool.get_relay(x) {
|
||||
} else {
|
||||
xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
MainContent
|
||||
.onReceive(handle_notify(.relays_changed)) { _ in
|
||||
self.relays = state.pool.descriptors
|
||||
}
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
dismiss()
|
||||
}
|
||||
.sheet(isPresented: $show_add_relay) {
|
||||
AddRelayView(show_add_relay: $show_add_relay, relay: $new_relay) { m_relay in
|
||||
guard var relay = m_relay else {
|
||||
return
|
||||
}
|
||||
|
||||
if relay.starts(with: "wss://") == false && relay.starts(with: "ws://") == false {
|
||||
relay = "wss://" + relay
|
||||
}
|
||||
|
||||
guard let url = URL(string: relay) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let ev = state.contacts.event else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let privkey = state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
let info = RelayInfo.rw
|
||||
|
||||
guard (try? state.pool.add_relay(url, info: info)) != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
state.pool.connect(to: [relay])
|
||||
|
||||
guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.descriptors, relay: relay, info: info) else {
|
||||
return
|
||||
}
|
||||
|
||||
process_contact_event(state: state, ev: ev)
|
||||
|
||||
state.pool.send(.event(new_ev))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var MainContent: some View {
|
||||
Form {
|
||||
Section {
|
||||
List(Array(relays), id: \.url) { relay in
|
||||
RelayView(state: state, relay: relay.url.absoluteString)
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text("Relays", comment: "Header text for relay server list for configuration.")
|
||||
Spacer()
|
||||
Button(action: { show_add_relay = true }) {
|
||||
Image(systemName: "plus")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if recommended.count > 0 {
|
||||
Section(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration")) {
|
||||
List(recommended, id: \.url) { r in
|
||||
RecommendedRelayView(damus: state, relay: r.url.absoluteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayConfigView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RelayConfigView(state: test_damus_state())
|
||||
}
|
||||
}
|
||||
96
damus/Views/Relays/RelayDetailView.swift
Normal file
96
damus/Views/Relays/RelayDetailView.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// RelayDetailView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Joel Klabo on 2/1/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RelayDetailView: View {
|
||||
let state: DamusState
|
||||
let relay: String
|
||||
let nip11: RelayMetadata
|
||||
|
||||
@State private var errorString: String?
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
func FieldText(_ str: String?) -> some View {
|
||||
Text(str ?? "No data available")
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
Form {
|
||||
if let pubkey = nip11.pubkey {
|
||||
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
|
||||
UserView(damus_state: state, pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
|
||||
HStack {
|
||||
Text(relay)
|
||||
Spacer()
|
||||
RelayStatus(pool: state.pool, relay: relay)
|
||||
}
|
||||
}
|
||||
if nip11.is_paid {
|
||||
Section(content: {
|
||||
RelayPaidDetail(payments_url: nip11.payments_url)
|
||||
}, header: {
|
||||
Text("Paid Relay")
|
||||
}, footer: {
|
||||
Text("This is a paid relay, you must pay for posts to be accepted.")
|
||||
})
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Description", comment: "Label to display relay description.")) {
|
||||
FieldText(nip11.description)
|
||||
}
|
||||
Section(NSLocalizedString("Contact", comment: "Label to display relay contact information.")) {
|
||||
FieldText(nip11.contact)
|
||||
}
|
||||
Section(NSLocalizedString("Software", comment: "Label to display relay software.")) {
|
||||
FieldText(nip11.software)
|
||||
}
|
||||
Section(NSLocalizedString("Version", comment: "Label to display relay software version.")) {
|
||||
FieldText(nip11.version)
|
||||
}
|
||||
if let nips = nip11.supported_nips, nips.count > 0 {
|
||||
Section(NSLocalizedString("Supported NIPs", comment: "Label to display relay's supported NIPs.")) {
|
||||
Text(nipsList(nips: nips))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.switched_timeline)) { notif in
|
||||
dismiss()
|
||||
}
|
||||
.navigationTitle(nip11.name ?? "")
|
||||
}
|
||||
|
||||
private func nipsList(nips: [Int]) -> AttributedString {
|
||||
var attrString = AttributedString()
|
||||
let lastNipIndex = nips.count - 1
|
||||
for (index, nip) in nips.enumerated() {
|
||||
if let link = NIPURLBuilder.url(forNIP: nip) {
|
||||
let nipString = NIPURLBuilder.formatNipNumber(nip: nip)
|
||||
var nipAttrString = AttributedString(stringLiteral: nipString)
|
||||
nipAttrString.link = link
|
||||
attrString = attrString + nipAttrString
|
||||
if index < lastNipIndex {
|
||||
attrString = attrString + AttributedString(stringLiteral: ", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
return attrString
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayDetailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let metadata = RelayMetadata(name: "name", description: "desc", pubkey: "pubkey", contact: "contact", supported_nips: [1,2,3], software: "software", version: "version", limitation: Limitations.empty, payments_url: "https://jb55.com")
|
||||
RelayDetailView(state: test_damus_state(), relay: "relay", nip11: metadata)
|
||||
}
|
||||
}
|
||||
50
damus/Views/Relays/RelayStatus.swift
Normal file
50
damus/Views/Relays/RelayStatus.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// RelayStatus.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-10.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RelayStatus: View {
|
||||
let pool: RelayPool
|
||||
let relay: String
|
||||
|
||||
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
|
||||
|
||||
@State var conn_color: Color = .gray
|
||||
|
||||
func update_connection_color() {
|
||||
for relay in pool.relays {
|
||||
if relay.id == self.relay {
|
||||
let c = relay.connection
|
||||
if c.isConnected {
|
||||
conn_color = .green
|
||||
} else if c.isConnecting || c.isReconnecting {
|
||||
conn_color = .yellow
|
||||
} else {
|
||||
conn_color = .red
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Circle()
|
||||
.frame(width: 8.0, height: 8.0)
|
||||
.foregroundColor(conn_color)
|
||||
.onReceive(timer) { _ in
|
||||
update_connection_color()
|
||||
}
|
||||
.onAppear() {
|
||||
update_connection_color()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayStatus_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RelayStatus(pool: test_damus_state().pool, relay: "relay")
|
||||
}
|
||||
}
|
||||
42
damus/Views/Relays/RelayToggle.swift
Normal file
42
damus/Views/Relays/RelayToggle.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// RelayToggle.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-10.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RelayToggle: View {
|
||||
let state: DamusState
|
||||
let timeline: Timeline
|
||||
let relay_id: String
|
||||
|
||||
func toggle_binding(relay_id: String) -> Binding<Bool> {
|
||||
return Binding(get: {
|
||||
!state.relay_filters.is_filtered(timeline: timeline, relay_id: relay_id)
|
||||
}, set: { on in
|
||||
if !on {
|
||||
state.relay_filters.insert(timeline: timeline, relay_id: relay_id)
|
||||
} else {
|
||||
state.relay_filters.remove(timeline: timeline, relay_id: relay_id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
RelayStatus(pool: state.pool, relay: relay_id)
|
||||
RelayType(is_paid: state.relay_metadata.lookup(relay_id: relay_id)?.is_paid ?? false)
|
||||
Toggle(relay_id, isOn: toggle_binding(relay_id: relay_id))
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayToggle_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RelayToggle(state: test_damus_state(), timeline: .search, relay_id: "wss://jb55.com")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
27
damus/Views/Relays/RelayType.swift
Normal file
27
damus/Views/Relays/RelayType.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// RelayType.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-10.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RelayType: View {
|
||||
let is_paid: Bool
|
||||
|
||||
var body: some View {
|
||||
|
||||
Image(systemName: is_paid ? "dollarsign.circle.fill" : "globe.americas.fill")
|
||||
.foregroundColor(is_paid ? Color("DamusGreen") : .gray)
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayType_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
RelayType(is_paid: false)
|
||||
RelayType(is_paid: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,37 +11,22 @@ struct RelayView: View {
|
||||
let state: DamusState
|
||||
let relay: String
|
||||
|
||||
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
|
||||
@State var conn_color: Color = .gray
|
||||
|
||||
func update_connection_color() {
|
||||
for relay in state.pool.relays {
|
||||
if relay.id == self.relay {
|
||||
let c = relay.connection
|
||||
if c.isConnected {
|
||||
conn_color = .green
|
||||
} else if c.isConnecting || c.isReconnecting {
|
||||
conn_color = .yellow
|
||||
var body: some View {
|
||||
Group {
|
||||
HStack {
|
||||
RelayStatus(pool: state.pool, relay: relay)
|
||||
RelayType(is_paid: state.relay_metadata.lookup(relay_id: relay)?.is_paid ?? false)
|
||||
if let meta = state.relay_metadata.lookup(relay_id: relay) {
|
||||
NavigationLink {
|
||||
RelayDetailView(state: state, relay: relay, nip11: meta)
|
||||
} label: {
|
||||
Text(relay)
|
||||
}
|
||||
} else {
|
||||
conn_color = .red
|
||||
Text(relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 8.0, height: 8.0)
|
||||
.foregroundColor(conn_color)
|
||||
Text(relay)
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
update_connection_color()
|
||||
}
|
||||
.onAppear() {
|
||||
update_connection_color()
|
||||
}
|
||||
.swipeActions {
|
||||
if let privkey = state.keypair.privkey {
|
||||
RemoveAction(privkey: privkey)
|
||||
@@ -75,7 +60,7 @@ struct RelayView: View {
|
||||
return
|
||||
}
|
||||
|
||||
process_contact_event(pool: state.pool, contacts: state.contacts, pubkey: state.pubkey, ev: new_ev)
|
||||
process_contact_event(state: state, ev: new_ev)
|
||||
state.pool.send(.event(new_ev))
|
||||
} label: {
|
||||
Label(NSLocalizedString("Delete", comment: "Button to delete a relay server that the user connects to."), systemImage: "trash")
|
||||
@@ -86,6 +71,6 @@ struct RelayView: View {
|
||||
|
||||
struct RelayView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RelayView(state: test_damus_state(), relay: "wss://relay.damus.io", conn_color: .red)
|
||||
RelayView(state: test_damus_state(), relay: "wss://relay.damus.io")
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// SwiftUIView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ReplyQuoteView: View {
|
||||
let privkey: String?
|
||||
let quoter: NostrEvent
|
||||
let event_id: String
|
||||
let profiles: Profiles
|
||||
let previews: PreviewCache
|
||||
|
||||
@EnvironmentObject var thread: ThreadModel
|
||||
|
||||
func MainContent(event: NostrEvent) -> some View {
|
||||
HStack(alignment: .top) {
|
||||
Rectangle()
|
||||
.frame(width: 2)
|
||||
.padding([.leading], 4)
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .top) {
|
||||
ProfilePicView(pubkey: event.pubkey, size: 16, highlight: .reply, profiles: profiles)
|
||||
Text(Profile.displayName(profile: profiles.lookup(id: event.pubkey), pubkey: event.pubkey))
|
||||
.foregroundColor(.accentColor)
|
||||
Text("\(format_relative_time(event.created_at))", comment: "Amount of time that has passed since reply quote event occurred.")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
NoteContentView(privkey: privkey, event: event, profiles: profiles, previews: previews, show_images: false, artifacts: .just_content(event.content), size: .normal)
|
||||
.font(.callout)
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
//Spacer()
|
||||
}
|
||||
//.border(Color.red)
|
||||
}
|
||||
//.border(Color.green)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let event = thread.lookup(event_id) {
|
||||
MainContent(event: event)
|
||||
.padding(4)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReplyQuoteView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let s = test_damus_state()
|
||||
let quoter = NostrEvent(content: "a\nb\nc", pubkey: "pubkey")
|
||||
ReplyQuoteView(privkey: s.keypair.privkey, quoter: quoter, event_id: "pubkey2", profiles: s.profiles, previews: PreviewCache())
|
||||
.environmentObject(ThreadModel(event: quoter, damus_state: s))
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ struct SaveKeysView: View {
|
||||
@State var priv_copied: Bool = false
|
||||
@State var loading: Bool = false
|
||||
@State var error: String? = nil
|
||||
|
||||
@FocusState var pubkey_focused: Bool
|
||||
@FocusState var privkey_focused: Bool
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
@@ -39,7 +42,7 @@ struct SaveKeysView: View {
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
SaveKeyView(text: account.pubkey_bech32, is_copied: $pub_copied)
|
||||
SaveKeyView(text: account.pubkey_bech32, textContentType: .username, is_copied: $pub_copied, focus: $pubkey_focused)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
if pub_copied {
|
||||
@@ -52,7 +55,7 @@ struct SaveKeysView: View {
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
SaveKeyView(text: account.privkey_bech32, is_copied: $priv_copied)
|
||||
SaveKeyView(text: account.privkey_bech32, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
|
||||
@@ -77,6 +80,13 @@ struct SaveKeysView: View {
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
.onAppear {
|
||||
// Hack to force keyboard to show up for a short moment and then hiding it to register password autofill flow.
|
||||
pubkey_focused = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
pubkey_focused = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func complete_account_creation(_ account: CreateAccountModel) {
|
||||
@@ -138,7 +148,9 @@ struct SaveKeysView: View {
|
||||
|
||||
struct SaveKeyView: View {
|
||||
let text: String
|
||||
let textContentType: UITextContentType
|
||||
@Binding var is_copied: Bool
|
||||
var focus: FocusState<Bool>.Binding
|
||||
|
||||
func copy_text() {
|
||||
UIPasteboard.general.string = text
|
||||
@@ -166,8 +178,8 @@ struct SaveKeyView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(text)
|
||||
|
||||
TextField("", text: .constant(text))
|
||||
.padding(5)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
|
||||
@@ -177,7 +189,14 @@ struct SaveKeyView: View {
|
||||
.foregroundColor(.white)
|
||||
.onTapGesture {
|
||||
copy_text()
|
||||
// Hack to force keyboard to hide. Showing keyboard on text field is necessary to register password autofill flow but the text itself should not be modified.
|
||||
DispatchQueue.main.async {
|
||||
end_editing()
|
||||
}
|
||||
}
|
||||
.textContentType(textContentType)
|
||||
.deleteDisabled(true)
|
||||
.focused(focus)
|
||||
|
||||
spacerBlock(width: 0, height: 0) /// set a 'width' > 0 here to vary key Text's aspect ratio
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ struct SearchResultsView: View {
|
||||
}
|
||||
}
|
||||
case .hashtag(let ht):
|
||||
let search_model = SearchModel(pool: damus_state.pool, search: .filter_hashtag([ht]))
|
||||
let search_model = SearchModel(contacts: damus_state.contacts, pool: damus_state.pool, search: .filter_hashtag([ht]))
|
||||
let dst = SearchView(appstate: damus_state, search: search_model)
|
||||
NavigationLink(destination: dst) {
|
||||
Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.")
|
||||
|
||||
@@ -25,6 +25,9 @@ struct SearchView: View {
|
||||
.onDisappear() {
|
||||
search.unsubscribe()
|
||||
}
|
||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||
search.filter_muted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +46,7 @@ struct SearchView_Previews: PreviewProvider {
|
||||
let filter = NostrFilter.filter_hashtag(["bitcoin"])
|
||||
let pool = test_state.pool
|
||||
|
||||
let model = SearchModel(pool: pool, search: filter)
|
||||
let model = SearchModel(contacts: test_state.contacts, pool: pool, search: filter)
|
||||
|
||||
SearchView(appstate: test_state, search: model)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import SwiftUI
|
||||
|
||||
struct SelectWalletView: View {
|
||||
@Binding var showingSelectWallet: Bool
|
||||
let our_pubkey: String
|
||||
let invoice: String
|
||||
@Environment(\.openURL) private var openURL
|
||||
@State var invoice_copied: Bool = false
|
||||
@EnvironmentObject var user_settings: UserSettingsStore
|
||||
|
||||
@State var allWalletModels: [Wallet.Model] = Wallet.allModels
|
||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||
@@ -38,7 +38,7 @@ struct SelectWalletView: View {
|
||||
Section(NSLocalizedString("Select a Lightning wallet", comment: "Title of section for selecting a Lightning wallet to pay a Lightning invoice.")) {
|
||||
List{
|
||||
Button() {
|
||||
let wallet_model = user_settings.default_wallet.model
|
||||
let wallet_model = get_default_wallet(our_pubkey).model
|
||||
open_with_wallet(wallet: wallet_model, invoice: invoice)
|
||||
} label: {
|
||||
HStack {
|
||||
@@ -73,6 +73,6 @@ struct SelectWalletView_Previews: PreviewProvider {
|
||||
@State static var invoice: String = ""
|
||||
|
||||
static var previews: some View {
|
||||
SelectWalletView(showingSelectWallet: $show, invoice: "")
|
||||
SelectWalletView(showingSelectWallet: $show, our_pubkey: "", invoice: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,10 @@ struct SetupView: View {
|
||||
DamusGradient()
|
||||
|
||||
VStack(alignment: .center) {
|
||||
NavigationLink(destination: EULAView(), tag: .create_account, selection: $state ) {
|
||||
NavigationLink(destination: EULAView(state: state), tag: .create_account, selection: $state ) {
|
||||
EmptyView()
|
||||
}
|
||||
NavigationLink(destination: LoginView(), tag: .login, selection: $state ) {
|
||||
NavigationLink(destination: EULAView(state: state), tag: .login, selection: $state ) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user