Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
84183d2b83
|
-105
@@ -1,108 +1,3 @@
|
||||
## [1.4.3-15] - 2023-04-29
|
||||
|
||||
### Added
|
||||
|
||||
- Add q tag to quoted renotes (William Casarin)
|
||||
- Add confirmation alert when clearing all bookmarks (Swift)
|
||||
- Show blurhash placeholders from image metadata (William Casarin)
|
||||
- Add image metadata to image uploads (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Load zaps instantly on events (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix thread incompatibility for clients that add more than one reply tag (amethyst, plebstr)
|
||||
- Preserve order of bookmarks when saving (William Casarin)
|
||||
- Fix crash when you have invalid relays in your relay list (William Casarin)
|
||||
|
||||
|
||||
|
||||
[1.4.3-14]: https://github.com/damus-io/damus/releases/tag/v1.4.3-14
|
||||
|
||||
## [1.4.3-10] - 2023-04-25
|
||||
|
||||
### Added
|
||||
|
||||
- Add paste button to login (Suhail Saqan)
|
||||
- Add nokyctranslate translation option (symbsrcool)
|
||||
- You can now change the default zap type (William Casarin)
|
||||
- Add partial support for different repost variants (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Change 500 custom zap to 420 (William Casarin)
|
||||
- New looks to the custom zaps view (ericholguin)
|
||||
- Adjust attachment images placement when posting (Swift)
|
||||
- Only show friends, not friend-of-friend in friend filter (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix reposts on macos and ipad (William Casarin)
|
||||
- Fix slow reconnection issues (Bryan Montz)
|
||||
- Fix issue where uploaded images were from someone else (Swift)
|
||||
- Fix crash with LibreTranslate server setting selection and remove delisted vern server (Terry Yiu)
|
||||
- Fix buggy zap amounts and wallet selector settings (William Casarin)
|
||||
|
||||
|
||||
[1.4.3-10]: https://github.com/damus-io/damus/releases/tag/v1.4.3-10
|
||||
|
||||
## [1.4.3-2] - 2023-04-17
|
||||
|
||||
### Added
|
||||
|
||||
- Add deep links for local notifications (Swift)
|
||||
- Add thread muting (Terry Yiu)
|
||||
- Preview media uploads when posting (Swift)
|
||||
- Add QR Code in profiles (ericholguin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Always check signatures of profile events (William Casarin)
|
||||
- Ask permission before uploading media (Swift)
|
||||
- Show DM message in local notification (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed repost turning green too early and not reposting sometimes (Swift)
|
||||
- Fix shuffling when choosing users to reply to (Joshua Jiang)
|
||||
- Do not translate own notes if logged in with private key (Terry Yiu)
|
||||
- Load missing profiles from boosts on home view (Gísli Kristjánsson)
|
||||
- Load missing profiles from boosts on profile view (Gísli Kristjánsson)
|
||||
- Fix tap area when mentioning users (OlegAba)
|
||||
- Fix invalid DM author notifications (William Casarin)
|
||||
- Fix relay signal indicator, properly show how many relays you are connected to (William Casarin)
|
||||
|
||||
|
||||
[1.4.3-2]: https://github.com/damus-io/damus/releases/tag/v1.4.3-2
|
||||
|
||||
## [1.4.2-2] - 2023-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Include #btc in custom #bitcoin hashtag (William Casarin)
|
||||
- Make notification dots configurable (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Display follows in most recent to oldest (Luis Cabrera)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix hitches caused by syncronous loading of cached images (William Casarin)
|
||||
- Fix tabs sometimes not switching (William Casarin)
|
||||
|
||||
|
||||
[1.4.2-2]: https://github.com/damus-io/damus/releases/tag/v1.4.2-2
|
||||
|
||||
## [1.4.1-8] - 2023-04-10
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/daltoniam/Starscream.git", majorVersion: 4),
|
||||
.Package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
|
||||
]
|
||||
|
||||
@@ -92,7 +92,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributors welcome! Start by examining known issues: https://github.com/damus-io/damus/issues.
|
||||
Contributors welcome!
|
||||
|
||||
### Code
|
||||
|
||||
@@ -100,11 +100,6 @@ Contributors welcome! Start by examining known issues: https://github.com/damus-
|
||||
|
||||
[git-send-email]: http://git-send-email.io
|
||||
|
||||
### Privacy
|
||||
Your internet protocol (IP) address is exposed to the relays you connect to, and third party media hosters (e.g. nostr.build, imgur.com, giphy.com, youtube.com etc.) that render on Damus. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
|
||||
|
||||
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
|
||||
|
||||
### Translations
|
||||
|
||||
Translators welcome! Join the [Transifex][transifex] project.
|
||||
|
||||
+5
-4
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include "bech32.h"
|
||||
|
||||
typedef unsigned char u8;
|
||||
|
||||
@@ -31,8 +32,8 @@ static inline int is_invalid_url_ending(char c) {
|
||||
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
|
||||
}
|
||||
|
||||
static inline int is_alphanumeric(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
||||
static inline int is_bech32_character(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || bech32_charset_rev[c] != -1;
|
||||
}
|
||||
|
||||
static inline void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
||||
@@ -74,14 +75,14 @@ static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
|
||||
static inline int consume_until_non_bech32_character(struct cursor *cur, int or_end) {
|
||||
char c;
|
||||
int consumedAtLeastOne = 0;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (!is_alphanumeric(c))
|
||||
if (!is_bech32_character(c))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
|
||||
@@ -222,7 +222,7 @@ int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
|
||||
|
||||
start = cur->p;
|
||||
|
||||
if (!consume_until_non_alphanumeric(cur, 1)) {
|
||||
if (!consume_until_non_bech32_character(cur, 1)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
+52
-122
@@ -38,12 +38,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 */; };
|
||||
4C198DEF29F88C6B004C165C /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DEB29F88C6B004C165C /* BlurHashEncode.swift */; };
|
||||
4C198DF029F88C6B004C165C /* Readme.md in Resources */ = {isa = PBXBuildFile; fileRef = 4C198DEC29F88C6B004C165C /* Readme.md */; };
|
||||
4C198DF129F88C6B004C165C /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4C198DED29F88C6B004C165C /* License.txt */; };
|
||||
4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DEE29F88C6B004C165C /* BlurHashDecode.swift */; };
|
||||
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DF429F88D2E004C165C /* ImageMetadata.swift */; };
|
||||
4C198DF829F89323004C165C /* BinaryParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DF729F89323004C165C /* BinaryParser.swift */; };
|
||||
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */; };
|
||||
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */; };
|
||||
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */; };
|
||||
@@ -127,6 +121,7 @@
|
||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9117283D88E40052CD1C /* FollowingModel.swift */; };
|
||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C63334F283D40E500B1C9C3 /* HomeModel.swift */; };
|
||||
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C633351283D419F00B1C9C3 /* SignalModel.swift */; };
|
||||
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C649843285A952100EAE2B3 /* LocalUserConfig.swift */; };
|
||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
|
||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
|
||||
@@ -147,9 +142,8 @@
|
||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */; };
|
||||
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; };
|
||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */; };
|
||||
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */; };
|
||||
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
|
||||
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
|
||||
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
|
||||
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
|
||||
@@ -160,8 +154,6 @@
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
|
||||
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
|
||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
||||
4CA3FA1029F593D000FDB3C3 /* ZapTypePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */; };
|
||||
4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.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 */; };
|
||||
@@ -190,6 +182,7 @@
|
||||
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
|
||||
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
|
||||
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; };
|
||||
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */; };
|
||||
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */; };
|
||||
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; };
|
||||
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
|
||||
@@ -199,14 +192,9 @@
|
||||
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
|
||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
|
||||
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128B29EB19C40006FA5A /* LocalNotification.swift */; };
|
||||
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
|
||||
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
|
||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE1398F29F0661A00AC6A0B /* RepostAction.swift */; };
|
||||
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE1399129F0666100AC6A0B /* ShareActionButton.swift */; };
|
||||
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE1399329F0669900AC6A0B /* BigButton.swift */; };
|
||||
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */; };
|
||||
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F329D779B5005914DB /* PostBox.swift */; };
|
||||
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F729DB7399005914DB /* ThiccDivider.swift */; };
|
||||
@@ -221,6 +209,7 @@
|
||||
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEF727F7A08200C66700 /* damusTests.swift */; };
|
||||
4CE6DF0227F7A08200C66700 /* damusUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0127F7A08200C66700 /* damusUITests.swift */; };
|
||||
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 */; };
|
||||
@@ -257,9 +246,7 @@
|
||||
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */; };
|
||||
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
|
||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
|
||||
50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; };
|
||||
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
|
||||
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
|
||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||
@@ -289,7 +276,7 @@
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
|
||||
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */; };
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
|
||||
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParticipantsView.swift */; };
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -343,6 +330,9 @@
|
||||
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
|
||||
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -350,9 +340,6 @@
|
||||
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>"; };
|
||||
3A821C3E29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A821C3F29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A821C4029E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -399,6 +386,9 @@
|
||||
3AD14EB829C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "sv-SE"; path = "sv-SE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3AD14EB929C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AD14EBA29C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AD14EBB29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AD14EBC29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3AD14EBD29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AD5662B29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AD5662C29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AD5662D29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -423,12 +413,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>"; };
|
||||
4C198DEB29F88C6B004C165C /* BlurHashEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashEncode.swift; sourceTree = "<group>"; };
|
||||
4C198DEC29F88C6B004C165C /* Readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = "<group>"; };
|
||||
4C198DED29F88C6B004C165C /* License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; };
|
||||
4C198DEE29F88C6B004C165C /* BlurHashDecode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||
4C198DF429F88D2E004C165C /* ImageMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageMetadata.swift; sourceTree = "<group>"; };
|
||||
4C198DF729F89323004C165C /* BinaryParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryParser.swift; sourceTree = "<group>"; };
|
||||
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyCounter.swift; sourceTree = "<group>"; };
|
||||
4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.swift; sourceTree = "<group>"; };
|
||||
4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsView.swift; sourceTree = "<group>"; };
|
||||
@@ -542,6 +526,7 @@
|
||||
4C5F9117283D88E40052CD1C /* FollowingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingModel.swift; sourceTree = "<group>"; };
|
||||
4C63334F283D40E500B1C9C3 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; };
|
||||
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
|
||||
4C649843285A952100EAE2B3 /* LocalUserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalUserConfig.swift; sourceTree = "<group>"; };
|
||||
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
|
||||
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
|
||||
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
||||
@@ -566,9 +551,8 @@
|
||||
4C8D00D129E397AD0036AF10 /* block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = block.h; sourceTree = "<group>"; };
|
||||
4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; };
|
||||
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.swift; sourceTree = "<group>"; };
|
||||
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendIcon.swift; sourceTree = "<group>"; };
|
||||
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendsButton.swift; sourceTree = "<group>"; };
|
||||
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; };
|
||||
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; };
|
||||
4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
|
||||
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
|
||||
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
|
||||
@@ -579,8 +563,6 @@
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
|
||||
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapTypePicker.swift; sourceTree = "<group>"; };
|
||||
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringCodable.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>"; };
|
||||
@@ -609,6 +591,7 @@
|
||||
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
|
||||
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
|
||||
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; };
|
||||
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedEventView.swift; sourceTree = "<group>"; };
|
||||
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescription.swift; sourceTree = "<group>"; };
|
||||
4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; };
|
||||
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
|
||||
@@ -618,14 +601,9 @@
|
||||
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
|
||||
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
|
||||
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = "<group>"; };
|
||||
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
|
||||
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
|
||||
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
|
||||
4CE1398F29F0661A00AC6A0B /* RepostAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostAction.swift; sourceTree = "<group>"; };
|
||||
4CE1399129F0666100AC6A0B /* ShareActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareActionButton.swift; sourceTree = "<group>"; };
|
||||
4CE1399329F0669900AC6A0B /* BigButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigButton.swift; sourceTree = "<group>"; };
|
||||
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncedOnChange.swift; sourceTree = "<group>"; };
|
||||
4CE4F0F329D779B5005914DB /* PostBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostBox.swift; sourceTree = "<group>"; };
|
||||
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThiccDivider.swift; sourceTree = "<group>"; };
|
||||
@@ -680,9 +658,7 @@
|
||||
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedEvent.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
|
||||
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
|
||||
50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
|
||||
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
|
||||
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
|
||||
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.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>"; };
|
||||
@@ -711,7 +687,7 @@
|
||||
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
|
||||
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePictureControl.swift; sourceTree = "<group>"; };
|
||||
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
|
||||
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsView.swift; sourceTree = "<group>"; };
|
||||
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -721,6 +697,7 @@
|
||||
files = (
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||
6C7DE41F2955169800E66263 /* Vault in Frameworks */,
|
||||
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */,
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -849,6 +826,7 @@
|
||||
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
|
||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
|
||||
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */,
|
||||
4C649843285A952100EAE2B3 /* LocalUserConfig.swift */,
|
||||
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */,
|
||||
4C216F372871EDE300040376 /* DirectMessageModel.swift */,
|
||||
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */,
|
||||
@@ -869,33 +847,6 @@
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C198DEA29F88C6B004C165C /* BlurHash */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C198DEB29F88C6B004C165C /* BlurHashEncode.swift */,
|
||||
4C198DEC29F88C6B004C165C /* Readme.md */,
|
||||
4C198DED29F88C6B004C165C /* License.txt */,
|
||||
4C198DEE29F88C6B004C165C /* BlurHashDecode.swift */,
|
||||
);
|
||||
path = BlurHash;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C198DF329F88D23004C165C /* Images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C198DF429F88D2E004C165C /* ImageMetadata.swift */,
|
||||
);
|
||||
path = Images;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C198DF629F89317004C165C /* Parser */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C198DF729F89323004C165C /* BinaryParser.swift */,
|
||||
);
|
||||
path = Parser;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C1A9A1B29DDCF8B00516EAC /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -931,7 +882,6 @@
|
||||
4C75EFA227FA576C0006080F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
|
||||
4C1A9A1B29DDCF8B00516EAC /* Settings */,
|
||||
4CFF8F6129CC9A80008DB934 /* Images */,
|
||||
4CCEB7AC29B53D180078AA28 /* Search */,
|
||||
@@ -957,6 +907,7 @@
|
||||
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */,
|
||||
4C216F31286E388800040376 /* DMChatView.swift */,
|
||||
4C216F33286F5ACD00040376 /* DMView.swift */,
|
||||
E990020E2955F837003BBC5A /* EditMetadataView.swift */,
|
||||
3169CAE4294E699400EE4006 /* Empty Views */,
|
||||
4C75EFB82804A2740006080F /* EventView.swift */,
|
||||
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */,
|
||||
@@ -974,7 +925,7 @@
|
||||
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
|
||||
4C363A8B28236B92006E126D /* PubkeyView.swift */,
|
||||
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
|
||||
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */,
|
||||
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
|
||||
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
|
||||
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
|
||||
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
|
||||
@@ -1011,7 +962,7 @@
|
||||
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
|
||||
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
|
||||
4C363A8F28247A1D006E126D /* NostrLink.swift */,
|
||||
50088DA029E8271A008A1FDF /* WebSocket.swift */,
|
||||
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */,
|
||||
);
|
||||
path = Nostr;
|
||||
sourceTree = "<group>";
|
||||
@@ -1019,9 +970,6 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C198DF629F89317004C165C /* Parser */,
|
||||
4C198DF329F88D23004C165C /* Images */,
|
||||
4C198DEA29F88C6B004C165C /* BlurHash */,
|
||||
4CE4F0F329D779B5005914DB /* PostBox.swift */,
|
||||
7C0F392D29B57C8F0039859C /* Extensions */,
|
||||
4CE879492995B58700F758CC /* Relays */,
|
||||
@@ -1059,21 +1007,10 @@
|
||||
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
|
||||
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */,
|
||||
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
|
||||
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */,
|
||||
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
|
||||
50B5685229F97CB400A23243 /* CredentialHandler.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C8D1A6D29F31E4100ACDF75 /* Buttons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */,
|
||||
);
|
||||
path = Buttons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CAAD8AE29888A9B00060CEA /* Relays */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1085,7 +1022,6 @@
|
||||
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
|
||||
4CE879512996B68900F758CC /* RelayType.swift */,
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */,
|
||||
);
|
||||
path = Relays;
|
||||
sourceTree = "<group>";
|
||||
@@ -1096,9 +1032,6 @@
|
||||
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */,
|
||||
4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */,
|
||||
5CF72FC129B9142F00124A13 /* ShareAction.swift */,
|
||||
4CE1398F29F0661A00AC6A0B /* RepostAction.swift */,
|
||||
4CE1399129F0666100AC6A0B /* ShareActionButton.swift */,
|
||||
4CE1399329F0669900AC6A0B /* BigButton.swift */,
|
||||
);
|
||||
path = ActionBar;
|
||||
sourceTree = "<group>";
|
||||
@@ -1117,14 +1050,12 @@
|
||||
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
|
||||
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
|
||||
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
|
||||
E990020E2955F837003BBC5A /* EditMetadataView.swift */,
|
||||
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
|
||||
4C8682862814DE470026224F /* ProfileView.swift */,
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
|
||||
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
|
||||
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
|
||||
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */,
|
||||
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
sourceTree = "<group>";
|
||||
@@ -1133,6 +1064,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
|
||||
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */,
|
||||
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */,
|
||||
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
|
||||
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */,
|
||||
@@ -1307,7 +1239,6 @@
|
||||
children = (
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */,
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
|
||||
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */,
|
||||
);
|
||||
path = Zaps;
|
||||
sourceTree = "<group>";
|
||||
@@ -1391,6 +1322,7 @@
|
||||
);
|
||||
name = damus;
|
||||
packageProductDependencies = (
|
||||
4CE6DF1127F7A2B300C66700 /* Starscream */,
|
||||
4C649880286E0EE300EAE2B3 /* secp256k1 */,
|
||||
4C06670328FC7EC500038D2A /* Kingfisher */,
|
||||
6C7DE41E2955169800E66263 /* Vault */,
|
||||
@@ -1474,7 +1406,8 @@
|
||||
"es-419",
|
||||
"es-ES",
|
||||
fa,
|
||||
fr,
|
||||
"fr-CA",
|
||||
"fr-FR",
|
||||
"hu-HU",
|
||||
id,
|
||||
"it-IT",
|
||||
@@ -1496,6 +1429,7 @@
|
||||
);
|
||||
mainGroup = 4CE6DEDA27F7A08100C66700;
|
||||
packageReferences = (
|
||||
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */,
|
||||
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
|
||||
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */,
|
||||
@@ -1520,8 +1454,6 @@
|
||||
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
|
||||
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
|
||||
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
|
||||
4C198DF129F88C6B004C165C /* License.txt in Resources */,
|
||||
4C198DF029F88C6B004C165C /* Readme.md in Resources */,
|
||||
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1550,7 +1482,6 @@
|
||||
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
|
||||
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */,
|
||||
4C363A8A28236B57006E126D /* MentionView.swift in Sources */,
|
||||
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */,
|
||||
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
|
||||
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
|
||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
|
||||
@@ -1564,7 +1495,6 @@
|
||||
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
|
||||
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
|
||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */,
|
||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
||||
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
|
||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||
@@ -1582,11 +1512,9 @@
|
||||
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */,
|
||||
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
|
||||
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
|
||||
4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */,
|
||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
|
||||
4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */,
|
||||
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
|
||||
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
|
||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
||||
@@ -1598,7 +1526,7 @@
|
||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
||||
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
|
||||
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
|
||||
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
|
||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
||||
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
|
||||
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
|
||||
@@ -1606,7 +1534,6 @@
|
||||
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
|
||||
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
|
||||
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
|
||||
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
|
||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
|
||||
@@ -1617,6 +1544,7 @@
|
||||
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */,
|
||||
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
|
||||
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
||||
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
|
||||
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
|
||||
@@ -1628,7 +1556,6 @@
|
||||
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
|
||||
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */,
|
||||
4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */,
|
||||
4CA3FA1029F593D000FDB3C3 /* ZapTypePicker.swift in Sources */,
|
||||
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
|
||||
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */,
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
|
||||
@@ -1649,10 +1576,10 @@
|
||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
|
||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
||||
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
|
||||
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */,
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
|
||||
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
|
||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
|
||||
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
|
||||
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
|
||||
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
|
||||
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
||||
@@ -1667,6 +1594,7 @@
|
||||
4C1A9A2329DDDB8100516EAC /* IconLabel.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 */,
|
||||
@@ -1693,21 +1621,16 @@
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
4C198DF829F89323004C165C /* BinaryParser.swift in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||
4CE1399229F0666100AC6A0B /* ShareActionButton.swift 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 */,
|
||||
4C198DEF29F88C6B004C165C /* BlurHashEncode.swift in Sources */,
|
||||
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */,
|
||||
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */,
|
||||
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */,
|
||||
50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */,
|
||||
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */,
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
|
||||
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
|
||||
@@ -1724,11 +1647,9 @@
|
||||
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
|
||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
||||
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */,
|
||||
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
|
||||
@@ -1764,7 +1685,6 @@
|
||||
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
|
||||
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */,
|
||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
|
||||
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
|
||||
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
|
||||
@@ -1859,6 +1779,7 @@
|
||||
3A5C4575296A879E0032D398 /* es-419 */,
|
||||
3A2B8B0A296A8982009CC16D /* en-US */,
|
||||
3AEB8005297CCEA900713A25 /* tr-TR */,
|
||||
3A4F3322297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A06297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C22297F2CF80090925E /* it-IT */,
|
||||
3AB5B86C2986D8A3006599D2 /* de */,
|
||||
@@ -1880,10 +1801,10 @@
|
||||
3AD5663229C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB529C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB829C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBC29C40F47009D2D9C /* fr-CA */,
|
||||
3A325AC629C9E0B8002BE7ED /* vi */,
|
||||
3A325AC929C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA929CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C4029E819D500B4BCA7 /* fr */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
@@ -1893,6 +1814,7 @@
|
||||
children = (
|
||||
3ACB685B297633BC00C46468 /* es-419 */,
|
||||
3AEB8003297CCEA800713A25 /* tr-TR */,
|
||||
3A4F3320297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A04297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C20297F2CF80090925E /* it-IT */,
|
||||
3AB5B86A2986D8A3006599D2 /* de */,
|
||||
@@ -1914,10 +1836,10 @@
|
||||
3AD5663329C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB629C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB929C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBB29C40F47009D2D9C /* fr-CA */,
|
||||
3A325AC529C9E0B8002BE7ED /* vi */,
|
||||
3A325AC829C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA829CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C3F29E819D500B4BCA7 /* fr */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1927,6 +1849,7 @@
|
||||
children = (
|
||||
3ACB685E297633BC00C46468 /* es-419 */,
|
||||
3AEB8004297CCEA800713A25 /* tr-TR */,
|
||||
3A4F3321297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A05297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C21297F2CF80090925E /* it-IT */,
|
||||
3AB5B86B2986D8A3006599D2 /* de */,
|
||||
@@ -1949,10 +1872,10 @@
|
||||
3AD5663129C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB729C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBD29C40F47009D2D9C /* fr-CA */,
|
||||
3A325AC429C9E0B8002BE7ED /* vi */,
|
||||
3A325AC729C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA729CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C3E29E819D500B4BCA7 /* fr */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -2088,7 +2011,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2113,12 +2036,9 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.3;
|
||||
MARKETING_VERSION = 1.4.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -2135,7 +2055,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2160,12 +2080,9 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.3;
|
||||
MARKETING_VERSION = 1.4.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -2307,6 +2224,14 @@
|
||||
revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9;
|
||||
};
|
||||
};
|
||||
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/daltoniam/Starscream";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.0.0;
|
||||
};
|
||||
};
|
||||
6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SparrowTek/Vault";
|
||||
@@ -2328,6 +2253,11 @@
|
||||
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||
productName = secp256k1;
|
||||
};
|
||||
4CE6DF1127F7A2B300C66700 /* Starscream */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */;
|
||||
productName = Starscream;
|
||||
};
|
||||
6C7DE41E2955169800E66263 /* Vault */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */;
|
||||
|
||||
@@ -17,6 +17,15 @@
|
||||
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "starscream",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/daltoniam/Starscream",
|
||||
"state" : {
|
||||
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
|
||||
"version" : "4.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "vault",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -37,6 +37,8 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||
text
|
||||
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
|
||||
.font(.system(size: 14, weight: .heavy))
|
||||
.contentShape(Rectangle())
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(
|
||||
Group {
|
||||
@@ -48,7 +50,6 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
.accentColor(tag == selection ? textColor() : .gray)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,53 +37,28 @@ enum ImageShape {
|
||||
case landscape
|
||||
case portrait
|
||||
case unknown
|
||||
|
||||
static func determine_image_shape(_ size: CGSize) -> ImageShape {
|
||||
guard size.height > 0 else {
|
||||
return .unknown
|
||||
}
|
||||
let imageRatio = size.width / size.height
|
||||
switch imageRatio {
|
||||
case 1.0: return .square
|
||||
case ..<1.0: return .portrait
|
||||
case 1.0...: return .landscape
|
||||
default: return .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try either calculated imagefill from the real image or from metadata hints in tags
|
||||
func lookup_imgmeta_size_hint(events: EventCache, url: URL?) -> CGSize? {
|
||||
guard let url,
|
||||
let meta = events.lookup_img_metadata(url: url),
|
||||
let img_size = meta.meta.dim?.size else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return img_size
|
||||
}
|
||||
|
||||
struct ImageCarousel: View {
|
||||
var urls: [URL]
|
||||
|
||||
let evid: String
|
||||
|
||||
let state: DamusState
|
||||
let previews: PreviewCache
|
||||
|
||||
@State private var open_sheet: Bool = false
|
||||
@State private var current_url: URL? = nil
|
||||
@State private var image_fill: ImageFill? = nil
|
||||
@State private var fillHeight: CGFloat = 350
|
||||
@State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 0.85
|
||||
|
||||
let fillHeight: CGFloat = 350
|
||||
let maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2
|
||||
|
||||
init(state: DamusState, evid: String, urls: [URL]) {
|
||||
init(previews: PreviewCache, evid: String, urls: [URL]) {
|
||||
_open_sheet = State(initialValue: false)
|
||||
_current_url = State(initialValue: nil)
|
||||
_image_fill = State(initialValue: state.previews.lookup_image_meta(evid))
|
||||
_image_fill = State(initialValue: previews.lookup_image_meta(evid))
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.state = state
|
||||
self.previews = previews
|
||||
}
|
||||
|
||||
var filling: Bool {
|
||||
@@ -91,70 +66,41 @@ struct ImageCarousel: View {
|
||||
}
|
||||
|
||||
var height: CGFloat {
|
||||
image_fill?.height ?? fillHeight
|
||||
}
|
||||
|
||||
func Placeholder(url: URL, geo_size: CGSize) -> some View {
|
||||
Group {
|
||||
if let meta = state.events.lookup_img_metadata(url: url),
|
||||
case .processed(let blurhash) = meta.state {
|
||||
Image(uiImage: blurhash)
|
||||
.resizable()
|
||||
.frame(width: geo_size.width * UIScreen.main.scale, height: self.height * UIScreen.main.scale)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.image_fill == nil,
|
||||
let meta = state.events.lookup_img_metadata(url: url),
|
||||
let size = meta.meta.dim?.size
|
||||
{
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||
self.image_fill = fill
|
||||
}
|
||||
}
|
||||
image_fill?.height ?? 0
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
ForEach(urls, id: \.absoluteString) { url in
|
||||
GeometryReader { geo in
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
Rectangle()
|
||||
.foregroundColor(Color.clear)
|
||||
.overlay {
|
||||
GeometryReader { geo in
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
previews.cache_image_meta(evid: evid, image_fill: fill)
|
||||
image_fill = fill
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
state.previews.cache_image_meta(evid: evid, image_fill: fill)
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
image_fill = fill
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $open_sheet) {
|
||||
ImageView(urls: urls, disable_animation: state.settings.disable_animation)
|
||||
ImageView(urls: urls)
|
||||
}
|
||||
.frame(height: self.height)
|
||||
.frame(height: height)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
@@ -172,7 +118,10 @@ extension KFOptionSetter {
|
||||
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
|
||||
let img_size = image.size
|
||||
let geo_size = size
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: img_size, maxHeight: max, fillHeight: fill)
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size,
|
||||
img_size: img_size,
|
||||
maxHeight: max,
|
||||
fillHeight: fill)
|
||||
DispatchQueue.main.async { [block, fill] in
|
||||
try? block(fill)
|
||||
}
|
||||
@@ -188,14 +137,25 @@ public struct ImageFill {
|
||||
let filling: Bool?
|
||||
let height: CGFloat
|
||||
|
||||
|
||||
static func determine_image_shape(_ size: CGSize) -> ImageShape {
|
||||
guard size.height > 0 else {
|
||||
return .unknown
|
||||
}
|
||||
let imageRatio = size.width / size.height
|
||||
switch imageRatio {
|
||||
case 1.0: return .square
|
||||
case ..<1.0: return .portrait
|
||||
case 1.0...: return .landscape
|
||||
default: return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
static func calculate_image_fill(geo_size: CGSize, img_size: CGSize, maxHeight: CGFloat, fillHeight: CGFloat) -> ImageFill {
|
||||
let shape = ImageShape.determine_image_shape(img_size)
|
||||
let shape = determine_image_shape(img_size)
|
||||
|
||||
let xfactor = geo_size.width / img_size.width
|
||||
let scaled = img_size.height * xfactor
|
||||
|
||||
//print("calc_img_fill \(img_size.width)x\(img_size.height) xfactor:\(xfactor) scaled:\(scaled)")
|
||||
|
||||
// calculate scaled image height
|
||||
// set scale factor and constrain images to minimum 150
|
||||
// and animations to scaled factor for dynamic size adjustment
|
||||
@@ -212,7 +172,7 @@ public struct ImageFill {
|
||||
|
||||
struct ImageCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
|
||||
ImageCarousel(previews: test_damus_state().previews, evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ struct InvoiceView: View {
|
||||
let invoice: Invoice
|
||||
@State var showing_select_wallet: Bool = false
|
||||
@State var copied = false
|
||||
let settings: UserSettingsStore
|
||||
|
||||
var CopyButton: some View {
|
||||
Button {
|
||||
@@ -37,10 +36,10 @@ struct InvoiceView: View {
|
||||
|
||||
var PayButton: some View {
|
||||
Button {
|
||||
if settings.show_wallet_selector {
|
||||
if should_show_wallet_selector(our_pubkey) {
|
||||
showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
|
||||
open_with_wallet(wallet: get_default_wallet(our_pubkey).model, invoice: invoice.string)
|
||||
}
|
||||
} label: {
|
||||
RoundedRectangle(cornerRadius: 20, style: .circular)
|
||||
@@ -81,7 +80,7 @@ struct InvoiceView: View {
|
||||
.padding(30)
|
||||
}
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(default_wallet: settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
|
||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,8 +111,7 @@ let test_invoice = Invoice(description: .description("this is a description"), a
|
||||
|
||||
struct InvoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InvoiceView(our_pubkey: "", invoice: test_invoice, settings: test_damus_state().settings)
|
||||
InvoiceView(our_pubkey: "", invoice: test_invoice)
|
||||
.frame(width: 300, height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
struct InvoicesView: View {
|
||||
let our_pubkey: String
|
||||
var invoices: [Invoice]
|
||||
let settings: UserSettingsStore
|
||||
|
||||
@State var open_sheet: Bool = false
|
||||
@State var current_invoice: Invoice? = nil
|
||||
@@ -18,7 +17,7 @@ struct InvoicesView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
ForEach(invoices, id: \.string) { invoice in
|
||||
InvoiceView(our_pubkey: our_pubkey, invoice: invoice, settings: settings)
|
||||
InvoiceView(our_pubkey: our_pubkey, invoice: invoice)
|
||||
.tabItem {
|
||||
Text(invoice.string)
|
||||
}
|
||||
@@ -32,7 +31,7 @@ struct InvoicesView: View {
|
||||
|
||||
struct InvoicesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)], settings: test_damus_state().settings)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ struct NIP05Badge: View {
|
||||
.mask(Image(systemName: "checkmark.seal.fill")
|
||||
.resizable()
|
||||
).frame(width: 14, height: 14)
|
||||
} else if show_domain {
|
||||
} else {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.footnote)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ struct Translated: Equatable {
|
||||
|
||||
enum TranslateStatus: Equatable {
|
||||
case havent_tried
|
||||
case trying
|
||||
case translating
|
||||
case translated(Translated)
|
||||
case not_needed
|
||||
@@ -25,19 +26,34 @@ struct TranslateView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let size: EventViewKind
|
||||
let currentLanguage: String
|
||||
|
||||
@ObservedObject var translations_model: TranslationModel
|
||||
@State var translated: TranslateStatus
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.size = size
|
||||
self._translations_model = ObservedObject(wrappedValue: damus_state.events.get_cache_data(event.id).translations_model)
|
||||
|
||||
if #available(iOS 16, *) {
|
||||
self.currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
|
||||
} else {
|
||||
self.currentLanguage = Locale.current.languageCode ?? "en"
|
||||
}
|
||||
|
||||
if let cached = damus_state.events.lookup_translated_artifacts(evid: event.id) {
|
||||
self._translated = State(initialValue: cached)
|
||||
} else {
|
||||
let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried
|
||||
self._translated = State(initialValue: initval)
|
||||
}
|
||||
}
|
||||
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
var TranslateButton: some View {
|
||||
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
||||
translate()
|
||||
self.translated = .trying
|
||||
}
|
||||
.translate_button_style()
|
||||
}
|
||||
@@ -58,43 +74,78 @@ struct TranslateView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func translate() {
|
||||
Task {
|
||||
guard let note_language = translations_model.note_language else {
|
||||
return
|
||||
}
|
||||
let res = await translate_note(profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, event: event, settings: damus_state.settings, note_lang: note_language)
|
||||
DispatchQueue.main.async {
|
||||
self.translations_model.state = res
|
||||
}
|
||||
func failed_attempt() {
|
||||
DispatchQueue.main.async {
|
||||
self.translated = .not_needed
|
||||
damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed)
|
||||
}
|
||||
}
|
||||
|
||||
func attempt_translation() {
|
||||
guard should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings, note_lang: self.translations_model.note_language), damus_state.settings.auto_translate else {
|
||||
func attempt_translation() async {
|
||||
guard case .trying = translated else {
|
||||
return
|
||||
}
|
||||
|
||||
translate()
|
||||
}
|
||||
|
||||
func should_transl(_ note_lang: String) -> Bool {
|
||||
should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings, note_lang: note_lang)
|
||||
guard damus_state.settings.can_translate(damus_state.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
let note_lang = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
|
||||
|
||||
// Don't translate if its in our preferred languages
|
||||
guard !preferredLanguages.contains(note_lang) else {
|
||||
failed_attempt()
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.translated = .translating
|
||||
}
|
||||
|
||||
// If the note language is different from our preferred languages, send a translation request.
|
||||
let translator = Translator(damus_state.settings)
|
||||
let originalContent = event.get_content(damus_state.keypair.privkey)
|
||||
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: currentLanguage)
|
||||
|
||||
guard let translated_note else {
|
||||
// if its the same, give up and don't retry
|
||||
failed_attempt()
|
||||
return
|
||||
}
|
||||
|
||||
guard originalContent != translated_note else {
|
||||
// if its the same, give up and don't retry
|
||||
failed_attempt()
|
||||
return
|
||||
}
|
||||
|
||||
// Render translated note
|
||||
let translated_blocks = event.get_blocks(content: translated_note)
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
|
||||
// and cache it
|
||||
DispatchQueue.main.async {
|
||||
self.translated = .translated(Translated(artifacts: artifacts, language: note_lang))
|
||||
damus_state.events.store_translation_artifacts(evid: event.id, translated: self.translated)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch self.translations_model.state {
|
||||
switch translated {
|
||||
case .havent_tried:
|
||||
if damus_state.settings.auto_translate {
|
||||
Text("")
|
||||
} else if let note_lang = translations_model.note_language, should_transl(note_lang) {
|
||||
TranslateButton
|
||||
} else {
|
||||
Text("")
|
||||
TranslateButton
|
||||
}
|
||||
case .translating:
|
||||
case .trying:
|
||||
Text("")
|
||||
case .translating:
|
||||
Text("Translating...", comment: "Text to display when waiting for the translation of a note to finish processing before showing it.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
.padding([.top, .bottom], 10)
|
||||
case .translated(let translated):
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
|
||||
TranslatedView(lang: languageName, artifacts: translated.artifacts)
|
||||
@@ -102,8 +153,17 @@ struct TranslateView: View {
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
.onChange(of: translated) { val in
|
||||
guard case .trying = translated else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
await attempt_translation()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
attempt_translation()
|
||||
await attempt_translation()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,37 +183,3 @@ struct TranslateView_Previews: PreviewProvider {
|
||||
TranslateView(damus_state: ds, event: test_event, size: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
func translate_note(profiles: Profiles, privkey: String?, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
|
||||
|
||||
// If the note language is different from our preferred languages, send a translation request.
|
||||
let translator = Translator(settings)
|
||||
let originalContent = event.get_content(privkey)
|
||||
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
|
||||
|
||||
guard let translated_note else {
|
||||
// if its the same, give up and don't retry
|
||||
return .not_needed
|
||||
}
|
||||
|
||||
guard originalContent != translated_note else {
|
||||
// if its the same, give up and don't retry
|
||||
return .not_needed
|
||||
}
|
||||
|
||||
// Render translated note
|
||||
let translated_blocks = event.get_blocks(content: translated_note)
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles, privkey: privkey)
|
||||
|
||||
// and cache it
|
||||
return .translated(Translated(artifacts: artifacts, language: note_lang))
|
||||
}
|
||||
|
||||
func current_language() -> String {
|
||||
if #available(iOS 16, *) {
|
||||
return Locale.current.language.languageCode?.identifier ?? "en"
|
||||
} else {
|
||||
return Locale.current.languageCode ?? "en"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,37 +7,14 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UserViewRow: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
|
||||
@State var navigating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
|
||||
|
||||
UserView(damus_state: damus_state, pubkey: pubkey)
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
NavigationLink(destination: dest, isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
.onTapGesture {
|
||||
navigating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserView: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
@@ -51,6 +28,7 @@ struct UserView: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ struct ZapButton: View {
|
||||
return "bolt"
|
||||
}
|
||||
|
||||
return "bolt.fill"
|
||||
return "bolt.horizontal.fill"
|
||||
}
|
||||
|
||||
var zap_color: Color? {
|
||||
@@ -86,7 +86,7 @@ struct ZapButton: View {
|
||||
return
|
||||
}
|
||||
|
||||
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
|
||||
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: ZapType.pub)
|
||||
self.zapping = true
|
||||
})
|
||||
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||
@@ -101,7 +101,7 @@ struct ZapButton: View {
|
||||
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
||||
}
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
|
||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { notif in
|
||||
let zap_ev = notif.object as! ZappingEvent
|
||||
@@ -118,12 +118,11 @@ struct ZapButton: View {
|
||||
case .failed:
|
||||
break
|
||||
case .got_zap_invoice(let inv):
|
||||
if damus_state.settings.show_wallet_selector {
|
||||
if should_show_wallet_selector(damus_state.pubkey) {
|
||||
self.invoice = inv
|
||||
self.showing_select_wallet = true
|
||||
} else {
|
||||
let wallet = damus_state.settings.default_wallet.model
|
||||
open_with_wallet(wallet: wallet, invoice: inv)
|
||||
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +173,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||
}
|
||||
|
||||
let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount
|
||||
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
|
||||
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
|
||||
DispatchQueue.main.async {
|
||||
|
||||
+92
-85
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Starscream
|
||||
|
||||
struct TimestampedProfile {
|
||||
let profile: Profile
|
||||
@@ -14,15 +15,17 @@ struct TimestampedProfile {
|
||||
}
|
||||
|
||||
enum Sheets: Identifiable {
|
||||
case post(PostAction)
|
||||
case post
|
||||
case report(ReportTarget)
|
||||
case reply(NostrEvent)
|
||||
case event(NostrEvent)
|
||||
case filter
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .report: return "report"
|
||||
case .post(let action): return "post-" + (action.ev?.id ?? "")
|
||||
case .post: return "post"
|
||||
case .reply(let ev): return "reply-" + ev.id
|
||||
case .event(let ev): return "event-" + ev.id
|
||||
case .filter: return "filter"
|
||||
}
|
||||
@@ -62,7 +65,8 @@ struct ContentView: View {
|
||||
@State var status: String = "Not connected"
|
||||
@State var active_sheet: Sheets? = nil
|
||||
@State var damus_state: DamusState? = nil
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||
@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
|
||||
@@ -76,7 +80,8 @@ struct ContentView: View {
|
||||
@State var confirm_mute: Bool = false
|
||||
@State var user_muted_confirm: Bool = false
|
||||
@State var confirm_overwrite_mutelist: Bool = false
|
||||
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||
@State var current_boost: NostrEvent? = nil
|
||||
@State var filter_state : FilterState = .posts_and_replies
|
||||
@State private var isSideBarOpened = false
|
||||
@StateObject var home: HomeModel = HomeModel()
|
||||
|
||||
@@ -86,19 +91,14 @@ struct ContentView: View {
|
||||
let sub_id = UUID().description
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var mystery: some View {
|
||||
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
||||
.id("what")
|
||||
}
|
||||
|
||||
|
||||
var PostingTimelineView: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
TabView(selection: $filter_state) {
|
||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||
mystery
|
||||
|
||||
Text("")
|
||||
.id("what")
|
||||
contentTimelineView(filter: FilterState.posts.filter)
|
||||
.tag(FilterState.posts)
|
||||
.id(FilterState.posts)
|
||||
@@ -110,7 +110,7 @@ struct ContentView: View {
|
||||
|
||||
if privkey != nil {
|
||||
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
|
||||
self.active_sheet = .post(.posting)
|
||||
self.active_sheet = .post
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,11 @@ struct ContentView: View {
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
|
||||
case .dms:
|
||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
|
||||
DirectMessagesView(damus_state: damus_state!)
|
||||
.environmentObject(home.dms)
|
||||
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
|
||||
@@ -206,7 +210,7 @@ struct ContentView: View {
|
||||
var MaybeSearchView: some View {
|
||||
Group {
|
||||
if let search = self.active_search {
|
||||
SearchView(appstate: damus_state!, search: SearchModel(state: damus_state!, search: search))
|
||||
SearchView(appstate: damus_state!, search: SearchModel(contacts: damus_state!.contacts, pool: damus_state!.pool, search: search))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -239,11 +243,6 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func open_event(ev: NostrEvent) {
|
||||
self.active_event = ev
|
||||
self.thread_open = true
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let damus = self.damus_state {
|
||||
@@ -255,7 +254,7 @@ struct ContentView: View {
|
||||
Button {
|
||||
isSideBarOpened.toggle()
|
||||
} label: {
|
||||
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, disable_animation: damus_state!.settings.disable_animation)
|
||||
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
@@ -264,7 +263,13 @@ struct ContentView: View {
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack(alignment: .center) {
|
||||
SignalView(state: damus_state!, signal: home.signal)
|
||||
if home.signal.signal != home.signal.max_signal {
|
||||
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
|
||||
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
// maybe expand this to other timelines in the future
|
||||
if selected_timeline == .search {
|
||||
@@ -303,12 +308,14 @@ struct ContentView: View {
|
||||
switch item {
|
||||
case .report(let target):
|
||||
MaybeReportView(target: target)
|
||||
case .post(let action):
|
||||
PostView(action: action, damus_state: damus_state!)
|
||||
case .post:
|
||||
PostView(replying_to: nil, damus_state: damus_state!)
|
||||
case .reply(let event):
|
||||
PostView(replying_to: event, damus_state: damus_state!)
|
||||
case .event:
|
||||
EventDetailView()
|
||||
case .filter:
|
||||
let timeline = selected_timeline
|
||||
let timeline = selected_timeline ?? .home
|
||||
if #available(iOS 16.0, *) {
|
||||
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||
.presentationDetents([.height(550)])
|
||||
@@ -331,9 +338,10 @@ struct ContentView: View {
|
||||
} else if ref.key == "e" {
|
||||
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
|
||||
if let ev {
|
||||
open_event(ev: ev)
|
||||
active_event = ev
|
||||
}
|
||||
}
|
||||
thread_open = true
|
||||
}
|
||||
case .filter(let filt):
|
||||
active_search = filt
|
||||
@@ -343,9 +351,19 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
}
|
||||
.onReceive(handle_notify(.compose)) { notif in
|
||||
let action = notif.object as! PostAction
|
||||
self.active_sheet = .post(action)
|
||||
.onReceive(handle_notify(.boost)) { notif in
|
||||
current_boost = (notif.object as? NostrEvent)
|
||||
}
|
||||
.onReceive(handle_notify(.open_thread)) { obj in
|
||||
//let ev = obj.object as! NostrEvent
|
||||
//thread.set_active_event(ev)
|
||||
//is_thread_open = true
|
||||
}
|
||||
.onReceive(handle_notify(.reply)) { notif in
|
||||
let ev = notif.object as! NostrEvent
|
||||
self.active_sheet = .reply(ev)
|
||||
}
|
||||
.onReceive(handle_notify(.like)) { like in
|
||||
}
|
||||
.onReceive(handle_notify(.deleted_account)) { notif in
|
||||
self.is_deleted_account = true
|
||||
@@ -451,56 +469,13 @@ struct ContentView: View {
|
||||
self.damus_state?.pool.connect_to_disconnected()
|
||||
}
|
||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||
home.filter_events()
|
||||
home.filter_muted()
|
||||
}
|
||||
.onReceive(handle_notify(.mute_thread)) { notif in
|
||||
home.filter_events()
|
||||
home.filter_muted()
|
||||
}
|
||||
.onReceive(handle_notify(.unmute_thread)) { notif in
|
||||
home.filter_events()
|
||||
}
|
||||
.onReceive(handle_notify(.local_notification)) { notif in
|
||||
|
||||
guard let local = notif.object as? LossyLocalNotification,
|
||||
let damus_state else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let target = damus_state.events.lookup(local.event_id) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch local.type {
|
||||
case .dm:
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.open_dm_by_pk(target.pubkey)
|
||||
|
||||
case .like: fallthrough
|
||||
case .zap: fallthrough
|
||||
case .mention: fallthrough
|
||||
case .repost:
|
||||
open_event(ev: target)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
|
||||
let hide = notif.object as! Bool
|
||||
home.filter_events()
|
||||
|
||||
guard let damus_state else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
profile.reactions = !hide
|
||||
|
||||
guard let profile_ev = make_metadata_event(keypair: damus_state.keypair, metadata: profile) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.postbox.send(profile_ev)
|
||||
home.filter_muted()
|
||||
}
|
||||
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
|
||||
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
|
||||
@@ -589,6 +564,18 @@ struct ContentView: View {
|
||||
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
|
||||
}
|
||||
})
|
||||
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) {
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
|
||||
current_boost = nil
|
||||
}
|
||||
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
|
||||
if let current_boost {
|
||||
self.damus_state?.pool.send(.event(current_boost))
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
|
||||
}
|
||||
}
|
||||
|
||||
func switch_timeline(_ timeline: Timeline) {
|
||||
@@ -626,18 +613,13 @@ struct ContentView: View {
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in bootstrap_relays {
|
||||
if let url = RelayURL(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)
|
||||
|
||||
// dumb stuff needed for property wrappers
|
||||
UserSettingsStore.pubkey = pubkey
|
||||
let settings = UserSettingsStore()
|
||||
UserSettingsStore.shared = settings
|
||||
|
||||
|
||||
self.damus_state = DamusState(pool: pool,
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
@@ -649,7 +631,7 @@ struct ContentView: View {
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
lnurls: LNUrls(),
|
||||
settings: settings,
|
||||
settings: UserSettingsStore(),
|
||||
relay_filters: relay_filters,
|
||||
relay_metadata: metadatas,
|
||||
drafts: Drafts(),
|
||||
@@ -658,7 +640,7 @@ struct ContentView: View {
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
muted_threads: MutedThreadsManager(keypair: keypair)
|
||||
muted_threads: MutedThreadsManager(pubkey: pubkey)
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
@@ -682,6 +664,31 @@ func get_since_time(last_event: NostrEvent?) -> Int64? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ws_nostr_event(relay: String, ev: WebSocketEvent) -> NostrEvent? {
|
||||
switch ev {
|
||||
case .binary(let dat):
|
||||
return NostrEvent(content: "binary data? \(dat.count) bytes", pubkey: relay)
|
||||
case .cancelled:
|
||||
return NostrEvent(content: "cancelled", pubkey: relay)
|
||||
case .connected:
|
||||
return NostrEvent(content: "connected", pubkey: relay)
|
||||
case .disconnected:
|
||||
return NostrEvent(content: "disconnected", pubkey: relay)
|
||||
case .error(let err):
|
||||
return NostrEvent(content: "error \(err.debugDescription)", pubkey: relay)
|
||||
case .text(let txt):
|
||||
return NostrEvent(content: "text \(txt)", pubkey: relay)
|
||||
case .pong:
|
||||
return NostrEvent(content: "pong", pubkey: relay)
|
||||
case .ping:
|
||||
return NostrEvent(content: "ping", pubkey: relay)
|
||||
case .viabilityChanged(let b):
|
||||
return NostrEvent(content: "viabilityChanged \(b)", pubkey: relay)
|
||||
case .reconnectSuggested(let b):
|
||||
return NostrEvent(content: "reconnectSuggested \(b)", pubkey: relay)
|
||||
}
|
||||
}
|
||||
|
||||
func is_notification(ev: NostrEvent, pubkey: String) -> Bool {
|
||||
if ev.pubkey == pubkey {
|
||||
return false
|
||||
@@ -795,7 +802,7 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
|
||||
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
|
||||
|
||||
if search_type == .profile {
|
||||
filter.kinds = [NostrKind.metadata.rawValue]
|
||||
filter.kinds = [0]
|
||||
}
|
||||
|
||||
filter.limit = 1
|
||||
|
||||
@@ -23,18 +23,6 @@ class ActionBarModel: ObservableObject {
|
||||
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||
}
|
||||
|
||||
init() {
|
||||
self.our_like = nil
|
||||
self.our_boost = nil
|
||||
self.our_reply = nil
|
||||
self.our_zap = nil
|
||||
self.likes = 0
|
||||
self.boosts = 0
|
||||
self.zaps = 0
|
||||
self.zap_total = 0
|
||||
self.replies = 0
|
||||
}
|
||||
|
||||
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) {
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
|
||||
@@ -19,7 +19,7 @@ func load_bookmarks(pubkey: String) -> [NostrEvent] {
|
||||
}
|
||||
|
||||
func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
|
||||
let uniq_bookmarks = uniq(value)
|
||||
let uniq_bookmarks = Array(Set(value))
|
||||
|
||||
if uniq_bookmarks != current_value {
|
||||
let encoded = uniq_bookmarks.map(event_to_json)
|
||||
|
||||
@@ -242,7 +242,7 @@ func follow_with_existing_contacts(our_pubkey: String, our_contacts: NostrEvent,
|
||||
|
||||
func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {
|
||||
return relays.reduce(into: [:]) { acc, relay in
|
||||
acc[relay.url.url.absoluteString] = relay.info
|
||||
acc[relay.url.absoluteString] = relay.info
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,6 @@ struct DamusState {
|
||||
let replies: ReplyCounter
|
||||
let muted_threads: MutedThreadsManager
|
||||
|
||||
@discardableResult
|
||||
func add_zap(zap: Zap) -> Bool {
|
||||
// store generic zap mapping
|
||||
self.zaps.add_zap(zap: zap)
|
||||
// associate with events as well
|
||||
return self.events.store_zap(zap: zap)
|
||||
}
|
||||
|
||||
var pubkey: String {
|
||||
return keypair.pubkey
|
||||
}
|
||||
@@ -47,8 +39,7 @@ struct DamusState {
|
||||
keypair.privkey != nil
|
||||
}
|
||||
|
||||
static var settings_pubkey: String? = nil
|
||||
|
||||
static var empty: DamusState {
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil))) }
|
||||
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(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(pubkey: ""))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DeepLPlan: String, CaseIterable, Identifiable, StringCodable {
|
||||
init?(from string: String) {
|
||||
guard let dl = DeepLPlan(rawValue: string) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self = dl
|
||||
}
|
||||
|
||||
func to_string() -> String {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
enum DeepLPlan: String, CaseIterable, Identifiable {
|
||||
var id: String { self.rawValue }
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
|
||||
@@ -16,8 +16,6 @@ class DirectMessageModel: ObservableObject {
|
||||
|
||||
@Published var draft: String
|
||||
|
||||
let pubkey: String
|
||||
|
||||
var is_request: Bool
|
||||
var our_pubkey: String
|
||||
|
||||
@@ -31,19 +29,17 @@ class DirectMessageModel: ObservableObject {
|
||||
return true
|
||||
}
|
||||
|
||||
init(events: [NostrEvent], our_pubkey: String, pubkey: String) {
|
||||
init(events: [NostrEvent], our_pubkey: String) {
|
||||
self.events = events
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
init(our_pubkey: String, pubkey: String) {
|
||||
init(our_pubkey: String) {
|
||||
self.events = []
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,43 +8,20 @@
|
||||
import Foundation
|
||||
|
||||
class DirectMessagesModel: ObservableObject {
|
||||
@Published var dms: [DirectMessageModel] = []
|
||||
@Published var dms: [(String, DirectMessageModel)] = []
|
||||
@Published var loading: Bool = false
|
||||
@Published var open_dm: Bool = false
|
||||
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: "", pubkey: "")
|
||||
let our_pubkey: String
|
||||
|
||||
init(our_pubkey: String) {
|
||||
self.our_pubkey = our_pubkey
|
||||
}
|
||||
|
||||
var message_requests: [DirectMessageModel] {
|
||||
return dms.filter { dm in dm.is_request }
|
||||
var message_requests: [(String, DirectMessageModel)] {
|
||||
return dms.filter { dm in dm.1.is_request }
|
||||
}
|
||||
|
||||
var friend_dms: [DirectMessageModel] {
|
||||
return dms.filter { dm in !dm.is_request }
|
||||
}
|
||||
|
||||
func set_active_dm_model(_ model: DirectMessageModel) {
|
||||
self.active_model = model
|
||||
}
|
||||
|
||||
func open_dm_by_pk(_ pubkey: String) {
|
||||
self.set_active_dm(pubkey)
|
||||
self.open_dm = true
|
||||
}
|
||||
|
||||
func open_dm_by_model(_ model: DirectMessageModel) {
|
||||
self.set_active_dm_model(model)
|
||||
self.open_dm = true
|
||||
}
|
||||
|
||||
func set_active_dm(_ pubkey: String) {
|
||||
for model in self.dms where model.pubkey == pubkey {
|
||||
self.set_active_dm_model(model)
|
||||
break
|
||||
}
|
||||
var friend_dms: [(String, DirectMessageModel)] {
|
||||
return dms.filter { dm in !dm.1.is_request }
|
||||
}
|
||||
|
||||
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
|
||||
@@ -52,15 +29,15 @@ class DirectMessagesModel: ObservableObject {
|
||||
return dm
|
||||
}
|
||||
|
||||
let new = DirectMessageModel(our_pubkey: our_pubkey, pubkey: pubkey)
|
||||
dms.append(new)
|
||||
let new = DirectMessageModel(our_pubkey: our_pubkey)
|
||||
dms.append((pubkey, new))
|
||||
return new
|
||||
}
|
||||
|
||||
func lookup(_ pubkey: String) -> DirectMessageModel? {
|
||||
for dm in dms {
|
||||
if pubkey == dm.pubkey {
|
||||
return dm
|
||||
if pubkey == dm.0 {
|
||||
return dm.1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,23 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class DraftArtifacts {
|
||||
var content: NSMutableAttributedString
|
||||
var media: [UploadedMedia]
|
||||
|
||||
init() {
|
||||
self.content = NSMutableAttributedString(string: "")
|
||||
self.media = []
|
||||
}
|
||||
|
||||
init(content: NSMutableAttributedString, media: [UploadedMedia]) {
|
||||
self.content = content
|
||||
self.media = media
|
||||
}
|
||||
}
|
||||
|
||||
class Drafts: ObservableObject {
|
||||
@Published var post: DraftArtifacts? = nil
|
||||
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
|
||||
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
|
||||
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
|
||||
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class FollowersModel: ObservableObject {
|
||||
if ev.known_kind == .contacts {
|
||||
handle_contact_event(ev)
|
||||
} else if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
case .notice(let msg):
|
||||
|
||||
@@ -22,7 +22,7 @@ class FollowingModel {
|
||||
}
|
||||
|
||||
func get_filter() -> NostrFilter {
|
||||
var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
|
||||
var f = NostrFilter.filter_kinds([0])
|
||||
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
|
||||
// don't fetch profiles we already have
|
||||
if damus_state.profiles.lookup(id: pk) != nil {
|
||||
@@ -62,7 +62,7 @@ class FollowingModel {
|
||||
break
|
||||
case .event(_, let ev):
|
||||
if ev.kind == 0 {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, 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)")
|
||||
|
||||
+123
-193
@@ -41,27 +41,36 @@ class HomeModel: ObservableObject {
|
||||
let dms_subid = UUID().description
|
||||
let init_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
|
||||
var loading: Bool = false
|
||||
|
||||
var signal = SignalModel()
|
||||
|
||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||
@Published var notifications = NotificationsModel()
|
||||
@Published var dms: DirectMessagesModel
|
||||
@Published var events = EventHolder()
|
||||
@Published var loading: Bool = false
|
||||
@Published var signal: SignalModel = SignalModel()
|
||||
|
||||
init() {
|
||||
self.damus_state = DamusState.empty
|
||||
filter_events()
|
||||
self.setup_debouncer()
|
||||
self.dms = DirectMessagesModel(our_pubkey: "")
|
||||
filter_muted()
|
||||
}
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
|
||||
self.setup_debouncer()
|
||||
filter_muted()
|
||||
}
|
||||
|
||||
var pool: RelayPool {
|
||||
return damus_state.pool
|
||||
}
|
||||
|
||||
var dms: DirectMessagesModel {
|
||||
return damus_state.dms
|
||||
func setup_debouncer() {
|
||||
// turn off debouncer after initial load
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
self.should_debounce_dms = false
|
||||
}
|
||||
}
|
||||
|
||||
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
|
||||
@@ -72,13 +81,6 @@ class HomeModel: ObservableObject {
|
||||
|
||||
return has_event[sub_id]!.contains(ev_id)
|
||||
}
|
||||
|
||||
func setup_debouncer() {
|
||||
// turn off debouncer after initial load
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
self.should_debounce_dms = false
|
||||
}
|
||||
}
|
||||
|
||||
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
||||
@@ -128,7 +130,7 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.add_zap(zap: zap)
|
||||
damus_state.zaps.add_zap(zap: zap)
|
||||
|
||||
guard zap.target.pubkey == our_keypair.pubkey else {
|
||||
return
|
||||
@@ -145,7 +147,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
if damus_state.settings.zap_notification {
|
||||
// Create in-app local notification for zap received.
|
||||
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
|
||||
create_in_app_zap_notification(profiles: profiles, zap: zap)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,31 +188,27 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_channel_create(_ ev: NostrEvent) {
|
||||
guard ev.is_valid else {
|
||||
return
|
||||
}
|
||||
|
||||
self.channels[ev.id] = ev
|
||||
}
|
||||
|
||||
func handle_channel_meta(_ ev: NostrEvent) {
|
||||
}
|
||||
|
||||
func filter_events() {
|
||||
events.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey)
|
||||
}
|
||||
|
||||
self.dms.dms = dms.dms.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey)
|
||||
}
|
||||
|
||||
notifications.filter { ev in
|
||||
if damus_state.settings.onlyzaps_mode && ev.known_kind == NostrKind.like {
|
||||
return false
|
||||
}
|
||||
|
||||
return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey)
|
||||
}
|
||||
func filter_muted() {
|
||||
events.filter { !damus_state.contacts.is_muted($0.pubkey) && !damus_state.muted_threads.isMutedThread($0) }
|
||||
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
|
||||
notifications.filter_and_build_notifications(damus_state)
|
||||
}
|
||||
|
||||
func handle_delete_event(_ ev: NostrEvent) {
|
||||
guard ev.is_valid else {
|
||||
return
|
||||
}
|
||||
|
||||
self.deleted_events.insert(ev.id)
|
||||
}
|
||||
|
||||
@@ -229,22 +227,16 @@ class HomeModel: ObservableObject {
|
||||
func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
|
||||
var boost_ev_id = ev.last_refid()?.ref_id
|
||||
|
||||
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
||||
if let inner_ev = ev.inner_event {
|
||||
boost_ev_id = inner_ev.id
|
||||
|
||||
|
||||
Task.init {
|
||||
guard validate_event(ev: inner_ev) == .ok else {
|
||||
return
|
||||
}
|
||||
|
||||
if inner_ev.is_textlike {
|
||||
DispatchQueue.main.async {
|
||||
self.handle_text_event(sub_id: sub_id, ev)
|
||||
}
|
||||
}
|
||||
guard inner_ev.is_valid else {
|
||||
return
|
||||
}
|
||||
|
||||
if inner_ev.is_textlike {
|
||||
handle_text_event(sub_id: sub_id, ev)
|
||||
}
|
||||
}
|
||||
|
||||
guard let e = boost_ev_id else {
|
||||
@@ -267,18 +259,14 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if damus_state.settings.onlyzaps_mode {
|
||||
return
|
||||
}
|
||||
|
||||
switch damus_state.likes.add_event(ev, target: e.ref_id) {
|
||||
case .already_counted:
|
||||
break
|
||||
case .success(let n):
|
||||
handle_notification(ev: ev)
|
||||
let liked = Counted(event: ev, id: e.ref_id, total: n)
|
||||
//notify(.liked, liked)
|
||||
//notify(.update_stats, e.ref_id)
|
||||
notify(.liked, liked)
|
||||
notify(.update_stats, e.ref_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,28 +284,34 @@ class HomeModel: ObservableObject {
|
||||
send_home_filters(relay_id: relay_id)
|
||||
}
|
||||
case .error(let merr):
|
||||
let desc = String(describing: merr)
|
||||
let desc = merr.debugDescription
|
||||
if desc.contains("Software caused connection abort") {
|
||||
pool.reconnect(to: [relay_id])
|
||||
}
|
||||
case .disconnected:
|
||||
case .disconnected: fallthrough
|
||||
case .cancelled:
|
||||
pool.reconnect(to: [relay_id])
|
||||
case .reconnectSuggested(let t):
|
||||
if t {
|
||||
pool.reconnect(to: [relay_id])
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
|
||||
update_signal_from_pool(signal: signal, pool: damus_state.pool)
|
||||
|
||||
print("ws_event \(ev)")
|
||||
|
||||
case .nostr_event(let ev):
|
||||
switch ev {
|
||||
case .event(let sub_id, let ev):
|
||||
// globally handle likes
|
||||
/*
|
||||
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
|
||||
if !always_process {
|
||||
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
||||
case .notice(let msg):
|
||||
@@ -327,13 +321,11 @@ class HomeModel: ObservableObject {
|
||||
case .eose(let sub_id):
|
||||
|
||||
if sub_id == dms_subid {
|
||||
var dms = dms.dms.flatMap { $0.events }
|
||||
var dms = dms.dms.flatMap { $0.1.events }
|
||||
dms.append(contentsOf: incoming_dms)
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
|
||||
} else if sub_id == notifications_subid {
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
|
||||
} else if sub_id == home_subid {
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state)
|
||||
}
|
||||
|
||||
self.loading = false
|
||||
@@ -363,13 +355,13 @@ class HomeModel: ObservableObject {
|
||||
var friends = damus_state.contacts.get_friend_list()
|
||||
friends.append(damus_state.pubkey)
|
||||
|
||||
var contacts_filter = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
|
||||
var contacts_filter = NostrFilter.filter_kinds([0])
|
||||
contacts_filter.authors = friends
|
||||
|
||||
var our_contacts_filter = NostrFilter.filter_kinds([NostrKind.contacts.rawValue, NostrKind.metadata.rawValue])
|
||||
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
|
||||
our_contacts_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var our_blocklist_filter = NostrFilter.filter_kinds([NostrKind.list.rawValue])
|
||||
var our_blocklist_filter = NostrFilter.filter_kinds([30000])
|
||||
our_blocklist_filter.parameter = ["mute"]
|
||||
our_blocklist_filter.authors = [damus_state.pubkey]
|
||||
|
||||
@@ -388,27 +380,21 @@ class HomeModel: ObservableObject {
|
||||
our_dms_filter.authors = [ damus_state.pubkey ]
|
||||
|
||||
// TODO: separate likes?
|
||||
var home_filter_kinds = [
|
||||
var home_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.boost.rawValue
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
home_filter_kinds.append(NostrKind.like.rawValue)
|
||||
}
|
||||
var home_filter = NostrFilter.filter_kinds(home_filter_kinds)
|
||||
NostrKind.like.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
])
|
||||
// include our pubkey as well even if we're not technically a friend
|
||||
home_filter.authors = friends
|
||||
home_filter.limit = 500
|
||||
|
||||
var notifications_filter_kinds = [
|
||||
var notifications_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.like.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
NostrKind.zap.rawValue,
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
notifications_filter_kinds.append(NostrKind.like.rawValue)
|
||||
}
|
||||
var notifications_filter = NostrFilter.filter_kinds(notifications_filter_kinds)
|
||||
])
|
||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||
notifications_filter.limit = 500
|
||||
|
||||
@@ -463,7 +449,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_metadata_event(_ ev: NostrEvent) {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, 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? {
|
||||
@@ -485,13 +471,12 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) else {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
||||
if let inner_ev = ev.inner_event {
|
||||
damus_state.events.insert(inner_ev)
|
||||
}
|
||||
|
||||
@@ -527,8 +512,6 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: will we need to process this in other places like zap request contents, etc?
|
||||
process_image_metadata(cache: damus_state.events, ev: ev)
|
||||
damus_state.replies.count_replies(ev)
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
@@ -539,26 +522,15 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
|
||||
self.new_events = notifs
|
||||
if damus_state.settings.dm_notification {
|
||||
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
|
||||
let notify = LocalNotification(type: .dm, event: ev, target: ev, content: convo)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
}
|
||||
}
|
||||
|
||||
func handle_dm(_ ev: NostrEvent) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
if !should_debounce_dms {
|
||||
self.incoming_dms.append(ev)
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
self.new_events = notifs
|
||||
}
|
||||
self.incoming_dms = []
|
||||
return
|
||||
@@ -568,7 +540,11 @@ class HomeModel: ObservableObject {
|
||||
|
||||
dm_debouncer.debounce { [self] in
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
self.new_events = notifs
|
||||
if damus_state.settings.dm_notification,
|
||||
let displayName = damus_state.profiles.lookup(id: self.incoming_dms.last!.pubkey)?.display_name {
|
||||
create_local_notification(displayName: displayName, conversation: "You have received a direct message", type: .dm)
|
||||
}
|
||||
}
|
||||
self.incoming_dms = []
|
||||
}
|
||||
@@ -581,8 +557,8 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
|
||||
signal.max_signal = pool.relays.count
|
||||
}
|
||||
|
||||
if signal.signal != pool.num_connected {
|
||||
signal.signal = pool.num_connected
|
||||
if signal.signal != pool.num_connecting {
|
||||
signal.signal = signal.max_signal - pool.num_connecting
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,9 +652,15 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
|
||||
print("-----")
|
||||
}
|
||||
|
||||
func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: Profile, 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) {
|
||||
notify(.deleted_account, ())
|
||||
DispatchQueue.main.async {
|
||||
notify(.deleted_account, ())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -695,7 +677,6 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
||||
profiles.add(id: ev.pubkey, profile: tprof)
|
||||
|
||||
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
|
||||
|
||||
Task.detached(priority: .background) {
|
||||
let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05)
|
||||
if validated != nil {
|
||||
@@ -711,64 +692,21 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
||||
}
|
||||
|
||||
// load pfps asap
|
||||
|
||||
var changed = false
|
||||
|
||||
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
|
||||
if URL(string: picture) != nil {
|
||||
changed = true
|
||||
DispatchQueue.main.async {
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
}
|
||||
}
|
||||
|
||||
let banner = tprof.profile.banner ?? ""
|
||||
if URL(string: banner) != nil {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
}
|
||||
}
|
||||
|
||||
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
|
||||
let validated = events.is_event_valid(ev.id)
|
||||
|
||||
switch validated {
|
||||
case .unknown:
|
||||
Task {
|
||||
let result = validate_event(ev: ev)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
events.store_event_validation(evid: ev.id, validated: result)
|
||||
guard result == .ok else {
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
case .ok:
|
||||
callback()
|
||||
|
||||
case .bad_id: fallthrough
|
||||
case .bad_sig:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
|
||||
guard_valid_event(events: events, ev: ev) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||
return
|
||||
}
|
||||
|
||||
profile.cache_lnurl()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
}
|
||||
}
|
||||
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
}
|
||||
|
||||
func robohash(_ pk: String) -> String {
|
||||
@@ -827,7 +765,7 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
for d in diff {
|
||||
changed = true
|
||||
if new.contains(d) {
|
||||
if let url = RelayURL(d) {
|
||||
if let url = URL(string: d) {
|
||||
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
|
||||
}
|
||||
} else {
|
||||
@@ -841,10 +779,10 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: RelayURL, info: RelayInfo, new_relay_filters: Bool) {
|
||||
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.id
|
||||
let relay_id = url.absoluteString
|
||||
guard metadatas.lookup(relay_id: relay_id) == nil else {
|
||||
return
|
||||
}
|
||||
@@ -909,10 +847,10 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
|
||||
}
|
||||
}
|
||||
|
||||
for model in dms.dms {
|
||||
if model.pubkey == the_pk {
|
||||
for (pk, _) in dms.dms {
|
||||
if pk == the_pk {
|
||||
found = true
|
||||
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].events), new_ev: ev) {
|
||||
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
|
||||
$0.created_at < $1.created_at
|
||||
}
|
||||
|
||||
@@ -922,8 +860,8 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
|
||||
}
|
||||
|
||||
if !found {
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey, pubkey: the_pk)
|
||||
dms.dms.append(model)
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
|
||||
dms.dms.append((the_pk, model))
|
||||
inserted = true
|
||||
}
|
||||
|
||||
@@ -950,13 +888,8 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o
|
||||
}
|
||||
|
||||
if inserted {
|
||||
Task.init {
|
||||
let new_dms = Array(dms.dms.filter({ $0.events.count > 0 })).sorted { a, b in
|
||||
return a.events.last!.created_at > b.events.last!.created_at
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
dms.dms = new_dms
|
||||
}
|
||||
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
|
||||
return a.1.events.last!.created_at > b.1.events.last!.created_at
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1076,13 +1009,12 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
|
||||
}
|
||||
}
|
||||
|
||||
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
|
||||
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = zap_notification_title(zap)
|
||||
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = LossyLocalNotification(type: .zap, event_id: evId).to_user_info()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
@@ -1110,59 +1042,57 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
|
||||
}
|
||||
|
||||
// Don't show notifications from muted threads.
|
||||
if damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) {
|
||||
if damus_state.muted_threads.isMutedThread(ev) {
|
||||
return
|
||||
}
|
||||
|
||||
if type == .text && damus_state.settings.mention_notification {
|
||||
let blocks = ev.blocks(damus_state.keypair.privkey)
|
||||
for case .mention(let mention) in blocks where mention.ref.ref_id == damus_state.keypair.pubkey {
|
||||
let content = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
|
||||
|
||||
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify )
|
||||
for block in ev.blocks(damus_state.keypair.privkey) {
|
||||
if case .mention(let mention) = block, mention.ref.ref_id == damus_state.keypair.pubkey,
|
||||
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
|
||||
let justContent = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
|
||||
create_local_notification(displayName: displayName, conversation: justContent, type: type)
|
||||
}
|
||||
}
|
||||
} else if type == .boost && damus_state.settings.repost_notification,
|
||||
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
|
||||
|
||||
if let inner_ev = ev.inner_event {
|
||||
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
|
||||
}
|
||||
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
||||
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: inner_ev.content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
} else if type == .like && damus_state.settings.like_notification,
|
||||
let evid = ev.referenced_ids.last?.ref_id,
|
||||
let liked_event = damus_state.events.lookup(evid)
|
||||
{
|
||||
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: liked_event.content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name,
|
||||
let e_ref = ev.referenced_ids.first?.ref_id,
|
||||
let content = damus_state.events.lookup(e_ref)?.content {
|
||||
|
||||
create_local_notification(displayName: displayName, conversation: content, type: type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
||||
func create_local_notification(displayName: String, conversation: String, type: NostrKind) {
|
||||
let content = UNMutableNotificationContent()
|
||||
var title = ""
|
||||
var identifier = ""
|
||||
|
||||
let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey)
|
||||
|
||||
switch notify.type {
|
||||
case .mention:
|
||||
switch type {
|
||||
case .text:
|
||||
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
|
||||
identifier = "myMentionNotification"
|
||||
case .repost:
|
||||
case .boost:
|
||||
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
|
||||
identifier = "myBoostNotification"
|
||||
case .like:
|
||||
title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName)
|
||||
identifier = "myLikeNotification"
|
||||
case .dm:
|
||||
title = String(format: NSLocalizedString("%@", comment: "DM by heading in local notification"), displayName)
|
||||
title = String(format: NSLocalizedString("DM by %@", comment: "DM by heading in local notification"), displayName)
|
||||
identifier = "myDMNotification"
|
||||
case .zap:
|
||||
// not handled here
|
||||
default:
|
||||
break
|
||||
}
|
||||
content.title = title
|
||||
content.body = notify.content
|
||||
content.body = conversation
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = notify.to_lossy().to_user_info()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
|
||||
@@ -25,15 +25,6 @@ enum MediaUpload {
|
||||
return url.pathExtension
|
||||
}
|
||||
}
|
||||
|
||||
var localURL: URL {
|
||||
switch self {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_image: Bool {
|
||||
if case .image = self {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum LibreTranslateServer: String, CaseIterable, Identifiable, StringCodable {
|
||||
enum LibreTranslateServer: String, CaseIterable, Identifiable {
|
||||
var id: String { self.rawValue }
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
@@ -17,19 +17,9 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable, StringCodable {
|
||||
var url: String?
|
||||
}
|
||||
|
||||
func to_string() -> String {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
init?(from string: String) {
|
||||
guard let libreTranslateServer = LibreTranslateServer(rawValue: string) else {
|
||||
return nil
|
||||
}
|
||||
self = libreTranslateServer
|
||||
}
|
||||
|
||||
case argosopentech
|
||||
case terraprint
|
||||
case vern
|
||||
case custom
|
||||
|
||||
var model: Model {
|
||||
@@ -38,6 +28,8 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable, StringCodable {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// LocalUserConfig.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-06-15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct LocalUserConfig: Codable {
|
||||
let relays: [RelayDescriptor]
|
||||
}
|
||||
|
||||
@@ -682,7 +682,7 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions:
|
||||
}
|
||||
|
||||
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
|
||||
let tags = post.references.map(refid_to_tag) + post.tags
|
||||
let tags = post.references.map(refid_to_tag)
|
||||
let post_blocks = parse_post_blocks(content: post.content)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: false)
|
||||
let content = render_blocks(blocks: post_tags.blocks)
|
||||
|
||||
@@ -30,7 +30,7 @@ func saveMutedThreads(pubkey: String, currentValue: [String], value: [String]) -
|
||||
class MutedThreadsManager: ObservableObject {
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let keypair: Keypair
|
||||
private let pubkey: String
|
||||
|
||||
private var _mutedThreadsSet: Set<String>
|
||||
private var _mutedThreads: [String]
|
||||
@@ -39,26 +39,26 @@ class MutedThreadsManager: ObservableObject {
|
||||
return _mutedThreads
|
||||
}
|
||||
set {
|
||||
if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) {
|
||||
if saveMutedThreads(pubkey: pubkey, currentValue: _mutedThreads, value: newValue) {
|
||||
self._mutedThreads = newValue
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(keypair: Keypair) {
|
||||
self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey)
|
||||
init(pubkey: String) {
|
||||
self._mutedThreads = loadMutedThreads(pubkey: pubkey)
|
||||
self._mutedThreadsSet = Set(_mutedThreads)
|
||||
self.keypair = keypair
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
func isMutedThread(_ ev: NostrEvent, privkey: String?) -> Bool {
|
||||
return _mutedThreadsSet.contains(ev.thread_id(privkey: privkey))
|
||||
func isMutedThread(_ ev: NostrEvent) -> Bool {
|
||||
return _mutedThreadsSet.contains(ev.thread_id(privkey: nil))
|
||||
}
|
||||
|
||||
func updateMutedThread(_ ev: NostrEvent) {
|
||||
let threadId = ev.thread_id(privkey: nil)
|
||||
if isMutedThread(ev, privkey: keypair.privkey) {
|
||||
if isMutedThread(ev) {
|
||||
mutedThreads = mutedThreads.filter { $0 != threadId }
|
||||
_mutedThreadsSet.remove(threadId)
|
||||
notify(.unmute_thread, ev)
|
||||
|
||||
@@ -29,22 +29,4 @@ class EventGroup {
|
||||
func insert(_ ev: NostrEvent) -> Bool {
|
||||
return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
|
||||
}
|
||||
|
||||
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
||||
for ev in events {
|
||||
if !isIncluded(ev) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filter(_ isIncluded: (NostrEvent) -> Bool) -> EventGroup? {
|
||||
let new_evs = events.filter(isIncluded)
|
||||
guard new_evs.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
return EventGroup(events: new_evs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,26 +30,10 @@ class ZapGroup {
|
||||
}
|
||||
}
|
||||
|
||||
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
||||
for zap in zaps {
|
||||
if !isIncluded(zap.request_ev) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
|
||||
let new_zaps = zaps.filter { isIncluded($0.request_ev) }
|
||||
guard new_zaps.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
let grp = ZapGroup()
|
||||
for zap in new_zaps {
|
||||
grp.insert(zap)
|
||||
}
|
||||
return grp
|
||||
init(zaps: [Zap]) {
|
||||
self.zaps = zaps
|
||||
self.msat_total = 0
|
||||
self.zappers = Set()
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -58,7 +42,6 @@ class ZapGroup {
|
||||
self.zappers = Set()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert(_ zap: Zap) -> Bool {
|
||||
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
|
||||
return false
|
||||
|
||||
@@ -65,37 +65,6 @@ enum NotificationItem {
|
||||
return reply.created_at
|
||||
}
|
||||
}
|
||||
|
||||
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
||||
switch self {
|
||||
case .repost(_, let evgrp):
|
||||
return evgrp.would_filter(isIncluded)
|
||||
case .reaction(_, let evgrp):
|
||||
return evgrp.would_filter(isIncluded)
|
||||
case .profile_zap(let zapgrp):
|
||||
return zapgrp.would_filter(isIncluded)
|
||||
case .event_zap(_, let zapgrp):
|
||||
return zapgrp.would_filter(isIncluded)
|
||||
case .reply(let ev):
|
||||
return !isIncluded(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func filter(_ isIncluded: (NostrEvent) -> Bool) -> NotificationItem? {
|
||||
switch self {
|
||||
case .repost(let evid, let evgrp):
|
||||
return evgrp.filter(isIncluded).map { .repost(evid, $0) }
|
||||
case .reaction(let evid, let evgrp):
|
||||
return evgrp.filter(isIncluded).map { .reaction(evid, $0) }
|
||||
case .profile_zap(let zapgrp):
|
||||
return zapgrp.filter(isIncluded).map { .profile_zap($0) }
|
||||
case .event_zap(let evid, let zapgrp):
|
||||
return zapgrp.filter(isIncluded).map { .event_zap(evid, $0) }
|
||||
case .reply(let ev):
|
||||
if isIncluded(ev) { return .reply(ev) }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
@@ -110,7 +79,6 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
var reposts: [String: EventGroup]
|
||||
var replies: [NostrEvent]
|
||||
var has_reply: Set<String>
|
||||
var has_ev: Set<String>
|
||||
|
||||
@Published var notifications: [NotificationItem]
|
||||
|
||||
@@ -125,7 +93,6 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
self.incoming_events = []
|
||||
self.profile_zaps = ZapGroup()
|
||||
self.notifications = []
|
||||
self.has_ev = Set()
|
||||
}
|
||||
|
||||
func set_should_queue(_ val: Bool) {
|
||||
@@ -194,8 +161,8 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
|
||||
|
||||
private func insert_repost(_ ev: NostrEvent, cache: EventCache) -> Bool {
|
||||
guard let reposted_ev = ev.get_inner_event(cache: cache) else {
|
||||
private func insert_repost(_ ev: NostrEvent) -> Bool {
|
||||
guard let reposted_ev = ev.inner_event else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -237,9 +204,9 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
}
|
||||
|
||||
private func insert_event_immediate(_ ev: NostrEvent, cache: EventCache) -> Bool {
|
||||
private func insert_event_immediate(_ ev: NostrEvent) -> Bool {
|
||||
if ev.known_kind == .boost {
|
||||
return insert_repost(ev, cache: cache)
|
||||
return insert_repost(ev)
|
||||
} else if ev.known_kind == .like {
|
||||
return insert_reaction(ev)
|
||||
} else if ev.known_kind == .text {
|
||||
@@ -267,18 +234,12 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
|
||||
func insert_event(_ ev: NostrEvent, damus_state: DamusState) -> Bool {
|
||||
if has_ev.contains(ev.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
if should_queue {
|
||||
incoming_events.append(ev)
|
||||
has_ev.insert(ev.id)
|
||||
return true
|
||||
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
|
||||
}
|
||||
|
||||
if insert_event_immediate(ev, cache: damus_state.events) {
|
||||
self.notifications = build_notifications()
|
||||
if insert_event_immediate(ev) {
|
||||
filter_and_build_notifications(damus_state)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -291,47 +252,47 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
|
||||
if insert_zap_immediate(zap) {
|
||||
self.notifications = build_notifications()
|
||||
filter_and_build_notifications(damus_state)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filter(_ isIncluded: (NostrEvent) -> Bool) {
|
||||
func filter_and_build_notifications(_ damus_state: DamusState) {
|
||||
var changed = false
|
||||
var count = 0
|
||||
|
||||
count = incoming_events.count
|
||||
incoming_events = incoming_events.filter(isIncluded)
|
||||
incoming_events = incoming_events.filter { include_event($0, damus_state: damus_state) }
|
||||
changed = changed || incoming_events.count != count
|
||||
|
||||
count = profile_zaps.zaps.count
|
||||
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
|
||||
profile_zaps.zaps = profile_zaps.zaps.filter { zap in include_event(zap.request.ev, damus_state: damus_state) }
|
||||
changed = changed || profile_zaps.zaps.count != count
|
||||
|
||||
for el in reactions {
|
||||
count = el.value.events.count
|
||||
el.value.events = el.value.events.filter(isIncluded)
|
||||
el.value.events = el.value.events.filter { include_event($0, damus_state: damus_state) }
|
||||
changed = changed || el.value.events.count != count
|
||||
}
|
||||
|
||||
for el in reposts {
|
||||
count = el.value.events.count
|
||||
el.value.events = el.value.events.filter(isIncluded)
|
||||
el.value.events = el.value.events.filter { include_event($0, damus_state: damus_state) }
|
||||
changed = changed || el.value.events.count != count
|
||||
}
|
||||
|
||||
for el in zaps {
|
||||
count = el.value.zaps.count
|
||||
el.value.zaps = el.value.zaps.filter {
|
||||
isIncluded($0.request.ev)
|
||||
include_event($0.request.ev, damus_state: damus_state)
|
||||
}
|
||||
changed = changed || el.value.zaps.count != count
|
||||
}
|
||||
|
||||
count = replies.count
|
||||
replies = replies.filter(isIncluded)
|
||||
replies = replies.filter { include_event($0, damus_state: damus_state) }
|
||||
changed = changed || replies.count != count
|
||||
|
||||
if changed {
|
||||
@@ -347,13 +308,17 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
|
||||
for event in incoming_events {
|
||||
inserted = insert_event_immediate(event, cache: damus_state.events) || inserted
|
||||
inserted = insert_event_immediate(event) || inserted
|
||||
}
|
||||
|
||||
if inserted {
|
||||
self.notifications = build_notifications()
|
||||
filter_and_build_notifications(damus_state)
|
||||
}
|
||||
|
||||
return inserted
|
||||
}
|
||||
|
||||
func include_event(_ event: NostrEvent, damus_state: DamusState) -> Bool {
|
||||
return !damus_state.contacts.is_muted(event.pubkey) && !damus_state.muted_threads.isMutedThread(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,17 +11,17 @@ struct NostrPost {
|
||||
let kind: NostrKind
|
||||
let content: String
|
||||
let references: [ReferencedId]
|
||||
let tags: [[String]]
|
||||
|
||||
init (content: String, references: [ReferencedId], kind: NostrKind = .text, tags: [[String]] = []) {
|
||||
init (content: String, references: [ReferencedId]) {
|
||||
self.content = content
|
||||
self.references = references
|
||||
self.kind = .text
|
||||
}
|
||||
|
||||
init (content: String, references: [ReferencedId], kind: NostrKind) {
|
||||
self.content = content
|
||||
self.references = references
|
||||
self.kind = kind
|
||||
self.tags = tags
|
||||
}
|
||||
|
||||
func to_event(keypair: FullKeypair) -> NostrEvent {
|
||||
return post_to_event(post: self, privkey: keypair.privkey, pubkey: keypair.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,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(events: damus.events, our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||
}
|
||||
seen_event.insert(ev.id)
|
||||
}
|
||||
@@ -140,9 +140,6 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
case .notice(let notice):
|
||||
notify(.notice, notice)
|
||||
case .eose:
|
||||
if resp.subid == sub_id {
|
||||
load_profiles(profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus)
|
||||
}
|
||||
progress += 1
|
||||
break
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func get_base_filter() -> NostrFilter {
|
||||
var filter = NostrFilter.filter_kinds([NostrKind.text.rawValue, NostrKind.chat.rawValue])
|
||||
var filter = NostrFilter.filter_kinds([1, 42])
|
||||
filter.limit = self.limit
|
||||
filter.until = Int64(Date.now.timeIntervalSince1970)
|
||||
return filter
|
||||
@@ -101,10 +101,10 @@ func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] {
|
||||
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad) -> [String] {
|
||||
switch load {
|
||||
case .from_events(let events):
|
||||
return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache)
|
||||
return find_profiles_to_fetch_from_events(profiles: profiles, events: events)
|
||||
case .from_keys(let pks):
|
||||
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
|
||||
}
|
||||
@@ -124,18 +124,15 @@ func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [Str
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] {
|
||||
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]) -> [String] {
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
for ev in events {
|
||||
// lookup profiles from boosted events
|
||||
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), profiles.lookup(id: bev.pubkey) == nil {
|
||||
pubkeys.insert(bev.pubkey)
|
||||
if profiles.lookup(id: ev.pubkey) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if profiles.lookup(id: ev.pubkey) == nil {
|
||||
pubkeys.insert(ev.pubkey)
|
||||
}
|
||||
pubkeys.insert(ev.pubkey)
|
||||
}
|
||||
|
||||
return Array(pubkeys)
|
||||
@@ -148,7 +145,7 @@ enum PubkeysToLoad {
|
||||
|
||||
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
|
||||
var filter = NostrFilter.filter_profiles
|
||||
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events)
|
||||
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load)
|
||||
filter.authors = authors
|
||||
|
||||
guard !authors.isEmpty else {
|
||||
@@ -164,7 +161,7 @@ func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad
|
||||
}
|
||||
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,41 +9,42 @@ import Foundation
|
||||
|
||||
|
||||
class SearchModel: ObservableObject {
|
||||
let state: DamusState
|
||||
var events: EventHolder = EventHolder()
|
||||
@Published var loading: Bool = false
|
||||
@Published var channel_name: String? = nil
|
||||
|
||||
let pool: RelayPool
|
||||
var search: NostrFilter
|
||||
let contacts: Contacts
|
||||
let sub_id = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
let limit: UInt32 = 500
|
||||
|
||||
init(state: DamusState, search: NostrFilter) {
|
||||
self.state = state
|
||||
init(contacts: Contacts, pool: RelayPool, search: NostrFilter) {
|
||||
self.contacts = contacts
|
||||
self.pool = pool
|
||||
self.search = search
|
||||
}
|
||||
|
||||
func filter_muted() {
|
||||
self.events.filter { should_show_event(contacts: state.contacts, ev: $0) }
|
||||
self.events.filter { should_show_event(contacts: contacts, ev: $0) }
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
// since 1 month
|
||||
search.limit = self.limit
|
||||
search.kinds = [NostrKind.text.rawValue, NostrKind.like.rawValue]
|
||||
search.kinds = [1,5,7]
|
||||
|
||||
//likes_filter.ids = ref_events.referenced_ids!
|
||||
|
||||
print("subscribing to search '\(search)' with sub_id \(sub_id)")
|
||||
state.pool.register_handler(sub_id: sub_id, handler: handle_event)
|
||||
pool.register_handler(sub_id: sub_id, handler: handle_event)
|
||||
loading = true
|
||||
state.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
|
||||
pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
state.pool.unsubscribe(sub_id: sub_id)
|
||||
self.pool.unsubscribe(sub_id: sub_id)
|
||||
loading = false
|
||||
print("unsubscribing from search '\(search)' with sub_id \(sub_id)")
|
||||
}
|
||||
@@ -53,7 +54,7 @@ class SearchModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(contacts: state.contacts, ev: ev) else {
|
||||
guard should_show_event(contacts: contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -73,7 +74,7 @@ class SearchModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||
let (sub_id, done) = handle_subid_event(pool: state.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
||||
let (_, done) = handle_subid_event(pool: pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
||||
if ev.is_textlike && ev.should_show_event {
|
||||
self.add_event(ev)
|
||||
} else if ev.known_kind == .channel_create {
|
||||
@@ -83,14 +84,8 @@ class SearchModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
guard done else {
|
||||
return
|
||||
}
|
||||
|
||||
self.loading = false
|
||||
|
||||
if sub_id == self.sub_id {
|
||||
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state)
|
||||
if done {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,23 +77,18 @@ class ThreadModel: ObservableObject {
|
||||
var meta_events = NostrFilter()
|
||||
var event_filter = NostrFilter()
|
||||
var ref_events = NostrFilter()
|
||||
//var likes_filter = NostrFilter.filter_kinds(7])
|
||||
|
||||
let thread_id = event.thread_id(privkey: nil)
|
||||
|
||||
ref_events.referenced_ids = [thread_id, event.id]
|
||||
ref_events.kinds = [NostrKind.text.rawValue]
|
||||
ref_events.kinds = [1]
|
||||
ref_events.limit = 1000
|
||||
|
||||
event_filter.ids = [thread_id, event.id]
|
||||
|
||||
meta_events.referenced_ids = [event.id]
|
||||
|
||||
var kinds = [NostrKind.zap.rawValue, NostrKind.text.rawValue, NostrKind.boost.rawValue]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
kinds.append(NostrKind.like.rawValue)
|
||||
}
|
||||
meta_events.kinds = kinds
|
||||
|
||||
meta_events.kinds = [9735, 1, 6, 7]
|
||||
meta_events.limit = 1000
|
||||
|
||||
/*
|
||||
@@ -134,7 +129,7 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, 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)
|
||||
}
|
||||
|
||||
@@ -7,19 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
init?(from string: String) {
|
||||
guard let ts = TranslationService(rawValue: string) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self = ts
|
||||
}
|
||||
|
||||
func to_string() -> String {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
enum TranslationService: String, CaseIterable, Identifiable {
|
||||
var id: String { self.rawValue }
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
@@ -31,7 +19,6 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
case none
|
||||
case libretranslate
|
||||
case deepl
|
||||
case nokyctranslate
|
||||
|
||||
var model: Model {
|
||||
switch self {
|
||||
@@ -41,8 +28,6 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
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."))
|
||||
case .nokyctranslate:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("NoKYCTranslate.com (Prepay with BTC)", comment: "Dropdown option for selecting NoKYCTranslate.com as the translation service."))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,157 +9,210 @@ import Foundation
|
||||
import Vault
|
||||
import UIKit
|
||||
|
||||
let fallback_zap_amount = 1000
|
||||
func should_show_wallet_selector(_ pubkey: String) -> Bool {
|
||||
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
|
||||
}
|
||||
|
||||
@propertyWrapper struct Setting<T: Equatable> {
|
||||
private let key: String
|
||||
private var value: T
|
||||
|
||||
init(key: String, default_value: T) {
|
||||
self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
|
||||
if let loaded = UserDefaults.standard.object(forKey: self.key) as? T {
|
||||
self.value = loaded
|
||||
} else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
|
||||
// try to load from deprecated non-pubkey-keyed setting
|
||||
self.value = loaded
|
||||
} else {
|
||||
self.value = default_value
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
var wrappedValue: T {
|
||||
get { return value }
|
||||
set {
|
||||
guard self.value != newValue else {
|
||||
return
|
||||
}
|
||||
self.value = newValue
|
||||
UserDefaults.standard.set(newValue, forKey: key)
|
||||
UserSettingsStore.shared!.objectWillChange.send()
|
||||
}
|
||||
return amt
|
||||
}
|
||||
|
||||
func should_disable_image_animation() -> Bool {
|
||||
return (UserDefaults.standard.object(forKey: "disable_animation") as? Bool)
|
||||
?? UIAccessibility.isReduceMotionEnabled
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@propertyWrapper class StringSetting<T: StringCodable & Equatable> {
|
||||
private let key: String
|
||||
private var value: T
|
||||
|
||||
init(key: String, default_value: T) {
|
||||
self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
|
||||
if let loaded = UserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
|
||||
self.value = val
|
||||
} else if let loaded = UserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
|
||||
// try to load from deprecated non-pubkey-keyed setting
|
||||
self.value = val
|
||||
} else {
|
||||
self.value = default_value
|
||||
}
|
||||
func get_media_uploader(_ pubkey: String) -> MediaUploader {
|
||||
if let defaultMediaUploader = UserDefaults.standard.string(forKey: "default_media_uploader"),
|
||||
let defaultMediaUploader = MediaUploader(rawValue: defaultMediaUploader) {
|
||||
return defaultMediaUploader
|
||||
} else {
|
||||
return .nostrBuild
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var wrappedValue: T {
|
||||
get { return value }
|
||||
set {
|
||||
guard self.value != newValue else {
|
||||
return
|
||||
}
|
||||
self.value = newValue
|
||||
UserDefaults.standard.set(newValue.to_string(), forKey: key)
|
||||
UserSettingsStore.shared!.objectWillChange.send()
|
||||
}
|
||||
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 {
|
||||
static var pubkey: String? = nil
|
||||
static var shared: UserSettingsStore? = nil
|
||||
|
||||
@StringSetting(key: "default_wallet", default_value: .system_default_wallet)
|
||||
var default_wallet: Wallet
|
||||
|
||||
@StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
|
||||
var default_media_uploader: MediaUploader
|
||||
|
||||
@Setting(key: "show_wallet_selector", default_value: true)
|
||||
var show_wallet_selector: Bool
|
||||
|
||||
@Setting(key: "left_handed", default_value: false)
|
||||
var left_handed: Bool
|
||||
|
||||
@Setting(key: "always_show_images", default_value: false)
|
||||
var always_show_images: Bool
|
||||
|
||||
@Setting(key: "zap_vibration", default_value: true)
|
||||
var zap_vibration: Bool
|
||||
|
||||
@Setting(key: "zap_notification", default_value: true)
|
||||
var zap_notification: Bool
|
||||
|
||||
@Setting(key: "default_zap_amount", default_value: fallback_zap_amount)
|
||||
var default_zap_amount: Int
|
||||
|
||||
@Setting(key: "mention_notification", default_value: true)
|
||||
var mention_notification: Bool
|
||||
|
||||
@StringSetting(key: "zap_type", default_value: ZapType.pub)
|
||||
var default_zap_type: ZapType
|
||||
|
||||
@Setting(key: "repost_notification", default_value: true)
|
||||
var repost_notification: Bool
|
||||
|
||||
@Setting(key: "dm_notification", default_value: true)
|
||||
var dm_notification: Bool
|
||||
|
||||
@Setting(key: "like_notification", default_value: true)
|
||||
var like_notification: Bool
|
||||
|
||||
@Setting(key: "notification_only_from_following", default_value: false)
|
||||
var notification_only_from_following: Bool
|
||||
|
||||
@Setting(key: "translate_dms", default_value: false)
|
||||
var translate_dms: Bool
|
||||
|
||||
@Setting(key: "truncate_timeline_text", default_value: false)
|
||||
var truncate_timeline_text: Bool
|
||||
|
||||
@Setting(key: "truncate_mention_text", default_value: true)
|
||||
var truncate_mention_text: Bool
|
||||
|
||||
@Setting(key: "notification_indicators", default_value: NewEventsBits.all.rawValue)
|
||||
var notification_indicators: Int
|
||||
|
||||
@Setting(key: "auto_translate", default_value: true)
|
||||
var auto_translate: Bool
|
||||
|
||||
@Setting(key: "show_only_preferred_languages", default_value: false)
|
||||
var show_only_preferred_languages: Bool
|
||||
|
||||
@Setting(key: "onlyzaps_mode", default_value: false)
|
||||
var onlyzaps_mode: Bool
|
||||
|
||||
@Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
|
||||
var disable_animation: Bool
|
||||
|
||||
// Helper for inverse of disable_animation.
|
||||
// disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
|
||||
var enable_animation: Bool {
|
||||
get {
|
||||
!disable_animation
|
||||
@Published var default_wallet: Wallet {
|
||||
didSet {
|
||||
UserDefaults.standard.set(default_wallet.rawValue, forKey: "default_wallet")
|
||||
}
|
||||
set {
|
||||
disable_animation = !newValue
|
||||
}
|
||||
|
||||
@Published var default_media_uploader: MediaUploader {
|
||||
didSet {
|
||||
UserDefaults.standard.set(default_media_uploader.rawValue, forKey: "default_media_uploader")
|
||||
}
|
||||
}
|
||||
|
||||
@StringSetting(key: "friend_filter", default_value: .all)
|
||||
var friend_filter: FriendFilter
|
||||
@Published var show_wallet_selector: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(show_wallet_selector, forKey: "show_wallet_selector")
|
||||
}
|
||||
}
|
||||
|
||||
@StringSetting(key: "translation_service", default_value: .none)
|
||||
var translation_service: TranslationService
|
||||
|
||||
@StringSetting(key: "deepl_plan", default_value: .free)
|
||||
var deepl_plan: DeepLPlan
|
||||
@Published var left_handed: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(left_handed, forKey: "left_handed")
|
||||
}
|
||||
}
|
||||
|
||||
var deepl_api_key: String {
|
||||
@Published var always_show_images: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(always_show_images, forKey: "always_show_images")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var zap_vibration: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(zap_vibration, forKey: "zap_vibration")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var zap_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(zap_notification, forKey: "zap_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var mention_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(mention_notification, forKey: "mention_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var repost_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(repost_notification, forKey: "repost_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var dm_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(dm_notification, forKey: "dm_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var like_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(like_notification, forKey: "like_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var notification_only_from_following: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(notification_only_from_following, forKey: "notification_only_from_following")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var translate_dms: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(translate_dms, forKey: "translate_dms")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var truncate_timeline_text: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(truncate_timeline_text, forKey: "truncate_timeline_text")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var notification_indicators: Int {
|
||||
didSet {
|
||||
UserDefaults.standard.set(notification_indicators, forKey: "notification_indicators")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var truncate_mention_text: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var auto_translate: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var show_only_preferred_languages: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(show_only_preferred_languages, forKey: "show_only_preferred_languages")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var translation_service: TranslationService {
|
||||
didSet {
|
||||
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var deepl_plan: DeepLPlan {
|
||||
didSet {
|
||||
UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var deepl_api_key: String {
|
||||
didSet {
|
||||
do {
|
||||
if deepl_api_key == "" {
|
||||
@@ -173,14 +226,31 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@StringSetting(key: "libretranslate_server", default_value: .terraprint)
|
||||
var libretranslate_server: LibreTranslateServer
|
||||
|
||||
@Setting(key: "libretranslate_url", default_value: "")
|
||||
var libretranslate_url: String
|
||||
@Published var libretranslate_server: LibreTranslateServer {
|
||||
didSet {
|
||||
if oldValue == libretranslate_server {
|
||||
return
|
||||
}
|
||||
|
||||
@Setting(key: "libretranslate_api_key", default_value: "")
|
||||
var libretranslate_api_key: String {
|
||||
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 == "" {
|
||||
@@ -194,33 +264,75 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@Published var nokyctranslate_api_key: String {
|
||||
@Published var disable_animation: Bool {
|
||||
didSet {
|
||||
do {
|
||||
if nokyctranslate_api_key == "" {
|
||||
try clearNoKYCTranslateApiKey()
|
||||
} else {
|
||||
try saveNoKYCTranslateApiKey(nokyctranslate_api_key)
|
||||
}
|
||||
} catch {
|
||||
// No-op.
|
||||
}
|
||||
UserDefaults.standard.set(disable_animation, forKey: "disable_animation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
// TODO: pubkey-scoped settings
|
||||
let pubkey = ""
|
||||
self.default_wallet = get_default_wallet(pubkey)
|
||||
show_wallet_selector = should_show_wallet_selector(pubkey)
|
||||
always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false
|
||||
|
||||
default_media_uploader = get_media_uploader(pubkey)
|
||||
|
||||
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
||||
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
|
||||
zap_notification = UserDefaults.standard.object(forKey: "zap_notification") as? Bool ?? true
|
||||
mention_notification = UserDefaults.standard.object(forKey: "mention_notification") as? Bool ?? true
|
||||
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
|
||||
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
|
||||
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
|
||||
notification_indicators = UserDefaults.standard.object(forKey: "notification_indicators") as? Int ?? NewEventsBits.all.rawValue
|
||||
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
|
||||
translate_dms = UserDefaults.standard.object(forKey: "translate_dms") as? Bool ?? false
|
||||
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
|
||||
truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? false
|
||||
disable_animation = should_disable_image_animation()
|
||||
auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true
|
||||
show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") 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 = ""
|
||||
}
|
||||
|
||||
do {
|
||||
nokyctranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration())
|
||||
} catch {
|
||||
nokyctranslate_api_key = ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
|
||||
@@ -231,14 +343,6 @@ class UserSettingsStore: ObservableObject {
|
||||
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||
}
|
||||
|
||||
private func saveNoKYCTranslateApiKey(_ apiKey: String) throws {
|
||||
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration())
|
||||
}
|
||||
|
||||
private func clearNoKYCTranslateApiKey() throws {
|
||||
try Vault.deletePrivateKey(keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration())
|
||||
}
|
||||
|
||||
private func saveDeepLApiKey(_ apiKey: String) throws {
|
||||
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||
}
|
||||
@@ -247,7 +351,7 @@ class UserSettingsStore: ObservableObject {
|
||||
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||
}
|
||||
|
||||
var can_translate: Bool {
|
||||
func can_translate(_ pubkey: String) -> Bool {
|
||||
switch translation_service {
|
||||
case .none:
|
||||
return false
|
||||
@@ -255,8 +359,6 @@ class UserSettingsStore: ObservableObject {
|
||||
return URLComponents(string: libretranslate_url) != nil
|
||||
case .deepl:
|
||||
return deepl_api_key != ""
|
||||
case .nokyctranslate:
|
||||
return nokyctranslate_api_key != ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,13 +374,3 @@ struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
|
||||
var accessGroup: String? = nil
|
||||
var accountName = "deepl_apikey"
|
||||
}
|
||||
|
||||
struct DamusNoKYCTranslateKeychainConfiguration: KeychainConfiguration {
|
||||
var serviceName = "damus"
|
||||
var accessGroup: String? = nil
|
||||
var accountName = "nokyctranslate_apikey"
|
||||
}
|
||||
|
||||
func pk_setting_key(_ pubkey: String, key: String) -> String {
|
||||
return "\(pubkey)_\(key)"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum Wallet: String, CaseIterable, Identifiable, StringCodable {
|
||||
enum Wallet: String, CaseIterable, Identifiable {
|
||||
var id: String { self.rawValue }
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
@@ -20,17 +20,6 @@ enum Wallet: String, CaseIterable, Identifiable, StringCodable {
|
||||
var image: String
|
||||
}
|
||||
|
||||
func to_string() -> String {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
init?(from string: String) {
|
||||
guard let w = Wallet(rawValue: string) else {
|
||||
return nil
|
||||
}
|
||||
self = w
|
||||
}
|
||||
|
||||
// New url prefixes needed to be added to LSApplicationQueriesSchemes
|
||||
case system_default_wallet
|
||||
case strike
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
class ZapsModel: ObservableObject {
|
||||
let state: DamusState
|
||||
let target: ZapTarget
|
||||
var zaps: [Zap]
|
||||
|
||||
let zaps_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
@@ -17,14 +18,11 @@ class ZapsModel: ObservableObject {
|
||||
init(state: DamusState, target: ZapTarget) {
|
||||
self.state = state
|
||||
self.target = target
|
||||
}
|
||||
|
||||
var zaps: [Zap] {
|
||||
return state.events.lookup_zaps(target: target)
|
||||
self.zaps = []
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var filter = NostrFilter.filter_kinds([NostrKind.zap.rawValue])
|
||||
var filter = NostrFilter.filter_kinds([9735])
|
||||
switch target {
|
||||
case .profile(let profile_id):
|
||||
filter.pubkeys = [profile_id]
|
||||
@@ -53,7 +51,7 @@ class ZapsModel: ObservableObject {
|
||||
case .notice:
|
||||
break
|
||||
case .eose:
|
||||
let events = state.events.lookup_zaps(target: target).map { $0.request_ev }
|
||||
let events = self.zaps.map { $0.request.ev }
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||
case .event(_, let ev):
|
||||
guard ev.kind == 9735 else {
|
||||
@@ -61,7 +59,7 @@ class ZapsModel: ObservableObject {
|
||||
}
|
||||
|
||||
if let zap = state.zaps.zaps[ev.id] {
|
||||
if state.events.store_zap(zap: zap) {
|
||||
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
} else {
|
||||
@@ -73,7 +71,9 @@ class ZapsModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if self.state.add_zap(zap: zap) {
|
||||
state.zaps.add_zap(zap: zap)
|
||||
|
||||
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,11 +52,6 @@ class Profile: Codable {
|
||||
set_val(key, val)
|
||||
}
|
||||
|
||||
var reactions: Bool? {
|
||||
get { return get_val("reactions"); }
|
||||
set(s) { set_val("reactions", s) }
|
||||
}
|
||||
|
||||
var deleted: Bool? {
|
||||
get { return get_val("deleted"); }
|
||||
set(s) { set_val("deleted", s) }
|
||||
@@ -115,18 +110,6 @@ class Profile: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
func cache_lnurl() {
|
||||
guard self._lnurl == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let addr = lud16 ?? lud06 else {
|
||||
return
|
||||
}
|
||||
|
||||
self._lnurl = lnaddress_to_lnurl(addr)
|
||||
}
|
||||
|
||||
private var _lnurl: String? = nil
|
||||
var lnurl: String? {
|
||||
if let _lnurl {
|
||||
|
||||
@@ -13,15 +13,11 @@ import CryptoKit
|
||||
import NaturalLanguage
|
||||
|
||||
|
||||
|
||||
enum ValidationResult: Decodable {
|
||||
case unknown
|
||||
case ok
|
||||
case bad_id
|
||||
case bad_sig
|
||||
|
||||
var is_bad: Bool {
|
||||
return self == .bad_id || self == .bad_sig
|
||||
}
|
||||
}
|
||||
|
||||
struct OtherEvent {
|
||||
@@ -42,18 +38,6 @@ struct ReferencedId: Identifiable, Hashable, Equatable {
|
||||
var id: String {
|
||||
return ref_id
|
||||
}
|
||||
|
||||
static func q(_ id: String, relay_id: String? = nil) -> ReferencedId {
|
||||
return ReferencedId(ref_id: id, relay_id: relay_id, key: "q")
|
||||
}
|
||||
|
||||
static func e(_ id: String, relay_id: String? = nil) -> ReferencedId {
|
||||
return ReferencedId(ref_id: id, relay_id: relay_id, key: "e")
|
||||
}
|
||||
|
||||
static func p(_ id: String, relay_id: String? = nil) -> ReferencedId {
|
||||
return ReferencedId(ref_id: id, relay_id: relay_id, key: "p")
|
||||
}
|
||||
}
|
||||
|
||||
struct EventId: Identifiable, CustomStringConvertible {
|
||||
@@ -98,7 +82,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
}
|
||||
|
||||
var too_big: Bool {
|
||||
return self.content.utf8.count > 16000
|
||||
return self.content.count > 16000
|
||||
}
|
||||
|
||||
var should_show_event: Bool {
|
||||
@@ -109,6 +93,14 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return calculate_event_id(ev: self) == self.id
|
||||
}
|
||||
|
||||
var is_valid: Bool {
|
||||
return validity == .ok
|
||||
}
|
||||
|
||||
lazy var validity: ValidationResult = {
|
||||
return .ok //validate_event(ev: self)
|
||||
}()
|
||||
|
||||
private var _blocks: [Block]? = nil
|
||||
func blocks(_ privkey: String?) -> [Block] {
|
||||
if let bs = _blocks {
|
||||
@@ -123,22 +115,14 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return parse_mentions(content: content, tags: self.tags)
|
||||
}
|
||||
|
||||
private lazy var inner_event: NostrEvent? = {
|
||||
return event_from_json(dat: self.content)
|
||||
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 {
|
||||
return event_from_json(dat: self.content)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
func get_inner_event(cache: EventCache) -> NostrEvent? {
|
||||
guard self.known_kind == .boost else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if self.content == "", let ref = self.referenced_ids.first {
|
||||
return cache.lookup(ref.ref_id)
|
||||
}
|
||||
|
||||
return self.inner_event
|
||||
}
|
||||
|
||||
private var _event_refs: [EventRef]? = nil
|
||||
func event_refs(_ privkey: String?) -> [EventRef] {
|
||||
if let rs = _event_refs {
|
||||
@@ -580,7 +564,7 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
||||
return ev
|
||||
}
|
||||
|
||||
func make_metadata_event(keypair: Keypair, metadata: Profile) -> NostrEvent? {
|
||||
func make_metadata_event(keypair: Keypair, metadata: NostrMetadata) -> NostrEvent? {
|
||||
guard let privkey = keypair.privkey else {
|
||||
return nil
|
||||
}
|
||||
@@ -706,7 +690,7 @@ func generate_private_keypair(our_privkey: String, id: String, created_at: Int64
|
||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> NostrEvent? {
|
||||
var tags = zap_target_to_tags(target)
|
||||
var relay_tag = ["relays"]
|
||||
relay_tag.append(contentsOf: relays.map { $0.url.id })
|
||||
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
||||
tags.append(relay_tag)
|
||||
|
||||
var kp = keypair
|
||||
@@ -740,34 +724,11 @@ func make_zap_request_event(keypair: FullKeypair, content: String, relays: [Rela
|
||||
return ev
|
||||
}
|
||||
|
||||
func uniq<T: Hashable>(_ xs: [T]) -> [T] {
|
||||
var s = Set<T>()
|
||||
var ys: [T] = []
|
||||
|
||||
for x in xs {
|
||||
if s.contains(x) {
|
||||
continue
|
||||
}
|
||||
s.insert(x)
|
||||
ys.append(x)
|
||||
}
|
||||
|
||||
return ys
|
||||
}
|
||||
|
||||
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
||||
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
|
||||
|
||||
ids.append(.e(from.id))
|
||||
ids.append(contentsOf: uniq(from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }))
|
||||
if from.pubkey != our_pubkey {
|
||||
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
||||
var ids: [ReferencedId] = [.q(from.id)]
|
||||
ids.append(ReferencedId(ref_id: from.id, relay_id: nil, key: "e"))
|
||||
ids.append(contentsOf: from.referenced_pubkeys.filter { $0.ref_id != our_pubkey })
|
||||
if from.pubkey != our_pubkey {
|
||||
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
|
||||
}
|
||||
@@ -1018,8 +979,8 @@ func last_etag(tags: [[String]]) -> String? {
|
||||
return e
|
||||
}
|
||||
|
||||
func inner_event_or_self(ev: NostrEvent, cache: EventCache) -> NostrEvent {
|
||||
guard let inner_ev = ev.get_inner_event(cache: cache) else {
|
||||
func inner_event_or_self(ev: NostrEvent) -> NostrEvent {
|
||||
guard let inner_ev = ev.inner_event else {
|
||||
return ev
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ struct NostrFilter: Codable, Equatable {
|
||||
}
|
||||
|
||||
public static var filter_text: NostrFilter {
|
||||
return filter_kinds([NostrKind.text.rawValue])
|
||||
return filter_kinds([1])
|
||||
}
|
||||
|
||||
public static func filter_ids(_ ids: [String]) -> NostrFilter {
|
||||
@@ -49,11 +49,11 @@ struct NostrFilter: Codable, Equatable {
|
||||
}
|
||||
|
||||
public static var filter_profiles: NostrFilter {
|
||||
return filter_kinds([NostrKind.metadata.rawValue])
|
||||
return filter_kinds([0])
|
||||
}
|
||||
|
||||
public static var filter_contacts: NostrFilter {
|
||||
return filter_kinds([NostrKind.contacts.rawValue])
|
||||
return filter_kinds([3])
|
||||
}
|
||||
|
||||
public static func filter_authors(_ authors: [String]) -> NostrFilter {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// NostrMetadata.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-05-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct NostrMetadata: Codable {
|
||||
let display_name: String?
|
||||
let name: String?
|
||||
let about: String?
|
||||
let website: String?
|
||||
let nip05: String?
|
||||
let picture: String?
|
||||
let banner: String?
|
||||
let lud06: String?
|
||||
let lud16: String?
|
||||
}
|
||||
|
||||
func create_account_to_metadata(_ model: CreateAccountModel) -> NostrMetadata {
|
||||
return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: model.profile_image, banner: nil, lud06: nil, lud16: nil)
|
||||
}
|
||||
@@ -14,8 +14,8 @@ public struct RelayInfo: Codable {
|
||||
static let rw = RelayInfo(read: true, write: true)
|
||||
}
|
||||
|
||||
public struct RelayDescriptor {
|
||||
public let url: RelayURL
|
||||
public struct RelayDescriptor: Codable {
|
||||
public let url: URL
|
||||
public let info: RelayInfo
|
||||
}
|
||||
|
||||
@@ -52,12 +52,14 @@ class Relay: Identifiable {
|
||||
let descriptor: RelayDescriptor
|
||||
let connection: RelayConnection
|
||||
|
||||
var last_pong: UInt32
|
||||
var flags: Int
|
||||
|
||||
init(descriptor: RelayDescriptor, connection: RelayConnection) {
|
||||
self.flags = 0
|
||||
self.descriptor = descriptor
|
||||
self.connection = connection
|
||||
self.last_pong = 0
|
||||
}
|
||||
|
||||
func mark_broken() {
|
||||
@@ -79,6 +81,6 @@ enum RelayError: Error {
|
||||
case RelayNotFound
|
||||
}
|
||||
|
||||
func get_relay_id(_ url: RelayURL) -> String {
|
||||
return url.url.absoluteString
|
||||
func get_relay_id(_ url: URL) -> String {
|
||||
return url.absoluteString
|
||||
}
|
||||
|
||||
@@ -5,54 +5,44 @@
|
||||
// Created by William Casarin on 2022-04-02.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import Starscream
|
||||
|
||||
enum NostrConnectionEvent {
|
||||
case ws_event(WebSocketEvent)
|
||||
case nostr_event(NostrResponse)
|
||||
}
|
||||
|
||||
public struct RelayURL: Hashable {
|
||||
private(set) var url: URL
|
||||
|
||||
var id: String {
|
||||
return url.absoluteString
|
||||
}
|
||||
|
||||
init?(_ str: String) {
|
||||
guard let url = URL(string: str) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let scheme = url.scheme else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard scheme == "ws" || scheme == "wss" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
final class RelayConnection {
|
||||
final class RelayConnection: WebSocketDelegate {
|
||||
private(set) var isConnected = false
|
||||
private(set) var isConnecting = false
|
||||
private(set) var isReconnecting = false
|
||||
|
||||
private(set) var last_connection_attempt: TimeInterval = 0
|
||||
private lazy var socket = WebSocket(url.url)
|
||||
private var subscriptionToken: AnyCancellable?
|
||||
|
||||
private lazy var socket = {
|
||||
let req = URLRequest(url: url)
|
||||
let socket = WebSocket(request: req, compressionHandler: .none)
|
||||
socket.delegate = self
|
||||
return socket
|
||||
}()
|
||||
private var handleEvent: (NostrConnectionEvent) -> ()
|
||||
private let url: RelayURL
|
||||
private let url: URL
|
||||
|
||||
init(url: RelayURL, handleEvent: @escaping (NostrConnectionEvent) -> ()) {
|
||||
init(url: URL, handleEvent: @escaping (NostrConnectionEvent) -> ()) {
|
||||
self.url = url
|
||||
self.handleEvent = handleEvent
|
||||
}
|
||||
|
||||
func reconnect() {
|
||||
if isConnected {
|
||||
isReconnecting = true
|
||||
disconnect()
|
||||
} else {
|
||||
// we're already disconnected, so just connect
|
||||
connect(force: true)
|
||||
}
|
||||
}
|
||||
|
||||
func connect(force: Bool = false) {
|
||||
if !force && (isConnected || isConnecting) {
|
||||
return
|
||||
@@ -60,27 +50,11 @@ final class RelayConnection {
|
||||
|
||||
isConnecting = true
|
||||
last_connection_attempt = Date().timeIntervalSince1970
|
||||
|
||||
subscriptionToken = socket.subject
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
self?.receive(event: .error(error))
|
||||
case .finished:
|
||||
self?.receive(event: .disconnected(.normalClosure, nil))
|
||||
}
|
||||
} receiveValue: { [weak self] event in
|
||||
self?.receive(event: event)
|
||||
}
|
||||
|
||||
socket.connect()
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
socket.disconnect()
|
||||
subscriptionToken = nil
|
||||
|
||||
isConnected = false
|
||||
isConnecting = false
|
||||
}
|
||||
@@ -90,58 +64,55 @@ final class RelayConnection {
|
||||
print("failed to encode nostr req: \(req)")
|
||||
return
|
||||
}
|
||||
socket.send(.string(req))
|
||||
|
||||
socket.write(string: req)
|
||||
}
|
||||
|
||||
private func receive(event: WebSocketEvent) {
|
||||
// MARK: - WebSocketDelegate
|
||||
|
||||
func didReceive(event: WebSocketEvent, client: WebSocket) {
|
||||
switch event {
|
||||
case .connected:
|
||||
self.isConnected = true
|
||||
self.isConnecting = false
|
||||
case .message(let message):
|
||||
self.receive(message: message)
|
||||
case .disconnected(let closeCode, let reason):
|
||||
if closeCode != .normalClosure {
|
||||
print("⚠️ Warning: RelayConnection (\(self.url)) closed with code \(closeCode), reason: \(String(describing: reason))")
|
||||
|
||||
case .disconnected:
|
||||
self.isConnecting = false
|
||||
self.isConnected = false
|
||||
if self.isReconnecting {
|
||||
self.isReconnecting = false
|
||||
self.connect()
|
||||
}
|
||||
isConnected = false
|
||||
isConnecting = false
|
||||
reconnect()
|
||||
case .error(let error):
|
||||
print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)")
|
||||
isConnected = false
|
||||
isConnecting = false
|
||||
reconnect()
|
||||
}
|
||||
self.handleEvent(.ws_event(event))
|
||||
}
|
||||
|
||||
func reconnect() {
|
||||
guard !isConnecting else {
|
||||
return // we're already trying to connect
|
||||
}
|
||||
disconnect()
|
||||
connect()
|
||||
}
|
||||
|
||||
private func receive(message: URLSessionWebSocketTask.Message) {
|
||||
switch message {
|
||||
case .string(let messageString):
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
if let ev = decode_nostr_event(txt: messageString) {
|
||||
DispatchQueue.main.async {
|
||||
self.handleEvent(.nostr_event(ev))
|
||||
|
||||
case .cancelled, .error:
|
||||
self.isConnecting = false
|
||||
self.isConnected = false
|
||||
|
||||
case .text(let txt):
|
||||
if txt.count > 2000 {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
if let ev = decode_nostr_event(txt: txt) {
|
||||
DispatchQueue.main.async {
|
||||
self.handleEvent(.nostr_event(ev))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let ev = decode_nostr_event(txt: txt) {
|
||||
handleEvent(.nostr_event(ev))
|
||||
return
|
||||
}
|
||||
}
|
||||
case .data(let messageData):
|
||||
if let messageString = String(data: messageData, encoding: .utf8) {
|
||||
receive(message: .string(messageString))
|
||||
}
|
||||
@unknown default:
|
||||
print("An unexpected URLSessionWebSocketTask.Message was received.")
|
||||
|
||||
print("decode failed for \(txt)")
|
||||
// TODO: trigger event error
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
handleEvent(.ws_event(event))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-30
@@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
struct SubscriptionId: Identifiable, CustomStringConvertible {
|
||||
let id: String
|
||||
@@ -45,24 +44,7 @@ class RelayPool {
|
||||
var request_queue: [QueuedRequest] = []
|
||||
var seen: Set<String> = Set()
|
||||
var counts: [String: UInt64] = [:]
|
||||
|
||||
private let network_monitor = NWPathMonitor()
|
||||
private let network_monitor_queue = DispatchQueue(label: "io.damus.network_monitor")
|
||||
private var last_network_status: NWPath.Status = .unsatisfied
|
||||
|
||||
init() {
|
||||
network_monitor.pathUpdateHandler = { [weak self] path in
|
||||
if (path.status == .satisfied || path.status == .requiresConnection) && self?.last_network_status != path.status {
|
||||
DispatchQueue.main.async {
|
||||
self?.connect_to_disconnected()
|
||||
}
|
||||
}
|
||||
|
||||
self?.last_network_status = path.status
|
||||
}
|
||||
network_monitor.start(queue: network_monitor_queue)
|
||||
}
|
||||
|
||||
var descriptors: [RelayDescriptor] {
|
||||
relays.map { $0.descriptor }
|
||||
}
|
||||
@@ -70,10 +52,6 @@ class RelayPool {
|
||||
var num_connecting: Int {
|
||||
return relays.reduce(0) { n, r in n + (r.connection.isConnecting ? 1 : 0) }
|
||||
}
|
||||
|
||||
var num_connected: Int {
|
||||
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
|
||||
}
|
||||
|
||||
func remove_handler(sub_id: String) {
|
||||
self.handlers = handlers.filter { $0.sub_id != sub_id }
|
||||
@@ -106,7 +84,7 @@ class RelayPool {
|
||||
}
|
||||
}
|
||||
|
||||
func add_relay(_ url: RelayURL, info: RelayInfo) throws {
|
||||
func add_relay(_ url: URL, info: RelayInfo) throws {
|
||||
let relay_id = get_relay_id(url)
|
||||
if get_relay(relay_id) != nil {
|
||||
throw RelayError.RelayAlreadyExists
|
||||
@@ -124,11 +102,11 @@ class RelayPool {
|
||||
for relay in relays {
|
||||
let c = relay.connection
|
||||
|
||||
let is_connecting = c.isConnecting
|
||||
let is_connecting = c.isReconnecting || c.isConnecting
|
||||
|
||||
if is_connecting && (Date.now.timeIntervalSince1970 - c.last_connection_attempt) > 5 {
|
||||
print("stale connection detected (\(relay.descriptor.url.url.absoluteString)). retrying...")
|
||||
relay.connection.reconnect()
|
||||
print("stale connection detected (\(relay.descriptor.url.absoluteString)). retrying...")
|
||||
relay.connection.connect(force: true)
|
||||
} else if relay.is_broken || is_connecting || c.isConnected {
|
||||
continue
|
||||
} else {
|
||||
@@ -226,6 +204,19 @@ class RelayPool {
|
||||
relays.first(where: { $0.id == id })
|
||||
}
|
||||
|
||||
func record_last_pong(relay_id: String, event: NostrConnectionEvent) {
|
||||
if case .ws_event(let ws_event) = event {
|
||||
if case .pong = ws_event {
|
||||
for relay in relays {
|
||||
if relay.id == relay_id {
|
||||
relay.last_pong = UInt32(Date.now.timeIntervalSince1970)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run_queue(_ relay_id: String) {
|
||||
self.request_queue = request_queue.reduce(into: Array<QueuedRequest>()) { (q, req) in
|
||||
guard req.relay == relay_id else {
|
||||
@@ -255,6 +246,7 @@ class RelayPool {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -271,10 +263,8 @@ class RelayPool {
|
||||
}
|
||||
|
||||
func add_rw_relay(_ pool: RelayPool, _ url: String) {
|
||||
guard let url = RelayURL(url) else {
|
||||
return
|
||||
}
|
||||
try? pool.add_relay(url, info: RelayInfo.rw)
|
||||
let url_ = URL(string: url)!
|
||||
try? pool.add_relay(url_, info: RelayInfo.rw)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
//
|
||||
// WebSocket.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Bryan Montz on 4/13/23.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum WebSocketEvent {
|
||||
case connected
|
||||
case message(URLSessionWebSocketTask.Message)
|
||||
case disconnected(URLSessionWebSocketTask.CloseCode, String?)
|
||||
case error(Error)
|
||||
}
|
||||
|
||||
final class WebSocket: NSObject, URLSessionWebSocketDelegate {
|
||||
|
||||
private let url: URL
|
||||
private let session: URLSession
|
||||
private lazy var webSocketTask: URLSessionWebSocketTask = {
|
||||
let task = session.webSocketTask(with: url)
|
||||
task.delegate = self
|
||||
return task
|
||||
}()
|
||||
|
||||
let subject = PassthroughSubject<WebSocketEvent, Never>()
|
||||
|
||||
init(_ url: URL, session: URLSession = .shared) {
|
||||
self.url = url
|
||||
self.session = session
|
||||
}
|
||||
|
||||
func connect() {
|
||||
resume()
|
||||
}
|
||||
|
||||
func disconnect(closeCode: URLSessionWebSocketTask.CloseCode = .normalClosure, reason: Data? = nil) {
|
||||
webSocketTask.cancel(with: closeCode, reason: reason)
|
||||
|
||||
// reset after disconnecting to be ready for reconnecting
|
||||
let task = session.webSocketTask(with: url)
|
||||
task.delegate = self
|
||||
webSocketTask = task
|
||||
|
||||
let reason_str: String?
|
||||
if let reason {
|
||||
reason_str = String(data: reason, encoding: .utf8)
|
||||
} else {
|
||||
reason_str = nil
|
||||
}
|
||||
subject.send(.disconnected(closeCode, reason_str))
|
||||
}
|
||||
|
||||
func send(_ message: URLSessionWebSocketTask.Message) {
|
||||
webSocketTask.send(message) { [weak self] error in
|
||||
if let error {
|
||||
self?.subject.send(.error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resume() {
|
||||
webSocketTask.receive { [weak self] result in
|
||||
switch result {
|
||||
case .success(let message):
|
||||
self?.subject.send(.message(message))
|
||||
self?.resume()
|
||||
case .failure(let error):
|
||||
self?.subject.send(.error(error))
|
||||
}
|
||||
}
|
||||
|
||||
webSocketTask.resume()
|
||||
}
|
||||
|
||||
// MARK: - URLSessionWebSocketDelegate
|
||||
|
||||
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol theProtocol: String?) {
|
||||
subject.send(.connected)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||
disconnect(closeCode: closeCode, reason: reason)
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
extension UIImage {
|
||||
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
|
||||
guard blurHash.count >= 6 else { return nil }
|
||||
|
||||
let sizeFlag = String(blurHash[0]).decode83()
|
||||
let numY = (sizeFlag / 9) + 1
|
||||
let numX = (sizeFlag % 9) + 1
|
||||
|
||||
let quantisedMaximumValue = String(blurHash[1]).decode83()
|
||||
let maximumValue = Float(quantisedMaximumValue + 1) / 166
|
||||
|
||||
guard blurHash.count == 4 + 2 * numX * numY else { return nil }
|
||||
|
||||
let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
|
||||
if i == 0 {
|
||||
let value = String(blurHash[2 ..< 6]).decode83()
|
||||
return decodeDC(value)
|
||||
} else {
|
||||
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
|
||||
return decodeAC(value, maximumValue: maximumValue * punch)
|
||||
}
|
||||
}
|
||||
|
||||
let width = Int(size.width)
|
||||
let height = Int(size.height)
|
||||
let bytesPerRow = width * 3
|
||||
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
|
||||
CFDataSetLength(data, bytesPerRow * height)
|
||||
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
|
||||
|
||||
for y in 0 ..< height {
|
||||
for x in 0 ..< width {
|
||||
var r: Float = 0
|
||||
var g: Float = 0
|
||||
var b: Float = 0
|
||||
|
||||
for j in 0 ..< numY {
|
||||
for i in 0 ..< numX {
|
||||
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
|
||||
let colour = colours[i + j * numX]
|
||||
r += colour.0 * basis
|
||||
g += colour.1 * basis
|
||||
b += colour.2 * basis
|
||||
}
|
||||
}
|
||||
|
||||
let intR = UInt8(linearTosRGB(r))
|
||||
let intG = UInt8(linearTosRGB(g))
|
||||
let intB = UInt8(linearTosRGB(b))
|
||||
|
||||
pixels[3 * x + 0 + y * bytesPerRow] = intR
|
||||
pixels[3 * x + 1 + y * bytesPerRow] = intG
|
||||
pixels[3 * x + 2 + y * bytesPerRow] = intB
|
||||
}
|
||||
}
|
||||
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
|
||||
|
||||
guard let provider = CGDataProvider(data: data) else { return nil }
|
||||
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
|
||||
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }
|
||||
|
||||
self.init(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
|
||||
private func decodeDC(_ value: Int) -> (Float, Float, Float) {
|
||||
let intR = value >> 16
|
||||
let intG = (value >> 8) & 255
|
||||
let intB = value & 255
|
||||
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
|
||||
}
|
||||
|
||||
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
|
||||
let quantR = value / (19 * 19)
|
||||
let quantG = (value / 19) % 19
|
||||
let quantB = value % 19
|
||||
|
||||
let rgb = (
|
||||
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
|
||||
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
|
||||
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
|
||||
)
|
||||
|
||||
return rgb
|
||||
}
|
||||
|
||||
private func signPow(_ value: Float, _ exp: Float) -> Float {
|
||||
return copysign(pow(abs(value), exp), value)
|
||||
}
|
||||
|
||||
private func linearTosRGB(_ value: Float) -> Int {
|
||||
let v = max(0, min(1, value))
|
||||
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
|
||||
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
|
||||
}
|
||||
|
||||
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
|
||||
let v = Float(Int64(value)) / 255
|
||||
if v <= 0.04045 { return v / 12.92 }
|
||||
else { return pow((v + 0.055) / 1.055, 2.4) }
|
||||
}
|
||||
|
||||
private let encodeCharacters: [String] = {
|
||||
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
|
||||
}()
|
||||
|
||||
private let decodeCharacters: [String: Int] = {
|
||||
var dict: [String: Int] = [:]
|
||||
for (index, character) in encodeCharacters.enumerated() {
|
||||
dict[character] = index
|
||||
}
|
||||
return dict
|
||||
}()
|
||||
|
||||
extension String {
|
||||
func decode83() -> Int {
|
||||
var value: Int = 0
|
||||
for character in self {
|
||||
if let digit = decodeCharacters[String(character)] {
|
||||
value = value * 83 + digit
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
subscript (offset: Int) -> Character {
|
||||
return self[index(startIndex, offsetBy: offset)]
|
||||
}
|
||||
|
||||
subscript (bounds: CountableClosedRange<Int>) -> Substring {
|
||||
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||
return self[start...end]
|
||||
}
|
||||
|
||||
subscript (bounds: CountableRange<Int>) -> Substring {
|
||||
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||
return self[start..<end]
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
extension UIImage {
|
||||
public func blurHash(numberOfComponents components: (Int, Int)) -> String? {
|
||||
let pixelWidth = Int(round(size.width * scale))
|
||||
let pixelHeight = Int(round(size.height * scale))
|
||||
|
||||
let context = CGContext(
|
||||
data: nil,
|
||||
width: pixelWidth,
|
||||
height: pixelHeight,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: pixelWidth * 4,
|
||||
space: CGColorSpace(name: CGColorSpace.sRGB)!,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
)!
|
||||
context.scaleBy(x: scale, y: -scale)
|
||||
context.translateBy(x: 0, y: -size.height)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
draw(at: .zero)
|
||||
UIGraphicsPopContext()
|
||||
|
||||
guard let cgImage = context.makeImage(),
|
||||
let dataProvider = cgImage.dataProvider,
|
||||
let data = dataProvider.data,
|
||||
let pixels = CFDataGetBytePtr(data) else {
|
||||
assertionFailure("Unexpected error!")
|
||||
return nil
|
||||
}
|
||||
|
||||
let width = cgImage.width
|
||||
let height = cgImage.height
|
||||
let bytesPerRow = cgImage.bytesPerRow
|
||||
|
||||
var factors: [(Float, Float, Float)] = []
|
||||
for y in 0 ..< components.1 {
|
||||
for x in 0 ..< components.0 {
|
||||
let normalisation: Float = (x == 0 && y == 0) ? 1 : 2
|
||||
let factor = multiplyBasisFunction(pixels: pixels, width: width, height: height, bytesPerRow: bytesPerRow, bytesPerPixel: cgImage.bitsPerPixel / 8, pixelOffset: 0) {
|
||||
normalisation * cos(Float.pi * Float(x) * $0 / Float(width)) as Float * cos(Float.pi * Float(y) * $1 / Float(height)) as Float
|
||||
}
|
||||
factors.append(factor)
|
||||
}
|
||||
}
|
||||
|
||||
let dc = factors.first!
|
||||
let ac = factors.dropFirst()
|
||||
|
||||
var hash = ""
|
||||
|
||||
let sizeFlag = (components.0 - 1) + (components.1 - 1) * 9
|
||||
hash += sizeFlag.encode83(length: 1)
|
||||
|
||||
let maximumValue: Float
|
||||
if ac.count > 0 {
|
||||
let actualMaximumValue = ac.map({ max(abs($0.0), abs($0.1), abs($0.2)) }).max()!
|
||||
let quantisedMaximumValue = Int(max(0, min(82, floor(actualMaximumValue * 166 - 0.5))))
|
||||
maximumValue = Float(quantisedMaximumValue + 1) / 166
|
||||
hash += quantisedMaximumValue.encode83(length: 1)
|
||||
} else {
|
||||
maximumValue = 1
|
||||
hash += 0.encode83(length: 1)
|
||||
}
|
||||
|
||||
hash += encodeDC(dc).encode83(length: 4)
|
||||
|
||||
for factor in ac {
|
||||
hash += encodeAC(factor, maximumValue: maximumValue).encode83(length: 2)
|
||||
}
|
||||
|
||||
return hash
|
||||
}
|
||||
|
||||
private func multiplyBasisFunction(pixels: UnsafePointer<UInt8>, width: Int, height: Int, bytesPerRow: Int, bytesPerPixel: Int, pixelOffset: Int, basisFunction: (Float, Float) -> Float) -> (Float, Float, Float) {
|
||||
var r: Float = 0
|
||||
var g: Float = 0
|
||||
var b: Float = 0
|
||||
|
||||
let buffer = UnsafeBufferPointer(start: pixels, count: height * bytesPerRow)
|
||||
|
||||
for x in 0 ..< width {
|
||||
for y in 0 ..< height {
|
||||
let basis = basisFunction(Float(x), Float(y))
|
||||
r += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 0 + y * bytesPerRow])
|
||||
g += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 1 + y * bytesPerRow])
|
||||
b += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 2 + y * bytesPerRow])
|
||||
}
|
||||
}
|
||||
|
||||
let scale = 1 / Float(width * height)
|
||||
|
||||
return (r * scale, g * scale, b * scale)
|
||||
}
|
||||
}
|
||||
|
||||
private func encodeDC(_ value: (Float, Float, Float)) -> Int {
|
||||
let roundedR = linearTosRGB(value.0)
|
||||
let roundedG = linearTosRGB(value.1)
|
||||
let roundedB = linearTosRGB(value.2)
|
||||
return (roundedR << 16) + (roundedG << 8) + roundedB
|
||||
}
|
||||
|
||||
private func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int {
|
||||
let quantR = Int(max(0, min(18, floor(signPow(value.0 / maximumValue, 0.5) * 9 + 9.5))))
|
||||
let quantG = Int(max(0, min(18, floor(signPow(value.1 / maximumValue, 0.5) * 9 + 9.5))))
|
||||
let quantB = Int(max(0, min(18, floor(signPow(value.2 / maximumValue, 0.5) * 9 + 9.5))))
|
||||
|
||||
return quantR * 19 * 19 + quantG * 19 + quantB
|
||||
}
|
||||
|
||||
private func signPow(_ value: Float, _ exp: Float) -> Float {
|
||||
return copysign(pow(abs(value), exp), value)
|
||||
}
|
||||
|
||||
private func linearTosRGB(_ value: Float) -> Int {
|
||||
let v = max(0, min(1, value))
|
||||
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
|
||||
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
|
||||
}
|
||||
|
||||
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
|
||||
let v = Float(Int64(value)) / 255
|
||||
if v <= 0.04045 { return v / 12.92 }
|
||||
else { return pow((v + 0.055) / 1.055, 2.4) }
|
||||
}
|
||||
|
||||
private let encodeCharacters: [String] = {
|
||||
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
|
||||
}()
|
||||
|
||||
extension BinaryInteger {
|
||||
func encode83(length: Int) -> String {
|
||||
var result = ""
|
||||
for i in 1 ... length {
|
||||
let digit = (Int(self) / pow(83, length - i)) % 83
|
||||
result += encodeCharacters[Int(digit)]
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private func pow(_ base: Int, _ exponent: Int) -> Int {
|
||||
return (0 ..< exponent).reduce(1) { value, _ in value * base }
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2018 Wolt Enterprises
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,45 +0,0 @@
|
||||
# BlurHash for iOS, in Swift
|
||||
|
||||
## Standalone decoder and encoder
|
||||
|
||||
[BlurHashDecode.swift](BlurHashDecode.swift) and [BlurHashEncode.swift](BlurHashEncode.swift) contain a decoder
|
||||
and encoder for BlurHash to and from `UIImage`. Both files are completeiy standalone, and can simply be copied into your
|
||||
project directly.
|
||||
|
||||
### Decoding
|
||||
|
||||
[BlurHashDecode.swift](BlurHashDecode.swift) implements the following extension on `UIImage`:
|
||||
|
||||
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1)
|
||||
|
||||
This creates a UIImage containing the placeholder image decoded from the BlurHash string, or returns nil if decoding failed.
|
||||
The parameters are:
|
||||
|
||||
* `blurHash` - A string containing the BlurHash.
|
||||
* `size` - The requested output size. You should keep this small, and let UIKit scale it up for you. 32 pixels wide is plenty.
|
||||
* `punch` - Adjusts the contrast of the output image. Tweak it if you want a different look for your placeholders.
|
||||
|
||||
### Encoding
|
||||
|
||||
[BlurHashEncode.swift](BlurHashEncode.swift) implements the following extension on `UIImage`:
|
||||
|
||||
public func blurHash(numberOfComponents components: (Int, Int)) -> String?
|
||||
|
||||
This returns a string containing the BlurHash for the image, or nil if the image was in a weird format that is not supported.
|
||||
The parameters are:
|
||||
|
||||
* `numberOfComponents` - a Tuple of integers specifying the number of components in the X and Y directions. Both must be
|
||||
between 1 and 9 inclusive, or the function will return nil. 3 to 5 is usually a good range.
|
||||
|
||||
## BlurHashKit
|
||||
|
||||
This is a more advanced library, currently in development. It will let you do more advanced operations using BlurHashes,
|
||||
such testing whether various parts of an image are dark and light, or generating BlurHashes as gradients from corner colours.
|
||||
|
||||
It is currently not documented or finalised, but feel free to look into the different files and what they implement, or look at
|
||||
how it is used by the test app.
|
||||
|
||||
## BlurHashTest.app
|
||||
|
||||
This is a simple test app that shows how to use the various pieces of BlurHash functionality, and lets you play with the
|
||||
algorithm.
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// CredentialHandler.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Bryan Montz on 4/26/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AuthenticationServices
|
||||
|
||||
final class CredentialHandler: NSObject, ASAuthorizationControllerDelegate {
|
||||
|
||||
func check_credentials() {
|
||||
let requests: [ASAuthorizationRequest] = [ASAuthorizationPasswordProvider().createRequest()]
|
||||
let authorizationController = ASAuthorizationController(authorizationRequests: requests)
|
||||
authorizationController.delegate = self
|
||||
authorizationController.performRequests()
|
||||
}
|
||||
|
||||
func save_credential(pubkey: String, privkey: String) {
|
||||
SecAddSharedWebCredential("damus.io" as CFString, pubkey as CFString, privkey as CFString, { error in
|
||||
if let error {
|
||||
print("⚠️ An error occurred while saving credentials: \(error)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - ASAuthorizationControllerDelegate
|
||||
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
|
||||
guard let cred = authorization.credential as? ASPasswordCredential,
|
||||
let parsedKey = parse_key(cred.password) else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
switch parsedKey {
|
||||
case .pub, .priv:
|
||||
try? await process_login(parsedKey, is_pubkey: false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
|
||||
print("⚠️ Warning: authentication failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
+10
-326
@@ -8,127 +8,13 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
import LinkPresentation
|
||||
import Kingfisher
|
||||
|
||||
class ImageMetadataState {
|
||||
var state: ImageMetaProcessState
|
||||
var meta: ImageMetadata
|
||||
|
||||
init(state: ImageMetaProcessState, meta: ImageMetadata) {
|
||||
self.state = state
|
||||
self.meta = meta
|
||||
}
|
||||
}
|
||||
|
||||
enum ImageMetaProcessState {
|
||||
case processing
|
||||
case failed
|
||||
case processed(UIImage)
|
||||
case not_needed
|
||||
|
||||
var img: UIImage? {
|
||||
switch self {
|
||||
case .processed(let img):
|
||||
return img
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TranslationModel: ObservableObject {
|
||||
@Published var note_language: String?
|
||||
@Published var state: TranslateStatus
|
||||
|
||||
init(state: TranslateStatus) {
|
||||
self.state = state
|
||||
self.note_language = nil
|
||||
}
|
||||
}
|
||||
|
||||
class NoteArtifactsModel: ObservableObject {
|
||||
@Published var state: NoteArtifactState
|
||||
|
||||
init(state: NoteArtifactState) {
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
|
||||
class PreviewModel: ObservableObject {
|
||||
@Published var state: PreviewState
|
||||
|
||||
func store(preview: LPLinkMetadata?) {
|
||||
state = .loaded(Preview(meta: preview))
|
||||
}
|
||||
|
||||
init(state: PreviewState) {
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
|
||||
class ZapsDataModel: ObservableObject {
|
||||
@Published var zaps: [Zap]
|
||||
|
||||
init(_ zaps: [Zap]) {
|
||||
self.zaps = zaps
|
||||
}
|
||||
}
|
||||
|
||||
class RelativeTimeModel: ObservableObject {
|
||||
private(set) var last_update: Int64
|
||||
@Published var value: String {
|
||||
didSet {
|
||||
self.last_update = Int64(Date().timeIntervalSince1970)
|
||||
}
|
||||
}
|
||||
|
||||
init(value: String) {
|
||||
self.last_update = 0
|
||||
self.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
class EventData {
|
||||
var translations_model: TranslationModel
|
||||
var artifacts_model: NoteArtifactsModel
|
||||
var preview_model: PreviewModel
|
||||
var zaps_model : ZapsDataModel
|
||||
var relative_time: RelativeTimeModel
|
||||
var validated: ValidationResult
|
||||
|
||||
var translations: TranslateStatus {
|
||||
return translations_model.state
|
||||
}
|
||||
|
||||
var artifacts: NoteArtifactState {
|
||||
return artifacts_model.state
|
||||
}
|
||||
|
||||
var preview: PreviewState {
|
||||
return preview_model.state
|
||||
}
|
||||
|
||||
var zaps: [Zap] {
|
||||
return zaps_model.zaps
|
||||
}
|
||||
|
||||
init(zaps: [Zap] = []) {
|
||||
self.translations_model = .init(state: .havent_tried)
|
||||
self.artifacts_model = .init(state: .not_loaded)
|
||||
self.zaps_model = .init(zaps)
|
||||
self.validated = .unknown
|
||||
self.preview_model = .init(state: .not_loaded)
|
||||
self.relative_time = .init(value: "")
|
||||
}
|
||||
}
|
||||
|
||||
class EventCache {
|
||||
private var events: [String: NostrEvent] = [:]
|
||||
private var replies = ReplyMap()
|
||||
private var cancellable: AnyCancellable?
|
||||
private var image_metadata: [String: ImageMetadataState] = [:]
|
||||
private var event_data: [String: EventData] = [:]
|
||||
private var translations: [String: TranslateStatus] = [:]
|
||||
private var artifacts: [String: NoteArtifacts] = [:]
|
||||
|
||||
//private var thread_latest: [String: Int64]
|
||||
|
||||
@@ -140,56 +26,20 @@ class EventCache {
|
||||
}
|
||||
}
|
||||
|
||||
func get_cache_data(_ evid: String) -> EventData {
|
||||
guard let data = event_data[evid] else {
|
||||
let data = EventData()
|
||||
event_data[evid] = data
|
||||
return data
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func is_event_valid(_ evid: String) -> ValidationResult {
|
||||
return get_cache_data(evid).validated
|
||||
}
|
||||
|
||||
func store_event_validation(evid: String, validated: ValidationResult) {
|
||||
get_cache_data(evid).validated = validated
|
||||
}
|
||||
|
||||
func store_translation_artifacts(evid: String, translated: TranslateStatus) {
|
||||
get_cache_data(evid).translations_model.state = translated
|
||||
self.translations[evid] = translated
|
||||
}
|
||||
|
||||
func store_artifacts(evid: String, artifacts: NoteArtifacts) {
|
||||
get_cache_data(evid).artifacts_model.state = .loaded(artifacts)
|
||||
self.artifacts[evid] = artifacts
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func store_zap(zap: Zap) -> Bool {
|
||||
let data = get_cache_data(zap.target.id).zaps_model
|
||||
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
|
||||
}
|
||||
|
||||
func lookup_zaps(target: ZapTarget) -> [Zap] {
|
||||
return get_cache_data(target.id).zaps_model.zaps
|
||||
}
|
||||
|
||||
func store_img_metadata(url: URL, meta: ImageMetadataState) {
|
||||
self.image_metadata[url.absoluteString.lowercased()] = meta
|
||||
}
|
||||
|
||||
func lookup_artifacts(evid: String) -> NoteArtifactState {
|
||||
return get_cache_data(evid).artifacts_model.state
|
||||
}
|
||||
|
||||
func lookup_img_metadata(url: URL) -> ImageMetadataState? {
|
||||
return image_metadata[url.absoluteString.lowercased()]
|
||||
func lookup_artifacts(evid: String) -> NoteArtifacts? {
|
||||
return self.artifacts[evid]
|
||||
}
|
||||
|
||||
func lookup_translated_artifacts(evid: String) -> TranslateStatus? {
|
||||
return get_cache_data(evid).translations_model.state
|
||||
return self.translations[evid]
|
||||
}
|
||||
|
||||
func parent_events(event: NostrEvent) -> [NostrEvent] {
|
||||
@@ -198,7 +48,7 @@ class EventCache {
|
||||
var ev = event
|
||||
|
||||
while true {
|
||||
guard let direct_reply = ev.direct_replies(nil).last else {
|
||||
guard let direct_reply = ev.direct_replies(nil).first else {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -255,174 +105,8 @@ class EventCache {
|
||||
|
||||
private func prune() {
|
||||
events = [:]
|
||||
event_data = [:]
|
||||
translations = [:]
|
||||
artifacts = [:]
|
||||
replies.replies = [:]
|
||||
}
|
||||
}
|
||||
|
||||
func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||
guard settings.can_translate else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not translate self-authored notes if logged in with a private key
|
||||
// as we can assume the user can understand their own notes.
|
||||
// The detected language prediction could be incorrect and not in the list of preferred languages.
|
||||
// Offering a translation in this case is definitely incorrect so let's avoid it altogether.
|
||||
if our_keypair.privkey != nil && our_keypair.pubkey == event.pubkey {
|
||||
return false
|
||||
}
|
||||
|
||||
if let note_lang {
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
// Don't translate if its in our preferred languages
|
||||
guard !preferredLanguages.contains(note_lang) else {
|
||||
// if its the same, give up and don't retry
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// we should start translating if we have auto_translate on
|
||||
return true
|
||||
}
|
||||
|
||||
func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current_status: TranslateStatus, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||
|
||||
switch current_status {
|
||||
case .havent_tried:
|
||||
return should_translate(event: event, our_keypair: our_keypair, settings: settings, note_lang: note_lang) && settings.auto_translate
|
||||
case .translating: return false
|
||||
case .translated: return false
|
||||
case .not_needed: return false
|
||||
}
|
||||
}
|
||||
|
||||
struct PreloadPlan {
|
||||
let data: EventData
|
||||
let event: NostrEvent
|
||||
let load_artifacts: Bool
|
||||
let load_translations: Bool
|
||||
let load_preview: Bool
|
||||
}
|
||||
|
||||
func load_preview(artifacts: NoteArtifacts) async -> Preview? {
|
||||
guard let link = artifacts.links.first else {
|
||||
return nil
|
||||
}
|
||||
let meta = await Preview.fetch_metadata(for: link)
|
||||
return Preview(meta: meta)
|
||||
}
|
||||
|
||||
func get_preload_plan(cache: EventData, ev: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore) -> PreloadPlan? {
|
||||
let load_artifacts = cache.artifacts.should_preload
|
||||
if load_artifacts {
|
||||
cache.artifacts_model.state = .loading
|
||||
}
|
||||
|
||||
// Cached event might not have the note language determined yet, so determine the language here before figuring out if translations should be preloaded.
|
||||
let note_lang = cache.translations_model.note_language ?? ev.note_language(our_keypair.privkey) ?? current_language()
|
||||
|
||||
let load_translations = should_preload_translation(event: ev, our_keypair: our_keypair, current_status: cache.translations, settings: settings, note_lang: note_lang)
|
||||
if load_translations {
|
||||
cache.translations_model.state = .translating
|
||||
}
|
||||
|
||||
let load_preview = cache.preview.should_preload
|
||||
if load_preview {
|
||||
cache.preview_model.state = .loading
|
||||
}
|
||||
|
||||
if !load_artifacts && !load_translations && !load_preview {
|
||||
return nil
|
||||
}
|
||||
|
||||
return PreloadPlan(data: cache, event: ev, load_artifacts: load_artifacts, load_translations: load_translations, load_preview: load_preview)
|
||||
}
|
||||
|
||||
func preload_image(url: URL) {
|
||||
if ImageCache.default.isCached(forKey: url.absoluteString) {
|
||||
print("Preloaded image \(url.absoluteString) found in cache")
|
||||
// looks like we already have it cached. no download needed
|
||||
return
|
||||
}
|
||||
|
||||
print("Preloading image \(url.absoluteString)")
|
||||
|
||||
KingfisherManager.shared.retrieveImage(with: ImageResource(downloadURL: url)) { val in
|
||||
print("Preloaded image \(url.absoluteString)")
|
||||
}
|
||||
}
|
||||
|
||||
func preload_event(plan: PreloadPlan, profiles: Profiles, our_keypair: Keypair, settings: UserSettingsStore) async {
|
||||
var artifacts: NoteArtifacts? = plan.data.artifacts.artifacts
|
||||
|
||||
print("Preloading event \(plan.event.content)")
|
||||
|
||||
// preload pfp
|
||||
if let profile = profiles.lookup(id: plan.event.pubkey),
|
||||
let picture = profile.picture,
|
||||
let url = URL(string: picture) {
|
||||
preload_image(url: url)
|
||||
}
|
||||
|
||||
if artifacts == nil && plan.load_artifacts {
|
||||
let arts = render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey)
|
||||
artifacts = arts
|
||||
|
||||
// we need these asap
|
||||
DispatchQueue.main.async {
|
||||
plan.data.artifacts_model.state = .loaded(arts)
|
||||
}
|
||||
|
||||
for url in arts.images {
|
||||
preload_image(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
if plan.load_preview {
|
||||
let arts = artifacts ?? render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey)
|
||||
let preview = await load_preview(artifacts: arts)
|
||||
DispatchQueue.main.async {
|
||||
if let preview {
|
||||
plan.data.preview_model.state = .loaded(preview)
|
||||
} else {
|
||||
plan.data.preview_model.state = .loaded(.failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let note_language = plan.data.translations_model.note_language ?? plan.event.note_language(our_keypair.privkey) ?? current_language()
|
||||
|
||||
var translations: TranslateStatus? = nil
|
||||
// We have to recheck should_translate here now that we have note_language
|
||||
if plan.load_translations && should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate
|
||||
{
|
||||
translations = await translate_note(profiles: profiles, privkey: our_keypair.privkey, event: plan.event, settings: settings, note_lang: note_language)
|
||||
}
|
||||
|
||||
let timeago = format_relative_time(plan.event.created_at)
|
||||
let ts = translations
|
||||
DispatchQueue.main.async {
|
||||
if let ts {
|
||||
plan.data.translations_model.state = ts
|
||||
}
|
||||
plan.data.relative_time.value = timeago
|
||||
plan.data.translations_model.note_language = note_language
|
||||
}
|
||||
}
|
||||
|
||||
func preload_events(event_cache: EventCache, events: [NostrEvent], profiles: Profiles, our_keypair: Keypair, settings: UserSettingsStore) {
|
||||
|
||||
let plans = events.compactMap { ev in
|
||||
get_preload_plan(cache: event_cache.get_cache_data(ev.id), ev: ev, our_keypair: our_keypair, settings: settings)
|
||||
}
|
||||
|
||||
Task.init {
|
||||
for plan in plans {
|
||||
await preload_event(plan: plan, profiles: profiles, our_keypair: our_keypair, settings: settings)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Kingfisher
|
||||
|
||||
extension KFOptionSetter {
|
||||
|
||||
func imageContext(_ imageContext: ImageContext, disable_animation: Bool) -> Self {
|
||||
func imageContext(_ imageContext: ImageContext) -> Self {
|
||||
options.callbackQueue = .dispatch(.global(qos: .background))
|
||||
options.processingQueue = .dispatch(.global(qos: .background))
|
||||
options.downloader = CustomImageDownloader.shared
|
||||
@@ -26,14 +26,7 @@ extension KFOptionSetter {
|
||||
options.backgroundDecode = true
|
||||
options.cacheOriginalImage = true
|
||||
options.scaleFactor = UIScreen.main.scale
|
||||
options.onlyLoadFirstFrame = disable_animation
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func image_fade(duration: TimeInterval) -> Self {
|
||||
options.transition = ImageTransition.fade(duration)
|
||||
options.keepCurrentImageWhileLoading = false
|
||||
options.onlyLoadFirstFrame = should_disable_image_animation()
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -35,9 +35,7 @@ let custom_hashtags: [String: CustomHashtag] = [
|
||||
"coffeechain": CustomHashtag.coffee,
|
||||
"plebchain": CustomHashtag.plebchain,
|
||||
"zap": CustomHashtag.zap,
|
||||
"zaps": CustomHashtag.zap,
|
||||
"zapathon": CustomHashtag.zap,
|
||||
"onlyzaps": CustomHashtag.zap,
|
||||
]
|
||||
|
||||
func hashtag_str(_ htag: String) -> CompatibleText {
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
//
|
||||
// ImageMetadata.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct ImageMetaDim: Equatable, StringCodable {
|
||||
init(width: Int, height: Int) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
}
|
||||
|
||||
init?(from string: String) {
|
||||
guard let dim = parse_image_meta_dim(string) else {
|
||||
return nil
|
||||
}
|
||||
self = dim
|
||||
}
|
||||
|
||||
func to_string() -> String {
|
||||
"\(width)x\(height)"
|
||||
}
|
||||
|
||||
var size: CGSize {
|
||||
return CGSize(width: CGFloat(self.width), height: CGFloat(self.height))
|
||||
}
|
||||
|
||||
let width: Int
|
||||
let height: Int
|
||||
}
|
||||
|
||||
struct ProcessedImageMetadata {
|
||||
let blurhash: UIImage?
|
||||
let dim: ImageMetaDim?
|
||||
}
|
||||
|
||||
struct ImageMetadata: Equatable {
|
||||
let url: URL
|
||||
let blurhash: String?
|
||||
let dim: ImageMetaDim?
|
||||
|
||||
init(url: URL, blurhash: String? = nil, dim: ImageMetaDim? = nil) {
|
||||
self.url = url
|
||||
self.blurhash = blurhash
|
||||
self.dim = dim
|
||||
}
|
||||
|
||||
init?(tag: [String]) {
|
||||
guard let meta = decode_image_metadata(tag) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self = meta
|
||||
}
|
||||
|
||||
func to_tag() -> [String] {
|
||||
return image_metadata_to_tag(self)
|
||||
}
|
||||
}
|
||||
|
||||
func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? {
|
||||
let res = Task.init {
|
||||
let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0))
|
||||
guard let img = UIImage.init(blurHash: blurhash, size: size) else {
|
||||
let noimg: UIImage? = nil
|
||||
return noimg
|
||||
}
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
|
||||
return await res.value
|
||||
}
|
||||
|
||||
func image_metadata_to_tag(_ meta: ImageMetadata) -> [String] {
|
||||
var tags = ["imeta", "url \(meta.url.absoluteString)"]
|
||||
if let blurhash = meta.blurhash {
|
||||
tags.append("blurhash \(blurhash)")
|
||||
}
|
||||
if let dim = meta.dim {
|
||||
tags.append("dim \(dim.to_string())")
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func decode_image_metadata(_ parts: [String]) -> ImageMetadata? {
|
||||
var url: URL? = nil
|
||||
var blurhash: String? = nil
|
||||
var dim: ImageMetaDim? = nil
|
||||
|
||||
for part in parts {
|
||||
if part == "imeta" {
|
||||
continue
|
||||
}
|
||||
|
||||
let ps = part.split(separator: " ")
|
||||
|
||||
guard ps.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
let pname = ps[0]
|
||||
let pval = ps[1]
|
||||
|
||||
if pname == "blurhash" {
|
||||
blurhash = String(pval)
|
||||
} else if pname == "dim" {
|
||||
dim = parse_image_meta_dim(String(pval))
|
||||
} else if pname == "url" {
|
||||
url = URL(string: String(pval))
|
||||
}
|
||||
}
|
||||
|
||||
guard let url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ImageMetadata(url: url, blurhash: blurhash, dim: dim)
|
||||
}
|
||||
|
||||
func parse_image_meta_dim(_ pval: String) -> ImageMetaDim? {
|
||||
let parts = pval.split(separator: "x")
|
||||
guard parts.count == 2,
|
||||
let width = Int(parts[0]),
|
||||
let height = Int(parts[1]) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ImageMetaDim(width: width, height: height)
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
func resized(to size: CGSize) -> UIImage {
|
||||
return UIGraphicsImageRenderer(size: size).image { _ in
|
||||
draw(in: CGRect(origin: .zero, size: size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func get_blurhash_size(img_size: CGSize) -> CGSize {
|
||||
return CGSize(width: 100.0, height: (100.0/img_size.width) * img_size.height)
|
||||
}
|
||||
|
||||
func calculate_blurhash(img: UIImage) async -> String? {
|
||||
guard img.size.height > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let res = Task.init {
|
||||
let bhs = get_blurhash_size(img_size: img.size)
|
||||
let smaller = img.resized(to: bhs)
|
||||
|
||||
guard let blurhash = smaller.blurHash(numberOfComponents: (5,5)) else {
|
||||
let meta: String? = nil
|
||||
return meta
|
||||
}
|
||||
|
||||
return blurhash
|
||||
}
|
||||
|
||||
return await res.value
|
||||
}
|
||||
|
||||
func calculate_image_metadata(url: URL, img: UIImage, blurhash: String) -> ImageMetadata {
|
||||
let width = Int(img.size.width)
|
||||
let height = Int(img.size.height)
|
||||
let dim = ImageMetaDim(width: width, height: height)
|
||||
|
||||
return ImageMetadata(url: url, blurhash: blurhash, dim: dim)
|
||||
}
|
||||
|
||||
|
||||
func process_image_metadata(cache: EventCache, ev: NostrEvent) {
|
||||
for tag in ev.tags {
|
||||
guard tag.count >= 2 && tag[0] == "imeta" else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let meta = ImageMetadata(tag: tag) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard cache.lookup_img_metadata(url: meta.url) == nil else {
|
||||
continue
|
||||
}
|
||||
|
||||
let state = ImageMetadataState(state: .processing, meta: meta)
|
||||
cache.store_img_metadata(url: meta.url, meta: state)
|
||||
|
||||
if let blurhash = meta.blurhash {
|
||||
Task.init {
|
||||
let img = await process_blurhash(blurhash: blurhash, size: meta.dim?.size)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let img {
|
||||
state.state = .processed(img)
|
||||
} else {
|
||||
state.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// LocalNotification.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LossyLocalNotification {
|
||||
let type: LocalNotificationType
|
||||
let event_id: String
|
||||
|
||||
func to_user_info() -> [AnyHashable: Any] {
|
||||
return [
|
||||
"type": self.type.rawValue,
|
||||
"evid": self.event_id
|
||||
]
|
||||
}
|
||||
|
||||
static func from_user_info(user_info: [AnyHashable: Any]) -> LossyLocalNotification {
|
||||
let target_id = user_info["evid"] as! String
|
||||
let typestr = user_info["type"] as! String
|
||||
let type = LocalNotificationType(rawValue: typestr)!
|
||||
|
||||
return LossyLocalNotification(type: type, event_id: target_id)
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalNotification {
|
||||
let type: LocalNotificationType
|
||||
let event: NostrEvent
|
||||
let target: NostrEvent
|
||||
let content: String
|
||||
|
||||
func to_lossy() -> LossyLocalNotification {
|
||||
return LossyLocalNotification(type: self.type, event_id: self.target.id)
|
||||
}
|
||||
}
|
||||
|
||||
enum LocalNotificationType: String {
|
||||
case dm
|
||||
case like
|
||||
case mention
|
||||
case repost
|
||||
case zap
|
||||
}
|
||||
@@ -20,6 +20,9 @@ extension Notification.Name {
|
||||
static var select_quote: Notification.Name {
|
||||
return Notification.Name("select quote")
|
||||
}
|
||||
static var reply: Notification.Name {
|
||||
return Notification.Name("reply")
|
||||
}
|
||||
static var profile_updated: Notification.Name {
|
||||
return Notification.Name("profile_updated")
|
||||
}
|
||||
@@ -53,9 +56,6 @@ extension Notification.Name {
|
||||
static var post: Notification.Name {
|
||||
return Notification.Name("send post")
|
||||
}
|
||||
static var compose: Notification.Name {
|
||||
return Notification.Name("compose")
|
||||
}
|
||||
static var boost: Notification.Name {
|
||||
return Notification.Name("boost")
|
||||
}
|
||||
@@ -110,12 +110,6 @@ extension Notification.Name {
|
||||
static var unmute_thread: Notification.Name {
|
||||
return Notification.Name("unmute_thread")
|
||||
}
|
||||
static var local_notification: Notification.Name {
|
||||
return Notification.Name("local_notification")
|
||||
}
|
||||
static var onlyzaps_mode: Notification.Name {
|
||||
return Notification.Name("hide_reactions")
|
||||
}
|
||||
}
|
||||
|
||||
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// BinaryParser.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class BinaryParser {
|
||||
var pos: Int
|
||||
var buf: [UInt8]
|
||||
|
||||
init(buf: [UInt8], pos: Int = 0) {
|
||||
self.pos = pos
|
||||
self.buf = buf
|
||||
}
|
||||
|
||||
func read_byte() -> UInt8? {
|
||||
guard pos < buf.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let v = buf[pos]
|
||||
pos += 1
|
||||
return v
|
||||
}
|
||||
|
||||
func read_bytes(_ n: Int) -> [UInt8]? {
|
||||
guard pos + n < buf.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let v = [UInt8](self.buf[pos...pos+n])
|
||||
return v
|
||||
}
|
||||
|
||||
func read_u16() -> UInt16? {
|
||||
let start = self.pos
|
||||
|
||||
guard let b1 = read_byte(), let b2 = read_byte() else {
|
||||
self.pos = start
|
||||
return nil
|
||||
}
|
||||
|
||||
return (UInt16(b1) << 8) | UInt16(b2)
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,9 @@ class PostBox {
|
||||
return
|
||||
}
|
||||
|
||||
let remaining = pool.descriptors.map { $0.url.id }
|
||||
let remaining = pool.descriptors.map {
|
||||
$0.url.absoluteString
|
||||
}
|
||||
|
||||
let posted_ev = PostedEvent(event: event, remaining: remaining)
|
||||
events[event.id] = posted_ev
|
||||
|
||||
@@ -21,47 +21,6 @@ class CachedMetadata {
|
||||
enum Preview {
|
||||
case value(CachedMetadata)
|
||||
case failed
|
||||
|
||||
init(meta: LPLinkMetadata?) {
|
||||
if let meta {
|
||||
self = .value(CachedMetadata(meta: meta))
|
||||
} else {
|
||||
self = .failed
|
||||
}
|
||||
}
|
||||
|
||||
static func fetch_metadata(for url: URL) async -> LPLinkMetadata? {
|
||||
// iOS 15 is crashing for some reason
|
||||
guard #available(iOS 16, *) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let provider = LPMetadataProvider()
|
||||
|
||||
do {
|
||||
return try await provider.startFetchingMetadata(for: url)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum PreviewState {
|
||||
case not_loaded
|
||||
case loading
|
||||
case loaded(Preview)
|
||||
|
||||
var should_preload: Bool {
|
||||
switch self {
|
||||
case .loaded:
|
||||
return false
|
||||
case .loading:
|
||||
return false
|
||||
case .not_loaded:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PreviewCache {
|
||||
@@ -80,6 +39,15 @@ class PreviewCache {
|
||||
self.image_meta[evid] = image_fill
|
||||
}
|
||||
|
||||
func store(evid: String, preview: LPLinkMetadata?) {
|
||||
switch preview {
|
||||
case .none:
|
||||
previews[evid] = .failed
|
||||
case .some(let meta):
|
||||
previews[evid] = .value(CachedMetadata(meta: meta))
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
self.previews = [:]
|
||||
self.image_meta = [:]
|
||||
|
||||
@@ -89,6 +89,6 @@ func load_relay_filters(_ pubkey: String) -> Set<RelayFilter>? {
|
||||
|
||||
func determine_to_relays(pool: RelayPool, filters: RelayFilters) -> [String] {
|
||||
return pool.descriptors
|
||||
.map { $0.url.url.absoluteString }
|
||||
.map { $0.url.absoluteString }
|
||||
.filter { !filters.is_filtered(timeline: .search, relay_id: $0) }
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// StringCodable.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol StringCodable {
|
||||
init?(from string: String)
|
||||
func to_string() -> String
|
||||
}
|
||||
@@ -24,8 +24,6 @@ public struct Translator {
|
||||
switch userSettingsStore.translation_service {
|
||||
case .libretranslate:
|
||||
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
|
||||
case .nokyctranslate:
|
||||
return try await translateWithNoKYCTranslate(text, from: sourceLanguage, to: targetLanguage)
|
||||
case .deepl:
|
||||
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
|
||||
case .none:
|
||||
@@ -87,29 +85,6 @@ public struct Translator {
|
||||
let response: Response = try await decodedData(for: request)
|
||||
return response.translations.map { $0.text }.joined(separator: " ")
|
||||
}
|
||||
|
||||
private func translateWithNoKYCTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||
let url = try makeURL("https://translate.nokyctranslate.com", 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.nokyctranslate_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 makeURL(_ baseUrl: String, path: String) throws -> URL {
|
||||
guard var components = URLComponents(string: baseUrl) else {
|
||||
|
||||
@@ -52,10 +52,6 @@ struct Zap {
|
||||
public let is_anon: Bool
|
||||
public let private_request: NostrEvent?
|
||||
|
||||
var request_ev: NostrEvent {
|
||||
return private_request ?? self.request.ev
|
||||
}
|
||||
|
||||
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: 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 {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// BigButton.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BigButton: View {
|
||||
let text: String
|
||||
let action: () -> ()
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(_ text: String, action: @escaping () -> ()) {
|
||||
self.text = text
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
action()
|
||||
}) {
|
||||
Text(text)
|
||||
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
|
||||
.foregroundColor(colorScheme == .light ? DamusColors.black : DamusColors.white)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.stroke(colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white, lineWidth: 1)
|
||||
}
|
||||
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BigButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BigButton("Cancel", action: {})
|
||||
}
|
||||
}
|
||||
@@ -8,37 +8,40 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
enum ActionBarSheet: Identifiable {
|
||||
case reply
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .reply: return "reply"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EventActionBar: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let test_lnurl: String?
|
||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
|
||||
// just used for previews
|
||||
@State var sheet: ActionBarSheet? = nil
|
||||
@State var show_share_sheet: Bool = false
|
||||
@State var show_share_action: Bool = false
|
||||
@State var show_repost_action: Bool = false
|
||||
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
|
||||
}
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var lnurl: String? {
|
||||
damus_state.profiles.lookup(id: event.pubkey)?.lnurl
|
||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = 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))
|
||||
}
|
||||
|
||||
var show_like: Bool {
|
||||
if damus_state.settings.onlyzaps_mode {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
var lnurl: String? {
|
||||
test_lnurl ?? damus_state.profiles.lookup(id: event.pubkey)?.lnurl
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -46,7 +49,7 @@ struct EventActionBar: View {
|
||||
if damus_state.keypair.privkey != nil {
|
||||
HStack(spacing: 4) {
|
||||
EventActionButton(img: "bubble.left", col: bar.replied ? DamusColors.purple : Color.gray) {
|
||||
notify(.compose, PostAction.replying_to(event))
|
||||
notify(.reply, event)
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
|
||||
@@ -61,7 +64,7 @@ struct EventActionBar: View {
|
||||
if bar.boosted {
|
||||
notify(.delete, bar.our_boost)
|
||||
} else {
|
||||
self.show_repost_action = true
|
||||
send_boost()
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
|
||||
@@ -69,25 +72,22 @@ struct EventActionBar: View {
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
||||
}
|
||||
|
||||
if show_like {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 4) {
|
||||
LikeButton(liked: bar.liked) {
|
||||
if bar.liked {
|
||||
notify(.delete, bar.our_like)
|
||||
} else {
|
||||
send_like()
|
||||
}
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 4) {
|
||||
LikeButton(liked: bar.liked) {
|
||||
if bar.liked {
|
||||
notify(.delete, bar.our_like)
|
||||
} else {
|
||||
send_like()
|
||||
}
|
||||
|
||||
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.nip05_colorized(gradient: bar.liked)
|
||||
}
|
||||
|
||||
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.nip05_colorized(gradient: bar.liked)
|
||||
}
|
||||
|
||||
|
||||
if let lnurl = self.lnurl {
|
||||
Spacer()
|
||||
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar)
|
||||
@@ -99,35 +99,26 @@ struct EventActionBar: View {
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
|
||||
}
|
||||
.onAppear {
|
||||
self.bar.update(damus: damus_state, evid: self.event.id)
|
||||
}
|
||||
.sheet(isPresented: $show_share_action, onDismiss: { self.show_share_action = false }) {
|
||||
.sheet(isPresented: $show_share_action) {
|
||||
if #available(iOS 16.0, *) {
|
||||
ShareAction(event: event, bookmarks: damus_state.bookmarks, show_share: $show_share_sheet)
|
||||
ShareAction(event: event, bookmarks: damus_state.bookmarks, show_share_sheet: $show_share_sheet, show_share_action: $show_share_action)
|
||||
.presentationDetents([.height(300)])
|
||||
.presentationDragIndicator(.visible)
|
||||
} else {
|
||||
ShareAction(event: event, bookmarks: damus_state.bookmarks, show_share: $show_share_sheet)
|
||||
if let note_id = bech32_note_id(event.id) {
|
||||
if let url = URL(string: "https://damus.io/" + note_id) {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_share_sheet, onDismiss: { self.show_share_sheet = false }) {
|
||||
.sheet(isPresented: $show_share_sheet) {
|
||||
if let note_id = bech32_note_id(event.id) {
|
||||
if let url = URL(string: "https://damus.io/" + note_id) {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_repost_action, onDismiss: { self.show_repost_action = false }) {
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
RepostAction(damus_state: self.damus_state, event: event)
|
||||
.presentationDetents([.height(300)])
|
||||
.presentationDragIndicator(.visible)
|
||||
} else {
|
||||
RepostAction(damus_state: self.damus_state, event: event)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.update_stats)) { n in
|
||||
let target = n.object as! String
|
||||
guard target == self.event.id else { return }
|
||||
@@ -145,6 +136,18 @@ struct EventActionBar: View {
|
||||
}
|
||||
}
|
||||
|
||||
func send_boost() {
|
||||
guard let privkey = self.damus_state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event)
|
||||
|
||||
self.bar.our_boost = boost
|
||||
|
||||
notify(.boost, boost)
|
||||
}
|
||||
|
||||
func send_like() {
|
||||
guard let privkey = damus_state.keypair.privkey else {
|
||||
return
|
||||
@@ -246,6 +249,8 @@ struct EventActionBar_Previews: PreviewProvider {
|
||||
EventActionBar(damus_state: ds, event: ev, bar: extra_max_bar)
|
||||
|
||||
EventActionBar(damus_state: ds, event: ev, bar: mega_max_bar)
|
||||
|
||||
EventActionBar(damus_state: ds, event: ev, bar: zapbar, test_lnurl: "lnurl")
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ struct EventDetailBar: View {
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
if bar.likes > 0 && !state.settings.onlyzaps_mode {
|
||||
if bar.likes > 0 {
|
||||
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
|
||||
let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray)
|
||||
Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// RepostAction.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RepostAction: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Repost Note", comment: "Title text to indicate that the buttons below are meant to be used to repost a note to others.")
|
||||
.padding()
|
||||
.font(.system(size: 17, weight: .bold))
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .top, spacing: 100) {
|
||||
|
||||
ShareActionButton(img: "arrow.2.squarepath", text: NSLocalizedString("Repost", comment: "Button to repost a note")) {
|
||||
dismiss()
|
||||
|
||||
guard let privkey = self.damus_state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event)
|
||||
|
||||
damus_state.postbox.send(boost)
|
||||
}
|
||||
|
||||
ShareActionButton(img: "quote.opening", text: NSLocalizedString("Quote", comment: "Button to compose a quoted note")) {
|
||||
dismiss()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
notify(.compose, PostAction.quoting(self.event))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
BigButton(NSLocalizedString("Cancel", comment: "Button to cancel a repost.")) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RepostAction_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RepostAction(damus_state: test_damus_state(), event: test_event)
|
||||
}
|
||||
}
|
||||
@@ -12,22 +12,25 @@ struct ShareAction: View {
|
||||
let bookmarks: BookmarksManager
|
||||
@State private var isBookmarked: Bool = false
|
||||
|
||||
@Binding var show_share: Bool
|
||||
@Binding var show_share_sheet: Bool
|
||||
@Binding var show_share_action: Bool
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
init(event: NostrEvent, bookmarks: BookmarksManager, show_share: Binding<Bool>) {
|
||||
init(event: NostrEvent, bookmarks: BookmarksManager, show_share_sheet: Binding<Bool>, show_share_action: Binding<Bool>) {
|
||||
let bookmarked = bookmarks.isBookmarked(event)
|
||||
self._isBookmarked = State(initialValue: bookmarked)
|
||||
|
||||
self.bookmarks = bookmarks
|
||||
self.event = event
|
||||
self._show_share = show_share
|
||||
self._show_share_sheet = show_share_sheet
|
||||
self._show_share_action = show_share_action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
let col = colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white
|
||||
|
||||
VStack {
|
||||
Text("Share Note", comment: "Title text to indicate that the buttons below are meant to be used to share a note with others.")
|
||||
.padding()
|
||||
@@ -37,28 +40,28 @@ struct ShareAction: View {
|
||||
|
||||
HStack(alignment: .top, spacing: 25) {
|
||||
|
||||
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note")) {
|
||||
dismiss()
|
||||
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note"), col: col) {
|
||||
show_share_action = false
|
||||
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
|
||||
}
|
||||
|
||||
let bookmarkImg = isBookmarked ? "bookmark.slash" : "bookmark"
|
||||
let bookmarkTxt = isBookmarked ? NSLocalizedString("Remove Bookmark", comment: "Button text to remove bookmark from a note.") : NSLocalizedString("Add Bookmark", comment: "Button text to add bookmark to a note.")
|
||||
let boomarkCol = isBookmarked ? Color(.red) : nil
|
||||
let boomarkCol = isBookmarked ? Color(.red) : col
|
||||
ShareActionButton(img: bookmarkImg, text: bookmarkTxt, col: boomarkCol) {
|
||||
dismiss()
|
||||
show_share_action = false
|
||||
self.bookmarks.updateBookmark(event)
|
||||
isBookmarked = self.bookmarks.isBookmarked(event)
|
||||
}
|
||||
|
||||
ShareActionButton(img: "globe", text: NSLocalizedString("Broadcast", comment: "Button to broadcast note to all your relays")) {
|
||||
dismiss()
|
||||
ShareActionButton(img: "globe", text: NSLocalizedString("Broadcast", comment: "Button to broadcast note to all your relays"), col: col) {
|
||||
show_share_action = false
|
||||
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||
}
|
||||
|
||||
ShareActionButton(img: "square.and.arrow.up", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet")) {
|
||||
show_share = true
|
||||
dismiss()
|
||||
ShareActionButton(img: "square.and.arrow.up", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet"), col: col) {
|
||||
show_share_action = false
|
||||
show_share_sheet = true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -66,11 +69,42 @@ struct ShareAction: View {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
BigButton(NSLocalizedString("Cancel", comment: "Button to cancel a repost.")) {
|
||||
dismiss()
|
||||
|
||||
Button(action: {
|
||||
show_share_action = false
|
||||
}) {
|
||||
Text(NSLocalizedString("Cancel", comment: "Button to cancel a repost."))
|
||||
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
|
||||
.foregroundColor(colorScheme == .light ? DamusColors.black : DamusColors.white)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.stroke(colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white, lineWidth: 1)
|
||||
}
|
||||
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ShareActionButton(img: String, text: String, col: Color, action: @escaping () -> ()) -> some View {
|
||||
Button(action: action) {
|
||||
VStack() {
|
||||
Image(systemName: img)
|
||||
.foregroundColor(col)
|
||||
.font(.system(size: 23, weight: .bold))
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(col, lineWidth: 1)
|
||||
.frame(width: 55.0, height: 55.0)
|
||||
}
|
||||
.frame(height: 25)
|
||||
Text(verbatim: text)
|
||||
.foregroundColor(col)
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.top)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// ShareActionButton.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ShareActionButton: View {
|
||||
let img: String
|
||||
let text: String
|
||||
let color: Color?
|
||||
let action: () -> ()
|
||||
|
||||
init(img: String, text: String, col: Color?, action: @escaping () -> ()) {
|
||||
self.img = img
|
||||
self.text = text
|
||||
self.color = col
|
||||
self.action = action
|
||||
}
|
||||
|
||||
init(img: String, text: String, action: @escaping () -> ()) {
|
||||
self.img = img
|
||||
self.text = text
|
||||
self.action = action
|
||||
self.color = nil
|
||||
}
|
||||
|
||||
var col: Color {
|
||||
colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white
|
||||
}
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
VStack() {
|
||||
Image(systemName: img)
|
||||
.foregroundColor(col)
|
||||
.font(.system(size: 23, weight: .bold))
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(col, lineWidth: 1)
|
||||
.frame(width: 55.0, height: 55.0)
|
||||
}
|
||||
.frame(height: 25)
|
||||
Text(verbatim: text)
|
||||
.foregroundColor(col)
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.top)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ShareActionButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ShareActionButton(img: "figure.flexibility", text: "Stretch", action: {})
|
||||
}
|
||||
}
|
||||
@@ -62,8 +62,13 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
|
||||
|
||||
guard let url = mediaUploader.getMediaURL(from: data) else {
|
||||
|
||||
guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else {
|
||||
print("Upload failed getting response string")
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
guard let url = mediaUploader.getMediaURL(from: responseString, mediaIsImage: mediaToUpload.is_image) else {
|
||||
print("Upload failed getting media url")
|
||||
return .failed(nil)
|
||||
}
|
||||
@@ -84,22 +89,10 @@ extension NSMutableData {
|
||||
}
|
||||
}
|
||||
|
||||
enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
enum MediaUploader: String, CaseIterable, Identifiable {
|
||||
var id: String { self.rawValue }
|
||||
case nostrBuild
|
||||
case nostrImg
|
||||
|
||||
init?(from string: String) {
|
||||
guard let mu = MediaUploader(rawValue: string) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self = mu
|
||||
}
|
||||
|
||||
func to_string() -> String {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
var nameParam: String {
|
||||
switch self {
|
||||
@@ -139,27 +132,28 @@ enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
var postAPI: String {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return "https://nostr.build/api/upload/ios.php"
|
||||
return "https://nostr.build/upload.php"
|
||||
case .nostrImg:
|
||||
return "https://nostrimg.com/api/upload"
|
||||
}
|
||||
}
|
||||
|
||||
func getMediaURL(from data: Data) -> String? {
|
||||
func getMediaURL(from responseString: String, mediaIsImage: Bool) -> String? {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
do {
|
||||
return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? String
|
||||
} catch {
|
||||
print("Failed JSONSerialization")
|
||||
guard let startIndex = responseString.range(of: "nostr.build_")?.lowerBound else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let stringContainingName = responseString[startIndex..<responseString.endIndex]
|
||||
guard let endIndex = stringContainingName.range(of: "<")?.lowerBound else {
|
||||
return nil
|
||||
}
|
||||
let nostrBuildImageName = responseString[startIndex..<endIndex]
|
||||
let nostrBuildURL = mediaIsImage ? "https://nostr.build/i/\(nostrBuildImageName)" : "https://nostr.build/av/\(nostrBuildImageName)"
|
||||
return nostrBuildURL
|
||||
|
||||
case .nostrImg:
|
||||
guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else {
|
||||
print("Upload failed getting response string")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let startIndex = responseString.range(of: "https://i.nostrimg.com/")?.lowerBound else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct InnerBannerImageView: View {
|
||||
let disable_animation: Bool
|
||||
|
||||
let url: URL?
|
||||
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
|
||||
|
||||
@@ -19,7 +19,7 @@ struct InnerBannerImageView: View {
|
||||
|
||||
if (url != nil) {
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.banner, disable_animation: disable_animation)
|
||||
.imageContext(.banner)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
@@ -35,21 +35,19 @@ struct InnerBannerImageView: View {
|
||||
}
|
||||
|
||||
struct BannerImageView: View {
|
||||
let disable_animation: Bool
|
||||
let pubkey: String
|
||||
let profiles: Profiles
|
||||
|
||||
@State var banner: String?
|
||||
|
||||
init (pubkey: String, profiles: Profiles, disable_animation: Bool, banner: String? = nil) {
|
||||
init (pubkey: String, profiles: Profiles, banner: String? = nil) {
|
||||
self.pubkey = pubkey
|
||||
self.profiles = profiles
|
||||
self._banner = State(initialValue: banner)
|
||||
self.disable_animation = disable_animation
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
InnerBannerImageView(disable_animation: disable_animation, url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles))
|
||||
InnerBannerImageView(url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles))
|
||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||
let updated = notif.object as! ProfileUpdate
|
||||
|
||||
@@ -78,9 +76,7 @@ struct BannerImageView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BannerImageView(
|
||||
pubkey: pubkey,
|
||||
profiles: make_preview_profiles(pubkey),
|
||||
disable_animation: false
|
||||
)
|
||||
profiles: make_preview_profiles(pubkey))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ struct BookmarksView: View {
|
||||
let state: DamusState
|
||||
private let noneFilter: (NostrEvent) -> Bool = { _ in true }
|
||||
private let bookmarksTitle = NSLocalizedString("Bookmarks", comment: "Title of bookmarks view")
|
||||
@State private var clearAllAlert: Bool = false
|
||||
|
||||
@ObservedObject var manager: BookmarksManager
|
||||
|
||||
@@ -46,17 +45,10 @@ struct BookmarksView: View {
|
||||
.toolbar {
|
||||
if !bookmarks.isEmpty {
|
||||
Button(NSLocalizedString("Clear All", comment: "Button for clearing bookmarks data.")) {
|
||||
clearAllAlert = true
|
||||
manager.clearAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Are you sure you want to delete all of your bookmarks?", comment: "Alert for deleting all of the bookmarks."), isPresented: $clearAllAlert) {
|
||||
Button(NSLocalizedString("Cancel", comment: "Cancel deleting bookmarks."), role: .cancel) {
|
||||
}
|
||||
Button(NSLocalizedString("Continue", comment: "Continue with bookmarks.")) {
|
||||
manager.clearAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// FriendsButton.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FriendsButton: View {
|
||||
@Binding var filter: FriendFilter
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
switch self.filter {
|
||||
case .all:
|
||||
self.filter = .friends
|
||||
case .friends:
|
||||
self.filter = .all
|
||||
}
|
||||
}) {
|
||||
if filter == .friends {
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image(systemName: "person.2.fill")
|
||||
.resizable()
|
||||
).frame(width: 30, height: 20)
|
||||
} else {
|
||||
Image(systemName: "person.2.fill")
|
||||
.resizable()
|
||||
.frame(width: 30, height: 20)
|
||||
.foregroundColor(DamusColors.adaptableGrey)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
struct FriendsButton_Previews: PreviewProvider {
|
||||
@State static var enabled: FriendFilter = .all
|
||||
|
||||
static var previews: some View {
|
||||
FriendsButton(filter: $enabled)
|
||||
}
|
||||
}
|
||||
@@ -71,15 +71,11 @@ struct ChatView: View {
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var disable_animation: Bool {
|
||||
self.damus_state.settings.disable_animation
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack {
|
||||
if is_active || just_started {
|
||||
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, profiles: damus_state.profiles, disable_animation: disable_animation)
|
||||
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, profiles: damus_state.profiles)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -45,7 +45,7 @@ struct ConfigView: View {
|
||||
}
|
||||
|
||||
NavigationLink(destination: NotificationSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "bell.fill", color: .blue)
|
||||
IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue)
|
||||
}
|
||||
|
||||
NavigationLink(destination: ZapSettingsView(pubkey: state.pubkey, settings: settings)) {
|
||||
@@ -144,15 +144,14 @@ struct ConfigView_Previews: PreviewProvider {
|
||||
|
||||
|
||||
func handle_string_amount(new_value: String) -> Int? {
|
||||
let filtered = new_value.filter {
|
||||
$0.isNumber
|
||||
}
|
||||
let digits = Set("0123456789")
|
||||
let filtered = new_value.filter { digits.contains($0) }
|
||||
|
||||
if filtered == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let amt = NumberFormatter().number(from: filtered) as? Int else {
|
||||
guard let amt = Int(filtered) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,10 @@ import SwiftUI
|
||||
|
||||
struct DMChatView: View {
|
||||
let damus_state: DamusState
|
||||
@ObservedObject var dms: DirectMessageModel
|
||||
let pubkey: String
|
||||
@EnvironmentObject var dms: DirectMessageModel
|
||||
@State var showPrivateKeyWarning: Bool = false
|
||||
|
||||
var pubkey: String {
|
||||
dms.pubkey
|
||||
}
|
||||
|
||||
|
||||
var Messages: some View {
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView {
|
||||
@@ -43,7 +40,7 @@ struct DMChatView: View {
|
||||
let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey)
|
||||
return NavigationLink(destination: profile_page) {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: true)
|
||||
}
|
||||
@@ -180,9 +177,10 @@ struct DMChatView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: [])
|
||||
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey", pubkey: "the_pk")
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey")
|
||||
|
||||
DMChatView(damus_state: test_damus_state(), dms: model)
|
||||
DMChatView(damus_state: test_damus_state(), pubkey: "pubkey")
|
||||
.environmentObject(model)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,21 @@ struct DirectMessagesView: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
@State var dm_type: DMType = .friend
|
||||
@ObservedObject var model: DirectMessagesModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
@State var open_dm: Bool = false
|
||||
@State var pubkey: String = ""
|
||||
@EnvironmentObject var model: DirectMessagesModel
|
||||
@State var active_model: DirectMessageModel
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self._active_model = State(initialValue: DirectMessageModel(our_pubkey: damus_state.pubkey))
|
||||
}
|
||||
|
||||
func MainContent(requests: Bool) -> some View {
|
||||
ScrollView {
|
||||
let chat = DMChatView(damus_state: damus_state, dms: model.active_model)
|
||||
NavigationLink(destination: chat, isActive: $model.open_dm) {
|
||||
let chat = DMChatView(damus_state: damus_state, pubkey: pubkey)
|
||||
.environmentObject(active_model)
|
||||
NavigationLink(destination: chat, isActive: $open_dm) {
|
||||
EmptyView()
|
||||
}
|
||||
LazyVStack(spacing: 0) {
|
||||
@@ -30,9 +38,12 @@ struct DirectMessagesView: View {
|
||||
EmptyTimelineView()
|
||||
} else {
|
||||
let dms = requests ? model.message_requests : model.friend_dms
|
||||
ForEach(dms, id: \.pubkey) { dm in
|
||||
MaybeEvent(dm)
|
||||
ForEach(dms, id: \.0) { tup in
|
||||
MaybeEvent(tup)
|
||||
.padding(.top, 10)
|
||||
|
||||
Divider()
|
||||
.padding([.top], 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,17 +59,15 @@ struct DirectMessagesView: View {
|
||||
return [.truncate_content, .no_action_bar, .no_translate]
|
||||
}
|
||||
|
||||
func MaybeEvent(_ model: DirectMessageModel) -> some View {
|
||||
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View {
|
||||
Group {
|
||||
let ok = damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: model.pubkey)
|
||||
if ok, let ev = model.events.last {
|
||||
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
|
||||
if let ev = tup.1.events.last {
|
||||
EventView(damus: damus_state, event: ev, pubkey: tup.0, options: options)
|
||||
.onTapGesture {
|
||||
self.model.open_dm_by_model(model)
|
||||
pubkey = tup.0
|
||||
active_model = tup.1
|
||||
open_dm = true
|
||||
}
|
||||
|
||||
Divider()
|
||||
.padding([.top], 10)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -86,28 +95,10 @@ struct DirectMessagesView: View {
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if would_filter_non_friends_from_dms(contacts: damus_state.contacts, dms: self.model.dms) {
|
||||
|
||||
FriendsButton(filter: $settings.friend_filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message."))
|
||||
}
|
||||
}
|
||||
|
||||
func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool {
|
||||
for dm in dms {
|
||||
if !FriendFilter.friends.filter(contacts: contacts, pubkey: dm.pubkey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
struct DirectMessagesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ev = NostrEvent(content: "encrypted stuff",
|
||||
@@ -115,6 +106,8 @@ struct DirectMessagesView_Previews: PreviewProvider {
|
||||
kind: 4,
|
||||
tags: [])
|
||||
let ds = test_damus_state()
|
||||
DirectMessagesView(damus_state: ds, model: ds.dms, settings: ds.settings)
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: ds.pubkey)
|
||||
DirectMessagesView(damus_state: ds)
|
||||
.environmentObject(model)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ struct EditMetadataView: View {
|
||||
@State var name: String
|
||||
@State var ln: String
|
||||
@State var website: String
|
||||
let profile: Profile?
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@@ -74,7 +73,6 @@ struct EditMetadataView: View {
|
||||
init (damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
let data = damus_state.profiles.lookup(id: damus_state.pubkey)
|
||||
self.profile = data
|
||||
|
||||
_name = State(initialValue: data?.name ?? "")
|
||||
_display_name = State(initialValue: data?.display_name ?? "")
|
||||
@@ -87,31 +85,27 @@ struct EditMetadataView: View {
|
||||
}
|
||||
|
||||
func imageBorderColor() -> Color {
|
||||
colorScheme == .light ? DamusColors.white : DamusColors.black
|
||||
}
|
||||
|
||||
func to_profile() -> Profile {
|
||||
let profile = self.profile ?? Profile()
|
||||
|
||||
profile.name = name
|
||||
profile.display_name = display_name
|
||||
profile.about = about
|
||||
profile.website = website
|
||||
profile.nip05 = nip05.isEmpty ? nil : nip05
|
||||
profile.picture = picture.isEmpty ? nil : picture
|
||||
profile.banner = banner.isEmpty ? nil : banner
|
||||
profile.lud06 = ln.contains("@") ? nil : ln
|
||||
profile.lud16 = ln.contains("@") ? ln : nil
|
||||
|
||||
return profile
|
||||
}
|
||||
colorScheme == .light ? DamusColors.white : DamusColors.black
|
||||
}
|
||||
|
||||
func save() {
|
||||
let profile = to_profile()
|
||||
guard let metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: profile) else {
|
||||
return
|
||||
let metadata = NostrMetadata(
|
||||
display_name: display_name,
|
||||
name: name,
|
||||
about: about,
|
||||
website: website,
|
||||
nip05: nip05.isEmpty ? nil : nip05,
|
||||
picture: picture.isEmpty ? nil : picture,
|
||||
banner: banner.isEmpty ? nil : banner,
|
||||
lud06: ln.contains("@") ? nil : ln,
|
||||
lud16: ln.contains("@") ? ln : nil
|
||||
);
|
||||
|
||||
let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata)
|
||||
|
||||
if let metadata_ev = m_metadata_ev {
|
||||
damus_state.postbox.send(metadata_ev)
|
||||
}
|
||||
damus_state.postbox.send(metadata_ev)
|
||||
}
|
||||
|
||||
func is_ln_valid(ln: String) -> Bool {
|
||||
@@ -125,7 +119,7 @@ struct EditMetadataView: View {
|
||||
var TopSection: some View {
|
||||
ZStack(alignment: .top) {
|
||||
GeometryReader { geo in
|
||||
BannerImageView(pubkey: damus_state.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
BannerImageView(pubkey: damus_state.pubkey, profiles: damus_state.profiles)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geo.size.width, height: BANNER_HEIGHT)
|
||||
.clipped()
|
||||
@@ -32,7 +32,7 @@ struct EventView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
if event.known_kind == .boost {
|
||||
if let inner_ev = event.get_inner_event(cache: damus.events) {
|
||||
if let inner_ev = event.inner_event {
|
||||
RepostedEvent(damus: damus, event: event, inner_ev: inner_ev, options: options)
|
||||
} else {
|
||||
EmptyView()
|
||||
@@ -69,6 +69,19 @@ func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
|
||||
return false
|
||||
}
|
||||
|
||||
func event_validity_color(_ validation: ValidationResult) -> some View {
|
||||
Group {
|
||||
switch validation {
|
||||
case .ok:
|
||||
EmptyView()
|
||||
case .bad_id:
|
||||
Color.orange.opacity(0.4)
|
||||
case .bad_sig:
|
||||
Color.red.opacity(0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func pubkey_context_menu(bech32_pubkey: String) -> some View {
|
||||
return self.contextMenu {
|
||||
|
||||
@@ -48,6 +48,10 @@ struct BuilderEventView: View {
|
||||
return
|
||||
}
|
||||
|
||||
guard nostr_event.known_kind == .text else {
|
||||
return
|
||||
}
|
||||
|
||||
if event != nil {
|
||||
return
|
||||
}
|
||||
@@ -70,12 +74,12 @@ struct BuilderEventView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let event {
|
||||
let ev = event.get_inner_event(cache: damus.events) ?? event
|
||||
let ev = event.inner_event ?? event
|
||||
let thread = ThreadModel(event: ev, damus_state: damus)
|
||||
let dest = ThreadView(state: damus, thread: thread)
|
||||
NavigationLink(destination: dest) {
|
||||
EventView(damus: damus, event: event, options: .embedded)
|
||||
.padding([.top, .bottom], 8)
|
||||
EmbeddedEventView(damus_state: damus, event: event)
|
||||
.padding(8)
|
||||
}.buttonStyle(.plain)
|
||||
} else {
|
||||
ProgressView().padding()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// EmbeddedEventView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-01-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EmbeddedEventView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
|
||||
var pubkey: String {
|
||||
event.pubkey
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
HStack {
|
||||
EventProfile(damus_state: damus_state, pubkey: pubkey, profile: profile, size: .small)
|
||||
|
||||
Spacer()
|
||||
|
||||
EventMenuContext(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)
|
||||
.padding([.bottom], 4)
|
||||
|
||||
}
|
||||
.minimumScaleFactor(0.75)
|
||||
.lineLimit(1)
|
||||
|
||||
if event_is_reply(event, privkey: damus_state.keypair.privkey) {
|
||||
ReplyDescription(event: event, profiles: damus_state.profiles)
|
||||
}
|
||||
|
||||
EventBody(damus_state: damus_state, event: event, size: .small, options: [.truncate_content])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EmbeddedEventView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EmbeddedEventView(damus_state: test_damus_state(), event: test_event)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ struct MenuItems: View {
|
||||
let bookmarked = bookmarks.isBookmarked(event)
|
||||
self._isBookmarked = State(initialValue: bookmarked)
|
||||
|
||||
let muted_thread = muted_threads.isMutedThread(event, privkey: keypair.privkey)
|
||||
let muted_thread = muted_threads.isMutedThread(event)
|
||||
self._isMutedThread = State(initialValue: muted_thread)
|
||||
|
||||
self.bookmarks = bookmarks
|
||||
@@ -96,7 +96,7 @@ struct MenuItems: View {
|
||||
if event.known_kind != .dm {
|
||||
Button {
|
||||
self.muted_threads.updateMutedThread(event)
|
||||
let muted = self.muted_threads.isMutedThread(event, privkey: self.keypair.privkey)
|
||||
let muted = self.muted_threads.isMutedThread(event)
|
||||
isMutedThread = muted
|
||||
} label: {
|
||||
let imageName = isMutedThread ? "speaker" : "speaker.slash"
|
||||
|
||||
@@ -28,15 +28,11 @@ struct EventProfile: View {
|
||||
eventview_pfp_size(size)
|
||||
}
|
||||
|
||||
var disable_animation: Bool {
|
||||
damus_state.settings.disable_animation
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
VStack {
|
||||
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
|
||||
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
|
||||
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,15 @@ import SwiftUI
|
||||
struct MutedEventView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let scroller: ScrollViewProxy?
|
||||
|
||||
let selected: Bool
|
||||
@State var shown: Bool
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, selected: Bool) {
|
||||
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, selected: Bool) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.scroller = scroller
|
||||
self.selected = selected
|
||||
self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event))
|
||||
}
|
||||
@@ -31,9 +33,9 @@ struct MutedEventView: View {
|
||||
.foregroundColor(DamusColors.adaptableGrey)
|
||||
|
||||
HStack {
|
||||
Text("Post from a user you've muted", comment: "Text to indicate that what is being shown is a post from a user who has been muted.")
|
||||
Text("Post from a user you've blocked", comment: "Text to indicate that what is being shown is a post from a user who has been blocked.")
|
||||
Spacer()
|
||||
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a post from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a post from a user who has been muted.")) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -87,7 +89,7 @@ struct MutedEventView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
MutedEventView(damus_state: test_damus_state(), event: test_event, selected: false)
|
||||
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, selected: false)
|
||||
.frame(width: .infinity, height: 50)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EventViewOptions: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
let rawValue: UInt8
|
||||
static let no_action_bar = EventViewOptions(rawValue: 1 << 0)
|
||||
static let no_replying_to = EventViewOptions(rawValue: 1 << 1)
|
||||
static let no_images = EventViewOptions(rawValue: 1 << 2)
|
||||
@@ -17,20 +16,6 @@ struct EventViewOptions: OptionSet {
|
||||
static let truncate_content = EventViewOptions(rawValue: 1 << 4)
|
||||
static let pad_content = EventViewOptions(rawValue: 1 << 5)
|
||||
static let no_translate = EventViewOptions(rawValue: 1 << 6)
|
||||
static let small_pfp = EventViewOptions(rawValue: 1 << 7)
|
||||
static let nested = EventViewOptions(rawValue: 1 << 8)
|
||||
|
||||
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
|
||||
}
|
||||
|
||||
struct RelativeTime: View {
|
||||
@ObservedObject var time: RelativeTimeModel
|
||||
|
||||
var body: some View {
|
||||
Text(verbatim: "\(time.value)")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
struct TextEvent: View {
|
||||
@@ -38,15 +23,6 @@ struct TextEvent: View {
|
||||
let event: NostrEvent
|
||||
let pubkey: String
|
||||
let options: EventViewOptions
|
||||
let evdata: EventData
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: String, options: EventViewOptions) {
|
||||
self.damus = damus
|
||||
self.event = event
|
||||
self.pubkey = pubkey
|
||||
self.options = options
|
||||
self.evdata = damus.events.get_cache_data(event.id)
|
||||
}
|
||||
|
||||
var has_action_bar: Bool {
|
||||
!options.contains(.no_action_bar)
|
||||
@@ -61,20 +37,21 @@ struct TextEvent: View {
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(event_validity_color(event.validity))
|
||||
.id(event.id)
|
||||
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
||||
.padding([.bottom], 2)
|
||||
}
|
||||
|
||||
func Pfp(is_anon: Bool) -> some View {
|
||||
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey, size: options.contains(.small_pfp) ? eventview_pfp_size(.small) : PFP_SIZE )
|
||||
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey)
|
||||
}
|
||||
|
||||
func TopPart(is_anon: Bool) -> some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
ProfileName(is_anon: is_anon)
|
||||
TimeDot
|
||||
RelativeTime(time: self.evdata.relative_time)
|
||||
Time
|
||||
Spacer()
|
||||
ContextButton
|
||||
}
|
||||
@@ -106,7 +83,7 @@ struct TextEvent: View {
|
||||
|
||||
EvBody(options: self.options.union(.pad_content))
|
||||
|
||||
if let mention = get_mention() {
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
Mention(mention)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
@@ -125,6 +102,12 @@ struct TextEvent: View {
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
var Time: some View {
|
||||
Text(verbatim: "\(format_relative_time(event.created_at))")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
var ContextButton: some View {
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
|
||||
.padding([.bottom], 4)
|
||||
@@ -137,17 +120,7 @@ struct TextEvent: View {
|
||||
}
|
||||
|
||||
func EvBody(options: EventViewOptions) -> some View {
|
||||
let show_imgs = should_show_images(settings: damus.settings, contacts: damus.contacts, ev: event, our_pubkey: damus.pubkey)
|
||||
let artifacts = damus.events.get_cache_data(event.id).artifacts.artifacts ?? .just_content(event.get_content(damus.keypair.privkey))
|
||||
return NoteContentView(
|
||||
damus_state: damus,
|
||||
event: event,
|
||||
show_images: show_imgs,
|
||||
size: .normal,
|
||||
artifacts: artifacts,
|
||||
options: options
|
||||
)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
return EventBody(damus_state: damus, event: event, size: .normal, options: options)
|
||||
}
|
||||
|
||||
func Mention(_ mention: Mention) -> some View {
|
||||
@@ -163,14 +136,6 @@ struct TextEvent: View {
|
||||
return Rectangle().frame(height: 2).opacity(0)
|
||||
}
|
||||
|
||||
func get_mention() -> Mention? {
|
||||
if self.options.contains(.nested) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return first_eref_mention(ev: event, privkey: damus.keypair.privkey)
|
||||
}
|
||||
|
||||
var ThreadedStyle: some View {
|
||||
HStack(alignment: .top) {
|
||||
|
||||
@@ -185,10 +150,9 @@ struct TextEvent: View {
|
||||
TopPart(is_anon: is_anon)
|
||||
|
||||
ReplyPart
|
||||
|
||||
EvBody(options: self.options)
|
||||
|
||||
if let mention = get_mention() {
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
Mention(mention)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,14 @@ struct FollowUserView: View {
|
||||
}
|
||||
|
||||
HStack {
|
||||
UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
|
||||
UserView(damus_state: damus_state, pubkey: target.pubkey)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
navigating = true
|
||||
}
|
||||
|
||||
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,8 @@ struct ImagePicker: UIViewControllerRepresentable {
|
||||
@Environment(\.presentationMode)
|
||||
private var presentationMode
|
||||
|
||||
let uploader: MediaUploader
|
||||
let sourceType: UIImagePickerController.SourceType
|
||||
let pubkey: String
|
||||
@Binding var image_upload_confirm: Bool
|
||||
var imagesOnly: Bool = false
|
||||
let onImagePicked: (URL) -> Void
|
||||
let onVideoPicked: (URL) -> Void
|
||||
@@ -26,18 +24,15 @@ struct ImagePicker: UIViewControllerRepresentable {
|
||||
private let sourceType: UIImagePickerController.SourceType
|
||||
private let onImagePicked: (URL) -> Void
|
||||
private let onVideoPicked: (URL) -> Void
|
||||
@Binding var image_upload_confirm: Bool
|
||||
|
||||
init(presentationMode: Binding<PresentationMode>,
|
||||
sourceType: UIImagePickerController.SourceType,
|
||||
onImagePicked: @escaping (URL) -> Void,
|
||||
onVideoPicked: @escaping (URL) -> Void,
|
||||
image_upload_confirm: Binding<Bool>) {
|
||||
onVideoPicked: @escaping (URL) -> Void) {
|
||||
_presentationMode = presentationMode
|
||||
self.sourceType = sourceType
|
||||
self.onImagePicked = onImagePicked
|
||||
self.onVideoPicked = onVideoPicked
|
||||
self._image_upload_confirm = image_upload_confirm
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
@@ -56,9 +51,9 @@ struct ImagePicker: UIViewControllerRepresentable {
|
||||
onImagePicked(editedImageURL)
|
||||
}
|
||||
}
|
||||
image_upload_confirm = true
|
||||
presentationMode.dismiss()
|
||||
}
|
||||
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
presentationMode.dismiss()
|
||||
}
|
||||
@@ -103,14 +98,15 @@ struct ImagePicker: UIViewControllerRepresentable {
|
||||
onVideoPicked: { videoURL in
|
||||
// Handle the selected video URL
|
||||
onVideoPicked(videoURL)
|
||||
}, image_upload_confirm: $image_upload_confirm)
|
||||
})
|
||||
}
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = sourceType
|
||||
let mediaUploader = get_media_uploader(pubkey)
|
||||
picker.mediaTypes = ["public.image", "com.compuserve.gif"]
|
||||
if uploader.supportsVideo && !imagesOnly {
|
||||
if mediaUploader.supportsVideo && !imagesOnly {
|
||||
picker.mediaTypes.append("public.movie")
|
||||
}
|
||||
picker.delegate = context.coordinator
|
||||
|
||||
@@ -9,14 +9,14 @@ import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
|
||||
// lots of overlap between this and ImageContainerView
|
||||
struct ImageContainerView: View {
|
||||
|
||||
let url: URL?
|
||||
|
||||
@State private var image: UIImage?
|
||||
@State private var showShareSheet = false
|
||||
|
||||
let disable_animation: Bool
|
||||
|
||||
private struct ImageHandler: ImageModifier {
|
||||
@Binding var handler: UIImage?
|
||||
|
||||
@@ -29,7 +29,7 @@ struct ImageContainerView: View {
|
||||
var body: some View {
|
||||
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.note, disable_animation: disable_animation)
|
||||
.imageContext(.note)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
@@ -46,6 +46,6 @@ let test_image_url = URL(string: "https://jb55.com/red-me.jpg")!
|
||||
|
||||
struct ImageContainerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageContainerView(url: test_image_url, disable_animation: false)
|
||||
ImageContainerView(url: test_image_url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ struct ImageView: View {
|
||||
@State private var selectedIndex = 0
|
||||
@State var showMenu = true
|
||||
|
||||
let disable_animation: Bool
|
||||
|
||||
var tabViewIndicator: some View {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
@@ -39,7 +37,7 @@ struct ImageView: View {
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
ZoomableScrollView {
|
||||
ImageContainerView(url: urls[index], disable_animation: disable_animation)
|
||||
ImageContainerView(url: urls[index])
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.top, Theme.safeAreaInsets?.top)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
@@ -79,6 +77,6 @@ struct ImageView: View {
|
||||
|
||||
struct ImageView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")], disable_animation: false)
|
||||
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,12 @@ import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ProfileImageContainerView: View {
|
||||
|
||||
let url: URL?
|
||||
|
||||
@State private var image: UIImage?
|
||||
@State private var showShareSheet = false
|
||||
|
||||
let disable_animation: Bool
|
||||
|
||||
private struct ImageHandler: ImageModifier {
|
||||
@Binding var handler: UIImage?
|
||||
|
||||
@@ -27,7 +26,7 @@ struct ProfileImageContainerView: View {
|
||||
var body: some View {
|
||||
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.pfp, disable_animation: disable_animation)
|
||||
.imageContext(.pfp)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
@@ -62,9 +61,9 @@ struct NavDismissBarView: View {
|
||||
}
|
||||
|
||||
struct ProfilePicImageView: View {
|
||||
|
||||
let pubkey: String
|
||||
let profiles: Profiles
|
||||
let disable_animation: Bool
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@@ -74,7 +73,7 @@ struct ProfilePicImageView: View {
|
||||
.ignoresSafeArea()
|
||||
|
||||
ZoomableScrollView {
|
||||
ProfileImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles), disable_animation: disable_animation)
|
||||
ProfileImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.top, Theme.safeAreaInsets?.top)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
@@ -95,8 +94,6 @@ struct ProfileZoomView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProfilePicImageView(
|
||||
pubkey: pubkey,
|
||||
profiles: make_preview_profiles(pubkey),
|
||||
disable_animation: false
|
||||
)
|
||||
profiles: make_preview_profiles(pubkey))
|
||||
}
|
||||
}
|
||||
|
||||
+88
-101
@@ -36,7 +36,6 @@ struct LoginView: View {
|
||||
@State var key: String = ""
|
||||
@State var is_pubkey: Bool = false
|
||||
@State var error: String? = nil
|
||||
@State private var credential_handler = CredentialHandler()
|
||||
|
||||
func get_error(parsed_key: ParsedKey?) -> String? {
|
||||
if self.error != nil {
|
||||
@@ -44,12 +43,85 @@ struct LoginView: View {
|
||||
}
|
||||
|
||||
if !key.isEmpty && parsed_key == nil {
|
||||
return LoginError.invalid_key.errorDescription
|
||||
return "Invalid key"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func process_login(_ key: ParsedKey, is_pubkey: Bool) -> Bool {
|
||||
switch key {
|
||||
case .priv(let priv):
|
||||
do {
|
||||
try save_privkey(privkey: priv)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let pk = privkey_to_pubkey(privkey: priv) else {
|
||||
return false
|
||||
}
|
||||
save_pubkey(pubkey: pk)
|
||||
|
||||
case .pub(let pub):
|
||||
do {
|
||||
try clear_saved_privkey()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
save_pubkey(pubkey: pub)
|
||||
|
||||
case .nip05(let id):
|
||||
Task.init {
|
||||
guard let nip05 = await get_nip05_pubkey(id: id) else {
|
||||
self.error = "Could not fetch pubkey"
|
||||
return
|
||||
}
|
||||
|
||||
// this is a weird way to login anyways
|
||||
/*
|
||||
var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey)
|
||||
for relay in nip05.relays {
|
||||
if !(bootstrap_relays.contains { $0 == relay }) {
|
||||
bootstrap_relays.append(relay)
|
||||
}
|
||||
}
|
||||
*/
|
||||
save_pubkey(pubkey: nip05.pubkey)
|
||||
|
||||
notify(.login, ())
|
||||
}
|
||||
|
||||
|
||||
case .hex(let hexstr):
|
||||
if is_pubkey {
|
||||
do {
|
||||
try clear_saved_privkey()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
save_pubkey(pubkey: hexstr)
|
||||
} else {
|
||||
do {
|
||||
try save_privkey(privkey: hexstr)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let pk = privkey_to_pubkey(privkey: hexstr) else {
|
||||
return false
|
||||
}
|
||||
save_pubkey(pubkey: pk)
|
||||
}
|
||||
}
|
||||
|
||||
notify(.login, ())
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
DamusGradient()
|
||||
@@ -86,26 +158,17 @@ struct LoginView: View {
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if let p = parsed {
|
||||
DamusWhiteButton(NSLocalizedString("Login", comment: "Button to log into account.")) {
|
||||
Task {
|
||||
do {
|
||||
try await process_login(p, is_pubkey: is_pubkey)
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
if !process_login(p, is_pubkey: self.is_pubkey) {
|
||||
self.error = NSLocalizedString("Invalid key", comment: "Error message indicating that an invalid account key was entered for login.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
credential_handler.check_credentials()
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
}
|
||||
@@ -149,71 +212,6 @@ func parse_key(_ thekey: String) -> ParsedKey? {
|
||||
return nil
|
||||
}
|
||||
|
||||
enum LoginError: LocalizedError {
|
||||
case invalid_key
|
||||
case nip05_failed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalid_key:
|
||||
return NSLocalizedString("Invalid key", comment: "Error message indicating that an invalid account key was entered for login.")
|
||||
case .nip05_failed:
|
||||
return "Could not fetch pubkey"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws {
|
||||
switch key {
|
||||
case .priv(let priv):
|
||||
try handle_privkey(priv)
|
||||
case .pub(let pub):
|
||||
try clear_saved_privkey()
|
||||
save_pubkey(pubkey: pub)
|
||||
|
||||
case .nip05(let id):
|
||||
guard let nip05 = await get_nip05_pubkey(id: id) else {
|
||||
throw LoginError.nip05_failed
|
||||
}
|
||||
|
||||
// this is a weird way to login anyways
|
||||
/*
|
||||
var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey)
|
||||
for relay in nip05.relays {
|
||||
if !(bootstrap_relays.contains { $0 == relay }) {
|
||||
bootstrap_relays.append(relay)
|
||||
}
|
||||
}
|
||||
*/
|
||||
save_pubkey(pubkey: nip05.pubkey)
|
||||
|
||||
case .hex(let hexstr):
|
||||
if is_pubkey {
|
||||
try clear_saved_privkey()
|
||||
save_pubkey(pubkey: hexstr)
|
||||
} else {
|
||||
try handle_privkey(hexstr)
|
||||
}
|
||||
}
|
||||
|
||||
func handle_privkey(_ privkey: String) throws {
|
||||
try save_privkey(privkey: privkey)
|
||||
|
||||
guard let pk = privkey_to_pubkey(privkey: privkey) else {
|
||||
throw LoginError.invalid_key
|
||||
}
|
||||
|
||||
if let pub = bech32_pubkey(pk), let priv = bech32_privkey(privkey) {
|
||||
CredentialHandler().save_credential(pubkey: pub, privkey: priv)
|
||||
}
|
||||
save_pubkey(pubkey: pk)
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
notify(.login, ())
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP05Result: Decodable {
|
||||
let names: Dictionary<String, String>
|
||||
let relays: Dictionary<String, [String]>?
|
||||
@@ -270,29 +268,18 @@ struct KeyInput: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
TextField("", text: key)
|
||||
.placeholder(when: key.wrappedValue.isEmpty) {
|
||||
Text(title).foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.padding()
|
||||
.padding(.leading, 20)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
|
||||
}
|
||||
.autocapitalization(.none)
|
||||
.foregroundColor(.white)
|
||||
.font(.body.monospaced())
|
||||
.textContentType(.password)
|
||||
|
||||
Label("", systemImage: "doc.on.clipboard")
|
||||
.padding(.leading, 10)
|
||||
.onTapGesture {
|
||||
if let pastedkey = UIPasteboard.general.string {
|
||||
self.key.wrappedValue = pastedkey
|
||||
}
|
||||
TextField("", text: key)
|
||||
.placeholder(when: key.wrappedValue.isEmpty) {
|
||||
Text(title).foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
|
||||
}
|
||||
.autocapitalization(.none)
|
||||
.foregroundColor(.white)
|
||||
.font(.body.monospaced())
|
||||
.textContentType(.password)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func show_indicator(timeline: Timeline, current: NewEventsBits, indicator_settin
|
||||
struct TabButton: View {
|
||||
let timeline: Timeline
|
||||
let img: String
|
||||
@Binding var selected: Timeline
|
||||
@Binding var selected: Timeline?
|
||||
@Binding var new_events: NewEventsBits
|
||||
|
||||
let settings: UserSettingsStore
|
||||
@@ -75,7 +75,7 @@ struct TabButton: View {
|
||||
|
||||
struct TabBar: View {
|
||||
@Binding var new_events: NewEventsBits
|
||||
@Binding var selected: Timeline
|
||||
@Binding var selected: Timeline?
|
||||
|
||||
let settings: UserSettingsStore
|
||||
let action: (Timeline) -> ()
|
||||
|
||||
@@ -29,7 +29,7 @@ struct MutelistView: View {
|
||||
damus_state.postbox.send(new_ev)
|
||||
users = get_mutelist_users(new_ev)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), systemImage: "trash")
|
||||
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ struct MutelistView: View {
|
||||
|
||||
var body: some View {
|
||||
List(users, id: \.self) { pubkey in
|
||||
UserViewRow(damus_state: damus_state, pubkey: pubkey)
|
||||
UserView(damus_state: damus_state, pubkey: pubkey)
|
||||
.id(pubkey)
|
||||
.swipeActions {
|
||||
RemoveAction(pubkey: pubkey)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user