diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 9dc908c0..1d517412 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -189,6 +189,7 @@ + @@ -281,7 +282,7 @@ - + diff --git a/CoreDataStack/Entity/Setting.swift b/CoreDataStack/Entity/Setting.swift index 953f773f..88768eda 100644 --- a/CoreDataStack/Entity/Setting.swift +++ b/CoreDataStack/Entity/Setting.swift @@ -10,10 +10,12 @@ import Foundation public final class Setting: NSManagedObject { - @NSManaged public var appearanceRaw: String - @NSManaged public var preferredTrueBlackDarkMode: Bool @NSManaged public var domain: String @NSManaged public var userID: String + + @NSManaged public var appearanceRaw: String + @NSManaged public var preferredTrueBlackDarkMode: Bool + @NSManaged public var preferredStaticAvatar: Bool @NSManaged public private(set) var createdAt: Date @NSManaged public private(set) var updatedAt: Date @@ -54,6 +56,12 @@ extension Setting { self.preferredTrueBlackDarkMode = preferredTrueBlackDarkMode didUpdate(at: Date()) } + + public func update(preferredStaticAvatar: Bool) { + guard preferredStaticAvatar != self.preferredStaticAvatar else { return } + self.preferredStaticAvatar = preferredStaticAvatar + didUpdate(at: Date()) + } public func didUpdate(at networkDate: Date) { self.updatedAt = networkDate diff --git a/Localization/app.json b/Localization/app.json index 28119a48..cac760f4 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -497,6 +497,9 @@ "appearance_settings": { "dark_mode": { "title": "True black Dark Mode" + }, + "avatar_animation": { + "title": "Disable avatar animation" } }, "notifications": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d87a1b8a..13bc247f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -411,6 +411,7 @@ DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; }; DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; }; DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; }; + DBA088DF26958164003EB4B2 /* UserFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */; }; DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; }; DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; }; DBA1DB80268F84F80052DB59 /* NotificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA1DB7F268F84F80052DB59 /* NotificationType.swift */; }; @@ -1039,6 +1040,7 @@ DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = ""; }; + DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFetchedResultsController.swift; sourceTree = ""; }; DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = ""; }; DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; DBA1DB7F268F84F80052DB59 /* NotificationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationType.swift; sourceTree = ""; }; @@ -2607,6 +2609,7 @@ isa = PBXGroup; children = ( DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */, + DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */, DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */, ); path = FetchedResultsController; @@ -3256,6 +3259,7 @@ DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */, 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, + DBA088DF26958164003EB4B2 /* UserFetchedResultsController.swift in Sources */, DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */, 0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 0f3da3a1..ecde1bcc 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 21 + 20 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -37,7 +37,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 20 + 21 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift b/Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift new file mode 100644 index 00000000..f46ee978 --- /dev/null +++ b/Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift @@ -0,0 +1,87 @@ +// +// UserFetchedResultsController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-7. +// + +import os.log +import UIKit +import Combine +import CoreData +import CoreDataStack +import MastodonSDK + +final class UserFetchedResultsController: NSObject { + + var disposeBag = Set() + + let fetchedResultsController: NSFetchedResultsController + + // input + let domain = CurrentValueSubject(nil) + let userIDs = CurrentValueSubject<[Mastodon.Entity.Account.ID], Never>([]) + + // output + let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) + + init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) { + self.domain.value = domain ?? "" + self.fetchedResultsController = { + let fetchRequest = MastodonUser.sortedFetchRequest + fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: []) + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.fetchBatchSize = 20 + let controller = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: managedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + + return controller + }() + super.init() + + fetchedResultsController.delegate = self + + Publishers.CombineLatest( + self.domain.removeDuplicates().eraseToAnyPublisher(), + self.userIDs.removeDuplicates().eraseToAnyPublisher() + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] domain, ids in + guard let self = self else { return } + var predicates = [MastodonUser.predicate(domain: domain ?? "", ids: ids)] + if let additionalPredicate = additionalTweetPredicate { + predicates.append(additionalPredicate) + } + self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) + do { + try self.fetchedResultsController.performFetch() + } catch { + assertionFailure(error.localizedDescription) + } + } + .store(in: &disposeBag) + } + +} + +// MARK: - NSFetchedResultsControllerDelegate +extension UserFetchedResultsController: NSFetchedResultsControllerDelegate { + func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + let indexes = userIDs.value + let objects = fetchedResultsController.fetchedObjects ?? [] + + let items: [NSManagedObjectID] = objects + .compactMap { object in + indexes.firstIndex(of: object.id).map { index in (index, object) } + } + .sorted { $0.0 < $1.0 } + .map { $0.1.objectID } + self.objectIDs.value = items + } +} diff --git a/Mastodon/Diffiable/Item/SettingsItem.swift b/Mastodon/Diffiable/Item/SettingsItem.swift index f21dea31..aca02474 100644 --- a/Mastodon/Diffiable/Item/SettingsItem.swift +++ b/Mastodon/Diffiable/Item/SettingsItem.swift @@ -11,6 +11,7 @@ import CoreData enum SettingsItem: Hashable { case appearance(settingObjectID: NSManagedObjectID) case appearanceDarkMode(settingObjectID: NSManagedObjectID) + case appearanceDisableAvatarAnimation(settingObjectID: NSManagedObjectID) case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode) case boringZone(item: Link) case spicyZone(item: Link) diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift index 1e0fe7da..b9be9165 100644 --- a/Mastodon/Extension/CoreDataStack/MastodonUser.swift +++ b/Mastodon/Extension/CoreDataStack/MastodonUser.swift @@ -74,11 +74,12 @@ extension MastodonUser { } public func avatarImageURL() -> URL? { - return URL(string: avatar) + let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar + return URL(string: string) } public func avatarImageURLWithFallback(domain: String) -> URL { - return URL(string: avatar) ?? URL(string: "https://\(domain)/avatars/original/missing.png")! + return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! } } diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift index 8fd6bd67..8109371c 100644 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift +++ b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift @@ -5,6 +5,7 @@ // Created by xiaojian sun on 2021/4/2. // +import UIKit import MastodonSDK extension Mastodon.Entity.Account: Hashable { @@ -16,3 +17,14 @@ extension Mastodon.Entity.Account: Hashable { return lhs.id == rhs.id } } + +extension Mastodon.Entity.Account { + public func avatarImageURL() -> URL? { + let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar + return URL(string: string) + } + + public func avatarImageURLWithFallback(domain: String) -> URL { + return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! + } +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 28c732b2..5b03f6ed 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -934,6 +934,10 @@ internal enum L10n { internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title") } internal enum AppearanceSettings { + internal enum AvatarAnimation { + /// Disable avatar animation + internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.AvatarAnimation.Title") + } internal enum DarkMode { /// True black Dark Mode internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.DarkMode.Title") diff --git a/Mastodon/Preference/AppearancePreference.swift b/Mastodon/Preference/AppearancePreference.swift index 78cf3d33..1b4c4280 100644 --- a/Mastodon/Preference/AppearancePreference.swift +++ b/Mastodon/Preference/AppearancePreference.swift @@ -17,4 +17,12 @@ extension UserDefaults { set { self[#function] = newValue.rawValue } } + @objc dynamic var preferredStaticAvatar: Bool { + get { + register(defaults: [#function: false]) + return bool(forKey: #function) + } + set { self[#function] = newValue } + } + } diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index ce34268c..6965d20f 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -317,6 +317,7 @@ any server."; "Scene.Settings.Section.Appearance.Dark" = "Always Dark"; "Scene.Settings.Section.Appearance.Light" = "Always Light"; "Scene.Settings.Section.Appearance.Title" = "Appearance"; +"Scene.Settings.Section.AppearanceSettings.AvatarAnimation.Title" = "Disable avatar animation"; "Scene.Settings.Section.AppearanceSettings.DarkMode.Title" = "True black Dark Mode"; "Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy"; "Scene.Settings.Section.Boringzone.Terms" = "Terms of Service"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index ce34268c..6965d20f 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -317,6 +317,7 @@ any server."; "Scene.Settings.Section.Appearance.Dark" = "Always Dark"; "Scene.Settings.Section.Appearance.Light" = "Always Light"; "Scene.Settings.Section.Appearance.Title" = "Appearance"; +"Scene.Settings.Section.AppearanceSettings.AvatarAnimation.Title" = "Disable avatar animation"; "Scene.Settings.Section.AppearanceSettings.DarkMode.Title" = "True black Dark Mode"; "Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy"; "Scene.Settings.Section.Boringzone.Terms" = "Terms of Service"; diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 5c443e09..fd7a8c81 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -40,6 +40,8 @@ final class SearchViewModel: NSObject { var accountDiffableDataSource: UICollectionViewDiffableDataSource? var searchResultDiffableDataSource: UITableViewDiffableDataSource? + let statusFetchedResultsController: StatusFetchedResultsController + // bottom loader private(set) lazy var loadoldestStateMachine: GKStateMachine = { // exclude timeline middle fetcher state @@ -59,6 +61,11 @@ final class SearchViewModel: NSObject { init(context: AppContext, coordinator: SceneCoordinator) { self.coordinator = coordinator self.context = context + self.statusFetchedResultsController = StatusFetchedResultsController( + managedObjectContext: context.managedObjectContext, + domain: nil, + additionalTweetPredicate: nil + ) super.init() // bind active authentication @@ -70,6 +77,7 @@ final class SearchViewModel: NSObject { return } self.currentMastodonUser.value = activeMastodonAuthentication.user + self.statusFetchedResultsController.domain.value = activeMastodonAuthentication.domain } .store(in: &disposeBag) @@ -224,7 +232,7 @@ final class SearchViewModel: NSObject { } } .store(in: &disposeBag) - + searchResult .receive(on: DispatchQueue.main) .sink { [weak self] searchResult in @@ -343,7 +351,7 @@ final class SearchViewModel: NSObject { DispatchQueue.main.async { self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show) } - + case .hashtag(let tag): let (tagInCoreData, _) = APIService.CoreData.createOrMergeTag(into: self.context.managedObjectContext, entity: tag) if let searchHistories = searchHistories { diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift index 6759eecb..6a40f790 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -10,10 +10,13 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit +import FLAnimatedImage +import Nuke final class SearchingTableViewCell: UITableViewCell { + let _imageView: UIImageView = { - let imageView = UIImageView() + let imageView = FLAnimatedImageView() imageView.tintColor = Asset.Colors.Label.primary.color imageView.layer.cornerRadius = 4 imageView.clipsToBounds = true @@ -37,8 +40,7 @@ final class SearchingTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - _imageView.af.cancelImageRequest() - _imageView.image = nil + Nuke.cancelRequest(for: _imageView) } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -90,22 +92,28 @@ extension SearchingTableViewCell { } func config(with account: Mastodon.Entity.Account) { - _imageView.af.setImage( - withURL: URL(string: account.avatar)!, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) + Nuke.loadImage( + with: account.avatarImageURL(), + options: ImageLoadingOptions( + placeholder: UIImage.placeholder(color: .systemFill), + transition: .fadeIn(duration: 0.2) + ), + into: _imageView ) _titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName _subTitleLabel.text = account.acct } func config(with account: MastodonUser) { - _imageView.af.setImage( - withURL: URL(string: account.avatar)!, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) + Nuke.loadImage( + with: account.avatarImageURL(), + options: ImageLoadingOptions( + placeholder: UIImage.placeholder(color: .systemFill), + transition: .fadeIn(duration: 0.2) + ), + into: _imageView ) - _titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName + _titleLabel.text = account.displayNameWithFallback _subTitleLabel.text = account.acct } @@ -122,7 +130,7 @@ extension SearchingTableViewCell { let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) _subTitleLabel.text = string } - + func config(with tag: Tag) { let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate) _imageView.image = image diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 89514a06..8915a44d 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -356,7 +356,7 @@ extension SettingsViewController: UITableViewDelegate { case .appearance: // do nothing break - case .appearanceDarkMode: + case .appearanceDarkMode, .appearanceDisableAvatarAnimation: // do nothing break case .notification: @@ -443,10 +443,12 @@ extension SettingsViewController: SettingsToggleCellDelegate { func settingsToggleCell(_ cell: SettingsToggleTableViewCell, switchValueDidChange switch: UISwitch) { guard let dataSource = viewModel.dataSource else { return } guard let indexPath = tableView.indexPath(for: cell) else { return } + + let isOn = `switch`.isOn let item = dataSource.itemIdentifier(for: indexPath) + switch item { case .appearanceDarkMode(let settingObjectID): - let isOn = `switch`.isOn let managedObjectContext = context.backgroundManagedObjectContext managedObjectContext.performChanges { let setting = managedObjectContext.object(with: settingObjectID) as! Setting @@ -462,8 +464,23 @@ extension SettingsViewController: SettingsToggleCellDelegate { } } .store(in: &disposeBag) + case .appearanceDisableAvatarAnimation(let settingObjectID): + let managedObjectContext = context.backgroundManagedObjectContext + managedObjectContext.performChanges { + let setting = managedObjectContext.object(with: settingObjectID) as! Setting + setting.update(preferredStaticAvatar: isOn) + } + .sink { result in + switch result { + case .success: + UserDefaults.shared.preferredStaticAvatar = isOn + case .failure(let error): + assertionFailure(error.localizedDescription) + break + } + } + .store(in: &disposeBag) case .notification(let settingObjectID, let switchMode): - let isOn = `switch`.isOn let managedObjectContext = context.backgroundManagedObjectContext managedObjectContext.performChanges { let setting = managedObjectContext.object(with: settingObjectID) as! Setting diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift index f212a591..142de7dc 100644 --- a/Mastodon/Scene/Settings/SettingsViewModel.swift +++ b/Mastodon/Scene/Settings/SettingsViewModel.swift @@ -96,7 +96,10 @@ extension SettingsViewModel { snapshot.appendSections([.appearance]) snapshot.appendItems(appearanceItems, toSection: .appearance) - let appearanceSettingItems = [SettingsItem.appearanceDarkMode(settingObjectID: setting.objectID)] + let appearanceSettingItems = [ + SettingsItem.appearanceDarkMode(settingObjectID: setting.objectID), + SettingsItem.appearanceDisableAvatarAnimation(settingObjectID: setting.objectID) + ] snapshot.appendSections([.appearanceSettings]) snapshot.appendItems(appearanceSettingItems, toSection: .appearanceSettings) @@ -146,7 +149,6 @@ extension SettingsViewModel { weak settingsToggleCellDelegate ] tableView, indexPath, item -> UITableViewCell? in guard let self = self else { return nil } - switch item { case .appearance(let objectID): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell @@ -167,13 +169,14 @@ extension SettingsViewModel { } cell.delegate = settingsAppearanceTableViewCellDelegate return cell - case .appearanceDarkMode(let objectID): + case .appearanceDarkMode(let objectID), + .appearanceDisableAvatarAnimation(let objectID): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell cell.delegate = settingsToggleCellDelegate - cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.DarkMode.title self.context.managedObjectContext.performAndWait { let setting = self.context.managedObjectContext.object(with: objectID) as! Setting - cell.switchButton.isOn = setting.preferredTrueBlackDarkMode + SettingsViewModel.configureSettingToggle(cell: cell, item: item, setting: setting) + ManagedObjectObserver.observe(object: setting) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { _ in @@ -182,7 +185,7 @@ extension SettingsViewModel { guard let cell = cell else { return } guard case .update(let object) = change.changeType, let setting = object as? Setting else { return } - cell.switchButton.isOn = setting.preferredTrueBlackDarkMode + SettingsViewModel.configureSettingToggle(cell: cell, item: item, setting: setting) }) .store(in: &cell.disposeBag) } @@ -220,6 +223,23 @@ extension SettingsViewModel { } extension SettingsViewModel { + + static func configureSettingToggle( + cell: SettingsToggleTableViewCell, + item: SettingsItem, + setting: Setting + ) { + switch item { + case .appearanceDarkMode: + cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.DarkMode.title + cell.switchButton.isOn = setting.preferredTrueBlackDarkMode + case .appearanceDisableAvatarAnimation: + cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.AvatarAnimation.title + cell.switchButton.isOn = setting.preferredStaticAvatar + default: + assertionFailure() + } + } static func configureSettingToggle( cell: SettingsToggleTableViewCell, diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index d813c221..a37b431f 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -95,10 +95,8 @@ final class StatusView: UIView { view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile return view }() - let avatarImageView: UIImageView = { + let avatarImageView: FLAnimatedImageView = { let imageView = FLAnimatedImageView() -// imageView.layer.shouldRasterize = true -// imageView.layer.rasterizationScale = UIScreen.main.scale return imageView }() let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton() @@ -480,6 +478,9 @@ extension StatusView { avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside) revealContentWarningButton.addTarget(self, action: #selector(StatusView.revealContentWarningButtonDidPressed(_:)), for: .touchUpInside) pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside) + + + } } diff --git a/Mastodon/Service/AuthenticationService.swift b/Mastodon/Service/AuthenticationService.swift index a0bbca57..4a460b1d 100644 --- a/Mastodon/Service/AuthenticationService.swift +++ b/Mastodon/Service/AuthenticationService.swift @@ -81,6 +81,26 @@ final class AuthenticationService: NSObject { .assign(to: \.value, on: activeMastodonAuthenticationBox) .store(in: &disposeBag) + activeMastodonAuthenticationBox + .receive(on: RunLoop.main) + .sink { [weak self] authenticationBox in + guard let _ = self else { return } + guard let authenticationBox = authenticationBox else { return } + let request = Setting.sortedFetchRequest + request.predicate = Setting.predicate(domain: authenticationBox.domain, userID: authenticationBox.userID) + guard let setting = managedObjectContext.safeFetch(request).first else { return } + + let themeName: ThemeName = setting.preferredTrueBlackDarkMode ? .system : .mastodon + if UserDefaults.shared.currentThemeNameRawValue != themeName.rawValue { + ThemeService.shared.set(themeName: themeName) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update theme style", ((#file as NSString).lastPathComponent), #line, #function) + } + if UserDefaults.shared.preferredStaticAvatar != setting.preferredStaticAvatar { + UserDefaults.shared.preferredStaticAvatar = setting.preferredStaticAvatar + } + } + .store(in: &disposeBag) + do { try mastodonAuthenticationFetchedResultsController.performFetch() mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? []