diff --git a/Packages/InstanceFeatures/.gitignore b/Packages/InstanceFeatures/.gitignore new file mode 100644 index 00000000..3b298120 --- /dev/null +++ b/Packages/InstanceFeatures/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Packages/InstanceFeatures/Package.resolved b/Packages/InstanceFeatures/Package.resolved new file mode 100644 index 00000000..a9ae5ab1 --- /dev/null +++ b/Packages/InstanceFeatures/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-url", + "kind" : "remoteSourceControl", + "location" : "https://github.com/karwa/swift-url.git", + "state" : { + "branch" : "main", + "revision" : "220f6ab9d8a7e0742f85eb9f21b745942e001ae6" + } + } + ], + "version" : 2 +} diff --git a/Packages/InstanceFeatures/Package.swift b/Packages/InstanceFeatures/Package.swift new file mode 100644 index 00000000..1927a00b --- /dev/null +++ b/Packages/InstanceFeatures/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "InstanceFeatures", + platforms: [ + .iOS(.v15), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "InstanceFeatures", + targets: ["InstanceFeatures"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(path: "../Pachyderm"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "InstanceFeatures", + dependencies: ["Pachyderm"]), + .testTarget( + name: "InstanceFeaturesTests", + dependencies: ["InstanceFeatures"]), + ] +) diff --git a/Packages/InstanceFeatures/README.md b/Packages/InstanceFeatures/README.md new file mode 100644 index 00000000..d9dc107e --- /dev/null +++ b/Packages/InstanceFeatures/README.md @@ -0,0 +1,3 @@ +# InstanceFeatures + +A description of this package. diff --git a/Tusker/API/InstanceFeatures.swift b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift similarity index 84% rename from Tusker/API/InstanceFeatures.swift rename to Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift index 23e3700e..c8760bc8 100644 --- a/Tusker/API/InstanceFeatures.swift +++ b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift @@ -7,17 +7,20 @@ // import Foundation +import Combine import Pachyderm -import Sentry -struct InstanceFeatures { +public class InstanceFeatures: ObservableObject { private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; pleroma (.*)\\)", options: .caseInsensitive) private static let akkomaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; akkoma (.*)\\)", options: .caseInsensitive) - private var instanceType: InstanceType = .mastodon(.vanilla, nil) - private(set) var maxStatusChars = 500 + private let _featuresUpdated = PassthroughSubject() + public var featuresUpdated: some Publisher { _featuresUpdated } - var localOnlyPosts: Bool { + @Published private var instanceType: InstanceType = .mastodon(.vanilla, nil) + @Published public private(set) var maxStatusChars = 500 + + public var localOnlyPosts: Bool { switch instanceType { case .mastodon(.hometown(_), _), .mastodon(.glitch, _): return true @@ -26,19 +29,19 @@ struct InstanceFeatures { } } - var mastodonAttachmentRestrictions: Bool { + public var mastodonAttachmentRestrictions: Bool { instanceType.isMastodon } - var pollsAndAttachments: Bool { + public var pollsAndAttachments: Bool { instanceType.isPleroma } - var boostToOriginalAudience: Bool { + public var boostToOriginalAudience: Bool { instanceType.isPleroma || instanceType.isMastodon } - var profilePinnedStatuses: Bool { + public var profilePinnedStatuses: Bool { switch instanceType { case .pixelfed: return false @@ -47,24 +50,24 @@ struct InstanceFeatures { } } - var trends: Bool { + public var trends: Bool { instanceType.isMastodon } - var profileSuggestions: Bool { + public var profileSuggestions: Bool { instanceType.isMastodon && hasMastodonVersion(3, 4, 0) } - var trendingStatusesAndLinks: Bool { + public var trendingStatusesAndLinks: Bool { instanceType.isMastodon && hasMastodonVersion(3, 5, 0) } - var reblogVisibility: Bool { + public var reblogVisibility: Bool { (instanceType.isMastodon && hasMastodonVersion(2, 8, 0)) || (instanceType.isPleroma && hasPleromaVersion(2, 0, 0)) } - var probablySupportsMarkdown: Bool { + public var probablySupportsMarkdown: Bool { switch instanceType { case .pleroma(_), .mastodon(.glitch, _), .mastodon(.hometown(_), _): return true @@ -73,7 +76,7 @@ struct InstanceFeatures { } } - var needsLocalOnlyEmojiHack: Bool { + public var needsLocalOnlyEmojiHack: Bool { if case .mastodon(.glitch, _) = instanceType { return true } else { @@ -81,7 +84,7 @@ struct InstanceFeatures { } } - var needsWideColorGamutHack: Bool { + public var needsWideColorGamutHack: Bool { if case .mastodon(_, .some(let version)) = instanceType { return version < Version(4, 0, 0) } else { @@ -89,23 +92,26 @@ struct InstanceFeatures { } } - var canFollowHashtags: Bool { + public var canFollowHashtags: Bool { hasMastodonVersion(4, 0, 0) } - var filtersV2: Bool { + public var filtersV2: Bool { hasMastodonVersion(4, 0, 0) } - var notificationsAllowedTypes: Bool { + public var notificationsAllowedTypes: Bool { hasMastodonVersion(3, 5, 0) } - var pollVotersCount: Bool { + public var pollVotersCount: Bool { instanceType.isMastodon } - mutating func update(instance: Instance, nodeInfo: NodeInfo?) { + public init() { + } + + public func update(instance: Instance, nodeInfo: NodeInfo?) { let ver = instance.version.lowercased() if ver.contains("glitch") { instanceType = .mastodon(.glitch, Version(string: ver)) @@ -150,10 +156,10 @@ struct InstanceFeatures { maxStatusChars = instance.maxStatusCharacters ?? 500 - setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo) + _featuresUpdated.send() } - func hasMastodonVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool { + public func hasMastodonVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool { if case .mastodon(_, .some(let version)) = instanceType { return version >= Version(major, minor, patch) } else { @@ -259,19 +265,3 @@ extension InstanceFeatures { } } } - -private func setInstanceBreadcrumb(instance: Instance, nodeInfo: NodeInfo?) { - let crumb = Breadcrumb(level: .info, category: "MastodonController") - crumb.data = [ - "instance": [ - "version": instance.version - ], - ] - if let nodeInfo { - crumb.data!["nodeInfo"] = [ - "software": nodeInfo.software.name, - "version": nodeInfo.software.version, - ] - } - SentrySDK.addBreadcrumb(crumb) -} diff --git a/TuskerTests/VersionTests.swift b/Packages/InstanceFeatures/Tests/InstanceFeaturesTests/VersionTests.swift similarity index 91% rename from TuskerTests/VersionTests.swift rename to Packages/InstanceFeatures/Tests/InstanceFeaturesTests/VersionTests.swift index a8f726f9..bdfcfde1 100644 --- a/TuskerTests/VersionTests.swift +++ b/Packages/InstanceFeatures/Tests/InstanceFeaturesTests/VersionTests.swift @@ -1,13 +1,13 @@ // // VersionTests.swift -// TuskerTests +// InstanceFeaturesTests // // Created by Shadowfacts on 4/2/22. // Copyright © 2022 Shadowfacts. All rights reserved. // import XCTest -@testable import Tusker +@testable import InstanceFeatures class VersionTests: XCTestCase { diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index cd4275d7..dcf4216a 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -113,7 +113,6 @@ D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; }; D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9984279CA23900C26176 /* URLSession+Development.swift */; }; D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */; }; - D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9988279DB2D100C26176 /* InstanceFeatures.swift */; }; D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; }; D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; }; D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; }; @@ -373,6 +372,7 @@ D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A556291F4F1600F496A8 /* MuteAccountView.swift */; }; D6F6A5592920676800F496A8 /* ComposeToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F6A5582920676800F496A8 /* ComposeToolbar.swift */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; + D6FA94E129B52898006AAC51 /* InstanceFeatures in Frameworks */ = {isa = PBXBuildFile; productRef = D6FA94E029B52898006AAC51 /* InstanceFeatures */; }; D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; }; /* End PBXBuildFile section */ @@ -530,7 +530,6 @@ D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = ""; }; D62E9984279CA23900C26176 /* URLSession+Development.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Development.swift"; sourceTree = ""; }; D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = ""; }; - D62E9988279DB2D100C26176 /* InstanceFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceFeatures.swift; sourceTree = ""; }; D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = ""; }; D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = ""; }; D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = ""; }; @@ -803,6 +802,7 @@ D6F6A556291F4F1600F496A8 /* MuteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteAccountView.swift; sourceTree = ""; }; D6F6A5582920676800F496A8 /* ComposeToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbar.swift; sourceTree = ""; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = ""; }; + D6FA94DF29B52891006AAC51 /* InstanceFeatures */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = InstanceFeatures; path = Packages/InstanceFeatures; sourceTree = ""; }; D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -811,6 +811,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D6FA94E129B52898006AAC51 /* InstanceFeatures in Frameworks */, D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */, D659F35E2953A212002D944A /* TTTKit in Frameworks */, D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */, @@ -1539,6 +1540,7 @@ D6BEA243291A0C83002F4D01 /* Duckable */, D68A76F22953915C001DA1B3 /* TTTKit */, D6B0026C29B5245400C70BE2 /* UserAccounts */, + D6FA94DF29B52891006AAC51 /* InstanceFeatures */, D6D4DDCE212518A000E1C4BB /* Tusker */, D6D4DDE3212518A200E1C4BB /* TuskerTests */, D6D4DDEE212518A200E1C4BB /* TuskerUITests */, @@ -1694,7 +1696,6 @@ D6F953F121251A2F00CF0F2B /* API */ = { isa = PBXGroup; children = ( - D62E9988279DB2D100C26176 /* InstanceFeatures.swift */, D6F953EF21251A2900CF0F2B /* MastodonController.swift */, D6E9CDA7281A427800BBC98E /* PostService.swift */, D61ABEFD28F1C92600B29151 /* FavoriteService.swift */, @@ -1743,6 +1744,7 @@ D6BEA244291A0EDE002F4D01 /* Duckable */, D659F35D2953A212002D944A /* TTTKit */, D6B0026D29B5248800C70BE2 /* UserAccounts */, + D6FA94E029B52898006AAC51 /* InstanceFeatures */, ); productName = Tusker; productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */; @@ -1999,7 +2001,6 @@ D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */, - D62E9989279DB2D100C26176 /* InstanceFeatures.swift in Sources */, D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */, @@ -2975,6 +2976,10 @@ isa = XCSwiftPackageProductDependency; productName = Duckable; }; + D6FA94E029B52898006AAC51 /* InstanceFeatures */ = { + isa = XCSwiftPackageProductDependency; + productName = InstanceFeatures; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Tusker/API/MastodonController.swift b/Tusker/API/MastodonController.swift index 489118fd..76d80669 100644 --- a/Tusker/API/MastodonController.swift +++ b/Tusker/API/MastodonController.swift @@ -10,6 +10,8 @@ import Foundation import Pachyderm import Combine import UserAccounts +import InstanceFeatures +import Sentry class MastodonController: ObservableObject { @@ -48,11 +50,11 @@ class MastodonController: ObservableObject { var accountPreferences: AccountPreferences! let client: Client! + let instanceFeatures = InstanceFeatures() @Published private(set) var account: Account! @Published private(set) var instance: Instance! @Published private(set) var nodeInfo: NodeInfo! - @Published private(set) var instanceFeatures = InstanceFeatures() @Published private(set) var lists: [List] = [] @Published private(set) var customEmojis: [Emoji]? @Published private(set) var followedHashtags: [FollowedHashtag] = [] @@ -84,11 +86,12 @@ class MastodonController: ObservableObject { } .sink { [unowned self] (instance, nodeInfo) in self.instanceFeatures.update(instance: instance, nodeInfo: nodeInfo) + setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo) } .store(in: &cancellables) - $instanceFeatures - .filter { [unowned self] in $0.canFollowHashtags && self.followedHashtags.isEmpty } + instanceFeatures.featuresUpdated + .filter { [unowned self] _ in self.instanceFeatures.canFollowHashtags && self.followedHashtags.isEmpty } .sink { [unowned self] _ in Task { await self.loadFollowedHashtags() @@ -454,3 +457,19 @@ class MastodonController: ObservableObject { } } + +private func setInstanceBreadcrumb(instance: Instance, nodeInfo: NodeInfo?) { + let crumb = Breadcrumb(level: .info, category: "MastodonController") + crumb.data = [ + "instance": [ + "version": instance.version + ], + ] + if let nodeInfo { + crumb.data!["nodeInfo"] = [ + "software": nodeInfo.software.name, + "version": nodeInfo.software.version, + ] + } + SentrySDK.addBreadcrumb(crumb) +} diff --git a/Tusker/Models/CompositionAttachmentData.swift b/Tusker/Models/CompositionAttachmentData.swift index 10fec3d8..112b974a 100644 --- a/Tusker/Models/CompositionAttachmentData.swift +++ b/Tusker/Models/CompositionAttachmentData.swift @@ -10,6 +10,7 @@ import UIKit import Photos import UniformTypeIdentifiers import PencilKit +import InstanceFeatures enum CompositionAttachmentData { case asset(PHAsset)