Merge remote-tracking branch 'upstream/master' into change-emoji-component

This commit is contained in:
2024-06-19 18:33:58 -04:00
22 changed files with 1334 additions and 77 deletions

View File

@@ -34,6 +34,9 @@
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
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 */; };
@@ -494,6 +497,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 +828,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>"; };
@@ -1410,6 +1419,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>"; };
@@ -1475,6 +1486,7 @@
files = (
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */,
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */,
);
@@ -1996,6 +2008,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
D78DB85D2C20FE9E00F0AB12 /* Chat */,
D71AC4CA2BA8E3320076268E /* Extensions */,
BA3759952ABCCF360018D73B /* Camera */,
F71694E82A66221E001F4053 /* Onboarding */,
@@ -2692,6 +2705,7 @@
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4C7D09752A0AF19E00943473 /* FillAndStroke.swift */,
D72E12772BEED22400F4F781 /* Array.swift */,
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -2760,6 +2774,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 = (
@@ -2842,6 +2867,7 @@
4C06670328FC7EC500038D2A /* Kingfisher */,
4C27C9312A64766F007DBC75 /* MarkdownUI */,
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */,
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -2982,6 +3008,7 @@
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */,
3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */,
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -3235,6 +3262,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 +3271,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 */,
@@ -3331,6 +3360,7 @@
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 */,
@@ -3390,6 +3420,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 */,
@@ -3499,6 +3530,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 */,
@@ -4324,6 +4356,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";
@@ -4360,6 +4400,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,5 @@
{
"originHash" : "8c4ef1768f3ccbc97566169b88ce1c3a482207b6d3e394f2584f8749d5314dc6",
"originHash" : "babaf4d5748afecf49bbb702530d8e9576460692f478b0a50ee43195dd4440e2",
"pins" : [
{
"identity" : "emojikit",
@@ -88,6 +88,15 @@
"revision" : "4c50bff6c168f74425f70476be62a072980d2da7",
"version" : "0.1.2"
}
},
{
"identity" : "swipeactions",
"kind" : "remoteSourceControl",
"location" : "https://github.com/aheze/SwipeActions",
"state" : {
"revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab",
"version" : "1.1.0"
}
}
],
"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

@@ -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")

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

@@ -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
}

File diff suppressed because one or more lines are too long

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

@@ -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

@@ -8,12 +8,15 @@
import SwiftUI
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
@@ -23,12 +26,14 @@ struct EventActionBar: View {
@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? {
@@ -45,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) { 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)
}
@@ -151,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)
}
}
@@ -178,7 +310,6 @@ struct LikeButton: View {
@State private var isReactionsVisible = false
// @State private var selectedEmoji: String = ""
@State private var selectedEmoji: Emoji?
// Following four are Shaka animation properties
@@ -287,7 +418,6 @@ struct LikeButton: View {
}
}
struct EventActionBar_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state
@@ -312,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

@@ -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,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

@@ -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
@@ -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")
}
}
}