feat: dynamic set compose post character limit. resolve #222
This commit is contained in:
parent
7113fc037c
commit
1eb9812588
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -63,6 +63,13 @@
|
|||
<attribute name="userID" attributeType="String"/>
|
||||
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="homeTimelineIndexes" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="Instance" representedClassName=".Instance" syncable="YES">
|
||||
<attribute name="configurationRaw" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
|
||||
</entity>
|
||||
<entity name="MastodonAuthentication" representedClassName=".MastodonAuthentication" syncable="YES">
|
||||
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="appAccessToken" attributeType="String"/>
|
||||
|
@ -75,6 +82,7 @@
|
|||
<attribute name="userAccessToken" attributeType="String"/>
|
||||
<attribute name="userID" attributeType="String"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="instance" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Instance" inverseName="authentications" inverseEntity="Instance"/>
|
||||
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="MastodonNotification" representedClassName=".MastodonNotification" syncable="YES">
|
||||
|
@ -280,7 +288,7 @@
|
|||
<element name="Emoji" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="History" positionX="0" positionY="0" width="128" height="119"/>
|
||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="224"/>
|
||||
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="734"/>
|
||||
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
||||
|
@ -293,5 +301,6 @@
|
|||
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
|
||||
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="Instance" positionX="45" positionY="162" width="128" height="104"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -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<MastodonAuthentication>
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
||||
DB73B48F261F030A002E9E9F /* SafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariActivity.swift; sourceTree = "<group>"; };
|
||||
DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Notification.swift"; sourceTree = "<group>"; };
|
||||
DB73BF4027118B6D00781945 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
|
||||
DB73BF42271192BB00781945 /* InstanceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceService.swift; sourceTree = "<group>"; };
|
||||
DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Instance.swift"; sourceTree = "<group>"; };
|
||||
DB73BF46271199CA00781945 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
|
||||
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = "<group>"; };
|
||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1270,7 +1279,6 @@
|
|||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
||||
DBAC6484267D0F9E007FE9FD /* StatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusNode.swift; sourceTree = "<group>"; };
|
||||
DBAC6487267D388B007FE9FD /* ASTableNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTableNode.swift; sourceTree = "<group>"; };
|
||||
DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSDiffableDataSourceSnapshot.swift; sourceTree = "<group>"; };
|
||||
DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableNodeDiffableDataSource.swift; sourceTree = "<group>"; };
|
||||
DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderNode.swift; sourceTree = "<group>"; };
|
||||
DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderNode.swift; sourceTree = "<group>"; };
|
||||
|
@ -1771,6 +1779,7 @@
|
|||
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
|
||||
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
|
||||
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
|
||||
DB73BF42271192BB00781945 /* InstanceService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2079,6 +2088,7 @@
|
|||
DBAFB7342645463500371D5F /* Emojis.swift */,
|
||||
DBA94439265CC0FC00C537E1 /* Fields.swift */,
|
||||
DBA1DB7F268F84F80052DB59 /* NotificationType.swift */,
|
||||
DB73BF46271199CA00781945 /* Instance.swift */,
|
||||
);
|
||||
path = CoreDataStack;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -2454,6 +2465,7 @@
|
|||
DB6804912637CD8700430867 /* AppName.swift */,
|
||||
DB6804FC2637CFEC00430867 /* AppSecret.swift */,
|
||||
DB6804D02637CE4700430867 /* UserDefaults.swift */,
|
||||
DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */,
|
||||
);
|
||||
path = AppShared;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2636,6 +2648,7 @@
|
|||
5B90C46D26259B2C0002E742 /* Setting.swift */,
|
||||
5B90C46C26259B2C0002E742 /* Subscription.swift */,
|
||||
5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */,
|
||||
DB73BF4027118B6D00781945 /* Instance.swift */,
|
||||
);
|
||||
path = Entity;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>44</integer>
|
||||
<integer>35</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>42</integer>
|
||||
<integer>37</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -37,7 +37,7 @@
|
|||
<key>Mastodon - ca.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>16</integer>
|
||||
<integer>18</integer>
|
||||
</dict>
|
||||
<key>Mastodon - de.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -67,7 +67,7 @@
|
|||
<key>Mastodon - jp.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>14</integer>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
<key>Mastodon - nl.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -87,7 +87,7 @@
|
|||
<key>Mastodon - zh_Hans.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>15</integer>
|
||||
<integer>16</integer>
|
||||
</dict>
|
||||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -97,7 +97,7 @@
|
|||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>41</integer>
|
||||
<integer>36</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>43</integer>
|
||||
<integer>38</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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..<numberOfSections ~= indexPath.section else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// let items = itemIdentifiers(inSection: sectionIdentifiers[indexPath.section])
|
||||
//
|
||||
// guard 0..<items.endIndex ~= indexPath.item else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// return items[indexPath.item]
|
||||
// }
|
||||
//}
|
|
@ -413,7 +413,7 @@ extension ComposeViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] attachmentServices in
|
||||
guard let self = self else { return }
|
||||
let isEnabled = attachmentServices.count < 4
|
||||
let isEnabled = attachmentServices.count < self.viewModel.maxMediaAttachments
|
||||
self.composeToolbarView.mediaBarButtonItem.isEnabled = isEnabled
|
||||
self.composeToolbarView.mediaButton.isEnabled = isEnabled
|
||||
self.resetImagePicker()
|
||||
|
@ -450,7 +450,7 @@ extension ComposeViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] characterCount in
|
||||
guard let self = self else { return }
|
||||
let count = ComposeViewModel.composeContentLimit - characterCount
|
||||
let count = self.viewModel.composeContentLimit - characterCount
|
||||
self.composeToolbarView.characterCountLabel.text = "\(count)"
|
||||
self.characterCountLabel.text = "\(count)"
|
||||
let font: UIFont
|
||||
|
@ -651,7 +651,7 @@ extension ComposeViewController {
|
|||
}
|
||||
|
||||
private func resetImagePicker() {
|
||||
let selectionLimit = max(1, 4 - viewModel.attachmentServices.value.count)
|
||||
let selectionLimit = max(1, viewModel.maxMediaAttachments - viewModel.attachmentServices.value.count)
|
||||
let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit)
|
||||
photoLibraryPicker = createImagePicker(configuration: configuration)
|
||||
}
|
||||
|
|
|
@ -48,15 +48,6 @@ extension ComposeViewModel {
|
|||
tableView.endUpdates()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// composeStatusPollTableViewCell.collectionViewHeightDidUpdate
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] _ in
|
||||
// guard let _ = self else { return }
|
||||
// tableView.beginUpdates()
|
||||
// tableView.endUpdates()
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
|
||||
attachmentServices
|
||||
.removeDuplicates()
|
||||
|
@ -100,7 +91,7 @@ extension ComposeViewModel {
|
|||
for attribute in pollOptionAttributes {
|
||||
items.append(.pollOption(attribute: attribute))
|
||||
}
|
||||
if pollOptionAttributes.count < 4 {
|
||||
if pollOptionAttributes.count < self.maxPollOptions {
|
||||
items.append(.pollOptionAppendEntry)
|
||||
}
|
||||
items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
|
||||
|
|
|
@ -15,7 +15,6 @@ import MastodonSDK
|
|||
|
||||
final class ComposeViewModel: NSObject {
|
||||
|
||||
static let composeContentLimit: Int = 500
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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<Mastodon.Response.Content<Mastodon.Entity.Instance>, 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<Mastodon.Entity.Instance> 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue