diff --git a/AppShared/UserDefaults+Notification.swift b/AppShared/UserDefaults+Notification.swift
new file mode 100644
index 00000000..e743e70a
--- /dev/null
+++ b/AppShared/UserDefaults+Notification.swift
@@ -0,0 +1,40 @@
+//
+// UserDefaults+Notification.swift
+// AppShared
+//
+// Created by Cirno MainasuK on 2021-10-9.
+//
+
+import UIKit
+import CryptoKit
+
+extension UserDefaults {
+ // always use hash value (SHA256) from accessToken as key
+ private static func deriveKey(from accessToken: String, prefix: String) -> String {
+ let digest = SHA256.hash(data: Data(accessToken.utf8))
+ let bytes = [UInt8](digest)
+ let hex = bytes.toHexString()
+ let key = prefix + "@" + hex
+ return key
+ }
+
+ private static let notificationCountKeyPrefix = "notification_count"
+
+ public func getNotificationCountWithAccessToken(accessToken: String) -> Int {
+ let prefix = UserDefaults.notificationCountKeyPrefix
+ let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix)
+ return integer(forKey: key)
+ }
+
+ public func setNotificationCountWithAccessToken(accessToken: String, value: Int) {
+ let prefix = UserDefaults.notificationCountKeyPrefix
+ let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix)
+ setValue(value, forKey: key)
+ }
+
+ public func increaseNotificationCount(accessToken: String) {
+ let count = getNotificationCountWithAccessToken(accessToken: accessToken)
+ setNotificationCountWithAccessToken(accessToken: accessToken, value: count + 1)
+ }
+
+}
diff --git a/AppShared/UserDefaults.swift b/AppShared/UserDefaults.swift
index 67a3cf68..753a3284 100644
--- a/AppShared/UserDefaults.swift
+++ b/AppShared/UserDefaults.swift
@@ -6,39 +6,8 @@
//
import UIKit
-import CryptoKit
extension UserDefaults {
public static let shared = UserDefaults(suiteName: AppName.groupID)!
}
-extension UserDefaults {
- // always use hash value (SHA256) from accessToken as key
- private static func deriveKey(from accessToken: String, prefix: String) -> String {
- let digest = SHA256.hash(data: Data(accessToken.utf8))
- let bytes = [UInt8](digest)
- let hex = bytes.toHexString()
- let key = prefix + "@" + hex
- return key
- }
-
- private static let notificationCountKeyPrefix = "notification_count"
-
- public func getNotificationCountWithAccessToken(accessToken: String) -> Int {
- let prefix = UserDefaults.notificationCountKeyPrefix
- let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix)
- return integer(forKey: key)
- }
-
- public func setNotificationCountWithAccessToken(accessToken: String, value: Int) {
- let prefix = UserDefaults.notificationCountKeyPrefix
- let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix)
- setValue(value, forKey: key)
- }
-
- public func increaseNotificationCount(accessToken: String) {
- let count = getNotificationCountWithAccessToken(accessToken: accessToken)
- setNotificationCountWithAccessToken(accessToken: accessToken, value: count + 1)
- }
-
-}
diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData 2.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData 2.xcdatamodel/contents
index c26a0bdb..6d576ca1 100644
--- a/CoreDataStack/CoreData.xcdatamodeld/CoreData 2.xcdatamodel/contents
+++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData 2.xcdatamodel/contents
@@ -63,6 +63,13 @@
+
+
+
+
+
+
+
@@ -75,6 +82,7 @@
+
@@ -280,7 +288,7 @@
-
+
@@ -293,5 +301,6 @@
+
\ No newline at end of file
diff --git a/CoreDataStack/Entity/Instance.swift b/CoreDataStack/Entity/Instance.swift
new file mode 100644
index 00000000..8976097e
--- /dev/null
+++ b/CoreDataStack/Entity/Instance.swift
@@ -0,0 +1,70 @@
+//
+// Instance.swift
+// CoreDataStack
+//
+// Created by Cirno MainasuK on 2021-10-9.
+//
+
+import Foundation
+import CoreData
+
+public final class Instance: NSManagedObject {
+ @NSManaged public var domain: String
+
+ @NSManaged public private(set) var createdAt: Date
+ @NSManaged public private(set) var updatedAt: Date
+
+ @NSManaged public private(set) var configurationRaw: Data?
+
+ // MARK: one-to-many relationships
+ @NSManaged public var authentications: Set
+}
+
+extension Instance {
+ public override func awakeFromInsert() {
+ super.awakeFromInsert()
+ let now = Date()
+ setPrimitiveValue(now, forKey: #keyPath(Instance.createdAt))
+ setPrimitiveValue(now, forKey: #keyPath(Instance.updatedAt))
+ }
+
+ @discardableResult
+ public static func insert(
+ into context: NSManagedObjectContext,
+ property: Property
+ ) -> Instance {
+ let instance: Instance = context.insertObject()
+ instance.domain = property.domain
+ return instance
+ }
+
+ public func update(configurationRaw: Data?) {
+ self.configurationRaw = configurationRaw
+ }
+
+ public func didUpdate(at networkDate: Date) {
+ self.updatedAt = networkDate
+ }
+}
+
+extension Instance {
+ public struct Property {
+ public let domain: String
+
+ public init(domain: String) {
+ self.domain = domain
+ }
+ }
+}
+
+extension Instance: Managed {
+ public static var defaultSortDescriptors: [NSSortDescriptor] {
+ return [NSSortDescriptor(keyPath: \Instance.createdAt, ascending: false)]
+ }
+}
+
+extension Instance {
+ public static func predicate(domain: String) -> NSPredicate {
+ return NSPredicate(format: "%K == %@", #keyPath(Instance.domain), domain)
+ }
+}
diff --git a/CoreDataStack/Entity/MastodonAuthentication.swift b/CoreDataStack/Entity/MastodonAuthentication.swift
index 0ee0e343..66b8ad6a 100644
--- a/CoreDataStack/Entity/MastodonAuthentication.swift
+++ b/CoreDataStack/Entity/MastodonAuthentication.swift
@@ -30,6 +30,9 @@ final public class MastodonAuthentication: NSManagedObject {
// one-to-one relationship
@NSManaged public private(set) var user: MastodonUser
+ // many-to-one relationship
+ @NSManaged public private(set) var instance: Instance?
+
}
extension MastodonAuthentication {
@@ -97,6 +100,12 @@ extension MastodonAuthentication {
}
}
+ public func update(instance: Instance) {
+ if self.instance != instance {
+ self.instance = instance
+ }
+ }
+
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}
@@ -143,7 +152,7 @@ extension MastodonAuthentication: Managed {
extension MastodonAuthentication {
- static func predicate(domain: String) -> NSPredicate {
+ public static func predicate(domain: String) -> NSPredicate {
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.domain), domain)
}
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index 674821cd..a1283ea0 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -356,6 +356,11 @@
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73B48F261F030A002E9E9F /* SafariActivity.swift */; };
+ DB73BF3B2711885500781945 /* UserDefaults+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */; };
+ DB73BF4127118B6D00781945 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF4027118B6D00781945 /* Instance.swift */; };
+ DB73BF43271192BB00781945 /* InstanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF42271192BB00781945 /* InstanceService.swift */; };
+ DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */; };
+ DB73BF47271199CA00781945 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF46271199CA00781945 /* Instance.swift */; };
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */; };
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
@@ -454,7 +459,6 @@
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC6482267D0B21007FE9FD /* DifferenceKit */; };
DBAC6485267D0F9E007FE9FD /* StatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6484267D0F9E007FE9FD /* StatusNode.swift */; };
DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6487267D388B007FE9FD /* ASTableNode.swift */; };
- DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */; };
DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */; };
DBAC6497267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */; };
DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */; };
@@ -1144,6 +1148,11 @@
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; };
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; };
DB73B48F261F030A002E9E9F /* SafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariActivity.swift; sourceTree = ""; };
+ DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Notification.swift"; sourceTree = ""; };
+ DB73BF4027118B6D00781945 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; };
+ DB73BF42271192BB00781945 /* InstanceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceService.swift; sourceTree = ""; };
+ DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Instance.swift"; sourceTree = ""; };
+ DB73BF46271199CA00781945 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; };
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = ""; };
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; };
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; };
@@ -1270,7 +1279,6 @@
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; };
DBAC6484267D0F9E007FE9FD /* StatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusNode.swift; sourceTree = ""; };
DBAC6487267D388B007FE9FD /* ASTableNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTableNode.swift; sourceTree = ""; };
- DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSDiffableDataSourceSnapshot.swift; sourceTree = ""; };
DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableNodeDiffableDataSource.swift; sourceTree = ""; };
DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderNode.swift; sourceTree = ""; };
DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderNode.swift; sourceTree = ""; };
@@ -1771,6 +1779,7 @@
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
+ DB73BF42271192BB00781945 /* InstanceService.swift */,
);
path = Service;
sourceTree = "";
@@ -2079,6 +2088,7 @@
DBAFB7342645463500371D5F /* Emojis.swift */,
DBA94439265CC0FC00C537E1 /* Fields.swift */,
DBA1DB7F268F84F80052DB59 /* NotificationType.swift */,
+ DB73BF46271199CA00781945 /* Instance.swift */,
);
path = CoreDataStack;
sourceTree = "";
@@ -2280,6 +2290,7 @@
2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */,
DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */,
5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */,
+ DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */,
);
path = CoreData;
sourceTree = "";
@@ -2454,6 +2465,7 @@
DB6804912637CD8700430867 /* AppName.swift */,
DB6804FC2637CFEC00430867 /* AppSecret.swift */,
DB6804D02637CE4700430867 /* UserDefaults.swift */,
+ DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */,
);
path = AppShared;
sourceTree = "";
@@ -2636,6 +2648,7 @@
5B90C46D26259B2C0002E742 /* Setting.swift */,
5B90C46C26259B2C0002E742 /* Subscription.swift */,
5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */,
+ DB73BF4027118B6D00781945 /* Instance.swift */,
);
path = Entity;
sourceTree = "";
@@ -2716,7 +2729,6 @@
DB0E91E926A9675100BD2ACC /* MetaLabel.swift */,
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */,
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */,
- DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */,
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
2D939AB425EDD8A90076FA61 /* String.swift */,
DB68A06225E905E000CFDF14 /* UIApplication.swift */,
@@ -3943,7 +3955,6 @@
DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */,
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
- DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */,
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
@@ -3993,6 +4004,7 @@
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */,
2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */,
+ DB73BF47271199CA00781945 /* Instance.swift in Sources */,
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
@@ -4015,6 +4027,7 @@
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
+ DB73BF43271192BB00781945 /* InstanceService.swift in Sources */,
DBA9443A265CC0FC00C537E1 /* Fields.swift in Sources */,
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */,
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */,
@@ -4190,6 +4203,7 @@
DB0C946B26A700AB0088FB11 /* MastodonUser+Property.swift in Sources */,
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
+ DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */,
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
@@ -4304,6 +4318,7 @@
buildActionMask = 2147483647;
files = (
DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */,
+ DB73BF3B2711885500781945 /* UserDefaults+Notification.swift in Sources */,
DB4932B726F30F0700EF46D4 /* Array.swift in Sources */,
DB6804922637CD8700430867 /* AppName.swift in Sources */,
DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */,
@@ -4324,6 +4339,7 @@
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
DBCC3B9B261584A00045B23D /* PrivateNote.swift in Sources */,
DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */,
+ DB73BF4127118B6D00781945 /* Instance.swift in Sources */,
DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */,
DB89BA1B25C1107F008580ED /* Collection.swift in Sources */,
DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */,
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index b0f597fa..b5ecab28 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,12 +7,12 @@
AppShared.xcscheme_^#shared#^_
orderHint
- 44
+ 35
CoreDataStack.xcscheme_^#shared#^_
orderHint
- 42
+ 37
Mastodon - ASDK.xcscheme_^#shared#^_
@@ -37,7 +37,7 @@
Mastodon - ca.xcscheme_^#shared#^_
orderHint
- 16
+ 18
Mastodon - de.xcscheme_^#shared#^_
@@ -67,7 +67,7 @@
Mastodon - jp.xcscheme_^#shared#^_
orderHint
- 14
+ 15
Mastodon - nl.xcscheme_^#shared#^_
@@ -87,7 +87,7 @@
Mastodon - zh_Hans.xcscheme_^#shared#^_
orderHint
- 15
+ 16
Mastodon.xcscheme_^#shared#^_
@@ -97,7 +97,7 @@
MastodonIntent.xcscheme_^#shared#^_
orderHint
- 41
+ 36
MastodonIntents.xcscheme_^#shared#^_
@@ -117,7 +117,7 @@
ShareActionExtension.xcscheme_^#shared#^_
orderHint
- 43
+ 38
SuppressBuildableAutocreation
diff --git a/Mastodon/Extension/CoreDataStack/Instance.swift b/Mastodon/Extension/CoreDataStack/Instance.swift
new file mode 100644
index 00000000..6cacd9db
--- /dev/null
+++ b/Mastodon/Extension/CoreDataStack/Instance.swift
@@ -0,0 +1,25 @@
+//
+// Instance.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-10-9.
+//
+
+import UIKit
+import CoreDataStack
+import MastodonSDK
+
+extension Instance {
+ var configuration: Mastodon.Entity.Instance.Configuration? {
+ guard let configurationRaw = configurationRaw else { return nil }
+ guard let configuration = try? JSONDecoder().decode(Mastodon.Entity.Instance.Configuration.self, from: configurationRaw) else {
+ return nil
+ }
+
+ return configuration
+ }
+
+ static func encode(configuration: Mastodon.Entity.Instance.Configuration) -> Data? {
+ return try? JSONEncoder().encode(configuration)
+ }
+}
diff --git a/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift b/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift
deleted file mode 100644
index c2ff341d..00000000
--- a/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// NSDiffableDataSourceSnapshot.swift
-// Mastodon
-//
-// Created by Cirno MainasuK on 2021-6-19.
-//
-
-import UIKit
-
-//extension NSDiffableDataSourceSnapshot {
-// func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? {
-// guard 0..()
@@ -38,6 +37,24 @@ final class ComposeViewModel: NSObject {
var isViewAppeared = false
// output
+ let instanceConfiguration: Mastodon.Entity.Instance.Configuration?
+ var composeContentLimit: Int {
+ guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 }
+ return max(1, maxCharacters)
+ }
+ var maxMediaAttachments: Int {
+ guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else {
+ return 4
+ }
+ // FIXME: update timeline media preview UI
+ return min(4, max(1, maxMediaAttachments))
+ // return max(1, maxMediaAttachments)
+ }
+ var maxPollOptions: Int {
+ guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 }
+ return max(2, maxOptions)
+ }
+
let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell()
let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell()
let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell()
@@ -128,8 +145,12 @@ final class ComposeViewModel: NSObject {
}
return CurrentValueSubject(visibility)
}()
- self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
+ let _activeAuthentication = context.authenticationService.activeMastodonAuthentication.value
+ self.activeAuthentication = CurrentValueSubject(_activeAuthentication)
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
+ // set limit
+ let _instanceConfiguration = _activeAuthentication?.instance?.configuration
+ self.instanceConfiguration = _instanceConfiguration
super.init()
// end init
@@ -243,8 +264,9 @@ final class ComposeViewModel: NSObject {
let isComposeContentEmpty = composeStatusAttribute.composeContent
.map { ($0 ?? "").isEmpty }
let isComposeContentValid = characterCount
- .map { characterCount -> Bool in
- return characterCount <= ComposeViewModel.composeContentLimit
+ .compactMap { [weak self] characterCount -> Bool in
+ guard let self = self else { return characterCount <= 500 }
+ return characterCount <= self.composeContentLimit
}
let isMediaEmpty = attachmentServices
.map { $0.isEmpty }
@@ -381,7 +403,7 @@ final class ComposeViewModel: NSObject {
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
guard let self = self else { return }
- let shouldMediaDisable = isPollComposing || attachmentServices.count >= 4
+ let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments
let shouldPollDisable = attachmentServices.count > 0
self.isMediaToolbarButtonEnabled.value = !shouldMediaDisable
@@ -455,7 +477,7 @@ extension ComposeViewModel {
extension ComposeViewModel {
func createNewPollOptionIfPossible() {
- guard pollOptionAttributes.value.count < 4 else { return }
+ guard pollOptionAttributes.value.count < maxPollOptions else { return }
let attribute = ComposeStatusPollItem.PollOptionAttribute()
pollOptionAttributes.value = pollOptionAttributes.value + [attribute]
@@ -488,7 +510,7 @@ extension ComposeViewModel {
// check exclusive limit:
// - up to 1 video
- // - up to 4 photos
+ // - up to N photos
func checkAttachmentPrecondition() throws {
let attachmentServices = self.attachmentServices.value
guard !attachmentServices.isEmpty else { return }
diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Instance.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Instance.swift
new file mode 100644
index 00000000..614d098a
--- /dev/null
+++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Instance.swift
@@ -0,0 +1,76 @@
+//
+// APIService+CoreData+Instance.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-10-9.
+//
+
+import os.log
+import Foundation
+import CoreData
+import CoreDataStack
+import MastodonSDK
+
+extension APIService.CoreData {
+
+ static func createOrMergeInstance(
+ into managedObjectContext: NSManagedObjectContext,
+ domain: String,
+ entity: Mastodon.Entity.Instance,
+ networkDate: Date,
+ log: OSLog
+ ) -> (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 {
+ // merge old
+ APIService.CoreData.merge(
+ instance: old,
+ entity: entity,
+ domain: domain,
+ networkDate: networkDate
+ )
+ return (old, false)
+ } else {
+ let instance = Instance.insert(
+ into: managedObjectContext,
+ property: Instance.Property(domain: domain)
+ )
+ let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) }
+ instance.update(configurationRaw: configurationRaw)
+
+ return (instance, true)
+ }
+ }
+
+}
+
+extension APIService.CoreData {
+
+ static func merge(
+ instance: Instance,
+ entity: Mastodon.Entity.Instance,
+ domain: String,
+ networkDate: Date
+ ) {
+ guard networkDate > instance.updatedAt else { return }
+
+ let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) }
+ instance.update(configurationRaw: configurationRaw)
+
+ instance.didUpdate(at: networkDate)
+ }
+
+}
diff --git a/Mastodon/Service/InstanceService.swift b/Mastodon/Service/InstanceService.swift
new file mode 100644
index 00000000..4fb6309f
--- /dev/null
+++ b/Mastodon/Service/InstanceService.swift
@@ -0,0 +1,103 @@
+//
+// InstanceService.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-10-9.
+//
+
+import os.log
+import Foundation
+import Combine
+import CoreData
+import CoreDataStack
+import MastodonSDK
+
+final class InstanceService {
+
+ var disposeBag = Set()
+
+ let logger = Logger(subsystem: "InstanceService", category: "Logic")
+
+ // input
+ let backgroundManagedObjectContext: NSManagedObjectContext
+ weak var apiService: APIService?
+ weak var authenticationService: AuthenticationService?
+
+ // output
+
+ init(
+ apiService: APIService,
+ authenticationService: AuthenticationService
+ ) {
+ self.backgroundManagedObjectContext = apiService.backgroundManagedObjectContext
+ self.apiService = apiService
+ self.authenticationService = authenticationService
+
+ authenticationService.activeMastodonAuthenticationBox
+ .receive(on: DispatchQueue.main)
+ .compactMap { $0?.domain }
+ .removeDuplicates() // prevent infinity loop
+ .sink { [weak self] domain in
+ guard let self = self else { return }
+ self.updateInstance(domain: domain)
+ }
+ .store(in: &disposeBag)
+ }
+
+}
+
+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: OSLog.api
+ )
+
+ // 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 -> Mastodon.Response.Content in
+ switch result {
+ case .success:
+ return response
+ case .failure(let error):
+ throw error
+ }
+ }
+ .eraseToAnyPublisher()
+ }
+ .sink { [weak self] completion in
+ guard let self = self else { return }
+ switch completion {
+ case .failure(let error):
+ self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Instance] update instance failure: \(error.localizedDescription)")
+ case .finished:
+ self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Instance] update instance for domain: \(domain)")
+ }
+ } receiveValue: { [weak self] response in
+ guard let self = self else { return }
+ // do nothing
+ }
+ .store(in: &disposeBag)
+ }
+}
diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift
index d4682ed5..d7c08d47 100644
--- a/Mastodon/State/AppContext.swift
+++ b/Mastodon/State/AppContext.swift
@@ -31,6 +31,7 @@ class AppContext: ObservableObject {
let statusPublishService = StatusPublishService()
let notificationService: NotificationService
let settingService: SettingService
+ let instanceService: InstanceService
let blockDomainService: BlockDomainService
let statusFilterService: StatusFilterService
@@ -87,6 +88,11 @@ class AppContext: ObservableObject {
notificationService: _notificationService
)
+ instanceService = InstanceService(
+ apiService: _apiService,
+ authenticationService: _authenticationService
+ )
+
blockDomainService = BlockDomainService(
backgroundManagedObjectContext: _backgroundManagedObjectContext,
authenticationService: _authenticationService
diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift
index 226af40f..d0d16ee4 100644
--- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift
+++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift
@@ -34,6 +34,9 @@ extension Mastodon.Entity {
public let thumbnail: String?
public let contactAccount: Account?
public let rules: [Rule]?
+
+ // https://github.com/mastodon/mastodon/pull/16485
+ public let configuration: Configuration?
enum CodingKeys: String, CodingKey {
case uri
@@ -52,6 +55,8 @@ extension Mastodon.Entity {
case thumbnail
case contactAccount = "contact_account"
case rules
+
+ case configuration
}
}
}
@@ -86,3 +91,63 @@ extension Mastodon.Entity.Instance {
public let text: String
}
}
+
+extension Mastodon.Entity.Instance {
+ public struct Configuration: Codable {
+ public let statuses: Statuses?
+ public let mediaAttachments: MediaAttachments?
+ public let polls: Polls?
+
+ enum CodingKeys: String, CodingKey {
+ case statuses
+ case mediaAttachments = "media_attachments"
+ case polls
+ }
+ }
+}
+
+extension Mastodon.Entity.Instance.Configuration {
+ 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"
+ }
+ }
+}