feat: dynamic set compose post character limit. resolve #222

This commit is contained in:
CMK 2021-10-09 19:01:08 +08:00
parent 7113fc037c
commit 1eb9812588
16 changed files with 465 additions and 88 deletions

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 */,

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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]
// }
//}

View File

@ -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)
}

View File

@ -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))

View File

@ -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 }

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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"
}
}
}