Compare commits

..

1 Commits

Author SHA1 Message Date
tyiu 84183d2b83 Add thread muting
Changelog-Added: Add thread muting
2023-04-13 11:42:56 +02:00
175 changed files with 2331 additions and 4693 deletions
-105
View File
@@ -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
View File
@@ -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")
]
+1 -6
View File
@@ -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
View File
@@ -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++;
+1 -1
View File
@@ -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
View File
@@ -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",
+2 -1
View File
@@ -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)
}
}
+51 -91
View File
@@ -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")!])
}
}
+4 -6
View File
@@ -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)
}
}
+2 -3
View File
@@ -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)
}
}
+2 -2
View File
@@ -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)
}
}
}
+85 -59
View File
@@ -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"
}
}
+2 -24
View File
@@ -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()
}
}
}
+6 -7
View File
@@ -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
View File
@@ -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
-12
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
}
}
+2 -11
View File
@@ -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: ""))
}
}
+1 -13
View File
@@ -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 {
+2 -6
View File
@@ -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
}
}
+9 -32
View File
@@ -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
}
}
+2 -18
View File
@@ -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] = [:]
}
+1 -1
View File
@@ -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):
+2 -2
View File
@@ -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
View File
@@ -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)
-9
View File
@@ -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 {
+4 -12
View File
@@ -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)
}
+14
View File
@@ -0,0 +1,14 @@
//
// LocalUserConfig.swift
// damus
//
// Created by William Casarin on 2022-06-15.
//
import Foundation
struct LocalUserConfig: Codable {
let relays: [RelayDescriptor]
}
+1 -1
View File
@@ -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)
+8 -8
View File
@@ -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)
}
}
+4 -21
View File
@@ -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
+21 -56
View File
@@ -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)
}
}
+7 -7
View File
@@ -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)
}
}
+1 -4
View File
@@ -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
}
+9 -12
View File
@@ -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)
}
}
+14 -19
View File
@@ -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
}
}
}
+4 -9
View File
@@ -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)
}
+1 -16
View File
@@ -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."))
}
}
+272 -180
View File
@@ -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)"
}
+1 -12
View File
@@ -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
+8 -8
View File
@@ -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()
}
}
-17
View File
@@ -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 {
+22 -61
View File
@@ -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
}
+3 -3
View File
@@ -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 {
+25
View File
@@ -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)
}
+6 -4
View File
@@ -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
}
+58 -87
View File
@@ -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
View File
@@ -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)
}
-87
View File
@@ -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)
}
}
-146
View File
@@ -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]
}
}
-145
View File
@@ -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 }
}
-19
View File
@@ -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.
-45
View File
@@ -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.
-48
View File
@@ -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
View File
@@ -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)
}
}
}
+2 -9
View File
@@ -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
}
-2
View File
@@ -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 {
-208
View File
@@ -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
}
}
}
}
}
}
-47
View File
@@ -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
}
+3 -9
View File
@@ -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 {
-48
View File
@@ -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)
}
}
+3 -1
View File
@@ -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
+9 -41
View File
@@ -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 = [:]
+1 -1
View File
@@ -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) }
}
-13
View File
@@ -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
}
-25
View File
@@ -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 {
-4
View File
@@ -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 {
-41
View File
@@ -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: {})
}
}
+56 -51
View File
@@ -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)
}
+1 -1
View File
@@ -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'.")
-62
View File
@@ -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)
}
}
+49 -15
View File
@@ -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: {})
}
}
+20 -26
View File
@@ -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
}
+5 -9
View File
@@ -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))
}
}
+1 -9
View File
@@ -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()
}
}
}
}
-44
View File
@@ -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)
}
}
+1 -5
View File
@@ -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()
+4 -5
View File
@@ -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
}
+7 -9
View File
@@ -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)
}
}
+26 -33
View File
@@ -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()
+14 -1
View File
@@ -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 {
+7 -3
View File
@@ -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()
}
}
+2 -2
View File
@@ -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"
+1 -5
View File
@@ -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)
}
}
+6 -4
View File
@@ -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)
}
}
+13 -49
View File
@@ -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)
}
+5 -2
View File
@@ -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()
}
}
+6 -10
View File
@@ -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
+4 -4
View File
@@ -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)
}
}
+2 -4
View File
@@ -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")])
}
}
+5 -8
View File
@@ -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
View File
@@ -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)
}
}
+2 -2
View File
@@ -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) -> ()
+2 -2
View File
@@ -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