Compare commits

...

21 Commits

Author SHA1 Message Date
847f31f5a6 Fix reactions view to not show reactions from replies on parent note
Changelog-Fixed: Fix reactions view to not show reactions from replies on parent note
2024-07-06 19:07:33 -04:00
Daniel D’Aquino
fd130b78e7 Merge pull request #2308 from ericholguin/simplify-onboarding
ux: Simplify Onboarding
2024-07-05 12:04:59 -07:00
Daniel D’Aquino
0be0273121 Update push notification device token address
This commit sets up the correct server address to send device token
notifications to.

Testing
-------

PASS

Device: iPhone 13 Mini
iOS: 17.5
Damus: This commit
strfry-push-notify: 6c52129ab52f37f6686b1a3d1d0d8b478de9e60f
Setup:
- strfry-push-notify and notification device token server setup on the real damus server
- APNS environment setup to development on the server (temporarily)
- Developer settings turned on
- Experimental push notifications support turned ON
- "Send device tokens to localhost" setting turned OFF
- Notification mode in notification settings set to PUSH notifications
Steps:
1. Get a simulator up and running and connected to the Damus relay
2. Send a DM to the main device under test.
3. Check if push notification arrives even with Damus closed. PASS

Closes: https://github.com/damus-io/damus/issues/1733
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
2024-07-03 11:08:13 -07:00
Daniel D’Aquino
b349de22b7 Merge changes from 'release_1.9' 2024-07-01 11:20:30 -07:00
Daniel D’Aquino
cc2d196705 Merge pull request #2310 from tyiu/mute-user-bug
Fix missing Mute button in profile view menu
2024-07-01 11:16:33 -07:00
Daniel D’Aquino
53be29efc2 Fix build error on the test target
This commit is a trivial fix for a build error on the test target

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2024-06-24 15:25:35 -07:00
Daniel D’Aquino
529ee63f29 Merge pull request #2295 from tyiu/change-emoji-component
Revamp emoji picker to be less error-prone and add search, frequently used, and multiple skin tone support capabilities
2024-06-24 15:23:27 -07:00
Daniel D’Aquino
490e8ec1fb Fix build error on the test target
This commit is a trivial fix for a build error on the test target

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2024-06-24 14:38:56 -07:00
Daniel D’Aquino
df267ffd04 Version bump to 1.10 (1) 2024-06-24 11:51:17 -07:00
Daniel D’Aquino
b771e8f49a Merge pull request #2295 from tyiu/change-emoji-component
Revamp emoji picker to be less error-prone and add search, frequently used, and multiple skin tone support capabilities
2024-06-24 11:38:31 -07:00
Daniel D’Aquino
a88e80a346 Merge pull request #2307 from damus-io/review_highlights_2024-06-19_rebased
Highlights (rebased to solve merge conflicts + minor tweaks)
2024-06-24 11:30:59 -07:00
8ac9863765 Fix missing Mute button in profile view menu
Changelog-Fixed: Fix missing Mute button in profile view menu
2024-06-23 22:40:33 -04:00
ericholguin
4a851501a1 ux: Simplify Onboarding
This patch simplifies the onboarding flow based on Jeroen's suggestions.

Setup view:
  - Removes extra nostr information
  - Only shows two buttons, create account and sign in.

Create Account view:
  - When a user uploads a photo it is now displayed
  - Name is now required
  - Public key is now hidden
  - Create account model has been updated to match metadata

Save Keys view:
  - Removes the requirement to copy the nsec
  - Simplified explanation
  - Only shows two buttons, save and not now

Testing
——
iPhone 15 Pro Max (17.0) Light Mode:
https://v.nostr.build/3P75x.mp4

iPhone SE (3rd generation) (16.4) Dark Mode:
https://v.nostr.build/wGBQL.mp4

——

Changelog-Fixed: Create Account model now uses correct metadata
Changelog-Changed: Onboarding design
2024-06-22 11:46:53 -06:00
Daniel D’Aquino
4ccfe81558 Allow highlighting to be disabled on SelectableText
Changed the interface of SelectableText to allow highlighting to be
disabled in places where it is not applicable (For example, on the
AboutView).

This prevents the need for adding dummy events in places where
highlighting is not applicable, preventing the user from making bad
highlights.

Testing
-------

PASS

Device: iPhone 13 mini
iOS: 17.5
Damus: This version
Steps:
1. Go to a user profile and select some text in their bio. The "highlight" option should not be present.
2. Go to a note and select some text. The "highlight" option should be available
2024-06-21 14:18:36 -07:00
Daniel D’Aquino
e7ed9dfe86 Small tweaks for better code safety 2024-06-21 14:11:45 -07:00
ericholguin
0dce7aea45 ux: Create Highlights
This patch allows users to create a highlight in Damus.
This is done by modifying the menu options when text is selected, including a custom highlight option.
This option presents a sheet to the user of what they are highlighting with a cancel or post button.
If they press Post the sheet will dismiss and their highlight will be posted.

Testing
——
iPhone 15 Pro Max (17.3.1) Dark Mode:
https://v.nostr.build/wGDnx.mp4

iPhone SE (3rd generation) (16.4) Light Mode:
https://v.nostr.build/xEK0e.mp4

——

Changelog-Added: Ability to create highlights

Signed-off-by: ericholguin <ericholguin@apache.org>
2024-06-21 12:00:44 -07:00
ericholguin
6376c61bad Highlights
This patch adds highlights (NIP-84) to Damus.

Kind 9802 are handled by all the necessary models.
We show highlighted events, longform events, and url references.
Url references also leverage text fragments to take the user to the highlighted text.

Testing
——
iPhone 15 Pro Max (17.0) Dark Mode:
https://v.nostr.build/oM6DW.mp4

iPhone 15 Pro Max (17.0) Light Mode:
https://v.nostr.build/BRrmP.mp4

iPhone SE (3rd generation) (16.4) Light Mode:
https://v.nostr.build/6GzKa.mp4
——

Closes: https://github.com/damus-io/damus/issues/2172
Closes: https://github.com/damus-io/damus/issues/1772
Closes: https://github.com/damus-io/damus/issues/1773
Closes: https://github.com/damus-io/damus/issues/2173
Closes: https://github.com/damus-io/damus/issues/2175
Changelog-Added: Highlights (NIP-84)

PATCH CHANGELOG:
V1 -> V2: addressed review comments highlights are now truncated and highlight label shown in Thread view
V2 -> V3: handle case where highlight context is smaller than the highlight content

Signed-off-by: ericholguin <ericholguin@apache.org>
2024-06-21 12:00:44 -07:00
bdd1403a7d Merge remote-tracking branch 'upstream/master' into change-emoji-component 2024-06-19 18:33:58 -04:00
Daniel D’Aquino
23c3130a82 New chat thread view
This commit changes the thread view to a new UX concept where children views of the selected view are now presented as chat bubbles, and the entire tree of conversation is shown flattened. New interactions, layout, and design changes have been introduced to revamp the user experience.

Testing
-------

Device: A mix of iPhone physical devices and simulator
iOS: A mix of iOS 17 versions
Damus: A mix of versions leading up to this one.
Coverage:
1. Unit tests are passing
2. A select few users have been using prototypes versions of this as their daily driver
3. Layout tested with an eclectic mix of threads
4. Posting new notes to the thread works
5. Clicking on reply quote view takes user to the mentioned message with a momentary visible highlight
6. Swipe actions work
7. Long press on chat bubbles works and shows emoji selector. Adding emoji sends the reaction
8. Clicking on notes selects them with an easy to follow transition

Known issues:
1. The text on the reply quote view occasionally appears to be off-center (in about 10% of occurrences). The cause is still unknown
2. Long press will still show the emoji keyboard even if user is on "onlyzaps" mode
3. Quoted events are not rendered on chat bubbles. When user posts a quoted event with no text, that could lead to confusion

Closes: https://github.com/damus-io/damus/issues/1126
Changelog-Added: Completely new threads experience that is easier and more pleasant to use
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2024-06-19 12:33:15 -07:00
9172102f4d Revamp emoji picker to be less error-prone and add search, frequently used, and multiple skin tone support capabilities
Changelog-Added: Revamp emoji picker to be less error-prone and add search, frequently used, and multiple skin tone support capabilities
2024-06-17 23:53:19 -04:00
ericholguin
8bcd8317f1 Fix persistent wallet on log out
This patch simply disconects the wallet connection when a user logs out.

Changelog-Fixed: Fixed wallet not disconnecting when a user logs out

Signed-off-by: ericholguin <ericholguin@apache.org>
Signed-off-by: William Casarin <jb55@jb55.com>
2024-06-10 05:34:30 -07:00
56 changed files with 2381 additions and 468 deletions

View File

@@ -12,6 +12,7 @@
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; };
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
@@ -32,8 +33,10 @@
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */; };
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; };
4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */; };
4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */; };
4C011B612BD0B25C002F2F9B /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */; };
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
@@ -396,6 +399,7 @@
5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; };
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */; };
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
@@ -403,6 +407,11 @@
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 */; };
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 */; };
5CC852A42BDF3CA10039FFC5 /* HighlightLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */; };
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */; };
5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */; };
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */; };
5CF2DCCE2AABE1A500984B8D /* DamusLightGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */; };
@@ -494,6 +503,9 @@
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = D789D11F2AFEFBF20083A7AB /* secp256k1 */; };
D78CD5982B8990300014D539 /* DamusAppNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */; };
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; };
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */; };
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */; };
D798D21A2B0856CC00234419 /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
D798D21B2B0856F200234419 /* NdbTagsIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDD1AE12A6B3074001CD4DF /* NdbTagsIterator.swift */; };
D798D21C2B0857E400234419 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
@@ -822,6 +834,9 @@
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nip98HTTPAuth.swift; sourceTree = "<group>"; };
4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEventView.swift; sourceTree = "<group>"; };
4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatroomThreadView.swift; sourceTree = "<group>"; };
4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; };
4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -1325,6 +1340,7 @@
5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = "<group>"; };
5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayNipList.swift; sourceTree = "<group>"; };
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightPostView.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
@@ -1332,6 +1348,11 @@
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>"; };
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>"; };
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightLink.swift; sourceTree = "<group>"; };
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEventRef.swift; sourceTree = "<group>"; };
5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeutralButtonStyle.swift; sourceTree = "<group>"; };
5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicView.swift; sourceTree = "<group>"; };
5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLightGradient.swift; sourceTree = "<group>"; };
@@ -1410,6 +1431,8 @@
D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = "<group>"; };
D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; };
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorMath.swift; sourceTree = "<group>"; };
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = "<group>"; };
D798D21D2B0858BB00234419 /* MigratedTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedTypes.swift; sourceTree = "<group>"; };
D798D2272B085CDA00234419 /* NdbNote+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NdbNote+.swift"; sourceTree = "<group>"; };
D798D22B2B086C7400234419 /* NostrEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrEvent+.swift"; sourceTree = "<group>"; };
@@ -1474,9 +1497,10 @@
buildActionMask = 2147483647;
files = (
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */,
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */,
3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1656,6 +1680,7 @@
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
B533694D2B66D791008A805E /* MutelistManager.swift */,
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -1996,6 +2021,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
D78DB85D2C20FE9E00F0AB12 /* Chat */,
D71AC4CA2BA8E3320076268E /* Extensions */,
BA3759952ABCCF360018D73B /* Camera */,
F71694E82A66221E001F4053 /* Onboarding */,
@@ -2389,6 +2415,7 @@
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
5CC852A02BDED9970039FFC5 /* Highlight */,
4CA927682A290F8F0098A105 /* Components */,
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
@@ -2686,12 +2713,25 @@
path = Images;
sourceTree = "<group>";
};
5CC852A02BDED9970039FFC5 /* Highlight */ = {
isa = PBXGroup;
children = (
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */,
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */,
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */,
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */,
5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */,
);
path = Highlight;
sourceTree = "<group>";
};
7C0F392D29B57C8F0039859C /* Extensions */ = {
isa = PBXGroup;
children = (
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4C7D09752A0AF19E00943473 /* FillAndStroke.swift */,
D72E12772BEED22400F4F781 /* Array.swift */,
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -2760,6 +2800,17 @@
path = Purple;
sourceTree = "<group>";
};
D78DB85D2C20FE9E00F0AB12 /* Chat */ = {
isa = PBXGroup;
children = (
4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */,
4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */,
4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */,
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */,
);
path = Chat;
sourceTree = "<group>";
};
D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = {
isa = PBXGroup;
children = (
@@ -2841,7 +2892,8 @@
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
4C27C9312A64766F007DBC75 /* MarkdownUI */,
3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */,
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */,
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -2981,7 +3033,8 @@
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */,
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */,
3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */,
3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */,
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -3126,6 +3179,7 @@
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */,
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.swift in Sources */,
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */,
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */,
@@ -3156,6 +3210,7 @@
4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
5CC852A42BDF3CA10039FFC5 /* HighlightLink.swift in Sources */,
4C32B9552A9AD44700DC3548 /* ByteBuffer.swift in Sources */,
4C32B95B2A9AD44700DC3548 /* NativeObject.swift in Sources */,
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
@@ -3235,6 +3290,7 @@
E02429952B7E97740088B16C /* CameraController.swift in Sources */,
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */,
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */,
D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */,
50A16FFF2AA76A0900DFEC1F /* VideoController.swift in Sources */,
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
@@ -3243,6 +3299,7 @@
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
4CC14FEF2A73FCCB007AEB17 /* IdType.swift in Sources */,
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
4C011B612BD0B25C002F2F9B /* ReplyQuoteView.swift in Sources */,
D71AC4CC2BA8E3480076268E /* VisibilityTracker.swift in Sources */,
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
@@ -3293,6 +3350,7 @@
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */,
4C94D6432BA5AEFE00C26EFF /* QuoteRepostsView.swift in Sources */,
D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */,
4CA352AE2A76C1AC003BB08B /* FollowedNotify.swift in Sources */,
@@ -3331,11 +3389,14 @@
4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */,
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */,
4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */,
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */,
4C9146FD2A2A87C200DDEA40 /* wasm.c in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
@@ -3390,6 +3451,7 @@
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */,
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */,
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */,
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
@@ -3446,6 +3508,7 @@
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
@@ -3499,6 +3562,7 @@
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */,
4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */,
4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */,
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
@@ -4005,6 +4069,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -4031,6 +4096,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.10;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -4054,6 +4120,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -4080,6 +4147,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.10;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -4284,12 +4352,12 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */ = {
3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/izyumkin/MCEmojiPicker";
repositoryURL = "https://github.com/tyiu/EmojiPicker.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.3;
minimumVersion = 0.1.1;
};
};
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
@@ -4324,6 +4392,14 @@
minimumVersion = 0.2.26;
};
};
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/aheze/SwipeActions";
requirement = {
kind = exactVersion;
version = 1.1.0;
};
};
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing";
@@ -4335,10 +4411,10 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */ = {
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */ = {
isa = XCSwiftPackageProductDependency;
package = 3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */;
productName = MCEmojiPicker;
package = 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */;
productName = EmojiPicker;
};
4C06670328FC7EC500038D2A /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
@@ -4360,6 +4436,11 @@
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
productName = secp256k1;
};
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */ = {
isa = XCSwiftPackageProductDependency;
package = D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */;
productName = SwipeActions;
};
D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */ = {
isa = XCSwiftPackageProductDependency;
package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;

View File

@@ -1,5 +1,24 @@
{
"originHash" : "babaf4d5748afecf49bbb702530d8e9576460692f478b0a50ee43195dd4440e2",
"pins" : [
{
"identity" : "emojikit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiKit",
"state" : {
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
"version" : "0.1.2"
}
},
{
"identity" : "emojipicker",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiPicker.git",
"state" : {
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
"version" : "0.1.1"
}
},
{
"identity" : "gsplayer",
"kind" : "remoteSourceControl",
@@ -18,15 +37,6 @@
"version" : "7.6.1"
}
},
{
"identity" : "mcemojipicker",
"kind" : "remoteSourceControl",
"location" : "https://github.com/izyumkin/MCEmojiPicker",
"state" : {
"revision" : "e0b4903b75ae1cc418d276d84d1cb946b8a1d73c",
"version" : "1.2.3"
}
},
{
"identity" : "secp256k1.swift",
"kind" : "remoteSourceControl",
@@ -35,6 +45,15 @@
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
"version" : "1.1.1"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
@@ -60,7 +79,25 @@
"revision" : "74203046135342e4a4a627476dd6caf8b28fe11b",
"version" : "509.0.0"
}
},
{
"identity" : "swift-trie",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/swift-trie",
"state" : {
"revision" : "4c50bff6c168f74425f70476be62a072980d2da7",
"version" : "0.1.2"
}
},
{
"identity" : "swipeactions",
"kind" : "remoteSourceControl",
"location" : "https://github.com/aheze/SwipeActions",
"state" : {
"revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab",
"version" : "1.1.0"
}
}
],
"version" : 2
"version" : 3
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xD7",
"green" : "0xD1",
"red" : "0xD1"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x13",
"green" : "0x11",
"red" : "0x11"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF9",
"green" : "0xF3",
"red" : "0xF3"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x25",
"green" : "0x22",
"red" : "0x22"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "244",
"green" : "218",
"red" : "244"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "92",
"green" : "45",
"red" : "93"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "236",
"green" : "194",
"red" : "238"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "109",
"green" : "49",
"red" : "111"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "197",
"green" : "67",
"red" : "204"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "255",
"green" : "194",
"red" : "255"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF2",
"green" : "0xD8",
"red" : "0xF4"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x45",
"green" : "0x17",
"red" : "0x47"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -10,6 +10,11 @@ import SwiftUI
class DamusColors {
static let adaptableGrey = Color("DamusAdaptableGrey")
static let adaptableGrey2 = Color("DamusAdaptableGrey 2")
static let adaptableLighterGrey = Color("DamusAdaptableLighterGrey")
static let adaptablePurpleBackground = Color("DamusAdaptablePurpleBackground 1")
static let adaptablePurpleBackground2 = Color("DamusAdaptablePurpleBackground 2")
static let adaptablePurpleForeground = Color("DamusAdaptablePurpleForeground")
static let adaptableBlack = Color("DamusAdaptableBlack")
static let adaptableWhite = Color("DamusAdaptableWhite")
static let white = Color("DamusWhite")
@@ -23,6 +28,7 @@ class DamusColors {
static let green = Color("DamusGreen")
static let purple = Color("DamusPurple")
static let deepPurple = Color("DamusDeepPurple")
static let highlight = Color("DamusHighlight")
static let blue = Color("DamusBlue")
static let bitcoin = Color("Bitcoin")
static let success = Color("DamusSuccessPrimary")

View File

@@ -9,16 +9,20 @@ import UIKit
import SwiftUI
struct SelectableText: View {
let damus_state: DamusState
let event: NostrEvent?
let attributedString: AttributedString
let textAlignment: NSTextAlignment
@State private var showHighlightPost = false
@State private var selectedText = ""
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
let size: EventViewKind
init(attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
init(damus_state: DamusState, event: NostrEvent?, attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.attributedString = attributedString
self.textAlignment = textAlignment ?? NSTextAlignment.natural
self.size = size
@@ -32,6 +36,9 @@ struct SelectableText: View {
font: eventviewsize_to_uifont(size),
fixedWidth: selectedTextWidth,
textAlignment: self.textAlignment,
enableHighlighting: self.enableHighlighting(),
showHighlightPost: $showHighlightPost,
selectedText: $selectedText,
height: $selectedTextHeight
)
.padding([.leading, .trailing], -1.0)
@@ -46,8 +53,48 @@ struct SelectableText: View {
self.selectedTextWidth = newSize.width
}
}
.sheet(isPresented: $showHighlightPost) {
if let event {
HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText)
.presentationDragIndicator(.visible)
.presentationDetents([.height(selectedTextHeight + 150), .medium, .large])
}
}
.frame(height: selectedTextHeight)
}
func enableHighlighting() -> Bool {
self.event != nil
}
}
fileprivate class TextView: UITextView {
@Binding var showHighlightPost: Bool
@Binding var selectedText: String
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, selectedText: Binding<String>) {
self._showHighlightPost = showHighlightPost
self._selectedText = selectedText
super.init(frame: frame, textContainer: textContainer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(highlightText(_:)) {
return true
}
return super.canPerformAction(action, withSender: sender)
}
@objc public func highlightText(_ sender: Any?) {
guard let selectedRange = self.selectedTextRange else { return }
selectedText = self.text(in: selectedRange) ?? ""
showHighlightPost.toggle()
}
}
fileprivate struct TextViewRepresentable: UIViewRepresentable {
@@ -57,11 +104,13 @@ struct SelectableText: View {
let font: UIFont
let fixedWidth: CGFloat
let textAlignment: NSTextAlignment
let enableHighlighting: Bool
@Binding var showHighlightPost: Bool
@Binding var selectedText: String
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
let view = UITextView()
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, selectedText: $selectedText)
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
@@ -71,10 +120,15 @@ struct SelectableText: View {
view.textContainerInset.left = 1.0
view.textContainerInset.right = 1.0
view.textAlignment = textAlignment
let menuController = UIMenuController.shared
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
menuController.menuItems = self.enableHighlighting ? [highlightItem] : []
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
func updateUIView(_ uiView: TextView, context: UIViewRepresentableContext<Self>) {
let mutableAttributedString = createNSAttributedString()
uiView.attributedText = mutableAttributedString
uiView.textAlignment = self.textAlignment

View File

@@ -51,9 +51,9 @@ struct TranslateView: View {
.foregroundColor(.gray)
.font(.footnote)
.padding([.top, .bottom], 10)
if self.size == .selected {
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
SelectableText(damus_state: damus_state, event: event, attributedString: artifacts.content.attributed, size: self.size)
} else {
artifacts.content.text
.font(eventviewsize_to_font(self.size, font_size: font_size))

View File

@@ -10,10 +10,12 @@ import SwiftUI
struct TruncatedText: View {
let text: CompatibleText
let maxChars: Int
let show_show_more_button: Bool
init(text: CompatibleText, maxChars: Int = 280) {
init(text: CompatibleText, maxChars: Int = 280, show_show_more_button: Bool) {
self.text = text
self.maxChars = maxChars
self.show_show_more_button = show_show_more_button
}
var body: some View {
@@ -29,8 +31,10 @@ struct TruncatedText: View {
if truncatedAttributedString != nil {
Spacer()
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
.allowsHitTesting(false)
if self.show_show_more_button {
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
.allowsHitTesting(false)
}
}
}
}
@@ -38,10 +42,10 @@ struct TruncatedText: View {
struct TruncatedText_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 100) {
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"))
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"), show_show_more_button: true)
.frame(width: 200, height: 200)
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"))
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"), show_show_more_button: true)
.frame(width: 200, height: 200)
}
}

View File

@@ -8,6 +8,7 @@
import SwiftUI
import AVKit
import MediaPlayer
import EmojiPicker
struct ZapSheet {
let target: ZapTarget
@@ -719,7 +720,8 @@ struct ContentView: View {
music: MusicController(onChange: music_changed),
video: VideoController(),
ndb: ndb,
quote_reposts: .init(our_pubkey: pubkey)
quote_reposts: .init(our_pubkey: pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
)
home.damus_state = self.damus_state!

View File

@@ -9,31 +9,31 @@ import Foundation
class CreateAccountModel: ObservableObject {
@Published var real_name: String = ""
@Published var nick_name: String = ""
@Published var display_name: String = ""
@Published var name: String = ""
@Published var about: String = ""
@Published var pubkey: Pubkey = .empty
@Published var privkey: Privkey = .empty
@Published var profile_image: URL? = nil
var rendered_name: String {
if real_name.isEmpty {
return nick_name
if display_name.isEmpty {
return name
}
return real_name
return display_name
}
var keypair: Keypair {
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
}
init(real: String = "", nick: String = "", about: String = "") {
init(display_name: String = "", name: String = "", about: String = "") {
let keypair = generate_new_keypair()
self.pubkey = keypair.pubkey
self.privkey = keypair.privkey
self.real_name = real
self.nick_name = nick
self.display_name = display_name
self.name = name
self.about = about
}
}

View File

@@ -7,6 +7,7 @@
import Foundation
import LinkPresentation
import EmojiPicker
class DamusState: HeadlessDamusState {
let pool: RelayPool
@@ -37,8 +38,9 @@ class DamusState: HeadlessDamusState {
let ndb: Ndb
var purple: DamusPurple
var push_notification_client: PushNotificationClient
let emoji_provider: EmojiProvider
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter) {
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
self.pool = pool
self.keypair = keypair
self.likes = likes
@@ -70,6 +72,7 @@ class DamusState: HeadlessDamusState {
)
self.quote_reposts = quote_reposts
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
self.emoji_provider = emoji_provider
}
@discardableResult
@@ -99,6 +102,7 @@ class DamusState: HeadlessDamusState {
func close() {
print("txn: damus close")
wallet.disconnect()
pool.close()
ndb.close()
}
@@ -134,7 +138,8 @@ class DamusState: HeadlessDamusState {
music: nil,
video: VideoController(),
ndb: .empty,
quote_reposts: .init(our_pubkey: empty_pub)
quote_reposts: .init(our_pubkey: empty_pub),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
)
}
}

View File

@@ -0,0 +1,34 @@
//
// HighlightEvent.swift
// damus
//
// Created by eric on 4/22/24.
//
import Foundation
struct HighlightEvent {
let event: NostrEvent
var event_ref: String? = nil
var url_ref: URL? = nil
var context: String? = nil
static func parse(from ev: NostrEvent) -> HighlightEvent {
var highlight = HighlightEvent(event: ev)
for tag in ev.tags {
guard tag.count >= 2 else { continue }
switch tag[0].string() {
case "e": highlight.event_ref = tag[1].string()
case "a": highlight.event_ref = tag[1].string()
case "r": highlight.url_ref = URL(string: tag[1].string())
case "context": highlight.context = tag[1].string()
default:
break
}
}
return highlight
}
}

View File

@@ -184,7 +184,7 @@ class HomeModel: ContactsDelegate {
}
switch kind {
case .chat, .longform, .text:
case .chat, .longform, .text, .highlight:
handle_text_event(sub_id: sub_id, ev)
case .contacts:
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
@@ -586,7 +586,7 @@ class HomeModel: ContactsDelegate {
func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: RelayURL? = nil) {
// TODO: separate likes?
var home_filter_kinds: [NostrKind] = [
.text, .longform, .boost
.text, .longform, .boost, .highlight
]
if !damus_state.settings.onlyzaps_mode {
home_filter_kinds.append(.like)

View File

@@ -60,11 +60,11 @@ class ProfileModel: ObservableObject, Equatable {
damus.pool.unsubscribe(sub_id: sub_id)
damus.pool.unsubscribe(sub_id: prof_subid)
}
func subscribe() {
var text_filter = NostrFilter(kinds: [.text, .longform])
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
profile_filter.authors = [pubkey]
text_filter.authors = [pubkey]

View File

@@ -36,7 +36,7 @@ class SearchModel: ObservableObject {
func subscribe() {
// since 1 month
search.limit = self.limit
search.kinds = [.text, .like, .longform]
search.kinds = [.text, .like, .longform, .highlight]
//likes_filter.ids = ref_events.referenced_ids!

View File

@@ -20,7 +20,13 @@ class ThreadModel: ObservableObject {
self.original_event = event
add_event(event, keypair: damus_state.keypair)
}
func events() -> [NostrEvent] {
return Array(event_map).sorted(by: { a, b in
return a.created_at < b.created_at
})
}
var is_original: Bool {
return original_event.id == event.id
}

View File

@@ -22,6 +22,7 @@ enum NostrKind: UInt32, Codable {
case longform = 30023
case zap = 9735
case zap_request = 9734
case highlight = 9802
case nwc_request = 23194
case nwc_response = 23195
case http_auth = 27235

File diff suppressed because one or more lines are too long

View File

@@ -10,9 +10,9 @@ import Foundation
class Constants {
//static let EXAMPLE_DEMOS: DamusState = .empty
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info")!
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info")!
static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")!
static let DEVICE_TOKEN_REVOKER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info/remove")!
static let DEVICE_TOKEN_REVOKER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info/remove")!
static let DEVICE_TOKEN_REVOKER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info/remove")!
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"

View File

@@ -97,13 +97,13 @@ class EventCache {
// TODO: remove me and change code to use ndb directly
private let ndb: Ndb
private var events: [NoteId: NostrEvent] = [:]
private var replies = ReplyMap()
private var cancellable: AnyCancellable?
private var image_metadata: [String: ImageMetadataState] = [:] // lowercased URL key
private var event_data: [NoteId: EventData] = [:]
var replies = ReplyMap()
//private var thread_latest: [String: Int64]
init(ndb: Ndb) {
self.ndb = ndb
cancellable = NotificationCenter.default.publisher(
@@ -187,7 +187,7 @@ class EventCache {
replies.add(id: reply, reply_id: ev.id)
}
}
func child_events(event: NostrEvent) -> [NostrEvent] {
guard let xs = replies.lookup(event.id) else {
return []

View File

@@ -0,0 +1,27 @@
//
// VectorMath.swift
// damus
//
// Created by Daniel DAquino on 2024-06-17.
//
import Foundation
extension CGPoint {
/// Summing a vector to a point
static func +(lhs: CGPoint, rhs: CGVector) -> CGPoint {
return CGPoint(x: lhs.x + rhs.dx, y: lhs.y + rhs.dy)
}
/// Subtracting a vector from a point
static func -(lhs: CGPoint, rhs: CGVector) -> CGPoint {
return CGPoint(x: lhs.x - rhs.dx, y: lhs.y - rhs.dy)
}
}
extension CGVector {
/// Multiplying a vector by a scalar
static func *(lhs: CGVector, rhs: CGFloat) -> CGVector {
return CGVector(dx: lhs.dx * rhs, dy: lhs.dy * rhs)
}
}

View File

@@ -85,7 +85,7 @@ enum Route: Hashable {
case .TranslationSettings(let settings):
TranslationSettingsView(settings: settings, damus_state: damusState)
case .ReactionsSettings(let settings):
ReactionsSettingsView(settings: settings)
ReactionsSettingsView(settings: settings, damus_state: damusState)
case .SearchSettings(let settings):
SearchSettingsView(settings: settings)
case .DeveloperSettings(let settings):
@@ -93,7 +93,8 @@ enum Route: Hashable {
case .FirstAidSettings(settings: let settings):
FirstAidSettingsView(damus_state: damusState, settings: settings)
case .Thread(let thread):
ThreadView(state: damusState, thread: thread)
ChatroomThreadView(damus: damusState, thread: thread)
//ThreadView(state: damusState, thread: thread)
case .Reposts(let reposts):
RepostsView(damus_state: damusState, model: reposts)
case .QuoteReposts(let quote_reposts):

View File

@@ -6,28 +6,34 @@
//
import SwiftUI
import MCEmojiPicker
import EmojiPicker
import EmojiKit
import SwipeActions
struct EventActionBar: View {
let damus_state: DamusState
let event: NostrEvent
let generator = UIImpactFeedbackGenerator(style: .medium)
let userProfile : ProfileModel
let swipe_context: SwipeContext?
let options: Options
// just used for previews
@State var show_share_sheet: Bool = false
@State var show_share_action: Bool = false
@State var show_repost_action: Bool = false
@State private var isOnTopHalfOfScreen: Bool = false
@State private var selectedEmoji: Emoji? = nil
@ObservedObject var bar: ActionBarModel
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil) {
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, options: Options = [], swipe_context: SwipeContext? = nil) {
self.damus_state = damus_state
self.event = event
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
self.userProfile = ProfileModel(pubkey: event.pubkey, damus: damus_state)
self.options = options
self.swipe_context = swipe_context
}
var lnurl: String? {
@@ -44,60 +50,176 @@ struct EventActionBar: View {
return true
}
var body: some View {
HStack {
if damus_state.keypair.privkey != nil {
HStack(spacing: 4) {
EventActionButton(img: "bubble2", col: bar.replied ? DamusColors.purple : Color.gray) {
notify(.compose(.replying_to(event)))
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
}
}
Spacer()
HStack(spacing: 4) {
EventActionButton(img: "repost", col: bar.boosted ? Color.green : nil) {
self.show_repost_action = true
}
.accessibilityLabel(NSLocalizedString("Reposts", comment: "Accessibility label for boosts button"))
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray)
}
if show_like {
Spacer()
HStack(spacing: 4) {
LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil, isOnTopHalfOfScreen: $isOnTopHalfOfScreen) { emoji in
if bar.liked {
//notify(.delete, bar.our_like)
} else {
send_like(emoji: emoji)
}
}
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium))
.nip05_colorized(gradient: bar.liked)
}
}
if let lnurl = self.lnurl {
Spacer()
NoteZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
}
Spacer()
EventActionButton(img: "upload", col: Color.gray) {
show_share_action = true
}
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note"))
var space_if_spread: AnyView {
if options.contains(.no_spread) {
return AnyView(EmptyView())
}
else {
return AnyView(Spacer())
}
}
// MARK: Swipe action menu buttons
var reply_swipe_button: some View {
SwipeAction(systemImage: "arrowshape.turn.up.left.fill", backgroundColor: DamusColors.adaptableGrey) {
notify(.compose(.replying_to(event)))
self.swipe_context?.state.wrappedValue = .closed
}
.allowSwipeToTrigger()
.swipeButtonStyle()
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
}
var repost_swipe_button: some View {
SwipeAction(image: "repost", backgroundColor: DamusColors.adaptableGrey) {
self.show_repost_action = true
self.swipe_context?.state.wrappedValue = .closed
}
.swipeButtonStyle()
.accessibilityLabel(NSLocalizedString("Repost or quote this note", comment: "Accessibility label for repost/quote button"))
}
var like_swipe_button: some View {
SwipeAction(image: "shaka", backgroundColor: DamusColors.adaptableGrey) {
send_like(emoji: damus_state.settings.default_emoji_reaction)
self.swipe_context?.state.wrappedValue = .closed
}
.swipeButtonStyle()
.accessibilityLabel(NSLocalizedString("React with default reaction emoji", comment: "Accessibility label for react button"))
}
var share_swipe_button: some View {
SwipeAction(image: "upload", backgroundColor: DamusColors.adaptableGrey) {
show_share_action = true
self.swipe_context?.state.wrappedValue = .closed
}
.swipeButtonStyle()
.accessibilityLabel(NSLocalizedString("Share externally", comment: "Accessibility label for external share button"))
}
// MARK: Bar buttons
var reply_button: some View {
HStack(spacing: 4) {
EventActionButton(img: "bubble2", col: bar.replied ? DamusColors.purple : Color.gray) {
notify(.compose(.replying_to(event)))
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
}
}
var repost_button: some View {
HStack(spacing: 4) {
EventActionButton(img: "repost", col: bar.boosted ? Color.green : nil) {
self.show_repost_action = true
}
.accessibilityLabel(NSLocalizedString("Reposts", comment: "Accessibility label for boosts button"))
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray)
}
}
var like_button: some View {
HStack(spacing: 4) {
LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil) { emoji in
if bar.liked {
//notify(.delete, bar.our_like)
} else {
send_like(emoji: emoji)
}
}
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium))
.nip05_colorized(gradient: bar.liked)
}
}
var share_button: some View {
EventActionButton(img: "upload", col: Color.gray) {
show_share_action = true
}
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note"))
}
// MARK: Main views
var swipe_action_menu_content: some View {
Group {
self.reply_swipe_button
self.repost_swipe_button
if show_like {
self.like_swipe_button
}
}
}
var swipe_action_menu_reverse_content: some View {
Group {
if show_like {
self.like_swipe_button
}
self.repost_swipe_button
self.reply_swipe_button
}
}
var action_bar_content: some View {
let hide_items_without_activity = options.contains(.hide_items_without_activity)
let should_hide_chat_bubble = hide_items_without_activity && bar.replies == 0
let should_hide_repost = hide_items_without_activity && bar.boosts == 0
let should_hide_reactions = hide_items_without_activity && bar.likes == 0
let zap_model = self.damus_state.events.get_cache_data(self.event.id).zaps_model
let should_hide_zap = hide_items_without_activity && zap_model.zap_total > 0
let should_hide_share_button = hide_items_without_activity
return HStack(spacing: options.contains(.no_spread) ? 10 : 0) {
if damus_state.keypair.privkey != nil && !should_hide_chat_bubble {
self.reply_button
}
if !should_hide_repost {
self.space_if_spread
self.repost_button
}
if show_like && !should_hide_reactions {
self.space_if_spread
self.like_button
}
if let lnurl = self.lnurl, !should_hide_zap {
self.space_if_spread
NoteZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: zap_model)
}
if !should_hide_share_button {
self.space_if_spread
self.share_button
}
}
}
var content: some View {
if options.contains(.swipe_action_menu) {
AnyView(self.swipe_action_menu_content)
}
else if options.contains(.swipe_action_menu_reverse) {
AnyView(self.swipe_action_menu_reverse_content)
}
else {
AnyView(self.action_bar_content)
}
}
var body: some View {
self.content
.onAppear {
self.bar.update(damus: damus_state, evid: self.event.id)
}
@@ -136,20 +258,6 @@ struct EventActionBar: View {
self.bar.our_like = liked.event
}
}
.background(
GeometryReader { geometry in
EmptyView()
.onAppear {
let eventActionBarY = geometry.frame(in: .global).midY
let screenMidY = UIScreen.main.bounds.midY
self.isOnTopHalfOfScreen = eventActionBarY > screenMidY
}
.onChange(of: geometry.frame(in: .global).midY) { newY in
let screenMidY = UIScreen.main.bounds.midY
self.isOnTopHalfOfScreen = newY > screenMidY
}
}
)
}
func send_like(emoji: String) {
@@ -164,6 +272,17 @@ struct EventActionBar: View {
damus_state.postbox.send(like_ev)
}
// MARK: Helper structures
struct Options: OptionSet {
let rawValue: UInt32
static let no_spread = Options(rawValue: 1 << 0)
static let hide_items_without_activity = Options(rawValue: 1 << 1)
static let swipe_action_menu = Options(rawValue: 1 << 2)
static let swipe_action_menu_reverse = Options(rawValue: 1 << 3)
}
}
@@ -183,7 +302,6 @@ struct LikeButton: View {
let damus_state: DamusState
let liked: Bool
let liked_emoji: String?
@Binding var isOnTopHalfOfScreen: Bool
let action: (_ emoji: String) -> Void
// For reactions background
@@ -192,7 +310,7 @@ struct LikeButton: View {
@State private var isReactionsVisible = false
@State private var selectedEmoji: String = ""
@State private var selectedEmoji: Emoji?
// Following four are Shaka animation properties
let timer = Timer.publish(every: 0.10, on: .main, in: .common).autoconnect()
@@ -231,6 +349,11 @@ struct LikeButton: View {
.foregroundColor(.gray)
}
}
.sheet(isPresented: $isReactionsVisible) {
NavigationView {
EmojiPickerView(selectedEmoji: $selectedEmoji, emojiProvider: damus_state.emoji_provider)
}.presentationDetents([.medium, .large])
}
.accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button"))
.rotationEffect(Angle(degrees: shouldAnimate ? rotationAngle : 0))
.onReceive(self.timer) { _ in
@@ -245,14 +368,10 @@ struct LikeButton: View {
amountOfAngleIncrease = 20.0
}
})
.emojiPicker(
isPresented: $isReactionsVisible,
selectedEmoji: $selectedEmoji,
arrowDirection: isOnTopHalfOfScreen ? .down : .up,
isDismissAfterChoosing: true
)
.onChange(of: selectedEmoji) { newSelectedEmoji in
self.action(newSelectedEmoji)
if let newSelectedEmoji {
self.action(newSelectedEmoji.value)
}
}
}
@@ -299,7 +418,6 @@ struct LikeButton: View {
}
}
struct EventActionBar_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state
@@ -324,7 +442,44 @@ struct EventActionBar_Previews: PreviewProvider {
EventActionBar(damus_state: ds, event: ev, bar: extra_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: mega_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: bar, options: [.no_spread])
}
.padding(20)
}
}
// MARK: Helpers
fileprivate struct SwipeButtonStyle: ViewModifier {
func body(content: Content) -> some View {
content
.frame(width: 50, height: 50)
.clipShape(Circle())
.overlay(Circle().stroke(Color.damusAdaptableGrey2, lineWidth: 2))
}
}
fileprivate extension View {
func swipeButtonStyle() -> some View {
modifier(SwipeButtonStyle())
}
}
// MARK: Needed extensions for SwipeAction
public extension SwipeAction where Label == Image, Background == Color {
init(
image: String,
backgroundColor: Color = Color.primary.opacity(0.1),
highlightOpacity: Double = 0.5,
action: @escaping () -> Void
) {
self.init(action: action) { highlight in
Image(image)
} background: { highlight in
backgroundColor
.opacity(highlight ? highlightOpacity : 1)
}
}
}

View File

@@ -0,0 +1,184 @@
//
// ChatBubbleView.swift
// damus
//
// Created by Daniel DAquino on 2024-06-17.
//
import Foundation
import SwiftUI
/// Use this view to display content inside of a custom-designed chat bubble shape.
struct ChatBubble<T: View, U: ShapeStyle, V: View>: View {
/// The direction at which the chat bubble tip will be pointing towards
let direction: Direction
let stroke_content: U
let stroke_style: StrokeStyle
let background_style: V
@ViewBuilder let content: T
// Constants, which are loosely tied to `OFFSET_X` and `OFFSET_Y`
let OFFSET_X_PADDING: CGFloat = 6
let OFFSET_Y_BOTTOM_PADDING: CGFloat = 3
var body: some View {
self.content
.padding(direction == .left ? .leading : .trailing, OFFSET_X_PADDING)
.padding(.bottom, OFFSET_Y_BOTTOM_PADDING)
.background(self.background_style)
.clipShape(
BubbleShape(direction: self.direction)
)
.overlay(
BubbleShape(direction: self.direction)
.stroke(self.stroke_content, style: self.stroke_style)
)
.padding(direction == .left ? .leading : .trailing, -OFFSET_X_PADDING)
.padding(.bottom, -OFFSET_Y_BOTTOM_PADDING)
}
enum Direction {
case right
case left
}
struct BubbleShape: Shape {
/// The direction at which the chat bubble tip will be pointing towards
let direction: Direction
// MARK: Constant parameters that defines the shape and look of the chat bubbles
/// The corner radius of the round edges
let CORNER_RADIUS: CGFloat = 10
/// The height of the chat bubble tip detail
let DETAIL_HEIGHT: CGFloat = 10
/// The horizontal distance between the chat bubble tip and the vertical edge of the bubble
let OFFSET_X: CGFloat = 7
/// The vertical distance between the chat bubble tip and the bottom edge of the bubble
let OFFSET_Y: CGFloat = 5
/// Value between 0 and 1 that determines curvature of the upper chat bubble curve detail
let DETAIL_CURVE_FACTOR: CGFloat = 0.75
/// Value between 0 and 1 that determines curvature of the lower chat bubble curve detail
let LOWER_DETAIL_CURVE_FACTOR: CGFloat = 0.4
/// The horizontal distance between the chat bubble tip and the point at which the lower chat bubble curve detail attaches to the bottom of the chat bubble
let LOWER_DETAIL_ATTACHMENT_OFFSET_X: CGFloat = 20
func path(in rect: CGRect) -> Path {
return self.direction == .left ? self.draw_left_bubble(in: rect) : self.draw_right_bubble(in: rect)
}
func draw_left_bubble(in rect: CGRect) -> Path {
return Path { p in
// Start at the top left, just below the end of the corner radius
let start = CGPoint(x: OFFSET_X, y: CORNER_RADIUS)
// Left edge
p.move(to: start)
p.addLine(to: CGPoint(x: OFFSET_X, y: rect.height - DETAIL_HEIGHT))
// Draw the chat bubble tip
p.addLine(to: CGPoint(x: OFFSET_X, y: rect.height - DETAIL_HEIGHT))
let tip_of_bubble = CGPoint(x: 0, y: rect.height)
p.addQuadCurve(
to: tip_of_bubble,
control: CGPoint(x: 0, y: rect.height - DETAIL_HEIGHT) + CGVector(dx: OFFSET_X, dy: DETAIL_HEIGHT) * DETAIL_CURVE_FACTOR
)
let lower_detail_attachment = CGPoint(x: LOWER_DETAIL_ATTACHMENT_OFFSET_X, y: rect.height - OFFSET_Y)
p.addCurve(
to: lower_detail_attachment,
control1: tip_of_bubble + CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR,
control2: lower_detail_attachment - CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR
)
// Draw the bottom edge
p.addLine(to: CGPoint(x: rect.width - CORNER_RADIUS, y: rect.height - OFFSET_Y))
// Draw the bottom right round corner
p.addQuadCurve(
to: CGPoint(x: rect.width, y: rect.height - OFFSET_Y - CORNER_RADIUS),
control: CGPoint(x: rect.width, y: rect.height - OFFSET_Y)
)
// Draw right edge
p.addLine(to: CGPoint(x: rect.width, y: CORNER_RADIUS))
// Draw top right round corner
p.addQuadCurve(
to: CGPoint(x: rect.width - CORNER_RADIUS, y: 0),
control: CGPoint(x: rect.width, y: 0)
)
// Draw top edge
p.addLine(to: CGPoint(x: CORNER_RADIUS + OFFSET_X, y: 0))
// Draw top left round corner
p.addQuadCurve(
to: start,
control: CGPoint(x: OFFSET_X, y: 0)
)
}
}
func draw_right_bubble(in rect: CGRect) -> Path {
return Path { p in
// Start at the top right, just below the end of the corner radius
let right_edge = rect.width - OFFSET_X
let start = CGPoint(x: right_edge, y: CORNER_RADIUS)
p.move(to: start)
// Right edge
p.addLine(to: CGPoint(x: right_edge, y: rect.height - DETAIL_HEIGHT))
// Draw the chat bubble tip
let tip_of_bubble = CGPoint(x: rect.width, y: rect.height)
p.addQuadCurve(
to: tip_of_bubble,
control: CGPoint(x: rect.width, y: rect.height - DETAIL_HEIGHT) + CGVector(dx: -OFFSET_X, dy: DETAIL_HEIGHT) * DETAIL_CURVE_FACTOR
)
let lower_detail_attachment = CGPoint(x: rect.width - LOWER_DETAIL_ATTACHMENT_OFFSET_X, y: rect.height - OFFSET_Y)
p.addCurve(
to: lower_detail_attachment,
control1: tip_of_bubble - CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR,
control2: lower_detail_attachment + CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR
)
// Draw the bottom edge
p.addLine(to: CGPoint(x: CORNER_RADIUS, y: rect.height - OFFSET_Y))
// Draw the bottom left round corner
p.addQuadCurve(
to: CGPoint(x: 0, y: rect.height - OFFSET_Y - CORNER_RADIUS),
control: CGPoint(x: 0, y: rect.height - OFFSET_Y)
)
// Draw left edge
p.addLine(to: CGPoint(x: 0, y: CORNER_RADIUS))
// Draw top right round corner
p.addQuadCurve(
to: CGPoint(x: CORNER_RADIUS, y: 0),
control: CGPoint(x: 0, y: 0)
)
// Draw top edge
p.addLine(to: CGPoint(x: rect.width - CORNER_RADIUS - OFFSET_X, y: 0))
// Draw top left round corner
p.addQuadCurve(
to: start,
control: CGPoint(x: rect.width - OFFSET_X, y: 0)
)
}
}
}
}
#Preview {
VStack {
ChatBubble(
direction: .left,
stroke_content: Color.accentColor.opacity(0),
stroke_style: .init(lineWidth: 4),
background_style: Color.accentColor
) {
Text("Hello there")
.padding()
}
.foregroundColor(.white)
ChatBubble(
direction: .right,
stroke_content: Color.accentColor.opacity(0),
stroke_style: .init(lineWidth: 4),
background_style: Color.accentColor
) {
Text("Hello there")
.padding()
}
.foregroundColor(.white)
}
}

View File

@@ -0,0 +1,324 @@
//
// ChatView.swift
// damus
//
// Created by William Casarin on 2022-04-19.
//
import SwiftUI
import EmojiKit
import EmojiPicker
import SwipeActions
fileprivate let CORNER_RADIUS: CGFloat = 10
struct ChatEventView: View {
// MARK: Parameters
let event: NostrEvent
let selected_event: NostrEvent
let prev_ev: NostrEvent?
let next_ev: NostrEvent?
let damus_state: DamusState
var thread: ThreadModel
let scroll_to_event: ((_ id: NoteId) -> Void)?
let focus_event: (() -> Void)?
let highlight_bubble: Bool
// MARK: long-press reaction control objects
/// Whether the user is actively pressing the view
@State var is_pressing = false
/// The dispatched work item scheduled by a timer to bounce the event bubble and show the emoji selector
@State var long_press_bounce_work_item: DispatchWorkItem?
@State var popover_state: PopoverState = .closed {
didSet {
let generator = UIImpactFeedbackGenerator(style: popover_state == .open_emoji_selector ? .heavy : .light)
generator.impactOccurred()
}
}
@State var selected_emoji: Emoji?
@State private var isOnTopHalfOfScreen: Bool = false
@ObservedObject var bar: ActionBarModel
enum PopoverState: String {
case closed
case open_emoji_selector
}
var just_started: Bool {
return prev_ev == nil || prev_ev!.pubkey != event.pubkey
}
func next_replies_to_this() -> Bool {
guard let next = next_ev else {
return false
}
return damus_state.events.replies.lookup(next.id) != nil
}
func is_reply_to_prev(ref_id: NoteId) -> Bool {
guard let prev = prev_ev else {
return true
}
if let rep = damus_state.events.replies.lookup(event.id) {
return rep.contains(prev.id)
}
return false
}
var disable_animation: Bool {
self.damus_state.settings.disable_animation
}
var reply_quote_options: EventViewOptions {
return [.no_previews, .no_action_bar, .truncate_content_very_short, .no_show_more, .no_translate, .no_media]
}
var profile_picture_view: some View {
VStack {
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
.onTapGesture {
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: event.pubkey)
}
}
.frame(maxWidth: 32)
}
var by_other_user: Bool {
return event.pubkey != damus_state.pubkey
}
var is_ours: Bool { return !by_other_user }
var event_bubble: some View {
ChatBubble(
direction: is_ours ? .right : .left,
stroke_content: Color.accentColor.opacity(highlight_bubble ? 1 : 0),
stroke_style: .init(lineWidth: 4),
background_style: by_other_user ? DamusColors.adaptableGrey : DamusColors.adaptablePurpleBackground
) {
VStack(alignment: .leading, spacing: 4) {
if by_other_user {
HStack {
ProfileName(pubkey: event.pubkey, damus: damus_state)
.onTapGesture {
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: event.pubkey)
}
Text(verbatim: "\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
}
}
if let replying_to = event.direct_replies(),
replying_to != selected_event.id {
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: replying_to, state: damus_state, thread: thread, options: reply_quote_options)
.background(is_ours ? DamusColors.adaptablePurpleBackground2 : DamusColors.adaptableGrey2)
.foregroundColor(is_ours ? Color.damusAdaptablePurpleForeground : Color.damusAdaptableBlack)
.cornerRadius(5)
.onTapGesture {
self.scroll_to_event?(replying_to)
}
}
let blur_images = should_blur_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(damus_state: damus_state, event: event, blur_images: blur_images, size: .normal, options: [])
.padding(2)
}
.frame(minWidth: 150, alignment: is_ours ? .trailing : .leading)
.padding(10)
}
.tint(is_ours ? Color.white : Color.accentColor)
.overlay(
ZStack(alignment: is_ours ? .bottomLeading : .bottomTrailing) {
VStack {
Spacer()
self.action_bar
.padding(.horizontal, 5)
}
}
)
.onTapGesture {
if popover_state == .closed {
focus_event?()
}
else {
popover_state = .closed
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
}
}
}
var event_bubble_with_long_press_interaction: some View {
ZStack(alignment: is_ours ? .bottomLeading : .bottomTrailing) {
self.event_bubble
.sheet(isPresented: Binding(get: { popover_state == .open_emoji_selector }, set: { new_state in
withAnimation(new_state == true ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) {
popover_state = new_state == true ? .open_emoji_selector : .closed
}
})) {
NavigationView {
EmojiPickerView(selectedEmoji: $selected_emoji, emojiProvider: damus_state.emoji_provider)
}.presentationDetents([.medium, .large])
}
.onChange(of: selected_emoji) { newSelectedEmoji in
if let newSelectedEmoji {
send_like(emoji: newSelectedEmoji.value)
popover_state = .closed
}
}
}
.scaleEffect(self.popover_state == .open_emoji_selector ? 1.08 : is_pressing ? 1.02 : 1)
.shadow(color: (is_pressing || self.popover_state == .open_emoji_selector) ? .black.opacity(0.1) : .black.opacity(0.3), radius: (is_pressing || self.popover_state == .open_emoji_selector) ? 8 : 0, y: (is_pressing || self.popover_state == .open_emoji_selector) ? 15 : 0)
.onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10, perform: {
long_press_bounce_work_item?.cancel()
}, onPressingChanged: { is_pressing in
withAnimation(is_pressing ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) {
self.is_pressing = is_pressing
if popover_state != .closed {
return
}
if self.is_pressing {
let item = DispatchWorkItem {
// Ensure the action is performed only if the condition is still valid
if self.is_pressing {
withAnimation(.bouncy(duration: 0.2, extraBounce: 0.35)) {
popover_state = .open_emoji_selector
}
}
}
long_press_bounce_work_item = item
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: item)
}
}
})
.background(
GeometryReader { geometry in
EmptyView()
.onAppear {
let eventActionBarY = geometry.frame(in: .global).midY
let screenMidY = UIScreen.main.bounds.midY
self.isOnTopHalfOfScreen = eventActionBarY > screenMidY
}
.onChange(of: geometry.frame(in: .global).midY) { newY in
let screenMidY = UIScreen.main.bounds.midY
self.isOnTopHalfOfScreen = newY > screenMidY
}
}
)
}
func send_like(emoji: String) {
guard let keypair = damus_state.keypair.to_full(),
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else {
return
}
self.bar.our_like = like_ev
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
damus_state.postbox.send(like_ev)
}
var action_bar: some View {
return Group {
if !bar.is_empty {
HStack {
if by_other_user {
Spacer()
}
EventActionBar(damus_state: damus_state, event: event, bar: bar, options: [.no_spread, .hide_items_without_activity])
.padding(10)
.background(DamusColors.adaptableLighterGrey)
.disabled(true)
.cornerRadius(100)
.overlay(RoundedRectangle(cornerSize: CGSize(width: 100, height: 100)).stroke(DamusColors.adaptableWhite, lineWidth: 1))
.shadow(color: Color.black.opacity(0.05),radius: 3, y: 3)
.scaleEffect(0.7, anchor: is_ours ? .leading : .trailing)
if !by_other_user {
Spacer()
}
}
.padding(.vertical, -20)
}
}
}
var event_bubble_with_long_press_and_swipe_interactions: some View {
Group {
SwipeView {
self.event_bubble_with_long_press_interaction
} leadingActions: { context in
EventActionBar(
damus_state: damus_state,
event: event,
bar: bar,
options: is_ours ? [.swipe_action_menu_reverse] : [.swipe_action_menu],
swipe_context: context
)
}
.swipeSpacing(-20)
.swipeActionsStyle(.mask)
.swipeMinimumDistance(20)
}
}
var content: some View {
return VStack {
HStack(alignment: .bottom, spacing: 4) {
if by_other_user {
self.profile_picture_view
}
else {
Spacer()
}
self.event_bubble_with_long_press_and_swipe_interactions
if !by_other_user {
self.profile_picture_view
}
else {
Spacer()
}
}
.contentShape(Rectangle())
.id(event.id)
.padding([.bottom], bar.is_empty ? 6 : 16)
}
}
var body: some View {
if [.boost, .zap, .longform].contains(where: { event.known_kind == $0 }) {
EmptyView()
} else {
self.content
}
}
}
extension Notification.Name {
static var toggle_thread_view: Notification.Name {
return Notification.Name("convert_to_thread")
}
}
#Preview {
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
return ChatEventView(event: test_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar)
}
#Preview {
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar)
}
#Preview {
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: true, bar: bar)
}

View File

@@ -0,0 +1,195 @@
//
// ChatroomView.swift
// damus
//
// Created by William Casarin on 2022-04-19.
//
import SwiftUI
import SwipeActions
struct ChatroomThreadView: View {
@Environment(\.dismiss) var dismiss
@State var once: Bool = false
let damus: DamusState
@ObservedObject var thread: ThreadModel
@State var selected_note_id: NoteId? = nil
@State var user_just_posted_flag: Bool = false
@Namespace private var animation
@State var parent_events: [NostrEvent] = []
@State var sorted_child_events: [NostrEvent] = []
func compute_events(selected_event: NostrEvent? = nil) {
let selected_event = selected_event ?? thread.event
self.parent_events = damus.events.parent_events(event: selected_event, keypair: damus.keypair)
let all_recursive_child_events = self.recursive_child_events(event: selected_event)
self.sorted_child_events = all_recursive_child_events.filter({
should_show_event(event: $0, damus_state: damus) // Hide muted events from chatroom conversation
}).sorted(by: { a, b in
return a.created_at < b.created_at
})
}
func recursive_child_events(event: NdbNote) -> [NdbNote] {
let immediate_children = damus.events.child_events(event: event)
var indirect_children: [NdbNote] = []
for immediate_child in immediate_children {
indirect_children.append(contentsOf: self.recursive_child_events(event: immediate_child))
}
return immediate_children + indirect_children
}
func go_to_event(scroller: ScrollViewProxy, note_id: NoteId) {
scroll_to_event(scroller: scroller, id: note_id, delay: 0, animate: true, anchor: .top)
selected_note_id = note_id
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
withAnimation {
selected_note_id = nil
}
})
}
func set_active_event(scroller: ScrollViewProxy, ev: NdbNote) {
withAnimation {
self.compute_events(selected_event: ev)
thread.set_active_event(ev, keypair: self.damus.keypair)
self.go_to_event(scroller: scroller, note_id: ev.id)
}
}
var body: some View {
ScrollViewReader { scroller in
ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: 8) {
// MARK: - Parents events view
ForEach(parent_events, id: \.id) { parent_event in
EventMutingContainerView(damus_state: damus, event: parent_event) {
EventView(damus: damus, event: parent_event)
.matchedGeometryEffect(id: parent_event.id.hex(), in: animation, anchor: .center)
}
.padding(.horizontal)
.onTapGesture {
self.set_active_event(scroller: scroller, ev: parent_event)
}
.id(parent_event.id)
Divider()
.padding(.top, 4)
.padding(.leading, 25 * 2)
}.background(GeometryReader { geometry in
// get the height and width of the EventView view
let eventHeight = geometry.frame(in: .global).height
// let eventWidth = geometry.frame(in: .global).width
// vertical gray line in the background
Rectangle()
.fill(Color.gray.opacity(0.25))
.frame(width: 2, height: eventHeight)
.offset(x: 40, y: 40)
})
// MARK: - Actual event view
EventMutingContainerView(
damus_state: damus,
event: self.thread.event,
muteBox: { event_shown, muted_reason in
AnyView(
EventMutedBoxView(shown: event_shown, reason: muted_reason)
.padding(5)
)
}
) {
SelectedEventView(damus: damus, event: self.thread.event, size: .selected)
.matchedGeometryEffect(id: self.thread.event.id.hex(), in: animation, anchor: .center)
}
.id(self.thread.event.id)
// MARK: - Children view
let events = sorted_child_events
let count = events.count
SwipeViewGroup {
ForEach(Array(zip(events, events.indices)), id: \.0.id) { (ev, ind) in
ChatEventView(event: events[ind],
selected_event: self.thread.event,
prev_ev: ind > 0 ? events[ind-1] : nil,
next_ev: ind == count-1 ? nil : events[ind+1],
damus_state: damus,
thread: thread,
scroll_to_event: { note_id in
self.go_to_event(scroller: scroller, note_id: note_id)
},
focus_event: {
self.set_active_event(scroller: scroller, ev: ev)
},
highlight_bubble: selected_note_id == ev.id,
bar: make_actionbar_model(ev: ev.id, damus: damus)
)
.padding(.horizontal)
.id(ev.id)
.matchedGeometryEffect(id: ev.id.hex(), in: animation, anchor: .center)
}
}
}
.padding(.top)
EndBlock()
}
.onReceive(handle_notify(.post), perform: { notify in
switch notify {
case .post(_):
user_just_posted_flag = true
case .cancel:
return
}
})
.onReceive(thread.objectWillChange) {
self.compute_events()
if let last_event = thread.events().last, last_event.pubkey == damus.pubkey, user_just_posted_flag {
self.go_to_event(scroller: scroller, note_id: last_event.id)
user_just_posted_flag = false
}
}
.onAppear() {
thread.subscribe()
self.compute_events()
scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.1, animate: false)
}
.onDisappear() {
thread.unsubscribe()
}
}
}
func toggle_thread_view() {
NotificationCenter.default.post(name: .toggle_thread_view, object: nil)
}
}
struct ChatroomView_Previews: PreviewProvider {
static var previews: some View {
Group {
ChatroomThreadView(damus: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state))
.previewDisplayName("Test note")
let test_thread = ThreadModel(event: test_thread_note_1, damus_state: test_damus_state)
ChatroomThreadView(damus: test_damus_state, thread: test_thread)
.onAppear {
test_thread.add_event(test_thread_note_2, keypair: test_keypair)
test_thread.add_event(test_thread_note_3, keypair: test_keypair)
test_thread.add_event(test_thread_note_4, keypair: test_keypair)
test_thread.add_event(test_thread_note_5, keypair: test_keypair)
test_thread.add_event(test_thread_note_6, keypair: test_keypair)
test_thread.add_event(test_thread_note_7, keypair: test_keypair)
}
}
}
}
func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
scroll_to_event(scroller: proxy, id: thread.event.id, delay: 0.1, animate: false)
}

View File

@@ -0,0 +1,70 @@
//
// ReplyQuoteView.swift
// damus
//
// Created by William Casarin on 2022-04-19.
//
import SwiftUI
struct ReplyQuoteView: View {
let keypair: Keypair
let quoter: NostrEvent
let event_id: NoteId
let state: DamusState
@ObservedObject var thread: ThreadModel
let options: EventViewOptions
func content(event: NdbNote) -> some View {
ZStack(alignment: .leading) {
VStack(alignment: .leading) {
HStack(alignment: .center) {
if should_show_event(event: event, damus_state: state) {
ProfilePicView(pubkey: event.pubkey, size: 14, highlight: .reply, profiles: state.profiles, disable_animation: false)
let blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event, our_pubkey: state.pubkey)
NoteContentView(damus_state: state, event: event, blur_images: blur_images, size: .small, options: options)
.font(.callout)
.lineLimit(1)
.padding(.bottom, -7)
.padding(.top, -5)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 20)
.clipped()
}
else {
Text("Note you've muted", comment: "Label indicating note has been muted")
.italic()
.font(.caption)
.opacity(0.5)
.padding(.bottom, -7)
.padding(.top, -5)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 20)
.clipped()
}
}
}
.padding(5)
.padding(.leading, 5+3)
Rectangle()
.foregroundStyle(.accent)
.frame(width: 3)
}
}
var body: some View {
Group {
if let event = state.events.lookup(event_id) {
self.content(event: event)
}
}
}
}
struct ReplyQuoteView_Previews: PreviewProvider {
static var previews: some View {
let s = test_damus_state
let quoter = test_note
ReplyQuoteView(keypair: s.keypair, quoter: quoter, event_id: test_note.id, state: s, thread: ThreadModel(event: quoter, damus_state: s), options: [.no_previews, .no_action_bar, .truncate_content_very_short, .no_show_more, .no_translate])
}
}

View File

@@ -25,68 +25,44 @@ struct CreateAccountView: View {
var body: some View {
ZStack(alignment: .top) {
VStack {
Spacer()
VStack(alignment: .center) {
EditPictureControl(uploader: .nostrBuild, pubkey: account.pubkey, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture)
Text("Public Key", comment: "Label to indicate the public key of the account.")
EditPictureControl(uploader: .nostrBuild, pubkey: account.pubkey, size: 75, setup: true, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture)
.shadow(radius: 2)
.padding(.top, 100)
Text("Add Photo", comment: "Label to indicate user can add a photo.")
.bold()
.padding()
.onTapGesture {
regen_key()
}
KeyText($account.pubkey)
.padding(.horizontal, 20)
.onTapGesture {
regen_key()
}
}
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 250, alignment: .center)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(DamusColors.adaptableGrey, strokeBorder: .gray.opacity(0.5), lineWidth: 1)
.foregroundColor(DamusColors.neutral6)
}
SignupForm {
FormLabel(NSLocalizedString("Display name", comment: "Label to prompt display name entry."), optional: true)
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.real_name)
FormLabel(NSLocalizedString("Name", comment: "Label to prompt name entry."), optional: false)
.foregroundColor(DamusColors.neutral6)
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.name)
.textInputAutocapitalization(.words)
FormLabel(NSLocalizedString("About", comment: "Label to prompt for about text entry for user to describe about themself."), optional: true)
FormTextInput(NSLocalizedString("Creator(s) of Bitcoin. Absolute legend.", comment: "Example description about Bitcoin creator(s), Satoshi Nakamoto."), text: $account.about)
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, 10)
.padding(.top, 25)
Button(action: {
nav.push(route: Route.SaveKeys(account: account))
}) {
HStack {
Text("Create account now", comment: "Button to create account.")
Text("Next", comment: "Button to continue with account creation.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.disabled(profileUploadObserver.isLoading)
.opacity(profileUploadObserver.isLoading ? 0.5 : 1)
.disabled(profileUploadObserver.isLoading || account.name.isEmpty)
.opacity(profileUploadObserver.isLoading || account.name.isEmpty ? 0.5 : 1)
.padding(.top, 20)
HStack(spacing: 0) {
Text("By signing up, you agree to our ", comment: "Ask the user if they already have an account on Nostr")
.font(.subheadline)
.foregroundColor(Color("DamusMediumGrey"))
Button(action: {
nav.push(route: Route.EULA)
}, label: {
Text("EULA")
.font(.subheadline)
})
.padding(.vertical, 5)
Spacer()
}
LoginPrompt()
.padding(.top)
@@ -94,8 +70,8 @@ struct CreateAccountView: View {
}
.padding()
}
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
.dismissKeyboardOnTap()
.navigationTitle("Create account")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
@@ -111,7 +87,7 @@ struct LoginPrompt: View {
var body: some View {
HStack {
Text("Already on Nostr?", comment: "Ask the user if they already have an account on Nostr")
.foregroundColor(Color("DamusMediumGrey"))
.foregroundColor(DamusColors.neutral6)
Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) {
self.dismiss()
@@ -127,8 +103,8 @@ struct BackNav: View {
var body: some View {
Image("chevron-left")
.foregroundColor(DamusColors.adaptableBlack)
.onTapGesture {
self.dismiss()
.onTapGesture {
self.dismiss()
}
}
}
@@ -148,20 +124,11 @@ extension View {
struct CreateAccountView_Previews: PreviewProvider {
static var previews: some View {
let model = CreateAccountModel(real: "", nick: "jb55", about: "")
let model = CreateAccountModel(display_name: "", name: "jb55", about: "")
return CreateAccountView(account: model, nav: .init())
}
}
func KeyText(_ pubkey: Binding<Pubkey>) -> some View {
let bechkey = bech32_encode(hrp: PUBKEY_HRP, pubkey.wrappedValue.bytes)
return Text(bechkey)
.textSelection(.enabled)
.multilineTextAlignment(.center)
.font(.callout.monospaced())
.foregroundStyle(DamusLogoGradient.gradient)
}
func FormTextInput(_ title: String, text: Binding<String>) -> some View {
return TextField("", text: text)
.placeholder(when: text.wrappedValue.isEmpty) {
@@ -171,6 +138,10 @@ func FormTextInput(_ title: String, text: Binding<String>) -> some View {
.background {
RoundedRectangle(cornerRadius: 12)
.stroke(.gray.opacity(0.5), lineWidth: 1)
.background {
RoundedRectangle(cornerRadius: 12)
.foregroundColor(.damusAdaptableWhite)
}
}
.font(.body.bold())
}
@@ -183,6 +154,10 @@ func FormLabel(_ title: String, optional: Bool = false) -> some View {
Text("optional", comment: "Label indicating that a form input is optional.")
.font(.callout)
.foregroundColor(DamusColors.mediumGrey)
} else {
Text("required", comment: "Label indicating that a form input is required.")
.font(.callout)
.foregroundColor(DamusColors.mediumGrey)
}
}
}

View File

@@ -45,6 +45,8 @@ struct EventView: View {
}
} else if event.known_kind == .longform {
LongformPreview(state: damus, ev: event, options: options)
} else if event.known_kind == .highlight {
HighlightView(state: damus, event: event, options: options)
} else {
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
//.padding([.top], 6)

View File

@@ -16,7 +16,15 @@ struct ReplyPart: View {
var body: some View {
Group {
if let reply_ref = event.thread_reply()?.reply {
ReplyDescription(event: event, replying_to: events.lookup(reply_ref.note_id), ndb: ndb)
let replying_to = events.lookup(reply_ref.note_id)
if event.known_kind != .highlight {
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
} else if event.known_kind == .highlight {
HighlightDescription(event: event, highlighted_event: replying_to, ndb: ndb)
}
else {
EmptyView()
}
} else {
EmptyView()
}

View File

@@ -35,6 +35,8 @@ struct EventBody: View {
if !options.contains(.truncate_content) {
note_content
}
} else if event.known_kind == .highlight {
HighlightBodyView(state: damus_state, ev: event, options: options)
} else {
note_content
}

View File

@@ -0,0 +1,53 @@
//
// HighlightDescription.swift
// damus
//
// Created by eric on 4/28/24.
//
import SwiftUI
// Modified from Reply Description
struct HighlightDescription: View {
let event: NostrEvent
let highlighted_event: NostrEvent?
let ndb: Ndb
var body: some View {
(Text(Image(systemName: "highlighter")) + Text(verbatim: " \(highlight_desc(ndb: ndb, event: event, highlighted_event: highlighted_event))"))
.font(.footnote)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct HighlightDescription_Previews: PreviewProvider {
static var previews: some View {
HighlightDescription(event: test_note, highlighted_event: test_note, ndb: test_damus_state.ndb)
}
}
func highlight_desc(ndb: Ndb, event: NostrEvent, highlighted_event: NostrEvent?, locale: Locale = Locale.current) -> String {
let desc = make_reply_description(event, replying_to: highlighted_event)
let pubkeys = desc.pubkeys
let bundle = bundleForLocale(locale: locale)
if pubkeys.count == 0 {
return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.")
}
guard let profile_txn = NdbTxn(ndb: ndb) else {
return ""
}
let names: [String] = pubkeys.map { pk in
let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn)
return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50)
}
let uniqueNames: [String] = Array(Set(names))
return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames.first ?? "")
}

View File

@@ -0,0 +1,92 @@
//
// HighlightEventRef.swift
// damus
//
// Created by eric on 4/29/24.
//
import SwiftUI
import Kingfisher
struct HighlightEventRef: View {
let damus_state: DamusState
let event_ref: NoteId
init(damus_state: DamusState, event_ref: NoteId) {
self.damus_state = damus_state
self.event_ref = event_ref
}
struct FailedImage: View {
var body: some View {
Image("markdown")
.resizable()
.foregroundColor(DamusColors.neutral6)
.background(DamusColors.neutral3)
.frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
.scaledToFit()
}
}
var body: some View {
EventLoaderView(damus_state: damus_state, event_id: event_ref) { event in
EventMutingContainerView(damus_state: damus_state, event: event) {
if event.known_kind == .longform {
HStack(alignment: .top, spacing: 10) {
let longform_event = LongformEvent.parse(from: event)
if let url = longform_event.image {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: true)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.background {
FailedImage()
}
.frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
.scaledToFit()
} else {
FailedImage()
}
VStack(alignment: .leading, spacing: 5) {
Text(longform_event.title ?? "Untitled")
.font(.system(size: 14, weight: .bold))
.lineLimit(1)
let profile_txn = damus_state.profiles.lookup(id: longform_event.event.pubkey, txn_name: "highlight-profile")
let profile = profile_txn?.unsafeUnownedValue
if let display_name = profile?.display_name {
Text(display_name)
.font(.system(size: 12))
.foregroundColor(.gray)
} else if let name = profile?.name {
Text(name)
.font(.system(size: 12))
.foregroundColor(.gray)
}
}
}
.padding([.leading, .vertical], 7)
.frame(maxWidth: .infinity, alignment: .leading)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 2)
)
} else {
EmptyView()
}
}
}
}
}

View File

@@ -0,0 +1,101 @@
//
// HighlightLink.swift
// damus
//
// Created by eric on 4/28/24.
//
import SwiftUI
import Kingfisher
struct HighlightLink: View {
let state: DamusState
let url: URL
let content: String
@Environment(\.openURL) var openURL
func text_fragment_url() -> URL? {
let fragmentDirective = "#:~:"
let textDirective = "text="
let separator = ","
var text = ""
let components = content.components(separatedBy: " ")
if components.count <= 10 {
text = content
} else {
let textStart = Array(components.prefix(5)).joined(separator: " ")
let textEnd = Array(components.suffix(2)).joined(separator: " ")
text = textStart + separator + textEnd
}
let url_with_fragments = url.absoluteString + fragmentDirective + textDirective + text
return URL(string: url_with_fragments)
}
func get_url_icon() -> URL? {
var icon = URL(string: url.absoluteString + "/favicon.ico")
if let url_host = url.host() {
icon = URL(string: "https://" + url_host + "/favicon.ico")
}
return icon
}
var body: some View {
Button(action: {
openURL(text_fragment_url() ?? url)
}, label: {
HStack(spacing: 10) {
if let url = get_url_icon() {
KFAnimatedImage(url)
.imageContext(.pfp, disable_animation: true)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.placeholder { _ in
Image("link")
.resizable()
.padding(5)
.foregroundColor(DamusColors.neutral6)
.background(DamusColors.adaptableWhite)
}
.frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 10))
.scaledToFit()
} else {
Image("link")
.resizable()
.padding(5)
.foregroundColor(DamusColors.neutral6)
.background(DamusColors.adaptableWhite)
.frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
Text(url.absoluteString)
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
.foregroundColor(DamusColors.adaptableBlack)
.truncationMode(.tail)
.lineLimit(1)
}
.padding([.leading, .vertical], 7)
.frame(maxWidth: .infinity, alignment: .leading)
.background(DamusColors.neutral3)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral3, lineWidth: 2)
)
})
}
}
struct HighlightLink_Previews: PreviewProvider {
static var previews: some View {
let url = URL(string: "https://damus.io")!
VStack {
HighlightLink(state: test_damus_state, url: url, content: "")
}
}
}

View File

@@ -0,0 +1,78 @@
//
// HighlightPostView.swift
// damus
//
// Created by eric on 5/26/24.
//
import SwiftUI
struct HighlightPostView: View {
let damus_state: DamusState
let event: NostrEvent
@Binding var selectedText: String
@Environment(\.dismiss) var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 0) {
VStack {
HStack(spacing: 5.0) {
Button(action: {
dismiss()
}, label: {
Text("Cancel", comment: "Button to cancel out of highlighting a note.")
.padding(10)
})
.buttonStyle(NeutralButtonStyle())
Spacer()
Button(NSLocalizedString("Post", comment: "Button to post a highlight.")) {
var tags: [[String]] = [ ["e", "\(self.event.id)"] ]
tags.append(["context", self.event.content])
let kind = NostrKind.highlight.rawValue
guard let ev = NostrEvent(content: selectedText, keypair: damus_state.keypair, kind: kind, tags: tags) else {
return
}
damus_state.postbox.send(ev)
dismiss()
}
.bold()
.buttonStyle(GradientButtonStyle(padding: 10))
}
Divider()
.foregroundColor(DamusColors.neutral3)
.padding(.top, 5)
}
.frame(height: 30)
.padding()
.padding(.top, 15)
HStack {
var attributedString: AttributedString {
var attributedString = AttributedString(self.event.content)
if let range = attributedString.range(of: selectedText) {
attributedString[range].backgroundColor = DamusColors.highlight
}
return attributedString
}
Text(attributedString)
.lineSpacing(5)
.padding(10)
}
.overlay(
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
alignment: .leading
)
.padding()
Spacer()
}
}
}

View File

@@ -0,0 +1,192 @@
//
// HighlightView.swift
// damus
//
// Created by eric on 4/22/24.
//
import SwiftUI
import Kingfisher
struct HighlightTruncatedText: View {
let attributedString: AttributedString
let maxChars: Int
init(attributedString: AttributedString, maxChars: Int = 360) {
self.attributedString = attributedString
self.maxChars = maxChars
}
var body: some View {
VStack(alignment: .leading) {
let truncatedAttributedString: AttributedString? = attributedString.truncateOrNil(maxLength: maxChars)
if let truncatedAttributedString {
Text(truncatedAttributedString)
.fixedSize(horizontal: false, vertical: true)
} else {
Text(attributedString)
.fixedSize(horizontal: false, vertical: true)
}
if truncatedAttributedString != nil {
Spacer()
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
.allowsHitTesting(false)
}
}
}
}
struct HighlightBodyView: View {
let state: DamusState
let event: HighlightEvent
let options: EventViewOptions
init(state: DamusState, ev: HighlightEvent, options: EventViewOptions) {
self.state = state
self.event = ev
self.options = options
}
init(state: DamusState, ev: NostrEvent, options: EventViewOptions) {
self.state = state
self.event = HighlightEvent.parse(from: ev)
self.options = options
}
var body: some View {
Group {
if options.contains(.wide) {
Main.padding(.horizontal)
} else {
Main
}
}
}
var truncate: Bool {
return options.contains(.truncate_content)
}
var truncate_very_short: Bool {
return options.contains(.truncate_content_very_short)
}
func truncatedText(attributedString: AttributedString) -> some View {
Group {
if truncate_very_short {
HighlightTruncatedText(attributedString: attributedString, maxChars: 140)
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
}
else if truncate {
HighlightTruncatedText(attributedString: attributedString)
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
} else {
Text(attributedString)
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
}
}
}
var Main: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
var attributedString: AttributedString {
var attributedString: AttributedString = ""
if let context = event.context {
if context.count < event.event.content.count {
attributedString = AttributedString(event.event.content)
} else {
attributedString = AttributedString(context)
}
} else {
attributedString = AttributedString(event.event.content)
}
if let range = attributedString.range(of: event.event.content) {
attributedString[range].backgroundColor = DamusColors.highlight
}
return attributedString
}
truncatedText(attributedString: attributedString)
.lineSpacing(5)
.padding(10)
}
.overlay(
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
alignment: .leading
)
.padding(.bottom, 10)
if let url = event.url_ref {
HighlightLink(state: state, url: url, content: event.event.content)
} else {
if let evRef = event.event_ref {
if let eventHex = hex_decode_id(evRef) {
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
.padding(.top, 5)
}
}
}
}
}
}
struct HighlightView: View {
let state: DamusState
let event: HighlightEvent
let options: EventViewOptions
init(state: DamusState, event: NostrEvent, options: EventViewOptions) {
self.state = state
self.event = HighlightEvent.parse(from: event)
self.options = options.union(.no_mentions)
}
var body: some View {
VStack(alignment: .leading) {
EventShell(state: state, event: event.event, options: options) {
HighlightBodyView(state: state, ev: event, options: options)
}
}
}
}
struct HighlightView_Previews: PreviewProvider {
static var previews: some View {
let content = "Nostr, a decentralized and open social network protocol. Without ads, toxic algorithms, or censorship"
let context = "Damus is built on Nostr, a decentralized and open social network protocol. Without ads, toxic algorithms, or censorship, Damus gives you access to the social network that a truly free and healthy society needs — and deserves."
let test_highlight_event = HighlightEvent.parse(from: NostrEvent(
content: content,
keypair: test_keypair,
kind: NostrKind.highlight.rawValue,
tags: [
["context", context],
["r", "https://damus.io"],
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
])!
)
let test_highlight_event2 = HighlightEvent.parse(from: NostrEvent(
content: content,
keypair: test_keypair,
kind: NostrKind.highlight.rawValue,
tags: [
["context", context],
["e", "36017b098859d62e1dbd802290d59c9de9f18bb0ca00ba4b875c2930dd5891ae"],
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
])!
)
VStack {
HighlightView(state: test_damus_state, event: test_highlight_event.event, options: [])
HighlightView(state: test_damus_state, event: test_highlight_event2.event, options: [.wide])
}
}
}

View File

@@ -51,13 +51,13 @@ struct LongformPreviewBody: View {
func truncatedText(content: CompatibleText) -> some View {
Group {
if truncate_very_short {
TruncatedText(text: content, maxChars: 140)
TruncatedText(text: content, maxChars: 140, show_show_more_button: !options.contains(.no_show_more))
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
}
else if truncate {
TruncatedText(text: content)
TruncatedText(text: content, show_show_more_button: !options.contains(.no_show_more))
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)

View File

@@ -21,10 +21,10 @@ struct LongformView: View {
var options: EventViewOptions {
return [.wide, .no_mentions, .no_replying_to]
}
var body: some View {
EventShell(state: state, event: event.event, options: options) {
SelectableText(attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title)
SelectableText(damus_state: state, event: event.event, attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title)
NoteContentView(damus_state: state, event: event.event, blur_images: false, size: .selected, options: options)
}

View File

@@ -41,7 +41,16 @@ struct SelectedEventView: View {
.lineLimit(1)
if let reply_ref = event.thread_reply()?.reply {
ReplyDescription(event: event, replying_to: damus.events.lookup(reply_ref.note_id), ndb: damus.ndb)
let replying_to = damus.events.lookup(reply_ref.note_id)
if event.known_kind == .highlight {
HighlightDescription(event: event, highlighted_event: replying_to, ndb: damus.ndb)
.padding(.horizontal)
} else {
ReplyDescription(event: event, replying_to: replying_to, ndb: damus.ndb)
.padding(.horizontal)
}
} else if event.known_kind == .highlight {
HighlightDescription(event: event, highlighted_event: nil, ndb: damus.ndb)
.padding(.horizontal)
}

View File

@@ -21,9 +21,11 @@ struct EventViewOptions: OptionSet {
static let no_mentions = EventViewOptions(rawValue: 1 << 9)
static let no_media = EventViewOptions(rawValue: 1 << 10)
static let truncate_content_very_short = EventViewOptions(rawValue: 1 << 11)
static let no_previews = EventViewOptions(rawValue: 1 << 12)
static let no_show_more = EventViewOptions(rawValue: 1 << 13)
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short]
static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short, .no_previews]
}
struct TextEvent: View {

View File

@@ -62,8 +62,9 @@ struct LoginView: View {
var body: some View {
ZStack(alignment: .top) {
VStack {
Spacer()
SignInHeader()
.padding(.top, 100)
SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey)
@@ -112,8 +113,9 @@ struct LoginView: View {
Spacer()
}
.padding()
.padding(.bottom, 50)
}
.background(DamusBackground(maxHeight: 350), alignment: .top)
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
.onAppear {
credential_handler.check_credentials()
}
@@ -320,9 +322,13 @@ struct KeyInput: View {
}
.padding(.vertical, 2)
.padding(.horizontal, 10)
.overlay {
.background {
RoundedRectangle(cornerRadius: 12)
.stroke(.gray, lineWidth: 1)
.background {
RoundedRectangle(cornerRadius: 12)
.foregroundColor(.damusAdaptableWhite)
}
}
}
}
@@ -337,11 +343,12 @@ struct SignInHeader: View {
.padding(.bottom)
Text("Sign in", comment: "Title of view to log into an account.")
.foregroundColor(DamusColors.neutral6)
.font(.system(size: 32, weight: .bold))
.padding(.bottom, 5)
Text("Welcome to the social network you control", comment: "Welcome text")
.foregroundColor(Color("DamusMediumGrey"))
.foregroundColor(DamusColors.neutral6)
}
}
}
@@ -353,6 +360,7 @@ struct SignInEntry: View {
var body: some View {
VStack(alignment: .leading) {
Text("Enter your account key", comment: "Prompt for user to enter an account key to login.")
.foregroundColor(DamusColors.neutral6)
.fontWeight(.medium)
.padding(.top, 30)
@@ -444,7 +452,9 @@ struct LoginView_Previews: PreviewProvider {
let bech32_pubkey = "KeyInput"
Group {
LoginView(key: pubkey, nav: .init())
.previewDevice(PreviewDevice(rawValue: "iPhone SE (3rd generation)"))
LoginView(key: bech32_pubkey, nav: .init())
.previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro Max"))
}
}
}

View File

@@ -78,11 +78,11 @@ struct NoteContentView: View {
func truncatedText(content: CompatibleText) -> some View {
Group {
if truncate_very_short {
TruncatedText(text: content, maxChars: 140)
TruncatedText(text: content, maxChars: 140, show_show_more_button: !options.contains(.no_show_more))
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
}
else if truncate {
TruncatedText(text: content)
TruncatedText(text: content, show_show_more_button: !options.contains(.no_show_more))
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
} else {
content.text
@@ -132,10 +132,10 @@ struct NoteContentView: View {
VStack(alignment: .leading) {
if size == .selected {
if with_padding {
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size)
.padding(.horizontal)
} else {
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size)
}
} else {
if with_padding {
@@ -185,18 +185,22 @@ struct NoteContentView: View {
invoicesView(invoices: artifacts.invoices)
}
}
if damus_state.settings.media_previews {
if damus_state.settings.media_previews, has_previews {
if with_padding {
previewView(links: artifacts.links).padding(.horizontal)
} else {
previewView(links: artifacts.links)
}
}
}
}
var has_previews: Bool {
!options.contains(.no_previews)
}
func loadMediaButton(artifacts: NoteArtifactsSeparated) -> some View {
Button(action: {
load_media = true
@@ -397,6 +401,14 @@ struct NoteContentView_Previews: PreviewProvider {
.border(Color.red)
}
.previewDisplayName("Long-form note")
VStack {
NoteContentView(damus_state: state, event: test_note, blur_images: false, size: .small, options: [.no_previews, .no_action_bar, .truncate_content_very_short, .no_show_more])
.font(.callout)
.foregroundColor(.secondary)
.lineLimit(1)
}
.previewDisplayName("Small single-line note")
}
}
}

View File

@@ -26,7 +26,7 @@ struct AboutView: View {
Group {
if let about_string {
let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length)
SelectableText(attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline)
SelectableText(damus_state: state, event: nil, attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline)
if truncated_about != nil {
if show_full_about {

View File

@@ -6,6 +6,7 @@
//
import SwiftUI
import Kingfisher
class ImageUploadingObserver: ObservableObject {
@Published var isLoading: Bool = false
@@ -14,6 +15,8 @@ class ImageUploadingObserver: ObservableObject {
struct EditPictureControl: View {
let uploader: MediaUploader
let pubkey: Pubkey
var size: CGFloat? = 25
var setup: Bool? = false
@Binding var image_url: URL?
@ObservedObject var uploadObserver: ImageUploadingObserver
let callback: (URL?) -> Void
@@ -43,20 +46,53 @@ struct EditPictureControl: View {
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 {
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)
.foregroundColor(DamusColors.white)
.clipShape(Circle())
.overlay(Circle().stroke(.white, lineWidth: 4))
} else {
Image("camera")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(DamusColors.purple)
.padding(10)
.background(DamusColors.white.opacity(0.7))
.clipShape(Circle())
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
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) {
@@ -110,7 +146,7 @@ struct EditPictureControl_Previews: PreviewProvider {
let observer = ImageUploadingObserver()
ZStack {
Color.gray
EditPictureControl(uploader: .nostrBuild, pubkey: test_pubkey, image_url: url, uploadObserver: observer) { _ in
EditPictureControl(uploader: .nostrBuild, pubkey: test_pubkey, size: 100, setup: false, image_url: url, uploadObserver: observer) { _ in
//
}
}

View File

@@ -70,6 +70,7 @@ struct ProfileView: View {
@State var show_share_sheet: Bool = false
@State var show_qr_code: Bool = false
@State var action_sheet_presented: Bool = false
@State var mute_dialog_presented: Bool = false
@State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0
@@ -162,7 +163,10 @@ struct ProfileView: View {
Button(action: {
action_sheet_presented = true
}) {
navImage(img: "share3")
Image(systemName: "ellipsis")
.frame(width: 33, height: 33)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or mute a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
@@ -196,15 +200,21 @@ struct ProfileView: View {
damus_state.postbox.send(new_ev)
}
} else {
MuteDurationMenu { duration in
notify(.mute(.user(profile.pubkey, duration?.date_from_now)))
} label: {
Text("Mute", comment: "Button to mute a profile.")
.foregroundStyle(.red)
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
mute_dialog_presented = true
}
}
}
}
.confirmationDialog(NSLocalizedString("Mute", comment: "Title for confirmation dialog to mute a profile."), isPresented: $mute_dialog_presented) {
ForEach(DamusDuration.allCases, id: \.self) { duration in
Button {
notify(.mute(.user(profile.pubkey, duration.date_from_now)))
} label: {
Text(duration.title)
}
}
}
}
var customNavbar: some View {

View File

@@ -16,7 +16,7 @@ struct ReactionsView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(model.events.events, id: \.id) { ev in
ForEach(model.events.events.filter { $0.last_refid() == model.target }, id: \.id) { ev in
ReactionView(damus_state: damus_state, reaction: ev)
}
}

View File

@@ -11,8 +11,6 @@ import Security
struct SaveKeysView: View {
let account: CreateAccountModel
let pool: RelayPool = RelayPool(ndb: Ndb()!)
@State var pub_copied: Bool = false
@State var priv_copied: Bool = false
@State var loading: Bool = false
@State var error: String? = nil
@@ -31,81 +29,98 @@ struct SaveKeysView: View {
var body: some View {
ZStack(alignment: .top) {
VStack(alignment: .center) {
Spacer()
Image("logo-nobg")
.resizable()
.shadow(color: DamusColors.purple, radius: 2)
.frame(width: 56, height: 56, alignment: .center)
.padding(.top, 20.0)
if account.rendered_name.isEmpty {
Text("Welcome!", comment: "Text to welcome user.")
.font(.title.bold())
.padding(.bottom, 10)
.font(.title)
.fontWeight(.heavy)
.foregroundStyle(DamusLogoGradient.gradient)
} else {
Text("Welcome, \(account.rendered_name)!", comment: "Text to welcome user.")
.font(.title.bold())
.padding(.bottom, 10)
.font(.title)
.fontWeight(.heavy)
.foregroundStyle(DamusLogoGradient.gradient)
}
Text("Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.", comment: "Reminder to user that they should save their account information.")
.padding(.bottom, 10)
Text("Save your login info?", comment: "Ask user if they want to save their account information.")
.font(.title)
.fontWeight(.heavy)
.foregroundColor(DamusColors.neutral6)
.padding(.top, 5)
Text("Private Key", comment: "Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account.")
.font(.title2.bold())
.padding(.bottom, 10)
Text("We'll save your account key, so you won't need to enter it manually next time you log in.", comment: "Reminder to user that they should save their account information.")
.font(.system(size: 14))
.foregroundColor(DamusColors.neutral6)
.padding(.top, 2)
.padding(.bottom, 100)
.multilineTextAlignment(.center)
Text("This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!", comment: "Label to describe that a private key is the user's secret account key and what they should do with it.")
.padding(.bottom, 10)
Spacer()
SaveKeyView(text: account.privkey.nsec, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused)
.padding(.bottom, 10)
if priv_copied {
if loading {
ProgressView()
.progressViewStyle(.circular)
} else if let err = error {
Text("Error: \(err)", comment: "Error message indicating why saving keys failed.")
.foregroundColor(.red)
Button(action: {
complete_account_creation(account)
}) {
HStack {
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
if loading {
ProgressView()
.progressViewStyle(.circular)
} else if let err = error {
Text("Error: \(err)", comment: "Error message indicating why saving keys failed.")
.foregroundColor(.red)
Button(action: {
complete_account_creation(account)
}) {
HStack {
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
.fontWeight(.semibold)
}
.buttonStyle(GradientButtonStyle())
.padding(.top, 20)
} else {
Button(action: {
complete_account_creation(account)
}) {
HStack {
Text("Let's go!", comment: "Button to complete account creation and start using the app.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.top, 20)
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.top, 20)
} else {
Button(action: {
save_key(account)
complete_account_creation(account)
}) {
HStack {
Text("Save", comment: "Button to save key, complete account creation, and start using the app.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.top, 20)
Button(action: {
complete_account_creation(account)
}) {
HStack {
Text("Not now", comment: "Button to not save key, complete account creation, and start using the app.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 12))
.padding(.top, 20)
}
}
.padding(20)
}
.background(
Image("eula-bg")
.resizable()
.blur(radius: 70)
.ignoresSafeArea(),
alignment: .top
)
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
.onAppear {
// Hack to force keyboard to show up for a short moment and then hiding it to register password autofill flow.
pubkey_focused = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
pubkey_focused = false
}
}
}
func save_key(_ account: CreateAccountModel) {
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
}
func complete_account_creation(_ account: CreateAccountModel) {
@@ -122,8 +137,6 @@ struct SaveKeysView: View {
}
self.pool.register_handler(sub_id: "signup", handler: handle_event)
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
self.loading = true
@@ -188,74 +201,13 @@ struct SaveKeysView: View {
}
}
struct SaveKeyView: View {
let text: String
let textContentType: UITextContentType
@Binding var is_copied: Bool
var focus: FocusState<Bool>.Binding
func copy_text() {
UIPasteboard.general.string = text
is_copied = true
}
var body: some View {
HStack {
Spacer()
VStack {
spacerBlock(width: 0, height: 0)
Button(action: copy_text) {
Label("", image: is_copied ? "check-circle.fill" : "copy2")
.foregroundColor(is_copied ? .green : .gray)
.background {
if is_copied {
Circle()
.foregroundColor(.white)
.frame(width: 25, height: 25, alignment: .center)
.padding(.leading, -8)
.padding(.top, 1)
} else {
EmptyView()
}
}
}
}
TextField("", text: .constant(text))
.padding(5)
.background {
RoundedRectangle(cornerRadius: 4.0).opacity(0.1)
}
.textSelection(.enabled)
.font(.callout.monospaced())
.onTapGesture {
copy_text()
// Hack to force keyboard to hide. Showing keyboard on text field is necessary to register password autofill flow but the text itself should not be modified.
DispatchQueue.main.async {
end_editing()
}
}
.textContentType(textContentType)
.deleteDisabled(true)
.focused(focus)
spacerBlock(width: 0, height: 0) /// set a 'width' > 0 here to vary key Text's aspect ratio
}
}
@ViewBuilder private func spacerBlock(width: CGFloat, height: CGFloat) -> some View {
Color.orange.opacity(1)
.frame(width: width, height: height)
}
}
struct SaveKeysView_Previews: PreviewProvider {
static var previews: some View {
let model = CreateAccountModel(real: "William", nick: "jb55", about: "I'm me")
let model = CreateAccountModel(display_name: "William", name: "jb55", about: "I'm me")
SaveKeysView(account: model)
}
}
func create_account_to_metadata(_ model: CreateAccountModel) -> Profile {
return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
return Profile(name: model.name, display_name: model.display_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
}

View File

@@ -6,22 +6,20 @@
//
import SwiftUI
import MCEmojiPicker
import EmojiPicker
import EmojiKit
struct ReactionsSettingsView: View {
@ObservedObject var settings: UserSettingsStore
let damus_state: DamusState
@State private var isReactionsVisible: Bool = false
@State private var selectedEmoji: Emoji? = nil
var body: some View {
Form {
Section {
Text(settings.default_emoji_reaction)
.emojiPicker(
isPresented: $isReactionsVisible,
selectedEmoji: $settings.default_emoji_reaction,
arrowDirection: .up,
isDismissAfterChoosing: true
)
.onTapGesture {
isReactionsVisible = true
}
@@ -31,43 +29,23 @@ struct ReactionsSettingsView: View {
}
.navigationTitle(NSLocalizedString("Reactions", comment: "Title of emoji reactions view"))
.navigationBarTitleDisplayMode(.large)
.sheet(isPresented: $isReactionsVisible) {
NavigationView {
EmojiPickerView(selectedEmoji: $selectedEmoji, emojiProvider: damus_state.emoji_provider)
}
.presentationDetents([.medium, .large])
}
.onChange(of: selectedEmoji) { newEmoji in
guard let newEmoji else {
return
}
settings.default_emoji_reaction = newEmoji.value
}
}
}
/// From: https://stackoverflow.com/a/39425959
extension Character {
/// A simple emoji is one scalar and presented to the user as an Emoji
var isSimpleEmoji: Bool {
guard let firstScalar = unicodeScalars.first else { return false }
return firstScalar.properties.isEmoji && firstScalar.value > 0x238C
}
/// Checks if the scalars will be merged into an emoji
var isCombinedIntoEmoji: Bool { unicodeScalars.count > 1 && unicodeScalars.first?.properties.isEmoji ?? false }
var isEmoji: Bool { isSimpleEmoji || isCombinedIntoEmoji }
}
extension String {
var isSingleEmoji: Bool { count == 1 && containsEmoji }
var containsEmoji: Bool { contains { $0.isEmoji } }
var containsOnlyEmoji: Bool { !isEmpty && !contains { !$0.isEmoji } }
var emojiString: String { emojis.map { String($0) }.reduce("", +) }
var emojis: [Character] { filter { $0.isEmoji } }
var emojiScalars: [UnicodeScalar] { filter { $0.isEmoji }.flatMap { $0.unicodeScalars } }
}
func isValidEmoji(_ string: String) -> Bool {
return string.isSingleEmoji
}
struct ReactionsSettingsView_Previews: PreviewProvider {
static var previews: some View {
ReactionsSettingsView(settings: UserSettingsStore())
ReactionsSettingsView(settings: UserSettingsStore(), damus_state: test_damus_state)
}
}

View File

@@ -28,32 +28,53 @@ struct SetupView: View {
.fontWeight(.heavy)
.foregroundStyle(DamusLogoGradient.gradient)
Text("The go-to iOS Nostr client", comment: "Quick description of what Damus is")
.foregroundColor(DamusColors.mediumGrey)
Text("The social network you control", comment: "Quick description of what Damus is")
.foregroundColor(DamusColors.neutral6)
.padding(.top, 10)
WhatIsNostr()
.padding()
WhyWeNeedNostr()
.padding()
Spacer()
Button(action: {
navigationCoordinator.push(route: Route.CreateAccount)
}) {
HStack {
Text("Create Account", comment: "Button to continue to the create account page.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.horizontal)
Button(action: {
navigationCoordinator.push(route: Route.Login)
}) {
HStack {
Text("Let's get started!", comment: "Button to continue to login page.")
Text("Sign In", comment: "Button to continue to login page.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding()
HStack(spacing: 0) {
Text("By continuing you agree to our ")
.font(.subheadline)
.foregroundColor(DamusColors.neutral6)
Button(action: {
navigationCoordinator.push(route: Route.EULA)
}, label: {
Text("EULA", comment: "End User License Agreement")
.font(.subheadline)
})
.padding(.vertical, 5)
}
.padding(.bottom)
}
}
.background(DamusBackground(maxHeight: 300), alignment: .top)
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
.navigationDestination(for: Route.self) { route in
route.view(navigationCoordinator: navigationCoordinator, damusState: DamusState.empty)
}
@@ -63,61 +84,6 @@ struct SetupView: View {
}
}
struct LearnAboutNostrLink: View {
@Environment(\.openURL) var openURL
var body: some View {
HStack {
Button(action: {
openURL(URL(string: "https://nostr.com")!)
}, label: {
Text("Learn more about Nostr", comment: "Button that opens up a webpage where the user can learn more about Nostr.")
.foregroundColor(.accentColor)
})
Image(systemName: "arrow.up.right")
.font(.footnote)
.foregroundColor(.accentColor)
}
}
}
struct WhatIsNostr: View {
var body: some View {
HStack(alignment: .top) {
Image("nostr-logo")
VStack(alignment: .leading) {
Text("What is Nostr?", comment: "Heading text for section describing what is Nostr.")
.fontWeight(.bold)
.padding(.vertical, 10)
Text("Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network", comment: "Description about what is Nostr.")
.foregroundColor(DamusColors.mediumGrey)
LearnAboutNostrLink()
.padding(.top, 10)
}
Spacer()
}
}
}
struct WhyWeNeedNostr: View {
var body: some View {
HStack(alignment: .top) {
Image("lightbulb")
VStack(alignment: .leading) {
Text("Why we need Nostr?", comment: "Heading text for section describing why Nostr is needed.")
.fontWeight(.bold)
.padding(.vertical, 10)
Text("Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken", comment: "Description about why Nostr is needed.")
.foregroundColor(DamusColors.mediumGrey)
}
Spacer()
}
}
}
struct SetupView_Previews: PreviewProvider {
static var previews: some View {
Group {

View File

@@ -7,6 +7,7 @@
import Foundation
@testable import damus
import EmojiPicker
// Generates a test damus state with configurable mock parameters
func generate_test_damus_state(
@@ -50,7 +51,9 @@ func generate_test_damus_state(
music: .init(onChange: {_ in }),
video: .init(),
ndb: ndb,
quote_reposts: .init(our_pubkey: our_pubkey) )
quote_reposts: .init(our_pubkey: our_pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: false)
)
return damus
}

View File

@@ -14,7 +14,7 @@ extension NdbNote {
}
func get_cached_inner_event(cache: EventCache) -> NdbNote? {
guard self.known_kind == .boost else {
guard self.known_kind == .boost || self.known_kind == .highlight else {
return nil
}

View File

@@ -277,7 +277,7 @@ class NdbNote: Encodable, Equatable, Hashable {
// Extension to make NdbNote compatible with NostrEvent's original API
extension NdbNote {
var is_textlike: Bool {
return kind == 1 || kind == 42 || kind == 30023
return kind == 1 || kind == 42 || kind == 30023 || kind == 9802
}
var is_quote_repost: NoteId? {