forked from shadowfacts/Tusker
V2 instance API, add translation to InstanceFeatures
This commit is contained in:
parent
158940f8e6
commit
5e609aa40d
|
@ -21,7 +21,8 @@ public class InstanceFeatures: ObservableObject {
|
|||
@Published public private(set) var charsReservedPerURL = 23
|
||||
@Published public private(set) var maxPollOptionChars: Int?
|
||||
@Published public private(set) var maxPollOptionsCount: Int?
|
||||
@Published public private(set) var mediaAttachmentsConfiguration: Instance.MediaAttachmentsConfiguration?
|
||||
@Published public private(set) var mediaAttachmentsConfiguration: InstanceV1.MediaAttachmentsConfiguration?
|
||||
@Published public private(set) var translation: Bool = false
|
||||
|
||||
public var localOnlyPosts: Bool {
|
||||
switch instanceType {
|
||||
|
@ -240,6 +241,7 @@ public class InstanceFeatures: ObservableObject {
|
|||
maxPollOptionsCount = pollsConfig.maxOptions
|
||||
}
|
||||
mediaAttachmentsConfiguration = instance.configuration?.mediaAttachments
|
||||
translation = instance.translation
|
||||
|
||||
_featuresUpdated.send()
|
||||
}
|
||||
|
|
|
@ -9,26 +9,39 @@ import Foundation
|
|||
import Pachyderm
|
||||
|
||||
public struct InstanceInfo {
|
||||
public let version: String
|
||||
public let maxStatusCharacters: Int?
|
||||
public let configuration: Instance.Configuration?
|
||||
public let pollsConfiguration: Instance.PollsConfiguration?
|
||||
public var version: String
|
||||
public var maxStatusCharacters: Int?
|
||||
public var configuration: InstanceV1.Configuration?
|
||||
public var pollsConfiguration: InstanceV1.PollsConfiguration?
|
||||
public var translation: Bool
|
||||
|
||||
public init(version: String, maxStatusCharacters: Int?, configuration: Instance.Configuration?, pollsConfiguration: Instance.PollsConfiguration?) {
|
||||
public init(
|
||||
version: String,
|
||||
maxStatusCharacters: Int?,
|
||||
configuration: InstanceV1.Configuration?,
|
||||
pollsConfiguration: InstanceV1.PollsConfiguration?,
|
||||
translation: Bool
|
||||
) {
|
||||
self.version = version
|
||||
self.maxStatusCharacters = maxStatusCharacters
|
||||
self.configuration = configuration
|
||||
self.pollsConfiguration = pollsConfiguration
|
||||
self.translation = translation
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceInfo {
|
||||
public init(instance: Instance) {
|
||||
public init(v1 instance: InstanceV1) {
|
||||
self.init(
|
||||
version: instance.version,
|
||||
maxStatusCharacters: instance.maxStatusCharacters,
|
||||
configuration: instance.configuration,
|
||||
pollsConfiguration: instance.pollsConfiguration
|
||||
pollsConfiguration: instance.pollsConfiguration,
|
||||
translation: false
|
||||
)
|
||||
}
|
||||
|
||||
public mutating func update(v2: InstanceV2) {
|
||||
translation = v2.configuration.translation.enabled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -231,8 +231,12 @@ public class Client {
|
|||
return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
|
||||
}
|
||||
|
||||
public static func getInstance() -> Request<Instance> {
|
||||
return Request<Instance>(method: .get, path: "/api/v1/instance")
|
||||
public static func getInstanceV1() -> Request<InstanceV1> {
|
||||
return Request<InstanceV1>(method: .get, path: "/api/v1/instance")
|
||||
}
|
||||
|
||||
public static func getInstanceV2() -> Request<InstanceV2> {
|
||||
return Request<InstanceV2>(method: .get, path: "/api/v2/instance")
|
||||
}
|
||||
|
||||
public static func getCustomEmoji() -> Request<[Emoji]> {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Instance.swift
|
||||
// InstanceV1.swift
|
||||
// Pachyderm
|
||||
//
|
||||
// Created by Shadowfacts on 9/9/18.
|
||||
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct Instance: Decodable, Sendable {
|
||||
public struct InstanceV1: Decodable, Sendable {
|
||||
public let uri: String
|
||||
public let title: String
|
||||
public let description: String
|
||||
|
@ -92,7 +92,7 @@ public struct Instance: Decodable, Sendable {
|
|||
}
|
||||
}
|
||||
|
||||
extension Instance {
|
||||
extension InstanceV1 {
|
||||
public struct Stats: Decodable, Sendable {
|
||||
public let domainCount: Int?
|
||||
public let statusCount: Int?
|
||||
|
@ -106,7 +106,7 @@ extension Instance {
|
|||
}
|
||||
}
|
||||
|
||||
extension Instance {
|
||||
extension InstanceV1 {
|
||||
public struct Configuration: Codable, Sendable {
|
||||
public let statuses: StatusesConfiguration
|
||||
public let mediaAttachments: MediaAttachmentsConfiguration
|
||||
|
@ -121,7 +121,8 @@ extension Instance {
|
|||
}
|
||||
}
|
||||
|
||||
extension Instance {
|
||||
extension InstanceV1 {
|
||||
// note: also used by InstanceV2
|
||||
public struct StatusesConfiguration: Codable, Sendable {
|
||||
public let maxCharacters: Int
|
||||
public let maxMediaAttachments: Int
|
||||
|
@ -135,7 +136,8 @@ extension Instance {
|
|||
}
|
||||
}
|
||||
|
||||
extension Instance {
|
||||
extension InstanceV1 {
|
||||
// note: also used by InstanceV2
|
||||
public struct MediaAttachmentsConfiguration: Codable, Sendable {
|
||||
public let supportedMIMETypes: [String]
|
||||
public let imageSizeLimit: Int
|
||||
|
@ -155,7 +157,8 @@ extension Instance {
|
|||
}
|
||||
}
|
||||
|
||||
extension Instance {
|
||||
extension InstanceV1 {
|
||||
// note: also used by InstanceV2
|
||||
public struct PollsConfiguration: Codable, Sendable {
|
||||
public let maxOptions: Int
|
||||
public let maxCharactersPerOption: Int
|
||||
|
@ -171,7 +174,8 @@ extension Instance {
|
|||
}
|
||||
}
|
||||
|
||||
extension Instance {
|
||||
extension InstanceV1 {
|
||||
// note: also used by InstanceV2
|
||||
public struct Rule: Decodable, Identifiable, Sendable {
|
||||
public let id: String
|
||||
public let text: String
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// InstanceV2.swift
|
||||
// Pachyderm
|
||||
//
|
||||
// Created by Shadowfacts on 12/4/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct InstanceV2: Decodable, Sendable {
|
||||
public let domain: String
|
||||
public let title: String
|
||||
public let version: String
|
||||
public let sourceURL: String
|
||||
public let description: String
|
||||
public let usage: Usage
|
||||
public let thumbnail: Thumbnail
|
||||
public let languages: [String]
|
||||
public let configuration: Configuration
|
||||
public let registrations: Registrations
|
||||
public let contact: Contact
|
||||
public let rules: [InstanceV1.Rule]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case domain
|
||||
case title
|
||||
case version
|
||||
case sourceURL = "source_url"
|
||||
case description
|
||||
case usage
|
||||
case thumbnail
|
||||
case languages
|
||||
case configuration
|
||||
case registrations
|
||||
case contact
|
||||
case rules
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceV2 {
|
||||
public struct Usage: Decodable, Sendable {
|
||||
public let users: Users
|
||||
}
|
||||
public struct Users: Decodable, Sendable {
|
||||
public let activeMonth: Int
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case activeMonth = "active_month"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceV2 {
|
||||
public struct Thumbnail: Decodable, Sendable {
|
||||
public let url: String
|
||||
public let blurhash: String?
|
||||
public let versions: ThumbnailVersions
|
||||
}
|
||||
|
||||
public struct ThumbnailVersions: Decodable, Sendable {
|
||||
public let oneX: String?
|
||||
public let twoX: String?
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case oneX = "@1x"
|
||||
case twoX = "@2x"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceV2 {
|
||||
public struct Configuration: Decodable, Sendable {
|
||||
public let urls: URLs
|
||||
public let accounts: Accounts
|
||||
public let statuses: InstanceV1.StatusesConfiguration
|
||||
public let mediaAttachments: InstanceV1.MediaAttachmentsConfiguration
|
||||
public let polls: InstanceV1.PollsConfiguration
|
||||
public let translation: Translation
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case urls
|
||||
case accounts
|
||||
case statuses
|
||||
case mediaAttachments = "media_attachments"
|
||||
case polls
|
||||
case translation
|
||||
}
|
||||
}
|
||||
|
||||
public struct URLs: Decodable, Sendable {
|
||||
// the docs incorrectly say the key for this is "streaming_api"
|
||||
public let streaming: String
|
||||
}
|
||||
|
||||
public struct Accounts: Decodable, Sendable {
|
||||
public let maxFeaturedTags: Int
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case maxFeaturedTags = "max_featured_tags"
|
||||
}
|
||||
}
|
||||
|
||||
public struct Translation: Decodable, Sendable {
|
||||
public let enabled: Bool
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceV2 {
|
||||
public struct Registrations: Decodable, Sendable {
|
||||
public let enabled: Bool
|
||||
public let approvalRequired: Bool
|
||||
public let message: String?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case enabled
|
||||
case approvalRequired = "approval_required"
|
||||
case message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceV2 {
|
||||
public struct Contact: Decodable, Sendable {
|
||||
public let email: String
|
||||
public let account: Account
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
|
|||
self.instanceFeatures = InstanceFeatures()
|
||||
|
||||
Task { @MainActor in
|
||||
async let instance = try? await run(Client.getInstance()).0
|
||||
async let instance = try? await run(Client.getInstanceV1()).0
|
||||
async let nodeInfo: NodeInfo? = await withCheckedContinuation({ continuation in
|
||||
self.client.nodeInfo { response in
|
||||
switch response {
|
||||
|
@ -40,7 +40,7 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
|
|||
}
|
||||
})
|
||||
guard let instance = await instance else { return }
|
||||
self.instanceFeatures.update(instance: InstanceInfo(instance: instance), nodeInfo: await nodeInfo)
|
||||
self.instanceFeatures.update(instance: InstanceInfo(v1: instance), nodeInfo: await nodeInfo)
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
|
|
|
@ -53,7 +53,8 @@ class MastodonController: ObservableObject {
|
|||
let instanceFeatures = InstanceFeatures()
|
||||
|
||||
@Published private(set) var account: AccountMO?
|
||||
@Published private(set) var instance: Instance?
|
||||
@Published private(set) var instance: InstanceV1?
|
||||
@Published private var instanceV2: InstanceV2?
|
||||
@Published private(set) var instanceInfo: InstanceInfo!
|
||||
@Published private(set) var nodeInfo: NodeInfo!
|
||||
@Published private(set) var lists: [List] = []
|
||||
|
@ -63,7 +64,7 @@ class MastodonController: ObservableObject {
|
|||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
|
||||
private var pendingOwnInstanceRequestCallbacks = [(Result<InstanceV1, Client.Error>) -> Void]()
|
||||
private var ownInstanceRequest: URLSessionTask?
|
||||
|
||||
var loggedIn: Bool {
|
||||
|
@ -107,9 +108,14 @@ class MastodonController: ObservableObject {
|
|||
|
||||
$instance
|
||||
.compactMap { $0 }
|
||||
.sink { [unowned self] in
|
||||
self.updateActiveInstance(from: $0)
|
||||
self.instanceInfo = InstanceInfo(instance: $0)
|
||||
.combineLatest($instanceV2)
|
||||
.sink {[unowned self] (instance, v2) in
|
||||
var info = InstanceInfo(v1: instance)
|
||||
if let v2 {
|
||||
info.update(v2: v2)
|
||||
}
|
||||
self.instanceInfo = info
|
||||
self.updateActiveInstance(from: info)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
@ -217,6 +223,10 @@ class MastodonController: ObservableObject {
|
|||
|
||||
_ = try await (ownAccount, ownInstance)
|
||||
|
||||
if instanceFeatures.hasMastodonVersion(4, 0, 0) {
|
||||
async let _ = try? getOwnInstanceV2()
|
||||
}
|
||||
|
||||
loadLists()
|
||||
_ = await loadFilters()
|
||||
await loadServerPreferences()
|
||||
|
@ -277,7 +287,7 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
||||
func getOwnInstance(completion: ((InstanceV1) -> Void)? = nil) {
|
||||
getOwnInstanceInternal(retryAttempt: 0) {
|
||||
if case let .success(instance) = $0 {
|
||||
completion?(instance)
|
||||
|
@ -286,7 +296,7 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
|
||||
@MainActor
|
||||
func getOwnInstance() async throws -> Instance {
|
||||
func getOwnInstance() async throws -> InstanceV1 {
|
||||
return try await withCheckedThrowingContinuation({ continuation in
|
||||
getOwnInstanceInternal(retryAttempt: 0) { result in
|
||||
continuation.resume(with: result)
|
||||
|
@ -294,7 +304,7 @@ class MastodonController: ObservableObject {
|
|||
})
|
||||
}
|
||||
|
||||
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<Instance, Client.Error>) -> Void)?) {
|
||||
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<InstanceV1, Client.Error>) -> Void)?) {
|
||||
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
|
@ -306,7 +316,7 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
|
||||
if ownInstanceRequest == nil {
|
||||
let request = Client.getInstance()
|
||||
let request = Client.getInstanceV1()
|
||||
ownInstanceRequest = run(request) { (response) in
|
||||
switch response {
|
||||
case .failure(let error):
|
||||
|
@ -361,6 +371,10 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func getOwnInstanceV2() async throws {
|
||||
self.instanceV2 = try await client.run(Client.getInstanceV2()).0
|
||||
}
|
||||
|
||||
// MainActor because the accountPreferences instance is bound to the view context
|
||||
@MainActor
|
||||
private func loadServerPreferences() async {
|
||||
|
@ -376,13 +390,13 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func updateActiveInstance(from instance: Instance) {
|
||||
private func updateActiveInstance(from info: InstanceInfo) {
|
||||
persistentContainer.performBackgroundTask { context in
|
||||
if let existing = try? context.fetch(ActiveInstance.fetchRequest()).first {
|
||||
existing.update(from: instance)
|
||||
existing.update(from: info)
|
||||
} else {
|
||||
let new = ActiveInstance(context: context)
|
||||
new.update(from: instance)
|
||||
new.update(from: info)
|
||||
}
|
||||
if context.hasChanges {
|
||||
try? context.save()
|
||||
|
|
|
@ -22,18 +22,20 @@ public final class ActiveInstance: NSManagedObject {
|
|||
@NSManaged public var maxStatusCharacters: Int
|
||||
@NSManaged private var configurationData: Data?
|
||||
@NSManaged private var pollsConfigurationData: Data?
|
||||
@NSManaged public var translation: Bool
|
||||
|
||||
@LazilyDecoding(from: \ActiveInstance.configurationData, fallback: nil)
|
||||
public var configuration: Instance.Configuration?
|
||||
public var configuration: InstanceV1.Configuration?
|
||||
|
||||
@LazilyDecoding(from: \ActiveInstance.pollsConfigurationData, fallback: nil)
|
||||
public var pollsConfiguration: Instance.PollsConfiguration?
|
||||
public var pollsConfiguration: InstanceV1.PollsConfiguration?
|
||||
|
||||
func update(from instance: Instance) {
|
||||
self.version = instance.version
|
||||
self.maxStatusCharacters = instance.maxStatusCharacters ?? 500
|
||||
self.configuration = instance.configuration
|
||||
self.pollsConfiguration = instance.pollsConfiguration
|
||||
func update(from info: InstanceInfo) {
|
||||
self.version = info.version
|
||||
self.maxStatusCharacters = info.maxStatusCharacters ?? 500
|
||||
self.configuration = info.configuration
|
||||
self.pollsConfiguration = info.pollsConfiguration
|
||||
self.translation = info.translation
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +45,8 @@ extension InstanceInfo {
|
|||
version: activeInstance.version,
|
||||
maxStatusCharacters: activeInstance.maxStatusCharacters,
|
||||
configuration: activeInstance.configuration,
|
||||
pollsConfiguration: activeInstance.pollsConfiguration
|
||||
pollsConfiguration: activeInstance.pollsConfiguration,
|
||||
translation: activeInstance.translation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
<attribute name="configurationData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="maxStatusCharacters" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="pollsConfigurationData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="translation" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="version" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="Filter" representedClassName="FilterMO" syncable="YES">
|
||||
|
|
|
@ -162,7 +162,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
}
|
||||
|
||||
let client = Client(baseURL: url, session: .appDefault)
|
||||
let request = Client.getInstance()
|
||||
let request = Client.getInstanceV1()
|
||||
client.run(request) { (response) in
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
if snapshot.indexOfSection(.selected) != nil {
|
||||
|
@ -309,7 +309,7 @@ extension InstanceSelectorTableViewController {
|
|||
case recommendedInstances
|
||||
}
|
||||
enum Item: Equatable, Hashable {
|
||||
case selected(URL, Instance)
|
||||
case selected(URL, InstanceV1)
|
||||
case recommended(InstanceSelector.Instance)
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
|
|
|
@ -16,7 +16,7 @@ class InstanceTableViewCell: UITableViewCell {
|
|||
@IBOutlet weak var adultLabel: UILabel!
|
||||
@IBOutlet weak var descriptionTextView: ContentTextView!
|
||||
|
||||
var instance: Instance?
|
||||
var instance: InstanceV1?
|
||||
var selectorInstance: InstanceSelector.Instance?
|
||||
|
||||
var thumbnailURL: URL?
|
||||
|
@ -53,7 +53,7 @@ class InstanceTableViewCell: UITableViewCell {
|
|||
updateThumbnail(url: instance.proxiedThumbnailURL)
|
||||
}
|
||||
|
||||
func updateUI(instance: Instance) {
|
||||
func updateUI(instance: InstanceV1) {
|
||||
self.instance = instance
|
||||
self.selectorInstance = nil
|
||||
|
||||
|
|
Loading…
Reference in New Issue