Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e456ac864d
|
|||
| 51ee4046a0 | |||
| 1e85bb946d | |||
| 6639c002ed | |||
| 2a61440aed | |||
| 823c2565da | |||
| b5a81e2586 | |||
| 6254cea600 | |||
| ce63f6a96b | |||
| 6fa2e8b5c6 | |||
| 2278ab09a4 | |||
| dfa72fceb1 | |||
| 9e0b9debb4 | |||
| 3902fe7b30 | |||
| 471bb4638a | |||
| 379de6ff8e | |||
| cb241741e3 | |||
| 1dbf7101b9 | |||
| d9bbca1005 | |||
| d2acf61e5a | |||
| d6898c77d8 | |||
| dd1fdf159b | |||
| 51b1b81c0e | |||
| da7af491d0 | |||
| 90b284fb6e | |||
| c1a89bd617 | |||
| a20f3ab2ab | |||
| 7b9d0edef4 | |||
| c22fc8613d | |||
| f61308e573 | |||
| d93b04a54c | |||
| 4b881e6839 | |||
| 63b0661728 | |||
| 46a66bc69d | |||
| c09018be48 | |||
| d71d448ac8 | |||
| 5834e1ee9b | |||
| d51179189c | |||
| b01243b101 | |||
| d2a80cce4e | |||
| 0cc9fc1670 | |||
| 1279791d65 | |||
| 5d2fc0ed54 | |||
| dcafcd9184 | |||
| cf16a9cd10 | |||
| 3a9dda5eb3 | |||
| c69ddd7241 | |||
| bfcb3e4c88 | |||
| 27083669fa | |||
| aaddbd847a | |||
| 1537501127 | |||
| 8b020e2bd6 | |||
| ad614f3e42 | |||
| 01497d0288 | |||
| eaad552273 | |||
| ef4afbc720 | |||
| a5cc3aec92 | |||
| 2b140d4279 | |||
| b43dcd2bc7 | |||
| c67a75d740 | |||
| abfe0f642f | |||
| f0b5162205 | |||
| a9bb2ef98b | |||
| eff4525720 | |||
| 858d9dc6f0 | |||
| 55090bc102 | |||
| 40d3d273f0 | |||
| f9271da11c | |||
| 4f881a5667 | |||
| 9d97886e3f | |||
| e70cfbbe63 | |||
| b2ba1e0e3b |
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
@@ -10,5 +12,9 @@
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.jb55.damus2</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -55,6 +55,9 @@ struct NotificationFormatter {
|
||||
var identifier = ""
|
||||
|
||||
switch notify.type {
|
||||
case .tagged:
|
||||
title = String(format: NSLocalizedString("Tagged by %@", comment: "Tagged by heading in local notification"), displayName)
|
||||
identifier = "myMentionNotification"
|
||||
case .mention:
|
||||
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
|
||||
identifier = "myMentionNotification"
|
||||
@@ -70,6 +73,9 @@ struct NotificationFormatter {
|
||||
case .zap, .profile_zap:
|
||||
// not handled here. Try `format_message(displayName: String, notify: LocalNotification, state: HeadlessDamusState) async -> (content: UNMutableNotificationContent, identifier: String)?`
|
||||
return nil
|
||||
case .reply:
|
||||
title = String(format: NSLocalizedString("%@ replied to your note", comment: "Heading for local notification indicating a new reply"), displayName)
|
||||
identifier = "myReplyNotification"
|
||||
}
|
||||
content.title = title
|
||||
content.body = notify.content
|
||||
@@ -87,10 +93,11 @@ struct NotificationFormatter {
|
||||
|
||||
// If it does not work, try async formatting methods
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
|
||||
switch notify.type {
|
||||
case .zap, .profile_zap:
|
||||
guard let zap = await get_zap(from: notify.event, state: state) else {
|
||||
Log.debug("format_message: async get_zap failed", for: .push_notifications)
|
||||
return nil
|
||||
}
|
||||
content.title = Self.zap_notification_title(zap)
|
||||
|
||||
@@ -27,9 +27,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
// Log that we got a push notification
|
||||
Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex())
|
||||
|
||||
guard let state = NotificationExtensionState(),
|
||||
let display_name = state.ndb.lookup_profile(nostr_event.pubkey)?.unsafeUnownedValue?.profile?.display_name // We are not holding the txn here.
|
||||
else {
|
||||
guard let state = NotificationExtensionState() else {
|
||||
Log.debug("Failed to open nostrdb", for: .push_notifications)
|
||||
|
||||
// Something failed to initialize so let's go for the next best thing
|
||||
guard let improved_content = NotificationFormatter.shared.format_message(event: nostr_event) else {
|
||||
// We cannot format this nostr event. Suppress notification.
|
||||
@@ -39,7 +39,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(improved_content)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let txn = state.ndb.lookup_profile(nostr_event.pubkey)
|
||||
let profile = txn?.unsafeUnownedValue?.profile
|
||||
let name = Profile.displayName(profile: profile, pubkey: nostr_event.pubkey).displayName
|
||||
|
||||
// Don't show notification details that match mute list.
|
||||
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
|
||||
if state.mutelist_manager.is_event_muted(nostr_event) {
|
||||
@@ -54,6 +58,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
|
||||
Log.debug("should_display_notification failed", for: .push_notifications)
|
||||
// We should not display notification for this event. Suppress notification.
|
||||
// contentHandler(UNNotificationContent())
|
||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
||||
@@ -62,6 +67,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
|
||||
Log.debug("generate_local_notification_object failed", for: .push_notifications)
|
||||
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
||||
// contentHandler(UNNotificationContent())
|
||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
||||
@@ -70,9 +76,13 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
Task {
|
||||
if let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: display_name, notify: notification_object, state: state) {
|
||||
contentHandler(improvedContent)
|
||||
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: name, notify: notification_object, state: state) else {
|
||||
|
||||
Log.debug("NotificationFormatter.format_message failed", for: .push_notifications)
|
||||
return
|
||||
}
|
||||
|
||||
contentHandler(improvedContent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1208
-20
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D703D7162C66E47100A400EA"
|
||||
BuildableName = "HighlighterActionExtension.appex"
|
||||
BlueprintName = "HighlighterActionExtension"
|
||||
ReferencedContainer = "container:damus.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||
BuildableName = "damus.app"
|
||||
BlueprintName = "damus"
|
||||
ReferencedContainer = "container:damus.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<RemoteRunnable
|
||||
runnableDebuggingMode = "1"
|
||||
BundleIdentifier = "com.apple.mobilesafari"
|
||||
RemotePath = "/Library/Developer/CoreSimulator/Volumes/iOS_21F79/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.5.simruntime/Contents/Resources/RuntimeRoot/Applications/MobileSafari.app">
|
||||
</RemoteRunnable>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||
BuildableName = "damus.app"
|
||||
BlueprintName = "damus"
|
||||
ReferencedContainer = "container:damus.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||
BuildableName = "damus.app"
|
||||
BlueprintName = "damus"
|
||||
ReferencedContainer = "container:damus.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -236,6 +236,7 @@ struct ImageCarousel<Content: View>: View {
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.kfClickable()
|
||||
.position(x: geo.size.width / 2, y: geo.size.height / 2)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
@@ -274,8 +275,14 @@ struct ImageCarousel<Content: View>: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Medias
|
||||
.onTapGesture { }
|
||||
if #available(iOS 18.0, *) {
|
||||
Medias
|
||||
} else {
|
||||
// An empty tap gesture recognizer is needed on iOS 17 and below to suppress other overlapping tap recognizers
|
||||
// Otherwise it will both open the carousel and go to a note at the same time
|
||||
Medias.onTapGesture { }
|
||||
}
|
||||
|
||||
|
||||
if urls.count > 1 {
|
||||
PageControlView(currentPage: $model.selectedIndex, numberOfPages: urls.count)
|
||||
|
||||
@@ -94,8 +94,8 @@ enum OpenWalletError: Error {
|
||||
}
|
||||
|
||||
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
||||
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url)
|
||||
if let url = URL(string: "\(wallet.link)\(invoice)"), this_app.canOpenURL(url) {
|
||||
this_app.open(url)
|
||||
} else {
|
||||
guard let store_link = wallet.appStoreLink else {
|
||||
throw OpenWalletError.no_wallet_to_open
|
||||
@@ -105,11 +105,11 @@ func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
||||
throw OpenWalletError.store_link_invalid
|
||||
}
|
||||
|
||||
guard UIApplication.shared.canOpenURL(url) else {
|
||||
guard this_app.canOpenURL(url) else {
|
||||
throw OpenWalletError.system_cannot_open_store_link
|
||||
}
|
||||
|
||||
UIApplication.shared.open(url)
|
||||
this_app.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +122,3 @@ struct InvoiceView_Previews: PreviewProvider {
|
||||
.frame(width: 300, height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func present_sheet(_ sheet: Sheets) {
|
||||
notify(.present_sheet(sheet))
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ struct SelectableText: View {
|
||||
let event: NostrEvent?
|
||||
let attributedString: AttributedString
|
||||
let textAlignment: NSTextAlignment
|
||||
@State private var showHighlightPost = false
|
||||
@State private var showMutePost = false
|
||||
@State private var selectedText = ""
|
||||
@State private var selectedTextActionState: SelectedTextActionState = .hide
|
||||
@State private var selectedTextHeight: CGFloat = .zero
|
||||
@State private var selectedTextWidth: CGFloat = .zero
|
||||
|
||||
@@ -38,9 +36,12 @@ struct SelectableText: View {
|
||||
fixedWidth: selectedTextWidth,
|
||||
textAlignment: self.textAlignment,
|
||||
enableHighlighting: self.enableHighlighting(),
|
||||
showHighlightPost: $showHighlightPost,
|
||||
showMutePost: $showMutePost,
|
||||
selectedText: $selectedText,
|
||||
postHighlight: { selectedText in
|
||||
self.selectedTextActionState = .show_highlight_post_view(highlighted_text: selectedText)
|
||||
},
|
||||
muteWord: { selectedText in
|
||||
self.selectedTextActionState = .show_mute_word_view(highlighted_text: selectedText)
|
||||
},
|
||||
height: $selectedTextHeight
|
||||
)
|
||||
.padding([.leading, .trailing], -1.0)
|
||||
@@ -55,17 +56,30 @@ 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])
|
||||
.sheet(isPresented: Binding(get: {
|
||||
return self.selectedTextActionState.should_show_highlight_post_view()
|
||||
}, set: { newValue in
|
||||
self.selectedTextActionState = newValue ? .show_highlight_post_view(highlighted_text: self.selectedTextActionState.highlighted_text() ?? "") : .hide
|
||||
})) {
|
||||
if let event, case .show_highlight_post_view(let highlighted_text) = self.selectedTextActionState {
|
||||
PostView(
|
||||
action: .highlighting(.init(selected_text: highlighted_text, source: .event(event))),
|
||||
damus_state: damus_state
|
||||
)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.height(selectedTextHeight + 450), .medium, .large])
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showMutePost) {
|
||||
AddMuteItemView(state: damus_state, new_text: $selectedText)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.height(300), .medium, .large])
|
||||
.sheet(isPresented: Binding(get: {
|
||||
return self.selectedTextActionState.should_show_mute_word_view()
|
||||
}, set: { newValue in
|
||||
self.selectedTextActionState = newValue ? .show_mute_word_view(highlighted_text: self.selectedTextActionState.highlighted_text() ?? "") : .hide
|
||||
})) {
|
||||
if case .show_mute_word_view(let highlighted_text) = selectedTextActionState {
|
||||
AddMuteItemView(state: damus_state, new_text: .constant(highlighted_text))
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.height(300), .medium, .large])
|
||||
}
|
||||
}
|
||||
.frame(height: selectedTextHeight)
|
||||
}
|
||||
@@ -73,17 +87,42 @@ struct SelectableText: View {
|
||||
func enableHighlighting() -> Bool {
|
||||
self.event != nil
|
||||
}
|
||||
|
||||
enum SelectedTextActionState {
|
||||
case hide
|
||||
case show_highlight_post_view(highlighted_text: String)
|
||||
case show_mute_word_view(highlighted_text: String)
|
||||
|
||||
func should_show_highlight_post_view() -> Bool {
|
||||
guard case .show_highlight_post_view(let highlighted_text) = self else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
func should_show_mute_word_view() -> Bool {
|
||||
guard case .show_mute_word_view(let highlighted_text) = self else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
func highlighted_text() -> String? {
|
||||
switch self {
|
||||
case .hide:
|
||||
return nil
|
||||
case .show_mute_word_view(highlighted_text: let highlighted_text):
|
||||
return highlighted_text
|
||||
case .show_highlight_post_view(highlighted_text: let highlighted_text):
|
||||
return highlighted_text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class TextView: UITextView {
|
||||
@Binding var showHighlightPost: Bool
|
||||
@Binding var showMutePost: Bool
|
||||
@Binding var selectedText: String
|
||||
var postHighlight: (String) -> Void
|
||||
var muteWord: (String) -> Void
|
||||
|
||||
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, showMutePost: Binding<Bool>, selectedText: Binding<String>) {
|
||||
self._showHighlightPost = showHighlightPost
|
||||
self._showMutePost = showMutePost
|
||||
self._selectedText = selectedText
|
||||
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void) {
|
||||
self.postHighlight = postHighlight
|
||||
self.muteWord = muteWord
|
||||
super.init(frame: frame, textContainer: textContainer)
|
||||
}
|
||||
|
||||
@@ -102,22 +141,25 @@ fileprivate class TextView: UITextView {
|
||||
|
||||
return super.canPerformAction(action, withSender: sender)
|
||||
}
|
||||
|
||||
func getSelectedText() -> String? {
|
||||
guard let selectedRange = self.selectedTextRange else { return nil }
|
||||
return self.text(in: selectedRange)
|
||||
}
|
||||
|
||||
@objc public func highlightText(_ sender: Any?) {
|
||||
guard let selectedRange = self.selectedTextRange else { return }
|
||||
selectedText = self.text(in: selectedRange) ?? ""
|
||||
showHighlightPost.toggle()
|
||||
guard let selectedText = self.getSelectedText() else { return }
|
||||
self.postHighlight(selectedText)
|
||||
}
|
||||
|
||||
@objc public func muteText(_ sender: Any?) {
|
||||
guard let selectedRange = self.selectedTextRange else { return }
|
||||
selectedText = self.text(in: selectedRange) ?? ""
|
||||
showMutePost.toggle()
|
||||
guard let selectedText = self.getSelectedText() else { return }
|
||||
self.muteWord(selectedText)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||
|
||||
let attributedString: AttributedString
|
||||
let textColor: UIColor
|
||||
@@ -125,13 +167,12 @@ fileprivate class TextView: UITextView {
|
||||
let fixedWidth: CGFloat
|
||||
let textAlignment: NSTextAlignment
|
||||
let enableHighlighting: Bool
|
||||
@Binding var showHighlightPost: Bool
|
||||
@Binding var showMutePost: Bool
|
||||
@Binding var selectedText: String
|
||||
let postHighlight: (String) -> Void
|
||||
let muteWord: (String) -> Void
|
||||
@Binding var height: CGFloat
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
||||
let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, showMutePost: $showMutePost, selectedText: $selectedText)
|
||||
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord)
|
||||
view.isEditable = false
|
||||
view.dataDetectorTypes = .all
|
||||
view.isSelectable = true
|
||||
|
||||
@@ -11,11 +11,13 @@ struct SupporterBadge: View {
|
||||
let percent: Int?
|
||||
let purple_account: DamusPurple.Account?
|
||||
let style: Style
|
||||
let text_color: Color
|
||||
|
||||
init(percent: Int?, purple_account: DamusPurple.Account? = nil, style: Style) {
|
||||
init(percent: Int?, purple_account: DamusPurple.Account? = nil, style: Style, text_color: Color = .secondary) {
|
||||
self.percent = percent
|
||||
self.purple_account = purple_account
|
||||
self.style = style
|
||||
self.text_color = text_color
|
||||
}
|
||||
|
||||
let size: CGFloat = 17
|
||||
@@ -31,7 +33,7 @@ struct SupporterBadge: View {
|
||||
if self.style == .full {
|
||||
let date = format_date(date: purple_account.created_at, time_style: .none)
|
||||
Text(date)
|
||||
.foregroundStyle(.secondary)
|
||||
.foregroundStyle(text_color)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,19 +27,26 @@ struct TranslateView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let size: EventViewKind
|
||||
|
||||
|
||||
@Binding var isAppleTranslationPopoverPresented: Bool
|
||||
|
||||
@ObservedObject var translations_model: TranslationModel
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, isAppleTranslationPopoverPresented: Binding<Bool>) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.size = size
|
||||
self._isAppleTranslationPopoverPresented = isAppleTranslationPopoverPresented
|
||||
self._translations_model = ObservedObject(wrappedValue: damus_state.events.get_cache_data(event.id).translations_model)
|
||||
}
|
||||
|
||||
var TranslateButton: some View {
|
||||
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
||||
translate()
|
||||
if damus_state.settings.translation_service == .none {
|
||||
isAppleTranslationPopoverPresented = true
|
||||
} else {
|
||||
translate()
|
||||
}
|
||||
}
|
||||
.translate_button_style()
|
||||
}
|
||||
@@ -74,17 +81,25 @@ struct TranslateView: View {
|
||||
}
|
||||
|
||||
func should_transl(_ note_lang: String) -> Bool {
|
||||
should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings, note_lang: note_lang)
|
||||
guard should_translate(event: event, our_keypair: damus_state.keypair, note_lang: note_lang) else {
|
||||
return false
|
||||
}
|
||||
|
||||
if TranslationService.isAppleTranslationPopoverSupported {
|
||||
return damus_state.settings.translation_service == .none || damus_state.settings.can_translate
|
||||
} else {
|
||||
return damus_state.settings.can_translate
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch self.translations_model.state {
|
||||
case .havent_tried:
|
||||
if damus_state.settings.auto_translate {
|
||||
if damus_state.settings.auto_translate && damus_state.settings.translation_service != .none {
|
||||
Text("")
|
||||
} else if let note_lang = translations_model.note_language, should_transl(note_lang) {
|
||||
TranslateButton
|
||||
TranslateButton
|
||||
} else {
|
||||
Text("")
|
||||
}
|
||||
@@ -114,9 +129,11 @@ extension View {
|
||||
}
|
||||
|
||||
struct TranslateView_Previews: PreviewProvider {
|
||||
@State static var isAppleTranslationPopoverPresented: Bool = false
|
||||
|
||||
static var previews: some View {
|
||||
let ds = test_damus_state
|
||||
TranslateView(damus_state: ds, event: test_note, size: .normal)
|
||||
TranslateView(damus_state: ds, event: test_note, size: .normal, isAppleTranslationPopoverPresented: $isAppleTranslationPopoverPresented)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ enum NoteContent {
|
||||
case content(String, TagsSequence?)
|
||||
|
||||
init(note: NostrEvent, keypair: Keypair) {
|
||||
if note.known_kind == .dm {
|
||||
if note.known_kind == .dm || note.known_kind == .highlight {
|
||||
self = .content(note.get_content(keypair), note.tags)
|
||||
} else {
|
||||
self = .note(note)
|
||||
|
||||
+25
-9
@@ -57,6 +57,10 @@ enum Sheets: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
func present_sheet(_ sheet: Sheets) {
|
||||
notify(.present_sheet(sheet))
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
let keypair: Keypair
|
||||
let appDelegate: AppDelegate?
|
||||
@@ -73,7 +77,12 @@ struct ContentView: View {
|
||||
|
||||
@State var active_sheet: Sheets? = nil
|
||||
@State var damus_state: DamusState!
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||
@State var menu_subtitle: String? = nil
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home {
|
||||
willSet {
|
||||
self.menu_subtitle = nil
|
||||
}
|
||||
}
|
||||
@State var muting: MuteItem? = nil
|
||||
@State var confirm_mute: Bool = false
|
||||
@State var hide_bar: Bool = false
|
||||
@@ -97,9 +106,16 @@ struct ContentView: View {
|
||||
isSideBarOpened = false
|
||||
}
|
||||
|
||||
var timelineNavItem: Text {
|
||||
return Text(timeline_name(selected_timeline))
|
||||
.bold()
|
||||
var timelineNavItem: some View {
|
||||
VStack {
|
||||
Text(timeline_name(selected_timeline))
|
||||
.bold()
|
||||
if let menu_subtitle {
|
||||
Text(menu_subtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MainContent(damus: DamusState) -> some View {
|
||||
@@ -118,7 +134,7 @@ struct ContentView: View {
|
||||
PostingTimelineView(damus_state: damus_state!, home: home, active_sheet: $active_sheet)
|
||||
|
||||
case .notifications:
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
|
||||
|
||||
case .dms:
|
||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
|
||||
@@ -718,7 +734,7 @@ struct ContentView: View {
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.set_active_dm(target.pubkey)
|
||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
||||
case .like, .zap, .mention, .repost:
|
||||
case .like, .zap, .mention, .repost, .reply, .tagged:
|
||||
open_event(ev: target)
|
||||
case .profile_zap:
|
||||
break
|
||||
@@ -819,7 +835,7 @@ func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [Nos
|
||||
|
||||
|
||||
func setup_notifications() {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
this_app.registerForRemoteNotifications()
|
||||
let center = UNUserNotificationCenter.current()
|
||||
|
||||
center.getNotificationSettings { settings in
|
||||
@@ -1051,7 +1067,7 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
||||
//let post = tup.0
|
||||
//let to_relays = tup.1
|
||||
print("post \(post.content)")
|
||||
guard let new_ev = post_to_event(post: post, keypair: keypair) else {
|
||||
guard let new_ev = post.to_event(keypair: keypair) else {
|
||||
return false
|
||||
}
|
||||
postbox.send(new_ev)
|
||||
@@ -1112,7 +1128,7 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
||||
}
|
||||
case .hashtag(let ht):
|
||||
result(.filter(.filter_hashtag([ht.hashtag])))
|
||||
case .param, .quote:
|
||||
case .param, .quote, .reference:
|
||||
// doesn't really make sense here
|
||||
break
|
||||
case .naddr(let naddr):
|
||||
|
||||
@@ -151,7 +151,7 @@ public class CameraService: NSObject, Identifiable {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.alertError = AlertError(title: "Camera Access", message: "Damus needs camera and microphone access. Enable in settings.", primaryButtonTitle: "Go to settings", secondaryButtonTitle: nil, primaryAction: {
|
||||
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!,
|
||||
this_app.open(URL(string: UIApplication.openSettingsURLString)!,
|
||||
options: [:], completionHandler: nil)
|
||||
|
||||
}, secondaryAction: nil)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// CommentItem.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-08-14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct CommentItem: TagConvertible {
|
||||
static let TAG_KEY: String = "comment"
|
||||
let content: String
|
||||
var tag: [String] {
|
||||
return [Self.TAG_KEY, content]
|
||||
}
|
||||
|
||||
static func from_tag(tag: TagSequence) -> CommentItem? {
|
||||
guard tag.count == 2 else { return nil }
|
||||
guard tag[0].string() == Self.TAG_KEY else { return nil }
|
||||
|
||||
return CommentItem(content: tag[1].string())
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
||||
case let (.pubkey(pk), .pubkey(follow_pk)):
|
||||
return pk == follow_pk
|
||||
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
||||
(.event, _), (.quote, _), (.param, _), (.naddr, _):
|
||||
(.event, _), (.quote, _), (.param, _), (.naddr, _), (.reference(_), _):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,79 @@ class DamusState: HeadlessDamusState {
|
||||
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
||||
self.emoji_provider = emoji_provider
|
||||
}
|
||||
|
||||
@MainActor
|
||||
convenience init?(keypair: Keypair) {
|
||||
// nostrdb
|
||||
var mndb = Ndb()
|
||||
if mndb == nil {
|
||||
// try recovery
|
||||
print("DB ISSUE! RECOVERING")
|
||||
mndb = Ndb.safemode()
|
||||
|
||||
// out of space or something?? maybe we need a in-memory fallback
|
||||
if mndb == nil {
|
||||
logout(nil)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||
let home: HomeModel = HomeModel()
|
||||
let sub_id = UUID().uuidString
|
||||
|
||||
guard let ndb = mndb else { return nil }
|
||||
let pubkey = keypair.pubkey
|
||||
|
||||
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
||||
let model_cache = RelayModelCache()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
|
||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in bootstrap_relays {
|
||||
let descriptor = RelayDescriptor(url: relay, info: .rw)
|
||||
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
||||
}
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
if let nwc_str = settings.nostr_wallet_connect,
|
||||
let nwc = WalletConnectURL(str: nwc_str) {
|
||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||
}
|
||||
self.init(
|
||||
pool: pool,
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
mutelist_manager: MutelistManager(user_keypair: keypair),
|
||||
profiles: Profiles(ndb: ndb),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache(),
|
||||
zaps: Zaps(our_pubkey: pubkey),
|
||||
lnurls: LNUrls(),
|
||||
settings: settings,
|
||||
relay_filters: relay_filters,
|
||||
relay_model_cache: model_cache,
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: ndb),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: navigationCoordinator,
|
||||
music: MusicController(onChange: { _ in }),
|
||||
video: VideoController(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func add_zap(zap: Zapping) -> Bool {
|
||||
|
||||
@@ -28,4 +28,5 @@ class Drafts: ObservableObject {
|
||||
@Published var post: DraftArtifacts? = nil
|
||||
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
|
||||
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
|
||||
@Published var highlights: [HighlightSource: DraftArtifacts] = [:]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Foundation
|
||||
|
||||
enum FriendFilter: String, StringCodable {
|
||||
case all
|
||||
case friends
|
||||
case friends_of_friends
|
||||
|
||||
init?(from string: String) {
|
||||
guard let ff = FriendFilter(rawValue: string) else {
|
||||
@@ -27,8 +27,17 @@ enum FriendFilter: String, StringCodable {
|
||||
switch self {
|
||||
case .all:
|
||||
return true
|
||||
case .friends:
|
||||
case .friends_of_friends:
|
||||
return contacts.is_in_friendosphere(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
func description() -> String {
|
||||
switch self {
|
||||
case .all:
|
||||
return NSLocalizedString("All", comment: "Human-readable short description of the 'friends filter' when it is set to 'all'")
|
||||
case .friends_of_friends:
|
||||
return NSLocalizedString("Friends of friends", comment: "Human-readable short description of the 'friends filter' when it is set to 'friends-of-friends'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,22 +13,203 @@ struct HighlightEvent {
|
||||
var event_ref: String? = nil
|
||||
var url_ref: URL? = nil
|
||||
var context: String? = nil
|
||||
|
||||
// MARK: - Initializers and parsers
|
||||
|
||||
static func parse(from ev: NostrEvent) -> HighlightEvent {
|
||||
var highlight = HighlightEvent(event: ev)
|
||||
|
||||
var best_url_source: (url: URL, tagged_as_source: Bool)? = nil
|
||||
|
||||
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 "r":
|
||||
if tag.count >= 3,
|
||||
tag[2].string() == HighlightSource.TAG_SOURCE_ELEMENT,
|
||||
let url = URL(string: tag[1].string()) {
|
||||
// URL marked as source. Very good candidate
|
||||
best_url_source = (url: url, tagged_as_source: true)
|
||||
}
|
||||
else if tag.count >= 3 && tag[2].string() != HighlightSource.TAG_SOURCE_ELEMENT {
|
||||
// URL marked as something else (not source). Not the source we are after
|
||||
}
|
||||
else if let url = URL(string: tag[1].string()), tag.count == 2 {
|
||||
// Unmarked URL. This might be what we are after (For NIP-84 backwards compatibility)
|
||||
if (best_url_source?.tagged_as_source ?? false) == false {
|
||||
// No URL candidates marked as the source. Mark this as the best option we have
|
||||
best_url_source = (url: url, tagged_as_source: false)
|
||||
}
|
||||
}
|
||||
case "context": highlight.context = tag[1].string()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let best_url_source {
|
||||
highlight.url_ref = best_url_source.url
|
||||
}
|
||||
|
||||
return highlight
|
||||
}
|
||||
|
||||
// MARK: - Getting information about source
|
||||
|
||||
func source_description_info(highlighted_event: NostrEvent?) -> ReplyDesc {
|
||||
var others_count = 0
|
||||
var highlighted_authors: [Pubkey] = []
|
||||
var i = event.tags.count
|
||||
|
||||
if let highlighted_event {
|
||||
highlighted_authors.append(highlighted_event.pubkey)
|
||||
}
|
||||
|
||||
for tag in event.tags {
|
||||
if let pubkey_with_role = PubkeyWithRole.from_tag(tag: tag) {
|
||||
others_count += 1
|
||||
if highlighted_authors.count < 2 {
|
||||
if let highlighted_event, pubkey_with_role.pubkey == highlighted_event.pubkey {
|
||||
continue
|
||||
} else {
|
||||
switch pubkey_with_role.role {
|
||||
case .author:
|
||||
highlighted_authors.append(pubkey_with_role.pubkey)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
|
||||
return ReplyDesc(pubkeys: highlighted_authors, others: others_count)
|
||||
}
|
||||
|
||||
func source_description_text(ndb: Ndb, highlighted_event: NostrEvent?, locale: Locale = Locale.current) -> String {
|
||||
let description_info = self.source_description_info(highlighted_event: highlighted_event)
|
||||
let pubkeys = description_info.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 ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper structures
|
||||
|
||||
extension HighlightEvent {
|
||||
struct PubkeyWithRole: TagKey, TagConvertible {
|
||||
let pubkey: Pubkey
|
||||
let role: Role
|
||||
|
||||
var tag: [String] {
|
||||
if let role_text = self.role.rawValue {
|
||||
return [keychar.description, self.pubkey.hex(), role_text]
|
||||
}
|
||||
else {
|
||||
return [keychar.description, self.pubkey.hex()]
|
||||
}
|
||||
}
|
||||
|
||||
var keychar: AsciiCharacter { "p" }
|
||||
|
||||
static func from_tag(tag: TagSequence) -> HighlightEvent.PubkeyWithRole? {
|
||||
var i = tag.makeIterator()
|
||||
|
||||
guard tag.count >= 2,
|
||||
let t0 = i.next(),
|
||||
let key = t0.single_char,
|
||||
key == "p",
|
||||
let t1 = i.next(),
|
||||
let pubkey = t1.id().map(Pubkey.init)
|
||||
else { return nil }
|
||||
|
||||
let t3: String? = i.next()?.string()
|
||||
let role = Role(rawValue: t3)
|
||||
return PubkeyWithRole(pubkey: pubkey, role: role)
|
||||
}
|
||||
|
||||
enum Role: RawRepresentable {
|
||||
case author
|
||||
case editor
|
||||
case mention
|
||||
case other(String)
|
||||
case no_role
|
||||
|
||||
typealias RawValue = String?
|
||||
var rawValue: String? {
|
||||
switch self {
|
||||
case .author: "author"
|
||||
case .editor: "editor"
|
||||
case .mention: "mention"
|
||||
case .other(let role): role
|
||||
case .no_role: nil
|
||||
}
|
||||
}
|
||||
|
||||
init(rawValue: String?) {
|
||||
switch rawValue {
|
||||
case "author": self = .author
|
||||
case "editor": self = .editor
|
||||
case "mention": self = .mention
|
||||
default:
|
||||
if let rawValue {
|
||||
self = .other(rawValue)
|
||||
}
|
||||
else {
|
||||
self = .no_role
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightContentDraft: Hashable {
|
||||
let selected_text: String
|
||||
let source: HighlightSource
|
||||
}
|
||||
|
||||
enum HighlightSource: Hashable {
|
||||
static let TAG_SOURCE_ELEMENT = "source"
|
||||
case event(NostrEvent)
|
||||
case external_url(URL)
|
||||
|
||||
func tags() -> [[String]] {
|
||||
switch self {
|
||||
case .event(let event):
|
||||
return [ ["e", "\(event.id)", HighlightSource.TAG_SOURCE_ELEMENT] ]
|
||||
case .external_url(let url):
|
||||
return [ ["r", "\(url)", HighlightSource.TAG_SOURCE_ELEMENT] ]
|
||||
}
|
||||
}
|
||||
|
||||
func ref() -> RefId {
|
||||
switch self {
|
||||
case .event(let event):
|
||||
return .event(event.id)
|
||||
case .external_url(let url):
|
||||
return .reference(url.absoluteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,11 +127,11 @@ class HomeModel: ContactsDelegate {
|
||||
/// This loads the latest contact event we have on file from NostrDB. This should be called as soon as we get the new DamusState
|
||||
/// Loading the latest contact list event into our `Contacts` instance from storage is important to avoid getting into weird states when the network is unreliable or when relays delete such information
|
||||
func load_latest_contact_event_from_damus_state() {
|
||||
damus_state.contacts.delegate = self
|
||||
guard let latest_contact_event_id_hex = damus_state.settings.latest_contact_event_id_hex else { return }
|
||||
guard let latest_contact_event_id = NoteId(hex: latest_contact_event_id_hex) else { return }
|
||||
guard let latest_contact_event: NdbNote = damus_state.ndb.lookup_note( latest_contact_event_id)?.unsafeUnownedValue?.to_owned() else { return }
|
||||
process_contact_event(state: damus_state, ev: latest_contact_event)
|
||||
damus_state.contacts.delegate = self
|
||||
}
|
||||
|
||||
// MARK: - ContactsDelegate functions
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
var id: String { self.rawValue }
|
||||
case nostrBuild
|
||||
case nostrImg
|
||||
case nostrcheck
|
||||
|
||||
init?(from string: String) {
|
||||
guard let mu = MediaUploader(rawValue: string) else {
|
||||
@@ -23,95 +23,73 @@ enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
func to_string() -> String {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
|
||||
var nameParam: String {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return "\"fileToUpload\""
|
||||
case .nostrImg:
|
||||
return "\"image\""
|
||||
default:
|
||||
return "\"file\""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var supportsVideo: Bool {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return true
|
||||
case .nostrImg:
|
||||
return false
|
||||
case .nostrcheck:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
var id: String { self.tag }
|
||||
var index: Int
|
||||
var tag: String
|
||||
var displayName : String
|
||||
}
|
||||
|
||||
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return .init(index: -1, tag: "nostrBuild", displayName: "nostr.build")
|
||||
case .nostrImg:
|
||||
return .init(index: 0, tag: "nostrImg", displayName: "nostrimg.com")
|
||||
case .nostrcheck:
|
||||
return .init(index: 0, tag: "nostrcheck", displayName: "nostrcheck.me")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var postAPI: String {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return "https://nostr.build/api/v2/upload/files"
|
||||
case .nostrImg:
|
||||
return "https://nostrimg.com/api/upload"
|
||||
return "https://nostr.build/api/v2/nip96/upload"
|
||||
case .nostrcheck:
|
||||
return "https://nostrcheck.me/api/v2/media"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func getMediaURL(from data: Data) -> String? {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
do {
|
||||
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any],
|
||||
let status = jsonObject["status"] as? String {
|
||||
|
||||
if status == "success", let dataArray = jsonObject["data"] as? [[String: Any]] {
|
||||
|
||||
var urls: [String] = []
|
||||
|
||||
for dataDict in dataArray {
|
||||
if let mainUrl = dataDict["url"] as? String {
|
||||
urls.append(mainUrl)
|
||||
do {
|
||||
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any],
|
||||
let status = jsonObject["status"] as? String {
|
||||
|
||||
if status == "success", let nip94Event = jsonObject["nip94_event"] as? [String: Any] {
|
||||
|
||||
if let tags = nip94Event["tags"] as? [[String]] {
|
||||
for tagArray in tags {
|
||||
if tagArray.count > 1, tagArray[0] == "url" {
|
||||
return tagArray[1]
|
||||
}
|
||||
}
|
||||
|
||||
return urls.joined(separator: "\n")
|
||||
} else if status == "error", let message = jsonObject["message"] as? String {
|
||||
print("Upload Error: \(message)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Failed JSONSerialization")
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
case .nostrImg:
|
||||
guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else {
|
||||
print("Upload failed getting response string")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let startIndex = responseString.range(of: "https://i.nostrimg.com/")?.lowerBound else {
|
||||
} else if status == "error", let message = jsonObject["message"] as? String {
|
||||
print("Upload Error: \(message)")
|
||||
return nil
|
||||
}
|
||||
let stringContainingName = responseString[startIndex..<responseString.endIndex]
|
||||
guard let endIndex = stringContainingName.range(of: "\"")?.lowerBound else {
|
||||
return nil
|
||||
}
|
||||
let nostrBuildImageName = responseString[startIndex..<endIndex]
|
||||
let nostrBuildURL = "\(nostrBuildImageName)"
|
||||
return nostrBuildURL
|
||||
} catch {
|
||||
print("Failed JSONSerialization")
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,46 +256,3 @@ func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
struct PostTags {
|
||||
let blocks: [Block]
|
||||
let tags: [[String]]
|
||||
}
|
||||
|
||||
/// Convert
|
||||
func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
|
||||
var new_tags = tags
|
||||
|
||||
for post_block in post_blocks {
|
||||
switch post_block {
|
||||
case .mention(let mention):
|
||||
switch(mention.ref) {
|
||||
case .note, .nevent:
|
||||
continue
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
new_tags.append(mention.ref.tag)
|
||||
case .hashtag(let hashtag):
|
||||
new_tags.append(["t", hashtag.lowercased()])
|
||||
case .text: break
|
||||
case .invoice: break
|
||||
case .relay: break
|
||||
case .url(let url):
|
||||
new_tags.append(["r", url.absoluteString])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return PostTags(blocks: post_blocks, tags: new_tags)
|
||||
}
|
||||
|
||||
func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? {
|
||||
let post_blocks = parse_post_blocks(content: post.content)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: post.tags)
|
||||
let content = post_tags.blocks
|
||||
.map(\.asString)
|
||||
.joined(separator: "")
|
||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: post.kind.rawValue, tags: post_tags.tags)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent)
|
||||
|
||||
func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent, mode: UserSettingsStore.NotificationsMode) -> Bool {
|
||||
// Do not show notification if it's coming from a mode different from the one selected by our user
|
||||
guard state.settings.notifications_mode == mode else {
|
||||
guard state.settings.notification_mode == mode else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -61,36 +61,55 @@ func generate_local_notification_object(from ev: NostrEvent, state: HeadlessDamu
|
||||
|
||||
if type == .text, state.settings.mention_notification {
|
||||
let blocks = ev.blocks(state.keypair).blocks
|
||||
|
||||
for case .mention(let mention) in blocks {
|
||||
guard case .pubkey(let pk) = mention.ref, pk == state.keypair.pubkey else {
|
||||
continue
|
||||
}
|
||||
let content_preview = render_notification_content_preview(ev: ev, profiles: state.profiles, keypair: state.keypair)
|
||||
return LocalNotification(type: .mention, event: ev, target: ev, content: content_preview)
|
||||
return LocalNotification(type: .mention, event: ev, target: .note(ev), content: content_preview)
|
||||
}
|
||||
|
||||
if ev.referenced_ids.contains(where: { note_id in
|
||||
guard let note_author: Pubkey = state.ndb.lookup_note(note_id)?.unsafeUnownedValue?.pubkey else { return false }
|
||||
guard note_author == state.keypair.pubkey else { return false }
|
||||
return true
|
||||
}) {
|
||||
// This is a reply to one of our posts
|
||||
let content_preview = render_notification_content_preview(ev: ev, profiles: state.profiles, keypair: state.keypair)
|
||||
return LocalNotification(type: .reply, event: ev, target: .note(ev), content: content_preview)
|
||||
}
|
||||
|
||||
if ev.referenced_pubkeys.contains(state.keypair.pubkey) {
|
||||
// not mentioned or replied to, just tagged
|
||||
let content_preview = render_notification_content_preview(ev: ev, profiles: state.profiles, keypair: state.keypair)
|
||||
return LocalNotification(type: .tagged, event: ev, target: .note(ev), content: content_preview)
|
||||
}
|
||||
|
||||
} else if type == .boost,
|
||||
state.settings.repost_notification,
|
||||
let inner_ev = ev.get_inner_event()
|
||||
{
|
||||
let content_preview = render_notification_content_preview(ev: inner_ev, profiles: state.profiles, keypair: state.keypair)
|
||||
return LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview)
|
||||
} else if type == .like,
|
||||
state.settings.like_notification,
|
||||
let evid = ev.referenced_ids.last,
|
||||
let txn = state.ndb.lookup_note(evid, txn_name: "local_notification_like"),
|
||||
let liked_event = txn.unsafeUnownedValue?.to_owned()
|
||||
{
|
||||
let content_preview = render_notification_content_preview(ev: liked_event, profiles: state.profiles, keypair: state.keypair)
|
||||
return LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview)
|
||||
return LocalNotification(type: .repost, event: ev, target: .note(inner_ev), content: content_preview)
|
||||
} else if type == .like, state.settings.like_notification, let evid = ev.referenced_ids.last {
|
||||
if let txn = state.ndb.lookup_note(evid, txn_name: "local_notification_like"),
|
||||
let liked_event = txn.unsafeUnownedValue
|
||||
{
|
||||
let content_preview = render_notification_content_preview(ev: liked_event, profiles: state.profiles, keypair: state.keypair)
|
||||
return LocalNotification(type: .like, event: ev, target: .note(liked_event), content: content_preview)
|
||||
} else {
|
||||
return LocalNotification(type: .like, event: ev, target: .note_id(evid), content: "")
|
||||
}
|
||||
}
|
||||
else if type == .dm,
|
||||
state.settings.dm_notification {
|
||||
let convo = ev.decrypted(keypair: state.keypair) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
|
||||
return LocalNotification(type: .dm, event: ev, target: ev, content: convo)
|
||||
return LocalNotification(type: .dm, event: ev, target: .note(ev), content: convo)
|
||||
}
|
||||
else if type == .zap,
|
||||
state.settings.zap_notification {
|
||||
return LocalNotification(type: .zap, event: ev, target: ev, content: ev.content)
|
||||
return LocalNotification(type: .zap, event: ev, target: .note(ev), content: ev.content)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+77
-1
@@ -17,10 +17,86 @@ struct NostrPost {
|
||||
self.kind = kind
|
||||
self.tags = tags
|
||||
}
|
||||
|
||||
func to_event(keypair: FullKeypair) -> NostrEvent? {
|
||||
let post_blocks = self.parse_blocks()
|
||||
let post_tags = self.make_post_tags(post_blocks: post_blocks, tags: self.tags)
|
||||
let content = post_tags.blocks
|
||||
.map(\.asString)
|
||||
.joined(separator: "")
|
||||
|
||||
if self.kind == .highlight {
|
||||
var new_tags = post_tags.tags.filter({ $0[safe: 0] != "comment" })
|
||||
if content.count > 0 {
|
||||
new_tags.append(["comment", content])
|
||||
}
|
||||
return NostrEvent(content: self.content, keypair: keypair.to_keypair(), kind: self.kind.rawValue, tags: new_tags)
|
||||
}
|
||||
|
||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: self.kind.rawValue, tags: post_tags.tags)
|
||||
}
|
||||
|
||||
func parse_blocks() -> [Block] {
|
||||
guard let content_for_parsing = self.default_content_for_block_parsing() else { return [] }
|
||||
return parse_post_blocks(content: content_for_parsing)
|
||||
}
|
||||
|
||||
private func default_content_for_block_parsing() -> String? {
|
||||
switch kind {
|
||||
case .highlight:
|
||||
return tags.filter({ $0[safe: 0] == "comment" }).first?[safe: 1]
|
||||
default:
|
||||
return self.content
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the post's contents to find more tags to apply to the final nostr event
|
||||
func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
|
||||
var new_tags = tags
|
||||
|
||||
for post_block in post_blocks {
|
||||
switch post_block {
|
||||
case .mention(let mention):
|
||||
switch(mention.ref) {
|
||||
case .note, .nevent:
|
||||
continue
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if self.kind == .highlight, case .pubkey(_) = mention.ref {
|
||||
var new_tag = mention.ref.tag
|
||||
new_tag.append("mention")
|
||||
new_tags.append(new_tag)
|
||||
}
|
||||
else {
|
||||
new_tags.append(mention.ref.tag)
|
||||
}
|
||||
case .hashtag(let hashtag):
|
||||
new_tags.append(["t", hashtag.lowercased()])
|
||||
case .text: break
|
||||
case .invoice: break
|
||||
case .relay: break
|
||||
case .url(let url):
|
||||
new_tags.append(self.kind == .highlight ? ["r", url.absoluteString, "mention"] : ["r", url.absoluteString])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return PostTags(blocks: post_blocks, tags: new_tags)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper structures and functions
|
||||
|
||||
extension NostrPost {
|
||||
/// A struct used for temporarily holding tag information that was parsed from a post contents to aid in building a nostr event
|
||||
struct PostTags {
|
||||
let blocks: [Block]
|
||||
let tags: [[String]]
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a list of tags
|
||||
func parse_post_blocks(content: String) -> [Block] {
|
||||
return parse_note_content(content: .content(content, nil)).blocks
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ struct PushNotificationClient {
|
||||
|
||||
mutating func set_device_token(new_device_token: Data) async throws {
|
||||
self.device_token = new_device_token
|
||||
if settings.enable_experimental_push_notifications && settings.notifications_mode == .push {
|
||||
if settings.enable_push_notifications && settings.notification_mode == .push {
|
||||
try await self.send_token()
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ struct PushNotificationClient {
|
||||
}
|
||||
|
||||
func current_push_notification_environment() -> Environment {
|
||||
return self.settings.send_device_token_to_localhost ? .local_test(host: nil) : .production
|
||||
return self.settings.push_notification_environment
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,9 +201,10 @@ extension PushNotificationClient {
|
||||
}
|
||||
|
||||
enum Environment: CaseIterable, Codable, Identifiable, StringCodable, Equatable, Hashable {
|
||||
static var allCases: [Environment] = [.local_test(host: nil), .production]
|
||||
static var allCases: [Environment] = [.local_test(host: nil), .staging, .production]
|
||||
|
||||
case local_test(host: String?)
|
||||
case staging
|
||||
case production
|
||||
|
||||
func text_description() -> String {
|
||||
@@ -212,6 +213,8 @@ extension PushNotificationClient {
|
||||
return NSLocalizedString("Test (local)", comment: "Label indicating a local test environment for Push notification functionality (Developer feature)")
|
||||
case .production:
|
||||
return NSLocalizedString("Production", comment: "Label indicating the production environment for Push notification functionality")
|
||||
case .staging:
|
||||
return NSLocalizedString("Staging (for dev builds)", comment: "Label indicating the staging environment for Push notification functionality")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,8 +223,9 @@ extension PushNotificationClient {
|
||||
case .local_test(let host):
|
||||
URL(string: "http://\(host ?? "localhost:8000")") ?? Constants.PUSH_NOTIFICATION_SERVER_TEST_BASE_URL
|
||||
case .production:
|
||||
Constants.PURPLE_API_PRODUCTION_BASE_URL
|
||||
|
||||
Constants.PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL
|
||||
case .staging:
|
||||
Constants.PUSH_NOTIFICATION_SERVER_STAGING_BASE_URL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +244,8 @@ extension PushNotificationClient {
|
||||
self = .local_test(host: nil)
|
||||
case "production":
|
||||
self = .production
|
||||
case "staging":
|
||||
self = .staging
|
||||
default:
|
||||
let components = string.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
|
||||
if components.count == 2 && components[0] == "local_test" {
|
||||
@@ -257,6 +263,8 @@ extension PushNotificationClient {
|
||||
return "local_test:\(host)"
|
||||
}
|
||||
return "local_test"
|
||||
case .staging:
|
||||
return "staging"
|
||||
case .production:
|
||||
return "production"
|
||||
}
|
||||
@@ -273,6 +281,8 @@ extension PushNotificationClient {
|
||||
}
|
||||
case .production:
|
||||
return "production"
|
||||
case .staging:
|
||||
return "staging"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,13 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .none:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service."))
|
||||
let displayName: String
|
||||
if TranslationService.isAppleTranslationPopoverSupported {
|
||||
displayName = NSLocalizedString("apple_translation_service", value: "Apple", comment: "Dropdown option for selecting Apple as a translation service.")
|
||||
} else {
|
||||
displayName = NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service.")
|
||||
}
|
||||
return .init(tag: self.rawValue, displayName: displayName)
|
||||
case .purple:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("Damus Purple", comment: "Dropdown option for selecting Damus Purple as a translation service."))
|
||||
case .libretranslate:
|
||||
@@ -51,4 +57,12 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("translate.nostr.wine (DeepL, Pay with BTC)", comment: "Dropdown option for selecting translate.nostr.wine as the translation service."))
|
||||
}
|
||||
}
|
||||
|
||||
static var isAppleTranslationPopoverSupported: Bool {
|
||||
if #available(iOS 17.4, macOS 14.4, *) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,8 +155,8 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "like_notification", default_value: true)
|
||||
var like_notification: Bool
|
||||
|
||||
@StringSetting(key: "notifications_mode", default_value: .local)
|
||||
var notifications_mode: NotificationsMode
|
||||
@StringSetting(key: "notification_mode", default_value: .push)
|
||||
var notification_mode: NotificationsMode
|
||||
|
||||
@Setting(key: "notification_only_from_following", default_value: false)
|
||||
var notification_only_from_following: Bool
|
||||
@@ -207,11 +207,12 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "always_show_onboarding_suggestions", default_value: false)
|
||||
var always_show_onboarding_suggestions: Bool
|
||||
|
||||
@Setting(key: "enable_experimental_push_notifications", default_value: false)
|
||||
var enable_experimental_push_notifications: Bool
|
||||
// @Setting(key: "enable_experimental_push_notifications", default_value: false)
|
||||
// This was a feature flag setting during early development, but now this is enabled for everyone.
|
||||
var enable_push_notifications: Bool = true
|
||||
|
||||
@Setting(key: "send_device_token_to_localhost", default_value: false)
|
||||
var send_device_token_to_localhost: Bool
|
||||
@StringSetting(key: "push_notification_environment", default_value: .production)
|
||||
var push_notification_environment: PushNotificationClient.Environment
|
||||
|
||||
@Setting(key: "enable_experimental_purple_api", default_value: false)
|
||||
var enable_experimental_purple_api: Bool
|
||||
|
||||
@@ -122,20 +122,22 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
case hashtag(Hashtag)
|
||||
case param(TagElem)
|
||||
case naddr(NAddr)
|
||||
case reference(String)
|
||||
|
||||
var key: RefKey {
|
||||
switch self {
|
||||
case .event: return .e
|
||||
case .pubkey: return .p
|
||||
case .quote: return .q
|
||||
case .hashtag: return .t
|
||||
case .param: return .d
|
||||
case .naddr: return .a
|
||||
case .event: return .e
|
||||
case .pubkey: return .p
|
||||
case .quote: return .q
|
||||
case .hashtag: return .t
|
||||
case .param: return .d
|
||||
case .naddr: return .a
|
||||
case .reference: return .r
|
||||
}
|
||||
}
|
||||
|
||||
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
|
||||
case e, p, t, d, q, a
|
||||
case e, p, t, d, q, a, r
|
||||
|
||||
var keychar: AsciiCharacter {
|
||||
self.rawValue
|
||||
@@ -159,6 +161,8 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
case .param(let string): return string.string()
|
||||
case .naddr(let naddr):
|
||||
return naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier
|
||||
case .reference(let string):
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +183,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
case .t: return .hashtag(Hashtag(hashtag: t1.string()))
|
||||
case .d: return .param(t1)
|
||||
case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0))
|
||||
case .r: return .reference(t1.string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,10 @@ final class RelayConnection: ObservableObject {
|
||||
|
||||
if err == nil {
|
||||
self.last_pong = .now
|
||||
Log.info("Got pong from '%s'", for: .networking, self.relay_url.absoluteString)
|
||||
self.log?.add("Successful ping")
|
||||
} else {
|
||||
print("pong failed, reconnecting \(self.relay_url.id)")
|
||||
Log.info("Ping failed, reconnecting to '%s'", for: .networking, self.relay_url.absoluteString)
|
||||
self.isConnected = false
|
||||
self.isConnecting = false
|
||||
self.reconnect_with_backoff()
|
||||
@@ -126,7 +127,7 @@ final class RelayConnection: ObservableObject {
|
||||
self.receive(message: message)
|
||||
case .disconnected(let closeCode, let reason):
|
||||
if closeCode != .normalClosure {
|
||||
print("⚠️ Warning: RelayConnection (\(self.relay_url)) closed with code \(closeCode), reason: \(String(describing: reason))")
|
||||
Log.error("⚠️ Warning: RelayConnection (%d) closed with code: %s", for: .networking, String(describing: closeCode), String(describing: reason))
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.isConnected = false
|
||||
@@ -134,12 +135,16 @@ final class RelayConnection: ObservableObject {
|
||||
self.reconnect()
|
||||
}
|
||||
case .error(let error):
|
||||
print("⚠️ Warning: RelayConnection (\(self.relay_url)) error: \(error)")
|
||||
Log.error("⚠️ Warning: RelayConnection (%s) error: %s", for: .networking, self.relay_url.absoluteString, error.localizedDescription)
|
||||
let nserr = error as NSError
|
||||
if nserr.domain == NSPOSIXErrorDomain && nserr.code == 57 {
|
||||
// ignore socket not connected?
|
||||
return
|
||||
}
|
||||
if nserr.domain == NSURLErrorDomain && nserr.code == -999 {
|
||||
// these aren't real error, it just means task was cancelled
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.isConnected = false
|
||||
self.isConnecting = false
|
||||
@@ -156,14 +161,21 @@ final class RelayConnection: ObservableObject {
|
||||
}
|
||||
|
||||
func reconnect_with_backoff() {
|
||||
self.backoff *= 1.5
|
||||
self.backoff *= 2.0
|
||||
self.reconnect_in(after: self.backoff)
|
||||
}
|
||||
|
||||
func reconnect() {
|
||||
guard !isConnecting && !isDisabled else {
|
||||
self.log?.add("Cancelling reconnect, already connecting")
|
||||
return // we're already trying to connect or we're disabled
|
||||
}
|
||||
|
||||
guard !self.isConnected else {
|
||||
self.log?.add("Cancelling reconnect, already connected")
|
||||
return
|
||||
}
|
||||
|
||||
disconnect()
|
||||
connect()
|
||||
log?.add("Reconnecting...")
|
||||
|
||||
@@ -89,6 +89,7 @@ class RelayPool {
|
||||
}
|
||||
|
||||
func ping() {
|
||||
Log.info("Pinging %d relays", for: .networking, relays.count)
|
||||
for relay in relays {
|
||||
relay.connection.ping()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ class Constants {
|
||||
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
|
||||
|
||||
// MARK: Push notification server
|
||||
static let PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL: URL = URL(string: "http://45.33.32.5:8000")!
|
||||
static let PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL: URL = URL(string: "https://notify.damus.io")!
|
||||
static let PUSH_NOTIFICATION_SERVER_STAGING_BASE_URL: URL = URL(string: "https://notify-staging.damus.io")!
|
||||
static let PUSH_NOTIFICATION_SERVER_TEST_BASE_URL: URL = URL(string: "http://localhost:8000")!
|
||||
|
||||
// MARK: Purple
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// DamusAliases.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-08-12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
let this_app: UIApplication = UIApplication.shared
|
||||
+15
-11
@@ -244,16 +244,12 @@ class EventCache {
|
||||
}
|
||||
}
|
||||
|
||||
func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||
guard settings.can_translate else {
|
||||
return false
|
||||
}
|
||||
|
||||
func should_translate(event: NostrEvent, our_keypair: Keypair, note_lang: String?) -> Bool {
|
||||
// don't translate reposts, longform, etc
|
||||
if event.kind != 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Do not translate self-authored notes if logged in with a private key
|
||||
// as we can assume the user can understand their own notes.
|
||||
// The detected language prediction could be incorrect and not in the list of preferred languages.
|
||||
@@ -261,25 +257,33 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSet
|
||||
if our_keypair.privkey != nil && our_keypair.pubkey == event.pubkey {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if let note_lang {
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
|
||||
// Don't translate if its in our preferred languages
|
||||
guard !preferredLanguages.contains(note_lang) else {
|
||||
// if its the same, give up and don't retry
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// we should start translating if we have auto_translate on
|
||||
return true
|
||||
}
|
||||
|
||||
func can_and_should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||
guard settings.can_translate else {
|
||||
return false
|
||||
}
|
||||
|
||||
return should_translate(event: event, our_keypair: our_keypair, note_lang: note_lang)
|
||||
}
|
||||
|
||||
func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current_status: TranslateStatus, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||
switch current_status {
|
||||
case .havent_tried:
|
||||
return should_translate(event: event, our_keypair: our_keypair, settings: settings, note_lang: note_lang) && settings.auto_translate
|
||||
return can_and_should_translate(event: event, our_keypair: our_keypair, settings: settings, note_lang: note_lang) && settings.auto_translate
|
||||
case .translating: return false
|
||||
case .translated: return false
|
||||
case .not_needed: return false
|
||||
@@ -413,7 +417,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async {
|
||||
|
||||
var translations: TranslateStatus? = nil
|
||||
// We have to recheck should_translate here now that we have note_language
|
||||
if plan.load_translations && should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate
|
||||
if plan.load_translations && can_and_should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate
|
||||
{
|
||||
translations = await translate_note(profiles: profiles, keypair: our_keypair, event: plan.event, settings: settings, note_lang: note_language, purple: state.purple)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public struct DismissKeyboardOnTap: ViewModifier {
|
||||
}
|
||||
|
||||
public func end_editing() {
|
||||
UIApplication.shared.connectedScenes
|
||||
this_app.connectedScenes
|
||||
.filter {$0.activationState == .foregroundActive}
|
||||
.map {$0 as? UIWindowScene}
|
||||
.compactMap({$0})
|
||||
|
||||
@@ -48,10 +48,24 @@ struct LossyLocalNotification {
|
||||
}
|
||||
}
|
||||
|
||||
enum NotificationTarget {
|
||||
case note(NostrEvent)
|
||||
case note_id(NoteId)
|
||||
|
||||
var id: NoteId {
|
||||
switch self {
|
||||
case .note(let note):
|
||||
return note.id
|
||||
case .note_id(let id):
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalNotification {
|
||||
let type: LocalNotificationType
|
||||
let event: NostrEvent
|
||||
let target: NostrEvent
|
||||
let target: NotificationTarget
|
||||
let content: String
|
||||
|
||||
func to_lossy() -> LossyLocalNotification {
|
||||
@@ -63,6 +77,8 @@ enum LocalNotificationType: String {
|
||||
case dm
|
||||
case like
|
||||
case mention
|
||||
case reply
|
||||
case tagged
|
||||
case repost
|
||||
case zap
|
||||
case profile_zap
|
||||
|
||||
@@ -13,6 +13,7 @@ enum LogCategory: String {
|
||||
case nav
|
||||
case render
|
||||
case storage
|
||||
case networking
|
||||
case push_notifications
|
||||
case damus_purple
|
||||
case image_uploading
|
||||
|
||||
@@ -11,8 +11,7 @@ import UIKit
|
||||
class Theme {
|
||||
|
||||
static var safeAreaInsets: UIEdgeInsets? {
|
||||
return UIApplication
|
||||
.shared
|
||||
return this_app
|
||||
.connectedScenes
|
||||
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
|
||||
.first { $0.isKeyWindow }?.safeAreaInsets
|
||||
|
||||
+6
-19
@@ -309,14 +309,10 @@ struct Zap {
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else {
|
||||
guard let zap_req = get_zap_request(zap_ev) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let zap_req = decode_nostr_event_json(desc) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard validate_event(ev: zap_req) == .ok else {
|
||||
return nil
|
||||
}
|
||||
@@ -399,21 +395,12 @@ func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
/// Fetches the description from either the invoice, or tags, depending on the type of invoice
|
||||
func get_zap_description(_ ev: NostrEvent, inv_desc: InvoiceDescription) -> String? {
|
||||
switch inv_desc {
|
||||
case .description(let string):
|
||||
return string
|
||||
case .description_hash(let deschash):
|
||||
guard let desc = event_tag(ev, name: "description") else {
|
||||
return nil
|
||||
}
|
||||
guard let data = desc.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return desc
|
||||
func get_zap_request(_ ev: NostrEvent) -> NostrEvent? {
|
||||
guard let desc = event_tag(ev, name: "description") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return decode_nostr_event_json(desc)
|
||||
}
|
||||
|
||||
func invoice_to_zap_invoice(_ invoice: Invoice) -> ZapInvoice? {
|
||||
|
||||
@@ -116,7 +116,7 @@ struct AddRelayView: View {
|
||||
}
|
||||
new_relay = ""
|
||||
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
|
||||
dismiss()
|
||||
}) {
|
||||
|
||||
@@ -40,7 +40,7 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
// If uploading to a media host that support NIP-98 authorization, add the header
|
||||
if mediaUploader == .nostrBuild,
|
||||
if mediaUploader == .nostrBuild || mediaUploader == .nostrcheck,
|
||||
let keypair,
|
||||
let method = request.httpMethod,
|
||||
let signature = create_nip98_signature(keypair: keypair, method: method, url: url) {
|
||||
|
||||
@@ -29,6 +29,7 @@ struct EditBannerImageView: View {
|
||||
Color(uiColor: .secondarySystemBackground)
|
||||
}
|
||||
.onFailureImage(defaultImage)
|
||||
.kfClickable()
|
||||
|
||||
EditPictureControl(uploader: damus_state.settings.default_media_uploader, pubkey: damus_state.pubkey, image_url: $banner_image, uploadObserver: viewModel, callback: callback)
|
||||
}
|
||||
@@ -54,6 +55,7 @@ struct InnerBannerImageView: View {
|
||||
Color(uiColor: .secondarySystemBackground)
|
||||
}
|
||||
.onFailureImage(defaultImage)
|
||||
.kfClickable()
|
||||
} else {
|
||||
Image(uiImage: defaultImage).resizable()
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ struct FriendsButton: View {
|
||||
Button(action: {
|
||||
switch self.filter {
|
||||
case .all:
|
||||
self.filter = .friends
|
||||
case .friends:
|
||||
self.filter = .friends_of_friends
|
||||
case .friends_of_friends:
|
||||
self.filter = .all
|
||||
}
|
||||
}) {
|
||||
if filter == .friends {
|
||||
if filter == .friends_of_friends {
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image("user-added")
|
||||
.resizable()
|
||||
@@ -28,7 +28,7 @@ struct FriendsButton: View {
|
||||
Image("user-added")
|
||||
.resizable()
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(DamusColors.adaptableGrey)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@@ -103,7 +103,7 @@ struct DirectMessagesView: View {
|
||||
|
||||
func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool {
|
||||
for dm in dms {
|
||||
if !FriendFilter.friends.filter(contacts: contacts, pubkey: dm.pubkey) {
|
||||
if !FriendFilter.friends_of_friends.filter(contacts: contacts, pubkey: dm.pubkey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,16 @@ func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
|
||||
return true
|
||||
}
|
||||
|
||||
// blame the porn bots for this code too
|
||||
func should_blur_images(damus_state: DamusState, ev: NostrEvent) -> Bool {
|
||||
return should_blur_images(
|
||||
settings: damus_state.settings,
|
||||
contacts: damus_state.contacts,
|
||||
ev: ev,
|
||||
our_pubkey: damus_state.pubkey
|
||||
)
|
||||
}
|
||||
|
||||
func format_relative_time(_ created_at: UInt32) -> String
|
||||
{
|
||||
return time_ago_since(Date(timeIntervalSince1970: Double(created_at)))
|
||||
|
||||
@@ -17,7 +17,8 @@ struct ReplyPart: View {
|
||||
Group {
|
||||
if event.known_kind == .highlight {
|
||||
let highlighted_note = event.highlighted_note_id().flatMap { events.lookup($0) }
|
||||
HighlightDescription(event: event, highlighted_event: highlighted_note, ndb: ndb)
|
||||
let highlight_note = HighlightEvent.parse(from: event)
|
||||
HighlightDescription(highlight_event: highlight_note, highlighted_event: highlighted_note, ndb: ndb)
|
||||
} else if let reply_ref = event.thread_reply()?.reply {
|
||||
let replying_to = events.lookup(reply_ref.note_id)
|
||||
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
|
||||
|
||||
@@ -9,12 +9,12 @@ import SwiftUI
|
||||
|
||||
// Modified from Reply Description
|
||||
struct HighlightDescription: View {
|
||||
let event: NostrEvent
|
||||
let highlight_event: HighlightEvent
|
||||
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))"))
|
||||
(Text(Image(systemName: "highlighter")) + Text(verbatim: " \(highlight_event.source_description_text(ndb: ndb, highlighted_event: highlighted_event))"))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -24,30 +24,6 @@ struct HighlightDescription: View {
|
||||
|
||||
struct HighlightDescription_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HighlightDescription(event: test_note, highlighted_event: test_note, ndb: test_damus_state.ndb)
|
||||
HighlightDescription(highlight_event: HighlightEvent.parse(from: test_note), highlighted_event: nil, 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 ?? "")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// HighlightDraftContentView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 5/26/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HighlightDraftContentView: View {
|
||||
let draft: HighlightContentDraft
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
var attributedString: AttributedString {
|
||||
var attributedString = AttributedString(draft.selected_text)
|
||||
|
||||
if let range = attributedString.range(of: draft.selected_text) {
|
||||
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
|
||||
)
|
||||
|
||||
if case .external_url(let url) = draft.source {
|
||||
LinkViewRepresentable(meta: .url(url))
|
||||
.frame(height: 50)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ struct HighlightEventRef: View {
|
||||
FailedImage()
|
||||
}
|
||||
.frame(width: 35, height: 35)
|
||||
.kfClickable()
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
|
||||
.scaledToFit()
|
||||
|
||||
@@ -61,6 +61,7 @@ struct HighlightLink: View {
|
||||
.background(DamusColors.adaptableWhite)
|
||||
}
|
||||
.frame(width: 35, height: 35)
|
||||
.kfClickable()
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.scaledToFit()
|
||||
} else {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
//
|
||||
// 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.")) {
|
||||
let tags: [[String]] = [ ["e", "\(self.event.id)"] ]
|
||||
|
||||
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(selectedText)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,9 +59,9 @@ struct HighlightBodyView: View {
|
||||
var body: some View {
|
||||
Group {
|
||||
if options.contains(.wide) {
|
||||
Main.padding(.horizontal)
|
||||
} else {
|
||||
Main
|
||||
} else {
|
||||
Main.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,18 @@ struct HighlightBodyView: View {
|
||||
|
||||
var Main: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
||||
if self.event.event.referenced_comment_items.first?.content != nil {
|
||||
let all_options = options.union(.no_action_bar)
|
||||
NoteContentView(
|
||||
damus_state: self.state,
|
||||
event: self.event.event,
|
||||
blur_images: should_blur_images(damus_state: self.state, ev: self.event.event),
|
||||
size: .normal,
|
||||
options: all_options
|
||||
).padding(.vertical, 10)
|
||||
}
|
||||
|
||||
HStack {
|
||||
var attributedString: AttributedString {
|
||||
var attributedString: AttributedString = ""
|
||||
@@ -119,14 +131,17 @@ struct HighlightBodyView: View {
|
||||
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
||||
alignment: .leading
|
||||
)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
if let url = event.url_ref {
|
||||
HighlightLink(state: state, url: url, content: event.event.content)
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
if let evRef = event.event_ref {
|
||||
if let eventHex = hex_decode_id(evRef) {
|
||||
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ struct LongformPreviewBody: View {
|
||||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150)
|
||||
.kfClickable()
|
||||
.cornerRadius(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ struct ImageContainerView: View {
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageModifier(ImageHandler(handler: $image))
|
||||
.kfClickable()
|
||||
.clipped()
|
||||
.modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet))
|
||||
.sheet(isPresented: $showShareSheet) {
|
||||
|
||||
@@ -33,6 +33,7 @@ struct ProfileImageContainerView: View {
|
||||
.imageModifier(ImageHandler(handler: $image))
|
||||
.clipShape(Circle())
|
||||
.modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet))
|
||||
.kfClickable()
|
||||
.sheet(isPresented: $showShareSheet) {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ struct AddMuteItemView: View {
|
||||
|
||||
new_text = ""
|
||||
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
|
||||
dismiss()
|
||||
}) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
import LinkPresentation
|
||||
import NaturalLanguage
|
||||
import MarkdownUI
|
||||
import Translation
|
||||
|
||||
struct Blur: UIViewRepresentable {
|
||||
var style: UIBlurEffect.Style = .systemUltraThinMaterial
|
||||
@@ -32,6 +33,8 @@ struct NoteContentView: View {
|
||||
let preview_height: CGFloat?
|
||||
let options: EventViewOptions
|
||||
|
||||
@State var isAppleTranslationPopoverPresented: Bool = false
|
||||
|
||||
@ObservedObject var artifacts_model: NoteArtifactsModel
|
||||
@ObservedObject var preview_model: PreviewModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
@@ -96,7 +99,7 @@ struct NoteContentView: View {
|
||||
}
|
||||
|
||||
var translateView: some View {
|
||||
TranslateView(damus_state: damus_state, event: event, size: self.size)
|
||||
TranslateView(damus_state: damus_state, event: event, size: self.size, isAppleTranslationPopoverPresented: $isAppleTranslationPopoverPresented)
|
||||
}
|
||||
|
||||
func previewView(links: [URL]) -> some View {
|
||||
@@ -120,8 +123,7 @@ struct NoteContentView: View {
|
||||
EventView(damus: damus_state, event: self.event, options: .embedded_text_only)
|
||||
.padding(.top)
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
.preferredColorScheme(.dark)
|
||||
.background(.thickMaterial)
|
||||
.onTapGesture(perform: {
|
||||
damus_state.nav.push(route: Route.Thread(thread: .init(event: self.event, damus_state: damus_state)))
|
||||
dismiss()
|
||||
@@ -146,7 +148,7 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if !options.contains(.no_translate) && (size == .selected || damus_state.settings.auto_translate) {
|
||||
if !options.contains(.no_translate) && (size == .selected || TranslationService.isAppleTranslationPopoverSupported || damus_state.settings.auto_translate) {
|
||||
if with_padding {
|
||||
translateView
|
||||
.padding(.horizontal)
|
||||
@@ -299,7 +301,12 @@ struct NoteContentView: View {
|
||||
Markdown(md.markdown)
|
||||
.padding([.leading, .trailing, .top])
|
||||
case .separated(let separated):
|
||||
MainContent(artifacts: separated)
|
||||
if #available(iOS 17.4, macOS 14.4, *) {
|
||||
MainContent(artifacts: separated)
|
||||
.translationPresentation(isPresented: $isAppleTranslationPopoverPresented, text: event.get_content(damus_state.keypair))
|
||||
} else {
|
||||
MainContent(artifacts: separated)
|
||||
}
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -96,7 +96,7 @@ struct DamusAppNotificationView: View {
|
||||
|
||||
@MainActor
|
||||
func open_url(url: URL) {
|
||||
UIApplication.shared.open(url)
|
||||
this_app.open(url)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -56,6 +56,7 @@ struct NotificationsView: View {
|
||||
@ObservedObject var notifications: NotificationsModel
|
||||
@StateObject var filter = NotificationFilter()
|
||||
@SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all
|
||||
@Binding var subtitle: String?
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -99,6 +100,15 @@ struct NotificationsView: View {
|
||||
.tag(NotificationFilterState.replies)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(
|
||||
action: { state.nav.push(route: Route.NotificationSettings(settings: state.settings)) },
|
||||
label: {
|
||||
Image("settings")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
|
||||
FriendsButton(filter: $filter.fine_filter)
|
||||
@@ -107,12 +117,14 @@ struct NotificationsView: View {
|
||||
}
|
||||
.onChange(of: filter.fine_filter) { val in
|
||||
state.settings.friend_filter = val
|
||||
self.subtitle = filter.fine_filter.description()
|
||||
}
|
||||
.onChange(of: filter_state) { val in
|
||||
filter.state = val
|
||||
}
|
||||
.onAppear {
|
||||
self.filter.fine_filter = state.settings.friend_filter
|
||||
self.subtitle = filter.fine_filter.description()
|
||||
filter.state = filter_state
|
||||
}
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
@@ -163,7 +175,7 @@ struct NotificationsView: View {
|
||||
|
||||
struct NotificationsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NotificationsView(state: test_damus_state, notifications: NotificationsModel(), filter: NotificationFilter())
|
||||
NotificationsView(state: test_damus_state, notifications: NotificationsModel(), filter: NotificationFilter(), subtitle: .constant(nil))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +186,7 @@ func would_filter_non_friends_from_notifications(contacts: Contacts, state: Noti
|
||||
continue
|
||||
}
|
||||
|
||||
if item.would_filter({ ev in FriendFilter.friends.filter(contacts: contacts, pubkey: ev.pubkey) }) {
|
||||
if item.would_filter({ ev in FriendFilter.friends_of_friends.filter(contacts: contacts, pubkey: ev.pubkey) }) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
+61
-30
@@ -30,15 +30,18 @@ enum PostAction {
|
||||
case replying_to(NostrEvent)
|
||||
case quoting(NostrEvent)
|
||||
case posting(PostTarget)
|
||||
case highlighting(HighlightContentDraft)
|
||||
|
||||
var ev: NostrEvent? {
|
||||
switch self {
|
||||
case .replying_to(let ev):
|
||||
return ev
|
||||
case .quoting(let ev):
|
||||
return ev
|
||||
case .posting:
|
||||
return nil
|
||||
case .replying_to(let ev):
|
||||
return ev
|
||||
case .quoting(let ev):
|
||||
return ev
|
||||
case .posting:
|
||||
return nil
|
||||
case .highlighting:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,7 +131,12 @@ struct PostView: View {
|
||||
}
|
||||
|
||||
var posting_disabled: Bool {
|
||||
return is_post_empty || uploading_disabled
|
||||
switch action {
|
||||
case .highlighting(_):
|
||||
return false
|
||||
default:
|
||||
return is_post_empty || uploading_disabled
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a valid height for the text box, even when textHeight is not a number
|
||||
@@ -204,6 +212,8 @@ struct PostView: View {
|
||||
damus_state.drafts.quotes.removeValue(forKey: quoting)
|
||||
case .posting:
|
||||
damus_state.drafts.post = nil
|
||||
case .highlighting(let draft):
|
||||
damus_state.drafts.highlights.removeValue(forKey: draft.source)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -371,6 +381,9 @@ struct PostView: View {
|
||||
if case .quoting(let ev) = action {
|
||||
BuilderEventView(damus: damus_state, event: ev)
|
||||
}
|
||||
else if case .highlighting(let draft) = action {
|
||||
HighlightDraftContentView(draft: draft)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
@@ -454,14 +467,15 @@ struct PostView: View {
|
||||
let loaded_draft = load_draft()
|
||||
|
||||
switch action {
|
||||
case .replying_to(let replying_to):
|
||||
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
|
||||
case .quoting(let quoting):
|
||||
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
|
||||
case .posting(let target):
|
||||
guard !loaded_draft else { break }
|
||||
|
||||
fill_target_content(target: target)
|
||||
case .replying_to(let replying_to):
|
||||
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
|
||||
case .quoting(let quoting):
|
||||
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
|
||||
case .posting(let target):
|
||||
guard !loaded_draft else { break }
|
||||
fill_target_content(target: target)
|
||||
case .highlighting(let draft):
|
||||
references = [draft.source.ref()]
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
@@ -597,6 +611,8 @@ func set_draft_for_post(drafts: Drafts, action: PostAction, artifacts: DraftArti
|
||||
drafts.quotes[ev] = artifacts
|
||||
case .posting:
|
||||
drafts.post = artifacts
|
||||
case .highlighting(let draft):
|
||||
drafts.highlights[draft.source] = artifacts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,6 +624,8 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts?
|
||||
return drafts.quotes[ev]
|
||||
case .posting:
|
||||
return drafts.post
|
||||
case .highlighting(let draft):
|
||||
return drafts.highlights[draft.source]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,27 +687,40 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post
|
||||
var tags: [[String]] = []
|
||||
|
||||
switch action {
|
||||
case .replying_to(let replying_to):
|
||||
// start off with the reply tags
|
||||
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
||||
case .replying_to(let replying_to):
|
||||
// start off with the reply tags
|
||||
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
||||
|
||||
case .quoting(let ev):
|
||||
content.append(" nostr:" + bech32_note_id(ev.id))
|
||||
case .quoting(let ev):
|
||||
content.append(" nostr:" + bech32_note_id(ev.id))
|
||||
|
||||
if let quoted_ev = state.events.lookup(ev.id) {
|
||||
tags.append(["p", quoted_ev.pubkey.hex()])
|
||||
}
|
||||
case .posting(let postTarget):
|
||||
break
|
||||
}
|
||||
|
||||
// include pubkeys
|
||||
tags += pubkeys.map { pk in
|
||||
["p", pk.hex()]
|
||||
if let quoted_ev = state.events.lookup(ev.id) {
|
||||
tags.append(["p", quoted_ev.pubkey.hex()])
|
||||
}
|
||||
case .posting(let postTarget):
|
||||
break
|
||||
case .highlighting(let draft):
|
||||
break
|
||||
}
|
||||
|
||||
// append additional tags
|
||||
tags += uploadedMedias.compactMap { $0.metadata?.to_tag() }
|
||||
|
||||
switch action {
|
||||
case .highlighting(let draft):
|
||||
tags.append(contentsOf: draft.source.tags())
|
||||
if !(content.isEmpty || content.allSatisfy { $0.isWhitespace }) {
|
||||
tags.append(["comment", content])
|
||||
}
|
||||
tags += pubkeys.map { pk in
|
||||
["p", pk.hex(), "mention"]
|
||||
}
|
||||
return NostrPost(content: draft.selected_text, kind: .highlight, tags: tags)
|
||||
default:
|
||||
tags += pubkeys.map { pk in
|
||||
["p", pk.hex()]
|
||||
}
|
||||
}
|
||||
|
||||
return NostrPost(content: content, kind: .text, tags: tags)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ struct EditPictureControl: View {
|
||||
}
|
||||
.scaledToFill()
|
||||
.frame(width: (size ?? 25) + 10, height: (size ?? 25) + 10)
|
||||
.kfClickable()
|
||||
.foregroundColor(DamusColors.white)
|
||||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(.white, lineWidth: 4))
|
||||
|
||||
@@ -57,6 +57,7 @@ struct InnerProfilePicView: View {
|
||||
}
|
||||
.scaledToFill()
|
||||
.frame(width: size, height: size)
|
||||
.kfClickable()
|
||||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ struct EditProfilePictureView: View {
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.scaledToFill()
|
||||
.kfClickable()
|
||||
|
||||
EditPictureControl(uploader: damus_state?.settings.default_media_uploader ?? .nostrBuild, pubkey: pubkey, image_url: $profile_url, uploadObserver: uploadObserver, callback: callback)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ struct DamusPurpleAccountView: View {
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.preferredColorScheme(.dark)
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.padding()
|
||||
}
|
||||
@@ -81,7 +80,8 @@ struct DamusPurpleAccountView: View {
|
||||
SupporterBadge(
|
||||
percent: nil,
|
||||
purple_account: account,
|
||||
style: .full
|
||||
style: .full,
|
||||
text_color: .white
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ struct InnerRelayPicView: View {
|
||||
Placeholder(url: url)
|
||||
}
|
||||
.scaledToFit()
|
||||
.kfClickable()
|
||||
} else {
|
||||
FailedRelayImage(url: nil)
|
||||
}
|
||||
|
||||
@@ -18,12 +18,41 @@ struct DeveloperSettingsView: View {
|
||||
.toggleStyle(.switch)
|
||||
if settings.developer_mode {
|
||||
Toggle(NSLocalizedString("Always show onboarding", comment: "Developer mode setting to always show onboarding suggestions."), isOn: $settings.always_show_onboarding_suggestions)
|
||||
|
||||
Toggle(NSLocalizedString("Enable experimental push notifications", comment: "Developer mode setting to enable experimental push notifications."), isOn: $settings.enable_experimental_push_notifications)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Toggle(NSLocalizedString("Send device token to localhost", comment: "Developer mode setting to send device token metadata to a local server instead of the damus.io server."), isOn: $settings.send_device_token_to_localhost)
|
||||
.toggleStyle(.switch)
|
||||
Picker(NSLocalizedString("Push notification environment", comment: "Prompt selection of the Push notification environment (Developer feature to switch between real/production mode to test modes)."),
|
||||
selection: Binding(
|
||||
get: { () -> PushNotificationClient.Environment in
|
||||
switch settings.push_notification_environment {
|
||||
case .local_test(_):
|
||||
return .local_test(host: nil) // Avoid errors related to a value which is not a valid picker option
|
||||
default:
|
||||
return settings.push_notification_environment
|
||||
}
|
||||
},
|
||||
set: { new_value in
|
||||
settings.push_notification_environment = new_value
|
||||
}
|
||||
)
|
||||
) {
|
||||
ForEach(PushNotificationClient.Environment.allCases, id: \.self) { push_notification_environment in
|
||||
Text(push_notification_environment.text_description())
|
||||
.tag(push_notification_environment.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
if case .local_test(_) = settings.push_notification_environment {
|
||||
TextField(
|
||||
NSLocalizedString("URL", comment: "Custom URL host for Damus push notification testing"),
|
||||
text: Binding.init(
|
||||
get: {
|
||||
return settings.push_notification_environment.custom_host() ?? ""
|
||||
}, set: { new_host_value in
|
||||
settings.push_notification_environment = .local_test(host: new_host_value)
|
||||
}
|
||||
)
|
||||
)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(UITextAutocapitalizationType.none)
|
||||
}
|
||||
|
||||
Toggle(NSLocalizedString("Enable experimental Purple API support", comment: "Developer mode setting to enable experimental Purple API support."), isOn: $settings.enable_experimental_purple_api)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
@@ -36,7 +36,7 @@ struct NotificationSettingsView: View {
|
||||
do {
|
||||
try await damus_state.push_notification_client.send_token()
|
||||
await self.sync_up_remote_notification_settings()
|
||||
settings.notifications_mode = new_value
|
||||
settings.notification_mode = new_value
|
||||
}
|
||||
catch {
|
||||
notification_mode_setting_error = String(format: NSLocalizedString("Error configuring push notifications with the server: %@", comment: "Error label shown when user tries to enable push notifications but something fails"), error.localizedDescription)
|
||||
@@ -47,7 +47,7 @@ struct NotificationSettingsView: View {
|
||||
Task {
|
||||
do {
|
||||
try await damus_state.push_notification_client.revoke_token()
|
||||
settings.notifications_mode = new_value
|
||||
settings.notification_mode = new_value
|
||||
notification_preferences_sync_state = .not_applicable
|
||||
}
|
||||
catch {
|
||||
@@ -67,7 +67,7 @@ struct NotificationSettingsView: View {
|
||||
set: { new_value in
|
||||
let old_value = raw_binding.wrappedValue
|
||||
raw_binding.wrappedValue = new_value
|
||||
if self.settings.notifications_mode == .push {
|
||||
if self.settings.notification_mode == .push {
|
||||
Task {
|
||||
await self.send_push_notification_preferences(on_failure: {
|
||||
raw_binding.wrappedValue = old_value
|
||||
@@ -114,7 +114,7 @@ struct NotificationSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if settings.enable_experimental_push_notifications {
|
||||
if settings.enable_push_notifications {
|
||||
Section(
|
||||
header: Text("General", comment: "Section header for general damus notifications user configuration"),
|
||||
footer: VStack {
|
||||
@@ -126,7 +126,7 @@ struct NotificationSettingsView: View {
|
||||
) {
|
||||
Picker(NSLocalizedString("Notifications mode", comment: "Prompt selection of the notification mode (Feature to switch between local notifications (generated from user's own phone) or push notifications (generated by Damus server)."),
|
||||
selection: Binding(
|
||||
get: { settings.notifications_mode },
|
||||
get: { settings.notification_mode },
|
||||
set: { newValue in
|
||||
self.try_to_set_notifications_mode(new_value: newValue)
|
||||
}
|
||||
@@ -194,7 +194,7 @@ struct NotificationSettingsView: View {
|
||||
}
|
||||
.onAppear(perform: {
|
||||
Task {
|
||||
if self.settings.notifications_mode == .push {
|
||||
if self.settings.notification_mode == .push {
|
||||
await self.sync_up_remote_notification_settings()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ struct TextViewWrapper: UIViewRepresentable {
|
||||
let updateCursorPosition: ((Int) -> Void)
|
||||
let initialTextSuffix: String?
|
||||
var initialTextSuffixWasAdded: Bool = false
|
||||
static let ESCAPE_SEQUENCES = ["\n", "@", " ", ", ", ". ", "! ", "? ", "; "]
|
||||
|
||||
init(attributedText: Binding<NSMutableAttributedString>,
|
||||
getFocusWordForMention: ((String?, NSRange?) -> Void)?,
|
||||
@@ -142,17 +143,27 @@ struct TextViewWrapper: UIViewRepresentable {
|
||||
|
||||
while startPosition != textView.beginningOfDocument {
|
||||
guard let previousPosition = textView.position(from: startPosition, offset: -1),
|
||||
let range = textView.textRange(from: previousPosition, to: startPosition),
|
||||
let text = textView.text(in: range), !text.isEmpty,
|
||||
let lastChar = text.last else {
|
||||
let range = textView.textRange(from: previousPosition, to: position),
|
||||
let text = textView.text(in: range), !text.isEmpty else {
|
||||
break
|
||||
}
|
||||
|
||||
if [" ", "\n", "@"].contains(lastChar) {
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
startPosition = previousPosition
|
||||
|
||||
if let styling = textView.textStyling(at: previousPosition, in: .backward),
|
||||
styling[NSAttributedString.Key.link] != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var found_escape_sequence = false
|
||||
for escape_sequence in Self.ESCAPE_SEQUENCES {
|
||||
if text.contains(escape_sequence) {
|
||||
startPosition = textView.position(from: startPosition, offset: escape_sequence.count) ?? startPosition
|
||||
found_escape_sequence = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found_escape_sequence { break }
|
||||
}
|
||||
|
||||
return startPosition == position ? nil : textView.textRange(from: startPosition, to: position)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// ClickableOverlay.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-09-20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Applies a property that makes `KFAnimatedImage` clickable again on iOS 18+
|
||||
fileprivate struct KFClickable: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Applies a property that makes `KFAnimatedImage` clickable again on iOS 18+
|
||||
func kfClickable() -> some View {
|
||||
return self.modifier(KFClickable())
|
||||
}
|
||||
}
|
||||
@@ -356,7 +356,7 @@ struct ZapSheetViewIfPossible: View {
|
||||
extension View {
|
||||
func hideKeyboard() {
|
||||
let resign = #selector(UIResponder.resignFirstResponder)
|
||||
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
|
||||
this_app.sendAction(resign, to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ class ReplyTests: XCTestCase {
|
||||
let ev = NostrEvent(content: content, keypair: test_keypair, tags: [])!
|
||||
let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
|
||||
let post_blocks = parse_post_blocks(content: content)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: [])
|
||||
let post = NostrPost(content: content, kind: NostrKind.text, tags: [])
|
||||
let post_tags = post.make_post_tags(post_blocks: post_blocks, tags: [])
|
||||
let tr = interpret_event_refs(tags: ev.tags)
|
||||
|
||||
XCTAssertNil(tr)
|
||||
@@ -240,7 +241,7 @@ class ReplyTests: XCTestCase {
|
||||
let content = "this is a @\(pk.npub) mention"
|
||||
let blocks = parse_post_blocks(content: content)
|
||||
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
|
||||
let ev = post_to_event(post: post, keypair: test_keypair_full)!
|
||||
let ev = post.to_event(keypair: test_keypair_full)!
|
||||
|
||||
XCTAssertEqual(ev.tags.count, 2)
|
||||
XCTAssertEqual(blocks.count, 3)
|
||||
@@ -255,7 +256,7 @@ class ReplyTests: XCTestCase {
|
||||
let content = "this is a @\(nsec) mention"
|
||||
let blocks = parse_post_blocks(content: content)
|
||||
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
|
||||
let ev = post_to_event(post: post, keypair: test_keypair_full)!
|
||||
let ev = post.to_event(keypair: test_keypair_full)!
|
||||
|
||||
XCTAssertEqual(ev.tags.count, 2)
|
||||
XCTAssertEqual(blocks.count, 3)
|
||||
@@ -275,7 +276,7 @@ class ReplyTests: XCTestCase {
|
||||
]
|
||||
|
||||
let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", tags: tags)
|
||||
let ev = post_to_event(post: post, keypair: test_keypair_full)!
|
||||
let ev = post.to_event(keypair: test_keypair_full)!
|
||||
|
||||
XCTAssertEqual(ev.content, "this is a (nostr:\(pubkey.npub)) mention")
|
||||
XCTAssertEqual(ev.tags[2][1].string(), pubkey.description)
|
||||
|
||||
@@ -16,7 +16,39 @@ final class ZapTests: XCTestCase {
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
|
||||
func test_alby_zap() throws {
|
||||
let zapjson = "eyJjb250ZW50Ijoi4pqhTm9uLWN1c3RvZGlhbCB6YXAgZnJvbSBteSBBbGJ5IEh1YiIsImNyZWF0ZWRfYXQiOjE3MjQ2ODUwNDcsImlkIjoiNGM3NWFiMWU3MDk4Y2NiN2FlYjhmZjdkNDIwMjM2ZDM1N2U1OGNjZmI3OWZiZTEwMTcwNGNiMzY0OTg3YjY4YSIsImtpbmQiOjk3MzUsInB1YmtleSI6Ijc5ZjAwZDNmNWExOWVjODA2MTg5ZmNhYjAzYzFiZTRmZjgxZDE4ZWU0ZjY1M2M4OGZhYzQxZmUwMzU3MGY0MzIiLCJzaWciOiI3OWM5ZDJjN2ExZWI1NmNhZjMyOTY1ZTRkMDJlYjJiYjFmYTY3NGViMDM4ZWE2MmFjZTg2YzBiMzA2OTJhMjU0YWU0M2JhNmMzNjcyMDJkZjgxNzQ5NGNhNTg4NzRkNWI1OWMxY2VhMDdjZTk5Mjk0MmIyOWYwZmVlZmJlM2FiZCIsInRhZ3MiOltbInAiLCIxNWI1Y2Y2Y2RmNGZkMWMwMmYyOGJjY2UwZjE5N2NhZmFlNGM4YzdjNjZhM2UyZTIzYWY5ZmU2MTA4NzUzMTVlIl0sWyJlIiwiYmNiMmZjZmUxYzQ2N2M1ZWM4Mjg1ZTM4NWMzNmVjMTM4Nzk3MDljZWQ5ZDg4MDBjYjM0MGViZjIxOGMzMjEwZCJdLFsiUCIsIjA1MjFkYjk1MzEwOTZkZmY3MDBkY2Y0MTBiMDFkYjQ3YWI2NTk4ZGU3ZTVlZjJjNWEyYmQ3ZTExNjAzMTViZjYiXSxbImJvbHQxMSIsImxuYmMxMHUxcG52ZXhoM2RwdXUyZDJ6bm4wZGNra3hhdG53M2hrZzZ0cGRzczg1Y3RzeXBuOHltbWR5cGtoamd6cGQzMzhqZ3pndzQzcW5wNHEyMjhhMnp0eGt3emF5cHZ6cnNoODIzcW5nbXY5N2YydjlwdXd2dHNhZGV0eXBtdXR5c2N3cHA1dmxjbGwwMHpwcGhoMzJ3OHV0NWpwcDVhMmZtcWg4c3o3bnUyaDd2MDdyMHU1bHN3ZzVsc3NwNXh2YXFlZnpsY2t6bXYwdzg5bHIwazB5dnI1eGQybmc1MmE1cmNkYXJmbTRmMGEwd2dwdXE5cXl5c2dxY3FwY3hxeXo1dnFlcHMzOXNleDUyc2ZtdHU5Z25tNWRhcGs1bGdsZDRwcDk2dXI1YTRhbTk0MHEyNXd6ZHNycmo1MjN4eWEwcnV4YTVscjk2M2cwMjk2cjZtZGZ5MjR2NjUzZXZjcHh5cjBtbWhnd21zcXh2cmhmZCJdLFsicHJlaW1hZ2UiLCJhZDA0N2MwMmZlNWYwNTljODA4NzdkNzk0YmU4OGU0N2M2NDRlYmVkZmRmZTY2M2IyODljOTMxNmRiNDk1ZjJkIl0sWyJkZXNjcmlwdGlvbiIsIntcImtpbmRcIjo5NzM0LFwiY3JlYXRlZF9hdFwiOjE3MjQ2ODUwMzgsXCJjb250ZW50XCI6XCLimqFOb24tY3VzdG9kaWFsIHphcCBmcm9tIG15IEFsYnkgSHViXCIsXCJ0YWdzXCI6W1tcInBcIixcIjE1YjVjZjZjZGY0ZmQxYzAyZjI4YmNjZTBmMTk3Y2FmYWU0YzhjN2M2NmEzZTJlMjNhZjlmZTYxMDg3NTMxNWVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9wdXJwbGVwYWcuZXMvXCIsXCJ3c3M6Ly9yZWxheS5nZXRhbGJ5LmNvbS92MVwiLFwid3NzOi8vbm9zdHIubW9tL1wiLFwid3NzOi8vbm9zdHIub3h0ci5kZXYvXCIsXCJ3c3M6Ly9ub3MubG9sL1wiLFwid3NzOi8vbm9zdHIud2luZS9cIixcIndzczovL3JlbGF5LmRhbXVzLmlvL1wiLFwid3NzOi8vcmVsYXkubm90b3NoaS53aW4vXCIsXCJ3c3M6Ly9lZGVuLm5vc3RyLmxhbmQvXCJdLFtcImFtb3VudFwiLFwiMTAwMDAwMFwiXSxbXCJlXCIsXCJiY2IyZmNmZTFjNDY3YzVlYzgyODVlMzg1YzM2ZWMxMzg3OTcwOWNlZDlkODgwMGNiMzQwZWJmMjE4YzMyMTBkXCJdXSxcInB1YmtleVwiOlwiMDUyMWRiOTUzMTA5NmRmZjcwMGRjZjQxMGIwMWRiNDdhYjY1OThkZTdlNWVmMmM1YTJiZDdlMTE2MDMxNWJmNlwiLFwiaWRcIjpcIjU3ZDg2MTIwMDc1MjFjMGI1MzJiOTFhZjI0OTgwOTVhMjUxZTYzZjQyNTE4N2U2Yzk1NzAwZmQwYTZiYWI3ZDRcIixcInNpZ1wiOlwiNzk4ZDczNTExOGJjZDE0MjI4YTEyYjZkNTI0MjNmZjI1YmI0ZWQ4Y2Q1ZGFjZjJmNTk3MWVmNTczZmRjM2ZjMDVmYzc5MzE4NWU2OTY4MmNjYTI0M2Q2NGYxNDdhNDQ5ODk2OGEwYmMyODhhZTgzZTc1YzAzZTk5ZjkzNmE2MDNcIn0iXV19Cg=="
|
||||
|
||||
guard let json_data = Data(base64Encoded: zapjson) else {
|
||||
XCTAssert(false)
|
||||
return
|
||||
}
|
||||
|
||||
let json_str = String(decoding: json_data, as: UTF8.self)
|
||||
|
||||
guard let ev = decode_nostr_event_json(json: json_str) else {
|
||||
XCTAssert(false)
|
||||
return
|
||||
}
|
||||
|
||||
let zapper = Pubkey(hex: "79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432")!
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: nil) else {
|
||||
XCTAssert(false)
|
||||
return
|
||||
}
|
||||
|
||||
let note_id = NoteId(hex: "bcb2fcfe1c467c5ec8285e385c36ec13879709ced9d8800cb340ebf218c3210d")!
|
||||
let author = Pubkey(hex: "15b5cf6cdf4fd1c02f28bcce0f197cafae4c8c7c66a3e2e23af9fe610875315e")!
|
||||
XCTAssertEqual(zap.zapper, zapper)
|
||||
XCTAssertEqual(zap.target, ZapTarget.note(id: note_id, author: author))
|
||||
|
||||
XCTAssertEqual(NotificationFormatter.zap_notification_title(zap), "Zap")
|
||||
XCTAssertEqual(NotificationFormatter.zap_notification_body(profiles: Profiles(ndb: test_damus_state.ndb), zap: zap), "You received 1k sats from 1q5sah9f:mqzxky65: \"⚡Non-custodial zap from my Alby Hub\"")
|
||||
|
||||
|
||||
}
|
||||
|
||||
func test_private_zap() throws {
|
||||
let alice = generate_new_keypair()
|
||||
let bob = generate_new_keypair()
|
||||
|
||||
@@ -193,7 +193,7 @@ class damusTests: XCTestCase {
|
||||
|
||||
func testMakeHashtagPost() {
|
||||
let post = NostrPost(content: "#damus some content #bitcoin derp #かっこいい wow", tags: [])
|
||||
let ev = post_to_event(post: post, keypair: test_keypair_full)!
|
||||
let ev = post.to_event(keypair: test_keypair_full)!
|
||||
|
||||
XCTAssertEqual(ev.tags.count, 3)
|
||||
XCTAssertEqual(ev.content, "#damus some content #bitcoin derp #かっこいい wow")
|
||||
@@ -270,7 +270,7 @@ class damusTests: XCTestCase {
|
||||
|
||||
private func createEventFromContentString(_ content: String) -> NostrEvent {
|
||||
let post = NostrPost(content: content, tags: [])
|
||||
guard let ev = post_to_event(post: post, keypair: test_keypair_full) else {
|
||||
guard let ev = post.to_event(keypair: test_keypair_full) else {
|
||||
XCTFail("Could not create event")
|
||||
return test_note
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
//
|
||||
// ActionViewController.swift
|
||||
// highlighter action extension
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-08-09.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import UniformTypeIdentifiers
|
||||
import SwiftUI
|
||||
|
||||
struct ShareExtensionView: View {
|
||||
@State var highlighter_state: HighlighterState = .loading
|
||||
let extensionContext: NSExtensionContext
|
||||
@State var state: DamusState? = nil
|
||||
@State var signedEvent: String? = nil
|
||||
|
||||
@State private var selectedText = ""
|
||||
@State private var selectedTextHeight: CGFloat = .zero
|
||||
@State private var selectedTextWidth: CGFloat = .zero
|
||||
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 15) {
|
||||
if let state {
|
||||
switch self.highlighter_state {
|
||||
case .loading:
|
||||
ProgressView()
|
||||
case .no_highlight_text:
|
||||
Group {
|
||||
Text("No text selected", comment: "Title indicating that a highlight cannot be posted because no text was selected.")
|
||||
.font(.largeTitle)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
Text("You cannot post a highlight because you have selected no text on the page! Please close this, select some text, and try again.", comment: "Label explaining a highlight cannot be made because there was no selected text, and some instructions on how to resolve the issue")
|
||||
.multilineTextAlignment(.center)
|
||||
Button(action: {
|
||||
self.done()
|
||||
}, label: {
|
||||
Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight")
|
||||
})
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
case .not_logged_in:
|
||||
Group {
|
||||
Text("Not logged in", comment: "Title indicating that a highlight cannot be posted because the user is not logged in.")
|
||||
.font(.largeTitle)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
Text("You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again.", comment: "Label explaining a highlight cannot be made because the user is not logged in")
|
||||
.multilineTextAlignment(.center)
|
||||
Button(action: {
|
||||
self.done()
|
||||
}, label: {
|
||||
Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight")
|
||||
})
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
case .loaded(let highlighted_text, let source_url):
|
||||
PostView(
|
||||
action: .highlighting(HighlightContentDraft(selected_text: highlighted_text, source: .external_url(source_url))),
|
||||
damus_state: state
|
||||
)
|
||||
case .failed(let error):
|
||||
Group {
|
||||
Text("Error", comment: "Title indicating that an error has occurred.")
|
||||
.font(.largeTitle)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
Text("An unexpected error occurred. Please contact Damus support via [Nostr](damus:npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955) or [email](support@damus.io) with the error message below.", comment: "Label explaining there was an error, and suggesting next steps")
|
||||
.multilineTextAlignment(.center)
|
||||
Text("Error: \(error)")
|
||||
Button(action: {
|
||||
self.done()
|
||||
}, label: {
|
||||
Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight")
|
||||
})
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
case .posted(event: let event):
|
||||
Group {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
Text("Posted", comment: "Title indicating that the user has posted a highlight successfully")
|
||||
.font(.largeTitle)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom)
|
||||
|
||||
Link(destination: URL(string: "damus:\(event.id.bech32)")!, label: {
|
||||
Text("Go to the app", comment: "Button label giving the user the option to go to the app after posting a highlight")
|
||||
})
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
Button(action: {
|
||||
self.done()
|
||||
}, label: {
|
||||
Text("Close", comment: "Button label giving the user the option to close the sheet from which they posted a highlight")
|
||||
})
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
case .cancelled:
|
||||
Group {
|
||||
Text("Cancelled", comment: "Title indicating that the user has cancelled.")
|
||||
.font(.largeTitle)
|
||||
.padding()
|
||||
Button(action: {
|
||||
self.done()
|
||||
}, label: {
|
||||
Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight")
|
||||
})
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
case .posting:
|
||||
Group {
|
||||
ProgressView()
|
||||
.frame(width: 20, height: 20)
|
||||
Text("Posting", comment: "Title indicating that the highlight post is being published to the network")
|
||||
.font(.largeTitle)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom)
|
||||
Text("Your highlight is being broadcasted to the network. Please wait.", comment: "Label explaining there their highlight publishing action is in progress")
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear(perform: {
|
||||
self.loadSharedUrl()
|
||||
guard let keypair = get_saved_keypair() else { return }
|
||||
guard keypair.privkey != nil else {
|
||||
self.highlighter_state = .not_logged_in
|
||||
return
|
||||
}
|
||||
self.state = DamusState(keypair: keypair)
|
||||
})
|
||||
.onChange(of: self.highlighter_state) {
|
||||
if case .cancelled = highlighter_state {
|
||||
self.done()
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.post)) { post_notification in
|
||||
switch post_notification {
|
||||
case .post(let post):
|
||||
self.post(post)
|
||||
case .cancel:
|
||||
self.highlighter_state = .cancelled
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
||||
guard let state else { return }
|
||||
switch phase {
|
||||
case .background:
|
||||
print("txn: 📙 HIGHLIGHTER BACKGROUNDED")
|
||||
Task { @MainActor in
|
||||
state.ndb.close()
|
||||
}
|
||||
break
|
||||
case .inactive:
|
||||
print("txn: 📙 HIGHLIGHTER INACTIVE")
|
||||
break
|
||||
case .active:
|
||||
print("txn: 📙 HIGHLIGHTER ACTIVE")
|
||||
state.pool.ping()
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
||||
guard let state else { return }
|
||||
print("txn: 📙 HIGHLIGHTER ACTIVE NOTIFY")
|
||||
if state.ndb.reopen() {
|
||||
print("txn: HIGHLIGHTER NOSTRDB REOPENED")
|
||||
} else {
|
||||
print("txn: HIGHLIGHTER NOSTRDB FAILED TO REOPEN closed: \(state.ndb.is_closed)")
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { obj in
|
||||
guard let state else { return }
|
||||
print("txn: 📙 HIGHLIGHTER BACKGROUNDED")
|
||||
Task { @MainActor in
|
||||
state.ndb.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadSharedUrl() {
|
||||
guard
|
||||
let extensionItem = extensionContext.inputItems.first as? NSExtensionItem,
|
||||
let itemProvider = extensionItem.attachments?.first else {
|
||||
self.highlighter_state = .failed(error: "Can't get itemProvider")
|
||||
return
|
||||
}
|
||||
|
||||
let propertyList = UTType.propertyList.identifier
|
||||
if itemProvider.hasItemConformingToTypeIdentifier(propertyList) {
|
||||
itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil, completionHandler: { (item, error) -> Void in
|
||||
guard let dictionary = item as? NSDictionary else { return }
|
||||
if error != nil {
|
||||
self.highlighter_state = .failed(error: "Error loading plist item: \(error?.localizedDescription ?? "Unknown")")
|
||||
return
|
||||
}
|
||||
OperationQueue.main.addOperation {
|
||||
if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary,
|
||||
let urlString = results["URL"] as? String,
|
||||
let selection = results["selectedText"] as? String,
|
||||
let url = URL(string: urlString) {
|
||||
guard selection != "" else {
|
||||
self.highlighter_state = .no_highlight_text
|
||||
return
|
||||
}
|
||||
self.highlighter_state = .loaded(highlighted_text: selection, source_url: url)
|
||||
}
|
||||
else {
|
||||
self.highlighter_state = .failed(error: "Cannot load results")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
self.highlighter_state = .failed(error: "No plist detected")
|
||||
}
|
||||
}
|
||||
|
||||
func post(_ post: NostrPost) {
|
||||
self.highlighter_state = .posting
|
||||
guard let state else {
|
||||
self.highlighter_state = .failed(error: "Damus state not initialized")
|
||||
return
|
||||
}
|
||||
guard let full_keypair = state.keypair.to_full() else {
|
||||
self.highlighter_state = .not_logged_in
|
||||
return
|
||||
}
|
||||
guard let posted_event = post.to_event(keypair: full_keypair) else {
|
||||
self.highlighter_state = .failed(error: "Cannot convert post data into a nostr event")
|
||||
return
|
||||
}
|
||||
state.postbox.send(posted_event, on_flush: .once({ flushed_event in
|
||||
if flushed_event.event.id == posted_event.id {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias
|
||||
self.highlighter_state = .posted(event: flushed_event.event)
|
||||
})
|
||||
}
|
||||
else {
|
||||
self.highlighter_state = .failed(error: "Flushed event is not the event we just tried to post.")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func done() {
|
||||
self.extensionContext.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
|
||||
enum HighlighterState: Equatable {
|
||||
case loading
|
||||
case no_highlight_text
|
||||
case not_logged_in
|
||||
case loaded(highlighted_text: String, source_url: URL)
|
||||
case posting
|
||||
case posted(event: NostrEvent)
|
||||
case cancelled
|
||||
case failed(error: String)
|
||||
}
|
||||
}
|
||||
|
||||
class ActionViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.view.tintColor = UIColor(DamusColors.purple)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let contentView = UIHostingController(rootView: ShareExtensionView(extensionContext: self.extensionContext!))
|
||||
self.addChild(contentView)
|
||||
self.view.addSubview(contentView.view)
|
||||
|
||||
// set up constraints
|
||||
contentView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
|
||||
contentView.view.bottomAnchor.constraint (equalTo: self.view.bottomAnchor).isActive = true
|
||||
contentView.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
|
||||
contentView.view.rightAnchor.constraint (equalTo: self.view.rightAnchor).isActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// HighlighterExtensionAliases.swift
|
||||
// highlighter action extension
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-08-12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
let this_app: UIApplication = UIApplication()
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionServiceRoleType</key>
|
||||
<string>NSExtensionServiceRoleTypeViewer</string>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRuleSupportsText</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSExtensionJavaScriptPreprocessingFile</key>
|
||||
<string>getSelection</string>
|
||||
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
|
||||
<true/>
|
||||
<key>NSExtensionServiceAllowsTouchBarItem</key>
|
||||
<true/>
|
||||
<key>NSExtensionServiceFinderPreviewIconName</key>
|
||||
<string>NSActionTemplate</string>
|
||||
<key>NSExtensionServiceTouchBarBezelColorName</key>
|
||||
<string>TouchBarBezel</string>
|
||||
<key>NSExtensionServiceTouchBarIconName</key>
|
||||
<string>NSActionTemplate</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.ui-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ActionViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "highlighter.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"color" : {
|
||||
"reference" : "systemPurpleColor"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
var Share = function() {};
|
||||
|
||||
Share.prototype = {
|
||||
run: function(arguments) {
|
||||
arguments.completionFunction({"URL": document.URL, "selectedText": document.getSelection().toString()});
|
||||
},
|
||||
finalize: function(arguments) {
|
||||
// alert shared!
|
||||
}
|
||||
};
|
||||
|
||||
var ExtensionPreprocessingJS = new Share
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.damus</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.jb55.damus2</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
+3
-1
@@ -111,8 +111,10 @@ class Ndb {
|
||||
var ok = false
|
||||
while !ok && mapsize > 1024 * 1024 * 700 {
|
||||
var cfg = ndb_config(flags: 0, ingester_threads: ingest_threads, mapsize: mapsize, filter_context: nil, ingest_filter: nil)
|
||||
ok = ndb_init(&ndb_p, testdir, &cfg) != 0
|
||||
let res = ndb_init(&ndb_p, testdir, &cfg)
|
||||
ok = res != 0;
|
||||
if !ok {
|
||||
Log.error("ndb_init failed: %d, reducing mapsize from %d to %d", for: .storage, res, mapsize, mapsize / 2)
|
||||
mapsize /= 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +335,10 @@ extension NdbNote {
|
||||
public var referenced_mute_items: References<MuteItem> {
|
||||
References<MuteItem>(tags: self.tags)
|
||||
}
|
||||
|
||||
public var referenced_comment_items: References<CommentItem> {
|
||||
References<CommentItem>(tags: self.tags)
|
||||
}
|
||||
|
||||
public var references: References<RefId> {
|
||||
References<RefId>(tags: self.tags)
|
||||
@@ -355,6 +359,9 @@ extension NdbNote {
|
||||
if known_kind == .dm {
|
||||
return decrypted(keypair: keypair) ?? "*failed to decrypt content*"
|
||||
}
|
||||
else if known_kind == .highlight {
|
||||
return self.referenced_comment_items.first?.content ?? ""
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
+11
-2
@@ -4893,8 +4893,17 @@ mdb_env_setup_locks(MDB_env *env, MDB_name *fname, int mode, int *excl)
|
||||
#ifdef MDB_SHORT_SEMNAMES
|
||||
encbuf[9] = '\0'; /* drop name from 15 chars to 14 chars */
|
||||
#endif
|
||||
sprintf(env->me_txns->mti_rmname, "/MDBr%s", encbuf);
|
||||
sprintf(env->me_txns->mti_wmname, "/MDBw%s", encbuf);
|
||||
|
||||
#define DEF_STR(x) #x
|
||||
#define DEF_TO_STRING(x) DEF_STR(x)
|
||||
sprintf(env->me_txns->mti_rmname, DEF_TO_STRING(MDB_SEM_NAME_PREFIX) "/MDBr%s", encbuf);
|
||||
sprintf(env->me_txns->mti_wmname, DEF_TO_STRING(MDB_SEM_NAME_PREFIX) "/MDBw%s", encbuf);
|
||||
#undef DEF_STR
|
||||
#undef DEF_TO_STRING
|
||||
|
||||
printf("mdb_env_setup_locks: using semnames '%s' (%d), '%s' (%d)\n",
|
||||
env->me_txns->mti_rmname, strlen(env->me_txns->mti_rmname),
|
||||
env->me_txns->mti_wmname, strlen(env->me_txns->mti_wmname));
|
||||
/* Clean up after a previous run, if needed: Try to
|
||||
* remove both semaphores before doing anything else.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user