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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
@@ -10,5 +12,9 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.jb55.damus2</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ struct NotificationFormatter {
|
|||||||
var identifier = ""
|
var identifier = ""
|
||||||
|
|
||||||
switch notify.type {
|
switch notify.type {
|
||||||
|
case .tagged:
|
||||||
|
title = String(format: NSLocalizedString("Tagged by %@", comment: "Tagged by heading in local notification"), displayName)
|
||||||
|
identifier = "myMentionNotification"
|
||||||
case .mention:
|
case .mention:
|
||||||
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
|
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
|
||||||
identifier = "myMentionNotification"
|
identifier = "myMentionNotification"
|
||||||
@@ -70,6 +73,9 @@ struct NotificationFormatter {
|
|||||||
case .zap, .profile_zap:
|
case .zap, .profile_zap:
|
||||||
// not handled here. Try `format_message(displayName: String, notify: LocalNotification, state: HeadlessDamusState) async -> (content: UNMutableNotificationContent, identifier: String)?`
|
// not handled here. Try `format_message(displayName: String, notify: LocalNotification, state: HeadlessDamusState) async -> (content: UNMutableNotificationContent, identifier: String)?`
|
||||||
return nil
|
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.title = title
|
||||||
content.body = notify.content
|
content.body = notify.content
|
||||||
@@ -87,10 +93,11 @@ struct NotificationFormatter {
|
|||||||
|
|
||||||
// If it does not work, try async formatting methods
|
// If it does not work, try async formatting methods
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
switch notify.type {
|
switch notify.type {
|
||||||
case .zap, .profile_zap:
|
case .zap, .profile_zap:
|
||||||
guard let zap = await get_zap(from: notify.event, state: state) else {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
content.title = Self.zap_notification_title(zap)
|
content.title = Self.zap_notification_title(zap)
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
// Log that we got a push notification
|
// Log that we got a push notification
|
||||||
Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex())
|
Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex())
|
||||||
|
|
||||||
guard let state = NotificationExtensionState(),
|
guard let state = NotificationExtensionState() else {
|
||||||
let display_name = state.ndb.lookup_profile(nostr_event.pubkey)?.unsafeUnownedValue?.profile?.display_name // We are not holding the txn here.
|
Log.debug("Failed to open nostrdb", for: .push_notifications)
|
||||||
else {
|
|
||||||
// Something failed to initialize so let's go for the next best thing
|
// 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 {
|
guard let improved_content = NotificationFormatter.shared.format_message(event: nostr_event) else {
|
||||||
// We cannot format this nostr event. Suppress notification.
|
// We cannot format this nostr event. Suppress notification.
|
||||||
@@ -39,7 +39,11 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
contentHandler(improved_content)
|
contentHandler(improved_content)
|
||||||
return
|
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.
|
// 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
|
// 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) {
|
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 {
|
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.
|
// We should not display notification for this event. Suppress notification.
|
||||||
// contentHandler(UNNotificationContent())
|
// contentHandler(UNNotificationContent())
|
||||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
// 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 {
|
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.
|
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
||||||
// contentHandler(UNNotificationContent())
|
// contentHandler(UNNotificationContent())
|
||||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
||||||
@@ -70,9 +76,13 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
if let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: display_name, notify: notification_object, state: state) {
|
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: name, notify: notification_object, state: state) else {
|
||||||
contentHandler(improvedContent)
|
|
||||||
|
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)
|
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||||
}
|
}
|
||||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||||
|
.kfClickable()
|
||||||
.position(x: geo.size.width / 2, y: geo.size.height / 2)
|
.position(x: geo.size.width / 2, y: geo.size.height / 2)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Text(url.absoluteString)
|
Text(url.absoluteString)
|
||||||
@@ -274,8 +275,14 @@ struct ImageCarousel<Content: View>: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Medias
|
if #available(iOS 18.0, *) {
|
||||||
.onTapGesture { }
|
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 {
|
if urls.count > 1 {
|
||||||
PageControlView(currentPage: $model.selectedIndex, numberOfPages: urls.count)
|
PageControlView(currentPage: $model.selectedIndex, numberOfPages: urls.count)
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ enum OpenWalletError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
||||||
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
if let url = URL(string: "\(wallet.link)\(invoice)"), this_app.canOpenURL(url) {
|
||||||
UIApplication.shared.open(url)
|
this_app.open(url)
|
||||||
} else {
|
} else {
|
||||||
guard let store_link = wallet.appStoreLink else {
|
guard let store_link = wallet.appStoreLink else {
|
||||||
throw OpenWalletError.no_wallet_to_open
|
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
|
throw OpenWalletError.store_link_invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
guard UIApplication.shared.canOpenURL(url) else {
|
guard this_app.canOpenURL(url) else {
|
||||||
throw OpenWalletError.system_cannot_open_store_link
|
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)
|
.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 event: NostrEvent?
|
||||||
let attributedString: AttributedString
|
let attributedString: AttributedString
|
||||||
let textAlignment: NSTextAlignment
|
let textAlignment: NSTextAlignment
|
||||||
@State private var showHighlightPost = false
|
@State private var selectedTextActionState: SelectedTextActionState = .hide
|
||||||
@State private var showMutePost = false
|
|
||||||
@State private var selectedText = ""
|
|
||||||
@State private var selectedTextHeight: CGFloat = .zero
|
@State private var selectedTextHeight: CGFloat = .zero
|
||||||
@State private var selectedTextWidth: CGFloat = .zero
|
@State private var selectedTextWidth: CGFloat = .zero
|
||||||
|
|
||||||
@@ -38,9 +36,12 @@ struct SelectableText: View {
|
|||||||
fixedWidth: selectedTextWidth,
|
fixedWidth: selectedTextWidth,
|
||||||
textAlignment: self.textAlignment,
|
textAlignment: self.textAlignment,
|
||||||
enableHighlighting: self.enableHighlighting(),
|
enableHighlighting: self.enableHighlighting(),
|
||||||
showHighlightPost: $showHighlightPost,
|
postHighlight: { selectedText in
|
||||||
showMutePost: $showMutePost,
|
self.selectedTextActionState = .show_highlight_post_view(highlighted_text: selectedText)
|
||||||
selectedText: $selectedText,
|
},
|
||||||
|
muteWord: { selectedText in
|
||||||
|
self.selectedTextActionState = .show_mute_word_view(highlighted_text: selectedText)
|
||||||
|
},
|
||||||
height: $selectedTextHeight
|
height: $selectedTextHeight
|
||||||
)
|
)
|
||||||
.padding([.leading, .trailing], -1.0)
|
.padding([.leading, .trailing], -1.0)
|
||||||
@@ -55,17 +56,30 @@ struct SelectableText: View {
|
|||||||
self.selectedTextWidth = newSize.width
|
self.selectedTextWidth = newSize.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showHighlightPost) {
|
.sheet(isPresented: Binding(get: {
|
||||||
if let event {
|
return self.selectedTextActionState.should_show_highlight_post_view()
|
||||||
HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText)
|
}, set: { newValue in
|
||||||
.presentationDragIndicator(.visible)
|
self.selectedTextActionState = newValue ? .show_highlight_post_view(highlighted_text: self.selectedTextActionState.highlighted_text() ?? "") : .hide
|
||||||
.presentationDetents([.height(selectedTextHeight + 150), .medium, .large])
|
})) {
|
||||||
|
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) {
|
.sheet(isPresented: Binding(get: {
|
||||||
AddMuteItemView(state: damus_state, new_text: $selectedText)
|
return self.selectedTextActionState.should_show_mute_word_view()
|
||||||
.presentationDragIndicator(.visible)
|
}, set: { newValue in
|
||||||
.presentationDetents([.height(300), .medium, .large])
|
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)
|
.frame(height: selectedTextHeight)
|
||||||
}
|
}
|
||||||
@@ -73,17 +87,42 @@ struct SelectableText: View {
|
|||||||
func enableHighlighting() -> Bool {
|
func enableHighlighting() -> Bool {
|
||||||
self.event != nil
|
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 {
|
fileprivate class TextView: UITextView {
|
||||||
@Binding var showHighlightPost: Bool
|
var postHighlight: (String) -> Void
|
||||||
@Binding var showMutePost: Bool
|
var muteWord: (String) -> Void
|
||||||
@Binding var selectedText: String
|
|
||||||
|
|
||||||
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, showMutePost: Binding<Bool>, selectedText: Binding<String>) {
|
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void) {
|
||||||
self._showHighlightPost = showHighlightPost
|
self.postHighlight = postHighlight
|
||||||
self._showMutePost = showMutePost
|
self.muteWord = muteWord
|
||||||
self._selectedText = selectedText
|
|
||||||
super.init(frame: frame, textContainer: textContainer)
|
super.init(frame: frame, textContainer: textContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,22 +141,25 @@ fileprivate class TextView: UITextView {
|
|||||||
|
|
||||||
return super.canPerformAction(action, withSender: sender)
|
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?) {
|
@objc public func highlightText(_ sender: Any?) {
|
||||||
guard let selectedRange = self.selectedTextRange else { return }
|
guard let selectedText = self.getSelectedText() else { return }
|
||||||
selectedText = self.text(in: selectedRange) ?? ""
|
self.postHighlight(selectedText)
|
||||||
showHighlightPost.toggle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func muteText(_ sender: Any?) {
|
@objc public func muteText(_ sender: Any?) {
|
||||||
guard let selectedRange = self.selectedTextRange else { return }
|
guard let selectedText = self.getSelectedText() else { return }
|
||||||
selectedText = self.text(in: selectedRange) ?? ""
|
self.muteWord(selectedText)
|
||||||
showMutePost.toggle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||||
|
|
||||||
let attributedString: AttributedString
|
let attributedString: AttributedString
|
||||||
let textColor: UIColor
|
let textColor: UIColor
|
||||||
@@ -125,13 +167,12 @@ fileprivate class TextView: UITextView {
|
|||||||
let fixedWidth: CGFloat
|
let fixedWidth: CGFloat
|
||||||
let textAlignment: NSTextAlignment
|
let textAlignment: NSTextAlignment
|
||||||
let enableHighlighting: Bool
|
let enableHighlighting: Bool
|
||||||
@Binding var showHighlightPost: Bool
|
let postHighlight: (String) -> Void
|
||||||
@Binding var showMutePost: Bool
|
let muteWord: (String) -> Void
|
||||||
@Binding var selectedText: String
|
|
||||||
@Binding var height: CGFloat
|
@Binding var height: CGFloat
|
||||||
|
|
||||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
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.isEditable = false
|
||||||
view.dataDetectorTypes = .all
|
view.dataDetectorTypes = .all
|
||||||
view.isSelectable = true
|
view.isSelectable = true
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ struct SupporterBadge: View {
|
|||||||
let percent: Int?
|
let percent: Int?
|
||||||
let purple_account: DamusPurple.Account?
|
let purple_account: DamusPurple.Account?
|
||||||
let style: Style
|
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.percent = percent
|
||||||
self.purple_account = purple_account
|
self.purple_account = purple_account
|
||||||
self.style = style
|
self.style = style
|
||||||
|
self.text_color = text_color
|
||||||
}
|
}
|
||||||
|
|
||||||
let size: CGFloat = 17
|
let size: CGFloat = 17
|
||||||
@@ -31,7 +33,7 @@ struct SupporterBadge: View {
|
|||||||
if self.style == .full {
|
if self.style == .full {
|
||||||
let date = format_date(date: purple_account.created_at, time_style: .none)
|
let date = format_date(date: purple_account.created_at, time_style: .none)
|
||||||
Text(date)
|
Text(date)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(text_color)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,19 +27,26 @@ struct TranslateView: View {
|
|||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
|
|
||||||
|
@Binding var isAppleTranslationPopoverPresented: Bool
|
||||||
|
|
||||||
@ObservedObject var translations_model: TranslationModel
|
@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.damus_state = damus_state
|
||||||
self.event = event
|
self.event = event
|
||||||
self.size = size
|
self.size = size
|
||||||
|
self._isAppleTranslationPopoverPresented = isAppleTranslationPopoverPresented
|
||||||
self._translations_model = ObservedObject(wrappedValue: damus_state.events.get_cache_data(event.id).translations_model)
|
self._translations_model = ObservedObject(wrappedValue: damus_state.events.get_cache_data(event.id).translations_model)
|
||||||
}
|
}
|
||||||
|
|
||||||
var TranslateButton: some View {
|
var TranslateButton: some View {
|
||||||
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
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()
|
.translate_button_style()
|
||||||
}
|
}
|
||||||
@@ -74,17 +81,25 @@ struct TranslateView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func should_transl(_ note_lang: String) -> Bool {
|
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 {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
switch self.translations_model.state {
|
switch self.translations_model.state {
|
||||||
case .havent_tried:
|
case .havent_tried:
|
||||||
if damus_state.settings.auto_translate {
|
if damus_state.settings.auto_translate && damus_state.settings.translation_service != .none {
|
||||||
Text("")
|
Text("")
|
||||||
} else if let note_lang = translations_model.note_language, should_transl(note_lang) {
|
} else if let note_lang = translations_model.note_language, should_transl(note_lang) {
|
||||||
TranslateButton
|
TranslateButton
|
||||||
} else {
|
} else {
|
||||||
Text("")
|
Text("")
|
||||||
}
|
}
|
||||||
@@ -114,9 +129,11 @@ extension View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct TranslateView_Previews: PreviewProvider {
|
struct TranslateView_Previews: PreviewProvider {
|
||||||
|
@State static var isAppleTranslationPopoverPresented: Bool = false
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let ds = test_damus_state
|
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?)
|
case content(String, TagsSequence?)
|
||||||
|
|
||||||
init(note: NostrEvent, keypair: Keypair) {
|
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)
|
self = .content(note.get_content(keypair), note.tags)
|
||||||
} else {
|
} else {
|
||||||
self = .note(note)
|
self = .note(note)
|
||||||
|
|||||||
+25
-9
@@ -57,6 +57,10 @@ enum Sheets: Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func present_sheet(_ sheet: Sheets) {
|
||||||
|
notify(.present_sheet(sheet))
|
||||||
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
let appDelegate: AppDelegate?
|
let appDelegate: AppDelegate?
|
||||||
@@ -73,7 +77,12 @@ struct ContentView: View {
|
|||||||
|
|
||||||
@State var active_sheet: Sheets? = nil
|
@State var active_sheet: Sheets? = nil
|
||||||
@State var damus_state: DamusState!
|
@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 muting: MuteItem? = nil
|
||||||
@State var confirm_mute: Bool = false
|
@State var confirm_mute: Bool = false
|
||||||
@State var hide_bar: Bool = false
|
@State var hide_bar: Bool = false
|
||||||
@@ -97,9 +106,16 @@ struct ContentView: View {
|
|||||||
isSideBarOpened = false
|
isSideBarOpened = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var timelineNavItem: Text {
|
var timelineNavItem: some View {
|
||||||
return Text(timeline_name(selected_timeline))
|
VStack {
|
||||||
.bold()
|
Text(timeline_name(selected_timeline))
|
||||||
|
.bold()
|
||||||
|
if let menu_subtitle {
|
||||||
|
Text(menu_subtitle)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MainContent(damus: DamusState) -> some View {
|
func MainContent(damus: DamusState) -> some View {
|
||||||
@@ -118,7 +134,7 @@ struct ContentView: View {
|
|||||||
PostingTimelineView(damus_state: damus_state!, home: home, active_sheet: $active_sheet)
|
PostingTimelineView(damus_state: damus_state!, home: home, active_sheet: $active_sheet)
|
||||||
|
|
||||||
case .notifications:
|
case .notifications:
|
||||||
NotificationsView(state: damus, notifications: home.notifications)
|
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
|
||||||
|
|
||||||
case .dms:
|
case .dms:
|
||||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
|
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
|
||||||
@@ -718,7 +734,7 @@ struct ContentView: View {
|
|||||||
selected_timeline = .dms
|
selected_timeline = .dms
|
||||||
damus_state.dms.set_active_dm(target.pubkey)
|
damus_state.dms.set_active_dm(target.pubkey)
|
||||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
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)
|
open_event(ev: target)
|
||||||
case .profile_zap:
|
case .profile_zap:
|
||||||
break
|
break
|
||||||
@@ -819,7 +835,7 @@ func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [Nos
|
|||||||
|
|
||||||
|
|
||||||
func setup_notifications() {
|
func setup_notifications() {
|
||||||
UIApplication.shared.registerForRemoteNotifications()
|
this_app.registerForRemoteNotifications()
|
||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
|
|
||||||
center.getNotificationSettings { settings in
|
center.getNotificationSettings { settings in
|
||||||
@@ -1051,7 +1067,7 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
|||||||
//let post = tup.0
|
//let post = tup.0
|
||||||
//let to_relays = tup.1
|
//let to_relays = tup.1
|
||||||
print("post \(post.content)")
|
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
|
return false
|
||||||
}
|
}
|
||||||
postbox.send(new_ev)
|
postbox.send(new_ev)
|
||||||
@@ -1112,7 +1128,7 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
|||||||
}
|
}
|
||||||
case .hashtag(let ht):
|
case .hashtag(let ht):
|
||||||
result(.filter(.filter_hashtag([ht.hashtag])))
|
result(.filter(.filter_hashtag([ht.hashtag])))
|
||||||
case .param, .quote:
|
case .param, .quote, .reference:
|
||||||
// doesn't really make sense here
|
// doesn't really make sense here
|
||||||
break
|
break
|
||||||
case .naddr(let naddr):
|
case .naddr(let naddr):
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ public class CameraService: NSObject, Identifiable {
|
|||||||
|
|
||||||
DispatchQueue.main.async {
|
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: {
|
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)
|
options: [:], completionHandler: nil)
|
||||||
|
|
||||||
}, secondaryAction: 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)):
|
case let (.pubkey(pk), .pubkey(follow_pk)):
|
||||||
return pk == follow_pk
|
return pk == follow_pk
|
||||||
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
||||||
(.event, _), (.quote, _), (.param, _), (.naddr, _):
|
(.event, _), (.quote, _), (.param, _), (.naddr, _), (.reference(_), _):
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,79 @@ class DamusState: HeadlessDamusState {
|
|||||||
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
||||||
self.emoji_provider = emoji_provider
|
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
|
@discardableResult
|
||||||
func add_zap(zap: Zapping) -> Bool {
|
func add_zap(zap: Zapping) -> Bool {
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ class Drafts: ObservableObject {
|
|||||||
@Published var post: DraftArtifacts? = nil
|
@Published var post: DraftArtifacts? = nil
|
||||||
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
|
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
|
||||||
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
|
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
|
||||||
|
@Published var highlights: [HighlightSource: DraftArtifacts] = [:]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
|
|
||||||
enum FriendFilter: String, StringCodable {
|
enum FriendFilter: String, StringCodable {
|
||||||
case all
|
case all
|
||||||
case friends
|
case friends_of_friends
|
||||||
|
|
||||||
init?(from string: String) {
|
init?(from string: String) {
|
||||||
guard let ff = FriendFilter(rawValue: string) else {
|
guard let ff = FriendFilter(rawValue: string) else {
|
||||||
@@ -27,8 +27,17 @@ enum FriendFilter: String, StringCodable {
|
|||||||
switch self {
|
switch self {
|
||||||
case .all:
|
case .all:
|
||||||
return true
|
return true
|
||||||
case .friends:
|
case .friends_of_friends:
|
||||||
return contacts.is_in_friendosphere(pubkey)
|
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 event_ref: String? = nil
|
||||||
var url_ref: URL? = nil
|
var url_ref: URL? = nil
|
||||||
var context: String? = nil
|
var context: String? = nil
|
||||||
|
|
||||||
|
// MARK: - Initializers and parsers
|
||||||
|
|
||||||
static func parse(from ev: NostrEvent) -> HighlightEvent {
|
static func parse(from ev: NostrEvent) -> HighlightEvent {
|
||||||
var highlight = HighlightEvent(event: ev)
|
var highlight = HighlightEvent(event: ev)
|
||||||
|
|
||||||
|
var best_url_source: (url: URL, tagged_as_source: Bool)? = nil
|
||||||
|
|
||||||
for tag in ev.tags {
|
for tag in ev.tags {
|
||||||
guard tag.count >= 2 else { continue }
|
guard tag.count >= 2 else { continue }
|
||||||
switch tag[0].string() {
|
switch tag[0].string() {
|
||||||
case "e": highlight.event_ref = tag[1].string()
|
case "e": highlight.event_ref = tag[1].string()
|
||||||
case "a": 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()
|
case "context": highlight.context = tag[1].string()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let best_url_source {
|
||||||
|
highlight.url_ref = best_url_source.url
|
||||||
|
}
|
||||||
|
|
||||||
return highlight
|
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
|
/// 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
|
/// 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() {
|
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_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_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 }
|
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)
|
process_contact_event(state: damus_state, ev: latest_contact_event)
|
||||||
damus_state.contacts.delegate = self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - ContactsDelegate functions
|
// MARK: - ContactsDelegate functions
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Foundation
|
|||||||
enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||||
var id: String { self.rawValue }
|
var id: String { self.rawValue }
|
||||||
case nostrBuild
|
case nostrBuild
|
||||||
case nostrImg
|
case nostrcheck
|
||||||
|
|
||||||
init?(from string: String) {
|
init?(from string: String) {
|
||||||
guard let mu = MediaUploader(rawValue: string) else {
|
guard let mu = MediaUploader(rawValue: string) else {
|
||||||
@@ -23,95 +23,73 @@ enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
|||||||
func to_string() -> String {
|
func to_string() -> String {
|
||||||
return rawValue
|
return rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameParam: String {
|
var nameParam: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .nostrBuild:
|
case .nostrBuild:
|
||||||
return "\"fileToUpload\""
|
return "\"fileToUpload\""
|
||||||
case .nostrImg:
|
default:
|
||||||
return "\"image\""
|
return "\"file\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportsVideo: Bool {
|
var supportsVideo: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .nostrBuild:
|
case .nostrBuild:
|
||||||
return true
|
return true
|
||||||
case .nostrImg:
|
case .nostrcheck:
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Model: Identifiable, Hashable {
|
struct Model: Identifiable, Hashable {
|
||||||
var id: String { self.tag }
|
var id: String { self.tag }
|
||||||
var index: Int
|
var index: Int
|
||||||
var tag: String
|
var tag: String
|
||||||
var displayName : String
|
var displayName : String
|
||||||
}
|
}
|
||||||
|
|
||||||
var model: Model {
|
var model: Model {
|
||||||
switch self {
|
switch self {
|
||||||
case .nostrBuild:
|
case .nostrBuild:
|
||||||
return .init(index: -1, tag: "nostrBuild", displayName: "nostr.build")
|
return .init(index: -1, tag: "nostrBuild", displayName: "nostr.build")
|
||||||
case .nostrImg:
|
case .nostrcheck:
|
||||||
return .init(index: 0, tag: "nostrImg", displayName: "nostrimg.com")
|
return .init(index: 0, tag: "nostrcheck", displayName: "nostrcheck.me")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var postAPI: String {
|
var postAPI: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .nostrBuild:
|
case .nostrBuild:
|
||||||
return "https://nostr.build/api/v2/upload/files"
|
return "https://nostr.build/api/v2/nip96/upload"
|
||||||
case .nostrImg:
|
case .nostrcheck:
|
||||||
return "https://nostrimg.com/api/upload"
|
return "https://nostrcheck.me/api/v2/media"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMediaURL(from data: Data) -> String? {
|
func getMediaURL(from data: Data) -> String? {
|
||||||
switch self {
|
do {
|
||||||
case .nostrBuild:
|
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any],
|
||||||
do {
|
let status = jsonObject["status"] as? String {
|
||||||
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 status == "success", let dataArray = jsonObject["data"] as? [[String: Any]] {
|
if let tags = nip94Event["tags"] as? [[String]] {
|
||||||
|
for tagArray in tags {
|
||||||
var urls: [String] = []
|
if tagArray.count > 1, tagArray[0] == "url" {
|
||||||
|
return tagArray[1]
|
||||||
for dataDict in dataArray {
|
|
||||||
if let mainUrl = dataDict["url"] as? String {
|
|
||||||
urls.append(mainUrl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls.joined(separator: "\n")
|
|
||||||
} else if status == "error", let message = jsonObject["message"] as? String {
|
|
||||||
print("Upload Error: \(message)")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
} else if status == "error", let message = jsonObject["message"] as? String {
|
||||||
} catch {
|
print("Upload Error: \(message)")
|
||||||
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 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let stringContainingName = responseString[startIndex..<responseString.endIndex]
|
|
||||||
guard let endIndex = stringContainingName.range(of: "\"")?.lowerBound else {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
let nostrBuildImageName = responseString[startIndex..<endIndex]
|
} catch {
|
||||||
let nostrBuildURL = "\(nostrBuildImageName)"
|
print("Failed JSONSerialization")
|
||||||
return nostrBuildURL
|
return nil
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,46 +256,3 @@ func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
|
|||||||
|
|
||||||
return nil
|
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 {
|
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
|
// 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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,36 +61,55 @@ func generate_local_notification_object(from ev: NostrEvent, state: HeadlessDamu
|
|||||||
|
|
||||||
if type == .text, state.settings.mention_notification {
|
if type == .text, state.settings.mention_notification {
|
||||||
let blocks = ev.blocks(state.keypair).blocks
|
let blocks = ev.blocks(state.keypair).blocks
|
||||||
|
|
||||||
for case .mention(let mention) in blocks {
|
for case .mention(let mention) in blocks {
|
||||||
guard case .pubkey(let pk) = mention.ref, pk == state.keypair.pubkey else {
|
guard case .pubkey(let pk) = mention.ref, pk == state.keypair.pubkey else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let content_preview = render_notification_content_preview(ev: ev, profiles: state.profiles, keypair: state.keypair)
|
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,
|
} else if type == .boost,
|
||||||
state.settings.repost_notification,
|
state.settings.repost_notification,
|
||||||
let inner_ev = ev.get_inner_event()
|
let inner_ev = ev.get_inner_event()
|
||||||
{
|
{
|
||||||
let content_preview = render_notification_content_preview(ev: inner_ev, profiles: state.profiles, keypair: state.keypair)
|
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)
|
return LocalNotification(type: .repost, event: ev, target: .note(inner_ev), content: content_preview)
|
||||||
} else if type == .like,
|
} else if type == .like, state.settings.like_notification, let evid = ev.referenced_ids.last {
|
||||||
state.settings.like_notification,
|
if let txn = state.ndb.lookup_note(evid, txn_name: "local_notification_like"),
|
||||||
let evid = ev.referenced_ids.last,
|
let liked_event = txn.unsafeUnownedValue
|
||||||
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: .note(liked_event), content: content_preview)
|
||||||
let content_preview = render_notification_content_preview(ev: liked_event, profiles: state.profiles, keypair: state.keypair)
|
} else {
|
||||||
return LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview)
|
return LocalNotification(type: .like, event: ev, target: .note_id(evid), content: "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if type == .dm,
|
else if type == .dm,
|
||||||
state.settings.dm_notification {
|
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")
|
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,
|
else if type == .zap,
|
||||||
state.settings.zap_notification {
|
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
|
return nil
|
||||||
|
|||||||
+77
-1
@@ -17,10 +17,86 @@ struct NostrPost {
|
|||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.tags = tags
|
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] {
|
func parse_post_blocks(content: String) -> [Block] {
|
||||||
return parse_note_content(content: .content(content, nil)).blocks
|
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 {
|
mutating func set_device_token(new_device_token: Data) async throws {
|
||||||
self.device_token = new_device_token
|
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()
|
try await self.send_token()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ struct PushNotificationClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func current_push_notification_environment() -> Environment {
|
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 {
|
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 local_test(host: String?)
|
||||||
|
case staging
|
||||||
case production
|
case production
|
||||||
|
|
||||||
func text_description() -> String {
|
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)")
|
return NSLocalizedString("Test (local)", comment: "Label indicating a local test environment for Push notification functionality (Developer feature)")
|
||||||
case .production:
|
case .production:
|
||||||
return NSLocalizedString("Production", comment: "Label indicating the production environment for Push notification functionality")
|
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):
|
case .local_test(let host):
|
||||||
URL(string: "http://\(host ?? "localhost:8000")") ?? Constants.PUSH_NOTIFICATION_SERVER_TEST_BASE_URL
|
URL(string: "http://\(host ?? "localhost:8000")") ?? Constants.PUSH_NOTIFICATION_SERVER_TEST_BASE_URL
|
||||||
case .production:
|
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)
|
self = .local_test(host: nil)
|
||||||
case "production":
|
case "production":
|
||||||
self = .production
|
self = .production
|
||||||
|
case "staging":
|
||||||
|
self = .staging
|
||||||
default:
|
default:
|
||||||
let components = string.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
|
let components = string.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
|
||||||
if components.count == 2 && components[0] == "local_test" {
|
if components.count == 2 && components[0] == "local_test" {
|
||||||
@@ -257,6 +263,8 @@ extension PushNotificationClient {
|
|||||||
return "local_test:\(host)"
|
return "local_test:\(host)"
|
||||||
}
|
}
|
||||||
return "local_test"
|
return "local_test"
|
||||||
|
case .staging:
|
||||||
|
return "staging"
|
||||||
case .production:
|
case .production:
|
||||||
return "production"
|
return "production"
|
||||||
}
|
}
|
||||||
@@ -273,6 +281,8 @@ extension PushNotificationClient {
|
|||||||
}
|
}
|
||||||
case .production:
|
case .production:
|
||||||
return "production"
|
return "production"
|
||||||
|
case .staging:
|
||||||
|
return "staging"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,13 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
|||||||
var model: Model {
|
var model: Model {
|
||||||
switch self {
|
switch self {
|
||||||
case .none:
|
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:
|
case .purple:
|
||||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("Damus Purple", comment: "Dropdown option for selecting Damus Purple as a translation service."))
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Damus Purple", comment: "Dropdown option for selecting Damus Purple as a translation service."))
|
||||||
case .libretranslate:
|
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."))
|
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)
|
@Setting(key: "like_notification", default_value: true)
|
||||||
var like_notification: Bool
|
var like_notification: Bool
|
||||||
|
|
||||||
@StringSetting(key: "notifications_mode", default_value: .local)
|
@StringSetting(key: "notification_mode", default_value: .push)
|
||||||
var notifications_mode: NotificationsMode
|
var notification_mode: NotificationsMode
|
||||||
|
|
||||||
@Setting(key: "notification_only_from_following", default_value: false)
|
@Setting(key: "notification_only_from_following", default_value: false)
|
||||||
var notification_only_from_following: Bool
|
var notification_only_from_following: Bool
|
||||||
@@ -207,11 +207,12 @@ class UserSettingsStore: ObservableObject {
|
|||||||
@Setting(key: "always_show_onboarding_suggestions", default_value: false)
|
@Setting(key: "always_show_onboarding_suggestions", default_value: false)
|
||||||
var always_show_onboarding_suggestions: Bool
|
var always_show_onboarding_suggestions: Bool
|
||||||
|
|
||||||
@Setting(key: "enable_experimental_push_notifications", default_value: false)
|
// @Setting(key: "enable_experimental_push_notifications", default_value: false)
|
||||||
var enable_experimental_push_notifications: Bool
|
// 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)
|
@StringSetting(key: "push_notification_environment", default_value: .production)
|
||||||
var send_device_token_to_localhost: Bool
|
var push_notification_environment: PushNotificationClient.Environment
|
||||||
|
|
||||||
@Setting(key: "enable_experimental_purple_api", default_value: false)
|
@Setting(key: "enable_experimental_purple_api", default_value: false)
|
||||||
var enable_experimental_purple_api: Bool
|
var enable_experimental_purple_api: Bool
|
||||||
|
|||||||
@@ -122,20 +122,22 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case hashtag(Hashtag)
|
case hashtag(Hashtag)
|
||||||
case param(TagElem)
|
case param(TagElem)
|
||||||
case naddr(NAddr)
|
case naddr(NAddr)
|
||||||
|
case reference(String)
|
||||||
|
|
||||||
var key: RefKey {
|
var key: RefKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .event: return .e
|
case .event: return .e
|
||||||
case .pubkey: return .p
|
case .pubkey: return .p
|
||||||
case .quote: return .q
|
case .quote: return .q
|
||||||
case .hashtag: return .t
|
case .hashtag: return .t
|
||||||
case .param: return .d
|
case .param: return .d
|
||||||
case .naddr: return .a
|
case .naddr: return .a
|
||||||
|
case .reference: return .r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
|
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
|
||||||
case e, p, t, d, q, a
|
case e, p, t, d, q, a, r
|
||||||
|
|
||||||
var keychar: AsciiCharacter {
|
var keychar: AsciiCharacter {
|
||||||
self.rawValue
|
self.rawValue
|
||||||
@@ -159,6 +161,8 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
|||||||
case .param(let string): return string.string()
|
case .param(let string): return string.string()
|
||||||
case .naddr(let naddr):
|
case .naddr(let naddr):
|
||||||
return naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier
|
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 .t: return .hashtag(Hashtag(hashtag: t1.string()))
|
||||||
case .d: return .param(t1)
|
case .d: return .param(t1)
|
||||||
case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0))
|
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 {
|
if err == nil {
|
||||||
self.last_pong = .now
|
self.last_pong = .now
|
||||||
|
Log.info("Got pong from '%s'", for: .networking, self.relay_url.absoluteString)
|
||||||
self.log?.add("Successful ping")
|
self.log?.add("Successful ping")
|
||||||
} else {
|
} 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.isConnected = false
|
||||||
self.isConnecting = false
|
self.isConnecting = false
|
||||||
self.reconnect_with_backoff()
|
self.reconnect_with_backoff()
|
||||||
@@ -126,7 +127,7 @@ final class RelayConnection: ObservableObject {
|
|||||||
self.receive(message: message)
|
self.receive(message: message)
|
||||||
case .disconnected(let closeCode, let reason):
|
case .disconnected(let closeCode, let reason):
|
||||||
if closeCode != .normalClosure {
|
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 {
|
DispatchQueue.main.async {
|
||||||
self.isConnected = false
|
self.isConnected = false
|
||||||
@@ -134,12 +135,16 @@ final class RelayConnection: ObservableObject {
|
|||||||
self.reconnect()
|
self.reconnect()
|
||||||
}
|
}
|
||||||
case .error(let error):
|
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
|
let nserr = error as NSError
|
||||||
if nserr.domain == NSPOSIXErrorDomain && nserr.code == 57 {
|
if nserr.domain == NSPOSIXErrorDomain && nserr.code == 57 {
|
||||||
// ignore socket not connected?
|
// ignore socket not connected?
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if nserr.domain == NSURLErrorDomain && nserr.code == -999 {
|
||||||
|
// these aren't real error, it just means task was cancelled
|
||||||
|
return
|
||||||
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.isConnected = false
|
self.isConnected = false
|
||||||
self.isConnecting = false
|
self.isConnecting = false
|
||||||
@@ -156,14 +161,21 @@ final class RelayConnection: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reconnect_with_backoff() {
|
func reconnect_with_backoff() {
|
||||||
self.backoff *= 1.5
|
self.backoff *= 2.0
|
||||||
self.reconnect_in(after: self.backoff)
|
self.reconnect_in(after: self.backoff)
|
||||||
}
|
}
|
||||||
|
|
||||||
func reconnect() {
|
func reconnect() {
|
||||||
guard !isConnecting && !isDisabled else {
|
guard !isConnecting && !isDisabled else {
|
||||||
|
self.log?.add("Cancelling reconnect, already connecting")
|
||||||
return // we're already trying to connect or we're disabled
|
return // we're already trying to connect or we're disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard !self.isConnected else {
|
||||||
|
self.log?.add("Cancelling reconnect, already connected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
disconnect()
|
disconnect()
|
||||||
connect()
|
connect()
|
||||||
log?.add("Reconnecting...")
|
log?.add("Reconnecting...")
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ping() {
|
func ping() {
|
||||||
|
Log.info("Pinging %d relays", for: .networking, relays.count)
|
||||||
for relay in relays {
|
for relay in relays {
|
||||||
relay.connection.ping()
|
relay.connection.ping()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ class Constants {
|
|||||||
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
|
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
|
||||||
|
|
||||||
// MARK: Push notification server
|
// 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")!
|
static let PUSH_NOTIFICATION_SERVER_TEST_BASE_URL: URL = URL(string: "http://localhost:8000")!
|
||||||
|
|
||||||
// MARK: Purple
|
// 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 {
|
func should_translate(event: NostrEvent, our_keypair: Keypair, note_lang: String?) -> Bool {
|
||||||
guard settings.can_translate else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't translate reposts, longform, etc
|
// don't translate reposts, longform, etc
|
||||||
if event.kind != 1 {
|
if event.kind != 1 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not translate self-authored notes if logged in with a private key
|
// Do not translate self-authored notes if logged in with a private key
|
||||||
// as we can assume the user can understand their own notes.
|
// 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.
|
// 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 {
|
if our_keypair.privkey != nil && our_keypair.pubkey == event.pubkey {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if let note_lang {
|
if let note_lang {
|
||||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||||
|
|
||||||
// Don't translate if its in our preferred languages
|
// Don't translate if its in our preferred languages
|
||||||
guard !preferredLanguages.contains(note_lang) else {
|
guard !preferredLanguages.contains(note_lang) else {
|
||||||
// if its the same, give up and don't retry
|
// if its the same, give up and don't retry
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we should start translating if we have auto_translate on
|
// we should start translating if we have auto_translate on
|
||||||
return true
|
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 {
|
func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current_status: TranslateStatus, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||||
switch current_status {
|
switch current_status {
|
||||||
case .havent_tried:
|
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 .translating: return false
|
||||||
case .translated: return false
|
case .translated: return false
|
||||||
case .not_needed: return false
|
case .not_needed: return false
|
||||||
@@ -413,7 +417,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async {
|
|||||||
|
|
||||||
var translations: TranslateStatus? = nil
|
var translations: TranslateStatus? = nil
|
||||||
// We have to recheck should_translate here now that we have note_language
|
// 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)
|
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() {
|
public func end_editing() {
|
||||||
UIApplication.shared.connectedScenes
|
this_app.connectedScenes
|
||||||
.filter {$0.activationState == .foregroundActive}
|
.filter {$0.activationState == .foregroundActive}
|
||||||
.map {$0 as? UIWindowScene}
|
.map {$0 as? UIWindowScene}
|
||||||
.compactMap({$0})
|
.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 {
|
struct LocalNotification {
|
||||||
let type: LocalNotificationType
|
let type: LocalNotificationType
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let target: NostrEvent
|
let target: NotificationTarget
|
||||||
let content: String
|
let content: String
|
||||||
|
|
||||||
func to_lossy() -> LossyLocalNotification {
|
func to_lossy() -> LossyLocalNotification {
|
||||||
@@ -63,6 +77,8 @@ enum LocalNotificationType: String {
|
|||||||
case dm
|
case dm
|
||||||
case like
|
case like
|
||||||
case mention
|
case mention
|
||||||
|
case reply
|
||||||
|
case tagged
|
||||||
case repost
|
case repost
|
||||||
case zap
|
case zap
|
||||||
case profile_zap
|
case profile_zap
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ enum LogCategory: String {
|
|||||||
case nav
|
case nav
|
||||||
case render
|
case render
|
||||||
case storage
|
case storage
|
||||||
|
case networking
|
||||||
case push_notifications
|
case push_notifications
|
||||||
case damus_purple
|
case damus_purple
|
||||||
case image_uploading
|
case image_uploading
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import UIKit
|
|||||||
class Theme {
|
class Theme {
|
||||||
|
|
||||||
static var safeAreaInsets: UIEdgeInsets? {
|
static var safeAreaInsets: UIEdgeInsets? {
|
||||||
return UIApplication
|
return this_app
|
||||||
.shared
|
|
||||||
.connectedScenes
|
.connectedScenes
|
||||||
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
|
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
|
||||||
.first { $0.isKeyWindow }?.safeAreaInsets
|
.first { $0.isKeyWindow }?.safeAreaInsets
|
||||||
|
|||||||
+6
-19
@@ -309,14 +309,10 @@ struct Zap {
|
|||||||
return nil
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let zap_req = decode_nostr_event_json(desc) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard validate_event(ev: zap_req) == .ok else {
|
guard validate_event(ev: zap_req) == .ok else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -399,21 +395,12 @@ func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the description from either the invoice, or tags, depending on the type of invoice
|
func get_zap_request(_ ev: NostrEvent) -> NostrEvent? {
|
||||||
func get_zap_description(_ ev: NostrEvent, inv_desc: InvoiceDescription) -> String? {
|
guard let desc = event_tag(ev, name: "description") else {
|
||||||
switch inv_desc {
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return decode_nostr_event_json(desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func invoice_to_zap_invoice(_ invoice: Invoice) -> ZapInvoice? {
|
func invoice_to_zap_invoice(_ invoice: Invoice) -> ZapInvoice? {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ struct AddRelayView: View {
|
|||||||
}
|
}
|
||||||
new_relay = ""
|
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()
|
dismiss()
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
|
|||||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
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 uploading to a media host that support NIP-98 authorization, add the header
|
||||||
if mediaUploader == .nostrBuild,
|
if mediaUploader == .nostrBuild || mediaUploader == .nostrcheck,
|
||||||
let keypair,
|
let keypair,
|
||||||
let method = request.httpMethod,
|
let method = request.httpMethod,
|
||||||
let signature = create_nip98_signature(keypair: keypair, method: method, url: url) {
|
let signature = create_nip98_signature(keypair: keypair, method: method, url: url) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ struct EditBannerImageView: View {
|
|||||||
Color(uiColor: .secondarySystemBackground)
|
Color(uiColor: .secondarySystemBackground)
|
||||||
}
|
}
|
||||||
.onFailureImage(defaultImage)
|
.onFailureImage(defaultImage)
|
||||||
|
.kfClickable()
|
||||||
|
|
||||||
EditPictureControl(uploader: damus_state.settings.default_media_uploader, pubkey: damus_state.pubkey, image_url: $banner_image, uploadObserver: viewModel, callback: callback)
|
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)
|
Color(uiColor: .secondarySystemBackground)
|
||||||
}
|
}
|
||||||
.onFailureImage(defaultImage)
|
.onFailureImage(defaultImage)
|
||||||
|
.kfClickable()
|
||||||
} else {
|
} else {
|
||||||
Image(uiImage: defaultImage).resizable()
|
Image(uiImage: defaultImage).resizable()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ struct FriendsButton: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
switch self.filter {
|
switch self.filter {
|
||||||
case .all:
|
case .all:
|
||||||
self.filter = .friends
|
self.filter = .friends_of_friends
|
||||||
case .friends:
|
case .friends_of_friends:
|
||||||
self.filter = .all
|
self.filter = .all
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
if filter == .friends {
|
if filter == .friends_of_friends {
|
||||||
LINEAR_GRADIENT
|
LINEAR_GRADIENT
|
||||||
.mask(Image("user-added")
|
.mask(Image("user-added")
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -28,7 +28,7 @@ struct FriendsButton: View {
|
|||||||
Image("user-added")
|
Image("user-added")
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 28, height: 28)
|
.frame(width: 28, height: 28)
|
||||||
.foregroundColor(DamusColors.adaptableGrey)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ struct DirectMessagesView: View {
|
|||||||
|
|
||||||
func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool {
|
func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool {
|
||||||
for dm in dms {
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
|
|||||||
return true
|
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
|
func format_relative_time(_ created_at: UInt32) -> String
|
||||||
{
|
{
|
||||||
return time_ago_since(Date(timeIntervalSince1970: Double(created_at)))
|
return time_ago_since(Date(timeIntervalSince1970: Double(created_at)))
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ struct ReplyPart: View {
|
|||||||
Group {
|
Group {
|
||||||
if event.known_kind == .highlight {
|
if event.known_kind == .highlight {
|
||||||
let highlighted_note = event.highlighted_note_id().flatMap { events.lookup($0) }
|
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 {
|
} else if let reply_ref = event.thread_reply()?.reply {
|
||||||
let replying_to = events.lookup(reply_ref.note_id)
|
let replying_to = events.lookup(reply_ref.note_id)
|
||||||
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
|
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import SwiftUI
|
|||||||
|
|
||||||
// Modified from Reply Description
|
// Modified from Reply Description
|
||||||
struct HighlightDescription: View {
|
struct HighlightDescription: View {
|
||||||
let event: NostrEvent
|
let highlight_event: HighlightEvent
|
||||||
let highlighted_event: NostrEvent?
|
let highlighted_event: NostrEvent?
|
||||||
let ndb: Ndb
|
let ndb: Ndb
|
||||||
|
|
||||||
var body: some View {
|
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)
|
.font(.footnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@@ -24,30 +24,6 @@ struct HighlightDescription: View {
|
|||||||
|
|
||||||
struct HighlightDescription_Previews: PreviewProvider {
|
struct HighlightDescription_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
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()
|
FailedImage()
|
||||||
}
|
}
|
||||||
.frame(width: 35, height: 35)
|
.frame(width: 35, height: 35)
|
||||||
|
.kfClickable()
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
|
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ struct HighlightLink: View {
|
|||||||
.background(DamusColors.adaptableWhite)
|
.background(DamusColors.adaptableWhite)
|
||||||
}
|
}
|
||||||
.frame(width: 35, height: 35)
|
.frame(width: 35, height: 35)
|
||||||
|
.kfClickable()
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
} else {
|
} 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 {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if options.contains(.wide) {
|
if options.contains(.wide) {
|
||||||
Main.padding(.horizontal)
|
|
||||||
} else {
|
|
||||||
Main
|
Main
|
||||||
|
} else {
|
||||||
|
Main.padding(.horizontal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +92,18 @@ struct HighlightBodyView: View {
|
|||||||
|
|
||||||
var Main: some View {
|
var Main: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
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 {
|
HStack {
|
||||||
var attributedString: AttributedString {
|
var attributedString: AttributedString {
|
||||||
var attributedString: AttributedString = ""
|
var attributedString: AttributedString = ""
|
||||||
@@ -119,14 +131,17 @@ struct HighlightBodyView: View {
|
|||||||
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
||||||
alignment: .leading
|
alignment: .leading
|
||||||
)
|
)
|
||||||
|
.padding(.horizontal)
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
|
|
||||||
if let url = event.url_ref {
|
if let url = event.url_ref {
|
||||||
HighlightLink(state: state, url: url, content: event.event.content)
|
HighlightLink(state: state, url: url, content: event.event.content)
|
||||||
|
.padding(.horizontal)
|
||||||
} else {
|
} else {
|
||||||
if let evRef = event.event_ref {
|
if let evRef = event.event_ref {
|
||||||
if let eventHex = hex_decode_id(evRef) {
|
if let eventHex = hex_decode_id(evRef) {
|
||||||
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
|
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
|
||||||
|
.padding(.horizontal)
|
||||||
.padding(.top, 5)
|
.padding(.top, 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ struct LongformPreviewBody: View {
|
|||||||
}
|
}
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150)
|
.frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150)
|
||||||
|
.kfClickable()
|
||||||
.cornerRadius(1)
|
.cornerRadius(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ struct ImageContainerView: View {
|
|||||||
view.framePreloadCount = 3
|
view.framePreloadCount = 3
|
||||||
}
|
}
|
||||||
.imageModifier(ImageHandler(handler: $image))
|
.imageModifier(ImageHandler(handler: $image))
|
||||||
|
.kfClickable()
|
||||||
.clipped()
|
.clipped()
|
||||||
.modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet))
|
.modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet))
|
||||||
.sheet(isPresented: $showShareSheet) {
|
.sheet(isPresented: $showShareSheet) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ struct ProfileImageContainerView: View {
|
|||||||
.imageModifier(ImageHandler(handler: $image))
|
.imageModifier(ImageHandler(handler: $image))
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet))
|
.modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet))
|
||||||
|
.kfClickable()
|
||||||
.sheet(isPresented: $showShareSheet) {
|
.sheet(isPresented: $showShareSheet) {
|
||||||
ShareSheet(activityItems: [url])
|
ShareSheet(activityItems: [url])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ struct AddMuteItemView: View {
|
|||||||
|
|
||||||
new_text = ""
|
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()
|
dismiss()
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SwiftUI
|
|||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
import NaturalLanguage
|
import NaturalLanguage
|
||||||
import MarkdownUI
|
import MarkdownUI
|
||||||
|
import Translation
|
||||||
|
|
||||||
struct Blur: UIViewRepresentable {
|
struct Blur: UIViewRepresentable {
|
||||||
var style: UIBlurEffect.Style = .systemUltraThinMaterial
|
var style: UIBlurEffect.Style = .systemUltraThinMaterial
|
||||||
@@ -32,6 +33,8 @@ struct NoteContentView: View {
|
|||||||
let preview_height: CGFloat?
|
let preview_height: CGFloat?
|
||||||
let options: EventViewOptions
|
let options: EventViewOptions
|
||||||
|
|
||||||
|
@State var isAppleTranslationPopoverPresented: Bool = false
|
||||||
|
|
||||||
@ObservedObject var artifacts_model: NoteArtifactsModel
|
@ObservedObject var artifacts_model: NoteArtifactsModel
|
||||||
@ObservedObject var preview_model: PreviewModel
|
@ObservedObject var preview_model: PreviewModel
|
||||||
@ObservedObject var settings: UserSettingsStore
|
@ObservedObject var settings: UserSettingsStore
|
||||||
@@ -96,7 +99,7 @@ struct NoteContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var translateView: some 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 {
|
func previewView(links: [URL]) -> some View {
|
||||||
@@ -120,8 +123,7 @@ struct NoteContentView: View {
|
|||||||
EventView(damus: damus_state, event: self.event, options: .embedded_text_only)
|
EventView(damus: damus_state, event: self.event, options: .embedded_text_only)
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
}
|
}
|
||||||
.background(.thinMaterial)
|
.background(.thickMaterial)
|
||||||
.preferredColorScheme(.dark)
|
|
||||||
.onTapGesture(perform: {
|
.onTapGesture(perform: {
|
||||||
damus_state.nav.push(route: Route.Thread(thread: .init(event: self.event, damus_state: damus_state)))
|
damus_state.nav.push(route: Route.Thread(thread: .init(event: self.event, damus_state: damus_state)))
|
||||||
dismiss()
|
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 {
|
if with_padding {
|
||||||
translateView
|
translateView
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
@@ -299,7 +301,12 @@ struct NoteContentView: View {
|
|||||||
Markdown(md.markdown)
|
Markdown(md.markdown)
|
||||||
.padding([.leading, .trailing, .top])
|
.padding([.leading, .trailing, .top])
|
||||||
case .separated(let separated):
|
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)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ struct DamusAppNotificationView: View {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func open_url(url: URL) {
|
func open_url(url: URL) {
|
||||||
UIApplication.shared.open(url)
|
this_app.open(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ struct NotificationsView: View {
|
|||||||
@ObservedObject var notifications: NotificationsModel
|
@ObservedObject var notifications: NotificationsModel
|
||||||
@StateObject var filter = NotificationFilter()
|
@StateObject var filter = NotificationFilter()
|
||||||
@SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all
|
@SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all
|
||||||
|
@Binding var subtitle: String?
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
@@ -99,6 +100,15 @@ struct NotificationsView: View {
|
|||||||
.tag(NotificationFilterState.replies)
|
.tag(NotificationFilterState.replies)
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button(
|
||||||
|
action: { state.nav.push(route: Route.NotificationSettings(settings: state.settings)) },
|
||||||
|
label: {
|
||||||
|
Image("settings")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
|
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
|
||||||
FriendsButton(filter: $filter.fine_filter)
|
FriendsButton(filter: $filter.fine_filter)
|
||||||
@@ -107,12 +117,14 @@ struct NotificationsView: View {
|
|||||||
}
|
}
|
||||||
.onChange(of: filter.fine_filter) { val in
|
.onChange(of: filter.fine_filter) { val in
|
||||||
state.settings.friend_filter = val
|
state.settings.friend_filter = val
|
||||||
|
self.subtitle = filter.fine_filter.description()
|
||||||
}
|
}
|
||||||
.onChange(of: filter_state) { val in
|
.onChange(of: filter_state) { val in
|
||||||
filter.state = val
|
filter.state = val
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
self.filter.fine_filter = state.settings.friend_filter
|
self.filter.fine_filter = state.settings.friend_filter
|
||||||
|
self.subtitle = filter.fine_filter.description()
|
||||||
filter.state = filter_state
|
filter.state = filter_state
|
||||||
}
|
}
|
||||||
.safeAreaInset(edge: .top, spacing: 0) {
|
.safeAreaInset(edge: .top, spacing: 0) {
|
||||||
@@ -163,7 +175,7 @@ struct NotificationsView: View {
|
|||||||
|
|
||||||
struct NotificationsView_Previews: PreviewProvider {
|
struct NotificationsView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
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
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-30
@@ -30,15 +30,18 @@ enum PostAction {
|
|||||||
case replying_to(NostrEvent)
|
case replying_to(NostrEvent)
|
||||||
case quoting(NostrEvent)
|
case quoting(NostrEvent)
|
||||||
case posting(PostTarget)
|
case posting(PostTarget)
|
||||||
|
case highlighting(HighlightContentDraft)
|
||||||
|
|
||||||
var ev: NostrEvent? {
|
var ev: NostrEvent? {
|
||||||
switch self {
|
switch self {
|
||||||
case .replying_to(let ev):
|
case .replying_to(let ev):
|
||||||
return ev
|
return ev
|
||||||
case .quoting(let ev):
|
case .quoting(let ev):
|
||||||
return ev
|
return ev
|
||||||
case .posting:
|
case .posting:
|
||||||
return nil
|
return nil
|
||||||
|
case .highlighting:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +131,12 @@ struct PostView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var posting_disabled: Bool {
|
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
|
// 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)
|
damus_state.drafts.quotes.removeValue(forKey: quoting)
|
||||||
case .posting:
|
case .posting:
|
||||||
damus_state.drafts.post = nil
|
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 {
|
if case .quoting(let ev) = action {
|
||||||
BuilderEventView(damus: damus_state, event: ev)
|
BuilderEventView(damus: damus_state, event: ev)
|
||||||
}
|
}
|
||||||
|
else if case .highlighting(let draft) = action {
|
||||||
|
HighlightDraftContentView(draft: draft)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
@@ -454,14 +467,15 @@ struct PostView: View {
|
|||||||
let loaded_draft = load_draft()
|
let loaded_draft = load_draft()
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .replying_to(let replying_to):
|
case .replying_to(let replying_to):
|
||||||
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
|
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
|
||||||
case .quoting(let quoting):
|
case .quoting(let quoting):
|
||||||
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
|
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
|
||||||
case .posting(let target):
|
case .posting(let target):
|
||||||
guard !loaded_draft else { break }
|
guard !loaded_draft else { break }
|
||||||
|
fill_target_content(target: target)
|
||||||
fill_target_content(target: target)
|
case .highlighting(let draft):
|
||||||
|
references = [draft.source.ref()]
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
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
|
drafts.quotes[ev] = artifacts
|
||||||
case .posting:
|
case .posting:
|
||||||
drafts.post = artifacts
|
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]
|
return drafts.quotes[ev]
|
||||||
case .posting:
|
case .posting:
|
||||||
return drafts.post
|
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]] = []
|
var tags: [[String]] = []
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .replying_to(let replying_to):
|
case .replying_to(let replying_to):
|
||||||
// start off with the reply tags
|
// start off with the reply tags
|
||||||
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
||||||
|
|
||||||
case .quoting(let ev):
|
case .quoting(let ev):
|
||||||
content.append(" nostr:" + bech32_note_id(ev.id))
|
content.append(" nostr:" + bech32_note_id(ev.id))
|
||||||
|
|
||||||
if let quoted_ev = state.events.lookup(ev.id) {
|
if let quoted_ev = state.events.lookup(ev.id) {
|
||||||
tags.append(["p", quoted_ev.pubkey.hex()])
|
tags.append(["p", quoted_ev.pubkey.hex()])
|
||||||
}
|
}
|
||||||
case .posting(let postTarget):
|
case .posting(let postTarget):
|
||||||
break
|
break
|
||||||
}
|
case .highlighting(let draft):
|
||||||
|
break
|
||||||
// include pubkeys
|
|
||||||
tags += pubkeys.map { pk in
|
|
||||||
["p", pk.hex()]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// append additional tags
|
// append additional tags
|
||||||
tags += uploadedMedias.compactMap { $0.metadata?.to_tag() }
|
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)
|
return NostrPost(content: content, kind: .text, tags: tags)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ struct EditPictureControl: View {
|
|||||||
}
|
}
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: (size ?? 25) + 10, height: (size ?? 25) + 10)
|
.frame(width: (size ?? 25) + 10, height: (size ?? 25) + 10)
|
||||||
|
.kfClickable()
|
||||||
.foregroundColor(DamusColors.white)
|
.foregroundColor(DamusColors.white)
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.overlay(Circle().stroke(.white, lineWidth: 4))
|
.overlay(Circle().stroke(.white, lineWidth: 4))
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ struct InnerProfilePicView: View {
|
|||||||
}
|
}
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
|
.kfClickable()
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ struct EditProfilePictureView: View {
|
|||||||
view.framePreloadCount = 3
|
view.framePreloadCount = 3
|
||||||
}
|
}
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
|
.kfClickable()
|
||||||
|
|
||||||
EditPictureControl(uploader: damus_state?.settings.default_media_uploader ?? .nostrBuild, pubkey: pubkey, image_url: $profile_url, uploadObserver: uploadObserver, callback: callback)
|
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)
|
.padding(.bottom, 20)
|
||||||
}
|
}
|
||||||
.foregroundColor(.white.opacity(0.8))
|
.foregroundColor(.white.opacity(0.8))
|
||||||
.preferredColorScheme(.dark)
|
|
||||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
@@ -81,7 +80,8 @@ struct DamusPurpleAccountView: View {
|
|||||||
SupporterBadge(
|
SupporterBadge(
|
||||||
percent: nil,
|
percent: nil,
|
||||||
purple_account: account,
|
purple_account: account,
|
||||||
style: .full
|
style: .full,
|
||||||
|
text_color: .white
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ struct InnerRelayPicView: View {
|
|||||||
Placeholder(url: url)
|
Placeholder(url: url)
|
||||||
}
|
}
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
|
.kfClickable()
|
||||||
} else {
|
} else {
|
||||||
FailedRelayImage(url: nil)
|
FailedRelayImage(url: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,41 @@ struct DeveloperSettingsView: View {
|
|||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
if settings.developer_mode {
|
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("Always show onboarding", comment: "Developer mode setting to always show onboarding suggestions."), isOn: $settings.always_show_onboarding_suggestions)
|
||||||
|
Picker(NSLocalizedString("Push notification environment", comment: "Prompt selection of the Push notification environment (Developer feature to switch between real/production mode to test modes)."),
|
||||||
Toggle(NSLocalizedString("Enable experimental push notifications", comment: "Developer mode setting to enable experimental push notifications."), isOn: $settings.enable_experimental_push_notifications)
|
selection: Binding(
|
||||||
.toggleStyle(.switch)
|
get: { () -> PushNotificationClient.Environment in
|
||||||
|
switch settings.push_notification_environment {
|
||||||
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)
|
case .local_test(_):
|
||||||
.toggleStyle(.switch)
|
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)
|
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)
|
.toggleStyle(.switch)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ struct NotificationSettingsView: View {
|
|||||||
do {
|
do {
|
||||||
try await damus_state.push_notification_client.send_token()
|
try await damus_state.push_notification_client.send_token()
|
||||||
await self.sync_up_remote_notification_settings()
|
await self.sync_up_remote_notification_settings()
|
||||||
settings.notifications_mode = new_value
|
settings.notification_mode = new_value
|
||||||
}
|
}
|
||||||
catch {
|
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)
|
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 {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await damus_state.push_notification_client.revoke_token()
|
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
|
notification_preferences_sync_state = .not_applicable
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -67,7 +67,7 @@ struct NotificationSettingsView: View {
|
|||||||
set: { new_value in
|
set: { new_value in
|
||||||
let old_value = raw_binding.wrappedValue
|
let old_value = raw_binding.wrappedValue
|
||||||
raw_binding.wrappedValue = new_value
|
raw_binding.wrappedValue = new_value
|
||||||
if self.settings.notifications_mode == .push {
|
if self.settings.notification_mode == .push {
|
||||||
Task {
|
Task {
|
||||||
await self.send_push_notification_preferences(on_failure: {
|
await self.send_push_notification_preferences(on_failure: {
|
||||||
raw_binding.wrappedValue = old_value
|
raw_binding.wrappedValue = old_value
|
||||||
@@ -114,7 +114,7 @@ struct NotificationSettingsView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
if settings.enable_experimental_push_notifications {
|
if settings.enable_push_notifications {
|
||||||
Section(
|
Section(
|
||||||
header: Text("General", comment: "Section header for general damus notifications user configuration"),
|
header: Text("General", comment: "Section header for general damus notifications user configuration"),
|
||||||
footer: VStack {
|
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)."),
|
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(
|
selection: Binding(
|
||||||
get: { settings.notifications_mode },
|
get: { settings.notification_mode },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
self.try_to_set_notifications_mode(new_value: newValue)
|
self.try_to_set_notifications_mode(new_value: newValue)
|
||||||
}
|
}
|
||||||
@@ -194,7 +194,7 @@ struct NotificationSettingsView: View {
|
|||||||
}
|
}
|
||||||
.onAppear(perform: {
|
.onAppear(perform: {
|
||||||
Task {
|
Task {
|
||||||
if self.settings.notifications_mode == .push {
|
if self.settings.notification_mode == .push {
|
||||||
await self.sync_up_remote_notification_settings()
|
await self.sync_up_remote_notification_settings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ struct TextViewWrapper: UIViewRepresentable {
|
|||||||
let updateCursorPosition: ((Int) -> Void)
|
let updateCursorPosition: ((Int) -> Void)
|
||||||
let initialTextSuffix: String?
|
let initialTextSuffix: String?
|
||||||
var initialTextSuffixWasAdded: Bool = false
|
var initialTextSuffixWasAdded: Bool = false
|
||||||
|
static let ESCAPE_SEQUENCES = ["\n", "@", " ", ", ", ". ", "! ", "? ", "; "]
|
||||||
|
|
||||||
init(attributedText: Binding<NSMutableAttributedString>,
|
init(attributedText: Binding<NSMutableAttributedString>,
|
||||||
getFocusWordForMention: ((String?, NSRange?) -> Void)?,
|
getFocusWordForMention: ((String?, NSRange?) -> Void)?,
|
||||||
@@ -142,17 +143,27 @@ struct TextViewWrapper: UIViewRepresentable {
|
|||||||
|
|
||||||
while startPosition != textView.beginningOfDocument {
|
while startPosition != textView.beginningOfDocument {
|
||||||
guard let previousPosition = textView.position(from: startPosition, offset: -1),
|
guard let previousPosition = textView.position(from: startPosition, offset: -1),
|
||||||
let range = textView.textRange(from: previousPosition, to: startPosition),
|
let range = textView.textRange(from: previousPosition, to: position),
|
||||||
let text = textView.text(in: range), !text.isEmpty,
|
let text = textView.text(in: range), !text.isEmpty else {
|
||||||
let lastChar = text.last else {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if [" ", "\n", "@"].contains(lastChar) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
startPosition = previousPosition
|
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)
|
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 {
|
extension View {
|
||||||
func hideKeyboard() {
|
func hideKeyboard() {
|
||||||
let resign = #selector(UIResponder.resignFirstResponder)
|
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 ev = NostrEvent(content: content, keypair: test_keypair, tags: [])!
|
||||||
let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
|
let blocks = parse_note_content(content: .init(note: ev, keypair: test_keypair)).blocks
|
||||||
let post_blocks = parse_post_blocks(content: content)
|
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)
|
let tr = interpret_event_refs(tags: ev.tags)
|
||||||
|
|
||||||
XCTAssertNil(tr)
|
XCTAssertNil(tr)
|
||||||
@@ -240,7 +241,7 @@ class ReplyTests: XCTestCase {
|
|||||||
let content = "this is a @\(pk.npub) mention"
|
let content = "this is a @\(pk.npub) mention"
|
||||||
let blocks = parse_post_blocks(content: content)
|
let blocks = parse_post_blocks(content: content)
|
||||||
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
|
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(ev.tags.count, 2)
|
||||||
XCTAssertEqual(blocks.count, 3)
|
XCTAssertEqual(blocks.count, 3)
|
||||||
@@ -255,7 +256,7 @@ class ReplyTests: XCTestCase {
|
|||||||
let content = "this is a @\(nsec) mention"
|
let content = "this is a @\(nsec) mention"
|
||||||
let blocks = parse_post_blocks(content: content)
|
let blocks = parse_post_blocks(content: content)
|
||||||
let post = NostrPost(content: content, tags: [["e", evid.hex()]])
|
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(ev.tags.count, 2)
|
||||||
XCTAssertEqual(blocks.count, 3)
|
XCTAssertEqual(blocks.count, 3)
|
||||||
@@ -275,7 +276,7 @@ class ReplyTests: XCTestCase {
|
|||||||
]
|
]
|
||||||
|
|
||||||
let post = NostrPost(content: "this is a (@\(pubkey.npub)) mention", tags: tags)
|
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.content, "this is a (nostr:\(pubkey.npub)) mention")
|
||||||
XCTAssertEqual(ev.tags[2][1].string(), pubkey.description)
|
XCTAssertEqual(ev.tags[2][1].string(), pubkey.description)
|
||||||
|
|||||||
@@ -16,7 +16,39 @@ final class ZapTests: XCTestCase {
|
|||||||
override func tearDownWithError() throws {
|
override func tearDownWithError() throws {
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
// 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 {
|
func test_private_zap() throws {
|
||||||
let alice = generate_new_keypair()
|
let alice = generate_new_keypair()
|
||||||
let bob = generate_new_keypair()
|
let bob = generate_new_keypair()
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class damusTests: XCTestCase {
|
|||||||
|
|
||||||
func testMakeHashtagPost() {
|
func testMakeHashtagPost() {
|
||||||
let post = NostrPost(content: "#damus some content #bitcoin derp #かっこいい wow", tags: [])
|
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.tags.count, 3)
|
||||||
XCTAssertEqual(ev.content, "#damus some content #bitcoin derp #かっこいい wow")
|
XCTAssertEqual(ev.content, "#damus some content #bitcoin derp #かっこいい wow")
|
||||||
@@ -270,7 +270,7 @@ class damusTests: XCTestCase {
|
|||||||
|
|
||||||
private func createEventFromContentString(_ content: String) -> NostrEvent {
|
private func createEventFromContentString(_ content: String) -> NostrEvent {
|
||||||
let post = NostrPost(content: content, tags: [])
|
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")
|
XCTFail("Could not create event")
|
||||||
return test_note
|
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
|
var ok = false
|
||||||
while !ok && mapsize > 1024 * 1024 * 700 {
|
while !ok && mapsize > 1024 * 1024 * 700 {
|
||||||
var cfg = ndb_config(flags: 0, ingester_threads: ingest_threads, mapsize: mapsize, filter_context: nil, ingest_filter: nil)
|
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 {
|
if !ok {
|
||||||
|
Log.error("ndb_init failed: %d, reducing mapsize from %d to %d", for: .storage, res, mapsize, mapsize / 2)
|
||||||
mapsize /= 2
|
mapsize /= 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,6 +335,10 @@ extension NdbNote {
|
|||||||
public var referenced_mute_items: References<MuteItem> {
|
public var referenced_mute_items: References<MuteItem> {
|
||||||
References<MuteItem>(tags: self.tags)
|
References<MuteItem>(tags: self.tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var referenced_comment_items: References<CommentItem> {
|
||||||
|
References<CommentItem>(tags: self.tags)
|
||||||
|
}
|
||||||
|
|
||||||
public var references: References<RefId> {
|
public var references: References<RefId> {
|
||||||
References<RefId>(tags: self.tags)
|
References<RefId>(tags: self.tags)
|
||||||
@@ -355,6 +359,9 @@ extension NdbNote {
|
|||||||
if known_kind == .dm {
|
if known_kind == .dm {
|
||||||
return decrypted(keypair: keypair) ?? "*failed to decrypt content*"
|
return decrypted(keypair: keypair) ?? "*failed to decrypt content*"
|
||||||
}
|
}
|
||||||
|
else if known_kind == .highlight {
|
||||||
|
return self.referenced_comment_items.first?.content ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
return 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
|
#ifdef MDB_SHORT_SEMNAMES
|
||||||
encbuf[9] = '\0'; /* drop name from 15 chars to 14 chars */
|
encbuf[9] = '\0'; /* drop name from 15 chars to 14 chars */
|
||||||
#endif
|
#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
|
/* Clean up after a previous run, if needed: Try to
|
||||||
* remove both semaphores before doing anything else.
|
* remove both semaphores before doing anything else.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user