From 2d9e9802156ebcc26c32a22a506b7d1afc2d989d Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 12 Dec 2022 16:41:13 +0100 Subject: [PATCH] feat: Implement /api/v2/instance to conditionally show Translation option --- Mastodon/Diffable/Status/StatusSection.swift | 2 + ...DiscoveryCommunityViewModel+Diffable.swift | 1 + .../DiscoveryPostsViewModel+Diffable.swift | 1 + .../HashtagTimelineViewModel+Diffable.swift | 1 + .../HomeTimelineViewModel+Diffable.swift | 1 + .../Bookmark/BookmarkViewModel+Diffable.swift | 1 + .../Favorite/FavoriteViewModel+Diffable.swift | 1 + .../UserTimelineViewModel+Diffable.swift | 1 + .../Thread/ThreadViewModel+Diffable.swift | 1 + .../CoreData.xcdatamodeld/.xccurrentversion | 2 +- .../CoreData 6.xcdatamodel/contents | 259 ++++++++++++++++++ .../Entity/Mastodon/Instance.swift | 7 +- .../Extension/CoreDataStack/Instance.swift | 40 ++- .../Service/API/APIService+Instance.swift | 5 + .../APIService+CoreData+InstanceV2.swift | 69 +++++ .../Service/InstanceService.swift | 118 +++++--- .../API/Mastodon+API+V2+Instance.swift | 50 ++++ .../MastodonSDK/API/Mastodon+API.swift | 1 + .../Entity/Mastodon+Entity+InstanceV2.swift | 158 +++++++++++ .../Content/NotificationView+ViewModel.swift | 25 +- .../View/Content/StatusAuthorView.swift | 3 +- .../View/Content/StatusView+ViewModel.swift | 17 ++ 22 files changed, 722 insertions(+), 42 deletions(-) create mode 100644 MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents create mode 100644 MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+InstanceV2.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Instance.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+InstanceV2.swift diff --git a/Mastodon/Diffable/Status/StatusSection.swift b/Mastodon/Diffable/Status/StatusSection.swift index 38b8e641f..8ccb32c0c 100644 --- a/Mastodon/Diffable/Status/StatusSection.swift +++ b/Mastodon/Diffable/Status/StatusSection.swift @@ -27,6 +27,7 @@ extension StatusSection { static let logger = Logger(subsystem: "StatusSection", category: "logic") struct Configuration { + let context: AppContext let authContext: AuthContext weak var statusTableViewCellDelegate: StatusTableViewCellDelegate? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? @@ -250,6 +251,7 @@ extension StatusSection { statusView: cell.statusView ) + cell.statusView.viewModel.context = configuration.context cell.statusView.viewModel.authContext = configuration.authContext cell.configure( diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift index 64b4d3b6a..caa1f8460 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift @@ -18,6 +18,7 @@ extension DiscoveryCommunityViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + context: context, authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift index afa0594d5..f36812538 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift @@ -18,6 +18,7 @@ extension DiscoveryPostsViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + context: context, authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift index 8d8b0126a..c7c0a3bd7 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift @@ -20,6 +20,7 @@ extension HashtagTimelineViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + context: context, authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 29bff623b..ff3224d3d 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -22,6 +22,7 @@ extension HomeTimelineViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + context: context, authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate, diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift index 69075a8ce..bb9148687 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift @@ -17,6 +17,7 @@ extension BookmarkViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + context: context, authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift index 3723dae5d..e0f741f62 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift @@ -17,6 +17,7 @@ extension FavoriteViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + context: context, authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift index 863d7b44e..4992e653a 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift @@ -18,6 +18,7 @@ extension UserTimelineViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + context: context, authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index 834d478e6..8846c8b95 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -24,6 +24,7 @@ extension ThreadViewModel { tableView: tableView, context: context, configuration: StatusSection.Configuration( + context: context, authContext: authContext, statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion index 2145ac780..e660b0a08 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreData 5.xcdatamodel + CoreData 6.xcdatamodel diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents new file mode 100644 index 000000000..3249c5510 --- /dev/null +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Instance.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Instance.swift index cc21e8351..c11a92b76 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Instance.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Instance.swift @@ -16,7 +16,8 @@ public final class Instance: NSManagedObject { @NSManaged public private(set) var updatedAt: Date @NSManaged public private(set) var configurationRaw: Data? - + @NSManaged public private(set) var configurationV2Raw: Data? + // MARK: one-to-many relationships @NSManaged public var authentications: Set } @@ -44,6 +45,10 @@ extension Instance { self.configurationRaw = configurationRaw } + public func update(configurationV2Raw: Data?) { + self.configurationV2Raw = configurationV2Raw + } + public func didUpdate(at networkDate: Date) { self.updatedAt = networkDate } diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift index 7e925b665..619abc91e 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Instance.swift @@ -25,8 +25,42 @@ extension Instance { } extension Instance { - public var canFollowTags: Bool { - guard let majorVersionString = version?.split(separator: ".").first else { return false } - return Int(majorVersionString) == 4 // following Tags is support beginning with Mastodon v4.0.0 + public var configurationV2: Mastodon.Entity.V2.Instance.Configuration? { + guard let configurationRaw = configurationV2Raw else { return nil } + guard let configuration = try? JSONDecoder().decode(Mastodon.Entity.V2.Instance.Configuration.self, from: configurationRaw) else { + return nil + } + + return configuration + } + + static func encodeV2(configuration: Mastodon.Entity.V2.Instance.Configuration) -> Data? { + return try? JSONEncoder().encode(configuration) + } +} + +extension Instance { + public var canFollowTags: Bool { + version?.majorServerVersion(greaterThanOrEquals: 4) ?? false // following Tags is support beginning with Mastodon v4.0.0 + } +} + +extension String { + public func majorServerVersion(greaterThanOrEquals comparedVersion: Int) -> Bool { + guard + let majorVersionString = split(separator: ".").first, + let majorVersionInt = Int(majorVersionString) + else { return false } + + return majorVersionInt >= comparedVersion + } +} + +extension Instance { + var isTranslationEnabled: Bool { + if let configuration = configurationV2 { + return configuration.translation?.enabled == true + } + return false } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift index 93bfcf09a..eb39b5585 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Instance.swift @@ -20,4 +20,9 @@ extension APIService { return Mastodon.API.Instance.instance(session: session, domain: domain) } + public func instanceV2( + domain: String + ) -> AnyPublisher, Error> { + return Mastodon.API.V2.Instance.instance(session: session, domain: domain) + } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+InstanceV2.swift b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+InstanceV2.swift new file mode 100644 index 000000000..17ebb5f55 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Service/API/CoreData/APIService+CoreData+InstanceV2.swift @@ -0,0 +1,69 @@ +import os.log +import Foundation +import CoreData +import CoreDataStack +import MastodonSDK + +extension APIService.CoreData { + + static func createOrMergeInstanceV2( + into managedObjectContext: NSManagedObjectContext, + domain: String, + entity: Mastodon.Entity.V2.Instance, + networkDate: Date, + log: Logger + ) -> (instance: Instance, isCreated: Bool) { + // fetch old mastodon user + let old: Instance? = { + let request = Instance.sortedFetchRequest + request.predicate = Instance.predicate(domain: domain) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + + if let old = old { + APIService.CoreData.mergeV2( + instance: old, + entity: entity, + domain: domain, + networkDate: networkDate + ) + return (old, false) + } else { + let instance = Instance.insert( + into: managedObjectContext, + property: Instance.Property(domain: domain, version: entity.version) + ) + let configurationRaw = entity.configuration.flatMap { Instance.encodeV2(configuration: $0) } + instance.update(configurationV2Raw: configurationRaw) + + return (instance, true) + } + } + +} + +extension APIService.CoreData { + + static func mergeV2( + instance: Instance, + entity: Mastodon.Entity.V2.Instance, + domain: String, + networkDate: Date + ) { + guard networkDate > instance.updatedAt else { return } + + let configurationRaw = entity.configuration.flatMap { Instance.encodeV2(configuration: $0) } + instance.update(configurationV2Raw: configurationRaw) + instance.version = entity.version + + instance.didUpdate(at: networkDate) + } + +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index 99ad6d0a2..02946ca6c 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -50,42 +50,18 @@ extension InstanceService { func updateInstance(domain: String) { guard let apiService = self.apiService else { return } apiService.instance(domain: domain) - .flatMap { response -> AnyPublisher, Error> in - let managedObjectContext = self.backgroundManagedObjectContext - return managedObjectContext.performChanges { - // get instance - let (instance, _) = APIService.CoreData.createOrMergeInstance( - into: managedObjectContext, - domain: domain, - entity: response.value, - networkDate: response.networkDate, - log: Logger(subsystem: "Update", category: "InstanceService") - ) - - // update relationship - let request = MastodonAuthentication.sortedFetchRequest - request.predicate = MastodonAuthentication.predicate(domain: domain) - request.returnsObjectsAsFaults = false - do { - let authentications = try managedObjectContext.fetch(request) - for authentication in authentications { - authentication.update(instance: instance) - } - } catch { - assertionFailure(error.localizedDescription) - } + .flatMap { [unowned self] response -> AnyPublisher in + if response.value.version?.majorServerVersion(greaterThanOrEquals: 4) == true { + return apiService.instanceV2(domain: domain) + .flatMap { return self.updateInstanceV2(domain: domain, response: $0) } + .eraseToAnyPublisher() + } else { + return self.updateInstance(domain: domain, response: response) } - .setFailureType(to: Error.self) - .tryMap { result -> Mastodon.Response.Content in - switch result { - case .success: - return response - case .failure(let error): - throw error - } - } - .eraseToAnyPublisher() } +// .flatMap { [unowned self] response -> AnyPublisher in +// return +// } .sink { [weak self] completion in guard let self = self else { return } switch completion { @@ -100,6 +76,80 @@ extension InstanceService { } .store(in: &disposeBag) } + + private func updateInstance(domain: String, response: Mastodon.Response.Content) -> AnyPublisher { + let managedObjectContext = self.backgroundManagedObjectContext + return managedObjectContext.performChanges { + // get instance + let (instance, _) = APIService.CoreData.createOrMergeInstance( + into: managedObjectContext, + domain: domain, + entity: response.value, + networkDate: response.networkDate, + log: Logger(subsystem: "Update", category: "InstanceService") + ) + + // update relationship + let request = MastodonAuthentication.sortedFetchRequest + request.predicate = MastodonAuthentication.predicate(domain: domain) + request.returnsObjectsAsFaults = false + do { + let authentications = try managedObjectContext.fetch(request) + for authentication in authentications { + authentication.update(instance: instance) + } + } catch { + assertionFailure(error.localizedDescription) + } + } + .setFailureType(to: Error.self) + .tryMap { result in + switch result { + case .success: + break + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + + private func updateInstanceV2(domain: String, response: Mastodon.Response.Content) -> AnyPublisher { + let managedObjectContext = self.backgroundManagedObjectContext + return managedObjectContext.performChanges { + // get instance + let (instance, _) = APIService.CoreData.createOrMergeInstanceV2( + into: managedObjectContext, + domain: domain, + entity: response.value, + networkDate: response.networkDate, + log: Logger(subsystem: "Update", category: "InstanceService") + ) + + // update relationship + let request = MastodonAuthentication.sortedFetchRequest + request.predicate = MastodonAuthentication.predicate(domain: domain) + request.returnsObjectsAsFaults = false + do { + let authentications = try managedObjectContext.fetch(request) + for authentication in authentications { + authentication.update(instance: instance) + } + } catch { + assertionFailure(error.localizedDescription) + } + } + .setFailureType(to: Error.self) + .tryMap { result in + switch result { + case .success: + break + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } } public extension InstanceService { diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Instance.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Instance.swift new file mode 100644 index 000000000..e276fddba --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Instance.swift @@ -0,0 +1,50 @@ +import Foundation +import Combine + +extension Mastodon.API.V2.Instance { + + private static func instanceEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent("instance") + } + + /// Information about the server + /// + /// - Since: 4.0.0 + /// - Version: 4.0.0 + /// # Last Update + /// 2022/12/09 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/instance/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - Returns: `AnyPublisher` contains `Instance` nested in the response + public static func instance( + session: URLSession, + domain: String + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: instanceEndpointURL(domain: domain), + query: nil, + authorization: nil + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value: Mastodon.Entity.V2.Instance + + do { + value = try Mastodon.API.decode(type: Mastodon.Entity.V2.Instance.self, from: data, response: response) + } catch { + if let response = response as? HTTPURLResponse, 400 ..< 500 ~= response.statusCode { + // For example, AUTHORIZED_FETCH may result in authentication errors + value = Mastodon.Entity.V2.Instance(domain: domain) + } else { + throw error + } + } + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index a1eb47873..f85d50bd0 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -126,6 +126,7 @@ extension Mastodon.API.V2 { public enum Search { } public enum Suggestions { } public enum Media { } + public enum Instance { } } extension Mastodon.API { diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+InstanceV2.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+InstanceV2.swift new file mode 100644 index 000000000..05913ebe9 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+InstanceV2.swift @@ -0,0 +1,158 @@ +import Foundation + +extension Mastodon.Entity.V2 { + /// Instance + /// + /// - Since: 4.0.0 + /// - Version: 4.0.3 + /// # Last Update + /// 2022/12/09 + /// # Reference + /// [Document](https://docs.joinmastodon.org/entities/instance/) + public struct Instance: Codable { + + public let domain: String? + public let title: String + public let description: String + public let shortDescription: String? + public let email: String? + public let version: String? + public let languages: [String]? // (ISO 639 Part 1-5 language codes) + public let registrations: Mastodon.Entity.V2.Instance.Registrations? + public let approvalRequired: Bool? + public let invitesEnabled: Bool? + public let urls: Mastodon.Entity.Instance.InstanceURL? + public let statistics: Mastodon.Entity.Instance.Statistics? + + public let thumbnail: Thumbnail? + public let contactAccount: Mastodon.Entity.Account? + public let rules: [Mastodon.Entity.Instance.Rule]? + + // https://github.com/mastodon/mastodon/pull/16485 + public let configuration: Configuration? + + public init(domain: String, approvalRequired: Bool? = nil) { + self.domain = domain + self.title = domain + self.description = "" + self.shortDescription = nil + self.email = "" + self.version = nil + self.languages = nil + self.registrations = nil + self.approvalRequired = approvalRequired + self.invitesEnabled = nil + self.urls = nil + self.statistics = nil + self.thumbnail = nil + self.contactAccount = nil + self.rules = nil + self.configuration = nil + } + + enum CodingKeys: String, CodingKey { + case domain + case title + case description + case shortDescription = "short_description" + case email + case version + case languages + case registrations + case approvalRequired = "approval_required" + case invitesEnabled = "invites_enabled" + case urls + case statistics = "stats" + + case thumbnail + case contactAccount = "contact_account" + case rules + + case configuration + } + } +} + +extension Mastodon.Entity.V2.Instance { + public struct Configuration: Codable { + public let statuses: Mastodon.Entity.Instance.Configuration.Statuses? + public let mediaAttachments: Mastodon.Entity.Instance.Configuration.MediaAttachments? + public let polls: Mastodon.Entity.Instance.Configuration.Polls? + public let translation: Mastodon.Entity.V2.Instance.Configuration.Translation? + + enum CodingKeys: String, CodingKey { + case statuses + case mediaAttachments = "media_attachments" + case polls + case translation + } + } +} + +extension Mastodon.Entity.V2.Instance { + public struct Registrations: Codable { + public let enabled: Bool + } +} + +extension Mastodon.Entity.V2.Instance.Configuration { + public struct Translation: Codable { + public let enabled: Bool + } +} + +extension Mastodon.Entity.V2.Instance { + public struct Thumbnail: Codable { + public let url: String? + } +} + +//extension Mastodon.Entity.V2.Instance { +// public struct Statuses: Codable { +// public let maxCharacters: Int +// public let maxMediaAttachments: Int +// public let charactersReservedPerURL: Int +// +// enum CodingKeys: String, CodingKey { +// case maxCharacters = "max_characters" +// case maxMediaAttachments = "max_media_attachments" +// case charactersReservedPerURL = "characters_reserved_per_url" +// } +// } +// +// public struct MediaAttachments: Codable { +// public let supportedMIMETypes: [String] +// public let imageSizeLimit: Int +// public let imageMatrixLimit: Int +// public let videoSizeLimit: Int +// public let videoFrameRateLimit: Int +// public let videoMatrixLimit: Int +// +// enum CodingKeys: String, CodingKey { +// case supportedMIMETypes = "supported_mime_types" +// case imageSizeLimit = "image_size_limit" +// case imageMatrixLimit = "image_matrix_limit" +// case videoSizeLimit = "video_size_limit" +// case videoFrameRateLimit = "video_frame_rate_limit" +// case videoMatrixLimit = "video_matrix_limit" +// } +// } +// +// public struct Polls: Codable { +// public let maxOptions: Int +// public let maxCharactersPerOption: Int +// public let minExpiration: Int +// public let maxExpiration: Int +// +// enum CodingKeys: String, CodingKey { +// case maxOptions = "max_options" +// case maxCharactersPerOption = "max_characters_per_option" +// case minExpiration = "min_expiration" +// case maxExpiration = "max_expiration" +// } +// } +// +// public struct Translation: Codable { +// public let enabled: Bool +// } +//} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index 40c4f2870..ed038f47f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -23,7 +23,8 @@ extension NotificationView { public var objects = Set() let logger = Logger(subsystem: "NotificationView", category: "ViewModel") - + + @Published public var context: AppContext? @Published public var authContext: AuthContext? @Published public var type: MastodonNotificationType? @@ -57,6 +58,9 @@ extension NotificationView.ViewModel { bindAuthorMenu(notificationView: notificationView) bindFollowRequest(notificationView: notificationView) + $context + .assign(to: \.context, on: notificationView.statusView.viewModel) + .store(in: &disposeBag) $authContext .assign(to: \.authContext, on: notificationView.statusView.viewModel) .store(in: &disposeBag) @@ -209,7 +213,7 @@ extension NotificationView.ViewModel { $isTranslated ) ) - .sink { authorName, isMuting, isBlocking, isMyselfIsTranslated in + .sink { [weak self] authorName, isMuting, isBlocking, isMyselfIsTranslated in guard let name = authorName?.string else { notificationView.menuButton.menu = nil return @@ -217,12 +221,29 @@ extension NotificationView.ViewModel { let (isMyself, isTranslated) = isMyselfIsTranslated + lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = { + guard + let self = self, + let context = self.context, + let authContext = self.authContext + else { return nil } + + var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil + context.managedObjectContext.performAndWait { + guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) + else { return } + configuration = authentication.instance?.configurationV2 + } + return configuration + }() + let menuContext = NotificationView.AuthorMenuContext( name: name, isMuting: isMuting, isBlocking: isBlocking, isMyself: isMyself, isBookmarking: false, // no bookmark action display for notification item + isTranslationEnabled: instanceConfigurationV2?.translation?.enabled == true, isTranslated: isTranslated, statusLanguage: "" ) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift index c930a7b66..ef40ab7fc 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusAuthorView.swift @@ -150,6 +150,7 @@ extension StatusAuthorView { public let isMyself: Bool public let isBookmarking: Bool + public let isTranslationEnabled: Bool public let isTranslated: Bool public let statusLanguage: String? } @@ -158,7 +159,7 @@ extension StatusAuthorView { var actions = [MastodonMenu.Action]() if !menuContext.isMyself { - if let statusLanguage = menuContext.statusLanguage, !menuContext.isTranslated { + if let statusLanguage = menuContext.statusLanguage, menuContext.isTranslationEnabled, !menuContext.isTranslated { actions.append( .translateStatus(.init(language: statusLanguage)) ) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index f45c07ea6..09995677c 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -27,6 +27,7 @@ extension StatusView { let logger = Logger(subsystem: "StatusView", category: "ViewModel") + public var context: AppContext? public var authContext: AuthContext? public var originalStatus: Status? @@ -609,12 +610,28 @@ extension StatusView.ViewModel { return } + lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = { + guard + let context = self.context, + let authContext = self.authContext + else { return nil } + + var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil + context.managedObjectContext.performAndWait { + guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) + else { return } + configuration = authentication.instance?.configurationV2 + } + return configuration + }() + let menuContext = StatusAuthorView.AuthorMenuContext( name: name, isMuting: isMuting, isBlocking: isBlocking, isMyself: isMyself, isBookmarking: isBookmark, + isTranslationEnabled: instanceConfigurationV2?.translation?.enabled == true, isTranslated: translatedFromLanguage != nil, statusLanguage: language )