Compare commits
17 Commits
change-emo
...
reactions-
| Author | SHA1 | Date | |
|---|---|---|---|
|
847f31f5a6
|
|||
|
|
fd130b78e7 | ||
|
|
0be0273121 | ||
|
|
b349de22b7 | ||
|
|
cc2d196705 | ||
|
|
53be29efc2 | ||
|
|
529ee63f29 | ||
|
|
490e8ec1fb | ||
|
|
df267ffd04 | ||
|
|
b771e8f49a | ||
|
|
a88e80a346 | ||
|
8ac9863765
|
|||
|
|
4a851501a1 | ||
|
|
4ccfe81558 | ||
|
|
e7ed9dfe86 | ||
|
|
0dce7aea45 | ||
|
|
6376c61bad |
@@ -399,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 */; };
|
||||
@@ -406,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 */; };
|
||||
@@ -1334,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>"; };
|
||||
@@ -1341,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>"; };
|
||||
@@ -1668,6 +1680,7 @@
|
||||
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
|
||||
B533694D2B66D791008A805E /* MutelistManager.swift */,
|
||||
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
|
||||
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -2402,6 +2415,7 @@
|
||||
4CC7AAEE297F11B300430951 /* Events */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CC852A02BDED9970039FFC5 /* Highlight */,
|
||||
4CA927682A290F8F0098A105 /* Components */,
|
||||
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
|
||||
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
|
||||
@@ -2699,6 +2713,18 @@
|
||||
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 = (
|
||||
@@ -3153,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 */,
|
||||
@@ -3183,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 */,
|
||||
@@ -3322,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 */,
|
||||
@@ -3365,7 +3394,9 @@
|
||||
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 */,
|
||||
@@ -3477,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 */,
|
||||
@@ -4037,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;
|
||||
@@ -4063,6 +4096,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -4086,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;
|
||||
@@ -4112,6 +4147,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -28,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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
34
damus/Models/HighlightEvent.swift
Normal file
34
damus/Models/HighlightEvent.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
53
damus/Views/Events/Highlight/HighlightDescription.swift
Normal file
53
damus/Views/Events/Highlight/HighlightDescription.swift
Normal 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 ?? "")
|
||||
}
|
||||
92
damus/Views/Events/Highlight/HighlightEventRef.swift
Normal file
92
damus/Views/Events/Highlight/HighlightEventRef.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
damus/Views/Events/Highlight/HighlightLink.swift
Normal file
101
damus/Views/Events/Highlight/HighlightLink.swift
Normal 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: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
78
damus/Views/Events/Highlight/HighlightPostView.swift
Normal file
78
damus/Views/Events/Highlight/HighlightPostView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
192
damus/Views/Events/Highlight/HighlightView.swift
Normal file
192
damus/Views/Events/Highlight/HighlightView.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -52,7 +52,7 @@ func generate_test_damus_state(
|
||||
video: .init(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: our_pubkey),
|
||||
emoji_provider: DefaultEmojiProvider()
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: false)
|
||||
)
|
||||
|
||||
return damus
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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? {
|
||||
|
||||
Reference in New Issue
Block a user