diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
index 9dc908c0f..1d517412a 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 953f773fe..88768eda4 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 28119a48c..5c8dd5cc0 100644
--- a/Localization/app.json
+++ b/Localization/app.json
@@ -200,7 +200,7 @@
"slogan": "Social networking\nback in your hands."
},
"server_picker": {
- "title": "Pick a Server,\nany server.",
+ "title": "Pick a server,\nany server.",
"button": {
"category": {
"all": "All",
@@ -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 d87a1b8ae..73ab14602 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 */,
@@ -4829,7 +4833,7 @@
repositoryURL = "https://github.com/TwidereProject/MetaTextView.git";
requirement = {
kind = exactVersion;
- version = 1.2.5;
+ version = 1.3.0;
};
};
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index 0f3da3a13..ecde1bcc5 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.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
index e143e8a71..1f3cc3145 100644
--- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -114,8 +114,8 @@
"repositoryURL": "https://github.com/TwidereProject/MetaTextView.git",
"state": {
"branch": null,
- "revision": "9ba4027ed0a88185ce95bb1773620c2ceaa9f3bb",
- "version": "1.2.5"
+ "revision": "e2049e14ef411c6810d53c1baf553b5161c6678f",
+ "version": "1.3.0"
}
},
{
diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift
index 310a819b5..b918e1812 100644
--- a/Mastodon/Coordinator/SceneCoordinator.swift
+++ b/Mastodon/Coordinator/SceneCoordinator.swift
@@ -141,7 +141,11 @@ extension SceneCoordinator {
if let navigationControllerVisibleViewController = presentingViewController.navigationController?.visibleViewController {
switch viewController {
case is ProfileViewController:
- let barButtonItem = UIBarButtonItem(title: navigationControllerVisibleViewController.navigationItem.title, style: .plain, target: nil, action: nil)
+ let title: String = {
+ let title = navigationControllerVisibleViewController.navigationItem.title ?? ""
+ return title.count > 10 ? "" : title
+ }()
+ let barButtonItem = UIBarButtonItem(title: title, style: .plain, target: nil, action: nil)
barButtonItem.tintColor = .white
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = barButtonItem
default:
diff --git a/Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift b/Mastodon/Diffiable/FetchedResultsController/UserFetchedResultsController.swift
new file mode 100644
index 000000000..f46ee978d
--- /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 f21dea31c..aca02474e 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/Diffiable/Section/PollSection.swift b/Mastodon/Diffiable/Section/PollSection.swift
index 872cc7aea..79ecd415d 100644
--- a/Mastodon/Diffiable/Section/PollSection.swift
+++ b/Mastodon/Diffiable/Section/PollSection.swift
@@ -68,7 +68,7 @@ extension PollSection {
cell.pollOptionView.checkmarkImageView.isHidden = true
case .off:
ThemeService.shared.currentTheme
- .receive(on: RunLoop.main)
+ .receive(on: DispatchQueue.main)
.sink { [weak cell] theme in
guard let cell = cell else { return }
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = theme.tertiarySystemBackgroundColor
@@ -80,7 +80,7 @@ extension PollSection {
cell.pollOptionView.checkmarkImageView.isHidden = true
case .on:
ThemeService.shared.currentTheme
- .receive(on: RunLoop.main)
+ .receive(on: DispatchQueue.main)
.sink { [weak cell] theme in
guard let cell = cell else { return }
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = theme.tertiarySystemBackgroundColor
diff --git a/Mastodon/Diffiable/Section/ProfileFieldSection.swift b/Mastodon/Diffiable/Section/ProfileFieldSection.swift
index 82f88f657..1c8546eb3 100644
--- a/Mastodon/Diffiable/Section/ProfileFieldSection.swift
+++ b/Mastodon/Diffiable/Section/ProfileFieldSection.swift
@@ -39,7 +39,7 @@ extension ProfileFieldSection {
.sink { [weak cell] name, emojiDict in
guard let cell = cell else { return }
cell.fieldView.titleActiveLabel.configure(field: name, emojiDict: emojiDict)
- cell.fieldView.titleTextField.text = name
+ // only bind label. The text field should only set once
}
.store(in: &cell.disposeBag)
@@ -55,7 +55,7 @@ extension ProfileFieldSection {
.sink { [weak cell] value, emojiDict in
guard let cell = cell else { return }
cell.fieldView.valueActiveLabel.configure(field: value, emojiDict: emojiDict)
- cell.fieldView.valueTextField.text = value
+ // only bind label. The text field should only set once
}
.store(in: &cell.disposeBag)
diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift
index d3c5ba809..7b613dfd3 100644
--- a/Mastodon/Diffiable/Section/StatusSection.swift
+++ b/Mastodon/Diffiable/Section/StatusSection.swift
@@ -11,7 +11,7 @@ import CoreDataStack
import os.log
import UIKit
import AVKit
-import Nuke
+import AlamofireImage
import MastodonMeta
// import LinkPresentation
@@ -137,6 +137,9 @@ extension StatusSection {
switch item {
case .root:
+ // allow select content
+ cell.statusView.contentMetaText.textView.isSelectable = true
+ // configure thread meta
StatusSection.configureThreadMeta(cell: cell, status: status)
ManagedObjectObserver.observe(object: status.reblog ?? status)
.receive(on: RunLoop.main)
@@ -519,13 +522,7 @@ extension StatusSection {
let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Common.Controls.Status.userRepliedTo(name)
}()
- MastodonStatusContent.parseResult(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
- .receive(on: DispatchQueue.main)
- .sink { [weak cell] parseResult in
- guard let cell = cell else { return }
- cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
- }
- .store(in: &cell.disposeBag)
+ cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
cell.statusView.headerInfoLabel.accessibilityLabel = headerText
cell.statusView.headerInfoLabel.isAccessibilityElement = status.replyTo != nil
} else {
@@ -541,13 +538,7 @@ extension StatusSection {
// name
let author = (status.reblog ?? status).author
let nameContent = author.displayNameWithFallback
- MastodonStatusContent.parseResult(content: nameContent, emojiDict: author.emojiDict)
- .receive(on: DispatchQueue.main)
- .sink { [weak cell] parseResult in
- guard let cell = cell else { return }
- cell.statusView.nameLabel.configure(contentParseResult: parseResult)
- }
- .store(in: &cell.disposeBag)
+ cell.statusView.nameLabel.configure(content: nameContent, emojiDict: author.emojiDict)
cell.statusView.nameLabel.accessibilityLabel = nameContent
// username
cell.statusView.usernameLabel.text = "@" + author.acct
@@ -648,48 +639,32 @@ extension StatusSection {
}
.store(in: &cell.disposeBag)
- let isSingleMosaicLayout = mosaics.count == 1
-
// set image
- let imageSize = CGSize(
- width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale,
- height: mosaic.imageViewSize.height * imageView.traitCollection.displayScale
- )
- let url: URL? = {
+ let url: URL = {
if UIDevice.current.userInterfaceIdiom == .phone {
return meta.previewURL ?? meta.url
}
return meta.url
}()
- let request = ImageRequest(
- url: url,
- processors: [
- ImageProcessors.Resize(
- size: imageSize,
- unit: .pixels,
- contentMode: isSingleMosaicLayout ? .aspectFill : .aspectFit,
- crop: isSingleMosaicLayout
- )
- ]
- )
- let options = ImageLoadingOptions(
- transition: .fadeIn(duration: 0.2)
- )
- Nuke.loadImage(
- with: request,
- options: options,
- into: imageView
- ) { result in
- switch result {
- case .failure:
- break
+ // let imageSize = CGSize(
+ // width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale,
+ // height: mosaic.imageViewSize.height * imageView.traitCollection.displayScale
+ // )
+ // let imageFilter = AspectScaledToFillSizeFilter(size: imageSize)
+
+ imageView.af.setImage(
+ withURL: url,
+ placeholderImage: UIImage.placeholder(color: .systemFill),
+ imageTransition: .crossDissolve(0.2)
+ ) { response in
+ switch response.result {
case .success:
statusItemAttribute.isImageLoaded.value = true
+ case .failure:
+ break
}
- }?
- .store(in: &cell.statusView.statusMosaicImageViewContainer.imageTasks)
-
+ }
imageView.accessibilityLabel = meta.altText
diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift
index 1e0fe7dad..b9be9165b 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 8fd6bd67a..8109371c4 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 28c732b28..ab2b75f40 100644
--- a/Mastodon/Generated/Strings.swift
+++ b/Mastodon/Generated/Strings.swift
@@ -830,7 +830,7 @@ internal enum L10n {
}
}
internal enum ServerPicker {
- /// Pick a Server,\nany server.
+ /// Pick a server,\nany server.
internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title")
internal enum Button {
/// See Less
@@ -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 78cf3d332..1b4c42807 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/Protocol/UserProvider/UserProviderFacade.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift
index 79185338a..c9546696f 100644
--- a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift
+++ b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift
@@ -210,6 +210,40 @@ extension UserProviderFacade {
) -> UIMenu {
var children: [UIMenuElement] = []
let name = mastodonUser.displayNameWithFallback
+
+ if let shareUser = shareUser {
+ let shareAction = UIAction(title: L10n.Common.Controls.Actions.shareUser(name), image: UIImage(systemName: "square.and.arrow.up"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in
+ guard let provider = provider else { return }
+ let activityViewController = createActivityViewControllerForMastodonUser(mastodonUser: shareUser, dependency: provider)
+ provider.coordinator.present(
+ scene: .activityViewController(
+ activityViewController: activityViewController,
+ sourceView: sourceView,
+ barButtonItem: barButtonItem
+ ),
+ from: provider,
+ transition: .activityViewControllerPresent(animated: true, completion: nil)
+ )
+ }
+ children.append(shareAction)
+ }
+
+ if let shareStatus = shareStatus {
+ let shareAction = UIAction(title: L10n.Common.Controls.Actions.sharePost, image: UIImage(systemName: "square.and.arrow.up"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in
+ guard let provider = provider else { return }
+ let activityViewController = createActivityViewControllerForMastodonUser(status: shareStatus, dependency: provider)
+ provider.coordinator.present(
+ scene: .activityViewController(
+ activityViewController: activityViewController,
+ sourceView: sourceView,
+ barButtonItem: barButtonItem
+ ),
+ from: provider,
+ transition: .activityViewControllerPresent(animated: true, completion: nil)
+ )
+ }
+ children.append(shareAction)
+ }
if !isMyself {
// mute
@@ -316,40 +350,6 @@ extension UserProviderFacade {
}
}
- if let shareUser = shareUser {
- let shareAction = UIAction(title: L10n.Common.Controls.Actions.shareUser(name), image: UIImage(systemName: "square.and.arrow.up"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in
- guard let provider = provider else { return }
- let activityViewController = createActivityViewControllerForMastodonUser(mastodonUser: shareUser, dependency: provider)
- provider.coordinator.present(
- scene: .activityViewController(
- activityViewController: activityViewController,
- sourceView: sourceView,
- barButtonItem: barButtonItem
- ),
- from: provider,
- transition: .activityViewControllerPresent(animated: true, completion: nil)
- )
- }
- children.append(shareAction)
- }
-
- if let shareStatus = shareStatus {
- let shareAction = UIAction(title: L10n.Common.Controls.Actions.sharePost, image: UIImage(systemName: "square.and.arrow.up"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in
- guard let provider = provider else { return }
- let activityViewController = createActivityViewControllerForMastodonUser(status: shareStatus, dependency: provider)
- provider.coordinator.present(
- scene: .activityViewController(
- activityViewController: activityViewController,
- sourceView: sourceView,
- barButtonItem: barButtonItem
- ),
- from: provider,
- transition: .activityViewControllerPresent(animated: true, completion: nil)
- )
- }
- children.append(shareAction)
- }
-
if let status = shareStatus, isMyself {
let deleteAction = UIAction(title: L10n.Common.Controls.Actions.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) {
[weak provider] _ in
diff --git a/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/Background/content.warning.overlay.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/Background/content.warning.overlay.background.colorset/Contents.json
index 485b4813a..54427c610 100644
--- a/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/Background/content.warning.overlay.background.colorset/Contents.json
+++ b/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/Background/content.warning.overlay.background.colorset/Contents.json
@@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
- "blue" : "0.263",
- "green" : "0.208",
- "red" : "0.192"
+ "blue" : "0x6E",
+ "green" : "0x57",
+ "red" : "0x4F"
}
},
"idiom" : "universal"
diff --git a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/content.warning.overlay.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/content.warning.overlay.background.colorset/Contents.json
index 485b4813a..d211d7df9 100644
--- a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/content.warning.overlay.background.colorset/Contents.json
+++ b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/content.warning.overlay.background.colorset/Contents.json
@@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
- "blue" : "0.263",
- "green" : "0.208",
- "red" : "0.192"
+ "blue" : "60",
+ "green" : "58",
+ "red" : "58"
}
},
"idiom" : "universal"
diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings
index ce34268c7..f6211e838 100644
--- a/Mastodon/Resources/ar.lproj/Localizable.strings
+++ b/Mastodon/Resources/ar.lproj/Localizable.strings
@@ -303,7 +303,7 @@ tap the link to confirm your account.";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
"Scene.ServerPicker.Label.Users" = "USERS";
-"Scene.ServerPicker.Title" = "Pick a Server,
+"Scene.ServerPicker.Title" = "Pick a server,
any server.";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
@@ -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 ce34268c7..f6211e838 100644
--- a/Mastodon/Resources/en.lproj/Localizable.strings
+++ b/Mastodon/Resources/en.lproj/Localizable.strings
@@ -303,7 +303,7 @@ tap the link to confirm your account.";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
"Scene.ServerPicker.Label.Users" = "USERS";
-"Scene.ServerPicker.Title" = "Pick a Server,
+"Scene.ServerPicker.Title" = "Pick a server,
any server.";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
@@ -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/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
index fcd35cec1..728c92c21 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
@@ -205,10 +205,6 @@ extension HomeTimelineViewController {
// needs trigger manually after onboarding dismiss
setNeedsStatusBarAppearanceUpdate()
-
- if (viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty {
- viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self)
- }
}
override func viewDidAppear(_ animated: Bool) {
@@ -216,11 +212,10 @@ extension HomeTimelineViewController {
viewModel.viewDidAppear.send()
- DispatchQueue.main.async { [weak self] in
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
guard let self = self else { return }
- if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 {
- self.viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self)
- }
+ // always try to refresh timeline after appear
+ self.viewModel.homeTimelineNeedRefresh.send()
}
}
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift
index d22c52be3..9d8f6f709 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift
@@ -135,8 +135,10 @@ final class HomeTimelineViewModel: NSObject {
self?.loadLatestStateMachine.enter(LoadLatestState.Loading.self)
}
.store(in: &disposeBag)
-
+
+ // refresh after publish post
homeTimelineNavigationBarTitleViewModel.isPublished
+ .delay(for: 2, scheduler: DispatchQueue.main)
.sink { [weak self] isPublished in
guard let self = self else { return }
self.homeTimelineNeedRefresh.send()
diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
index 24cfe90a3..618faf1c2 100644
--- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
+++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
@@ -209,7 +209,12 @@ extension ProfileHeaderViewController {
.sink { [weak self] isEditing, note, editingNote in
guard let self = self else { return }
self.profileHeaderView.bioActiveLabel.configure(note: note ?? "", emojiDict: [:]) // FIXME: custom emoji
- self.profileHeaderView.bioTextEditorView.text = editingNote ?? ""
+
+ // prevent duplicate set
+ let editingNote = editingNote ?? ""
+ if self.profileHeaderView.bioTextEditorView.text != editingNote {
+ self.profileHeaderView.bioTextEditorView.text = editingNote
+ }
}
.store(in: &disposeBag)
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift
index e008e1bce..956b8d6d9 100644
--- a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift
+++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift
@@ -37,6 +37,7 @@ final class ProfileFieldView: UIView {
let valueActiveLabel: ActiveLabel = {
let label = ActiveLabel(style: .profileFieldValue)
label.configure(content: "value", emojiDict: [:])
+ label.textAlignment = .right
return label
}()
diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift
index fc29d308f..1ac0c4a4e 100644
--- a/Mastodon/Scene/Profile/ProfileViewController.swift
+++ b/Mastodon/Scene/Profile/ProfileViewController.swift
@@ -152,7 +152,11 @@ extension ProfileViewController {
.store(in: &disposeBag)
let barAppearance = UINavigationBarAppearance()
- barAppearance.configureWithTransparentBackground()
+ if isModal {
+ barAppearance.configureWithDefaultBackground()
+ } else {
+ barAppearance.configureWithTransparentBackground()
+ }
navigationItem.standardAppearance = barAppearance
navigationItem.compactAppearance = barAppearance
navigationItem.scrollEdgeAppearance = barAppearance
@@ -228,10 +232,10 @@ extension ProfileViewController {
overlayScrollView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(ProfileViewController.refreshControlValueChanged(_:)), for: .valueChanged)
- let postsUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter())
+ let postsUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: true))
bind(userTimelineViewModel: postsUserTimelineViewModel)
- let repliesUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: true))
+ let repliesUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: false))
bind(userTimelineViewModel: repliesUserTimelineViewModel)
let mediaUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(onlyMedia: true))
diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift
index 5c443e09a..fd7a8c813 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 6759eecba..6a40f7902 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 89514a064..6a08bd45d 100644
--- a/Mastodon/Scene/Settings/SettingsViewController.swift
+++ b/Mastodon/Scene/Settings/SettingsViewController.swift
@@ -236,6 +236,8 @@ class SettingsViewController: UIViewController, NeedsDependency {
return theme.secondarySystemBackgroundColor
}
})
+
+ tableView.separatorColor = theme.separator
}
private func setupNavigation() {
@@ -356,7 +358,7 @@ extension SettingsViewController: UITableViewDelegate {
case .appearance:
// do nothing
break
- case .appearanceDarkMode:
+ case .appearanceDarkMode, .appearanceDisableAvatarAnimation:
// do nothing
break
case .notification:
@@ -443,10 +445,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 +466,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 f212a591f..142de7dcf 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/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift b/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift
index 28b13e2fb..fb2d282af 100644
--- a/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift
+++ b/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift
@@ -8,8 +8,6 @@
import func AVFoundation.AVMakeRect
import UIKit
import Combine
-import Nuke
-import FLAnimatedImage
final class ContextMenuImagePreviewViewController: UIViewController {
@@ -17,19 +15,13 @@ final class ContextMenuImagePreviewViewController: UIViewController {
var viewModel: ContextMenuImagePreviewViewModel!
- var imageTask: ImageTask?
let imageView: UIImageView = {
- let imageView = FLAnimatedImageView()
+ let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
return imageView
}()
- deinit {
- imageTask?.cancel()
- imageTask = nil
- }
-
}
extension ContextMenuImagePreviewViewController {
@@ -55,13 +47,12 @@ extension ContextMenuImagePreviewViewController {
.sink { [weak self] url in
guard let self = self else { return }
guard let url = url else { return }
- self.imageTask = Nuke.loadImage(
- with: url,
- options: ImageLoadingOptions(
- placeholder: self.viewModel.thumbnail,
- transition: .fadeIn(duration: 0.2)
- ),
- into: self.imageView
+ self.imageView.af.setImage(
+ withURL: url,
+ placeholderImage: self.viewModel.thumbnail,
+ imageTransition: .crossDissolve(0.2),
+ runImageTransitionIfCached: true,
+ completion: nil
)
}
.store(in: &disposeBag)
diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift
index d60d40fe7..8336e852e 100644
--- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift
+++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift
@@ -8,7 +8,6 @@
import os.log
import func AVFoundation.AVMakeRect
import UIKit
-import Nuke
protocol MosaicImageViewContainerPresentable: AnyObject {
var mosaicImageViewContainer: MosaicImageViewContainer { get }
@@ -24,8 +23,6 @@ final class MosaicImageViewContainer: UIView {
weak var delegate: MosaicImageViewContainerDelegate?
- var imageTasks = Set()
-
let container = UIStackView()
private(set) lazy var imageViews: [UIImageView] = {
(0..<4).map { _ -> UIImageView in
@@ -94,11 +91,17 @@ extension MosaicImageViewContainer {
}
extension MosaicImageViewContainer {
+
+ func resetImageTask() {
+ imageViews.forEach { imageView in
+ imageView.af.cancelImageRequest()
+ imageView.image = nil
+ }
+ }
func reset() {
- imageTasks.forEach { $0?.cancel() }
- imageTasks.removeAll()
-
+ resetImageTask()
+
container.arrangedSubviews.forEach { subview in
container.removeArrangedSubview(subview)
subview.removeFromSuperview()
diff --git a/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift
index 931932060..d5a457a26 100644
--- a/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift
+++ b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift
@@ -17,6 +17,7 @@ protocol ContentWarningOverlayViewDelegate: AnyObject {
class ContentWarningOverlayView: UIView {
var disposeBag = Set()
+ private var _disposeBag = Set()
static let cornerRadius: CGFloat = 4
static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
@@ -36,7 +37,6 @@ class ContentWarningOverlayView: UIView {
// for status style overlay
let contentOverlayView: UIView = {
let view = UIView()
- view.backgroundColor = ThemeService.shared.currentTheme.value.contentWarningOverlayBackgroundColor
view.applyCornerRadius(radius: ContentWarningOverlayView.cornerRadius)
return view
}()
@@ -156,6 +156,18 @@ extension ContentWarningOverlayView {
addGestureRecognizer(tapGestureRecognizer)
configure(style: .media)
+ setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
+ ThemeService.shared.currentTheme
+ .receive(on: RunLoop.main)
+ .sink { [weak self] theme in
+ guard let self = self else { return }
+ self.setupBackgroundColor(theme: theme)
+ }
+ .store(in: &_disposeBag)
+ }
+
+ private func setupBackgroundColor(theme: Theme) {
+ contentOverlayView.backgroundColor = theme.contentWarningOverlayBackgroundColor
}
}
diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift
index d813c2219..d62fba0df 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()
@@ -222,6 +220,7 @@ final class StatusView: UIView {
metaText.textView.textContainer.lineFragmentPadding = 0
metaText.textView.textContainerInset = .zero
metaText.textView.layer.masksToBounds = false
+ metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment
let paragraphStyle: NSMutableParagraphStyle = {
let style = NSMutableParagraphStyle()
@@ -480,6 +479,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)
+
+
+
}
}
@@ -582,6 +584,17 @@ extension StatusView: MetaTextViewDelegate {
// MARK: - UITextViewDelegate
extension StatusView: UITextViewDelegate {
+
+ func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
+ switch textView {
+ case contentMetaText.textView:
+ return false
+ default:
+ assertionFailure()
+ return true
+ }
+ }
+
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
switch textView {
case contentMetaText.textView:
diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift
index 27461cfc6..0b3d3fddb 100644
--- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift
+++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift
@@ -74,7 +74,7 @@ final class StatusTableViewCell: UITableViewCell, StatusCell {
override func prepareForReuse() {
super.prepareForReuse()
selectionStyle = .default
- statusView.statusMosaicImageViewContainer.reset()
+ statusView.statusMosaicImageViewContainer.resetImageTask()
statusView.contentMetaText.textView.isSelectable = false
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = true
diff --git a/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift
index 9461bb282..ee9ac3438 100644
--- a/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift
+++ b/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift
@@ -18,11 +18,11 @@ final class ThreadReplyLoaderTableViewCell: UITableViewCell {
static let cellHeight: CGFloat = 44
weak var delegate: ThreadReplyLoaderTableViewCellDelegate?
+ var _disposeBag = Set()
let loadMoreButton: UIButton = {
let button = HighlightDimmableButton()
button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont
- button.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
button.setTitle(L10n.Common.Controls.Timeline.Loader.showMoreReplies, for: .normal)
return button
@@ -83,6 +83,15 @@ extension ThreadReplyLoaderTableViewCell {
resetSeparatorLineLayout()
loadMoreButton.addTarget(self, action: #selector(ThreadReplyLoaderTableViewCell.loadMoreButtonDidPressed(_:)), for: .touchUpInside)
+
+ setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
+ ThemeService.shared.currentTheme
+ .receive(on: RunLoop.main)
+ .sink { [weak self] theme in
+ guard let self = self else { return }
+ self.setupBackgroundColor(theme: theme)
+ }
+ .store(in: &_disposeBag)
}
private func resetSeparatorLineLayout() {
@@ -113,6 +122,10 @@ extension ThreadReplyLoaderTableViewCell {
}
}
}
+
+ private func setupBackgroundColor(theme: Theme) {
+ loadMoreButton.backgroundColor = theme.secondarySystemGroupedBackgroundColor
+ }
}
diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift
index 7a508fc75..e876041ca 100644
--- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift
+++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift
@@ -49,18 +49,26 @@ final class SuggestionAccountViewModel: NSObject {
super.init()
- Publishers.CombineLatest(self.accounts,self.selectedAccounts)
- .sink { [weak self] accounts,selectedAccounts in
- self?.applyTableViewDataSource(accounts: accounts)
- self?.applySelectedCollectionViewDataSource(accounts: selectedAccounts)
- }
- .store(in: &disposeBag)
+ Publishers.CombineLatest(
+ self.accounts,
+ self.selectedAccounts
+ )
+ .receive(on: RunLoop.main)
+ .sink { [weak self] accounts,selectedAccounts in
+ self?.applyTableViewDataSource(accounts: accounts)
+ self?.applySelectedCollectionViewDataSource(accounts: selectedAccounts)
+ }
+ .store(in: &disposeBag)
- Publishers.CombineLatest(self.selectedAccounts,self.headerPlaceholderCount)
- .sink { [weak self] selectedAccount,count in
- self?.applySelectedCollectionViewDataSource(accounts: selectedAccount)
- }
- .store(in: &disposeBag)
+ Publishers.CombineLatest(
+ self.selectedAccounts,
+ self.headerPlaceholderCount
+ )
+ .receive(on: RunLoop.main)
+ .sink { [weak self] selectedAccount,count in
+ self?.applySelectedCollectionViewDataSource(accounts: selectedAccount)
+ }
+ .store(in: &disposeBag)
viewWillAppear
.sink { [weak self] _ in
@@ -133,6 +141,7 @@ final class SuggestionAccountViewModel: NSObject {
}
func applyTableViewDataSource(accounts: [NSManagedObjectID]) {
+ assert(Thread.isMainThread)
guard let dataSource = diffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.main])
@@ -141,6 +150,7 @@ final class SuggestionAccountViewModel: NSObject {
}
func applySelectedCollectionViewDataSource(accounts: [NSManagedObjectID]) {
+ assert(Thread.isMainThread)
guard let count = headerPlaceholderCount.value else { return }
guard let dataSource = collectionDiffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot()
diff --git a/Mastodon/Service/APIService/APIService.swift b/Mastodon/Service/APIService/APIService.swift
index b38e2e059..1316f2cf0 100644
--- a/Mastodon/Service/APIService/APIService.swift
+++ b/Mastodon/Service/APIService/APIService.swift
@@ -13,6 +13,7 @@ import CoreDataStack
import MastodonSDK
import AlamofireImage
import AlamofireNetworkActivityIndicator
+import Nuke
final class APIService {
@@ -34,6 +35,10 @@ final class APIService {
// setup cache. 10MB RAM + 50MB Disk
URLCache.shared = URLCache(memoryCapacity: 10 * 1024 * 1024, diskCapacity: 50 * 1024 * 1024, diskPath: nil)
+
+ // setup Nuke cache
+ // using LRU disk cache
+ ImagePipeline.shared = ImagePipeline(configuration: .withDataCache)
// enable network activity manager for AlamofireImage
NetworkActivityIndicatorManager.shared.isEnabled = true
diff --git a/Mastodon/Service/AuthenticationService.swift b/Mastodon/Service/AuthenticationService.swift
index a0bbca57a..4a460b1d0 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 ?? []
diff --git a/Mastodon/Service/PhotoLibraryService.swift b/Mastodon/Service/PhotoLibraryService.swift
index 45b47a836..53e7529c0 100644
--- a/Mastodon/Service/PhotoLibraryService.swift
+++ b/Mastodon/Service/PhotoLibraryService.swift
@@ -9,7 +9,7 @@ import os.log
import UIKit
import Combine
import Photos
-import Nuke
+import AlamofireImage
final class PhotoLibraryService: NSObject {
@@ -49,28 +49,29 @@ extension PhotoLibraryService {
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
- return ImagePipeline.shared.imagePublisher(with: url)
- .handleEvents(receiveSubscription: { _ in
- impactFeedbackGenerator.impactOccurred()
- }, receiveOutput: { response in
- self.save(image: response.image)
- }, receiveCompletion: { completion in
- switch completion {
+ return Future { promise in
+ ImageDownloader.default.download(URLRequest(url: url), completion: { response in
+ switch response.result {
case .failure(let error):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription)
-
- notificationFeedbackGenerator.notificationOccurred(.error)
- case .finished:
+ promise(.failure(error))
+ case .success(let image):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription)
-
- notificationFeedbackGenerator.notificationOccurred(.success)
+ promise(.success(image))
}
})
- .map { response in
- return response.image
+ }
+ .handleEvents(receiveSubscription: { _ in
+ impactFeedbackGenerator.impactOccurred()
+ }, receiveCompletion: { completion in
+ switch completion {
+ case .failure:
+ notificationFeedbackGenerator.notificationOccurred(.error)
+ case .finished:
+ notificationFeedbackGenerator.notificationOccurred(.success)
}
- .mapError { error in error as Error }
- .eraseToAnyPublisher()
+ })
+ .eraseToAnyPublisher()
}
func save(image: UIImage, withNotificationFeedback: Bool = false) {
diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift
index 4e09e4939..e65ea9aca 100644
--- a/NotificationService/NotificationService.swift
+++ b/NotificationService/NotificationService.swift
@@ -56,7 +56,7 @@ class NotificationService: UNNotificationServiceExtension {
bestAttemptContent.title = notification.title
bestAttemptContent.subtitle = ""
- bestAttemptContent.body = notification.body
+ bestAttemptContent.body = notification.body.escape()
bestAttemptContent.sound = UNNotificationSound.init(named: UNNotificationSoundName(rawValue: "BoopSound.caf"))
bestAttemptContent.userInfo["plaintext"] = plaintextData
@@ -105,3 +105,16 @@ extension NotificationService {
return try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData)
}
}
+
+extension String {
+ func escape() -> String {
+ return self
+ .replacingOccurrences(of: "&", with: "&")
+ .replacingOccurrences(of: "<", with: "<")
+ .replacingOccurrences(of: ">", with: ">")
+ .replacingOccurrences(of: """, with: "\"")
+ .replacingOccurrences(of: "'", with: "'")
+ .replacingOccurrences(of: "'", with: "’")
+
+ }
+}