diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 96fda0ed0..70fa2e57b 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 030030A12C416B0B009A65FF /* RefreshPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030030A02C416B0B009A65FF /* RefreshPopupView.swift */; }; 030050D32D109B7E002B1E99 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030050D22D109B7E002B1E99 /* ReportView.swift */; }; 030050D52D10AE30002B1E99 /* Report+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030050D42D10AE30002B1E99 /* Report+Extensions.swift */; }; + 030056A42D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030056A32D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift */; }; + 030056A62D7DBD4F00EB0BA3 /* Sharable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030056A52D7DBD4F00EB0BA3 /* Sharable+Extensions.swift */; }; + 030056BB2D7E137800EB0BA3 /* ShareInstancePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030056BA2D7E137800EB0BA3 /* ShareInstancePickerView.swift */; }; 03036C742C71408700C6DA1D /* CounterAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03036C732C71408700C6DA1D /* CounterAppearance.swift */; }; 03036C762C71427B00C6DA1D /* InteractionBarCounterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03036C752C71427B00C6DA1D /* InteractionBarCounterLabelView.swift */; }; 03036C832C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03036C822C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift */; }; @@ -61,7 +64,6 @@ 0325B93E2D3AAE9E00E28B97 /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0325B93D2D3AAE9E00E28B97 /* SettingsHeaderView.swift */; }; 03267D822BED489C009D6268 /* AvatarBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03267D812BED489C009D6268 /* AvatarBannerView.swift */; }; 03267D842BED49CE009D6268 /* AccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03267D832BED49CE009D6268 /* AccountSettingsView.swift */; }; - 032C32022C3438DB00595286 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C32012C3438DB00595286 /* ShareAction.swift */; }; 032C32042C3439C600595286 /* ActorIdentifiable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C32032C3439C600595286 /* ActorIdentifiable+Extensions.swift */; }; 032C32082C34469900595286 /* SelectableContentProviding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C32072C34469900595286 /* SelectableContentProviding+Extensions.swift */; }; 032C320A2C34495D00595286 /* SelectTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032C32092C34495D00595286 /* SelectTextView.swift */; }; @@ -538,6 +540,9 @@ 030030A02C416B0B009A65FF /* RefreshPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshPopupView.swift; sourceTree = ""; }; 030050D22D109B7E002B1E99 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = ""; }; 030050D42D10AE30002B1E99 /* Report+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Report+Extensions.swift"; sourceTree = ""; }; + 030056A32D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingLinksSettingsView.swift; sourceTree = ""; }; + 030056A52D7DBD4F00EB0BA3 /* Sharable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sharable+Extensions.swift"; sourceTree = ""; }; + 030056BA2D7E137800EB0BA3 /* ShareInstancePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareInstancePickerView.swift; sourceTree = ""; }; 03036C732C71408700C6DA1D /* CounterAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterAppearance.swift; sourceTree = ""; }; 03036C752C71427B00C6DA1D /* InteractionBarCounterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionBarCounterLabelView.swift; sourceTree = ""; }; 03036C822C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InteractionBarEditorView+Logic.swift"; sourceTree = ""; }; @@ -587,7 +592,6 @@ 0325B93D2D3AAE9E00E28B97 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = ""; }; 03267D812BED489C009D6268 /* AvatarBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarBannerView.swift; sourceTree = ""; }; 03267D832BED49CE009D6268 /* AccountSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsView.swift; sourceTree = ""; }; - 032C32012C3438DB00595286 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = ""; }; 032C32032C3439C600595286 /* ActorIdentifiable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActorIdentifiable+Extensions.swift"; sourceTree = ""; }; 032C32072C34469900595286 /* SelectableContentProviding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SelectableContentProviding+Extensions.swift"; sourceTree = ""; }; 032C32092C34495D00595286 /* SelectTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTextView.swift; sourceTree = ""; }; @@ -1148,6 +1152,7 @@ 03531EEB2C2D81DC004A3464 /* LinkSettingsView.swift */, 03A630EE2D497143009A47A6 /* TappableLinksSettingsView.swift */, 03A630EC2D497005009A47A6 /* ExternalLinkSettingsView.swift */, + 030056A32D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift */, 038028D72CACAB960091A8A2 /* ModeratorSettingsView.swift */, 03F6BDF72D555F6E006A425E /* ModMailInteractionBarSettingsView.swift */, 03A6316C2D4E3D24009A47A6 /* ModeratorActionSeparationSettingsView.swift */, @@ -1534,7 +1539,6 @@ 0397D4A32C6FBC04002C6CDC /* ActionAppearance+StaticValues.swift */, 03036C732C71408700C6DA1D /* CounterAppearance.swift */, 03D3A1F02BB9D48E009DE55E /* BasicAction.swift */, - 032C32012C3438DB00595286 /* ShareAction.swift */, 03D3A1F22BB9D49B009DE55E /* ActionGroup.swift */, 0389DDDA2C3AB6340005B808 /* ActionBuilder.swift */, 03D3A1E42BB8B7A3009DE55E /* ActionType.swift */, @@ -1866,6 +1870,7 @@ 0305EBB12D35C1B70066E5AD /* RegistrationApplicationView.swift */, 03531EEF2C2DA291004A3464 /* Search */, 032C32092C34495D00595286 /* SelectTextView.swift */, + 030056BA2D7E137800EB0BA3 /* ShareInstancePickerView.swift */, 03500C252BF694A800CAA076 /* Toast */, CD0E07002C12707700445849 /* ToolbarEllipsisMenu.swift */, CDD99C3D2C73F4380010367F /* WarningView.swift */, @@ -2105,6 +2110,7 @@ children = ( CD87BEBB2D5125750099F190 /* Instance2Providing+Extensions.swift */, 032C32032C3439C600595286 /* ActorIdentifiable+Extensions.swift */, + 030056A52D7DBD4F00EB0BA3 /* Sharable+Extensions.swift */, 0305EBAB2D32C9300066E5AD /* ApiModlogActionType+Extensions.swift */, 0320B6532C8B65EB00D38548 /* Captcha+Extensions.swift */, 033F84BE2C2ACC9F002E3EDF /* Comment1Providing+Extensions.swift */, @@ -2508,7 +2514,6 @@ CDA683F82C77E577000C4486 /* NsfwBlurBehavior.swift in Sources */, 03B25B2F2CC43F8600EB6DF5 /* InstanceSafetyView.swift in Sources */, 03E0EF452CA74036002CB66C /* CommentPage.swift in Sources */, - 032C32022C3438DB00595286 /* ShareAction.swift in Sources */, 030BCB1B2C3EA5FD0037680F /* InstanceDetailsView.swift in Sources */, 03134A582BEC1C46002662CC /* AccountListSettingsView.swift in Sources */, 0397D4622C676B46002C6CDC /* ApiSortType+Extensions.swift in Sources */, @@ -2518,6 +2523,7 @@ 03B25B352CC4446400EB6DF5 /* FediseerOpinionListView.swift in Sources */, CDB2EC7D2BFADAB300DBC0EF /* CompactPostView.swift in Sources */, CD33CA522D3C18BF00106C8C /* ImageViewer+Views.swift in Sources */, + 030056A42D7DB05A00EB0BA3 /* SharingLinksSettingsView.swift in Sources */, 0315B1C12C74C71A006D4F82 /* CommentEditorView+Context.swift in Sources */, 03D283FE2D25EEC500A6659B /* SearchView+Views.swift in Sources */, 03AFD0DF2C3B2E000054B8AD /* PersonListRow.swift in Sources */, @@ -2804,6 +2810,7 @@ 0320B64F2C8A638A00D38548 /* SignUpView.swift in Sources */, 036FFA2D2D45110C00998D8A /* ChangePasswordView.swift in Sources */, 03ECD7192C81195000D48BF6 /* PostEditorView+LinkView.swift in Sources */, + 030056BB2D7E137800EB0BA3 /* ShareInstancePickerView.swift in Sources */, 0320B6582C8BB3C400D38548 /* SignUpView+Views.swift in Sources */, CD17B24B2C19109A001E174B /* SwipeConfiguration.swift in Sources */, 03036C832C727D0500C6DA1D /* InteractionBarEditorView+Logic.swift in Sources */, @@ -2817,6 +2824,7 @@ CDF9EF332AB2845C003F885B /* Icons.swift in Sources */, 03AF91E32C1C616F00E56644 /* InteractionBarView.swift in Sources */, 0397D4802C693A88002C6CDC /* [BlockNode]+Extensions.swift in Sources */, + 030056A62D7DBD4F00EB0BA3 /* Sharable+Extensions.swift in Sources */, 03C0EAEF2CA8288A00B4B2A5 /* CapsuleButtonStyle.swift in Sources */, 03DD69422D4FDE8900F8950D /* Person+Mock.swift in Sources */, 03D284062D2AEE3A00A6659B /* TabBarSettingsView.swift in Sources */, @@ -3398,7 +3406,7 @@ repositoryURL = "https://github.com/mlemgroup/MlemMiddleware"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.83.0; + minimumVersion = 0.84.0; }; }; CD4368BF2AE23FD400BD8BD1 /* XCRemoteSwiftPackageReference "Semaphore" */ = { diff --git a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cd7382cc7..03326331c 100644 --- a/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mlem.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mlemgroup/MlemMiddleware", "state" : { - "revision" : "bd13585a4941ef04cad6943a3ed576c9f4904b11", - "version" : "0.83.0" + "revision" : "072a2431ae50ade0846fd782e67e3696d25e47ef", + "version" : "0.84.0" } }, { diff --git a/Mlem/App/Configuration/User Settings/CodableSettings.swift b/Mlem/App/Configuration/User Settings/CodableSettings.swift index 23e6dbaf2..1f33b1061 100644 --- a/Mlem/App/Configuration/User Settings/CodableSettings.swift +++ b/Mlem/App/Configuration/User Settings/CodableSettings.swift @@ -52,6 +52,7 @@ struct CodableSettings: Codable { // swiftlint:disable:this type_body_length var links_displayMode: TappableLinksDisplayMode var links_openInBrowser: Bool var links_readerMode: Bool + var links_shareMode: LinkSharingMode var links_embedLoops: Bool var links_tappableLinksDisplayMode: TappableLinksDisplayMode var menus_allModActions: Bool @@ -173,6 +174,7 @@ struct CodableSettings: Codable { // swiftlint:disable:this type_body_length self.links_displayMode = try container.decodeIfPresent(TappableLinksDisplayMode.self, forKey: .links_displayMode) ?? .contextual self.links_openInBrowser = try container.decodeIfPresent(Bool.self, forKey: .links_openInBrowser) ?? false self.links_readerMode = try container.decodeIfPresent(Bool.self, forKey: .links_readerMode) ?? false + self.links_shareMode = try container.decodeIfPresent(LinkSharingMode.self, forKey: .links_shareMode) ?? .myInstance self.links_tappableLinksDisplayMode = try container.decodeIfPresent(TappableLinksDisplayMode.self, forKey: .links_tappableLinksDisplayMode) ?? .contextual self.links_embedLoops = try container.decodeIfPresent(Bool.self, forKey: .links_embedLoops) ?? true self.menus_allModActions = try container.decodeIfPresent(Bool.self, forKey: .menus_allModActions) ?? false @@ -255,6 +257,7 @@ struct CodableSettings: Codable { // swiftlint:disable:this type_body_length self.links_displayMode = settings.tappableLinksDisplayMode self.links_openInBrowser = settings.openLinksInBrowser self.links_readerMode = settings.openLinksInReaderMode + self.links_shareMode = settings.linkSharingMode self.links_tappableLinksDisplayMode = settings.tappableLinksDisplayMode self.links_embedLoops = settings.embedLoops self.menus_allModActions = settings.showAllModActions diff --git a/Mlem/App/Configuration/User Settings/Settings.swift b/Mlem/App/Configuration/User Settings/Settings.swift index 5d03d5521..ca7875889 100644 --- a/Mlem/App/Configuration/User Settings/Settings.swift +++ b/Mlem/App/Configuration/User Settings/Settings.swift @@ -64,6 +64,7 @@ class Settings: ObservableObject { @AppStorage("links.openInBrowser") var openLinksInBrowser = false @AppStorage("links.readerMode") var openLinksInReaderMode = false @AppStorage("links.displayMode") var tappableLinksDisplayMode: TappableLinksDisplayMode = .contextual + @AppStorage("links.shareMode") var linkSharingMode: LinkSharingMode = .myInstance @AppStorage("links.embedLoops") var embedLoops: Bool = true @AppStorage("feed.markReadOnScroll") var markReadOnScroll: Bool = false @@ -160,6 +161,7 @@ class Settings: ObservableObject { confirmImageUploads = settings.behavior_confirmImageUploads openLinksInBrowser = settings.links_openInBrowser openLinksInReaderMode = settings.links_readerMode + linkSharingMode = settings.links_shareMode tappableLinksDisplayMode = settings.links_tappableLinksDisplayMode markReadOnScroll = settings.feed_markReadOnScroll showReadInFeed = settings.feed_showRead diff --git a/Mlem/App/Logic/ImageFunctions.swift b/Mlem/App/Logic/ImageFunctions.swift index 6c864e54c..a40e82cf1 100644 --- a/Mlem/App/Logic/ImageFunctions.swift +++ b/Mlem/App/Logic/ImageFunctions.swift @@ -33,7 +33,7 @@ func saveMedia(url: URL) async { func shareImage(url: URL, navigation: NavigationLayer) async { if let fileUrl = await downloadImageToFileSystem(url: url) { - navigation.shareInfo = .init(url: fileUrl) + navigation.model?.shareInfo = .init(url: fileUrl) } } diff --git a/Mlem/App/Models/Action/ShareAction.swift b/Mlem/App/Models/Action/ShareAction.swift deleted file mode 100644 index 58f81cb21..000000000 --- a/Mlem/App/Models/Action/ShareAction.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// BasicAction.swift -// Mlem -// -// Created by Sjmarf on 31/03/2024. -// - -import SwiftUI -import UIKit - -struct ShareAction: Action { - let id: String - let url: URL - let actions: [BasicAction] - - init(id: String, url: URL, actions: [BasicAction] = []) { - self.id = id - self.url = url - self.actions = actions - } - - var appearance: ActionAppearance { .share() } -} diff --git a/Mlem/App/Utility/Extensions/Content Models/ActorIdentifiable+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/ActorIdentifiable+Extensions.swift index da3a4f629..fc14d246b 100644 --- a/Mlem/App/Utility/Extensions/Content Models/ActorIdentifiable+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/ActorIdentifiable+Extensions.swift @@ -7,30 +7,9 @@ import Foundation import MlemMiddleware +import SwiftUI extension ActorIdentifiable { - func shareAction() -> ShareAction { - .init(id: "share\(actorId)", url: actorId.url, actions: [sendLinkInPrivateMessageAction()]) - } - - func sendLinkInPrivateMessageAction() -> BasicAction { - .init( - id: "sendLinkInPrivateMessage\(actorId)", - appearance: .init( - label: "Send to Lemmy User", - color: .themedAccent, - icon: Icons.personCircle - ), - callback: { - NavigationModel.main.openSheet(.personPicker(callback: { person, navigation in - navigation.push( - .messageFeed(person, messageContent: String(describing: actorId), focusTextField: true) - ) - })) - } - ) - } - func openInstanceAction(navigation: NavigationLayer?) -> BasicAction { let callback: (@MainActor () -> Void)? if let navigation { diff --git a/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift index b350a7cc6..ad9ca19b4 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Comment1Providing+Extensions.swift @@ -59,10 +59,16 @@ extension Comment1Providing { expanded: Bool = false, feedback: Set = [.haptic, .toast], showAllActions: Bool = true, + navigation: NavigationLayer?, commentTreeTracker: CommentTreeTracker? = nil, report: Report? = nil ) -> [any Action] { - basicMenuActions(appState: appState, feedback: feedback, commentTreeTracker: commentTreeTracker) + basicMenuActions( + appState: appState, + feedback: feedback, + navigation: navigation, + commentTreeTracker: commentTreeTracker + ) if canModerate { ActionGroup( appearance: .init(label: "Moderation...", color: .themedModeration, icon: Icons.moderation), @@ -77,6 +83,7 @@ extension Comment1Providing { func basicMenuActions( appState: AppState, feedback: Set = [.haptic, .toast], + navigation: NavigationLayer?, commentTreeTracker: CommentTreeTracker? = nil ) -> [any Action] { ActionGroup(displayMode: .compactSection) { @@ -87,7 +94,7 @@ extension Comment1Providing { if !deleted { selectTextAction() } - shareAction() + shareAction(navigation: navigation) if isOwnComment { editAction(appState: appState) @@ -135,6 +142,7 @@ extension Comment1Providing { func action( appState: AppState, type: CommentBarConfiguration.ActionType, + navigation: NavigationLayer?, commentTreeTracker: CommentTreeTracker? = nil, communityContext: (any CommunityStubProviding)? = nil, reportContext: Report? = nil @@ -144,7 +152,7 @@ extension Comment1Providing { case .downvote: api.downvotesEnabled ? downvoteAction(appState: appState, feedback: [.haptic]) : nil case .save: saveAction(appState: appState, feedback: [.haptic]) case .reply: replyAction(appState: appState, commentTreeTracker: commentTreeTracker) - case .share: shareAction() + case .share: shareAction(navigation: navigation) case .selectText: selectTextAction() case .report: reportAction(appState: appState, communityContext: communityContext) case .resolve: reportContext?.resolveAction(appState: appState, feedback: [.haptic]) diff --git a/Mlem/App/Utility/Extensions/Content Models/Community1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Community1Providing+Extensions.swift index 6c1c1d90d..f4be18e7e 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Community1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Community1Providing+Extensions.swift @@ -126,7 +126,7 @@ extension Community1Providing { favoriteAction(appState: appState, feedback: feedback) openInstanceAction(navigation: navigation) copyNameAction() - shareAction() + shareAction(navigation: navigation) blockAction(appState: appState, feedback: feedback) if api.isAdmin { ActionGroup { diff --git a/Mlem/App/Utility/Extensions/Content Models/InstanceStubProviding+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/InstanceStubProviding+Extensions.swift index d9eece503..863ead73c 100644 --- a/Mlem/App/Utility/Extensions/Content Models/InstanceStubProviding+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/InstanceStubProviding+Extensions.swift @@ -71,6 +71,7 @@ extension InstanceStubProviding { func menuActions( appState: AppState, feedback: Set = [.haptic, .toast], + navigation: NavigationLayer?, allowExternalBlocking: Bool = false ) -> [any Action] { ActionGroup { @@ -80,7 +81,7 @@ extension InstanceStubProviding { } ActionGroup { openInBrowserAction() - shareAction() + shareAction(navigation: navigation) } if !local || (allowExternalBlocking && actorId != AppState.main.firstApi.actorId) { ActionGroup { @@ -93,6 +94,12 @@ extension InstanceStubProviding { } } + func shareAction(navigation: NavigationLayer?) -> BasicAction { + .init(id: "share\(actorId)", appearance: .share()) { + navigation?.model?.shareInfo = .init(url: actorId.url) + } + } + func visitAction() -> BasicAction { .init( id: "visit\(actorId)", diff --git a/Mlem/App/Utility/Extensions/Content Models/Person1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Person1Providing+Extensions.swift index 79d082345..fbe02ca04 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Person1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Person1Providing+Extensions.swift @@ -100,7 +100,7 @@ extension Person1Providing { ActionGroup { openInstanceAction(navigation: navigation) copyNameAction() - shareAction() + shareAction(navigation: navigation) if (AppState.main.firstSession as? UserSession)?.person?.person1 !== person1 { if !isInMessageFeed { sendMessageAction() diff --git a/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift index 7c25a404a..3fd43c115 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Post1Providing+Extensions.swift @@ -139,7 +139,12 @@ extension Post1Providing { report: Report? = nil, commentTreeTracker: CommentTreeTracker? = nil ) -> [any Action] { - basicMenuActions(appState: appState, feedback: feedback, commentTreeTracker: commentTreeTracker) + basicMenuActions( + appState: appState, + feedback: feedback, + navigation: navigation, + commentTreeTracker: commentTreeTracker + ) if canModerate { ActionGroup( appearance: .init(label: "Moderation...", color: .themedModeration, icon: Icons.moderation), @@ -160,6 +165,7 @@ extension Post1Providing { func basicMenuActions( appState: AppState, feedback: Set = [.haptic, .toast], + navigation: NavigationLayer?, commentTreeTracker: CommentTreeTracker? = nil ) -> [any Action] { ActionGroup(displayMode: .compactSection) { @@ -170,7 +176,7 @@ extension Post1Providing { if !deleted { selectTextAction() } - shareAction() + shareAction(navigation: navigation) if isOwnPost { editAction(appState: appState) @@ -228,6 +234,7 @@ extension Post1Providing { // swiftlint:disable:next cyclomatic_complexity func action( appState: AppState, + navigation: NavigationLayer, type: PostBarConfiguration.ActionType, feedback: Set = [.haptic, .toast], commentTreeTracker: CommentTreeTracker? = nil, @@ -239,7 +246,7 @@ extension Post1Providing { case .downvote: api.downvotesEnabled ? downvoteAction(appState: appState, feedback: feedback) : nil case .save: saveAction(appState: appState, feedback: feedback) case .reply: replyAction(appState: appState, commentTreeTracker: commentTreeTracker) - case .share: shareAction() + case .share: shareAction(navigation: navigation) case .selectText: selectTextAction() case .hide: hideAction(appState: appState, feedback: feedback) case .block: blockAction(appState: appState, feedback: feedback) @@ -333,15 +340,6 @@ extension Post1Providing { // MARK: Actions - // Overrides the `ActorIdentifiable+Extensions` implementation - func shareAction() -> ShareAction { - .init( - id: "share\(actorId)", - url: actorId.url, - actions: [crossPostAction(), sendLinkInPrivateMessageAction()] - ) - } - func crossPostAction() -> BasicAction { .init( id: "crosspost\(uid)", diff --git a/Mlem/App/Utility/Extensions/Content Models/Reply1Providing+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Reply1Providing+Extensions.swift index 87a813a48..3e58fb429 100644 --- a/Mlem/App/Utility/Extensions/Content Models/Reply1Providing+Extensions.swift +++ b/Mlem/App/Utility/Extensions/Content Models/Reply1Providing+Extensions.swift @@ -31,7 +31,11 @@ extension Reply1Providing { } @ActionBuilder - func menuActions(appState: AppState, feedback: Set = [.haptic, .toast]) -> [any Action] { + func menuActions( + appState: AppState, + navigation: NavigationLayer, + feedback: Set = [.haptic, .toast] + ) -> [any Action] { ActionGroup(displayMode: .compactSection) { upvoteAction(appState: appState, feedback: feedback) downvoteAction(appState: appState, feedback: feedback) @@ -42,7 +46,7 @@ extension Reply1Providing { if !comment.deleted { comment.selectTextAction() } - comment.shareAction() + comment.shareAction(navigation: navigation) if !comment.deleted { reportAction(appState: appState) } diff --git a/Mlem/App/Utility/Extensions/Content Models/Sharable+Extensions.swift b/Mlem/App/Utility/Extensions/Content Models/Sharable+Extensions.swift new file mode 100644 index 000000000..6e7c28fcd --- /dev/null +++ b/Mlem/App/Utility/Extensions/Content Models/Sharable+Extensions.swift @@ -0,0 +1,53 @@ +// +// Sharable+Extensions.swift +// Mlem +// +// Created by Sjmarf on 2025-03-09. +// + +import Foundation +import MlemMiddleware +import SwiftUI + +extension Sharable { + func shareAction(navigation: NavigationLayer?) -> BasicAction { + .init(id: "share\(actorId)", appearance: .share(), callback: { + let url: URL? = switch Settings.main.linkSharingMode { + case .myInstance: self.url() + case .hostInstance: self.actorId.url + case .askEveryTime: nil + } + if let url, let navigation { + navigation.model?.shareInfo = .init(url: url, actions: self.shareSheetActions()) + } else { + navigation?.openSheet(.shareInstancePicker(self)) + } + }) + } + + func shareSheetActions() -> [BasicAction] { + var shareActions: [BasicAction] = [sendLinkInPrivateMessageAction()] + if let post = self as? any Post1Providing { + shareActions.prepend(post.crossPostAction()) + } + return shareActions + } + + func sendLinkInPrivateMessageAction() -> BasicAction { + .init( + id: "sendLinkInPrivateMessage\(actorId)", + appearance: .init( + label: "Send to Lemmy User", + color: .themedAccent, + icon: Icons.personCircle + ), + callback: { + NavigationModel.main.openSheet(.personPicker(callback: { person, navigation in + navigation.push( + .messageFeed(person, messageContent: String(describing: self.actorId), focusTextField: true) + ) + })) + } + ) + } +} diff --git a/Mlem/App/Utility/Extensions/EnvironmentValues+Extensions.swift b/Mlem/App/Utility/Extensions/EnvironmentValues+Extensions.swift index 94fe0fc78..06269ce38 100644 --- a/Mlem/App/Utility/Extensions/EnvironmentValues+Extensions.swift +++ b/Mlem/App/Utility/Extensions/EnvironmentValues+Extensions.swift @@ -20,3 +20,7 @@ extension EnvironmentValues { @Entry var loadingTracker: MediaLoadingTracker? } + +struct RootLayer { + let layer: NavigationLayer +} diff --git a/Mlem/App/Utility/Extensions/Views/View Modifiers/Quick Swipes/View+QuickSwipes.swift b/Mlem/App/Utility/Extensions/Views/View Modifiers/Quick Swipes/View+QuickSwipes.swift index 471b3a8b3..285aa7255 100644 --- a/Mlem/App/Utility/Extensions/Views/View Modifiers/Quick Swipes/View+QuickSwipes.swift +++ b/Mlem/App/Utility/Extensions/Views/View Modifiers/Quick Swipes/View+QuickSwipes.swift @@ -197,8 +197,6 @@ struct QuickSwipeView: ViewModifier { if let action = action as? BasicAction { action.callbackWithConfirmation(popupModel: popupModel) - } else if let action = action as? ShareAction { - navigation.shareInfo = .init(action) } else if let action = action as? ActionGroup { popupModel.showPopup(action) } diff --git a/Mlem/App/Views/Pages/Instance/InstanceView.swift b/Mlem/App/Views/Pages/Instance/InstanceView.swift index b6c2453eb..c992e170b 100644 --- a/Mlem/App/Views/Pages/Instance/InstanceView.swift +++ b/Mlem/App/Views/Pages/Instance/InstanceView.swift @@ -35,8 +35,8 @@ struct InstanceView: View { var uptimeRefreshTimer = Timer.publish(every: 30, tolerance: 0.5, on: .main, in: .common) .autoconnect() - @Environment(NavigationLayer.self) var navigation @Environment(AppState.self) var appState + @Environment(NavigationLayer.self) var navigation @Environment(\.palette) var palette @Environment(\.colorScheme) var colorScheme @@ -133,7 +133,11 @@ struct InstanceView: View { } } .toolbar { - ToolbarEllipsisMenu(instance.menuActions(appState: appState, allowExternalBlocking: true)) + ToolbarEllipsisMenu(instance.menuActions( + appState: appState, + navigation: navigation, + allowExternalBlocking: true + )) } } diff --git a/Mlem/App/Views/Root/ContentView+Logic.swift b/Mlem/App/Views/Root/ContentView+Logic.swift index 1c3b972a7..24180f7d3 100644 --- a/Mlem/App/Views/Root/ContentView+Logic.swift +++ b/Mlem/App/Views/Root/ContentView+Logic.swift @@ -31,7 +31,6 @@ extension ContentView { .circleMasked .withRenderingMode(.alwaysOriginal) - print("RENDER", colorPalette.palette.accent) let selectedAvatarImage = try await imageTask.image .resized(to: .init(width: imageTask.image.size.width / imageTask.image.size.height * 26, height: 26)) .circleBorder(color: .init(colorPalette.palette.accent), width: 3.5) diff --git a/Mlem/App/Views/Root/ContentView.swift b/Mlem/App/Views/Root/ContentView.swift index 6141cdec2..624a0b6f0 100644 --- a/Mlem/App/Views/Root/ContentView.swift +++ b/Mlem/App/Views/Root/ContentView.swift @@ -56,6 +56,8 @@ struct ContentView: View { } .navigationSheetModifiers( nextLayer: navigationModel.layers.first, + isTopSheet: navigationModel.layers.isEmpty, + shareInfo: .init(get: { navigationModel.shareInfo }, set: { navigationModel.shareInfo = $0 }), contentPickerTracker: navigationModel.contentPickerTracker ) .tint(.themedAccent) diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/FeedCommentView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/FeedCommentView.swift index c8fe4732f..bcde47e5e 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/FeedCommentView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/FeedCommentView.swift @@ -12,6 +12,7 @@ import SwiftUI struct FeedCommentView: View { @Environment(AppState.self) private var appState @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? + @Environment(NavigationLayer.self) var navigation @Environment(\.reportContext) var reportContext: Report? @Setting(\.postSize) var settingsPostSize @@ -39,6 +40,7 @@ struct FeedCommentView: View { .contextMenu { comment.allMenuActions( appState: appState, showAllActions: false, + navigation: navigation, commentTreeTracker: commentTreeTracker, report: reportContext ) } diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/TileCommentView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/TileCommentView.swift index 872b58084..c5416c8fb 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/TileCommentView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Comments/TileCommentView.swift @@ -12,6 +12,7 @@ import SwiftUI struct TileCommentView: View { @Environment(AppState.self) var appState + @Environment(NavigationLayer.self) var navigation @Environment(\.palette) var palette @Environment(\.parentFrameWidth) var parentFrameWidth: CGFloat @@ -100,7 +101,7 @@ struct TileCommentView: View { var score: some View { Menu { - ForEach(comment.allMenuActions(appState: appState), id: \.id) { action in + ForEach(comment.allMenuActions(appState: appState, navigation: navigation), id: \.id) { action in MenuButton(action: action) } } label: { diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostView.swift index 24be17d96..3fc10cfe5 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/HeadlinePostView.swift @@ -18,6 +18,7 @@ struct HeadlinePostView: View { @Environment(AppState.self) private var appState @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? + @Environment(NavigationLayer.self) var navigation @Environment(\.communityContext) var communityContext: (any Community1Providing)? @Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor @Environment(\.reportContext) private var reportContext: Report? @@ -85,6 +86,7 @@ struct HeadlinePostView: View { appState: appState, post: post, configuration: interactionBarConfiguration, + navigation: navigation, commentTreeTracker: commentTreeTracker, communityContext: communityContext, reportContext: reportContext diff --git a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostView.swift b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostView.swift index ecc915337..df7d63dbb 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/Feed Posts/LargePostView.swift @@ -19,6 +19,7 @@ struct LargePostView: View { @Environment(AppState.self) private var appState @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? + @Environment(NavigationLayer.self) var navigation @Environment(\.communityContext) private var communityContext @Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor @Environment(\.reportContext) private var reportContext: Report? @@ -96,6 +97,7 @@ struct LargePostView: View { appState: appState, post: post, configuration: interactionBarConfiguration, + navigation: navigation, commentTreeTracker: commentTreeTracker, communityContext: communityContext, reportContext: reportContext diff --git a/Mlem/App/Views/Root/Tabs/Settings/ErrorLogView.swift b/Mlem/App/Views/Root/Tabs/Settings/ErrorLogView.swift index b5a3fd9f1..41c4184a5 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/ErrorLogView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/ErrorLogView.swift @@ -35,7 +35,7 @@ struct ErrorLogView: View { fileName: "mlem_error_log.txt", text: errorsTracker.createErrorLog() ) { - navigation.shareInfo = .init(url: url) + navigation.model?.shareInfo = .init(url: url) } else { ToastModel.main.add(.failure(String("Failed to share error log"))) } diff --git a/Mlem/App/Views/Root/Tabs/Settings/ExternalLinkSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/ExternalLinkSettingsView.swift index 0a35ea4ed..bae004a81 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/ExternalLinkSettingsView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/ExternalLinkSettingsView.swift @@ -15,7 +15,7 @@ struct ExternalLinkSettingsView: View { Form { Section("Open External Links") { Picker("Open External Links", selection: $openLinksInBrowser) { - Label("In-App", systemImage: Icons.inApp).tag(false) + Label("In Mlem", systemImage: Icons.inApp).tag(false) Label("In Default Browser", systemImage: Icons.browser).tag(true) } .pickerStyle(.inline) diff --git a/Mlem/App/Views/Root/Tabs/Settings/FiltersSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/FiltersSettingsView.swift index 5df3a4650..f98ca4199 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/FiltersSettingsView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/FiltersSettingsView.swift @@ -46,7 +46,7 @@ struct FiltersSettingsView: View { fileName: "keywords.txt", text: filtersTracker.filteredKeywords.joined(separator: "\n") ) { - navigation.shareInfo = .init(url: url) + navigation.model?.shareInfo = .init(url: url) } else { ToastModel.main.add(.failure()) } diff --git a/Mlem/App/Views/Root/Tabs/Settings/ImportExportSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/ImportExportSettingsView.swift index 763ca22bf..655e1a39c 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/ImportExportSettingsView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/ImportExportSettingsView.swift @@ -76,7 +76,7 @@ struct ImportExportSettingsView: View { let data = try JSONEncoder().encode(Settings.main.codable) let fileUrl = FileManager.default.temporaryDirectory.appending(path: "settings.json") try data.write(to: fileUrl, options: .atomic) - navigation.shareInfo = .init(url: fileUrl) + navigation.model?.shareInfo = .init(url: fileUrl) } } diff --git a/Mlem/App/Views/Root/Tabs/Settings/LinkSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/LinkSettingsView.swift index 59832672b..e91bc53dc 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/LinkSettingsView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/LinkSettingsView.swift @@ -10,6 +10,7 @@ import SwiftUI struct LinkSettingsView: View { @Setting(\.openLinksInBrowser) var openLinksInBrowser @Setting(\.openLinksInReaderMode) var openLinksInReaderMode + @Setting(\.linkSharingMode) var linkSharingMode @Setting(\.tappableLinksDisplayMode) var tappableLinksDisplayMode @Setting(\.compactComments) var compactComments @Setting(\.embedLoops) var embedLoops @@ -32,6 +33,13 @@ struct LinkSettingsView: View { systemImage: "arrow.up.right", destination: .settings(.externalLinks) ) + NavigationLink( + "Share Links", + value: .init(localized: sharingLinksNavigationLinkValue), + fallbackValue: "", + systemImage: Icons.share, + destination: .settings(.sharingLinks) + ) NavigationLink( "Tappable Links", value: tappableLinksDisplayMode == .disabled ? "Off" : "On", @@ -66,7 +74,15 @@ struct LinkSettingsView: View { if openLinksInBrowser { "In Browser" } else { - openLinksInReaderMode ? "In Reader" : "In-App" + openLinksInReaderMode ? "In Reader" : "In Mlem" + } + } + + var sharingLinksNavigationLinkValue: LocalizedStringResource { + switch linkSharingMode { + case .myInstance: "My Instance" + case .hostInstance: "Host Instance" + case .askEveryTime: "Ask Every Time" } } } diff --git a/Mlem/App/Views/Root/Tabs/Settings/SharingLinksSettingsView.swift b/Mlem/App/Views/Root/Tabs/Settings/SharingLinksSettingsView.swift new file mode 100644 index 000000000..2d57dca5b --- /dev/null +++ b/Mlem/App/Views/Root/Tabs/Settings/SharingLinksSettingsView.swift @@ -0,0 +1,38 @@ +// +// SharingLinksSettingsView.swift +// Mlem +// +// Created by Sjmarf on 2025-03-09. +// + +import SwiftUI + +struct SharingLinksSettingsView: View { + @Setting(\.linkSharingMode) var linkSharingMode + + var body: some View { + Form { + Section("Share links using...") { + Picker("Share links using...", selection: $linkSharingMode) { + ForEach(LinkSharingMode.allCases, id: \.self) { mode in + Text(mode.label) + } + } + .labelsHidden() + .pickerStyle(.inline) + } + } + } +} + +enum LinkSharingMode: String, Codable, CaseIterable { + case myInstance, hostInstance, askEveryTime + + var label: LocalizedStringResource { + switch self { + case .myInstance: "My Instance" + case .hostInstance: "Host Instance" + case .askEveryTime: "Ask Every Time" + } + } +} diff --git a/Mlem/App/Views/Shared/CommentView.swift b/Mlem/App/Views/Shared/CommentView.swift index afc10ff48..4fa7c0fa3 100644 --- a/Mlem/App/Views/Shared/CommentView.swift +++ b/Mlem/App/Views/Shared/CommentView.swift @@ -12,6 +12,7 @@ import SwiftUI struct CommentView: View { @Environment(AppState.self) var appState @Environment(CommentTreeTracker.self) private var commentTreeTracker: CommentTreeTracker? + @Environment(NavigationLayer.self) var navigation @Environment(\.communityContext) var communityContext: (any Community1Providing)? @Environment(\.reportContext) private var reportContext: Report? @Environment(\.palette) private var palette @@ -115,6 +116,7 @@ struct CommentView: View { if !compact { InteractionBarView( appState: appState, + navigation: navigation, comment: comment, configuration: interactionBarConfiguration, commentTreeTracker: commentTreeTracker, @@ -152,13 +154,18 @@ struct CommentView: View { } } EllipsisMenu(size: 24) { - comment.basicMenuActions(appState: appState, commentTreeTracker: commentTreeTracker) + comment.basicMenuActions( + appState: appState, + navigation: navigation, + commentTreeTracker: commentTreeTracker + ) } } else { EllipsisMenu(size: 24) { comment.allMenuActions( appState: appState, showAllActions: !inFeed, + navigation: navigation, commentTreeTracker: commentTreeTracker, report: reportContext ) diff --git a/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Views.swift b/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Views.swift index 9237954db..73789d347 100644 --- a/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Views.swift +++ b/Mlem/App/Views/Shared/ExpandedPost/ExpandedPostView+Views.swift @@ -39,7 +39,7 @@ extension ExpandedPostView { depthOffset: tracker.proposedDepthOffset ) .quickSwipes(comment.swipeActions(appState: appState, behavior: .standard, commentTreeTracker: tracker)) - .contextMenu { comment.allMenuActions(appState: appState) } + .contextMenu { comment.allMenuActions(appState: appState, navigation: navigation) } .paletteBorder(cornerRadius: Constants.main.standardSpacing) .transition(.move(edge: .top).combined(with: .opacity)) .zIndex(1000 - Double(comment.depth)) diff --git a/Mlem/App/Views/Shared/Images/Wrappers/ThumbnailImageView.swift b/Mlem/App/Views/Shared/Images/Wrappers/ThumbnailImageView.swift index e5548bb88..a869bf730 100644 --- a/Mlem/App/Views/Shared/Images/Wrappers/ThumbnailImageView.swift +++ b/Mlem/App/Views/Shared/Images/Wrappers/ThumbnailImageView.swift @@ -148,7 +148,7 @@ struct ThumbnailImageView: View { func shareImage(url: URL) async { if let fileUrl = await downloadImageToFileSystem(url: url) { - navigation.shareInfo = .init(url: fileUrl) + navigation.model?.shareInfo = .init(url: fileUrl) } } } diff --git a/Mlem/App/Views/Shared/InteractionBar/InteractionBarView.swift b/Mlem/App/Views/Shared/InteractionBar/InteractionBarView.swift index 4e10f91ef..a19b09575 100644 --- a/Mlem/App/Views/Shared/InteractionBar/InteractionBarView.swift +++ b/Mlem/App/Views/Shared/InteractionBar/InteractionBarView.swift @@ -20,12 +20,14 @@ struct InteractionBarView: View { appState: AppState, post: any Post1Providing, configuration: PostBarConfiguration, + navigation: NavigationLayer, commentTreeTracker: CommentTreeTracker? = nil, communityContext: (any CommunityStubProviding)? = nil, reportContext: Report? = nil ) { self.leading = .init( appState: appState, + navigation: navigation, post: post, items: configuration.leading, commentTreeTracker: commentTreeTracker, @@ -34,6 +36,7 @@ struct InteractionBarView: View { ) self.trailing = .init( appState: appState, + navigation: navigation, post: post, items: configuration.trailing, commentTreeTracker: commentTreeTracker, @@ -45,6 +48,7 @@ struct InteractionBarView: View { init( appState: AppState, + navigation: NavigationLayer, comment: any Comment1Providing, configuration: CommentBarConfiguration, commentTreeTracker: CommentTreeTracker? = nil, @@ -53,6 +57,7 @@ struct InteractionBarView: View { ) { self.leading = .init( appState: appState, + navigation: navigation, comment: comment, items: configuration.leading, commentTreeTracker: commentTreeTracker, @@ -61,6 +66,7 @@ struct InteractionBarView: View { ) self.trailing = .init( appState: appState, + navigation: navigation, comment: comment, items: configuration.trailing, commentTreeTracker: commentTreeTracker, @@ -141,27 +147,19 @@ struct InteractionBarView: View { @ViewBuilder private func actionView(_ action: any Action) -> some View { Group { - if let action = action as? ShareAction { - Button { - navigation.shareInfo = .init(action) + if let action = action as? ActionGroup { + Menu { + ForEach(action.children, id: \.id) { child in + MenuButton(action: child) + } } label: { InteractionBarActionLabelView(action.appearance) + .opacity(action.disabled ? 0.5 : 1) } - } else { - if let action = action as? ActionGroup { - Menu { - ForEach(action.children, id: \.id) { child in - MenuButton(action: child) - } - } label: { - InteractionBarActionLabelView(action.appearance) - .opacity(action.disabled ? 0.5 : 1) - } - .onTapGesture {} - } else if let action = action as? BasicAction { - InteractionBarBasicButton(action: action) - .popupAnchor() - } + .onTapGesture {} + } else if let action = action as? BasicAction { + InteractionBarBasicButton(action: action) + .popupAnchor() } } .accessibilityLabel(action.appearance.label) @@ -227,6 +225,7 @@ private enum EnrichedWidget { extension [EnrichedWidget] { init( appState: AppState, + navigation: NavigationLayer, post: any Post1Providing, items: [PostBarConfiguration.Item], commentTreeTracker: CommentTreeTracker?, @@ -238,6 +237,7 @@ extension [EnrichedWidget] { case let .action(action): if let action = post.action( appState: appState, + navigation: navigation, type: action, commentTreeTracker: commentTreeTracker, communityContext: communityContext, @@ -256,6 +256,7 @@ extension [EnrichedWidget] { init( appState: AppState, + navigation: NavigationLayer, comment: any Comment1Providing, items: [CommentBarConfiguration.Item], commentTreeTracker: CommentTreeTracker?, @@ -268,6 +269,7 @@ extension [EnrichedWidget] { if let action = comment.action( appState: appState, type: action, + navigation: navigation, commentTreeTracker: commentTreeTracker, communityContext: communityContext, reportContext: reportContext diff --git a/Mlem/App/Views/Shared/MenuButton.swift b/Mlem/App/Views/Shared/MenuButton.swift index 60f9491a4..fe61b9fe7 100644 --- a/Mlem/App/Views/Shared/MenuButton.swift +++ b/Mlem/App/Views/Shared/MenuButton.swift @@ -32,12 +32,6 @@ struct MenuButton: View { } ) .disabled(action.disabled) - } else if let action = action as? ShareAction { - Button { - navigation.shareInfo = .init(action) - } label: { - Label("Share...", systemImage: Icons.share) - } } else if let action = action as? ActionGroup { switch action.displayMode { case .section: diff --git a/Mlem/App/Views/Shared/Navigation/NavigationLayer.swift b/Mlem/App/Views/Shared/Navigation/NavigationLayer.swift index b6d0ae70a..8c2f1e75f 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationLayer.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationLayer.swift @@ -11,27 +11,6 @@ import UniformTypeIdentifiers @Observable class NavigationLayer: Identifiable { - struct ShareInfo { - let url: URL - let activities: [ShareActivity] - - init(url: URL, activities: [ShareActivity] = []) { - self.url = url - self.activities = activities - } - - init(_ action: ShareAction) { - self.url = action.url - self.activities = action.actions.compactMap { action in - if let callback = action.callback { - .init(appearance: action.appearance, performAction: callback) - } else { - nil - } - } - } - } - var id: ObjectIdentifier { .init(self) } weak var model: NavigationModel? @@ -39,7 +18,6 @@ class NavigationLayer: Identifiable { var root: NavigationPage var path: [NavigationPage] - var shareInfo: ShareInfo? var hasNavigationStack: Bool var isFullScreenCover: Bool var canDisplayToasts: Bool @@ -92,6 +70,8 @@ class NavigationLayer: Identifiable { isInsideSheet && index == (model?.layers.count ?? 0) - 1 } + var isBottomLayer: Bool { index == -1 } + var isToastDisplayer: Bool { isInsideSheet && canDisplayToasts diff --git a/Mlem/App/Views/Shared/Navigation/NavigationLayerView.swift b/Mlem/App/Views/Shared/Navigation/NavigationLayerView.swift index 34ed0dc35..916b912f9 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationLayerView.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationLayerView.swift @@ -12,7 +12,7 @@ import UIKit struct NavigationLayerView: View { @Setting(\.interfaceStyle) var interfaceStyle - @Bindable var layer: NavigationLayer + @State var layer: NavigationLayer let hasSheetModifiers: Bool private let fullWidthGestureRecognizerDelegate: FullWidthGestureRecognizerDelegate = .init() @@ -65,24 +65,6 @@ struct NavigationLayerView: View { ) .padding(.bottom, 8) } - // https://stackoverflow.com/questions/69693871/how-to-open-share-sheet-from-presented-sheet - .background(SharingViewController( - isPresenting: Binding(get: { layer.shareInfo != nil }, set: { if !$0 { layer.shareInfo = nil }}) - ) { - let activityView = UIActivityViewController( - activityItems: [layer.shareInfo?.url ?? URL(string: "www.apple.com")!], - applicationActivities: layer.shareInfo?.activities - ) - - if UIDevice.isPad { - activityView.popoverPresentationController?.sourceView = UIView() - } - - activityView.completionWithItemsHandler = { _, _, _, _ in - layer.shareInfo = nil - } - return activityView - }) .modifier(HandleLemmyLinksModifier()) .environment(layer) .preferredColorScheme(preferredColorScheme) @@ -111,21 +93,6 @@ struct NavigationLayerView: View { } } -private struct SharingViewController: UIViewControllerRepresentable { - @Binding var isPresenting: Bool - var content: () -> UIViewController - - func makeUIViewController(context: Context) -> UIViewController { - UIViewController() - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) { - if isPresenting { - uiViewController.present(content(), animated: true, completion: { isPresenting = false }) - } - } -} - private class FullWidthGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { var navigationController: UINavigationController? diff --git a/Mlem/App/Views/Shared/Navigation/NavigationModel.swift b/Mlem/App/Views/Shared/Navigation/NavigationModel.swift index 1b83d83b1..15a11d33d 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationModel.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationModel.swift @@ -14,6 +14,27 @@ class NavigationModel { private(set) var layers: [NavigationLayer] = .init() + struct ShareInfo { + let url: URL + let activities: [ShareActivity] + + init(url: URL, activities: [ShareActivity]) { + self.url = url + self.activities = activities + } + + init(url: URL, actions: [BasicAction] = []) { + self.url = url + self.activities = actions.compactMap { action in + if let callback = action.callback { + .init(appearance: action.appearance, performAction: callback) + } else { + nil + } + } + } + } + @Observable class ContentPickerTracker { var photosPickerCallback: ((PhotosPickerItem) -> Void)? @@ -30,7 +51,8 @@ class NavigationModel { var contentPickerTracker: ContentPickerTracker = .init() var mediaUrl: URL? - + var shareInfo: ShareInfo? + @MainActor private func openSheet(_ page: NavigationPage, hasNavigationStack: Bool? = nil, isFullScreenCover: Bool) { guard Thread.isMainThread else { diff --git a/Mlem/App/Views/Shared/Navigation/NavigationPage+View.swift b/Mlem/App/Views/Shared/Navigation/NavigationPage+View.swift index 0cd0d5a49..0e0496b34 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationPage+View.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationPage+View.swift @@ -16,6 +16,8 @@ extension NavigationPage { SubscriptionListView() case let .selectText(string): SelectTextView(text: string) + case let .shareInstancePicker(sharable): + ShareInstancePickerView(entity: sharable.wrappedValue) case let .settings(page): page.view() case let .logIn(page): diff --git a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift index 849c50703..452f42c08 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift @@ -37,6 +37,7 @@ enum NavigationPage: Hashable { case instancePicker(callback: HashWrapper<(InstanceSummary, NavigationLayer) -> Void>, minimumVersion: SiteVersion? = nil) case languagePicker(selectedLanguages: Set, callback: HashWrapper<(Locale.Language) -> Void>) case selectText(_ string: String) + case shareInstancePicker(_ sharable: SharableHashWrapper) case subscriptionList case createComment(_ context: CommentEditorView.Context, commentTreeTracker: CommentTreeTracker? = nil) case editComment(_ comment: Comment2, context: CommentEditorView.Context?) @@ -112,6 +113,10 @@ enum NavigationPage: Hashable { Self.instance(.init(wrappedValue: instance), visitContext: visitContext) } + static func shareInstancePicker(_ sharable: any Sharable) -> NavigationPage { + shareInstancePicker(.init(wrappedValue: sharable)) + } + static func modlog(community: any Community) -> NavigationPage { modlog(.community(.init(community))) } @@ -208,9 +213,9 @@ enum NavigationPage: Hashable { callback: @escaping (Community2) -> Void ) -> NavigationPage { communityPicker(api: api, callback: .init(wrappedValue: { value, navigation in - callback(value) Task { @MainActor in navigation.dismissSheet() + callback(value) } })) } @@ -221,9 +226,9 @@ enum NavigationPage: Hashable { callback: @escaping (Person2) -> Void ) -> NavigationPage { personPicker(api: api, filter: filter, callback: .init(wrappedValue: { value, navigation in - callback(value) Task { @MainActor in navigation.dismissSheet() + callback(value) } })) } @@ -234,9 +239,9 @@ enum NavigationPage: Hashable { ) -> NavigationPage { assert((minimumVersion ?? .infinity) > Constants.main.minimumLemmyVersion) return instancePicker(callback: .init(wrappedValue: { value, navigation in - callback(value) Task { @MainActor in navigation.dismissSheet() + callback(value) } }), minimumVersion: minimumVersion) } @@ -382,6 +387,18 @@ struct ReportableHashWrapper: Hashable { } } +struct SharableHashWrapper: Hashable { + var wrappedValue: any Sharable + + func hash(into hasher: inout Hasher) { + hasher.combine(wrappedValue.hashValue) + } + + static func == (lhs: SharableHashWrapper, rhs: SharableHashWrapper) -> Bool { + lhs.hashValue == rhs.hashValue + } +} + struct RemovableHashWrapper: Hashable { var wrappedValue: any RemovableProviding diff --git a/Mlem/App/Views/Shared/Navigation/SettingsPage.swift b/Mlem/App/Views/Shared/Navigation/SettingsPage.swift index 5f7903c1d..3b71253ac 100644 --- a/Mlem/App/Views/Shared/Navigation/SettingsPage.swift +++ b/Mlem/App/Views/Shared/Navigation/SettingsPage.swift @@ -18,7 +18,7 @@ enum SettingsPage: Hashable { case privacyBypassImageProxy case safetyBlurNsfw, safetyWarnings case links, embedding - case externalLinks, tappableLinks + case externalLinks, sharingLinks, tappableLinks case importExportSettings case theme, icon case post, comment, inbox, subscriptionList, tabBar @@ -121,6 +121,8 @@ enum SettingsPage: Hashable { LinkSettingsView() case .externalLinks: ExternalLinkSettingsView() + case .sharingLinks: + SharingLinksSettingsView() case .tappableLinks: TappableLinksSettingsView() case .embedding: diff --git a/Mlem/App/Views/Shared/Navigation/View+NavigationSheetModifiers.swift b/Mlem/App/Views/Shared/Navigation/View+NavigationSheetModifiers.swift index fe073cb49..a67d4c125 100644 --- a/Mlem/App/Views/Shared/Navigation/View+NavigationSheetModifiers.swift +++ b/Mlem/App/Views/Shared/Navigation/View+NavigationSheetModifiers.swift @@ -10,13 +10,20 @@ import SwiftUI private struct NavigationSheetModifier: ViewModifier { let nextLayer: NavigationLayer? let contentPickerTracker_: () -> NavigationModel.ContentPickerTracker? + let isTopSheet: Bool + + @Binding var shareInfo: NavigationModel.ShareInfo? init( nextLayer: NavigationLayer?, + isTopSheet: Bool, + shareInfo: Binding, // This tomfoolery exists to prevent this view being subject to NavigationModel view updates, which caused #1492 contentPickerTracker: @escaping () -> NavigationModel.ContentPickerTracker? ) { self.nextLayer = nextLayer + self.isTopSheet = isTopSheet + self._shareInfo = shareInfo self.contentPickerTracker_ = contentPickerTracker } @@ -27,11 +34,14 @@ private struct NavigationSheetModifier: ViewModifier { func body(content: Content) -> some View { content + // https://stackoverflow.com/questions/69693871/how-to-open-share-sheet-from-presented-sheet + .background(SharingViewController( + isPresenting: Binding(get: { shareInfo != nil && isTopSheet }, set: { if !$0 { shareInfo = nil }}) + ) { activityViewController } + ) .sheet(isPresented: Binding( get: { !(nextLayer?.isFullScreenCover ?? true) }, - set: { - if !$0 { closeSheet() } - } + set: { if !$0 { closeSheet() } } )) { if let nextLayer { NavigationLayerView(layer: nextLayer, hasSheetModifiers: true) @@ -39,9 +49,7 @@ private struct NavigationSheetModifier: ViewModifier { } .fullScreenCover(isPresented: Binding( get: { nextLayer?.isFullScreenCover ?? false }, - set: { - if !$0 { closeSheet() } - } + set: { if !$0 { closeSheet() } } )) { if let nextLayer { NavigationLayerView(layer: nextLayer, hasSheetModifiers: true) @@ -76,6 +84,22 @@ private struct NavigationSheetModifier: ViewModifier { ) } + var activityViewController: UIActivityViewController { + let activityView = UIActivityViewController( + activityItems: [shareInfo?.url ?? URL(string: "www.apple.com")!], + applicationActivities: shareInfo?.activities + ) + + if UIDevice.isPad { + activityView.popoverPresentationController?.sourceView = UIView() + } + + activityView.completionWithItemsHandler = { _, _, _, _ in + shareInfo = nil + } + return activityView + } + func closeSheet() { if let nextLayer, let model = nextLayer.model { model.closeSheets(aboveIndex: nextLayer.index) @@ -93,6 +117,8 @@ private struct ComputeNextLayerModifier: ViewModifier { Group { content.navigationSheetModifiers( nextLayer: nextLayer, + isTopSheet: layer.isTopSheet, + shareInfo: .init(get: { layer.model?.shareInfo }, set: { layer.model?.shareInfo = $0 }), contentPickerTracker: layer.model?.contentPickerTracker ) }.onChange(of: computeNextLayer()?.id, initial: true) { @@ -116,8 +142,30 @@ extension View { @ViewBuilder func navigationSheetModifiers( nextLayer: NavigationLayer?, + isTopSheet: Bool, + shareInfo: Binding, contentPickerTracker: @autoclosure @escaping () -> NavigationModel.ContentPickerTracker? ) -> some View { - modifier(NavigationSheetModifier(nextLayer: nextLayer, contentPickerTracker: contentPickerTracker)) + modifier(NavigationSheetModifier( + nextLayer: nextLayer, + isTopSheet: isTopSheet, + shareInfo: shareInfo, + contentPickerTracker: contentPickerTracker + )) + } +} + +private struct SharingViewController: UIViewControllerRepresentable { + @Binding var isPresenting: Bool + var content: () -> UIViewController + + func makeUIViewController(context: Context) -> UIViewController { + UIViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + if isPresenting { + uiViewController.present(content(), animated: true, completion: { isPresenting = false }) + } } } diff --git a/Mlem/App/Views/Shared/PostEllipsisMenus.swift b/Mlem/App/Views/Shared/PostEllipsisMenus.swift index 421ce8888..ea2845322 100644 --- a/Mlem/App/Views/Shared/PostEllipsisMenus.swift +++ b/Mlem/App/Views/Shared/PostEllipsisMenus.swift @@ -33,7 +33,11 @@ struct PostEllipsisMenus: View { } } EllipsisMenu(size: size) { - post.basicMenuActions(appState: appState, commentTreeTracker: commentTreeTracker) + post.basicMenuActions( + appState: appState, + navigation: navigation, + commentTreeTracker: commentTreeTracker + ) } } else { EllipsisMenu(size: size) { diff --git a/Mlem/App/Views/Shared/ReplyView.swift b/Mlem/App/Views/Shared/ReplyView.swift index 217e14195..a63013ddf 100644 --- a/Mlem/App/Views/Shared/ReplyView.swift +++ b/Mlem/App/Views/Shared/ReplyView.swift @@ -23,7 +23,7 @@ struct ReplyView: View { Image(systemName: reply.isMention ? Icons.mention : Icons.reply) .symbolVariant(reply.read ? .none : .fill) .foregroundStyle(.themedAccent) - EllipsisMenu(size: 24) { reply.menuActions(appState: appState) } + EllipsisMenu(size: 24) { reply.menuActions(appState: appState, navigation: navigation) } .frame(height: 10) } @@ -50,7 +50,7 @@ struct ReplyView: View { .quickSwipes(reply.swipeActions(appState: appState, behavior: .standard)) .clipShape(.rect(cornerRadius: Constants.main.standardSpacing)) .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing)) - .contextMenu { reply.menuActions(appState: appState) } + .contextMenu { reply.menuActions(appState: appState, navigation: navigation) } .paletteBorder(cornerRadius: Constants.main.standardSpacing) } } diff --git a/Mlem/App/Views/Shared/Search/Results/InstanceListRow.swift b/Mlem/App/Views/Shared/Search/Results/InstanceListRow.swift index dacc4af5a..917e97a8e 100644 --- a/Mlem/App/Views/Shared/Search/Results/InstanceListRow.swift +++ b/Mlem/App/Views/Shared/Search/Results/InstanceListRow.swift @@ -61,7 +61,11 @@ struct InstanceListRow: View { .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: Constants.main.standardSpacing)) .contentShape(.contextMenuPreview, .rect(cornerRadius: Constants.main.standardSpacing)) .contextMenu { - instanceStub?.menuActions(appState: appState, allowExternalBlocking: true) ?? [] + instanceStub?.menuActions( + appState: appState, + navigation: navigation, + allowExternalBlocking: true + ) ?? [] } .paletteBorder(cornerRadius: Constants.main.standardSpacing) } diff --git a/Mlem/App/Views/Shared/ShareInstancePickerView.swift b/Mlem/App/Views/Shared/ShareInstancePickerView.swift new file mode 100644 index 000000000..b4a626457 --- /dev/null +++ b/Mlem/App/Views/Shared/ShareInstancePickerView.swift @@ -0,0 +1,135 @@ +// +// ShareInstancePickerView.swift +// Mlem +// +// Created by Sjmarf on 2025-03-09. +// + +import MlemMiddleware +import SwiftUI + +struct ShareInstancePickerView: View { + @Environment(NavigationLayer.self) var navigation + @Environment(\.dismiss) var dismiss + + let entity: any Sharable + + @State private var sheetContentHeight: CGFloat = SheetHeightKey.defaultValue + + var body: some View { + VStack(spacing: 16) { + HStack { + Text("Share using...") + .fontWeight(.bold) + .foregroundStyle(.themedSecondary) + .padding(.leading, 8) + Spacer() + CloseButtonView() + } + VStack(spacing: 0) { + instanceTargetRow(entity.api.host, label: "My Instance", url: entity.url()) + Divider() + instanceTargetRow(entity.actorId.host, label: "Host Instance", url: entity.actorId.url) + } + .frame(maxWidth: .infinity) + .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 16)) + chooseButtonView + } + .padding(16) + .presentationBackground(.themedGroupedBackground) + .overlay { + GeometryReader { proxy in + Color.clear.preference( + key: SheetHeightKey.self, + value: proxy.size.height + ) + } + } + .onPreferenceChange(SheetHeightKey.self) { sheetContentHeight = $0 } + .presentationDetents([.height(sheetContentHeight)]) + } + + @ViewBuilder + func instanceTargetRow(_ host: String, label: LocalizedStringResource, url: URL) -> some View { + Button { + navigation.dismissSheet() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + NavigationModel.main.shareInfo = .init(url: url, actions: entity.shareSheetActions()) + } + } label: { + VStack(alignment: .leading, spacing: 5) { + Text(host) + .foregroundStyle(.themedPrimary) + Text(label) + .font(.footnote) + .foregroundStyle(.themedSecondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + } + + @ViewBuilder + var chooseButtonView: some View { + Button { + let model = navigation.model + navigation.dismissSheet() + guard let model else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { + model.openSheet(.instancePicker(callback: { instance in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + Task { + await resolveEntity(url: instance.instanceStub.actorId.url, model: model) + } + } + })) + } + } label: { + Text("Choose Another Instance...") + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.themedSecondaryGroupedBackground, in: .rect(cornerRadius: 16)) + } + } + + func resolveEntity(url: URL, model: NavigationModel) async { + let toastId = ToastModel.main.add(.loading("Resolving..."), location: .bottom) + do { + let client = ApiClient.getApiClient(url: url, username: nil) + let resolvedEntity = try await client.resolve(url: entity.actorId.url) + NavigationModel.main.shareInfo = .init( + url: resolvedEntity.url(), + actions: entity.shareSheetActions() + ) + ToastModel.main.removeToast(id: toastId) + } catch { + ToastModel.main.removeToast(id: toastId) + handleError(error) + } + } +} + +struct SheetHeightKey: PreferenceKey { + static var defaultValue: CGFloat = 500 + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +#if DEBUG + #Preview(traits: .sampleEnvironment) { + ScrollView { + VStack(spacing: Constants.main.standardSpacing) { + LargePostView(post: Post2.mock(.realistic(.yorkshireDales))) + LargePostView(post: Post2.mock(.realistic(.meguroRiver))) + } + .padding(.horizontal, Constants.main.standardSpacing) + } + .background(.themedGroupedBackground) + .sheet(isPresented: .constant(true)) { + ShareInstancePickerView(entity: Community2.mock(.realistic(.pics))) + } + } +#endif diff --git a/Mlem/Localizable.xcstrings b/Mlem/Localizable.xcstrings index dc53d344f..c3aa4a353 100644 --- a/Mlem/Localizable.xcstrings +++ b/Mlem/Localizable.xcstrings @@ -1078,6 +1078,9 @@ } } } + }, + "Ask Every Time" : { + }, "Ask First" : { "localizations" : { @@ -1658,6 +1661,9 @@ } } } + }, + "Choose Another Instance..." : { + }, "Choose Community..." : { "localizations" : { @@ -3567,6 +3573,9 @@ } } } + }, + "Host Instance" : { + }, "Hot" : { "localizations" : { @@ -3697,6 +3706,9 @@ } } } + }, + "In Mlem" : { + }, "In Reader" : { "localizations" : { @@ -3709,6 +3721,7 @@ } }, "In-App" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -4626,6 +4639,9 @@ } } } + }, + "My Instance" : { + }, "My Profile" : { "localizations" : { @@ -6331,6 +6347,9 @@ } } } + }, + "Resolving..." : { + }, "Response Time" : { "localizations" : { @@ -6698,6 +6717,15 @@ } } } + }, + "Share Links" : { + + }, + "Share links using..." : { + + }, + "Share using..." : { + }, "Share..." : { "localizations" : {