Compare commits
58 Commits
localizati
...
preferred-
| Author | SHA1 | Date | |
|---|---|---|---|
|
8e852ed742
|
|||
|
7b4fc79030
|
|||
|
|
7a4af31859 | ||
|
|
e106be1412 | ||
|
|
282bf80daa | ||
|
|
bcb861a61b | ||
|
|
bb0ad18913 | ||
|
|
81830c7540 | ||
|
|
68128b5ff1 | ||
|
|
aebeb26bc6 | ||
|
|
79cf3db279 | ||
|
|
dcae0d2cc7 | ||
|
|
2b12dc5920 | ||
|
|
51930e7a12 | ||
|
|
b04e09d2e0 | ||
| b6c4213515 | |||
|
|
8230c6eded | ||
|
|
e79590f795 | ||
|
|
79bced1246 | ||
|
|
896f4b55e3 | ||
|
|
52e65f9429 | ||
|
|
a22cc532e2 | ||
|
|
823227920c | ||
|
|
3e2bbce25e | ||
|
|
e05b2d9ecf | ||
|
|
d7b31a1cd8 | ||
|
|
70f01c0880 | ||
|
|
2cf5f21f78 | ||
|
|
96e8f8b6b2 | ||
|
|
370cfd1b08 | ||
|
|
046af15734 | ||
|
|
9e4ab2d54c | ||
|
|
7cf12e2e0d | ||
|
|
a63a81b387 | ||
|
|
d994cd13dc | ||
|
|
95e985cfce | ||
|
|
3a69de9274 | ||
|
|
64f5acf98c | ||
|
|
5167ab264d | ||
|
|
e02895b29f | ||
|
|
0009d11025 | ||
|
|
afc317bb52 | ||
|
|
629212ea23 | ||
|
|
ec1252200f | ||
|
|
54ea1ab803 | ||
|
|
4cf8097de4 | ||
|
|
2c7384b0a9 | ||
|
|
19e312a8fb | ||
|
|
3986308638 | ||
| fa7740948b | |||
| 892a1420f3 | |||
| ee4cbf7363 | |||
| a1b1ce949b | |||
| 902e8c3950 | |||
| b776788b38 | |||
|
|
78066773f4 | ||
|
|
0bac284eee | ||
|
|
07c95d1003 |
@@ -406,9 +406,10 @@
|
||||
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
|
||||
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */; };
|
||||
5C7389B12B6EFA7100781E0A /* ProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B02B6EFA7100781E0A /* ProxyView.swift */; };
|
||||
5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B62B9E692E00781E0A /* MutinyButton.swift */; };
|
||||
5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */; };
|
||||
5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */; };
|
||||
5CB017212D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; };
|
||||
5CB017222D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; };
|
||||
5CB017232D2D985E00A9ED05 /* CoinosButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB017202D2D985800A9ED05 /* CoinosButton.swift */; };
|
||||
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */; };
|
||||
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */; };
|
||||
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */; };
|
||||
@@ -535,7 +536,6 @@
|
||||
82D6FB0F2CD99F7900C925F4 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; };
|
||||
82D6FB102CD99F7900C925F4 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; };
|
||||
82D6FB112CD99F7900C925F4 /* DamusLightGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */; };
|
||||
82D6FB122CD99F7900C925F4 /* MutinyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */; };
|
||||
82D6FB132CD99F7900C925F4 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
||||
82D6FB142CD99F7900C925F4 /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||
82D6FB152CD99F7900C925F4 /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
||||
@@ -733,7 +733,6 @@
|
||||
82D6FBD82CD99F7900C925F4 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
|
||||
82D6FBD92CD99F7900C925F4 /* GradientFollowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F32A6732B7001F4053 /* GradientFollowButton.swift */; };
|
||||
82D6FBDA2CD99F7900C925F4 /* AlbyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09652A0AE62100943473 /* AlbyButton.swift */; };
|
||||
82D6FBDB2CD99F7900C925F4 /* MutinyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B62B9E692E00781E0A /* MutinyButton.swift */; };
|
||||
82D6FBDC2CD99F7900C925F4 /* DamusVideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2929DDF54400516EAC /* DamusVideoPlayerView.swift */; };
|
||||
82D6FBDD2CD99F7900C925F4 /* DamusVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFC2AA7525700DFEC1F /* DamusVideoPlayer.swift */; };
|
||||
82D6FBDE2CD99F7900C925F4 /* DamusVideoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFE2AA76A0900DFEC1F /* DamusVideoCoordinator.swift */; };
|
||||
@@ -1127,7 +1126,6 @@
|
||||
D73E5E442C6A97F4007EB227 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; };
|
||||
D73E5E452C6A97F4007EB227 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; };
|
||||
D73E5E462C6A97F4007EB227 /* DamusLightGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */; };
|
||||
D73E5E472C6A97F4007EB227 /* MutinyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */; };
|
||||
D73E5E482C6A97F4007EB227 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
||||
D73E5E492C6A97F4007EB227 /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||
D73E5E4D2C6A97F4007EB227 /* NIP05Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */; };
|
||||
@@ -1255,7 +1253,6 @@
|
||||
D73E5ED42C6A97F4007EB227 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
|
||||
D73E5ED52C6A97F4007EB227 /* GradientFollowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F32A6732B7001F4053 /* GradientFollowButton.swift */; };
|
||||
D73E5ED62C6A97F4007EB227 /* AlbyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09652A0AE62100943473 /* AlbyButton.swift */; };
|
||||
D73E5ED72C6A97F4007EB227 /* MutinyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B62B9E692E00781E0A /* MutinyButton.swift */; };
|
||||
D73E5ED82C6A97F4007EB227 /* DamusVideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2929DDF54400516EAC /* DamusVideoPlayerView.swift */; };
|
||||
D73E5ED92C6A97F4007EB227 /* DamusVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFC2AA7525700DFEC1F /* DamusVideoPlayer.swift */; };
|
||||
D73E5EDA2C6A97F4007EB227 /* DamusVideoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A16FFE2AA76A0900DFEC1F /* DamusVideoCoordinator.swift */; };
|
||||
@@ -1485,6 +1482,7 @@
|
||||
D798D22E2B086E4800234419 /* NostrResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB028049D510006080F /* NostrResponse.swift */; };
|
||||
D79C4C172AFEB061003A41B4 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C4C162AFEB061003A41B4 /* NotificationService.swift */; };
|
||||
D79C4C1B2AFEB061003A41B4 /* DamusNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
D7A0D8752D1FE67900DCBE59 /* EditPictureControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */; };
|
||||
D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */; };
|
||||
D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343EF2AD0D77C00CED48B /* SnapshotTesting */; };
|
||||
D7ADD3DE2B53854300F104C4 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
|
||||
@@ -1492,6 +1490,9 @@
|
||||
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */; };
|
||||
D7B76C902C825042003A16CB /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
|
||||
D7B76C912C82507F003A16CB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; };
|
||||
D7C48C0B2D12DE0C00A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */; };
|
||||
D7C48C0D2D12E34900A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0C2D12E34900A3BACF /* SwiftyCrop */; };
|
||||
D7C48C0F2D12E35600A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0E2D12E35600A3BACF /* SwiftyCrop */; };
|
||||
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; };
|
||||
D7C9701E2C890FC500C56602 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
||||
D7C9701F2C890FEB00C56602 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
|
||||
@@ -2335,9 +2336,8 @@
|
||||
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
|
||||
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinkGradient.swift; sourceTree = "<group>"; };
|
||||
5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = "<group>"; };
|
||||
5C7389B62B9E692E00781E0A /* MutinyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyButton.swift; sourceTree = "<group>"; };
|
||||
5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyGradient.swift; sourceTree = "<group>"; };
|
||||
5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingTimelineView.swift; sourceTree = "<group>"; };
|
||||
5CB017202D2D985800A9ED05 /* CoinosButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinosButton.swift; sourceTree = "<group>"; };
|
||||
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEvent.swift; sourceTree = "<group>"; };
|
||||
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = "<group>"; };
|
||||
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDescription.swift; sourceTree = "<group>"; };
|
||||
@@ -2446,6 +2446,7 @@
|
||||
D79C4C162AFEB061003A41B4 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DamusNotificationService.entitlements; sourceTree = "<group>"; };
|
||||
D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditPictureControlTests.swift; sourceTree = "<group>"; };
|
||||
D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleURL.swift; sourceTree = "<group>"; };
|
||||
D7ADD3DF2B538D4200F104C4 /* DamusPurpleURLSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleURLSheetView.swift; sourceTree = "<group>"; };
|
||||
D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleVerifyNpubView.swift; sourceTree = "<group>"; };
|
||||
@@ -2508,6 +2509,7 @@
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
|
||||
D70D90982CDED61800CD0534 /* CodeScanner in Frameworks */,
|
||||
D7C48C0B2D12DE0C00A3BACF /* SwiftyCrop in Frameworks */,
|
||||
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */,
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
|
||||
4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */,
|
||||
@@ -2537,6 +2539,7 @@
|
||||
82D6FC862CD9A4A600C925F4 /* MarkdownUI in Frameworks */,
|
||||
82D6FC8A2CD9A54600C925F4 /* SwipeActions in Frameworks */,
|
||||
D7F360292CEBBE34009D34DA /* CodeScanner in Frameworks */,
|
||||
D7C48C0D2D12E34900A3BACF /* SwiftyCrop in Frameworks */,
|
||||
82D6FC882CD9A4DE00C925F4 /* EmojiPicker in Frameworks */,
|
||||
82D6FC842CD9A48500C925F4 /* Kingfisher in Frameworks */,
|
||||
82D6FC812CD99FC500C925F4 /* secp256k1 in Frameworks */,
|
||||
@@ -2551,6 +2554,7 @@
|
||||
D73E5F9D2C6AA8E3007EB227 /* SwipeActions in Frameworks */,
|
||||
D73E5F762C6A997E007EB227 /* EmojiPicker in Frameworks */,
|
||||
D703D7192C66E47100A400EA /* UniformTypeIdentifiers.framework in Frameworks */,
|
||||
D7C48C0F2D12E35600A3BACF /* SwiftyCrop in Frameworks */,
|
||||
D703D7492C6709B100A400EA /* secp256k1 in Frameworks */,
|
||||
D70D909C2CDED7B200CD0534 /* CodeScanner in Frameworks */,
|
||||
D73E5F9B2C6AA8B0007EB227 /* Kingfisher in Frameworks */,
|
||||
@@ -3184,7 +3188,6 @@
|
||||
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */,
|
||||
4C687C202A5F7ED00092C550 /* DamusBackground.swift */,
|
||||
5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */,
|
||||
5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */,
|
||||
);
|
||||
path = Gradients;
|
||||
sourceTree = "<group>";
|
||||
@@ -3253,10 +3256,10 @@
|
||||
4C8D1A6D29F31E4100ACDF75 /* Buttons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CB017202D2D985800A9ED05 /* CoinosButton.swift */,
|
||||
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */,
|
||||
F71694F32A6732B7001F4053 /* GradientFollowButton.swift */,
|
||||
4C7D09652A0AE62100943473 /* AlbyButton.swift */,
|
||||
5C7389B62B9E692E00781E0A /* MutinyButton.swift */,
|
||||
);
|
||||
path = Buttons;
|
||||
sourceTree = "<group>";
|
||||
@@ -3590,6 +3593,7 @@
|
||||
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */,
|
||||
E06336A72B7582D600A88E6B /* Assets */,
|
||||
D72A2D032AD9C165002AFF62 /* Mocking */,
|
||||
4C9B0DEC2A65A74000CBDA21 /* Util */,
|
||||
@@ -3967,6 +3971,7 @@
|
||||
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */,
|
||||
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
|
||||
D70D90972CDED61800CD0534 /* CodeScanner */,
|
||||
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */,
|
||||
);
|
||||
productName = damus;
|
||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||
@@ -4032,6 +4037,7 @@
|
||||
82D6FC872CD9A4DE00C925F4 /* EmojiPicker */,
|
||||
82D6FC892CD9A54600C925F4 /* SwipeActions */,
|
||||
D7F360282CEBBE34009D34DA /* CodeScanner */,
|
||||
D7C48C0C2D12E34900A3BACF /* SwiftyCrop */,
|
||||
);
|
||||
productName = "share extension";
|
||||
productReference = 82D6FA972CD9820500C925F4 /* ShareExtension.appex */;
|
||||
@@ -4059,6 +4065,7 @@
|
||||
D73E5F9A2C6AA8B0007EB227 /* Kingfisher */,
|
||||
D73E5F9C2C6AA8E3007EB227 /* SwipeActions */,
|
||||
D70D909B2CDED7B200CD0534 /* CodeScanner */,
|
||||
D7C48C0E2D12E35600A3BACF /* SwiftyCrop */,
|
||||
);
|
||||
productName = "highlighter action extension";
|
||||
productReference = D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */;
|
||||
@@ -4167,6 +4174,7 @@
|
||||
3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */,
|
||||
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */,
|
||||
D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */,
|
||||
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */,
|
||||
);
|
||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -4299,7 +4307,6 @@
|
||||
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
|
||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
|
||||
4CDD1AE22A6B3074001CD4DF /* NdbTagsIterator.swift in Sources */,
|
||||
5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */,
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
||||
D7CB5D512B1174D100AD4105 /* FriendFilter.swift in Sources */,
|
||||
D7CBD1D42B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift in Sources */,
|
||||
@@ -4604,7 +4611,6 @@
|
||||
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
|
||||
F71694EA2A662232001F4053 /* OnboardingSuggestionsView.swift in Sources */,
|
||||
4C12536A2A76D3850004F4B8 /* RelaysChangedNotify.swift in Sources */,
|
||||
5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */,
|
||||
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
|
||||
D7373BAA2B68A65A00F7783D /* PurpleAccountUpdateNotify.swift in Sources */,
|
||||
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
|
||||
@@ -4702,6 +4708,7 @@
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
|
||||
4C1253622A76D00B0004F4B8 /* PostNotify.swift in Sources */,
|
||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
||||
5CB017232D2D985E00A9ED05 /* CoinosButton.swift in Sources */,
|
||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
||||
4C9147002A2A891E00DDEA40 /* error.c in Sources */,
|
||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
||||
@@ -4795,6 +4802,7 @@
|
||||
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */,
|
||||
E06336AA2B75832100A88E6B /* ImageMetadataTest.swift in Sources */,
|
||||
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
|
||||
D7A0D8752D1FE67900DCBE59 /* EditPictureControlTests.swift in Sources */,
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
|
||||
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
|
||||
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
|
||||
@@ -4926,7 +4934,6 @@
|
||||
82D6FB0F2CD99F7900C925F4 /* DamusLogoGradient.swift in Sources */,
|
||||
82D6FB102CD99F7900C925F4 /* DamusBackground.swift in Sources */,
|
||||
82D6FB112CD99F7900C925F4 /* DamusLightGradient.swift in Sources */,
|
||||
82D6FB122CD99F7900C925F4 /* MutinyGradient.swift in Sources */,
|
||||
82D6FB132CD99F7900C925F4 /* Shimmer.swift in Sources */,
|
||||
82D6FB142CD99F7900C925F4 /* EndBlock.swift in Sources */,
|
||||
82D6FB152CD99F7900C925F4 /* ImageCarousel.swift in Sources */,
|
||||
@@ -5124,7 +5131,6 @@
|
||||
82D6FBD82CD99F7900C925F4 /* FriendsButton.swift in Sources */,
|
||||
82D6FBD92CD99F7900C925F4 /* GradientFollowButton.swift in Sources */,
|
||||
82D6FBDA2CD99F7900C925F4 /* AlbyButton.swift in Sources */,
|
||||
82D6FBDB2CD99F7900C925F4 /* MutinyButton.swift in Sources */,
|
||||
82D6FBDC2CD99F7900C925F4 /* DamusVideoPlayerView.swift in Sources */,
|
||||
82D6FBDD2CD99F7900C925F4 /* DamusVideoPlayer.swift in Sources */,
|
||||
82D6FBDE2CD99F7900C925F4 /* DamusVideoCoordinator.swift in Sources */,
|
||||
@@ -5179,6 +5185,7 @@
|
||||
82D6FC0E2CD99F7900C925F4 /* ProfilePicView.swift in Sources */,
|
||||
82D6FC0F2CD99F7900C925F4 /* ProfileView.swift in Sources */,
|
||||
82D6FC102CD99F7900C925F4 /* ProfileNameView.swift in Sources */,
|
||||
5CB017212D2D985E00A9ED05 /* CoinosButton.swift in Sources */,
|
||||
82D6FC112CD99F7900C925F4 /* MaybeAnonPfpView.swift in Sources */,
|
||||
82D6FC122CD99F7900C925F4 /* EventProfileName.swift in Sources */,
|
||||
82D6FC132CD99F7900C925F4 /* FriendIcon.swift in Sources */,
|
||||
@@ -5337,7 +5344,6 @@
|
||||
D73E5E442C6A97F4007EB227 /* DamusLogoGradient.swift in Sources */,
|
||||
D73E5E452C6A97F4007EB227 /* DamusBackground.swift in Sources */,
|
||||
D73E5E462C6A97F4007EB227 /* DamusLightGradient.swift in Sources */,
|
||||
D73E5E472C6A97F4007EB227 /* MutinyGradient.swift in Sources */,
|
||||
D73E5E482C6A97F4007EB227 /* Shimmer.swift in Sources */,
|
||||
D73E5E492C6A97F4007EB227 /* EndBlock.swift in Sources */,
|
||||
D73E5E4D2C6A97F4007EB227 /* NIP05Badge.swift in Sources */,
|
||||
@@ -5457,6 +5463,7 @@
|
||||
D73E5EBB2C6A97F4007EB227 /* Nip98HTTPAuth.swift in Sources */,
|
||||
D73E5EBC2C6A97F4007EB227 /* Relay.swift in Sources */,
|
||||
D73E5EBD2C6A97F4007EB227 /* NostrRequest.swift in Sources */,
|
||||
5CB017222D2D985E00A9ED05 /* CoinosButton.swift in Sources */,
|
||||
D73E5EBE2C6A97F4007EB227 /* NostrLink.swift in Sources */,
|
||||
D73E5EBF2C6A97F4007EB227 /* WebSocket.swift in Sources */,
|
||||
D73E5F812C6AA07A007EB227 /* HighlighterExtensionAliases.swift in Sources */,
|
||||
@@ -5476,7 +5483,6 @@
|
||||
D73E5ED42C6A97F4007EB227 /* FriendsButton.swift in Sources */,
|
||||
D73E5ED52C6A97F4007EB227 /* GradientFollowButton.swift in Sources */,
|
||||
D73E5ED62C6A97F4007EB227 /* AlbyButton.swift in Sources */,
|
||||
D73E5ED72C6A97F4007EB227 /* MutinyButton.swift in Sources */,
|
||||
D73E5ED82C6A97F4007EB227 /* DamusVideoPlayerView.swift in Sources */,
|
||||
D73E5ED92C6A97F4007EB227 /* DamusVideoPlayer.swift in Sources */,
|
||||
D73E5EDA2C6A97F4007EB227 /* DamusVideoCoordinator.swift in Sources */,
|
||||
@@ -6251,6 +6257,7 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -6300,6 +6307,7 @@
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -6711,6 +6719,14 @@
|
||||
minimumVersion = 1.14.1;
|
||||
};
|
||||
};
|
||||
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/benedom/SwiftyCrop";
|
||||
requirement = {
|
||||
kind = revision;
|
||||
revision = 454d0a0d4faf6f3a19c8d817ab9d7d27524bd79f;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -6824,6 +6840,21 @@
|
||||
package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;
|
||||
productName = SnapshotTesting;
|
||||
};
|
||||
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */;
|
||||
productName = SwiftyCrop;
|
||||
};
|
||||
D7C48C0C2D12E34900A3BACF /* SwiftyCrop */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */;
|
||||
productName = SwiftyCrop;
|
||||
};
|
||||
D7C48C0E2D12E35600A3BACF /* SwiftyCrop */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */;
|
||||
productName = SwiftyCrop;
|
||||
};
|
||||
D7EDED242B117F7C0018B19C /* MarkdownUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "534c8e58993919d5ead25ceb4788c8e492c86bc2cf5833dc651ae60a0f30169c",
|
||||
"originHash" : "0d806129a33991730dd1aa3d38c47a745f9e9e6ff44999f4a7f28b695f024832",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "codescanner",
|
||||
@@ -97,6 +97,14 @@
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftycrop",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/benedom/SwiftyCrop",
|
||||
"state" : {
|
||||
"revision" : "454d0a0d4faf6f3a19c8d817ab9d7d27524bd79f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swipeactions",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 511 KiB After Width: | Height: | Size: 511 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 547 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
6
damus/Assets.xcassets/Logos/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
23
damus/Assets.xcassets/Logos/alby.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "alby.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "alby.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "alby.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "profile-banner.jpeg",
|
||||
"filename" : "coinos.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
damus/Assets.xcassets/Logos/coinos.imageset/coinos.png
vendored
Normal file
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 290 KiB |
12
damus/Assets.xcassets/alby-go.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "alby-go.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
damus/Assets.xcassets/alby-go.imageset/alby-go.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "alby.svg",
|
||||
"idiom": "universal",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"scale": "2x",
|
||||
"filename": "alby.svg"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"scale": "3x",
|
||||
"filename": "alby.svg"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// MutinyGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 3/9/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let mutiny_grad_c1 = hex_col(r: 39, g: 95, b: 161)
|
||||
fileprivate let mutiny_grad_c2 = hex_col(r: 13, g: 33, b: 56)
|
||||
fileprivate let mutiny_grad = [mutiny_grad_c2, mutiny_grad_c1]
|
||||
|
||||
let MutinyGradient: LinearGradient =
|
||||
LinearGradient(colors: mutiny_grad, startPoint: .top, endPoint: .bottom)
|
||||
@@ -931,7 +931,6 @@ enum FindEventType {
|
||||
|
||||
enum FoundEvent {
|
||||
case profile(Pubkey)
|
||||
case invalid_profile(NostrEvent)
|
||||
case event(NostrEvent)
|
||||
}
|
||||
|
||||
@@ -988,10 +987,6 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
switch query {
|
||||
case .profile:
|
||||
if ev.known_kind == .metadata {
|
||||
guard state.ndb.lookup_profile_key(ev.pubkey) != nil else {
|
||||
callback(.invalid_profile(ev))
|
||||
return
|
||||
}
|
||||
callback(.profile(ev.pubkey))
|
||||
}
|
||||
case .event:
|
||||
@@ -1000,17 +995,16 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
case .eose:
|
||||
if !has_event {
|
||||
attempts += 1
|
||||
if attempts == state.pool.our_descriptors.count / 2 {
|
||||
callback(nil)
|
||||
if attempts >= state.pool.our_descriptors.count {
|
||||
callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil
|
||||
}
|
||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
}
|
||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
|
||||
case .notice:
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>river</string>
|
||||
<string>alby</string>
|
||||
<string>albygo</string>
|
||||
<string>bitcoinbeach</string>
|
||||
<string>breez</string>
|
||||
<string>muun</string>
|
||||
|
||||
@@ -77,11 +77,19 @@ enum MediaUpload {
|
||||
}
|
||||
}
|
||||
|
||||
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||
protocol ImageUploadModelProtocol {
|
||||
init()
|
||||
|
||||
func start(media: MediaUpload, uploader: any MediaUploaderProtocol, mediaType: ImageUploadMediaType, keypair: Keypair?) async -> ImageUploadResult
|
||||
}
|
||||
|
||||
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject, ImageUploadModelProtocol {
|
||||
@Published var progress: Double? = nil
|
||||
|
||||
func start(media: MediaUpload, uploader: MediaUploader, keypair: Keypair? = nil) async -> ImageUploadResult {
|
||||
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self, keypair: keypair)
|
||||
override required init() { }
|
||||
|
||||
func start(media: MediaUpload, uploader: any MediaUploaderProtocol, mediaType: ImageUploadMediaType, keypair: Keypair? = nil) async -> ImageUploadResult {
|
||||
let res = await AttachMediaUtility.create_upload_request(mediaToUpload: media, mediaUploader: uploader, mediaType: mediaType, progress: self, keypair: keypair)
|
||||
|
||||
switch res {
|
||||
case .success(_):
|
||||
@@ -89,10 +97,17 @@ class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||
self.progress = nil
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
}
|
||||
case .failed(_):
|
||||
case .failed(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.progress = nil
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
if let nsError = error as NSError?,
|
||||
nsError.domain == NSURLErrorDomain,
|
||||
nsError.code == NSURLErrorCancelled {
|
||||
print("Upload forced cancelled by user after Cancelling the Post, no feedback triggered.")
|
||||
} else {
|
||||
// Trigger feedback for all other errors
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,18 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
protocol MediaUploaderProtocol: Identifiable {
|
||||
var nameParam: String { get }
|
||||
var mediaTypeParam: String { get }
|
||||
var supportsVideo: Bool { get }
|
||||
var requiresNip98: Bool { get }
|
||||
var postAPI: String { get }
|
||||
|
||||
func getMediaURL(from data: Data) -> String?
|
||||
func mediaTypeValue(for mediaType: ImageUploadMediaType) -> String?
|
||||
}
|
||||
|
||||
enum MediaUploader: String, CaseIterable, MediaUploaderProtocol, StringCodable {
|
||||
var id: String { self.rawValue }
|
||||
case nostrBuild
|
||||
case nostrcheck
|
||||
@@ -33,6 +44,19 @@ enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
}
|
||||
}
|
||||
|
||||
var mediaTypeParam: String {
|
||||
return "media_type"
|
||||
}
|
||||
|
||||
func mediaTypeValue(for mediaType: ImageUploadMediaType) -> String? {
|
||||
switch mediaType {
|
||||
case .normal:
|
||||
return nil
|
||||
case .profile_picture:
|
||||
return "avatar"
|
||||
}
|
||||
}
|
||||
|
||||
var supportsVideo: Bool {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
@@ -42,6 +66,15 @@ enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
}
|
||||
}
|
||||
|
||||
var requiresNip98: Bool {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return true
|
||||
case .nostrcheck:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
var id: String { self.tag }
|
||||
var index: Int
|
||||
|
||||
@@ -185,9 +185,6 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
@Setting(key: "show_music_statuses", default_value: true)
|
||||
var show_music_statuses: Bool
|
||||
|
||||
@Setting(key: "show_only_preferred_languages", default_value: false)
|
||||
var show_only_preferred_languages: Bool
|
||||
|
||||
@Setting(key: "multiple_events_per_pubkey", default_value: false)
|
||||
var multiple_events_per_pubkey: Bool
|
||||
|
||||
@@ -46,6 +46,7 @@ enum Wallet: String, CaseIterable, Identifiable, StringCodable {
|
||||
case bitcoinbeach
|
||||
case blixtwallet
|
||||
case river
|
||||
case albygo
|
||||
|
||||
var model: Model {
|
||||
switch self {
|
||||
@@ -90,6 +91,9 @@ enum Wallet: String, CaseIterable, Identifiable, StringCodable {
|
||||
case .river:
|
||||
return .init(index: 12, tag: "river", displayName: "River", link: "river://",
|
||||
appStoreLink: "https://apps.apple.com/us/app/river-buy-mine-bitcoin/id1536176542", image: "river")
|
||||
case .albygo:
|
||||
return .init(index: 13, tag: "albygo", displayName: "Alby Go", link: "alby:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/alby-go/id6471335774", image: "alby-go")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,11 +259,10 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, note_lang: String
|
||||
}
|
||||
|
||||
if let note_lang {
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
let currentLanguage = localeToLanguage(Locale.current.identifier)
|
||||
|
||||
// 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
|
||||
// Don't translate if the note is in our current language
|
||||
guard currentLanguage != note_lang else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,30 @@ enum ImageUploadResult {
|
||||
case failed(Error?)
|
||||
}
|
||||
|
||||
fileprivate func create_upload_body(mediaData: Data, boundary: String, mediaUploader: MediaUploader, mediaToUpload: MediaUpload) -> Data {
|
||||
enum ImageUploadMediaType {
|
||||
case normal
|
||||
case profile_picture
|
||||
}
|
||||
|
||||
protocol AttachMediaUtilityProtocol {
|
||||
static func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: any MediaUploaderProtocol, mediaType: ImageUploadMediaType, progress: URLSessionTaskDelegate, keypair: Keypair?) async -> ImageUploadResult
|
||||
}
|
||||
|
||||
class AttachMediaUtility {
|
||||
fileprivate static func create_upload_body(mediaData: Data, boundary: String, mediaUploader: any MediaUploaderProtocol, mediaToUpload: MediaUpload, mediaType: ImageUploadMediaType) -> Data {
|
||||
let mediaTypeFieldValue = mediaUploader.mediaTypeValue(for: mediaType)
|
||||
let mediaTypeFieldEntry: String?
|
||||
if let mediaTypeFieldValue {
|
||||
mediaTypeFieldEntry = "; \(mediaUploader.mediaTypeParam)=\(mediaTypeFieldValue)"
|
||||
}
|
||||
else {
|
||||
mediaTypeFieldEntry = nil
|
||||
}
|
||||
let body = NSMutableData();
|
||||
let contentType = mediaToUpload.mime_type
|
||||
body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n")
|
||||
body.appendString(string: "--\(boundary)\r\n")
|
||||
body.appendString(string: "Content-Disposition: form-data; name=\(mediaUploader.nameParam); filename=\(mediaToUpload.genericFileName)\r\n")
|
||||
body.appendString(string: "Content-Disposition: form-data; name=\(mediaUploader.nameParam); filename=\(mediaToUpload.genericFileName)\(mediaTypeFieldEntry ?? "")\r\n")
|
||||
body.appendString(string: "Content-Type: \(contentType)\r\n\r\n")
|
||||
body.append(mediaData as Data)
|
||||
body.appendString(string: "\r\n")
|
||||
@@ -28,59 +46,60 @@ fileprivate func create_upload_body(mediaData: Data, boundary: String, mediaUplo
|
||||
return body as Data
|
||||
}
|
||||
|
||||
func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploader, progress: URLSessionTaskDelegate, keypair: Keypair? = nil) async -> ImageUploadResult {
|
||||
var mediaData: Data?
|
||||
guard let url = URL(string: mediaUploader.postAPI) else {
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST";
|
||||
let boundary = "Boundary-\(UUID().description)"
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
// If uploading to a media host that support NIP-98 authorization, add the header
|
||||
if mediaUploader == .nostrBuild || mediaUploader == .nostrcheck,
|
||||
let keypair,
|
||||
let method = request.httpMethod,
|
||||
let signature = create_nip98_signature(keypair: keypair, method: method, url: url) {
|
||||
|
||||
request.setValue(signature, forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
switch mediaToUpload {
|
||||
case .image(let url):
|
||||
do {
|
||||
mediaData = try Data(contentsOf: url)
|
||||
} catch {
|
||||
return .failed(error)
|
||||
}
|
||||
case .video(let url):
|
||||
do {
|
||||
mediaData = try Data(contentsOf: url)
|
||||
} catch {
|
||||
return .failed(error)
|
||||
}
|
||||
}
|
||||
|
||||
guard let mediaData else {
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
request.httpBody = create_upload_body(mediaData: mediaData, boundary: boundary, mediaUploader: mediaUploader, mediaToUpload: mediaToUpload)
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
|
||||
|
||||
guard let url = mediaUploader.getMediaURL(from: data) else {
|
||||
print("Upload failed getting media url")
|
||||
static func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: any MediaUploaderProtocol, mediaType: ImageUploadMediaType, progress: URLSessionTaskDelegate, keypair: Keypair? = nil) async -> ImageUploadResult {
|
||||
var mediaData: Data?
|
||||
guard let url = URL(string: mediaUploader.postAPI) else {
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
return .success(url)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST";
|
||||
let boundary = "Boundary-\(UUID().description)"
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
} catch {
|
||||
return .failed(error)
|
||||
// If uploading to a media host that support NIP-98 authorization, add the header
|
||||
if mediaUploader.requiresNip98,
|
||||
let keypair,
|
||||
let method = request.httpMethod,
|
||||
let signature = create_nip98_signature(keypair: keypair, method: method, url: url) {
|
||||
|
||||
request.setValue(signature, forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
switch mediaToUpload {
|
||||
case .image(let url):
|
||||
do {
|
||||
mediaData = try Data(contentsOf: url)
|
||||
} catch {
|
||||
return .failed(error)
|
||||
}
|
||||
case .video(let url):
|
||||
do {
|
||||
mediaData = try Data(contentsOf: url)
|
||||
} catch {
|
||||
return .failed(error)
|
||||
}
|
||||
}
|
||||
|
||||
guard let mediaData else {
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
request.httpBody = create_upload_body(mediaData: mediaData, boundary: boundary, mediaUploader: mediaUploader, mediaToUpload: mediaToUpload, mediaType: mediaType)
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
|
||||
|
||||
guard let url = mediaUploader.getMediaURL(from: data) else {
|
||||
print("Upload failed getting media url")
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
return .success(url)
|
||||
|
||||
} catch {
|
||||
return .failed(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,15 @@ struct EditBannerImageView: View {
|
||||
.onFailureImage(defaultImage)
|
||||
.kfClickable()
|
||||
|
||||
EditPictureControl(uploader: damus_state.settings.default_media_uploader, keypair: damus_state.keypair, pubkey: damus_state.pubkey, image_url: $banner_image, uploadObserver: viewModel, callback: callback)
|
||||
EditPictureControl(
|
||||
uploader: damus_state.settings.default_media_uploader,
|
||||
context: .normal,
|
||||
keypair: damus_state.keypair,
|
||||
pubkey: damus_state.pubkey,
|
||||
current_image_url: $banner_image,
|
||||
upload_observer: viewModel,
|
||||
callback: callback
|
||||
)
|
||||
.padding(10)
|
||||
.backwardsCompatibleSafeAreaPadding(self.safeAreaInsets)
|
||||
.accessibilityLabel(NSLocalizedString("Edit banner image", comment: "Accessibility label for edit banner image button"))
|
||||
|
||||
48
damus/Views/Buttons/CoinosButton.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// CoinosButton.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 1/7/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CoinosButton: View {
|
||||
let action: () -> ()
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(action: @escaping () -> ()) {
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
action()
|
||||
}) {
|
||||
HStack {
|
||||
Image("coinos")
|
||||
.resizable()
|
||||
.frame(width: 35, height: 35)
|
||||
|
||||
Text("Connect to Coinos", comment: "Button to attach a Coinos Wallet, a service that provides a Lightning wallet for zapping sats. Coinos is the name of the service and should not be translated.")
|
||||
.padding()
|
||||
.bold()
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
.foregroundColor(DamusColors.black)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(GrayGradient, strokeBorder: colorScheme == .light ? DamusColors.black.opacity(0.2) : DamusColors.white.opacity(0.2), lineWidth: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CoinosButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CoinosButton(action: {
|
||||
print("mutiny button")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// MutinyButton.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 3/9/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MutinyButton: View {
|
||||
let action: () -> ()
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(action: @escaping () -> ()) {
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
action()
|
||||
}) {
|
||||
HStack {
|
||||
Image("mutiny")
|
||||
.resizable()
|
||||
.frame(width: 45, height: 45)
|
||||
|
||||
Text("Connect to Mutiny Wallet", comment: "Button to attach an Mutiny Wallet, a service that provides a Lightning wallet for zapping sats. Mutiny is the name of the service and should not be translated.")
|
||||
.padding()
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
.foregroundColor(DamusColors.white)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(MutinyGradient, strokeBorder: colorScheme == .light ? DamusColors.black.opacity(0.2) : DamusColors.white.opacity(0.2), lineWidth: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MutinyButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MutinyButton(action: {
|
||||
print("mutiny button")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,14 @@ struct CameraController: UIViewControllerRepresentable {
|
||||
@Environment(\.presentationMode)
|
||||
@Binding private var presentationMode
|
||||
|
||||
let uploader: MediaUploader
|
||||
let done: () -> Void
|
||||
let uploader: any MediaUploaderProtocol
|
||||
var imagesOnly: Bool = false
|
||||
var mode: Mode
|
||||
|
||||
enum Mode {
|
||||
case save_to_library(when_done: () -> Void)
|
||||
case handle_image(handler: (UIImage) -> Void)
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
|
||||
let parent: CameraController
|
||||
@@ -25,18 +30,29 @@ struct CameraController: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
if !parent.imagesOnly, let videoURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL {
|
||||
// Handle the selected video
|
||||
UISaveVideoAtPathToSavedPhotosAlbum(videoURL.relativePath, nil, nil, nil)
|
||||
} else if let cameraImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
|
||||
let orientedImage = cameraImage.fixOrientation()
|
||||
UIImageWriteToSavedPhotosAlbum(orientedImage, nil, nil, nil)
|
||||
} else if let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
|
||||
let orientedImage = editedImage.fixOrientation()
|
||||
UIImageWriteToSavedPhotosAlbum(orientedImage, nil, nil, nil)
|
||||
switch parent.mode {
|
||||
case .save_to_library(when_done: let done):
|
||||
if !parent.imagesOnly, let videoURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL {
|
||||
// Handle the selected video
|
||||
UISaveVideoAtPathToSavedPhotosAlbum(videoURL.relativePath, nil, nil, nil)
|
||||
} else if let cameraImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
|
||||
let orientedImage = cameraImage.fixOrientation()
|
||||
UIImageWriteToSavedPhotosAlbum(orientedImage, nil, nil, nil)
|
||||
} else if let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
|
||||
let orientedImage = editedImage.fixOrientation()
|
||||
UIImageWriteToSavedPhotosAlbum(orientedImage, nil, nil, nil)
|
||||
}
|
||||
done()
|
||||
case .handle_image(handler: let handler):
|
||||
if let cameraImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
|
||||
let orientedImage = cameraImage.fixOrientation()
|
||||
handler(orientedImage)
|
||||
} else if let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
|
||||
let orientedImage = editedImage.fixOrientation()
|
||||
handler(orientedImage)
|
||||
}
|
||||
}
|
||||
|
||||
parent.done()
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct CreateAccountView: View {
|
||||
struct CreateAccountView: View, KeyboardReadable {
|
||||
@StateObject var account: CreateAccountModel = CreateAccountModel()
|
||||
@StateObject var profileUploadObserver = ImageUploadingObserver()
|
||||
var nav: NavigationCoordinator
|
||||
@State var keyboardVisible: Bool = false
|
||||
let maxViewportHeightForAdaptiveContentSize: CGFloat = 975 // 956px height = iPhone 16 Pro Max
|
||||
|
||||
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
|
||||
return VStack(alignment: .leading, spacing: 10.0, content: content)
|
||||
@@ -26,15 +29,25 @@ struct CreateAccountView: View {
|
||||
ZStack(alignment: .top) {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .center) {
|
||||
|
||||
EditPictureControl(uploader: .nostrBuild, keypair: account.keypair, pubkey: account.pubkey, size: 75, setup: true, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture)
|
||||
let screenHeight = UIScreen.main.bounds.height
|
||||
let style = EditPictureControl.Style(
|
||||
size: keyboardVisible && screenHeight < maxViewportHeightForAdaptiveContentSize ? 25 : 75,
|
||||
first_time_setup: true
|
||||
)
|
||||
|
||||
EditPictureControl(
|
||||
uploader: MediaUploader.nostrBuild,
|
||||
context: .profile_picture,
|
||||
keypair: account.keypair,
|
||||
pubkey: account.pubkey,
|
||||
style: style,
|
||||
current_image_url: $account.profile_image,
|
||||
upload_observer: profileUploadObserver,
|
||||
callback: uploadedProfilePicture
|
||||
)
|
||||
.shadow(radius: 2)
|
||||
.padding(.top, 100)
|
||||
|
||||
Text("Add Photo", comment: "Label to indicate user can add a photo.")
|
||||
.bold()
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
}
|
||||
|
||||
SignupForm {
|
||||
@@ -42,13 +55,13 @@ struct CreateAccountView: View {
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.name)
|
||||
.textInputAutocapitalization(.words)
|
||||
|
||||
|
||||
FormLabel(NSLocalizedString("Bio", comment: "Label to prompt bio entry for user to describe themself."), optional: true)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
FormTextInput(NSLocalizedString("Absolute legend.", comment: "Example Bio"), text: $account.about)
|
||||
}
|
||||
.padding(.top, 25)
|
||||
|
||||
|
||||
Button(action: {
|
||||
nav.push(route: Route.SaveKeys(account: account))
|
||||
}) {
|
||||
@@ -72,6 +85,11 @@ struct CreateAccountView: View {
|
||||
}
|
||||
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
|
||||
.dismissKeyboardOnTap()
|
||||
.onReceive(keyboardPublisher) { visible in
|
||||
withAnimation {
|
||||
self.keyboardVisible = visible
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
|
||||
@@ -20,8 +20,14 @@ struct MediaPicker: UIViewControllerRepresentable {
|
||||
@Binding private var presentationMode
|
||||
let mediaPickerEntry: MediaPickerEntry
|
||||
|
||||
@Binding var image_upload_confirm: Bool
|
||||
let onMediaSelected: (() -> Void)?
|
||||
let onMediaPicked: (PreUploadedMedia) -> Void
|
||||
|
||||
init(mediaPickerEntry: MediaPickerEntry, onMediaSelected: (() -> Void)? = nil, onMediaPicked: @escaping (PreUploadedMedia) -> Void) {
|
||||
self.mediaPickerEntry = mediaPickerEntry
|
||||
self.onMediaSelected = onMediaSelected
|
||||
self.onMediaPicked = onMediaPicked
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||
var parent: MediaPicker
|
||||
@@ -121,7 +127,7 @@ struct MediaPicker: UIViewControllerRepresentable {
|
||||
|
||||
|
||||
private func chooseMedia(_ media: PreUploadedMedia, orderId: String) {
|
||||
self.parent.image_upload_confirm = true
|
||||
self.parent.onMediaSelected?()
|
||||
self.orderMap[orderId] = media
|
||||
self.dispatchGroup.leave()
|
||||
}
|
||||
|
||||
@@ -144,21 +144,25 @@ struct NotificationsView: View {
|
||||
func NotificationTab(_ filter: NotificationFilter) -> some View {
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
Color.white.opacity(0)
|
||||
.id("startblock")
|
||||
.frame(height: 5)
|
||||
let notifs = Array(zip(1..., filter.filter(contacts: state.contacts, items: notifications.notifications)))
|
||||
ForEach(notifs, id: \.0) { zip in
|
||||
NotificationItemView(state: state, item: zip.1)
|
||||
let notifs = Array(zip(1..., filter.filter(contacts: state.contacts, items: notifications.notifications)))
|
||||
if notifs.isEmpty {
|
||||
EmptyTimelineView()
|
||||
} else {
|
||||
LazyVStack(alignment: .leading) {
|
||||
Color.white.opacity(0)
|
||||
.id("startblock")
|
||||
.frame(height: 5)
|
||||
ForEach(notifs, id: \.0) { zip in
|
||||
NotificationItemView(state: state, item: zip.1)
|
||||
}
|
||||
}
|
||||
.background(GeometryReader { proxy -> Color in
|
||||
DispatchQueue.main.async {
|
||||
handle_scroll_queue(proxy, queue: self.notifications)
|
||||
}
|
||||
return Color.clear
|
||||
})
|
||||
}
|
||||
.background(GeometryReader { proxy -> Color in
|
||||
DispatchQueue.main.async {
|
||||
handle_scroll_queue(proxy, queue: self.notifications)
|
||||
}
|
||||
return Color.clear
|
||||
})
|
||||
}
|
||||
.coordinateSpace(name: "scroll")
|
||||
.onReceive(handle_notify(.scroll_to_top)) { notif in
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import Kingfisher
|
||||
|
||||
enum NostrPostResult {
|
||||
case post(NostrPost)
|
||||
@@ -72,6 +73,7 @@ struct PostView: View {
|
||||
@StateObject var tagModel: TagModel = TagModel()
|
||||
|
||||
@State private var current_placeholder_index = 0
|
||||
@State private var uploadTasks: [Task<Void, Never>] = []
|
||||
|
||||
let action: PostAction
|
||||
let damus_state: DamusState
|
||||
@@ -97,9 +99,15 @@ struct PostView: View {
|
||||
|
||||
func cancel() {
|
||||
notify(.post(.cancel))
|
||||
cancelUploadTasks()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
func cancelUploadTasks() {
|
||||
uploadTasks.forEach { $0.cancel() }
|
||||
uploadTasks.removeAll()
|
||||
}
|
||||
|
||||
func send_post() {
|
||||
// don't add duplicate pubkeys but retain order
|
||||
var pkset = Set<Pubkey>()
|
||||
@@ -338,7 +346,7 @@ struct PostView: View {
|
||||
print("img size w:\(img.size.width) h:\(img.size.height)")
|
||||
|
||||
async let blurhash = calculate_blurhash(img: img)
|
||||
let res = await image_upload.start(media: media, uploader: uploader, keypair: damus_state.keypair)
|
||||
let res = await image_upload.start(media: media, uploader: uploader, mediaType: .normal, keypair: damus_state.keypair)
|
||||
|
||||
switch res {
|
||||
case .success(let url):
|
||||
@@ -473,19 +481,20 @@ struct PostView: View {
|
||||
}
|
||||
.background(DamusColors.adaptableWhite.edgesIgnoringSafeArea(.all))
|
||||
.sheet(isPresented: $attach_media) {
|
||||
MediaPicker(mediaPickerEntry: .postView, image_upload_confirm: $image_upload_confirm){ media in
|
||||
MediaPicker(mediaPickerEntry: .postView, onMediaSelected: { image_upload_confirm = true }) { media in
|
||||
self.preUploadedMedia.append(media)
|
||||
}
|
||||
.alert(NSLocalizedString("Are you sure you want to upload the selected media?", comment: "Alert message asking if the user wants to upload media."), isPresented: $image_upload_confirm) {
|
||||
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
|
||||
// initiate asynchronous uploading Task for multiple-images
|
||||
Task {
|
||||
// initiate asynchronous uploading Task for multiple-images
|
||||
let task = Task {
|
||||
for media in preUploadedMedia {
|
||||
if let mediaToUpload = generateMediaUpload(media) {
|
||||
await self.handle_upload(media: mediaToUpload)
|
||||
}
|
||||
}
|
||||
}
|
||||
uploadTasks.append(task)
|
||||
self.attach_media = false
|
||||
}
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {
|
||||
@@ -494,19 +503,20 @@ struct PostView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $attach_camera) {
|
||||
CameraController(uploader: damus_state.settings.default_media_uploader) {
|
||||
CameraController(uploader: damus_state.settings.default_media_uploader, mode: .save_to_library(when_done: {
|
||||
self.attach_camera = false
|
||||
self.attach_media = true
|
||||
}
|
||||
}))
|
||||
}
|
||||
// This alert seeks confirmation about Image-upload when user taps Paste option
|
||||
.alert(NSLocalizedString("Are you sure you want to upload this media?", comment: "Alert message asking if the user wants to upload media."), isPresented: $imageUploadConfirmPasteboard) {
|
||||
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
|
||||
if let image = imagePastedFromPasteboard,
|
||||
let mediaToUpload = generateMediaUpload(image) {
|
||||
Task {
|
||||
await self.handle_upload(media: mediaToUpload)
|
||||
let task = Task {
|
||||
_ = await self.handle_upload(media: mediaToUpload)
|
||||
}
|
||||
uploadTasks.append(task)
|
||||
}
|
||||
}
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
||||
@@ -514,13 +524,14 @@ struct PostView: View {
|
||||
// This alert seeks confirmation about media-upload from Damus Share Extension
|
||||
.alert(NSLocalizedString("Are you sure you want to upload the selected media?", comment: "Alert message asking if the user wants to upload media."), isPresented: $imageUploadConfirmDamusShare) {
|
||||
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
|
||||
Task {
|
||||
let task = Task {
|
||||
for media in preUploadedMedia {
|
||||
if let mediaToUpload = generateMediaUpload(media) {
|
||||
await self.handle_upload(media: mediaToUpload)
|
||||
}
|
||||
}
|
||||
}
|
||||
uploadTasks.append(task)
|
||||
}
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
||||
}
|
||||
@@ -609,38 +620,79 @@ struct PVImageCarouselView: View {
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(media.map({$0.representingImage}), id: \.self) { image in
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: media.count == 1 ? deviceWidth*0.8 : 250, height: media.count == 1 ? 400 : 250)
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
.contextMenu {
|
||||
if let uploadedURL = media.first(where: { $0.representingImage == image })?.uploadedURL {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = uploadedURL.absoluteString
|
||||
}) {
|
||||
Label(NSLocalizedString("Copy URL", comment: "Label for button in context menu to copy URL of the selected uploaded media asset."), image: "copy")
|
||||
}
|
||||
ForEach(media.indices, id: \.self) { index in
|
||||
ZStack(alignment: .topLeading) {
|
||||
if isSupportedVideo(url: media[index].uploadedURL) {
|
||||
VideoPlayer(player: configurePlayer(with: media[index].localURL))
|
||||
.frame(width: media.count == 1 ? deviceWidth * 0.8 : 250, height: media.count == 1 ? 400 : 250)
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
.contextMenu { contextMenuContent(for: media[index]) }
|
||||
} else if is_animated_image(url: media[index].uploadedURL) {
|
||||
KFAnimatedImage(media[index].uploadedURL)
|
||||
.imageContext(.note, disable_animation: false)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
}
|
||||
Image("close-circle")
|
||||
.foregroundColor(.white)
|
||||
.padding(20)
|
||||
.shadow(radius: 5)
|
||||
.onTapGesture {
|
||||
if let index = media.map({$0.representingImage}).firstIndex(of: image) {
|
||||
media.remove(at: index)
|
||||
.frame(width: media.count == 1 ? deviceWidth * 0.8 : 250, height: media.count == 1 ? 400 : 250)
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
.contextMenu { contextMenuContent(for: media[index]) }
|
||||
} else {
|
||||
Image(uiImage: media[index].representingImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: media.count == 1 ? deviceWidth * 0.8 : 250, height: media.count == 1 ? 400 : 250)
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
.contextMenu { contextMenuContent(for: media[index]) }
|
||||
}
|
||||
|
||||
VStack { // Set spacing to 0 to remove the gap between items
|
||||
Image("close-circle")
|
||||
.foregroundColor(.white)
|
||||
.padding(20)
|
||||
.shadow(radius: 5)
|
||||
.onTapGesture {
|
||||
media.remove(at: index) // Direct removal using index
|
||||
}
|
||||
}
|
||||
|
||||
if isSupportedVideo(url: media[index].uploadedURL) {
|
||||
Spacer()
|
||||
Image(systemName: "video")
|
||||
.foregroundColor(.white)
|
||||
.padding(10)
|
||||
.shadow(radius: 5)
|
||||
.opacity(0.6)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 35)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper Function for Context Menu
|
||||
@ViewBuilder
|
||||
private func contextMenuContent(for mediaItem: UploadedMedia) -> some View {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = mediaItem.uploadedURL.absoluteString
|
||||
}) {
|
||||
Label(
|
||||
NSLocalizedString("Copy URL", comment: "Copy URL of the selected uploaded media asset."),
|
||||
systemImage: "doc.on.doc"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func configurePlayer(with url: URL) -> AVPlayer {
|
||||
let player = AVPlayer(url: url)
|
||||
player.allowsExternalPlayback = false
|
||||
player.usesExternalPlaybackWhileExternalScreenIsActive = false
|
||||
return player
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func getImage(media: MediaUpload) -> UIImage {
|
||||
@@ -813,3 +865,14 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post
|
||||
return NostrPost(content: content, kind: .text, tags: tags)
|
||||
}
|
||||
|
||||
func isSupportedVideo(url: URL?) -> Bool {
|
||||
guard let url = url else { return false }
|
||||
let fileExtension = url.pathExtension.lowercased()
|
||||
let supportedUTIs = AVURLAsset.audiovisualTypes().map { $0.rawValue }
|
||||
return supportedUTIs.contains { utiString in
|
||||
if let utType = UTType(utiString), let fileUTType = UTType(filenameExtension: fileExtension) {
|
||||
return fileUTType.conforms(to: utType)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,223 +7,751 @@
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftyCrop
|
||||
|
||||
class ImageUploadingObserver: ObservableObject {
|
||||
@Published var isLoading: Bool = false
|
||||
}
|
||||
// MARK: - Main view
|
||||
|
||||
/// A view that shows an existing picture, and allows a user to upload a new one.
|
||||
struct EditPictureControl: View {
|
||||
let uploader: MediaUploader
|
||||
let keypair: Keypair?
|
||||
let pubkey: Pubkey
|
||||
var size: CGFloat? = 25
|
||||
var setup: Bool? = false
|
||||
@Binding var image_url: URL?
|
||||
@State var image_url_temp: URL?
|
||||
@ObservedObject var uploadObserver: ImageUploadingObserver
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
typealias T = ImageUploadModel
|
||||
typealias Model = EditPictureControlViewModel<T>
|
||||
|
||||
|
||||
// MARK: Properties and state
|
||||
|
||||
@StateObject var model: Model
|
||||
@Binding var current_image_url: URL?
|
||||
let style: Style
|
||||
let callback: (URL?) -> Void
|
||||
|
||||
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
|
||||
|
||||
@State private var show_camera = false
|
||||
@State private var show_library = false
|
||||
@State private var show_url_sheet = false
|
||||
@State var image_upload_confirm: Bool = false
|
||||
|
||||
@State var preUploadedMedia: PreUploadedMedia? = nil
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
init(model: Model, style: Style? = nil, callback: @escaping (URL?) -> Void) {
|
||||
self._model = StateObject.init(wrappedValue: model)
|
||||
self.style = style ?? Style(size: nil, first_time_setup: false)
|
||||
self.callback = callback
|
||||
self._current_image_url = model.$current_image_url
|
||||
}
|
||||
|
||||
init(
|
||||
uploader: any MediaUploaderProtocol,
|
||||
context: Model.Context,
|
||||
keypair: Keypair?,
|
||||
pubkey: Pubkey,
|
||||
style: Style? = nil,
|
||||
current_image_url: Binding<URL?>,
|
||||
upload_observer: ImageUploadingObserver? = nil,
|
||||
callback: @escaping (URL?) -> Void
|
||||
) {
|
||||
let model = EditPictureControlViewModel(
|
||||
context: context,
|
||||
pubkey: pubkey,
|
||||
current_image_url: current_image_url,
|
||||
keypair: keypair,
|
||||
uploader: uploader,
|
||||
callback: callback
|
||||
)
|
||||
self.init(model: model, style: style, callback: callback)
|
||||
}
|
||||
|
||||
|
||||
// MARK: View definitions
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Button(action: {
|
||||
self.show_url_sheet = true
|
||||
}) {
|
||||
self.menu_options
|
||||
} label: {
|
||||
if self.style.first_time_setup {
|
||||
self.first_time_setup_view
|
||||
}
|
||||
else {
|
||||
self.default_view
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(self.accessibility_label)
|
||||
.accessibilityHint(self.accessibility_hint)
|
||||
.maybeAccessibilityValue(self.accessibility_value)
|
||||
.sheet(isPresented: self.model.show_camera) {
|
||||
CameraController(uploader: model.uploader, mode: .handle_image(handler: { image in
|
||||
self.model.request_upload_authorization(PreUploadedMedia.uiimage(image))
|
||||
}))
|
||||
}
|
||||
.sheet(isPresented: self.model.show_library) {
|
||||
MediaPicker(mediaPickerEntry: .editPictureControl) { media in
|
||||
self.model.request_upload_authorization(media)
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
NSLocalizedString("Are you sure you want to upload this image?", comment: "Alert message asking if the user wants to upload an image."),
|
||||
isPresented: Binding.constant(self.model.state.is_confirming_upload)
|
||||
) {
|
||||
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
|
||||
self.model.confirm_upload_authorization()
|
||||
}
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
||||
}
|
||||
.fullScreenCover(isPresented: self.model.show_image_cropper) {
|
||||
self.image_cropper
|
||||
}
|
||||
.sheet(isPresented: self.model.show_url_sheet) {
|
||||
ImageURLSelector(callback: { url in
|
||||
self.model.choose_url(url)
|
||||
}, cancel: { self.model.cancel() })
|
||||
.presentationDetents([.height(300)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(item: self.model.error_message, onDismiss: { self.model.cancel() }, content: { error in
|
||||
Text(error.rawValue)
|
||||
})
|
||||
}
|
||||
|
||||
var progress_view: some View {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: DamusColors.purple))
|
||||
.frame(width: style.size, height: style.size)
|
||||
.padding(10)
|
||||
.background(DamusColors.white.opacity(0.7))
|
||||
.clipShape(Circle())
|
||||
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
|
||||
}
|
||||
|
||||
var menu_options: some View {
|
||||
Group {
|
||||
Button(action: { self.model.select_image_from_url() }) {
|
||||
Text("Image URL", comment: "Option to enter a url")
|
||||
}
|
||||
.accessibilityIdentifier(AppAccessibilityIdentifiers.own_profile_banner_image_edit_from_url.rawValue)
|
||||
|
||||
Button(action: {
|
||||
self.show_library = true
|
||||
}) {
|
||||
Button(action: { self.model.select_image_from_library() }) {
|
||||
Text("Choose from Library", comment: "Option to select photo from library")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
self.show_camera = true
|
||||
}) {
|
||||
Button(action: { self.model.select_image_from_camera() }) {
|
||||
Text("Take Photo", comment: "Option to take a photo with the camera")
|
||||
}
|
||||
} label: {
|
||||
if uploadObserver.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: DamusColors.purple))
|
||||
.frame(width: size, height: size)
|
||||
.padding(10)
|
||||
.background(DamusColors.white.opacity(0.7))
|
||||
.clipShape(Circle())
|
||||
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
|
||||
} else if let url = image_url, setup ?? false {
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.pfp, disable_animation: false)
|
||||
.onFailure(fallbackUrl: URL(string: robohash(pubkey)), cacheKey: url.absoluteString)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.scaledToFill()
|
||||
.frame(width: (size ?? 25) + 10, height: (size ?? 25) + 10)
|
||||
.kfClickable()
|
||||
.foregroundColor(DamusColors.white)
|
||||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(.white, lineWidth: 4))
|
||||
} else {
|
||||
if setup ?? false {
|
||||
Image(systemName: "person")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(DamusColors.white)
|
||||
.padding(20)
|
||||
.clipShape(Circle())
|
||||
.background {
|
||||
Circle()
|
||||
.fill(PinkGradient, strokeBorder: .white, lineWidth: 4)
|
||||
}
|
||||
|
||||
} else {
|
||||
Image("camera")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(DamusColors.purple)
|
||||
.padding(10)
|
||||
.background(DamusColors.white.opacity(0.7))
|
||||
.clipShape(Circle())
|
||||
.background {
|
||||
Circle()
|
||||
.fill(DamusColors.purple, strokeBorder: .white, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_camera) {
|
||||
CameraController(uploader: uploader) {
|
||||
self.show_camera = false
|
||||
self.show_library = true
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_library) {
|
||||
MediaPicker(mediaPickerEntry: .editPictureControl, image_upload_confirm: $image_upload_confirm) { media in
|
||||
self.preUploadedMedia = media
|
||||
}
|
||||
.alert(NSLocalizedString("Are you sure you want to upload this image?", comment: "Alert message asking if the user wants to upload an image."), isPresented: $image_upload_confirm) {
|
||||
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
|
||||
if let mediaToUpload = generateMediaUpload(preUploadedMedia) {
|
||||
self.handle_upload(media: mediaToUpload)
|
||||
self.show_library = false
|
||||
}
|
||||
}
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_url_sheet) {
|
||||
ZStack {
|
||||
DamusColors.adaptableWhite.edgesIgnoringSafeArea(.all)
|
||||
VStack {
|
||||
Text("Image URL")
|
||||
.bold()
|
||||
|
||||
Divider()
|
||||
.padding(.horizontal)
|
||||
|
||||
HStack {
|
||||
Image(systemName: "doc.on.clipboard")
|
||||
.foregroundColor(.gray)
|
||||
.onTapGesture {
|
||||
if let pastedURL = UIPasteboard.general.string {
|
||||
image_url_temp = URL(string: pastedURL)
|
||||
}
|
||||
}
|
||||
TextField(image_url_temp?.absoluteString ?? "", text: Binding(
|
||||
get: { image_url_temp?.absoluteString ?? "" },
|
||||
set: { image_url_temp = URL(string: $0) }
|
||||
))
|
||||
}
|
||||
.padding(12)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.gray.opacity(0.5), lineWidth: 1)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(.damusAdaptableWhite)
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
|
||||
Button(action: {
|
||||
show_url_sheet.toggle()
|
||||
}, label: {
|
||||
Text("Cancel", comment: "Cancel button text for dismissing updating image url.")
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
.padding(10)
|
||||
})
|
||||
.buttonStyle(NeutralButtonStyle())
|
||||
.padding(10)
|
||||
|
||||
Button(action: {
|
||||
image_url = image_url_temp
|
||||
callback(image_url)
|
||||
show_url_sheet.toggle()
|
||||
}, label: {
|
||||
Text("Update", comment: "Update button text for updating image url.")
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
})
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
.padding(.horizontal, 10)
|
||||
.disabled(image_url_temp == image_url)
|
||||
.opacity(image_url_temp == image_url ? 0.5 : 1)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
image_url_temp = image_url
|
||||
}
|
||||
.presentationDetents([.height(300)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
|
||||
private func handle_upload(media: MediaUpload) {
|
||||
uploadObserver.isLoading = true
|
||||
Task {
|
||||
let res = await image_upload.start(media: media, uploader: uploader, keypair: keypair)
|
||||
|
||||
switch res {
|
||||
case .success(let urlString):
|
||||
let url = URL(string: urlString)
|
||||
image_url = url
|
||||
callback(url)
|
||||
case .failed(let error):
|
||||
if let error {
|
||||
print("Error uploading profile image \(error.localizedDescription)")
|
||||
} else {
|
||||
print("Error uploading image :(")
|
||||
}
|
||||
callback(nil)
|
||||
/// We show this on non-onboarding places such as profile edit page
|
||||
var default_view: some View {
|
||||
Group {
|
||||
switch self.model.state {
|
||||
case .uploading:
|
||||
self.progress_view
|
||||
default:
|
||||
Image("camera")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: style.size ?? 25, height: style.size ?? 25)
|
||||
.foregroundColor(DamusColors.purple)
|
||||
.padding(10)
|
||||
.background(DamusColors.white.opacity(0.7))
|
||||
.clipShape(Circle())
|
||||
.background {
|
||||
Circle()
|
||||
.fill(DamusColors.purple, strokeBorder: .white, lineWidth: 2)
|
||||
}
|
||||
.shadow(radius: 3)
|
||||
}
|
||||
uploadObserver.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
/// We show this on onboarding
|
||||
var first_time_setup_view: some View {
|
||||
Group {
|
||||
switch self.model.state {
|
||||
case .uploading:
|
||||
self.progress_view
|
||||
default:
|
||||
if let url = current_image_url {
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.pfp, disable_animation: false)
|
||||
.onFailure(fallbackUrl: URL(string: robohash(model.pubkey)), cacheKey: url.absoluteString)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.scaledToFill()
|
||||
.frame(width: (style.size ?? 25) + 30, height: (style.size ?? 25) + 30)
|
||||
.kfClickable()
|
||||
.foregroundColor(DamusColors.white)
|
||||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(.white, lineWidth: 4))
|
||||
}
|
||||
else {
|
||||
self.first_time_setup_no_image_view
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We show this on onboarding before the user enters any image
|
||||
var first_time_setup_no_image_view: some View {
|
||||
Image(systemName: "person.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: style.size, height: style.size)
|
||||
.foregroundColor(DamusColors.white)
|
||||
.padding(20)
|
||||
.clipShape(Circle())
|
||||
.background {
|
||||
Circle()
|
||||
.fill(PinkGradient, strokeBorder: .white, lineWidth: 4)
|
||||
}
|
||||
.overlay(
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.resizable()
|
||||
.frame(
|
||||
width: max((style.size ?? 30)/3, 20),
|
||||
height: max((style.size ?? 30)/3, 20)
|
||||
)
|
||||
.background(.damusDeepPurple)
|
||||
.clipShape(Circle())
|
||||
.padding(.leading, -10)
|
||||
.padding(.top, -10)
|
||||
.foregroundStyle(.white)
|
||||
.shadow(color: .black.opacity(0.2), radius: 4)
|
||||
, alignment: .bottomTrailing
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
var crop_configuration: SwiftyCropConfiguration = SwiftyCropConfiguration(rotateImage: false, zoomSensitivity: 5)
|
||||
|
||||
var image_cropper: some View {
|
||||
Group {
|
||||
if case .cropping(let preUploadedMedia) = model.state {
|
||||
switch preUploadedMedia {
|
||||
case .uiimage(let image):
|
||||
SwiftyCropView(
|
||||
imageToCrop: image,
|
||||
maskShape: .circle
|
||||
) { croppedImage in
|
||||
self.model.finished_cropping(croppedImage: croppedImage)
|
||||
}
|
||||
case .unprocessed_image(let url), .processed_image(let url):
|
||||
if let image = try? UIImage.from(url: url) {
|
||||
SwiftyCropView(
|
||||
imageToCrop: image,
|
||||
maskShape: .circle,
|
||||
configuration: crop_configuration
|
||||
) { croppedImage in
|
||||
self.model.finished_cropping(croppedImage: croppedImage)
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.cropping_error_screen // Cannot load image
|
||||
}
|
||||
case .unprocessed_video(_), .processed_video(_):
|
||||
self.cropping_error_screen // No support for video profile pictures
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.cropping_error_screen // Some form of internal logical inconsistency
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cropping_error_screen: some View {
|
||||
VStack(spacing: 5) {
|
||||
Text("Error while cropping image", comment: "Heading on cropping error page")
|
||||
.font(.headline)
|
||||
Text("Sorry, but for some reason there has been an issue while trying to crop this image. Please try again later. If the error persists, please contact [Damus support](mailto:support@damus.io)", comment: "Cropping error message")
|
||||
Button(action: { self.model.cancel() }, label: {
|
||||
Text("Dismiss", comment: "Button to dismiss error")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Accesibility helpers
|
||||
|
||||
var accessibility_label: String {
|
||||
switch self.model.context {
|
||||
case .normal:
|
||||
return NSLocalizedString("Edit Image", comment: "Accessibility label for a button that edits an image")
|
||||
case .profile_picture:
|
||||
return NSLocalizedString("Edit profile picture", comment: "Accessibility label for a button that edits a profile picture")
|
||||
}
|
||||
}
|
||||
|
||||
var accessibility_hint: String {
|
||||
return NSLocalizedString("Shows options to edit the image", comment: "Accessibility hint for a button that edits an image")
|
||||
}
|
||||
|
||||
var accessibility_value: String? {
|
||||
if style.first_time_setup {
|
||||
if let current_image_url = model.current_image_url {
|
||||
switch self.model.context {
|
||||
case .normal:
|
||||
return NSLocalizedString("Image is setup", comment: "Accessibility value on image control")
|
||||
case .profile_picture:
|
||||
return NSLocalizedString("Profile picture is setup", comment: "Accessibility value on profile picture image control")
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch self.model.context {
|
||||
case .normal:
|
||||
return NSLocalizedString("No image is currently setup", comment: "Accessibility value on image control")
|
||||
case .profile_picture:
|
||||
return NSLocalizedString("No profile picture is currently setup", comment: "Accessibility value on profile picture image control")
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return nil // Image is shown outside this control and will have its accessibility defined outside this view.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - View model
|
||||
|
||||
|
||||
/// Tracks the state, and provides the logic needed for the EditPictureControl view
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This makes it easier to test the logic as well as the view, and makes the view easier to work with by separating concerns.
|
||||
@MainActor
|
||||
class EditPictureControlViewModel<T: ImageUploadModelProtocol>: ObservableObject {
|
||||
// MARK: Properties
|
||||
// Properties are designed to reduce statefulness and hopefully increase predictability.
|
||||
|
||||
/// The context of the upload. Is it a profile picture? A regular picture?
|
||||
let context: Context
|
||||
/// Pubkey of the user
|
||||
let pubkey: Pubkey
|
||||
/// The currently loaded image URL
|
||||
@Binding var current_image_url: URL?
|
||||
/// The state of the picture selection process
|
||||
@Published private(set) var state: PictureSelectionState
|
||||
/// User's keypair
|
||||
let keypair: Keypair?
|
||||
/// The uploader service to be used when uploading
|
||||
let uploader: any MediaUploaderProtocol
|
||||
/// An image upload observer, that can be set when the parent view wants to keep track of the upload process
|
||||
let image_upload_observer: ImageUploadingObserver?
|
||||
/// A callback to receive new image urls once the picture selection and upload is complete.
|
||||
let callback: (URL?) -> Void
|
||||
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// The desired profile image size
|
||||
var profile_image_size: CGSize = CGSize(width: 400, height: 400)
|
||||
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
init(
|
||||
context: Context,
|
||||
pubkey: Pubkey,
|
||||
setup: Bool? = nil,
|
||||
current_image_url: Binding<URL?>,
|
||||
state: PictureSelectionState = .ready,
|
||||
keypair: Keypair?,
|
||||
uploader: any MediaUploaderProtocol,
|
||||
image_upload_observer: ImageUploadingObserver? = nil,
|
||||
callback: @escaping (URL?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.pubkey = pubkey
|
||||
self._current_image_url = current_image_url
|
||||
self.state = state
|
||||
self.keypair = keypair
|
||||
self.uploader = uploader
|
||||
self.image_upload_observer = image_upload_observer
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
|
||||
// MARK: Convenience bindings to be used in views
|
||||
|
||||
var show_camera: Binding<Bool> {
|
||||
Binding(
|
||||
get: { self.state.show_camera },
|
||||
set: { newShowCamera in
|
||||
switch self.state {
|
||||
case .selecting_picture_from_camera:
|
||||
self.state = newShowCamera ? .selecting_picture_from_camera : .ready
|
||||
default:
|
||||
if newShowCamera == true { self.state = .selecting_picture_from_camera }
|
||||
else { return } // Leave state as-is
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var show_library: Binding<Bool> {
|
||||
Binding(
|
||||
get: { self.state.show_library },
|
||||
set: { newValue in
|
||||
switch self.state {
|
||||
case .selecting_picture_from_library:
|
||||
self.state = newValue ? .selecting_picture_from_library : .ready
|
||||
default:
|
||||
if newValue == true { self.state = .selecting_picture_from_library }
|
||||
else { return } // Leave state as-is
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var show_url_sheet: Binding<Bool> {
|
||||
Binding(
|
||||
get: { self.state.show_url_sheet },
|
||||
set: { newValue in self.state = newValue ? .selecting_picture_from_url : .ready }
|
||||
)
|
||||
}
|
||||
|
||||
var show_image_cropper: Binding<Bool> {
|
||||
Binding(
|
||||
get: { self.state.show_image_cropper },
|
||||
set: { newValue in
|
||||
switch self.state {
|
||||
case .cropping(let media):
|
||||
self.state = newValue ? .cropping(media) : .ready
|
||||
default:
|
||||
return // Leave state as-is
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fileprivate var error_message: Binding<IdentifiableString?> {
|
||||
Binding(
|
||||
get: { IdentifiableString(text: self.state.error_message) },
|
||||
set: { newValue in
|
||||
if let newValue {
|
||||
self.state = .failed(message: newValue.rawValue)
|
||||
}
|
||||
else {
|
||||
self.state = .ready
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Control methods
|
||||
// These are methods to be used by the view or a test program to represent user actions.
|
||||
|
||||
/// Ask user if they are sure they want to upload an image
|
||||
func request_upload_authorization(_ media: PreUploadedMedia) {
|
||||
self.state = .confirming_upload(media)
|
||||
}
|
||||
|
||||
/// Confirm on behalf of the user that we have their permission to upload image
|
||||
func confirm_upload_authorization() {
|
||||
guard case .confirming_upload(let preUploadedMedia) = state else {
|
||||
return
|
||||
}
|
||||
switch self.context {
|
||||
case .normal:
|
||||
self.upload(media: preUploadedMedia)
|
||||
case .profile_picture:
|
||||
self.state = .cropping(preUploadedMedia)
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicate the image has finished being cropped. This will resize the image and upload it
|
||||
func finished_cropping(croppedImage: UIImage?) {
|
||||
guard let croppedImage else { return }
|
||||
let resizedCroppedImage = croppedImage.resized(to: profile_image_size)
|
||||
let newPreUploadedMedia: PreUploadedMedia = .uiimage(resizedCroppedImage)
|
||||
self.upload(media: newPreUploadedMedia)
|
||||
}
|
||||
|
||||
/// Upload the media
|
||||
func upload(media: PreUploadedMedia) {
|
||||
if let mediaToUpload = generateMediaUpload(media) {
|
||||
self.handle_upload(media: mediaToUpload)
|
||||
}
|
||||
else {
|
||||
self.state = .failed(message: NSLocalizedString("Failed to generate media for upload. Please try again. If error persists, please contact Damus support at support@damus.io", comment: "Error label forming media for upload after user crops the image."))
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the picture selection process
|
||||
func cancel() {
|
||||
self.state = .ready
|
||||
}
|
||||
|
||||
/// Mark the picture selection process as failed
|
||||
func failed(message: String) {
|
||||
self.state = .failed(message: message)
|
||||
}
|
||||
|
||||
/// Choose an image based on a URL
|
||||
func choose_url(_ url: URL?) {
|
||||
self.current_image_url = url
|
||||
callback(url)
|
||||
self.state = .ready
|
||||
}
|
||||
|
||||
/// Select an image from the gallery
|
||||
func select_image_from_library() {
|
||||
self.state = .selecting_picture_from_library
|
||||
}
|
||||
|
||||
/// Select an image by taking a photo
|
||||
func select_image_from_camera() {
|
||||
self.state = .selecting_picture_from_camera
|
||||
}
|
||||
|
||||
/// Select an image by specifying a URL
|
||||
func select_image_from_url() {
|
||||
self.state = .selecting_picture_from_url
|
||||
}
|
||||
|
||||
|
||||
// MARK: Internal logic
|
||||
|
||||
/// Handles the upload process
|
||||
private func handle_upload(media: MediaUpload) {
|
||||
let image_upload = T()
|
||||
let upload_observer = ImageUploadingObserver()
|
||||
self.state = .uploading(media: media, upload: image_upload, uploadObserver: upload_observer)
|
||||
upload_observer.isLoading = true
|
||||
Task {
|
||||
let res = await image_upload.start(media: media, uploader: uploader, mediaType: self.context.mediaType, keypair: keypair)
|
||||
|
||||
switch res {
|
||||
case .success(let urlString):
|
||||
let url = URL(string: urlString)
|
||||
current_image_url = url
|
||||
self.state = .ready
|
||||
callback(url)
|
||||
case .failed(let error):
|
||||
if let error {
|
||||
Log.info("Error uploading profile image with error: %@", for: .image_uploading, error.localizedDescription)
|
||||
} else {
|
||||
Log.info("Failed to upload profile image without error", for: .image_uploading)
|
||||
}
|
||||
self.state = .failed(message: NSLocalizedString("Error uploading profile image. Please check your internet connection and try again. If error persists, please contact Damus support (support@damus.io).", comment: "Error label when uploading profile image"))
|
||||
}
|
||||
upload_observer.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper views
|
||||
|
||||
/// A view that can be used for inputting a URL.
|
||||
struct ImageURLSelector: View {
|
||||
@State var image_url_temp: String = ""
|
||||
@State var error: String? = nil
|
||||
@State var image_url: URL? = nil
|
||||
let callback: (URL?) -> Void
|
||||
let cancel: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
DamusColors.adaptableWhite.edgesIgnoringSafeArea(.all)
|
||||
VStack {
|
||||
Text("Image URL", comment: "Label for image url text field")
|
||||
.bold()
|
||||
|
||||
Divider()
|
||||
.padding(.horizontal)
|
||||
|
||||
HStack {
|
||||
Image(systemName: "doc.on.clipboard")
|
||||
.foregroundColor(.gray)
|
||||
.onTapGesture {
|
||||
if let pastedURL = UIPasteboard.general.string {
|
||||
image_url_temp = URL(string: pastedURL)?.absoluteString ?? ""
|
||||
}
|
||||
}
|
||||
TextField(image_url_temp, text: $image_url_temp)
|
||||
}
|
||||
.padding(12)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.gray.opacity(0.5), lineWidth: 1)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(.damusAdaptableWhite)
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
|
||||
if let error {
|
||||
Text(error)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
self.cancel()
|
||||
}, label: {
|
||||
Text("Cancel", comment: "Cancel button text for dismissing updating image url.")
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
.padding(10)
|
||||
})
|
||||
.buttonStyle(NeutralButtonStyle())
|
||||
.padding(10)
|
||||
|
||||
Button(action: {
|
||||
guard let the_url = URL(string: image_url_temp) else {
|
||||
error = NSLocalizedString("Invalid URL", comment: "Error label when user enters an invalid URL")
|
||||
return
|
||||
}
|
||||
image_url = the_url
|
||||
callback(the_url)
|
||||
}, label: {
|
||||
Text("Update", comment: "Update button text for updating image url.")
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
})
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
.padding(.horizontal, 10)
|
||||
.disabled(image_url_temp == image_url?.absoluteString)
|
||||
.opacity(image_url_temp == image_url?.absoluteString ? 0.5 : 1)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
image_url_temp = image_url?.absoluteString ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper structures
|
||||
|
||||
extension EditPictureControlViewModel {
|
||||
/// Tracks the state of the picture selection process in the picture control view and provides convenient computed properties for the view
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// Made as an enum with associated values to reduce the amount of independent variables in the view model, and enforce the presence of certain values in certain steps of the process.
|
||||
enum PictureSelectionState {
|
||||
case ready
|
||||
case selecting_picture_from_library
|
||||
case selecting_picture_from_url
|
||||
case selecting_picture_from_camera
|
||||
case confirming_upload(PreUploadedMedia)
|
||||
case cropping(PreUploadedMedia)
|
||||
case uploading(media: MediaUpload, upload: any ImageUploadModelProtocol, uploadObserver: ImageUploadingObserver)
|
||||
case failed(message: String)
|
||||
|
||||
// MARK: Convenience computed properties
|
||||
// Translates the information in the state, in a way that does not introduce further statefulness
|
||||
|
||||
var is_confirming_upload: Bool { self.step == .confirming_upload }
|
||||
var show_image_cropper: Bool { self.step == .cropping }
|
||||
var show_library: Bool { self.step == .selecting_picture_from_library }
|
||||
var show_camera: Bool { self.step == .selecting_picture_from_camera }
|
||||
var show_url_sheet: Bool { self.step == .selecting_picture_from_url }
|
||||
var is_uploading: Bool { self.step == .uploading }
|
||||
var error_message: String? { if case .failed(let message) = self { return message } else { return nil } }
|
||||
var step: Step {
|
||||
switch self {
|
||||
case .ready: .ready
|
||||
case .selecting_picture_from_library: .selecting_picture_from_library
|
||||
case .selecting_picture_from_url: .selecting_picture_from_url
|
||||
case .selecting_picture_from_camera: .selecting_picture_from_camera
|
||||
case .confirming_upload(_): .confirming_upload
|
||||
case .cropping(_): .cropping
|
||||
case .uploading(_,_,_): .uploading
|
||||
case .failed(_): .failed
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the specific step of the picture selection state, without any associated values, to make easy comparisons on where in the process we are
|
||||
enum Step: String, RawRepresentable, Equatable {
|
||||
case ready
|
||||
case selecting_picture_from_library
|
||||
case selecting_picture_from_url
|
||||
case selecting_picture_from_camera
|
||||
case confirming_upload
|
||||
case cropping
|
||||
case uploading
|
||||
case failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EditPictureControlViewModel {
|
||||
/// Defines the context of this picture. Is it a profile picture? A normal picture?
|
||||
enum Context {
|
||||
case normal
|
||||
case profile_picture
|
||||
|
||||
var mediaType: ImageUploadMediaType {
|
||||
switch self {
|
||||
case .normal: .normal
|
||||
case .profile_picture: .profile_picture
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An object that can be used for tracking the status of an upload across the view hierarchy.
|
||||
/// For example, a parent view can instantiate this object and pass it to a child view that handles uploads,
|
||||
/// and that parent view can change its own style accordingly
|
||||
///
|
||||
/// ## Implementation note:
|
||||
///
|
||||
/// It would be correct to put this entire class in the MainActor, but for some reason adding `@MainActor` crashes the Swift compiler with no helpful messages (on Xcode 16.2 (16C5032a)), so individual members of this class need to be manually put into the main actor.
|
||||
//@MainActor
|
||||
class ImageUploadingObserver: ObservableObject {
|
||||
@MainActor @Published var isLoading: Bool = false
|
||||
}
|
||||
|
||||
fileprivate struct IdentifiableString: Identifiable, RawRepresentable {
|
||||
var id: String { return rawValue }
|
||||
typealias RawValue = String
|
||||
var rawValue: String
|
||||
|
||||
init?(rawValue: String) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
init?(text: String?) {
|
||||
guard let text else { return nil }
|
||||
self.rawValue = text
|
||||
}
|
||||
}
|
||||
|
||||
extension EditPictureControl {
|
||||
struct Style {
|
||||
let size: CGFloat?
|
||||
let first_time_setup: Bool
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience extensions
|
||||
|
||||
fileprivate extension UIImage {
|
||||
/// Convenience function to easily get an UIImage from a URL
|
||||
static func from(url: URL) throws -> UIImage? {
|
||||
let data = try Data(contentsOf: url)
|
||||
return UIImage(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension View {
|
||||
func maybeAccessibilityValue(_ value: String?) -> some View {
|
||||
Group {
|
||||
if let value { self.accessibilityValue(value) } else { self }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct EditPictureControl_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let url = Binding<URL?>.constant(URL(string: "https://damus.io")!)
|
||||
let observer = ImageUploadingObserver()
|
||||
ZStack {
|
||||
Color.gray
|
||||
EditPictureControl(uploader: .nostrBuild, keypair: test_keypair, pubkey: test_pubkey, size: 100, setup: false, image_url: url, uploadObserver: observer) { _ in
|
||||
EditPictureControl(uploader: MediaUploader.nostrBuild, context: .profile_picture, keypair: test_keypair, pubkey: test_pubkey, style: .init(size: 100, first_time_setup: false), current_image_url: url) { _ in
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,15 @@ struct EditProfilePictureView: View {
|
||||
.scaledToFill()
|
||||
.kfClickable()
|
||||
|
||||
EditPictureControl(uploader: damus_state?.settings.default_media_uploader ?? .nostrBuild, keypair: damus_state?.keypair, pubkey: pubkey, image_url: $profile_url, uploadObserver: uploadObserver, callback: callback)
|
||||
EditPictureControl(
|
||||
uploader: damus_state?.settings.default_media_uploader ?? .nostrBuild,
|
||||
context: .profile_picture,
|
||||
keypair: damus_state?.keypair,
|
||||
pubkey: pubkey,
|
||||
current_image_url: $profile_url,
|
||||
upload_observer: uploadObserver,
|
||||
callback: callback
|
||||
)
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(Circle())
|
||||
|
||||
@@ -20,8 +20,6 @@ struct SearchHomeView: View {
|
||||
return ContentFilters(filters: filters).filter
|
||||
}
|
||||
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
var SearchInput: some View {
|
||||
HStack {
|
||||
HStack{
|
||||
@@ -64,17 +62,7 @@ struct SearchHomeView: View {
|
||||
return false
|
||||
}
|
||||
|
||||
if damus_state.settings.show_only_preferred_languages == false {
|
||||
return true
|
||||
}
|
||||
|
||||
// If we can't determine the note's language with 50%+ confidence, lean on the side of caution and show it anyway.
|
||||
let note_lang = damus_state.events.get_cache_data(ev.id).translations_model.note_language
|
||||
guard let note_lang else {
|
||||
return true
|
||||
}
|
||||
|
||||
return preferredLanguages.contains(note_lang)
|
||||
return true
|
||||
},
|
||||
content: {
|
||||
AnyView(VStack {
|
||||
|
||||
@@ -70,7 +70,7 @@ struct InnerSearchResults: View {
|
||||
func TextSearch(_ txt: String) -> some View {
|
||||
return NavigationLink(value: Route.NDBSearch(results: $results)) {
|
||||
HStack {
|
||||
Text(txt)
|
||||
Text("Search word: \(txt)", comment: "Navigation link to search for a word.")
|
||||
}
|
||||
.padding(.horizontal, 15)
|
||||
.padding(.vertical, 5)
|
||||
|
||||
@@ -16,9 +16,6 @@ struct TranslationSettingsView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
|
||||
Toggle(NSLocalizedString("Show only preferred languages on Universe feed", comment: "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."), isOn: $settings.show_only_preferred_languages)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
|
||||
ForEach(TranslationService.allCases.filter({ damus_state.purple.enable_purple ? true : $0 != .purple }), id: \.self) { server in
|
||||
Text(server.model.displayName)
|
||||
|
||||
@@ -96,16 +96,9 @@ struct ConnectWalletView: View {
|
||||
openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!)
|
||||
}
|
||||
|
||||
//
|
||||
// Mutiny Wallet NWC is way too advanced to recommend for normal
|
||||
// users until they have a way to do async receive.
|
||||
//
|
||||
|
||||
/*
|
||||
MutinyButton() {
|
||||
openURL(URL(string:"https://app.mutinywallet.com/settings/connections?callbackUri=nostr%2bwalletconnect&name=Damus")!)
|
||||
CoinosButton() {
|
||||
openURL(URL(string:"https://coinos.io/settings/nostr")!)
|
||||
}
|
||||
*/
|
||||
|
||||
Button(action: {
|
||||
if let pasted_nwc = UIPasteboard.general.string {
|
||||
|
||||