Compare commits
97 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 | |||
|
83ecc3142e
|
|||
| ef4afbc720 | |||
| a5cc3aec92 | |||
| 2b140d4279 | |||
| b43dcd2bc7 | |||
| c67a75d740 | |||
| 7f00ef5d9d | |||
| d663155941 | |||
| abfe0f642f | |||
| f0b5162205 | |||
| a9bb2ef98b | |||
| eff4525720 | |||
| 858d9dc6f0 | |||
| 55090bc102 | |||
| 40d3d273f0 | |||
| f9271da11c | |||
| 4f881a5667 | |||
| 9d97886e3f | |||
| e70cfbbe63 | |||
| 8a75537ea3 | |||
| 49c8d63d0b | |||
| 6480023c96 | |||
| 774da239b9 | |||
| 90c80645ec | |||
| 613ec23f7f | |||
| 1d73ae1d32 | |||
|
63e364ce5b
|
|||
|
ee5f53e4eb
|
|||
|
9de21a730a
|
|||
|
36c09c8657
|
|||
|
e8ac143192
|
|||
|
93f44939e3
|
|||
|
48078b9b6a
|
|||
|
d6d6858e0b
|
|||
|
0187ff1dc0
|
|||
|
4f9fef8515
|
|||
|
1ebadd42f0
|
|||
|
4fb4f3a2de
|
|||
|
f49169c03c
|
|||
| 740c10c9b2 | |||
| 653f9fbcbe | |||
| 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>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "damoose.jpeg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "tor.svg.png",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -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,8 +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 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
|
||||||
|
|
||||||
@@ -37,8 +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
|
||||||
selectedText: $selectedText,
|
self.selectedTextActionState = .show_highlight_post_view(highlighted_text: 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)
|
||||||
@@ -53,11 +56,29 @@ 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
|
||||||
|
self.selectedTextActionState = newValue ? .show_highlight_post_view(highlighted_text: self.selectedTextActionState.highlighted_text() ?? "") : .hide
|
||||||
|
})) {
|
||||||
|
if let event, case .show_highlight_post_view(let highlighted_text) = self.selectedTextActionState {
|
||||||
|
PostView(
|
||||||
|
action: .highlighting(.init(selected_text: highlighted_text, source: .event(event))),
|
||||||
|
damus_state: damus_state
|
||||||
|
)
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
|
.presentationDetents([.height(selectedTextHeight + 450), .medium, .large])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: Binding(get: {
|
||||||
|
return self.selectedTextActionState.should_show_mute_word_view()
|
||||||
|
}, set: { newValue in
|
||||||
|
self.selectedTextActionState = newValue ? .show_mute_word_view(highlighted_text: self.selectedTextActionState.highlighted_text() ?? "") : .hide
|
||||||
|
})) {
|
||||||
|
if case .show_mute_word_view(let highlighted_text) = selectedTextActionState {
|
||||||
|
AddMuteItemView(state: damus_state, new_text: .constant(highlighted_text))
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
.presentationDetents([.height(selectedTextHeight + 150), .medium, .large])
|
.presentationDetents([.height(300), .medium, .large])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: selectedTextHeight)
|
.frame(height: selectedTextHeight)
|
||||||
@@ -66,15 +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 selectedText: String
|
var muteWord: (String) -> Void
|
||||||
|
|
||||||
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: 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._selectedText = selectedText
|
self.muteWord = muteWord
|
||||||
super.init(frame: frame, textContainer: textContainer)
|
super.init(frame: frame, textContainer: textContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,18 +134,32 @@ fileprivate class TextView: UITextView {
|
|||||||
if action == #selector(highlightText(_:)) {
|
if action == #selector(highlightText(_:)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if action == #selector(muteText(_:)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
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?) {
|
||||||
|
guard let selectedText = self.getSelectedText() else { return }
|
||||||
|
self.muteWord(selectedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||||
|
|
||||||
let attributedString: AttributedString
|
let attributedString: AttributedString
|
||||||
let textColor: UIColor
|
let textColor: UIColor
|
||||||
@@ -105,12 +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 selectedText: String
|
let muteWord: (String) -> Void
|
||||||
@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, 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
|
||||||
@@ -123,7 +185,8 @@ fileprivate class TextView: UITextView {
|
|||||||
|
|
||||||
let menuController = UIMenuController.shared
|
let menuController = UIMenuController.shared
|
||||||
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
|
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
|
||||||
menuController.menuItems = self.enableHighlighting ? [highlightItem] : []
|
let muteItem = UIMenuItem(title: "Mute", action: #selector(view.muteText(_:)))
|
||||||
|
menuController.menuItems = self.enableHighlighting ? [highlightItem, muteItem] : []
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,35 +11,34 @@ struct PushNotificationClient {
|
|||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
let settings: UserSettingsStore
|
let settings: UserSettingsStore
|
||||||
private(set) var device_token: Data? = nil
|
private(set) var device_token: Data? = nil
|
||||||
|
var device_token_hex: String? {
|
||||||
|
guard let device_token else { return nil }
|
||||||
|
return device_token.map { String(format: "%02.2hhx", $0) }.joined()
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func send_token() async throws {
|
func send_token() async throws {
|
||||||
guard let device_token else { return }
|
|
||||||
// Send the device token and pubkey to the server
|
// Send the device token and pubkey to the server
|
||||||
let token = device_token.map { String(format: "%02.2hhx", $0) }.joined()
|
guard let token = device_token_hex else { return }
|
||||||
|
|
||||||
Log.info("Sending device token to server: %s", for: .push_notifications, token)
|
Log.info("Sending device token to server: %s", for: .push_notifications, token)
|
||||||
|
|
||||||
let pubkey = self.keypair.pubkey
|
|
||||||
|
|
||||||
// Send those as JSON to the server
|
|
||||||
let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
|
|
||||||
|
|
||||||
// create post request
|
// create post request
|
||||||
let url = self.settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL
|
let url = self.current_push_notification_environment().api_base_url()
|
||||||
let json_data = try JSONSerialization.data(withJSONObject: json)
|
.appendingPathComponent("user-info")
|
||||||
|
.appendingPathComponent(self.keypair.pubkey.hex())
|
||||||
|
.appendingPathComponent(token)
|
||||||
|
|
||||||
let (data, response) = try await make_nip98_authenticated_request(
|
let (data, response) = try await make_nip98_authenticated_request(
|
||||||
method: .post,
|
method: .put,
|
||||||
url: url,
|
url: url,
|
||||||
payload: json_data,
|
payload: nil,
|
||||||
payload_type: .json,
|
payload_type: .json,
|
||||||
auth_keypair: self.keypair
|
auth_keypair: self.keypair
|
||||||
)
|
)
|
||||||
@@ -58,26 +57,23 @@ struct PushNotificationClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func revoke_token() async throws {
|
func revoke_token() async throws {
|
||||||
guard let device_token else { return }
|
guard let token = device_token_hex else { return }
|
||||||
// Send the device token and pubkey to the server
|
|
||||||
let token = device_token.map { String(format: "%02.2hhx", $0) }.joined()
|
|
||||||
|
|
||||||
Log.info("Revoking device token from server: %s", for: .push_notifications, token)
|
Log.info("Revoking device token from server: %s", for: .push_notifications, token)
|
||||||
|
|
||||||
let pubkey = self.keypair.pubkey
|
let pubkey = self.keypair.pubkey
|
||||||
|
|
||||||
// Send those as JSON to the server
|
|
||||||
let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
|
|
||||||
|
|
||||||
// create post request
|
// create post request
|
||||||
let url = self.settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_REVOKER_TEST_URL : Constants.DEVICE_TOKEN_REVOKER_PRODUCTION_URL
|
let url = self.current_push_notification_environment().api_base_url()
|
||||||
let json_data = try JSONSerialization.data(withJSONObject: json)
|
.appendingPathComponent("user-info")
|
||||||
|
.appendingPathComponent(pubkey.hex())
|
||||||
|
.appendingPathComponent(token)
|
||||||
|
|
||||||
|
|
||||||
let (data, response) = try await make_nip98_authenticated_request(
|
let (data, response) = try await make_nip98_authenticated_request(
|
||||||
method: .post,
|
method: .delete,
|
||||||
url: url,
|
url: url,
|
||||||
payload: json_data,
|
payload: nil,
|
||||||
payload_type: .json,
|
payload_type: .json,
|
||||||
auth_keypair: self.keypair
|
auth_keypair: self.keypair
|
||||||
)
|
)
|
||||||
@@ -94,6 +90,78 @@ struct PushNotificationClient {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func set_settings(_ new_settings: NotificationSettings? = nil) async throws {
|
||||||
|
// Send the device token and pubkey to the server
|
||||||
|
guard let token = device_token_hex else { return }
|
||||||
|
|
||||||
|
Log.info("Sending notification preferences to the server", for: .push_notifications)
|
||||||
|
|
||||||
|
let url = self.current_push_notification_environment().api_base_url()
|
||||||
|
.appendingPathComponent("user-info")
|
||||||
|
.appendingPathComponent(self.keypair.pubkey.hex())
|
||||||
|
.appendingPathComponent(token)
|
||||||
|
.appendingPathComponent("preferences")
|
||||||
|
|
||||||
|
let json_payload = try JSONEncoder().encode(new_settings ?? NotificationSettings.from(settings: settings))
|
||||||
|
|
||||||
|
let (data, response) = try await make_nip98_authenticated_request(
|
||||||
|
method: .put,
|
||||||
|
url: url,
|
||||||
|
payload: json_payload,
|
||||||
|
payload_type: .json,
|
||||||
|
auth_keypair: self.keypair
|
||||||
|
)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
|
switch httpResponse.statusCode {
|
||||||
|
case 200:
|
||||||
|
Log.info("Sent notification settings to Damus push notification server successfully", for: .push_notifications)
|
||||||
|
default:
|
||||||
|
Log.error("Error in sending notification settings to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||||
|
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_settings() async throws -> NotificationSettings {
|
||||||
|
// Send the device token and pubkey to the server
|
||||||
|
guard let token = device_token_hex else {
|
||||||
|
throw ClientError.no_device_token
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = self.current_push_notification_environment().api_base_url()
|
||||||
|
.appendingPathComponent("user-info")
|
||||||
|
.appendingPathComponent(self.keypair.pubkey.hex())
|
||||||
|
.appendingPathComponent(token)
|
||||||
|
.appendingPathComponent("preferences")
|
||||||
|
|
||||||
|
let (data, response) = try await make_nip98_authenticated_request(
|
||||||
|
method: .get,
|
||||||
|
url: url,
|
||||||
|
payload: nil,
|
||||||
|
payload_type: .json,
|
||||||
|
auth_keypair: self.keypair
|
||||||
|
)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
|
switch httpResponse.statusCode {
|
||||||
|
case 200:
|
||||||
|
guard let notification_settings = NotificationSettings.from(json_data: data) else { throw ClientError.json_decoding_error }
|
||||||
|
return notification_settings
|
||||||
|
default:
|
||||||
|
Log.error("Error in getting notification settings to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||||
|
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw ClientError.could_not_process_response
|
||||||
|
}
|
||||||
|
|
||||||
|
func current_push_notification_environment() -> Environment {
|
||||||
|
return self.settings.push_notification_environment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Helper structures
|
// MARK: Helper structures
|
||||||
@@ -101,5 +169,121 @@ struct PushNotificationClient {
|
|||||||
extension PushNotificationClient {
|
extension PushNotificationClient {
|
||||||
enum ClientError: Error {
|
enum ClientError: Error {
|
||||||
case http_response_error(status_code: Int, response: Data)
|
case http_response_error(status_code: Int, response: Data)
|
||||||
|
case could_not_process_response
|
||||||
|
case no_device_token
|
||||||
|
case json_decoding_error
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationSettings: Codable, Equatable {
|
||||||
|
let zap_notifications_enabled: Bool
|
||||||
|
let mention_notifications_enabled: Bool
|
||||||
|
let repost_notifications_enabled: Bool
|
||||||
|
let reaction_notifications_enabled: Bool
|
||||||
|
let dm_notifications_enabled: Bool
|
||||||
|
let only_notifications_from_following_enabled: Bool
|
||||||
|
|
||||||
|
static func from(json_data: Data) -> Self? {
|
||||||
|
guard let decoded = try? JSONDecoder().decode(Self.self, from: json_data) else { return nil }
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
static func from(settings: UserSettingsStore) -> Self {
|
||||||
|
return NotificationSettings(
|
||||||
|
zap_notifications_enabled: settings.zap_notification,
|
||||||
|
mention_notifications_enabled: settings.mention_notification,
|
||||||
|
repost_notifications_enabled: settings.repost_notification,
|
||||||
|
reaction_notifications_enabled: settings.like_notification,
|
||||||
|
dm_notifications_enabled: settings.dm_notification,
|
||||||
|
only_notifications_from_following_enabled: settings.notification_only_from_following
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Environment: CaseIterable, Codable, Identifiable, StringCodable, Equatable, Hashable {
|
||||||
|
static var allCases: [Environment] = [.local_test(host: nil), .staging, .production]
|
||||||
|
|
||||||
|
case local_test(host: String?)
|
||||||
|
case staging
|
||||||
|
case production
|
||||||
|
|
||||||
|
func text_description() -> String {
|
||||||
|
switch self {
|
||||||
|
case .local_test:
|
||||||
|
return NSLocalizedString("Test (local)", comment: "Label indicating a local test environment for Push notification functionality (Developer feature)")
|
||||||
|
case .production:
|
||||||
|
return NSLocalizedString("Production", comment: "Label indicating the production environment for Push notification functionality")
|
||||||
|
case .staging:
|
||||||
|
return NSLocalizedString("Staging (for dev builds)", comment: "Label indicating the staging environment for Push notification functionality")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func api_base_url() -> URL {
|
||||||
|
switch self {
|
||||||
|
case .local_test(let host):
|
||||||
|
URL(string: "http://\(host ?? "localhost:8000")") ?? Constants.PUSH_NOTIFICATION_SERVER_TEST_BASE_URL
|
||||||
|
case .production:
|
||||||
|
Constants.PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL
|
||||||
|
case .staging:
|
||||||
|
Constants.PUSH_NOTIFICATION_SERVER_STAGING_BASE_URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func custom_host() -> String? {
|
||||||
|
switch self {
|
||||||
|
case .local_test(let host):
|
||||||
|
return host
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(from string: String) {
|
||||||
|
switch string {
|
||||||
|
case "local_test":
|
||||||
|
self = .local_test(host: nil)
|
||||||
|
case "production":
|
||||||
|
self = .production
|
||||||
|
case "staging":
|
||||||
|
self = .staging
|
||||||
|
default:
|
||||||
|
let components = string.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
|
||||||
|
if components.count == 2 && components[0] == "local_test" {
|
||||||
|
self = .local_test(host: String(components[1]))
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func to_string() -> String {
|
||||||
|
switch self {
|
||||||
|
case .local_test(let host):
|
||||||
|
if let host {
|
||||||
|
return "local_test:\(host)"
|
||||||
|
}
|
||||||
|
return "local_test"
|
||||||
|
case .staging:
|
||||||
|
return "staging"
|
||||||
|
case .production:
|
||||||
|
return "production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .local_test(let host):
|
||||||
|
if let host {
|
||||||
|
return "local_test:\(host)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "local_test"
|
||||||
|
}
|
||||||
|
case .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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import Foundation
|
|||||||
class Constants {
|
class Constants {
|
||||||
//static let EXAMPLE_DEMOS: DamusState = .empty
|
//static let EXAMPLE_DEMOS: DamusState = .empty
|
||||||
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
|
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
|
||||||
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info")!
|
|
||||||
static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")!
|
|
||||||
static let DEVICE_TOKEN_REVOKER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info/remove")!
|
|
||||||
static let DEVICE_TOKEN_REVOKER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info/remove")!
|
|
||||||
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"
|
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"
|
||||||
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
|
||||||
|
static let PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL: URL = URL(string: "https://notify.damus.io")!
|
||||||
|
static let PUSH_NOTIFICATION_SERVER_STAGING_BASE_URL: URL = URL(string: "https://notify-staging.damus.io")!
|
||||||
|
static let PUSH_NOTIFICATION_SERVER_TEST_BASE_URL: URL = URL(string: "http://localhost:8000")!
|
||||||
|
|
||||||
// MARK: Purple
|
// MARK: Purple
|
||||||
// API
|
// API
|
||||||
static let PURPLE_API_LOCAL_TEST_BASE_URL: URL = URL(string: "http://localhost:8989")!
|
static let PURPLE_API_LOCAL_TEST_BASE_URL: URL = URL(string: "http://localhost:8989")!
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ struct ImageMetadata: Equatable {
|
|||||||
|
|
||||||
func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? {
|
func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? {
|
||||||
let res = Task.detached(priority: .low) {
|
let res = Task.detached(priority: .low) {
|
||||||
let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0))
|
let default_size = CGSize(width: 100.0, height: 100.0)
|
||||||
|
let size = get_blurhash_size(img_size: size ?? default_size) ?? default_size
|
||||||
guard let img = UIImage.init(blurHash: blurhash, size: size) else {
|
guard let img = UIImage.init(blurHash: blurhash, size: size) else {
|
||||||
let noimg: UIImage? = nil
|
let noimg: UIImage? = nil
|
||||||
return noimg
|
return noimg
|
||||||
@@ -135,7 +136,8 @@ extension UIImage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_blurhash_size(img_size: CGSize) -> CGSize {
|
func get_blurhash_size(img_size: CGSize) -> CGSize? {
|
||||||
|
guard img_size.width > 0 && img_size.height > 0 else { return nil }
|
||||||
return CGSize(width: 100.0, height: (100.0/img_size.width) * img_size.height)
|
return CGSize(width: 100.0, height: (100.0/img_size.width) * img_size.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +147,7 @@ func calculate_blurhash(img: UIImage) async -> String? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let res = Task.detached(priority: .low) {
|
let res = Task.detached(priority: .low) {
|
||||||
let bhs = get_blurhash_size(img_size: img.size)
|
let bhs = get_blurhash_size(img_size: img.size) ?? CGSize(width: 100.0, height: 100.0)
|
||||||
let smaller = img.resized(to: bhs)
|
let smaller = img.resized(to: bhs)
|
||||||
|
|
||||||
guard let blurhash = smaller.blurHash(numberOfComponents: (5,5)) else {
|
guard let blurhash = smaller.blurHash(numberOfComponents: (5,5)) else {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ struct EditBannerImageView: View {
|
|||||||
var damus_state: DamusState
|
var damus_state: DamusState
|
||||||
@ObservedObject var viewModel: ImageUploadingObserver
|
@ObservedObject var viewModel: ImageUploadingObserver
|
||||||
let callback: (URL?) -> Void
|
let callback: (URL?) -> Void
|
||||||
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
|
let defaultImage = UIImage(named: "damoose") ?? UIImage()
|
||||||
|
|
||||||
@State var banner_image: URL? = nil
|
@State var banner_image: URL? = nil
|
||||||
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -38,7 +39,7 @@ struct EditBannerImageView: View {
|
|||||||
struct InnerBannerImageView: View {
|
struct InnerBannerImageView: View {
|
||||||
let disable_animation: Bool
|
let disable_animation: Bool
|
||||||
let url: URL?
|
let url: URL?
|
||||||
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
|
let defaultImage = UIImage(named: "damoose") ?? UIImage()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ struct ChatBubble<T: View, U: ShapeStyle, V: View>: View {
|
|||||||
stroke_style: .init(lineWidth: 4),
|
stroke_style: .init(lineWidth: 4),
|
||||||
background_style: Color.accentColor
|
background_style: Color.accentColor
|
||||||
) {
|
) {
|
||||||
Text("Hello there")
|
Text(verbatim: "Hello there")
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
@@ -176,7 +176,7 @@ struct ChatBubble<T: View, U: ShapeStyle, V: View>: View {
|
|||||||
stroke_style: .init(lineWidth: 4),
|
stroke_style: .init(lineWidth: 4),
|
||||||
background_style: Color.accentColor
|
background_style: Color.accentColor
|
||||||
) {
|
) {
|
||||||
Text("Hello there")
|
Text(verbatim: "Hello there")
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|||||||
@@ -182,25 +182,6 @@ extension CodeScannerView {
|
|||||||
delegate?.didFail(reason: .badOutput)
|
delegate?.didFail(reason: .badOutput)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override public func viewWillLayoutSubviews() {
|
|
||||||
previewLayer?.frame = view.layer.bounds
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func updateOrientation() {
|
|
||||||
guard let orientation = view.window?.windowScene?.interfaceOrientation else { return }
|
|
||||||
guard let connection = captureSession.connections.last, connection.isVideoOrientationSupported else { return }
|
|
||||||
connection.videoOrientation = AVCaptureVideoOrientation(rawValue: orientation.rawValue) ?? .portrait
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
updateOrientation()
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
if previewLayer == nil {
|
if previewLayer == nil {
|
||||||
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
|
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
|
||||||
@@ -220,6 +201,21 @@ extension CodeScannerView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override public func viewWillLayoutSubviews() {
|
||||||
|
previewLayer?.frame = view.layer.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func updateOrientation() {
|
||||||
|
guard let orientation = view.window?.windowScene?.interfaceOrientation else { return }
|
||||||
|
guard let connection = captureSession.connections.last, connection.isVideoOrientationSupported else { return }
|
||||||
|
connection.videoOrientation = AVCaptureVideoOrientation(rawValue: orientation.rawValue) ?? .portrait
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
|
||||||
private func addviewfinder() {
|
private func addviewfinder() {
|
||||||
guard showViewfinder, let imageView = viewFinder else { return }
|
guard showViewfinder, let imageView = viewFinder else { return }
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct AddMuteItemView: View {
|
struct AddMuteItemView: View {
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
@State var new_text: String = ""
|
@Binding var new_text: String
|
||||||
@State var expiration: DamusDuration = .indefinite
|
@State var expiration: DamusDuration = .indefinite
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@@ -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()
|
||||||
}) {
|
}) {
|
||||||
@@ -108,6 +108,6 @@ struct AddMuteItemView: View {
|
|||||||
|
|
||||||
struct AddMuteItemView_Previews: PreviewProvider {
|
struct AddMuteItemView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
AddMuteItemView(state: test_damus_state)
|
AddMuteItemView(state: test_damus_state, new_text: .constant(""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ struct MutelistView: View {
|
|||||||
@State var hashtags: [MuteItem] = []
|
@State var hashtags: [MuteItem] = []
|
||||||
@State var threads: [MuteItem] = []
|
@State var threads: [MuteItem] = []
|
||||||
@State var words: [MuteItem] = []
|
@State var words: [MuteItem] = []
|
||||||
|
|
||||||
|
@State var new_text: String = ""
|
||||||
|
|
||||||
func RemoveAction(item: MuteItem) -> some View {
|
func RemoveAction(item: MuteItem) -> some View {
|
||||||
Button {
|
Button {
|
||||||
@@ -120,13 +122,9 @@ struct MutelistView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $show_add_muteitem, onDismiss: { self.show_add_muteitem = false }) {
|
.sheet(isPresented: $show_add_muteitem, onDismiss: { self.show_add_muteitem = false }) {
|
||||||
if #available(iOS 16.0, *) {
|
AddMuteItemView(state: damus_state, new_text: $new_text)
|
||||||
AddMuteItemView(state: damus_state)
|
.presentationDetents([.height(300)])
|
||||||
.presentationDetents([.height(300)])
|
.presentationDragIndicator(.visible)
|
||||||
.presentationDragIndicator(.visible)
|
|
||||||
} else {
|
|
||||||
AddMuteItemView(state: damus_state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,15 @@ struct EditMetadataView: View {
|
|||||||
@State var ln: String
|
@State var ln: String
|
||||||
@State var website: String
|
@State var website: String
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
|
||||||
|
|
||||||
@State var confirm_ln_address: Bool = false
|
@State var confirm_ln_address: Bool = false
|
||||||
|
@State var confirm_save_alert: Bool = false
|
||||||
|
|
||||||
@StateObject var profileUploadObserver = ImageUploadingObserver()
|
@StateObject var profileUploadObserver = ImageUploadingObserver()
|
||||||
@StateObject var bannerUploadObserver = ImageUploadingObserver()
|
@StateObject var bannerUploadObserver = ImageUploadingObserver()
|
||||||
|
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
init(damus_state: DamusState) {
|
init(damus_state: DamusState) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
|
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
|
||||||
@@ -77,7 +79,7 @@ struct EditMetadataView: View {
|
|||||||
var TopSection: some View {
|
var TopSection: some View {
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
EditBannerImageView(damus_state: damus_state, viewModel: bannerUploadObserver, callback: uploadedBanner(image_url:))
|
EditBannerImageView(damus_state: damus_state, viewModel: bannerUploadObserver, callback: uploadedBanner(image_url:), banner_image: URL(string: banner))
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: geo.size.width, height: BANNER_HEIGHT)
|
.frame(width: geo.size.width, height: BANNER_HEIGHT)
|
||||||
.clipped()
|
.clipped()
|
||||||
@@ -86,7 +88,7 @@ struct EditMetadataView: View {
|
|||||||
let pfp_size: CGFloat = 90.0
|
let pfp_size: CGFloat = 90.0
|
||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
EditProfilePictureView(pubkey: damus_state.pubkey, damus_state: damus_state, size: pfp_size, uploadObserver: profileUploadObserver, callback: uploadedProfilePicture(image_url:))
|
EditProfilePictureView(profile_url: URL(string: picture), pubkey: damus_state.pubkey, damus_state: damus_state, size: pfp_size, uploadObserver: profileUploadObserver, callback: uploadedProfilePicture(image_url:))
|
||||||
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
|
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -97,6 +99,28 @@ struct EditMetadataView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func navImage(img: String) -> some View {
|
||||||
|
Image(img)
|
||||||
|
.frame(width: 33, height: 33)
|
||||||
|
.background(Color.black.opacity(0.6))
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
|
||||||
|
var navBackButton: some View {
|
||||||
|
HStack {
|
||||||
|
Button {
|
||||||
|
if didChange() {
|
||||||
|
confirm_save_alert.toggle()
|
||||||
|
} else {
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
navImage(img: "chevron-left")
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
TopSection
|
TopSection
|
||||||
@@ -116,18 +140,6 @@ struct EditMetadataView: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section (NSLocalizedString("Profile Picture", comment: "Label for Profile Picture section of user profile form.")) {
|
|
||||||
TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $picture)
|
|
||||||
.autocorrectionDisabled(true)
|
|
||||||
.textInputAutocapitalization(.never)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section (NSLocalizedString("Banner Image", comment: "Label for Banner Image section of user profile form.")) {
|
|
||||||
TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $banner)
|
|
||||||
.autocorrectionDisabled(true)
|
|
||||||
.textInputAutocapitalization(.never)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(NSLocalizedString("Website", comment: "Label for Website section of user profile form.")) {
|
Section(NSLocalizedString("Website", comment: "Label for Website section of user profile form.")) {
|
||||||
TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website)
|
TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website)
|
||||||
.autocorrectionDisabled(true)
|
.autocorrectionDisabled(true)
|
||||||
@@ -139,10 +151,10 @@ struct EditMetadataView: View {
|
|||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
TextEditor(text: $about)
|
TextEditor(text: $about)
|
||||||
.textInputAutocapitalization(.sentences)
|
.textInputAutocapitalization(.sentences)
|
||||||
.frame(minHeight: 20, alignment: .leading)
|
.frame(minHeight: 45, alignment: .leading)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
Text(about.isEmpty ? placeholder : about)
|
Text(about.isEmpty ? placeholder : about)
|
||||||
.padding(.leading, 4)
|
.padding(4)
|
||||||
.opacity(about.isEmpty ? 1 : 0)
|
.opacity(about.isEmpty ? 1 : 0)
|
||||||
.foregroundColor(Color(uiColor: .placeholderText))
|
.foregroundColor(Color(uiColor: .placeholderText))
|
||||||
}
|
}
|
||||||
@@ -175,25 +187,48 @@ struct EditMetadataView: View {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
Button(NSLocalizedString("Save", comment: "Button for saving profile.")) {
|
|
||||||
if !ln.isEmpty && !is_ln_valid(ln: ln) {
|
}
|
||||||
confirm_ln_address = true
|
|
||||||
} else {
|
Button(action: {
|
||||||
save()
|
if !ln.isEmpty && !is_ln_valid(ln: ln) {
|
||||||
dismiss()
|
confirm_ln_address = true
|
||||||
}
|
} else {
|
||||||
|
save()
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
.disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading)
|
}, label: {
|
||||||
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
|
Text(NSLocalizedString("Save", comment: "Button for saving profile."))
|
||||||
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
|
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||||
}
|
})
|
||||||
} message: {
|
.buttonStyle(GradientButtonStyle(padding: 15))
|
||||||
Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.")
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
.disabled(!didChange())
|
||||||
|
.opacity(!didChange() ? 0.5 : 1)
|
||||||
|
.disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading)
|
||||||
|
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
|
||||||
|
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
|
||||||
}
|
}
|
||||||
|
} message: {
|
||||||
|
Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(edges: .top)
|
.ignoresSafeArea(edges: .top)
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
|
.navigationBarBackButtonHidden()
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .principal) {
|
||||||
|
navBackButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(NSLocalizedString("Discard changes?", comment: "Alert user that changes have been made."), isPresented: $confirm_save_alert) {
|
||||||
|
Button(NSLocalizedString("No", comment: "Do not discard changes."), role: .cancel) {
|
||||||
|
}
|
||||||
|
Button(NSLocalizedString("Yes", comment: "Agree to discard changes made to profile.")) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadedProfilePicture(image_url: URL?) {
|
func uploadedProfilePicture(image_url: URL?) {
|
||||||
@@ -203,6 +238,45 @@ struct EditMetadataView: View {
|
|||||||
func uploadedBanner(image_url: URL?) {
|
func uploadedBanner(image_url: URL?) {
|
||||||
banner = image_url?.absoluteString ?? ""
|
banner = image_url?.absoluteString ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didChange() -> Bool {
|
||||||
|
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
|
||||||
|
let data = profile_txn?.unsafeUnownedValue
|
||||||
|
|
||||||
|
if data?.name ?? "" != name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if data?.display_name ?? "" != display_name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if data?.about ?? "" != about {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if data?.website ?? "" != website {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if data?.picture ?? "" != picture {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if data?.banner ?? "" != banner {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if data?.nip05 ?? "" != nip05 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if data?.lud16 ?? data?.lud06 ?? "" != ln {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EditMetadataView_Previews: PreviewProvider {
|
struct EditMetadataView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct EditPictureControl: View {
|
|||||||
var size: CGFloat? = 25
|
var size: CGFloat? = 25
|
||||||
var setup: Bool? = false
|
var setup: Bool? = false
|
||||||
@Binding var image_url: URL?
|
@Binding var image_url: URL?
|
||||||
|
@State var image_url_temp: URL?
|
||||||
@ObservedObject var uploadObserver: ImageUploadingObserver
|
@ObservedObject var uploadObserver: ImageUploadingObserver
|
||||||
let callback: (URL?) -> Void
|
let callback: (URL?) -> Void
|
||||||
|
|
||||||
@@ -25,12 +26,21 @@ struct EditPictureControl: View {
|
|||||||
|
|
||||||
@State private var show_camera = false
|
@State private var show_camera = false
|
||||||
@State private var show_library = false
|
@State private var show_library = false
|
||||||
|
@State private var show_url_sheet = false
|
||||||
@State var image_upload_confirm: Bool = false
|
@State var image_upload_confirm: Bool = false
|
||||||
|
|
||||||
@State var preUploadedMedia: PreUploadedMedia? = nil
|
@State var preUploadedMedia: PreUploadedMedia? = nil
|
||||||
|
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Menu {
|
Menu {
|
||||||
|
Button(action: {
|
||||||
|
self.show_url_sheet = true
|
||||||
|
}) {
|
||||||
|
Text("Image URL", comment: "Option to enter a url")
|
||||||
|
}
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
self.show_library = true
|
self.show_library = true
|
||||||
}) {
|
}) {
|
||||||
@@ -51,7 +61,7 @@ struct EditPictureControl: View {
|
|||||||
.background(DamusColors.white.opacity(0.7))
|
.background(DamusColors.white.opacity(0.7))
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
|
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
|
||||||
} else if let url = image_url {
|
} else if let url = image_url, setup ?? false {
|
||||||
KFAnimatedImage(url)
|
KFAnimatedImage(url)
|
||||||
.imageContext(.pfp, disable_animation: false)
|
.imageContext(.pfp, disable_animation: false)
|
||||||
.onFailure(fallbackUrl: URL(string: robohash(pubkey)), cacheKey: url.absoluteString)
|
.onFailure(fallbackUrl: URL(string: robohash(pubkey)), cacheKey: url.absoluteString)
|
||||||
@@ -61,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))
|
||||||
@@ -115,6 +126,70 @@ struct EditPictureControl: View {
|
|||||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $show_url_sheet) {
|
||||||
|
ZStack {
|
||||||
|
DamusColors.adaptableWhite.edgesIgnoringSafeArea(.all)
|
||||||
|
VStack {
|
||||||
|
Text("Image URL")
|
||||||
|
.bold()
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "doc.on.clipboard")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.onTapGesture {
|
||||||
|
if let pastedURL = UIPasteboard.general.string {
|
||||||
|
image_url_temp = URL(string: pastedURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextField(image_url_temp?.absoluteString ?? "", text: Binding(
|
||||||
|
get: { image_url_temp?.absoluteString ?? "" },
|
||||||
|
set: { image_url_temp = URL(string: $0) }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.stroke(.gray.opacity(0.5), lineWidth: 1)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.foregroundColor(.damusAdaptableWhite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
show_url_sheet.toggle()
|
||||||
|
}, label: {
|
||||||
|
Text("Cancel", comment: "Cancel button text for dismissing updating image url.")
|
||||||
|
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||||
|
.padding(10)
|
||||||
|
})
|
||||||
|
.buttonStyle(NeutralButtonStyle())
|
||||||
|
.padding(10)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
image_url = image_url_temp
|
||||||
|
callback(image_url)
|
||||||
|
show_url_sheet.toggle()
|
||||||
|
}, label: {
|
||||||
|
Text("Update", comment: "Update button text for updating image url.")
|
||||||
|
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||||
|
})
|
||||||
|
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.disabled(image_url_temp == image_url)
|
||||||
|
.opacity(image_url_temp == image_url ? 0.5 : 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
image_url_temp = image_url
|
||||||
|
}
|
||||||
|
.presentationDetents([.height(300)])
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handle_upload(media: MediaUpload) {
|
private func handle_upload(media: MediaUpload) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ struct ProfileActionSheetView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var muteButton: some View {
|
||||||
|
let target_pubkey = self.profile.pubkey
|
||||||
|
return VStack(alignment: .center, spacing: 10) {
|
||||||
|
MuteDurationMenu { duration in
|
||||||
|
notify(.mute(.user(target_pubkey, duration?.date_from_now)))
|
||||||
|
} label: {
|
||||||
|
Image("mute")
|
||||||
|
}
|
||||||
|
.buttonStyle(NeutralButtonShape.circle.style)
|
||||||
|
Text("Mute", comment: "Button label that allows the user to mute the user shown on-screen")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var dmButton: some View {
|
var dmButton: some View {
|
||||||
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
|
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
|
||||||
return VStack(alignment: .center, spacing: 10) {
|
return VStack(alignment: .center, spacing: 10) {
|
||||||
@@ -103,6 +118,9 @@ struct ProfileActionSheetView: View {
|
|||||||
self.followButton
|
self.followButton
|
||||||
self.zapButton
|
self.zapButton
|
||||||
self.dmButton
|
self.dmButton
|
||||||
|
if damus_state.keypair.pubkey != profile.pubkey && damus_state.keypair.privkey != nil {
|
||||||
|
self.muteButton
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,11 +126,11 @@ struct QRCodeView: View {
|
|||||||
|
|
||||||
if our_profile?.picture != nil {
|
if our_profile?.picture != nil {
|
||||||
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||||
.padding(.top, 50)
|
.padding(.top, 20)
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "person.fill")
|
Image(systemName: "person.fill")
|
||||||
.font(.system(size: 60))
|
.font(.system(size: 60))
|
||||||
.padding(.top, 50)
|
.padding(.top, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let display_name = profile?.display_name {
|
if let display_name = profile?.display_name {
|
||||||
@@ -150,17 +150,18 @@ struct QRCodeView: View {
|
|||||||
.interpolation(.none)
|
.interpolation(.none)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: 300, height: 300)
|
.frame(minWidth: 100, maxWidth: 300, minHeight: 100, maxHeight: 300)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.overlay(RoundedRectangle(cornerRadius: 10)
|
.overlay(RoundedRectangle(cornerRadius: 10)
|
||||||
.stroke(DamusColors.white, lineWidth: 5.0))
|
.stroke(DamusColors.white, lineWidth: 5.0)
|
||||||
|
.scaledToFit())
|
||||||
.shadow(radius: 10)
|
.shadow(radius: 10)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
|
Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
|
||||||
.font(.system(size: 24, weight: .heavy))
|
.font(.system(size: 24, weight: .heavy))
|
||||||
.padding(.top)
|
.padding(.top, 10)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
|
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
|
||||||
@@ -179,7 +180,7 @@ struct QRCodeView: View {
|
|||||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||||
}
|
}
|
||||||
.buttonStyle(GradientButtonStyle())
|
.buttonStyle(GradientButtonStyle())
|
||||||
.padding(50)
|
.padding(20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,11 +202,11 @@ struct QRCodeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: 300, height: 300)
|
.frame(maxWidth: 300, maxHeight: 300)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0))
|
.overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0).scaledToFit())
|
||||||
.overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5)
|
.overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5)
|
||||||
.rotationEffect(.degrees(-90)))
|
.rotationEffect(.degrees(-90)).scaledToFit())
|
||||||
.shadow(radius: 10)
|
.shadow(radius: 10)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ struct InnerRelayPicView: View {
|
|||||||
Placeholder(url: url)
|
Placeholder(url: url)
|
||||||
}
|
}
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
|
.kfClickable()
|
||||||
} else {
|
} else {
|
||||||
FailedRelayImage(url: nil)
|
FailedRelayImage(url: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ struct RelayView: View {
|
|||||||
.padding(.bottom, 2)
|
.padding(.bottom, 2)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
RelayType(is_paid: state.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false)
|
RelayType(is_paid: state.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false)
|
||||||
|
|
||||||
|
if relay.absoluteString.hasSuffix(".onion") {
|
||||||
|
Image("tor")
|
||||||
|
.resizable()
|
||||||
|
.interpolation(.none)
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text(relay.absoluteString)
|
Text(relay.absoluteString)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -7,10 +7,13 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
let MINIMUM_PUSH_NOTIFICATION_SYNC_DELAY_IN_SECONDS = 0.25
|
||||||
|
|
||||||
struct NotificationSettingsView: View {
|
struct NotificationSettingsView: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
@ObservedObject var settings: UserSettingsStore
|
@ObservedObject var settings: UserSettingsStore
|
||||||
@State var notification_mode_setting_error: String? = nil
|
@State var notification_mode_setting_error: String? = nil
|
||||||
|
@State var notification_preferences_sync_state: PreferencesSyncState = .undefined
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
@@ -32,7 +35,8 @@ struct NotificationSettingsView: View {
|
|||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await damus_state.push_notification_client.send_token()
|
try await damus_state.push_notification_client.send_token()
|
||||||
settings.notifications_mode = new_value
|
await self.sync_up_remote_notification_settings()
|
||||||
|
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)
|
||||||
@@ -43,7 +47,8 @@ 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
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
notification_mode_setting_error = String(format: NSLocalizedString("Error disabling push notifications with the server: %@", comment: "Error label shown when user tries to disable push notifications but something fails"), error.localizedDescription)
|
notification_mode_setting_error = String(format: NSLocalizedString("Error disabling push notifications with the server: %@", comment: "Error label shown when user tries to disable push notifications but something fails"), error.localizedDescription)
|
||||||
@@ -52,9 +57,64 @@ struct NotificationSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Push notification preference sync management
|
||||||
|
|
||||||
|
func notification_preference_binding<T>(_ raw_binding: Binding<T>) -> Binding<T> {
|
||||||
|
return Binding(
|
||||||
|
get: {
|
||||||
|
return raw_binding.wrappedValue
|
||||||
|
},
|
||||||
|
set: { new_value in
|
||||||
|
let old_value = raw_binding.wrappedValue
|
||||||
|
raw_binding.wrappedValue = new_value
|
||||||
|
if self.settings.notification_mode == .push {
|
||||||
|
Task {
|
||||||
|
await self.send_push_notification_preferences(on_failure: {
|
||||||
|
raw_binding.wrappedValue = old_value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sync_up_remote_notification_settings() async {
|
||||||
|
do {
|
||||||
|
notification_preferences_sync_state = .syncing
|
||||||
|
let remote_settings = try await damus_state.push_notification_client.get_settings()
|
||||||
|
let local_settings = PushNotificationClient.NotificationSettings.from(settings: settings)
|
||||||
|
if remote_settings != local_settings {
|
||||||
|
await self.send_push_notification_preferences(local_settings)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notification_preferences_sync_state = .success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
notification_preferences_sync_state = .failure(error: String(format: NSLocalizedString("Failed to get push notification preferences from the server", comment: "Error label indicating about a failure in fetching notification preferences"), error.localizedDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func send_push_notification_preferences(_ new_settings: PushNotificationClient.NotificationSettings? = nil, on_failure: (() -> Void)? = nil) async {
|
||||||
|
do {
|
||||||
|
notification_preferences_sync_state = .syncing
|
||||||
|
try await damus_state.push_notification_client.set_settings(new_settings)
|
||||||
|
// Make sync appear to take at least a few milliseconds or so to avoid issues with labor perception bias (https://growth.design/case-studies/labor-perception-bias)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + MINIMUM_PUSH_NOTIFICATION_SYNC_DELAY_IN_SECONDS) {
|
||||||
|
notification_preferences_sync_state = .success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
notification_preferences_sync_state = .failure(error: String(format: NSLocalizedString("Error syncing up push notifications preferences with the server: %@", comment: "Error label shown when system tries to sync up notification preferences to the push notification server but something fails"), error.localizedDescription))
|
||||||
|
on_failure?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - View layout
|
||||||
|
|
||||||
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 {
|
||||||
@@ -66,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)
|
||||||
}
|
}
|
||||||
@@ -80,21 +140,40 @@ struct NotificationSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Local Notifications", comment: "Section header for damus local notifications user configuration")) {
|
Section(
|
||||||
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: $settings.zap_notification)
|
header: Text("Notification Preferences", comment: "Section header for Notification Preferences"),
|
||||||
|
footer: VStack {
|
||||||
|
switch notification_preferences_sync_state {
|
||||||
|
case .undefined, .not_applicable:
|
||||||
|
EmptyView()
|
||||||
|
case .success:
|
||||||
|
HStack {
|
||||||
|
Image("check-circle.fill")
|
||||||
|
.foregroundStyle(.damusGreen)
|
||||||
|
Text("Successfully synced", comment: "Label indicating success in syncing notification preferences")
|
||||||
|
}
|
||||||
|
case .syncing:
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
ProgressView()
|
||||||
|
Text("Syncing", comment: "Label indicating success in syncing notification preferences")
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
Text(error)
|
||||||
|
.foregroundStyle(.damusDangerPrimary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: self.notification_preference_binding($settings.zap_notification))
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: $settings.mention_notification)
|
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: self.notification_preference_binding($settings.mention_notification))
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: $settings.repost_notification)
|
Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: self.notification_preference_binding($settings.repost_notification))
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: $settings.like_notification)
|
Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: self.notification_preference_binding($settings.like_notification))
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
Toggle(NSLocalizedString("DMs", comment: "Setting to enable DM Local Notification"), isOn: $settings.dm_notification)
|
Toggle(NSLocalizedString("DMs", comment: "Setting to enable DM Local Notification"), isOn: self.notification_preference_binding($settings.dm_notification))
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
}
|
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: self.notification_preference_binding($settings.notification_only_from_following))
|
||||||
|
|
||||||
Section(header: Text("Notification Preference", comment: "Section header for Notification Preferences")) {
|
|
||||||
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: $settings.notification_only_from_following)
|
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +192,28 @@ struct NotificationSettingsView: View {
|
|||||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
.onAppear(perform: {
|
||||||
|
Task {
|
||||||
|
if self.settings.notification_mode == .push {
|
||||||
|
await self.sync_up_remote_notification_settings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationSettingsView {
|
||||||
|
enum PreferencesSyncState {
|
||||||
|
/// State is unknown
|
||||||
|
case undefined
|
||||||
|
/// State is not applicable (e.g. Notifications are set to local)
|
||||||
|
case not_applicable
|
||||||
|
/// Preferences are successfully synced
|
||||||
|
case success
|
||||||
|
/// Preferences are being synced
|
||||||
|
case syncing
|
||||||
|
/// There was a failure during syncing
|
||||||
|
case failure(error: String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
<file original="damus/en-US.lproj/InfoPlist.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
<file original="damus/en-US.lproj/InfoPlist.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||||
<header>
|
<header>
|
||||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
|
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||||
</header>
|
</header>
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</file>
|
</file>
|
||||||
<file original="damus/en-US.lproj/Localizable.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
<file original="damus/en-US.lproj/Localizable.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||||
<header>
|
<header>
|
||||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
|
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||||
</header>
|
</header>
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="%@ %@" xml:space="preserve">
|
<trans-unit id="%@ %@" xml:space="preserve">
|
||||||
@@ -144,11 +144,6 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>API Key (required)</target>
|
<target>API Key (required)</target>
|
||||||
<note>Prompt for required entry of API Key to use translation server.</note>
|
<note>Prompt for required entry of API Key to use translation server.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="About" xml:space="preserve">
|
|
||||||
<source>About</source>
|
|
||||||
<target>About</target>
|
|
||||||
<note>Label to prompt for about text entry for user to describe about themself.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="About Me" xml:space="preserve">
|
<trans-unit id="About Me" xml:space="preserve">
|
||||||
<source>About Me</source>
|
<source>About Me</source>
|
||||||
<target>About Me</target>
|
<target>About Me</target>
|
||||||
@@ -159,6 +154,11 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>Absolute Boss</target>
|
<target>Absolute Boss</target>
|
||||||
<note>Placeholder text for About Me description.</note>
|
<note>Placeholder text for About Me description.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Absolute legend." xml:space="preserve">
|
||||||
|
<source>Absolute legend.</source>
|
||||||
|
<target>Absolute legend.</target>
|
||||||
|
<note>Example Bio</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Accessibility" xml:space="preserve">
|
<trans-unit id="Accessibility" xml:space="preserve">
|
||||||
<source>Accessibility</source>
|
<source>Accessibility</source>
|
||||||
<target>Accessibility</target>
|
<target>Accessibility</target>
|
||||||
@@ -189,6 +189,11 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>Add Bookmark</target>
|
<target>Add Bookmark</target>
|
||||||
<note>Button text to add bookmark to a note.</note>
|
<note>Button text to add bookmark to a note.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Add Photo" xml:space="preserve">
|
||||||
|
<source>Add Photo</source>
|
||||||
|
<target>Add Photo</target>
|
||||||
|
<note>Label to indicate user can add a photo.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Add all" xml:space="preserve">
|
<trans-unit id="Add all" xml:space="preserve">
|
||||||
<source>Add all</source>
|
<source>Add all</source>
|
||||||
<target>Add all</target>
|
<target>Add all</target>
|
||||||
@@ -255,6 +260,11 @@ Button text to add a relay</note>
|
|||||||
<target>An additional percentage of each zap will be sent to support Damus development</target>
|
<target>An additional percentage of each zap will be sent to support Damus development</target>
|
||||||
<note>Text indicating that they can contribute zaps to support Damus development.</note>
|
<note>Text indicating that they can contribute zaps to support Damus development.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="An unexpected error happened while trying to create the new contact list. Please contact support." xml:space="preserve">
|
||||||
|
<source>An unexpected error happened while trying to create the new contact list. Please contact support.</source>
|
||||||
|
<target>An unexpected error happened while trying to create the new contact list. Please contact support.</target>
|
||||||
|
<note>Error message for a failed contact list reset operation</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Animations" xml:space="preserve">
|
<trans-unit id="Animations" xml:space="preserve">
|
||||||
<source>Animations</source>
|
<source>Animations</source>
|
||||||
<target>Animations</target>
|
<target>Animations</target>
|
||||||
@@ -351,10 +361,10 @@ Tip: You can always change this later in Settings → Translations</target>
|
|||||||
<target>Be the first to access upcoming premium features: Automatic translations, longer note storage, and more</target>
|
<target>Be the first to access upcoming premium features: Automatic translations, longer note storage, and more</target>
|
||||||
<note>Description of new features to be expected</note>
|
<note>Description of new features to be expected</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus." xml:space="preserve">
|
<trans-unit id="Bio" xml:space="preserve">
|
||||||
<source>Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.</source>
|
<source>Bio</source>
|
||||||
<target>Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.</target>
|
<target>Bio</target>
|
||||||
<note>Reminder to user that they should save their account information.</note>
|
<note>Label to prompt bio entry for user to describe themself.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Bitcoin Lightning Tips" xml:space="preserve">
|
<trans-unit id="Bitcoin Lightning Tips" xml:space="preserve">
|
||||||
<source>Bitcoin Lightning Tips</source>
|
<source>Bitcoin Lightning Tips</source>
|
||||||
@@ -383,10 +393,10 @@ Tip: You can always change this later in Settings → Translations</target>
|
|||||||
<target>Broadcast music playing on Apple Music</target>
|
<target>Broadcast music playing on Apple Music</target>
|
||||||
<note>Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.</note>
|
<note>Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="By signing up, you agree to our " xml:space="preserve">
|
<trans-unit id="By continuing you agree to our " xml:space="preserve">
|
||||||
<source>By signing up, you agree to our </source>
|
<source>By continuing you agree to our </source>
|
||||||
<target>By signing up, you agree to our </target>
|
<target>By continuing you agree to our </target>
|
||||||
<note>Ask the user if they already have an account on Nostr</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)" xml:space="preserve">
|
<trans-unit id="By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)" xml:space="preserve">
|
||||||
<source>By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)</source>
|
<source>By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)</source>
|
||||||
@@ -413,7 +423,8 @@ Tip: You can always change this later in Settings → Translations</target>
|
|||||||
Button to cancel the upload.
|
Button to cancel the upload.
|
||||||
Cancel deleting bookmarks.
|
Cancel deleting bookmarks.
|
||||||
Cancel deleting the user.
|
Cancel deleting the user.
|
||||||
Cancel out of logging out the user.</note>
|
Cancel out of logging out the user.
|
||||||
|
Cancel resetting the contact list.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?" xml:space="preserve">
|
<trans-unit id="Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?" xml:space="preserve">
|
||||||
<source>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</source>
|
<source>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</source>
|
||||||
@@ -486,6 +497,16 @@ Button to connect to the relay.</note>
|
|||||||
<target>Connecting</target>
|
<target>Connecting</target>
|
||||||
<note>Relay status label that indicates a relay is connecting.</note>
|
<note>Relay status label that indicates a relay is connecting.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Contact list (Follows + Relay list)" xml:space="preserve">
|
||||||
|
<source>Contact list (Follows + Relay list)</source>
|
||||||
|
<target>Contact list (Follows + Relay list)</target>
|
||||||
|
<note>Section title for Contact list first aid tools</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Contact list has been reset" xml:space="preserve">
|
||||||
|
<source>Contact list has been reset</source>
|
||||||
|
<target>Contact list has been reset</target>
|
||||||
|
<note>Message indicating that the contact list was successfully reset.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Content filters" xml:space="preserve">
|
<trans-unit id="Content filters" xml:space="preserve">
|
||||||
<source>Content filters</source>
|
<source>Content filters</source>
|
||||||
<target>Content filters</target>
|
<target>Content filters</target>
|
||||||
@@ -495,7 +516,8 @@ Button to connect to the relay.</note>
|
|||||||
<source>Continue</source>
|
<source>Continue</source>
|
||||||
<target>Continue</target>
|
<target>Continue</target>
|
||||||
<note>Continue with bookmarks.
|
<note>Continue with bookmarks.
|
||||||
Continue with deleting the user.</note>
|
Continue with deleting the user.
|
||||||
|
Continue with resetting the contact list.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Copied" xml:space="preserve">
|
<trans-unit id="Copied" xml:space="preserve">
|
||||||
<source>Copied</source>
|
<source>Copied</source>
|
||||||
@@ -569,6 +591,11 @@ Button to connect to the relay.</note>
|
|||||||
<target>Copy user public key</target>
|
<target>Copy user public key</target>
|
||||||
<note>Context menu option for copying the ID of the user who created the note.</note>
|
<note>Context menu option for copying the ID of the user who created the note.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help." xml:space="preserve">
|
||||||
|
<source>Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.</source>
|
||||||
|
<target>Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.</target>
|
||||||
|
<note>Error message to the user indicating that the initial contact list failed to be created.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Could not find the user you're looking for" xml:space="preserve">
|
<trans-unit id="Could not find the user you're looking for" xml:space="preserve">
|
||||||
<source>Could not find the user you're looking for</source>
|
<source>Could not find the user you're looking for</source>
|
||||||
<target>Could not find the user you're looking for</target>
|
<target>Could not find the user you're looking for</target>
|
||||||
@@ -579,26 +606,21 @@ Button to connect to the relay.</note>
|
|||||||
<target>Could not find user to mute...</target>
|
<target>Could not find user to mute...</target>
|
||||||
<note>Alert message to indicate that the muted user could not be found.</note>
|
<note>Alert message to indicate that the muted user could not be found.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Create Account" xml:space="preserve">
|
||||||
|
<source>Create Account</source>
|
||||||
|
<target>Create Account</target>
|
||||||
|
<note>Button to continue to the create account page.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Create account" xml:space="preserve">
|
<trans-unit id="Create account" xml:space="preserve">
|
||||||
<source>Create account</source>
|
<source>Create account</source>
|
||||||
<target>Create account</target>
|
<target>Create account</target>
|
||||||
<note>Button to navigate to create account view.</note>
|
<note>Button to navigate to create account view.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Create account now" xml:space="preserve">
|
|
||||||
<source>Create account now</source>
|
|
||||||
<target>Create account now</target>
|
|
||||||
<note>Button to create account.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Create new mutelist" xml:space="preserve">
|
<trans-unit id="Create new mutelist" xml:space="preserve">
|
||||||
<source>Create new mutelist</source>
|
<source>Create new mutelist</source>
|
||||||
<target>Create new mutelist</target>
|
<target>Create new mutelist</target>
|
||||||
<note>Title of alert prompting the user to create a new mutelist.</note>
|
<note>Title of alert prompting the user to create a new mutelist.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Creator(s) of Bitcoin. Absolute legend." xml:space="preserve">
|
|
||||||
<source>Creator(s) of Bitcoin. Absolute legend.</source>
|
|
||||||
<target>Creator(s) of Bitcoin. Absolute legend.</target>
|
|
||||||
<note>Example description about Bitcoin creator(s), Satoshi Nakamoto.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Custom" xml:space="preserve">
|
<trans-unit id="Custom" xml:space="preserve">
|
||||||
<source>Custom</source>
|
<source>Custom</source>
|
||||||
<target>Custom</target>
|
<target>Custom</target>
|
||||||
@@ -609,6 +631,7 @@ Button to connect to the relay.</note>
|
|||||||
<target>DMs</target>
|
<target>DMs</target>
|
||||||
<note>Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
|
<note>Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
|
||||||
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message.
|
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message.
|
||||||
|
Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.
|
||||||
Setting to enable DM Local Notification
|
Setting to enable DM Local Notification
|
||||||
Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.</note>
|
Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -695,11 +718,6 @@ Button to disconnect from a relay server.</note>
|
|||||||
<target>Dismiss</target>
|
<target>Dismiss</target>
|
||||||
<note>Button to dismiss alert</note>
|
<note>Button to dismiss alert</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Display name" xml:space="preserve">
|
|
||||||
<source>Display name</source>
|
|
||||||
<target>Display name</target>
|
|
||||||
<note>Label to prompt display name entry.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Done" xml:space="preserve">
|
<trans-unit id="Done" xml:space="preserve">
|
||||||
<source>Done</source>
|
<source>Done</source>
|
||||||
<target>Done</target>
|
<target>Done</target>
|
||||||
@@ -766,6 +784,16 @@ Button to disconnect from a relay server.</note>
|
|||||||
<note>Label to display that authentication to a server has failed.
|
<note>Label to display that authentication to a server has failed.
|
||||||
Relay status label that indicates a relay had an error when connecting</note>
|
Relay status label that indicates a relay had an error when connecting</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Error configuring push notifications with the server: %@" xml:space="preserve">
|
||||||
|
<source>Error configuring push notifications with the server: %@</source>
|
||||||
|
<target>Error configuring push notifications with the server: %@</target>
|
||||||
|
<note>Error label shown when user tries to enable push notifications but something fails</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Error disabling push notifications with the server: %@" xml:space="preserve">
|
||||||
|
<source>Error disabling push notifications with the server: %@</source>
|
||||||
|
<target>Error disabling push notifications with the server: %@</target>
|
||||||
|
<note>Error label shown when user tries to disable push notifications but something fails</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Error fetching lightning invoice" xml:space="preserve">
|
<trans-unit id="Error fetching lightning invoice" xml:space="preserve">
|
||||||
<source>Error fetching lightning invoice</source>
|
<source>Error fetching lightning invoice</source>
|
||||||
<target>Error fetching lightning invoice</target>
|
<target>Error fetching lightning invoice</target>
|
||||||
@@ -816,6 +844,12 @@ Relay status label that indicates a relay had an error when connecting</note>
|
|||||||
<target>Failed to parse</target>
|
<target>Failed to parse</target>
|
||||||
<note>NostrScript error message when it fails to parse a script.</note>
|
<note>NostrScript error message when it fails to parse a script.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="First Aid" xml:space="preserve">
|
||||||
|
<source>First Aid</source>
|
||||||
|
<target>First Aid</target>
|
||||||
|
<note>Navigation title for first aid settings and tools
|
||||||
|
Section header for first aid tools and settings</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Follow" xml:space="preserve">
|
<trans-unit id="Follow" xml:space="preserve">
|
||||||
<source>Follow</source>
|
<source>Follow</source>
|
||||||
<target>Follow</target>
|
<target>Follow</target>
|
||||||
@@ -907,6 +941,11 @@ My side interests include languages and I am striving to be a #polyglot - I am a
|
|||||||
<target>Free</target>
|
<target>Free</target>
|
||||||
<note>Dropdown option for selecting Free plan for DeepL translation service.</note>
|
<note>Dropdown option for selecting Free plan for DeepL translation service.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="General" xml:space="preserve">
|
||||||
|
<source>General</source>
|
||||||
|
<target>General</target>
|
||||||
|
<note>Section header for general damus notifications user configuration</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Get API Key" xml:space="preserve">
|
<trans-unit id="Get API Key" xml:space="preserve">
|
||||||
<source>Get API Key</source>
|
<source>Get API Key</source>
|
||||||
<target>Get API Key</target>
|
<target>Get API Key</target>
|
||||||
@@ -926,7 +965,8 @@ My side interests include languages and I am striving to be a #polyglot - I am a
|
|||||||
<trans-unit id="Hashtags" xml:space="preserve">
|
<trans-unit id="Hashtags" xml:space="preserve">
|
||||||
<source>Hashtags</source>
|
<source>Hashtags</source>
|
||||||
<target>Hashtags</target>
|
<target>Hashtags</target>
|
||||||
<note>Section header title for a list of hashtags that are muted.</note>
|
<note>Label for filter for seeing only hashtag follows.
|
||||||
|
Section header title for a list of hashtags that are muted.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Hello everybody! This is my first post on Damus, I am happy to meet you all 🤙. What’s up? #introductions" xml:space="preserve">
|
<trans-unit id="Hello everybody! This is my first post on Damus, I am happy to meet you all 🤙. What’s up? #introductions" xml:space="preserve">
|
||||||
<source>Hello everybody!
|
<source>Hello everybody!
|
||||||
@@ -971,6 +1011,16 @@ This is my first post on Damus, I am happy to meet you all 🤙. What’s up?
|
|||||||
<target>Hide notes with #nsfw tags</target>
|
<target>Hide notes with #nsfw tags</target>
|
||||||
<note>Setting to hide notes with the #nsfw (not safe for work) tags</note>
|
<note>Setting to hide notes with the #nsfw (not safe for work) tags</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Highlighted" xml:space="preserve">
|
||||||
|
<source>Highlighted</source>
|
||||||
|
<target>Highlighted</target>
|
||||||
|
<note>Label to indicate that the user is highlighting their own post.</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Highlighted %@" xml:space="preserve">
|
||||||
|
<source>Highlighted %@</source>
|
||||||
|
<target>Highlighted %@</target>
|
||||||
|
<note>Label to indicate that the user is highlighting 1 user.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Home" xml:space="preserve">
|
<trans-unit id="Home" xml:space="preserve">
|
||||||
<source>Home</source>
|
<source>Home</source>
|
||||||
<target>Home</target>
|
<target>Home</target>
|
||||||
@@ -1005,6 +1055,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
|||||||
<target>Impersonation</target>
|
<target>Impersonation</target>
|
||||||
<note>Description of report type for impersonation.</note>
|
<note>Description of report type for impersonation.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="In progress…" xml:space="preserve">
|
||||||
|
<source>In progress…</source>
|
||||||
|
<target>In progress…</target>
|
||||||
|
<note>Loading message indicating that a contact list reset operation is in progress.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Indefinite" xml:space="preserve">
|
<trans-unit id="Indefinite" xml:space="preserve">
|
||||||
<source>Indefinite</source>
|
<source>Indefinite</source>
|
||||||
<target>Indefinite</target>
|
<target>Indefinite</target>
|
||||||
@@ -1051,11 +1106,6 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
|||||||
<target>LIVE</target>
|
<target>LIVE</target>
|
||||||
<note>Text indicator that the video is a livestream.</note>
|
<note>Text indicator that the video is a livestream.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Learn more about Nostr" xml:space="preserve">
|
|
||||||
<source>Learn more about Nostr</source>
|
|
||||||
<target>Learn more about Nostr</target>
|
|
||||||
<note>Button that opens up a webpage where the user can learn more about Nostr.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Learn more about the features" xml:space="preserve">
|
<trans-unit id="Learn more about the features" xml:space="preserve">
|
||||||
<source>Learn more about the features</source>
|
<source>Learn more about the features</source>
|
||||||
<target>Learn more about the features</target>
|
<target>Learn more about the features</target>
|
||||||
@@ -1066,16 +1116,6 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
|||||||
<target>Left Handed</target>
|
<target>Left Handed</target>
|
||||||
<note>Moves the post button to the left side of the screen</note>
|
<note>Moves the post button to the left side of the screen</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Let's get started!" xml:space="preserve">
|
|
||||||
<source>Let's get started!</source>
|
|
||||||
<target>Let's get started!</target>
|
|
||||||
<note>Button to continue to login page.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Let's go!" xml:space="preserve">
|
|
||||||
<source>Let's go!</source>
|
|
||||||
<target>Let's go!</target>
|
|
||||||
<note>Button to complete account creation and start using the app.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="LibreTranslate (Open Source)" xml:space="preserve">
|
<trans-unit id="LibreTranslate (Open Source)" xml:space="preserve">
|
||||||
<source>LibreTranslate (Open Source)</source>
|
<source>LibreTranslate (Open Source)</source>
|
||||||
<target>LibreTranslate (Open Source)</target>
|
<target>LibreTranslate (Open Source)</target>
|
||||||
@@ -1106,6 +1146,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
|||||||
<target>Load media</target>
|
<target>Load media</target>
|
||||||
<note>Button to show media in note.</note>
|
<note>Button to show media in note.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Local" xml:space="preserve">
|
||||||
|
<source>Local</source>
|
||||||
|
<target>Local</target>
|
||||||
|
<note>Option for notification mode setting: Local notification mode</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Local Notifications" xml:space="preserve">
|
<trans-unit id="Local Notifications" xml:space="preserve">
|
||||||
<source>Local Notifications</source>
|
<source>Local Notifications</source>
|
||||||
<target>Local Notifications</target>
|
<target>Local Notifications</target>
|
||||||
@@ -1175,7 +1220,8 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
|||||||
<trans-unit id="Mentions" xml:space="preserve">
|
<trans-unit id="Mentions" xml:space="preserve">
|
||||||
<source>Mentions</source>
|
<source>Mentions</source>
|
||||||
<target>Mentions</target>
|
<target>Mentions</target>
|
||||||
<note>Setting to enable Mention Local Notification</note>
|
<note>Label for filter for seeing mention notifications (replies, etc).
|
||||||
|
Setting to enable Mention Local Notification</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Merch" xml:space="preserve">
|
<trans-unit id="Merch" xml:space="preserve">
|
||||||
<source>Merch</source>
|
<source>Merch</source>
|
||||||
@@ -1195,7 +1241,9 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
|||||||
<trans-unit id="Mute" xml:space="preserve">
|
<trans-unit id="Mute" xml:space="preserve">
|
||||||
<source>Mute</source>
|
<source>Mute</source>
|
||||||
<target>Mute</target>
|
<target>Mute</target>
|
||||||
<note>Alert button to mute a user.</note>
|
<note>Alert button to mute a user.
|
||||||
|
Button to mute a profile
|
||||||
|
Title for confirmation dialog to mute a profile.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Mute %@?" xml:space="preserve">
|
<trans-unit id="Mute %@?" xml:space="preserve">
|
||||||
<source>Mute %@?</source>
|
<source>Mute %@?</source>
|
||||||
@@ -1235,6 +1283,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
|||||||
Text label indicating that there is no NIP-11 relay description information found. In English, N/A stands for not applicable.
|
Text label indicating that there is no NIP-11 relay description information found. In English, N/A stands for not applicable.
|
||||||
Text label indicating that there is no NIP-11 relay software information found. In English, N/A stands for not applicable.</note>
|
Text label indicating that there is no NIP-11 relay software information found. In English, N/A stands for not applicable.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Name" xml:space="preserve">
|
||||||
|
<source>Name</source>
|
||||||
|
<target>Name</target>
|
||||||
|
<note>Label to prompt name entry.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Never" xml:space="preserve">
|
<trans-unit id="Never" xml:space="preserve">
|
||||||
<source>Never</source>
|
<source>Never</source>
|
||||||
<target>Never</target>
|
<target>Never</target>
|
||||||
@@ -1260,11 +1313,21 @@ Text label indicating that there is no NIP-11 relay software information found.
|
|||||||
<target>New to Nostr?</target>
|
<target>New to Nostr?</target>
|
||||||
<note>Ask the user if they are new to Nostr</note>
|
<note>Ask the user if they are new to Nostr</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Next" xml:space="preserve">
|
||||||
|
<source>Next</source>
|
||||||
|
<target>Next</target>
|
||||||
|
<note>Button to continue with account creation.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="No" xml:space="preserve">
|
<trans-unit id="No" xml:space="preserve">
|
||||||
<source>No</source>
|
<source>No</source>
|
||||||
<target>No</target>
|
<target>No</target>
|
||||||
<note>User confirm No</note>
|
<note>User confirm No</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it" xml:space="preserve">
|
||||||
|
<source>No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it</source>
|
||||||
|
<target>No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it</target>
|
||||||
|
<note>Section footer for Contact list first aid tools</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="No logs to display" xml:space="preserve">
|
<trans-unit id="No logs to display" xml:space="preserve">
|
||||||
<source>No logs to display</source>
|
<source>No logs to display</source>
|
||||||
<target>No logs to display</target>
|
<target>No logs to display</target>
|
||||||
@@ -1310,11 +1373,6 @@ Text label indicating that there is no NIP-11 relay software information found.
|
|||||||
<target>Nostr Address</target>
|
<target>Nostr Address</target>
|
||||||
<note>Label for the Nostr Address section of user profile form.</note>
|
<note>Label for the Nostr Address section of user profile form.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network" xml:space="preserve">
|
|
||||||
<source>Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network</source>
|
|
||||||
<target>Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network</target>
|
|
||||||
<note>Description about what is Nostr.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="NostrScript" xml:space="preserve">
|
<trans-unit id="NostrScript" xml:space="preserve">
|
||||||
<source>NostrScript</source>
|
<source>NostrScript</source>
|
||||||
<target>NostrScript</target>
|
<target>NostrScript</target>
|
||||||
@@ -1325,6 +1383,11 @@ Text label indicating that there is no NIP-11 relay software information found.
|
|||||||
<target>NostrScript Error</target>
|
<target>NostrScript Error</target>
|
||||||
<note>Text indicating that there was an error with loading NostrScript. There is a more descriptive error message shown separately underneath.</note>
|
<note>Text indicating that there was an error with loading NostrScript. There is a more descriptive error message shown separately underneath.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Not now" xml:space="preserve">
|
||||||
|
<source>Not now</source>
|
||||||
|
<target>Not now</target>
|
||||||
|
<note>Button to not save key, complete account creation, and start using the app.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Note from a %@ you've muted" xml:space="preserve">
|
<trans-unit id="Note from a %@ you've muted" xml:space="preserve">
|
||||||
<source>Note from a %@ you've muted</source>
|
<source>Note from a %@ you've muted</source>
|
||||||
<target>Note from a %@ you've muted</target>
|
<target>Note from a %@ you've muted</target>
|
||||||
@@ -1333,20 +1396,18 @@ Text label indicating that there is no NIP-11 relay software information found.
|
|||||||
<trans-unit id="Note you've muted" xml:space="preserve">
|
<trans-unit id="Note you've muted" xml:space="preserve">
|
||||||
<source>Note you've muted</source>
|
<source>Note you've muted</source>
|
||||||
<target>Note you've muted</target>
|
<target>Note you've muted</target>
|
||||||
<note>Text to indicate that what is being shown is a note which has been muted.</note>
|
<note>Text to indicate that what is being shown is a note which has been muted.
|
||||||
|
Label indicating note has been muted</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Notes" xml:space="preserve">
|
<trans-unit id="Notes" xml:space="preserve">
|
||||||
<source>Notes</source>
|
<source>Notes</source>
|
||||||
<target>Notes</target>
|
<target>Notes</target>
|
||||||
<note>Label for filter for seeing only notes (instead of notes and replies).
|
<note>Label for filter for seeing only notes (instead of notes and replies).</note>
|
||||||
Label for filter for seeing only your notes (instead of notes and replies).
|
|
||||||
A label indicating that the notes being displayed below it are from a timeline, not search results</note>
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Notes & Replies" xml:space="preserve">
|
<trans-unit id="Notes & Replies" xml:space="preserve">
|
||||||
<source>Notes & Replies</source>
|
<source>Notes & Replies</source>
|
||||||
<target>Notes & Replies</target>
|
<target>Notes & Replies</target>
|
||||||
<note>Label for filter for seeing notes and replies (instead of only notes).
|
<note>Label for filter for seeing notes and replies (instead of only notes).</note>
|
||||||
Label for filter for seeing your notes and replies (instead of only your notes).</note>
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Notes with the #nsfw tag usually contains adult content or other "Not safe for work" content" xml:space="preserve">
|
<trans-unit id="Notes with the #nsfw tag usually contains adult content or other "Not safe for work" content" xml:space="preserve">
|
||||||
<source>Notes with the #nsfw tag usually contains adult content or other "Not safe for work" content</source>
|
<source>Notes with the #nsfw tag usually contains adult content or other "Not safe for work" content</source>
|
||||||
@@ -1374,6 +1435,11 @@ Label for filter for seeing your notes and replies (instead of only your notes).
|
|||||||
<note>Section header for Damus notifications
|
<note>Section header for Damus notifications
|
||||||
Toolbar label for Notifications view.</note>
|
Toolbar label for Notifications view.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Notifications mode" xml:space="preserve">
|
||||||
|
<source>Notifications mode</source>
|
||||||
|
<target>Notifications mode</target>
|
||||||
|
<note>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).</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Nudity" xml:space="preserve">
|
<trans-unit id="Nudity" xml:space="preserve">
|
||||||
<source>Nudity</source>
|
<source>Nudity</source>
|
||||||
<target>Nudity</target>
|
<target>Nudity</target>
|
||||||
@@ -1430,6 +1496,11 @@ Button label to dismiss an error dialog</note>
|
|||||||
<target>Optional</target>
|
<target>Optional</target>
|
||||||
<note>Prompt to enter optional additional information when reporting an account or content.</note>
|
<note>Prompt to enter optional additional information when reporting an account or content.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Orange-pill" xml:space="preserve">
|
||||||
|
<source>Orange-pill</source>
|
||||||
|
<target>Orange-pill</target>
|
||||||
|
<note>Button label that allows the user to start a direct message conversation with the user shown on-screen, to orange-pill them (i.e. help them to setup zaps)</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Paid Relay" xml:space="preserve">
|
<trans-unit id="Paid Relay" xml:space="preserve">
|
||||||
<source>Paid Relay</source>
|
<source>Paid Relay</source>
|
||||||
<target>Paid Relay</target>
|
<target>Paid Relay</target>
|
||||||
@@ -1478,7 +1549,8 @@ Button label to dismiss an error dialog</note>
|
|||||||
<trans-unit id="Post" xml:space="preserve">
|
<trans-unit id="Post" xml:space="preserve">
|
||||||
<source>Post</source>
|
<source>Post</source>
|
||||||
<target>Post</target>
|
<target>Post</target>
|
||||||
<note>Button to post a note.</note>
|
<note>Button to post a highlight.
|
||||||
|
Button to post a note.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Private" xml:space="preserve">
|
<trans-unit id="Private" xml:space="preserve">
|
||||||
<source>Private</source>
|
<source>Private</source>
|
||||||
@@ -1540,11 +1612,6 @@ Button label to dismiss an error dialog</note>
|
|||||||
<target>Public Account ID</target>
|
<target>Public Account ID</target>
|
||||||
<note>Section title for the user's public account ID.</note>
|
<note>Section title for the user's public account ID.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Public Key" xml:space="preserve">
|
|
||||||
<source>Public Key</source>
|
|
||||||
<target>Public Key</target>
|
|
||||||
<note>Label to indicate the public key of the account.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Public key" xml:space="preserve">
|
<trans-unit id="Public key" xml:space="preserve">
|
||||||
<source>Public key</source>
|
<source>Public key</source>
|
||||||
<target>Public key</target>
|
<target>Public key</target>
|
||||||
@@ -1570,6 +1637,11 @@ Button label to dismiss an error dialog</note>
|
|||||||
<target>Purple</target>
|
<target>Purple</target>
|
||||||
<note>Subscription service name</note>
|
<note>Subscription service name</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Push" xml:space="preserve">
|
||||||
|
<source>Push</source>
|
||||||
|
<target>Push</target>
|
||||||
|
<note>Option for notification mode setting: Push notification mode</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="QR Code" xml:space="preserve">
|
<trans-unit id="QR Code" xml:space="preserve">
|
||||||
<source>QR Code</source>
|
<source>QR Code</source>
|
||||||
<target>QR Code</target>
|
<target>QR Code</target>
|
||||||
@@ -1590,6 +1662,11 @@ Button label to dismiss an error dialog</note>
|
|||||||
<target>Ran to suspension.</target>
|
<target>Ran to suspension.</target>
|
||||||
<note>Indication that a NostrScript was run until it reached a suspended state.</note>
|
<note>Indication that a NostrScript was run until it reached a suspended state.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="React with default reaction emoji" xml:space="preserve">
|
||||||
|
<source>React with default reaction emoji</source>
|
||||||
|
<target>React with default reaction emoji</target>
|
||||||
|
<note>Accessibility label for react button</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Reactions" xml:space="preserve">
|
<trans-unit id="Reactions" xml:space="preserve">
|
||||||
<source>Reactions</source>
|
<source>Reactions</source>
|
||||||
<target>Reactions</target>
|
<target>Reactions</target>
|
||||||
@@ -1700,6 +1777,11 @@ Button label to dismiss an error dialog</note>
|
|||||||
<target>Repost</target>
|
<target>Repost</target>
|
||||||
<note>Button to repost a note</note>
|
<note>Button to repost a note</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Repost or quote this note" xml:space="preserve">
|
||||||
|
<source>Repost or quote this note</source>
|
||||||
|
<target>Repost or quote this note</target>
|
||||||
|
<note>Accessibility label for repost/quote button</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Reposted" xml:space="preserve">
|
<trans-unit id="Reposted" xml:space="preserve">
|
||||||
<source>Reposted</source>
|
<source>Reposted</source>
|
||||||
<target>Reposted</target>
|
<target>Reposted</target>
|
||||||
@@ -1720,7 +1802,12 @@ Button label to dismiss an error dialog</note>
|
|||||||
<trans-unit id="Requests" xml:space="preserve">
|
<trans-unit id="Requests" xml:space="preserve">
|
||||||
<source>Requests</source>
|
<source>Requests</source>
|
||||||
<target>Requests</target>
|
<target>Requests</target>
|
||||||
<note>Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet). DM is the English abbreviation for Direct Message.</note>
|
<note>Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Reset contact list" xml:space="preserve">
|
||||||
|
<source>Reset contact list</source>
|
||||||
|
<target>Reset contact list</target>
|
||||||
|
<note>Button to reset contact list.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Retry" xml:space="preserve">
|
<trans-unit id="Retry" xml:space="preserve">
|
||||||
<source>Retry</source>
|
<source>Retry</source>
|
||||||
@@ -1777,6 +1864,11 @@ Button label to dismiss an error dialog</note>
|
|||||||
<target>Save Key in Secure Keychain</target>
|
<target>Save Key in Secure Keychain</target>
|
||||||
<note>Toggle to save private key to the Apple secure keychain.</note>
|
<note>Toggle to save private key to the Apple secure keychain.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Save your login info?" xml:space="preserve">
|
||||||
|
<source>Save your login info?</source>
|
||||||
|
<target>Save your login info?</target>
|
||||||
|
<note>Ask user if they want to save their account information.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Scan Code" xml:space="preserve">
|
<trans-unit id="Scan Code" xml:space="preserve">
|
||||||
<source>Scan Code</source>
|
<source>Scan Code</source>
|
||||||
<target>Scan Code</target>
|
<target>Scan Code</target>
|
||||||
@@ -1907,6 +1999,11 @@ Button label to dismiss an error dialog</note>
|
|||||||
<target>Share Via...</target>
|
<target>Share Via...</target>
|
||||||
<note>Button to present iOS share sheet</note>
|
<note>Button to present iOS share sheet</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Share externally" xml:space="preserve">
|
||||||
|
<source>Share externally</source>
|
||||||
|
<target>Share externally</target>
|
||||||
|
<note>Accessibility label for external share button</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Show" xml:space="preserve">
|
<trans-unit id="Show" xml:space="preserve">
|
||||||
<source>Show</source>
|
<source>Show</source>
|
||||||
<target>Show</target>
|
<target>Show</target>
|
||||||
@@ -1954,6 +2051,11 @@ Button label to dismiss an error dialog</note>
|
|||||||
<target>Show wallet selector</target>
|
<target>Show wallet selector</target>
|
||||||
<note>Toggle to show or hide selection of wallet.</note>
|
<note>Toggle to show or hide selection of wallet.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Sign In" xml:space="preserve">
|
||||||
|
<source>Sign In</source>
|
||||||
|
<target>Sign In</target>
|
||||||
|
<note>Button to continue to login page.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Sign Out" xml:space="preserve">
|
<trans-unit id="Sign Out" xml:space="preserve">
|
||||||
<source>Sign Out</source>
|
<source>Sign Out</source>
|
||||||
<target>Sign Out</target>
|
<target>Sign Out</target>
|
||||||
@@ -1974,11 +2076,6 @@ Button label to dismiss an error dialog</note>
|
|||||||
<target>Skip</target>
|
<target>Skip</target>
|
||||||
<note>Button to dismiss the suggested users screen</note>
|
<note>Button to dismiss the suggested users screen</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken" xml:space="preserve">
|
|
||||||
<source>Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken</source>
|
|
||||||
<target>Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken</target>
|
|
||||||
<note>Description about why Nostr is needed.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Someone posted a note" xml:space="preserve">
|
<trans-unit id="Someone posted a note" xml:space="preserve">
|
||||||
<source>Someone posted a note</source>
|
<source>Someone posted a note</source>
|
||||||
<target>Someone posted a note</target>
|
<target>Someone posted a note</target>
|
||||||
@@ -2087,11 +2184,6 @@ Enjoy!</target>
|
|||||||
<target>The address should either begin with LNURL or should look like an email address.</target>
|
<target>The address should either begin with LNURL or should look like an email address.</target>
|
||||||
<note>Giving the description of the alert message.</note>
|
<note>Giving the description of the alert message.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The go-to iOS Nostr client" xml:space="preserve">
|
|
||||||
<source>The go-to iOS Nostr client</source>
|
|
||||||
<target>The go-to iOS Nostr client</target>
|
|
||||||
<note>Quick description of what Damus is</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="The relay you are trying to add is already added. You're all set!" xml:space="preserve">
|
<trans-unit id="The relay you are trying to add is already added. You're all set!" xml:space="preserve">
|
||||||
<source>The relay you are trying to add is already added.
|
<source>The relay you are trying to add is already added.
|
||||||
You're all set!</source>
|
You're all set!</source>
|
||||||
@@ -2099,6 +2191,11 @@ You're all set!</source>
|
|||||||
You're all set!</target>
|
You're all set!</target>
|
||||||
<note>An error message that appears when the user attempts to add a relay that has already been added.</note>
|
<note>An error message that appears when the user attempts to add a relay that has already been added.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="The social network you control" xml:space="preserve">
|
||||||
|
<source>The social network you control</source>
|
||||||
|
<target>The social network you control</target>
|
||||||
|
<note>Quick description of what Damus is</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@" xml:space="preserve">
|
<trans-unit id="There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@" xml:space="preserve">
|
||||||
<source>There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@</source>
|
<source>There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@</source>
|
||||||
<target>There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@</target>
|
<target>There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@</target>
|
||||||
@@ -2128,16 +2225,16 @@ Nice to meet you all! #introductions #plebchain </source>
|
|||||||
Nice to meet you all! #introductions #plebchain </target>
|
Nice to meet you all! #introductions #plebchain </target>
|
||||||
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
|
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" xml:space="preserve">
|
|
||||||
<source>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</source>
|
|
||||||
<target>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</target>
|
|
||||||
<note>Label to describe that a private key is the user's secret account key and what they should do with it.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="This note contains too many items and cannot be rendered" xml:space="preserve">
|
<trans-unit id="This note contains too many items and cannot be rendered" xml:space="preserve">
|
||||||
<source>This note contains too many items and cannot be rendered</source>
|
<source>This note contains too many items and cannot be rendered</source>
|
||||||
<target>This note contains too many items and cannot be rendered</target>
|
<target>This note contains too many items and cannot be rendered</target>
|
||||||
<note>Error message indicating that a note is too big and cannot be rendered</note>
|
<note>Error message indicating that a note is too big and cannot be rendered</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?" xml:space="preserve">
|
||||||
|
<source>This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?</source>
|
||||||
|
<target>This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?</target>
|
||||||
|
<note>Comment explaining why a user cannot be zapped.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Thread" xml:space="preserve">
|
<trans-unit id="Thread" xml:space="preserve">
|
||||||
<source>Thread</source>
|
<source>Thread</source>
|
||||||
<target>Thread</target>
|
<target>Thread</target>
|
||||||
@@ -2275,6 +2372,11 @@ Nice to meet you all! #introductions #plebchain </target>
|
|||||||
<target>User muted</target>
|
<target>User muted</target>
|
||||||
<note>Alert message to indicate the user has been muted</note>
|
<note>Alert message to indicate the user has been muted</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="User not zappable" xml:space="preserve">
|
||||||
|
<source>User not zappable</source>
|
||||||
|
<target>User not zappable</target>
|
||||||
|
<note>Headline indicating a user cannot be zapped</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Username" xml:space="preserve">
|
<trans-unit id="Username" xml:space="preserve">
|
||||||
<source>Username</source>
|
<source>Username</source>
|
||||||
<target>Username</target>
|
<target>Username</target>
|
||||||
@@ -2342,6 +2444,15 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
ARE YOU SURE YOU WANT TO CONTINUE?</target>
|
ARE YOU SURE YOU WANT TO CONTINUE?</target>
|
||||||
<note>Alert for deleting the users account.</note>
|
<note>Alert for deleting the users account.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="WARNING: This will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY." xml:space="preserve">
|
||||||
|
<source>WARNING:
|
||||||
|
|
||||||
|
This will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.</source>
|
||||||
|
<target>WARNING:
|
||||||
|
|
||||||
|
This will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.</target>
|
||||||
|
<note>Alert for resetting the user's contact list.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Wake up, %@" xml:space="preserve">
|
<trans-unit id="Wake up, %@" xml:space="preserve">
|
||||||
<source>Wake up, %@</source>
|
<source>Wake up, %@</source>
|
||||||
<target>Wake up, %@</target>
|
<target>Wake up, %@</target>
|
||||||
@@ -2365,6 +2476,16 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
<target>Wallet Relay</target>
|
<target>Wallet Relay</target>
|
||||||
<note>Label text indicating that below it is the information about the wallet relay.</note>
|
<note>Label text indicating that below it is the information about the wallet relay.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)" xml:space="preserve">
|
||||||
|
<source>We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)</source>
|
||||||
|
<target>We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)</target>
|
||||||
|
<note>Message indicating that no First Aid actions are available.</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="We'll save your account key, so you won't need to enter it manually next time you log in." xml:space="preserve">
|
||||||
|
<source>We'll save your account key, so you won't need to enter it manually next time you log in.</source>
|
||||||
|
<target>We'll save your account key, so you won't need to enter it manually next time you log in.</target>
|
||||||
|
<note>Reminder to user that they should save their account information.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Website" xml:space="preserve">
|
<trans-unit id="Website" xml:space="preserve">
|
||||||
<source>Website</source>
|
<source>Website</source>
|
||||||
<target>Website</target>
|
<target>Website</target>
|
||||||
@@ -2405,21 +2526,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
<target>What do you want to report?</target>
|
<target>What do you want to report?</target>
|
||||||
<note>Header text to prompt user what issue they want to report.</note>
|
<note>Header text to prompt user what issue they want to report.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="What is Nostr?" xml:space="preserve">
|
|
||||||
<source>What is Nostr?</source>
|
|
||||||
<target>What is Nostr?</target>
|
|
||||||
<note>Heading text for section describing what is Nostr.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Who to Follow" xml:space="preserve">
|
<trans-unit id="Who to Follow" xml:space="preserve">
|
||||||
<source>Who to Follow</source>
|
<source>Who to Follow</source>
|
||||||
<target>Who to Follow</target>
|
<target>Who to Follow</target>
|
||||||
<note>Title for a screen displaying suggestions of who to follow</note>
|
<note>Title for a screen displaying suggestions of who to follow</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Why we need Nostr?" xml:space="preserve">
|
|
||||||
<source>Why we need Nostr?</source>
|
|
||||||
<target>Why we need Nostr?</target>
|
|
||||||
<note>Heading text for section describing why Nostr is needed.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Words" xml:space="preserve">
|
<trans-unit id="Words" xml:space="preserve">
|
||||||
<source>Words</source>
|
<source>Words</source>
|
||||||
<target>Words</target>
|
<target>Words</target>
|
||||||
@@ -2530,7 +2641,8 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
<trans-unit id="Zaps" xml:space="preserve">
|
<trans-unit id="Zaps" xml:space="preserve">
|
||||||
<source>Zaps</source>
|
<source>Zaps</source>
|
||||||
<target>Zaps</target>
|
<target>Zaps</target>
|
||||||
<note>Navigation bar title for the Zaps view.
|
<note>Label for filter for zap notifications.
|
||||||
|
Navigation bar title for the Zaps view.
|
||||||
Navigation title for zap settings.
|
Navigation title for zap settings.
|
||||||
Section header for zap settings
|
Section header for zap settings
|
||||||
Setting to enable Zap Local Notification
|
Setting to enable Zap Local Notification
|
||||||
@@ -2647,6 +2759,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
<target>%@ and %@ reposted your profile</target>
|
<target>%@ and %@ reposted your profile</target>
|
||||||
<note>Notification that 2 users reposted the current user's profile</note>
|
<note>Notification that 2 users reposted the current user's profile</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="required" xml:space="preserve">
|
||||||
|
<source>required</source>
|
||||||
|
<target>required</target>
|
||||||
|
<note>Label indicating that a form input is required.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="self" xml:space="preserve">
|
<trans-unit id="self" xml:space="preserve">
|
||||||
<source>self</source>
|
<source>self</source>
|
||||||
<target>self</target>
|
<target>self</target>
|
||||||
@@ -2701,7 +2818,7 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
</file>
|
</file>
|
||||||
<file original="damus/en-US.lproj/Localizable.stringsdict" source-language="en-US" target-language="en-US" datatype="plaintext">
|
<file original="damus/en-US.lproj/Localizable.stringsdict" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||||
<header>
|
<header>
|
||||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
|
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||||
</header>
|
</header>
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="/followed_by_three_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
|
<trans-unit id="/followed_by_three_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
|
||||||
@@ -3083,7 +3200,7 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
</file>
|
</file>
|
||||||
<file original="DamusNotificationService/InfoPlist.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
<file original="DamusNotificationService/InfoPlist.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||||
<header>
|
<header>
|
||||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
|
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||||
</header>
|
</header>
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||||
@@ -3105,7 +3222,7 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
</file>
|
</file>
|
||||||
<file original="DamusNotificationService/Localizable.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
<file original="DamusNotificationService/Localizable.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||||
<header>
|
<header>
|
||||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
|
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
|
||||||
</header>
|
</header>
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="" xml:space="preserve">
|
<trans-unit id="" xml:space="preserve">
|
||||||
@@ -3158,6 +3275,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
<target state="new">LibreTranslate (Open Source)</target>
|
<target state="new">LibreTranslate (Open Source)</target>
|
||||||
<note>Dropdown option for selecting LibreTranslate as the translation service.</note>
|
<note>Dropdown option for selecting LibreTranslate as the translation service.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Local" xml:space="preserve">
|
||||||
|
<source>Local</source>
|
||||||
|
<target state="new">Local</target>
|
||||||
|
<note>Option for notification mode setting: Local notification mode</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Local default" xml:space="preserve">
|
<trans-unit id="Local default" xml:space="preserve">
|
||||||
<source>Local default</source>
|
<source>Local default</source>
|
||||||
<target state="new">Local default</target>
|
<target state="new">Local default</target>
|
||||||
@@ -3168,6 +3290,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
<target state="new">Mentioned by %@</target>
|
<target state="new">Mentioned by %@</target>
|
||||||
<note>Mentioned by heading in local notification</note>
|
<note>Mentioned by heading in local notification</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Muted event" xml:space="preserve">
|
||||||
|
<source>Muted event</source>
|
||||||
|
<target state="new">Muted event</target>
|
||||||
|
<note>Title for a push notification which has been muted</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="New encrypted direct message" xml:space="preserve">
|
<trans-unit id="New encrypted direct message" xml:space="preserve">
|
||||||
<source>New encrypted direct message</source>
|
<source>New encrypted direct message</source>
|
||||||
<target state="new">New encrypted direct message</target>
|
<target state="new">New encrypted direct message</target>
|
||||||
@@ -3203,6 +3330,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
<target state="new">Production</target>
|
<target state="new">Production</target>
|
||||||
<note>Label indicating the production environment for Damus Purple</note>
|
<note>Label indicating the production environment for Damus Purple</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Push" xml:space="preserve">
|
||||||
|
<source>Push</source>
|
||||||
|
<target state="new">Push</target>
|
||||||
|
<note>Option for notification mode setting: Push notification mode</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Reposted by %@" xml:space="preserve">
|
<trans-unit id="Reposted by %@" xml:space="preserve">
|
||||||
<source>Reposted by %@</source>
|
<source>Reposted by %@</source>
|
||||||
<target state="new">Reposted by %@</target>
|
<target state="new">Reposted by %@</target>
|
||||||
@@ -3238,6 +3370,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
<target state="new">Test (local)</target>
|
<target state="new">Test (local)</target>
|
||||||
<note>Label indicating a local test environment for Damus Purple functionality (Developer feature)</note>
|
<note>Label indicating a local test environment for Damus Purple functionality (Developer feature)</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences" xml:space="preserve">
|
||||||
|
<source>This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences</source>
|
||||||
|
<target state="new">This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences</target>
|
||||||
|
<note>Description for a push notification which has been muted, and explanation that we cannot suppress it</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="This note contains too many items and cannot be rendered" xml:space="preserve">
|
<trans-unit id="This note contains too many items and cannot be rendered" xml:space="preserve">
|
||||||
<source>This note contains too many items and cannot be rendered</source>
|
<source>This note contains too many items and cannot be rendered</source>
|
||||||
<target state="new">This note contains too many items and cannot be rendered</target>
|
<target state="new">This note contains too many items and cannot be rendered</target>
|
||||||
|
|||||||
@@ -39,12 +39,18 @@
|
|||||||
"LibreTranslate (Open Source)" : {
|
"LibreTranslate (Open Source)" : {
|
||||||
"comment" : "Dropdown option for selecting LibreTranslate as the translation service."
|
"comment" : "Dropdown option for selecting LibreTranslate as the translation service."
|
||||||
},
|
},
|
||||||
|
"Local" : {
|
||||||
|
"comment" : "Option for notification mode setting: Local notification mode"
|
||||||
|
},
|
||||||
"Local default" : {
|
"Local default" : {
|
||||||
"comment" : "Dropdown option label for system default for Lightning wallet."
|
"comment" : "Dropdown option label for system default for Lightning wallet."
|
||||||
},
|
},
|
||||||
"Mentioned by %@" : {
|
"Mentioned by %@" : {
|
||||||
"comment" : "Mentioned by heading in local notification"
|
"comment" : "Mentioned by heading in local notification"
|
||||||
},
|
},
|
||||||
|
"Muted event" : {
|
||||||
|
"comment" : "Title for a push notification which has been muted"
|
||||||
|
},
|
||||||
"New encrypted direct message" : {
|
"New encrypted direct message" : {
|
||||||
"comment" : "Notification that the user has received a new direct message"
|
"comment" : "Notification that the user has received a new direct message"
|
||||||
},
|
},
|
||||||
@@ -78,6 +84,9 @@
|
|||||||
"Production" : {
|
"Production" : {
|
||||||
"comment" : "Label indicating the production environment for Damus Purple"
|
"comment" : "Label indicating the production environment for Damus Purple"
|
||||||
},
|
},
|
||||||
|
"Push" : {
|
||||||
|
"comment" : "Option for notification mode setting: Push notification mode"
|
||||||
|
},
|
||||||
"Reposted by %@" : {
|
"Reposted by %@" : {
|
||||||
"comment" : "Reposted by heading in local notification"
|
"comment" : "Reposted by heading in local notification"
|
||||||
},
|
},
|
||||||
@@ -99,6 +108,9 @@
|
|||||||
"Test (local)" : {
|
"Test (local)" : {
|
||||||
"comment" : "Label indicating a local test environment for Damus Purple functionality (Developer feature)"
|
"comment" : "Label indicating a local test environment for Damus Purple functionality (Developer feature)"
|
||||||
},
|
},
|
||||||
|
"This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences" : {
|
||||||
|
"comment" : "Description for a push notification which has been muted, and explanation that we cannot suppress it"
|
||||||
|
},
|
||||||
"This note contains too many items and cannot be rendered" : {
|
"This note contains too many items and cannot be rendered" : {
|
||||||
"comment" : "Error message indicating that a note is too big and cannot be rendered"
|
"comment" : "Error message indicating that a note is too big and cannot be rendered"
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
@@ -3,10 +3,10 @@
|
|||||||
"project" : "damus.xcodeproj",
|
"project" : "damus.xcodeproj",
|
||||||
"targetLocale" : "en-US",
|
"targetLocale" : "en-US",
|
||||||
"toolInfo" : {
|
"toolInfo" : {
|
||||||
"toolBuildNumber" : "15E204a",
|
"toolBuildNumber" : "15F31d",
|
||||||
"toolID" : "com.apple.dt.xcode",
|
"toolID" : "com.apple.dt.xcode",
|
||||||
"toolName" : "Xcode",
|
"toolName" : "Xcode",
|
||||||
"toolVersion" : "15.3"
|
"toolVersion" : "15.4"
|
||||||
},
|
},
|
||||||
"version" : "1.0"
|
"version" : "1.0"
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -226,6 +226,22 @@
|
|||||||
<string>Megosztások</string>
|
<string>Megosztások</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>quoted_reposts_count</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@QUOTE_REPOSTS@</string>
|
||||||
|
<key>QUOTE_REPOSTS</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>d</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>Idézet</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>Idézetek</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>sats</key>
|
<key>sats</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
|||||||
@@ -198,6 +198,20 @@
|
|||||||
<string>リポスト</string>
|
<string>リポスト</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>quoted_reposts_count</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@QUOTE_REPOSTS@</string>
|
||||||
|
<key>QUOTE_REPOSTS</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>d</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>引用</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>sats</key>
|
<key>sats</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -226,6 +226,22 @@
|
|||||||
<string>Delningar</string>
|
<string>Delningar</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>quoted_reposts_count</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@QUOTE_REPOSTS@</string>
|
||||||
|
<key>QUOTE_REPOSTS</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>d</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>Citat</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>Citat</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>sats</key>
|
<key>sats</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user