diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 1d517412a..98ed7f356 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -191,6 +191,7 @@ + @@ -282,7 +283,7 @@ - + diff --git a/CoreDataStack/Entity/Setting.swift b/CoreDataStack/Entity/Setting.swift index 88768eda4..0be80f973 100644 --- a/CoreDataStack/Entity/Setting.swift +++ b/CoreDataStack/Entity/Setting.swift @@ -16,6 +16,7 @@ public final class Setting: NSManagedObject { @NSManaged public var appearanceRaw: String @NSManaged public var preferredTrueBlackDarkMode: Bool @NSManaged public var preferredStaticAvatar: Bool + @NSManaged public var preferredUsingDefaultBrowser: Bool @NSManaged public private(set) var createdAt: Date @NSManaged public private(set) var updatedAt: Date @@ -62,6 +63,12 @@ extension Setting { self.preferredStaticAvatar = preferredStaticAvatar didUpdate(at: Date()) } + + public func update(preferredUsingDefaultBrowser: Bool) { + guard preferredUsingDefaultBrowser != self.preferredUsingDefaultBrowser else { return } + self.preferredUsingDefaultBrowser = preferredUsingDefaultBrowser + didUpdate(at: Date()) + } public func didUpdate(at networkDate: Date) { self.updatedAt = networkDate diff --git a/Localization/app.json b/Localization/app.json index 5c8dd5cc0..50abb54b8 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -495,12 +495,8 @@ "dark": "Always Dark" }, "appearance_settings": { - "dark_mode": { - "title": "True black Dark Mode" - }, - "avatar_animation": { - "title": "Disable avatar animation" - } + "true_black_dark_mode": "True black Dark Mode", + "disable_avatar_animation": "Disable avatar animation" }, "notifications": { "title": "Notifications", @@ -516,6 +512,10 @@ "title": "Notify me when" } }, + "preference": { + "title": "Preference", + "using_default_browser": "Using default browser open link" + }, "boringzone": { "title": "The Boring zone", "terms": "Terms of Service", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4ffc262a8..e6a687da0 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -415,6 +415,8 @@ DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; }; DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; }; DBA1DB80268F84F80052DB59 /* NotificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA1DB7F268F84F80052DB59 /* NotificationType.swift */; }; + DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA465922696B495002B41DB /* APIService+WebFinger.swift */; }; + DBA465952696E387002B41DB /* AppPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA465942696E387002B41DB /* AppPreference.swift */; }; DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */; }; DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */; }; DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */; }; @@ -1044,6 +1046,8 @@ DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = ""; }; DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; DBA1DB7F268F84F80052DB59 /* NotificationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationType.swift; sourceTree = ""; }; + DBA465922696B495002B41DB /* APIService+WebFinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+WebFinger.swift"; sourceTree = ""; }; + DBA465942696E387002B41DB /* AppPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreference.swift; sourceTree = ""; }; DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryService.swift; sourceTree = ""; }; DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewModel.swift; sourceTree = ""; }; DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewController.swift; sourceTree = ""; }; @@ -1935,6 +1939,7 @@ 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */, DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */, DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */, + DBA465922696B495002B41DB /* APIService+WebFinger.swift */, DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */, DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */, DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */, @@ -1984,6 +1989,7 @@ isa = PBXGroup; children = ( DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */, + DBA465942696E387002B41DB /* AppPreference.swift */, DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */, DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */, DB1D842F26566512000346B3 /* KeyboardPreference.swift */, @@ -3225,6 +3231,7 @@ DBAC649B267DF8C8007FE9FD /* ActivityIndicatorNode.swift in Sources */, DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */, + DBA465952696E387002B41DB /* AppPreference.swift in Sources */, DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */, 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */, @@ -3430,6 +3437,7 @@ DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */, 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */, DBCBCC052680AFB9000F5B51 /* AsyncHomeTimelineViewController+Provider.swift in Sources */, + DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */, 2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */, @@ -3898,7 +3906,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3906,7 +3914,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.8.7; + MARKETING_VERSION = 0.8.8; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3925,7 +3933,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3933,7 +3941,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.8.7; + MARKETING_VERSION = 0.8.8; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4253,7 +4261,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4261,7 +4269,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.8.7; + MARKETING_VERSION = 0.8.8; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4367,7 +4375,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4375,7 +4383,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.8.7; + MARKETING_VERSION = 0.8.8; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4486,7 +4494,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4494,7 +4502,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.8.7; + MARKETING_VERSION = 0.8.8; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4600,7 +4608,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4608,7 +4616,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.8.7; + MARKETING_VERSION = 0.8.8; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4654,7 +4662,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4662,7 +4670,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.8.7; + MARKETING_VERSION = 0.8.8; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4677,7 +4685,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4685,7 +4693,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.8.7; + MARKETING_VERSION = 0.8.8; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4833,7 +4841,7 @@ repositoryURL = "https://github.com/TwidereProject/MetaTextView.git"; requirement = { kind = exactVersion; - version = 1.3.0; + version = 1.3.1; }; }; DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = { diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1f3cc3145..5118c78b0 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -114,8 +114,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextView.git", "state": { "branch": null, - "revision": "e2049e14ef411c6810d53c1baf553b5161c6678f", - "version": "1.3.0" + "revision": "9021b330dd72898583f62ee7f4c98768d72e7654", + "version": "1.3.1" } }, { diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index b918e1812..f1f288fa1 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -192,8 +192,12 @@ extension SceneCoordinator { sender?.navigationController?.pushViewController(viewController, animated: true) case .safariPresent(let animated, let completion): - viewController.modalPresentationCapturesStatusBarAppearance = true - presentingViewController.present(viewController, animated: animated, completion: completion) + if UserDefaults.shared.preferredUsingDefaultBrowser, case let .safari(url) = scene { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } else { + viewController.modalPresentationCapturesStatusBarAppearance = true + presentingViewController.present(viewController, animated: animated, completion: completion) + } case .alertController(let animated, let completion): viewController.modalPresentationCapturesStatusBarAppearance = true diff --git a/Mastodon/Diffiable/Item/SettingsItem.swift b/Mastodon/Diffiable/Item/SettingsItem.swift index aca02474e..7320e9fc5 100644 --- a/Mastodon/Diffiable/Item/SettingsItem.swift +++ b/Mastodon/Diffiable/Item/SettingsItem.swift @@ -13,6 +13,7 @@ enum SettingsItem: Hashable { case appearanceDarkMode(settingObjectID: NSManagedObjectID) case appearanceDisableAvatarAnimation(settingObjectID: NSManagedObjectID) case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode) + case preferenceUsingDefaultBrowser(settingObjectID: NSManagedObjectID) case boringZone(item: Link) case spicyZone(item: Link) } diff --git a/Mastodon/Diffiable/Section/SettingsSection.swift b/Mastodon/Diffiable/Section/SettingsSection.swift index 9a248dca3..46ac82a1a 100644 --- a/Mastodon/Diffiable/Section/SettingsSection.swift +++ b/Mastodon/Diffiable/Section/SettingsSection.swift @@ -11,6 +11,7 @@ enum SettingsSection: Hashable { case appearance case appearanceSettings case notifications + case preference case boringZone case spicyZone @@ -19,6 +20,7 @@ enum SettingsSection: Hashable { case .appearance: return L10n.Scene.Settings.Section.Appearance.title case .appearanceSettings: return "" case .notifications: return L10n.Scene.Settings.Section.Notifications.title + case .preference: return L10n.Scene.Settings.Section.Preference.title case .boringZone: return L10n.Scene.Settings.Section.Boringzone.title case .spicyZone: return L10n.Scene.Settings.Section.Spicyzone.title } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index ab2b75f40..6ea2504a4 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -934,14 +934,10 @@ internal enum L10n { internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title") } internal enum AppearanceSettings { - internal enum AvatarAnimation { - /// Disable avatar animation - internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.AvatarAnimation.Title") - } - internal enum DarkMode { - /// True black Dark Mode - internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.DarkMode.Title") - } + /// Disable avatar animation + internal static let disableAvatarAnimation = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.DisableAvatarAnimation") + /// True black Dark Mode + internal static let trueBlackDarkMode = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.TrueBlackDarkMode") } internal enum Boringzone { /// Privacy Policy @@ -975,6 +971,12 @@ internal enum L10n { internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Notifications.Trigger.Title") } } + internal enum Preference { + /// Preference + internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Preference.Title") + /// Using default browser open link + internal static let usingDefaultBrowser = L10n.tr("Localizable", "Scene.Settings.Section.Preference.UsingDefaultBrowser") + } internal enum Spicyzone { /// Clear Media Cache internal static let clear = L10n.tr("Localizable", "Scene.Settings.Section.Spicyzone.Clear") diff --git a/Mastodon/Preference/AppPreference.swift b/Mastodon/Preference/AppPreference.swift new file mode 100644 index 000000000..4ede61cf8 --- /dev/null +++ b/Mastodon/Preference/AppPreference.swift @@ -0,0 +1,20 @@ +// +// AppPreference.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-8. +// + +import UIKit + +extension UserDefaults { + + @objc dynamic var preferredUsingDefaultBrowser: Bool { + get { + register(defaults: [#function: false]) + return bool(forKey: #function) + } + set { self[#function] = newValue } + } + +} diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index eff4ad124..e732789ef 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -128,12 +128,10 @@ extension StatusProviderFacade { static func responseToStatusActiveLabelAction(provider: StatusProvider, cell: UITableViewCell, activeLabel: ActiveLabel, didTapEntity entity: ActiveEntity) { switch entity.type { - case .hashtag(let text, _): - let hashtagTimelienViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text) - provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelienViewModel), from: provider, transition: .show) - case .mention(let text, _): - coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text) - case .url(_, _, let url, _): + case .url(_, _, let url, _), + .mention(let url, _) where url.lowercased().hasPrefix("http"): + // note: + // some server mark the normal url as "u-url" class. : guard let url = URL(string: url) else { return } if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain, url.pathComponents.count >= 4, @@ -146,6 +144,12 @@ extension StatusProviderFacade { } else { provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) } + case .hashtag(let text, _): + let hashtagTimelienViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text) + provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelienViewModel), from: provider, transition: .show) + case .mention(let text, let userInfo): + let href = userInfo?["href"] as? String + coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text, href: href) default: break } @@ -153,7 +157,10 @@ extension StatusProviderFacade { static func responseToStatusMetaTextAction(provider: StatusProvider, cell: UITableViewCell, metaText: MetaText, didSelectMeta meta: Meta) { switch meta { - case .url(_, _, let url, _): + case .url(_, _, let url, _), + .mention(_, let url, _) where url.lowercased().hasPrefix("http"): + // note: + // some server mark the normal url as "u-url" class. highlighted content is a URL guard let url = URL(string: url) else { return } if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain, url.pathComponents.count >= 4, @@ -169,8 +176,9 @@ extension StatusProviderFacade { case .hashtag(_, let hashtag, _): let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: hashtag) provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show) - case .mention(_, let mention, _): - coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: mention) + case .mention(_, let mention, let userInfo): + let href = userInfo?["href"] as? String + coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: mention, href: href) default: break } @@ -208,17 +216,17 @@ extension StatusProviderFacade { } #endif - private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell, mention: String) { + private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell, mention: String, href: String?) { provider.status(for: cell, indexPath: nil) .sink { [weak provider] status in guard let provider = provider else { return } guard let status = status else { return } - coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention) + coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention, href: href) } .store(in: &provider.disposeBag) } - private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, status: Status, mention: String) { + private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, status: Status, mention: String, href: String?) { guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } let domain = activeMastodonAuthenticationBox.domain @@ -230,7 +238,13 @@ extension StatusProviderFacade { }() // cannot continue without meta - guard let mentionMeta = (status.mentions ?? Set()).first(where: { $0.username == mention }) else { return } + guard let mentionMeta = (status.mentions ?? Set()).first(where: { $0.username == mention }) else { + // present web page if possible + if let url = href.flatMap({ URL(string: $0) }) { + provider.coordinator.present(scene: .safari(url: url), from: provider, transition: .safariPresent(animated: true, completion: nil)) + } + return + } let userID = mentionMeta.id diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index f6211e838..82663ba28 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -317,8 +317,8 @@ any server."; "Scene.Settings.Section.Appearance.Dark" = "Always Dark"; "Scene.Settings.Section.Appearance.Light" = "Always Light"; "Scene.Settings.Section.Appearance.Title" = "Appearance"; -"Scene.Settings.Section.AppearanceSettings.AvatarAnimation.Title" = "Disable avatar animation"; -"Scene.Settings.Section.AppearanceSettings.DarkMode.Title" = "True black Dark Mode"; +"Scene.Settings.Section.AppearanceSettings.DisableAvatarAnimation" = "Disable avatar animation"; +"Scene.Settings.Section.AppearanceSettings.TrueBlackDarkMode" = "True black Dark Mode"; "Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy"; "Scene.Settings.Section.Boringzone.Terms" = "Terms of Service"; "Scene.Settings.Section.Boringzone.Title" = "The Boring zone"; @@ -332,6 +332,8 @@ any server."; "Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower"; "Scene.Settings.Section.Notifications.Trigger.Noone" = "no one"; "Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when"; +"Scene.Settings.Section.Preference.Title" = "Preference"; +"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Using default browser open link"; "Scene.Settings.Section.Spicyzone.Clear" = "Clear Media Cache"; "Scene.Settings.Section.Spicyzone.Signout" = "Sign Out"; "Scene.Settings.Section.Spicyzone.Title" = "The spicy zone"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index f6211e838..82663ba28 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -317,8 +317,8 @@ any server."; "Scene.Settings.Section.Appearance.Dark" = "Always Dark"; "Scene.Settings.Section.Appearance.Light" = "Always Light"; "Scene.Settings.Section.Appearance.Title" = "Appearance"; -"Scene.Settings.Section.AppearanceSettings.AvatarAnimation.Title" = "Disable avatar animation"; -"Scene.Settings.Section.AppearanceSettings.DarkMode.Title" = "True black Dark Mode"; +"Scene.Settings.Section.AppearanceSettings.DisableAvatarAnimation" = "Disable avatar animation"; +"Scene.Settings.Section.AppearanceSettings.TrueBlackDarkMode" = "True black Dark Mode"; "Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy"; "Scene.Settings.Section.Boringzone.Terms" = "Terms of Service"; "Scene.Settings.Section.Boringzone.Title" = "The Boring zone"; @@ -332,6 +332,8 @@ any server."; "Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower"; "Scene.Settings.Section.Notifications.Trigger.Noone" = "no one"; "Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when"; +"Scene.Settings.Section.Preference.Title" = "Preference"; +"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Using default browser open link"; "Scene.Settings.Section.Spicyzone.Clear" = "Clear Media Cache"; "Scene.Settings.Section.Spicyzone.Signout" = "Sign Out"; "Scene.Settings.Section.Spicyzone.Title" = "The spicy zone"; diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index 95fc2be04..ef9275b0d 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -188,10 +188,17 @@ extension MastodonPickServerViewModel { return Just(Result.failure(APIService.APIError.implicit(.badRequest))).eraseToAnyPublisher() } self.unindexedServers.value = nil - return self.context.apiService.instance(domain: domain) - .map { response -> Result, Error>in - let newResponse = response.map { [Mastodon.Entity.Server(instance: $0)] } - return Result.success(newResponse) + return self.context.apiService.webFinger(domain: domain) + .flatMap { domain -> AnyPublisher, Error>, Never> in + return self.context.apiService.instance(domain: domain) + .map { response -> Result, Error>in + let newResponse = response.map { [Mastodon.Entity.Server(domain: domain, instance: $0)] } + return Result.success(newResponse) + } + .catch { error in + return Just(Result.failure(error)) + } + .eraseToAnyPublisher() } .catch { error in return Just(Result.failure(error)) diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 6a08bd45d..aa2ff0cf9 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -364,6 +364,9 @@ extension SettingsViewController: UITableViewDelegate { case .notification: // do nothing break + case .preferenceUsingDefaultBrowser: + // do nothing + break case .boringZone(let link), .spicyZone(let link): switch link { case .termsOfService, .privacyPolicy: @@ -501,7 +504,24 @@ extension SettingsViewController: SettingsToggleCellDelegate { // do nothing } .store(in: &disposeBag) + case .preferenceUsingDefaultBrowser(let settingObjectID): + let managedObjectContext = context.backgroundManagedObjectContext + managedObjectContext.performChanges { + let setting = managedObjectContext.object(with: settingObjectID) as! Setting + setting.update(preferredUsingDefaultBrowser: isOn) + } + .sink { result in + switch result { + case .success: + UserDefaults.shared.preferredUsingDefaultBrowser = isOn + case .failure(let error): + assertionFailure(error.localizedDescription) + break + } + } + .store(in: &disposeBag) default: + assertionFailure() break } } diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift index 142de7dcf..5d60f1669 100644 --- a/Mastodon/Scene/Settings/SettingsViewModel.swift +++ b/Mastodon/Scene/Settings/SettingsViewModel.swift @@ -102,13 +102,18 @@ extension SettingsViewModel { ] snapshot.appendSections([.appearanceSettings]) snapshot.appendItems(appearanceSettingItems, toSection: .appearanceSettings) - + + // notification let notificationItems = SettingsItem.NotificationSwitchMode.allCases.map { mode in SettingsItem.notification(settingObjectID: setting.objectID, switchMode: mode) } snapshot.appendSections([.notifications]) snapshot.appendItems(notificationItems, toSection: .notifications) + // preference + snapshot.appendSections([.preference]) + snapshot.appendItems([.preferenceUsingDefaultBrowser(settingObjectID: setting.objectID)], toSection: .preference) + // boring zone let boringZoneSettingsItems: [SettingsItem] = { let links: [SettingsItem.Link] = [ @@ -170,7 +175,8 @@ extension SettingsViewModel { cell.delegate = settingsAppearanceTableViewCellDelegate return cell case .appearanceDarkMode(let objectID), - .appearanceDisableAvatarAnimation(let objectID): + .appearanceDisableAvatarAnimation(let objectID), + .preferenceUsingDefaultBrowser(let objectID): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell cell.delegate = settingsToggleCellDelegate self.context.managedObjectContext.performAndWait { @@ -231,11 +237,14 @@ extension SettingsViewModel { ) { switch item { case .appearanceDarkMode: - cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.DarkMode.title + cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.trueBlackDarkMode cell.switchButton.isOn = setting.preferredTrueBlackDarkMode case .appearanceDisableAvatarAnimation: - cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.AvatarAnimation.title + cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.disableAvatarAnimation cell.switchButton.isOn = setting.preferredStaticAvatar + case .preferenceUsingDefaultBrowser: + cell.textLabel?.text = L10n.Scene.Settings.Section.Preference.usingDefaultBrowser + cell.switchButton.isOn = setting.preferredUsingDefaultBrowser default: assertionFailure() } diff --git a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift b/Mastodon/Scene/Share/View/Decoration/SawToothView.swift index 94dab47fa..e344b62ef 100644 --- a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift +++ b/Mastodon/Scene/Share/View/Decoration/SawToothView.swift @@ -52,7 +52,7 @@ final class SawToothView: UIView { } bezierPath.addLine(to: CGPoint(x: 0, y: bottomY)) bezierPath.close() - ThemeService.shared.currentTheme.value.systemBackgroundColor.setFill() + ThemeService.shared.currentTheme.value.tableViewCellBackgroundColor.setFill() bezierPath.fill() bezierPath.lineWidth = 0 bezierPath.stroke() diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift index b18314df2..8c329d31e 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift @@ -86,7 +86,7 @@ class TimelineLoaderTableViewCell: UITableViewCell { loadMoreButton.heightAnchor.constraint(equalToConstant: TimelineLoaderTableViewCell.buttonHeight).priority(.required - 1), ]) - // use stack view to alignlment content center + // use stack view to alignment content center stackView.spacing = 4 stackView.axis = .horizontal stackView.alignment = .center @@ -127,7 +127,7 @@ class TimelineLoaderTableViewCell: UITableViewCell { } private func setupBackgroundColor(theme: Theme) { - loadMoreButton.backgroundColor = theme.systemBackgroundColor + loadMoreButton.backgroundColor = theme.tableViewCellBackgroundColor } } diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift index 63cf10c3e..1fedf0d40 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift @@ -18,9 +18,9 @@ extension MediaPreviewableViewController { case .mosaic(let mosaicImageViewContainer): guard index < mosaicImageViewContainer.imageViews.count else { return nil } let imageView = mosaicImageViewContainer.imageViews[index] - return imageView.superview!.convert(imageView.frame, to: nil) + return imageView.superview?.convert(imageView.frame, to: nil) case .profileAvatar(let profileHeaderView): - return profileHeaderView.avatarImageView.superview!.convert(profileHeaderView.avatarImageView.frame, to: nil) + return profileHeaderView.avatarImageView.superview?.convert(profileHeaderView.avatarImageView.frame, to: nil) case .profileBanner: return nil // fallback to snapshot.frame } diff --git a/Mastodon/Service/APIService/APIService+WebFinger.swift b/Mastodon/Service/APIService/APIService+WebFinger.swift new file mode 100644 index 000000000..7cc0425dc --- /dev/null +++ b/Mastodon/Service/APIService/APIService+WebFinger.swift @@ -0,0 +1,36 @@ +// +// APIService+WebFinger.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-8. +// + +import Foundation +import Combine +import CoreData +import CoreDataStack +import CommonOSLog +import DateToolsSwift +import MastodonSDK + +extension APIService { + + private static func webFingerEndpointURL(domain: String) -> URL { + return URL(string: "https://\(domain)/")! + .appendingPathComponent(".well-known") + .appendingPathComponent("webfinger") + } + + func webFinger( + domain: String + ) -> AnyPublisher { + let url = APIService.webFingerEndpointURL(domain: domain) + let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 3) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + return response.url?.host ?? domain + } + .eraseToAnyPublisher() + } + +} diff --git a/Mastodon/Service/AuthenticationService.swift b/Mastodon/Service/AuthenticationService.swift index 4a460b1d0..a0bbca57a 100644 --- a/Mastodon/Service/AuthenticationService.swift +++ b/Mastodon/Service/AuthenticationService.swift @@ -81,26 +81,6 @@ final class AuthenticationService: NSObject { .assign(to: \.value, on: activeMastodonAuthenticationBox) .store(in: &disposeBag) - activeMastodonAuthenticationBox - .receive(on: RunLoop.main) - .sink { [weak self] authenticationBox in - guard let _ = self else { return } - guard let authenticationBox = authenticationBox else { return } - let request = Setting.sortedFetchRequest - request.predicate = Setting.predicate(domain: authenticationBox.domain, userID: authenticationBox.userID) - guard let setting = managedObjectContext.safeFetch(request).first else { return } - - let themeName: ThemeName = setting.preferredTrueBlackDarkMode ? .system : .mastodon - if UserDefaults.shared.currentThemeNameRawValue != themeName.rawValue { - ThemeService.shared.set(themeName: themeName) - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update theme style", ((#file as NSString).lastPathComponent), #line, #function) - } - if UserDefaults.shared.preferredStaticAvatar != setting.preferredStaticAvatar { - UserDefaults.shared.preferredStaticAvatar = setting.preferredStaticAvatar - } - } - .store(in: &disposeBag) - do { try mastodonAuthenticationFetchedResultsController.performFetch() mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? [] diff --git a/Mastodon/Service/SettingService.swift b/Mastodon/Service/SettingService.swift index 7a25d6c7b..7da8c3683 100644 --- a/Mastodon/Service/SettingService.swift +++ b/Mastodon/Service/SettingService.swift @@ -91,20 +91,16 @@ final class SettingService { self.currentSettingUpdateSubscription = nil return } - + + SettingService.updatePreference(setting: setting) self.currentSettingUpdateSubscription = ManagedObjectObserver.observe(object: setting) .sink(receiveCompletion: { _ in // do nothing }, receiveValue: { change in guard case .update(let object) = change.changeType, let setting = object as? Setting else { return } - - // observe apparance mode - switch setting.appearance { - case .automatic: UserDefaults.shared.customUserInterfaceStyle = .unspecified - case .light: UserDefaults.shared.customUserInterfaceStyle = .light - case .dark: UserDefaults.shared.customUserInterfaceStyle = .dark - } + + SettingService.updatePreference(setting: setting) }) } .store(in: &disposeBag) @@ -187,3 +183,37 @@ extension SettingService { } } + +extension SettingService { + + static func updatePreference(setting: Setting) { + // set appearance + let userInterfaceStyle: UIUserInterfaceStyle = { + switch setting.appearance { + case .automatic: return .unspecified + case .light: return .light + case .dark: return .dark + } + }() + if UserDefaults.shared.customUserInterfaceStyle != userInterfaceStyle { + UserDefaults.shared.customUserInterfaceStyle = userInterfaceStyle + } + + // set theme + let themeName: ThemeName = setting.preferredTrueBlackDarkMode ? .system : .mastodon + if UserDefaults.shared.currentThemeNameRawValue != themeName.rawValue { + ThemeService.shared.set(themeName: themeName) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update theme style", ((#file as NSString).lastPathComponent), #line, #function) + } + + // set avatar mode + if UserDefaults.shared.preferredStaticAvatar != setting.preferredStaticAvatar { + UserDefaults.shared.preferredStaticAvatar = setting.preferredStaticAvatar + } + + // set browser + if UserDefaults.shared.preferredUsingDefaultBrowser != setting.preferredUsingDefaultBrowser { + UserDefaults.shared.preferredUsingDefaultBrowser = setting.preferredUsingDefaultBrowser + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift index 505ed730d..2d1f9953f 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Server.swift @@ -38,8 +38,8 @@ extension Mastodon.Entity { case category } - public init(instance: Instance) { - self.domain = instance.uri + public init(domain: String, instance: Instance) { + self.domain = domain // make domain configurable for WebFinger self.version = instance.version ?? "" self.description = instance.shortDescription ?? instance.description self.language = instance.languages?.first ?? "" diff --git a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AppTests.swift b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AppTests.swift index 476318e6b..5638a9754 100644 --- a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AppTests.swift +++ b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AppTests.swift @@ -21,6 +21,7 @@ extension MastodonSDKTests { let query = Mastodon.API.App.CreateQuery( clientName: "XCTest", + redirectURIs: "mastodon://joinmastodon.org/oauth", website: nil ) Mastodon.API.App.create(session: session, domain: domain, query: query) diff --git a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+OAuthTests.swift b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+OAuthTests.swift index 547ee0991..b14aad24e 100644 --- a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+OAuthTests.swift +++ b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+OAuthTests.swift @@ -17,12 +17,12 @@ extension MastodonSDKTests { } func _testOAuthAuthorize(domain: String) throws { - let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: "StubClientID") + let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: "StubClientID", redirectURI: "mastodon://joinmastodon.org/oauth") let authorizeURL = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query) os_log("%{public}s[%{public}ld], %{public}s: (%s) authorizeURL %s", ((#file as NSString).lastPathComponent), #line, #function, domain, authorizeURL.absoluteString) XCTAssertEqual( authorizeURL.absoluteString, - "https://\(domain)/oauth/authorize?response_type=code&client_id=StubClientID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=read%20write%20follow%20push" + "https://\(domain)/oauth/authorize?response_type=code&client_id=StubClientID&redirect_uri=mastodon://joinmastodon.org/oauth&scope=read%20write%20follow%20push" ) } @@ -31,7 +31,7 @@ extension MastodonSDKTests { } func _testRevokeTokenFail() { - let theExpectation = expectation(description: "Revoke Instance Infomation") + let theExpectation = expectation(description: "Revoke Instance Information") let query = Mastodon.API.OAuth.RevokeTokenQuery(clientID: "StubClientID", clientSecret: "", token: "") Mastodon.API.OAuth.revokeToken(session: session, domain: domain, query: query) .receive(on: DispatchQueue.main) diff --git a/MastodonTests/MastodonTests.swift b/MastodonTests/MastodonTests.swift index a8483b18b..5da71aa43 100644 --- a/MastodonTests/MastodonTests.swift +++ b/MastodonTests/MastodonTests.swift @@ -31,3 +31,16 @@ class MastodonTests: XCTestCase { } } + +extension MastodonTests { + func testWebFinger() { + let expectation = expectation(description: "webfinger") + let cancellable = AppContext.shared.apiService.webFinger(domain: "pawoo.net") + .sink { completion in + expectation.fulfill() + } receiveValue: { domain in + expectation.fulfill() + } + wait(for: [expectation], timeout: 10) + } +}