diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
index 5bc61b648..14c7dc2ec 100644
--- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
+++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
@@ -85,7 +85,7 @@
-
+
@@ -132,6 +132,7 @@
+
@@ -181,8 +182,10 @@
+
+
@@ -281,12 +284,12 @@
-
+
-
+
diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift
index 6b27b4cd8..b7a101152 100644
--- a/CoreDataStack/Entity/MastodonUser.swift
+++ b/CoreDataStack/Entity/MastodonUser.swift
@@ -47,6 +47,7 @@ final public class MastodonUser: NSManagedObject {
// one-to-many relationship
@NSManaged public private(set) var statuses: Set?
+ @NSManaged public private(set) var notifications: Set?
// many-to-many relationship
@NSManaged public private(set) var favourite: Set?
diff --git a/CoreDataStack/Entity/SearchHistory.swift b/CoreDataStack/Entity/SearchHistory.swift
index da6d98bc2..3894d1c1b 100644
--- a/CoreDataStack/Entity/SearchHistory.swift
+++ b/CoreDataStack/Entity/SearchHistory.swift
@@ -11,6 +11,8 @@ import CoreData
public final class SearchHistory: NSManagedObject {
public typealias ID = UUID
@NSManaged public private(set) var identifier: ID
+ @NSManaged public private(set) var domain: String
+ @NSManaged public private(set) var userID: MastodonUser.ID
@NSManaged public private(set) var createAt: Date
@NSManaged public private(set) var updatedAt: Date
@@ -37,9 +39,12 @@ extension SearchHistory {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
+ property: Property,
account: MastodonUser
) -> SearchHistory {
let searchHistory: SearchHistory = context.insertObject()
+ searchHistory.domain = property.domain
+ searchHistory.userID = property.userID
searchHistory.account = account
return searchHistory
}
@@ -47,9 +52,12 @@ extension SearchHistory {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
+ property: Property,
hashtag: Tag
) -> SearchHistory {
let searchHistory: SearchHistory = context.insertObject()
+ searchHistory.domain = property.domain
+ searchHistory.userID = property.userID
searchHistory.hashtag = hashtag
return searchHistory
}
@@ -57,22 +65,54 @@ extension SearchHistory {
@discardableResult
public static func insert(
into context: NSManagedObjectContext,
+ property: Property,
status: Status
) -> SearchHistory {
let searchHistory: SearchHistory = context.insertObject()
+ searchHistory.domain = property.domain
+ searchHistory.userID = property.userID
searchHistory.status = status
return searchHistory
}
}
-public extension SearchHistory {
- func update(updatedAt: Date) {
+extension SearchHistory {
+ public func update(updatedAt: Date) {
setValue(updatedAt, forKey: #keyPath(SearchHistory.updatedAt))
}
}
+extension SearchHistory {
+ public struct Property {
+ public let domain: String
+ public let userID: MastodonUser.ID
+
+ public init(domain: String, userID: MastodonUser.ID) {
+ self.domain = domain
+ self.userID = userID
+ }
+ }
+}
+
extension SearchHistory: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \SearchHistory.updatedAt, ascending: false)]
}
}
+
+extension SearchHistory {
+ static func predicate(domain: String) -> NSPredicate {
+ return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.domain), domain)
+ }
+
+ static func predicate(userID: String) -> NSPredicate {
+ return NSPredicate(format: "%K == %@", #keyPath(SearchHistory.userID), userID)
+ }
+
+ public static func predicate(domain: String, userID: String) -> NSPredicate {
+ return NSCompoundPredicate(andPredicateWithSubpredicates: [
+ predicate(domain: domain),
+ predicate(userID: userID)
+ ])
+ }
+}
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index 51128d9ef..fa8b06c3d 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -190,7 +190,12 @@
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; };
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
- DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */ = {isa = PBXBuildFile; productRef = DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */; };
+ DB0C946526A6FD4D0088FB11 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB0C946426A6FD4D0088FB11 /* AlamofireImage */; };
+ DB0C946B26A700AB0088FB11 /* MastodonUser+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */; };
+ DB0C946C26A700CE0088FB11 /* MastodonUser+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */; };
+ DB0C946F26A7D2A80088FB11 /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C946E26A7D2A80088FB11 /* AvatarImageView.swift */; };
+ DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C947126A7D2D70088FB11 /* AvatarButton.swift */; };
+ DB0C947726A7FE840088FB11 /* NotificationAvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */; };
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; };
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; };
@@ -222,16 +227,8 @@
DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */; };
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
- DB41ED7A26A54D4400F58330 /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */; };
- DB41ED7B26A54D4D00F58330 /* MastodonStatusContent+ParseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */; };
- DB41ED7C26A54D5500F58330 /* MastodonStatusContent+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */; };
- DB41ED7E26A54D6D00F58330 /* Fuzi in Frameworks */ = {isa = PBXBuildFile; productRef = DB41ED7D26A54D6D00F58330 /* Fuzi */; };
- DB41ED8026A54D7C00F58330 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB41ED7F26A54D7C00F58330 /* AlamofireImage */; };
DB41ED8226A54D8A00F58330 /* MastodonMeta in Frameworks */ = {isa = PBXBuildFile; productRef = DB41ED8126A54D8A00F58330 /* MastodonMeta */; };
DB41ED8426A54D8A00F58330 /* MetaTextView in Frameworks */ = {isa = PBXBuildFile; productRef = DB41ED8326A54D8A00F58330 /* MetaTextView */; };
- DB41ED8926A54F4000F58330 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; };
- DB41ED8A26A54F4C00F58330 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
- DB41ED8B26A54F5800F58330 /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; };
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; };
@@ -280,10 +277,8 @@
DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */; };
DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */; };
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; };
- DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; };
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; };
- DB52D33A26839DD800D43133 /* ImageTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB52D33926839DD800D43133 /* ImageTask.swift */; };
DB564BD0269F2F83001E39A7 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */; };
DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */; };
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
@@ -340,8 +335,6 @@
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; };
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; };
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; };
- DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; };
- DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; };
DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; };
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
@@ -450,9 +443,10 @@
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; };
- DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; };
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */; };
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
+ DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
+ DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
@@ -470,13 +464,8 @@
DBBC24AA26A5301B00398BB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24A926A5301B00398BB9 /* MastodonSDK */; };
DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */; };
DBBC24AE26A53DC100398BB9 /* ReplicaStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24AD26A53DC100398BB9 /* ReplicaStatusView.swift */; };
- DBBC24B026A53DF900398BB9 /* ReplicaStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24AD26A53DC100398BB9 /* ReplicaStatusView.swift */; };
- DBBC24B226A53ED200398BB9 /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24B126A53ED200398BB9 /* ActiveLabel */; };
- DBBC24B326A53EE700398BB9 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
DBBC24B526A540AE00398BB9 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24B426A540AE00398BB9 /* AvatarConfigurableView.swift */; };
- DBBC24B626A5419700398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */; };
DBBC24B826A5421800398BB9 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24B726A5421800398BB9 /* CommonOSLog */; };
- DBBC24B926A5426000398BB9 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
DBBC24BC26A542F500398BB9 /* ThemeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24BB26A542F500398BB9 /* ThemeService.swift */; };
DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24BE26A5443100398BB9 /* MastodonTheme.swift */; };
DBBC24C126A5443100398BB9 /* SystemTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24BF26A5443100398BB9 /* SystemTheme.swift */; };
@@ -567,13 +556,9 @@
DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */; };
DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */; };
DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */; };
- DBFEF06826A67DEE006D7ED1 /* MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */; };
DBFEF06926A67E45006D7ED1 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; };
- DBFEF06A26A67E53006D7ED1 /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
- DBFEF06B26A67E58006D7ED1 /* Fields.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94439265CC0FC00C537E1 /* Fields.swift */; };
DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */; };
DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
- DBFEF07126A690E8006D7ED1 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DBFEF07026A690E8006D7ED1 /* AlamofireNetworkActivityIndicator */; };
DBFEF07326A6913D006D7ED1 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07226A6913D006D7ED1 /* APIService.swift */; };
DBFEF07526A69192006D7ED1 /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; };
DBFEF07726A691FB006D7ED1 /* MastodonAuthenticationBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */; };
@@ -686,7 +671,6 @@
files = (
DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */,
DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */,
- DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -908,6 +892,10 @@
DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = ""; };
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; };
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; };
+ DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonUser+Property.swift"; sourceTree = ""; };
+ DB0C946E26A7D2A80088FB11 /* AvatarImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarImageView.swift; sourceTree = ""; };
+ DB0C947126A7D2D70088FB11 /* AvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarButton.swift; sourceTree = ""; };
+ DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAvatarButton.swift; sourceTree = ""; };
DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; };
DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; };
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = ""; };
@@ -996,10 +984,8 @@
DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryFetchedResultController.swift; sourceTree = ""; };
DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchToSearchDetailViewControllerAnimatedTransitioning.swift; sourceTree = ""; };
DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTransitionController.swift; sourceTree = ""; };
- DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; };
DB51D170262832380062B7A1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; };
DB51D171262832380062B7A1 /* BlurHashEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashEncode.swift; sourceTree = ""; };
- DB52D33926839DD800D43133 /* ImageTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTask.swift; sourceTree = ""; };
DB564BCF269F2F83001E39A7 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = ""; };
DB564BD1269F2F8A001E39A7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; };
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFilterService.swift; sourceTree = ""; };
@@ -1159,6 +1145,7 @@
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = ""; };
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentCacheService.swift; sourceTree = ""; };
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = ""; };
+ DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = ""; };
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = ""; };
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = ""; };
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = ""; };
@@ -1269,8 +1256,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */,
- DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */,
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
DB03F7ED268976B5007B274C /* MetaTextView in Frameworks */,
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
@@ -1283,7 +1268,6 @@
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
- DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
@@ -1347,14 +1331,11 @@
DBC6463726A195DB00B0E31B /* CoreDataStack.framework in Frameworks */,
DB41ED8426A54D8A00F58330 /* MetaTextView in Frameworks */,
DB41ED8226A54D8A00F58330 /* MastodonMeta in Frameworks */,
- DB41ED7E26A54D6D00F58330 /* Fuzi in Frameworks */,
DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */,
- DBBC24B226A53ED200398BB9 /* ActiveLabel in Frameworks */,
DBBC24AA26A5301B00398BB9 /* MastodonSDK in Frameworks */,
- DB41ED8026A54D7C00F58330 /* AlamofireImage in Frameworks */,
+ DB0C946526A6FD4D0088FB11 /* AlamofireImage in Frameworks */,
DBC6463326A195DB00B0E31B /* AppShared.framework in Frameworks */,
4278334D6033AEEE0A1C5155 /* Pods_ShareActionExtension.framework in Frameworks */,
- DBFEF07126A690E8006D7ED1 /* AlamofireNetworkActivityIndicator in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1564,7 +1545,8 @@
2D42FF8325C82245004A627A /* Button */ = {
isa = PBXGroup;
children = (
- DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */,
+ DB0C947126A7D2D70088FB11 /* AvatarButton.swift */,
+ DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */,
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */,
0FAA101125E105390017CCDE /* PrimaryActionButton.swift */,
);
@@ -1715,6 +1697,7 @@
DB9D6C1325E4F97A0051B173 /* Container */,
DBA9B90325F1D4420012E7B6 /* Control */,
2D152A8A25C295B8009AA50C /* Content */,
+ DB0C947026A7D2AB0088FB11 /* ImageView */,
DB87D45C2609DE6600D12C0D /* TextField */,
DB1D187125EF5BBD003F1F23 /* TableView */,
2D7631A625C1533800929FB9 /* TableviewCell */,
@@ -1918,6 +1901,7 @@
children = (
DB084B5625CBC56C00F898ED /* Status.swift */,
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
+ DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */,
DB9D6C3725E508BE0051B173 /* Attachment.swift */,
DB6D9F6E2635807F008423CD /* Setting.swift */,
DB6D9F4826353FD6008423CD /* Subscription.swift */,
@@ -1929,6 +1913,22 @@
path = CoreDataStack;
sourceTree = "";
};
+ DB0C947026A7D2AB0088FB11 /* ImageView */ = {
+ isa = PBXGroup;
+ children = (
+ DB0C946E26A7D2A80088FB11 /* AvatarImageView.swift */,
+ );
+ path = ImageView;
+ sourceTree = "";
+ };
+ DB0C947826A7FE950088FB11 /* Button */ = {
+ isa = PBXGroup;
+ children = (
+ DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */,
+ );
+ path = Button;
+ sourceTree = "";
+ };
DB1D187125EF5BBD003F1F23 /* TableView */ = {
isa = PBXGroup;
children = (
@@ -2528,7 +2528,7 @@
DBCC3B35261440BA0045B23D /* UINavigationController.swift */,
DB97131E2666078B00BD1E90 /* Date.swift */,
DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */,
- DB52D33926839DD800D43133 /* ImageTask.swift */,
+ DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */,
);
path = Extension;
sourceTree = "";
@@ -2587,13 +2587,14 @@
DB9D6BFD25E4F57B0051B173 /* Notification */ = {
isa = PBXGroup;
children = (
+ DB0C947826A7FE950088FB11 /* Button */,
+ 2D35237F26256F470031AF25 /* TableViewCell */,
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */,
2D607AD726242FC500B70763 /* NotificationViewModel.swift */,
2D084B8C26258EA3003AA3AF /* NotificationViewModel+Diffable.swift */,
2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */,
2D24E12C2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift */,
- 2D35237F26256F470031AF25 /* TableViewCell */,
);
path = Notification;
sourceTree = "";
@@ -2658,7 +2659,6 @@
DBA9B90325F1D4420012E7B6 /* Control */ = {
isa = PBXGroup;
children = (
- DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */,
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */,
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */,
);
@@ -2995,13 +2995,10 @@
2D939AC725EE14620076FA61 /* CropViewController */,
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
DBB525072611EAC0002F1F29 /* Tabman */,
- DB6F5E31264E7410009108F4 /* TwitterTextEditor */,
- DBAEDE5E267A0B1500D25FF5 /* Nuke */,
DBAC6482267D0B21007FE9FD /* DifferenceKit */,
DBAC649D267DFE43007FE9FD /* DiffableDataSources */,
DBAC64A0267E6D02007FE9FD /* Fuzi */,
DBF7A0FB26830C33004176A2 /* FPSIndicator */,
- DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */,
DB03F7EA268976B5007B274C /* MastodonMeta */,
DB03F7EC268976B5007B274C /* MetaTextView */,
DBC6462A26A1738900B0E31B /* MastodonUI */,
@@ -3128,14 +3125,11 @@
packageProductDependencies = (
DBC6462426A1720B00B0E31B /* MastodonUI */,
DBBC24A926A5301B00398BB9 /* MastodonSDK */,
- DBBC24B126A53ED200398BB9 /* ActiveLabel */,
DBBC24B726A5421800398BB9 /* CommonOSLog */,
DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */,
- DB41ED7D26A54D6D00F58330 /* Fuzi */,
- DB41ED7F26A54D7C00F58330 /* AlamofireImage */,
DB41ED8126A54D8A00F58330 /* MastodonMeta */,
DB41ED8326A54D8A00F58330 /* MetaTextView */,
- DBFEF07026A690E8006D7ED1 /* AlamofireNetworkActivityIndicator */,
+ DB0C946426A6FD4D0088FB11 /* AlamofireImage */,
);
productName = ShareActionExtension;
productReference = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */;
@@ -3227,7 +3221,6 @@
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */,
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
- DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */,
DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */,
DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */,
DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */,
@@ -3563,6 +3556,7 @@
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */,
+ DB0C946F26A7D2A80088FB11 /* AvatarImageView.swift in Sources */,
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
@@ -3592,6 +3586,7 @@
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */,
+ DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */,
DBF1D251269DB01200C1C08A /* SearchHistoryViewController.swift in Sources */,
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
@@ -3630,6 +3625,7 @@
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */,
+ DB0C947726A7FE840088FB11 /* NotificationAvatarButton.swift in Sources */,
5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */,
DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */,
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
@@ -3687,7 +3683,6 @@
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
- DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
@@ -3699,7 +3694,6 @@
DBCBCC012680AF2A000F5B51 /* AsyncHomeTimelineViewModel+Diffable.swift in Sources */,
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */,
DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */,
- DB52D33A26839DD800D43133 /* ImageTask.swift in Sources */,
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
@@ -3844,6 +3838,7 @@
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
+ DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */,
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */,
DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */,
@@ -3853,6 +3848,7 @@
5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */,
0F20223926146553000C64BF /* Array.swift in Sources */,
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */,
+ DB0C946B26A700AB0088FB11 /* MastodonUser+Property.swift in Sources */,
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
@@ -4019,41 +4015,30 @@
files = (
DBFEF05E26A57715006D7ED1 /* ComposeView.swift in Sources */,
DBFEF07326A6913D006D7ED1 /* APIService.swift in Sources */,
- DB41ED7C26A54D5500F58330 /* MastodonStatusContent+Appearance.swift in Sources */,
DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */,
DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */,
- DBBC24B326A53EE700398BB9 /* ActiveLabel.swift in Sources */,
DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */,
DBBC24CB26A546C000398BB9 /* ThemePreference.swift in Sources */,
DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */,
DBFEF05D26A57715006D7ED1 /* ContentWarningEditorView.swift in Sources */,
- DBFEF06A26A67E53006D7ED1 /* Emojis.swift in Sources */,
DBFEF07526A69192006D7ED1 /* APIService+Media.swift in Sources */,
- DB41ED7B26A54D4D00F58330 /* MastodonStatusContent+ParseResult.swift in Sources */,
- DB41ED8A26A54F4C00F58330 /* AttachmentContainerView.swift in Sources */,
DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */,
DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */,
DBBC24C726A5456400398BB9 /* SystemTheme.swift in Sources */,
- DBBC24B626A5419700398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */,
DBC6462926A1736700B0E31B /* Strings.swift in Sources */,
DBBC24C826A5456400398BB9 /* ThemeService.swift in Sources */,
- DBFEF06826A67DEE006D7ED1 /* MastodonUser.swift in Sources */,
DBBC24C926A5456400398BB9 /* MastodonTheme.swift in Sources */,
DBFEF07C26A6BD0A006D7ED1 /* APIService+Status+Publish.swift in Sources */,
- DBBC24B926A5426000398BB9 /* StatusContentWarningEditorView.swift in Sources */,
- DB41ED8B26A54F5800F58330 /* AttachmentContainerView+EmptyStateView.swift in Sources */,
+ DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */,
DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */,
- DB41ED7A26A54D4400F58330 /* MastodonStatusContent.swift in Sources */,
DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */,
- DBBC24B026A53DF900398BB9 /* ReplicaStatusView.swift in Sources */,
DBBC24C626A5456000398BB9 /* Theme.swift in Sources */,
DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */,
- DB41ED8926A54F4000F58330 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
DBBC24D226A5488600398BB9 /* AvatarConfigurableView.swift in Sources */,
DBC6462C26A176B000B0E31B /* Assets.swift in Sources */,
DBFEF06926A67E45006D7ED1 /* AppearancePreference.swift in Sources */,
- DBFEF06B26A67E58006D7ED1 /* Fields.swift in Sources */,
DBFEF07826A69209006D7ED1 /* MastodonAuthenticationBox.swift in Sources */,
+ DB0C946C26A700CE0088FB11 /* MastodonUser+Property.swift in Sources */,
DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -4323,7 +4308,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -4331,7 +4316,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -4350,7 +4335,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -4358,7 +4343,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -4513,6 +4498,7 @@
DB89BA0625C10FD0008580ED /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@@ -4542,6 +4528,7 @@
DB89BA0725C10FD0008580ED /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@@ -4613,7 +4600,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4621,13 +4608,13 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
@@ -4637,7 +4624,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4645,13 +4632,13 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
};
name = "ASDK - Debug";
};
@@ -4661,7 +4648,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4669,13 +4656,13 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
};
name = "ASDK - Release";
};
@@ -4685,7 +4672,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4693,13 +4680,13 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
@@ -4774,7 +4761,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -4782,7 +4769,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -4836,6 +4823,7 @@
DBCBCC122680BE3E000F5B51 /* ASDK - Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@@ -4888,7 +4876,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4896,7 +4884,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -5007,7 +4995,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -5015,7 +5003,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -5069,6 +5057,7 @@
DBCBCC2226818F6F000F5B51 /* ASDK - Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
@@ -5121,7 +5110,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5129,7 +5118,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -5175,7 +5164,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5183,7 +5172,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -5198,7 +5187,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 41;
+ CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5206,7 +5195,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 0.9.1;
+ MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -5432,14 +5421,6 @@
minimumVersion = 3.1.3;
};
};
- DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */ = {
- isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/kean/Nuke.git";
- requirement = {
- kind = upToNextMajorVersion;
- minimumVersion = 10.3.0;
- };
- };
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/uias/Tabman";
@@ -5503,22 +5484,12 @@
package = DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */;
productName = MetaTextView;
};
- DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */ = {
- isa = XCSwiftPackageProductDependency;
- package = DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */;
- productName = NukeFLAnimatedImagePlugin;
- };
- DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = {
+ DB0C946426A6FD4D0088FB11 /* AlamofireImage */ = {
isa = XCSwiftPackageProductDependency;
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
productName = AlamofireImage;
};
- DB41ED7D26A54D6D00F58330 /* Fuzi */ = {
- isa = XCSwiftPackageProductDependency;
- package = DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */;
- productName = Fuzi;
- };
- DB41ED7F26A54D7C00F58330 /* AlamofireImage */ = {
+ DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = {
isa = XCSwiftPackageProductDependency;
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
productName = AlamofireImage;
@@ -5543,11 +5514,6 @@
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
productName = AlamofireImage;
};
- DB6F5E31264E7410009108F4 /* TwitterTextEditor */ = {
- isa = XCSwiftPackageProductDependency;
- package = DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
- productName = TwitterTextEditor;
- };
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
isa = XCSwiftPackageProductDependency;
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
@@ -5568,11 +5534,6 @@
package = DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */;
productName = Fuzi;
};
- DBAEDE5E267A0B1500D25FF5 /* Nuke */ = {
- isa = XCSwiftPackageProductDependency;
- package = DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */;
- productName = Nuke;
- };
DBB525072611EAC0002F1F29 /* Tabman */ = {
isa = XCSwiftPackageProductDependency;
package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */;
@@ -5582,11 +5543,6 @@
isa = XCSwiftPackageProductDependency;
productName = MastodonSDK;
};
- DBBC24B126A53ED200398BB9 /* ActiveLabel */ = {
- isa = XCSwiftPackageProductDependency;
- package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */;
- productName = ActiveLabel;
- };
DBBC24B726A5421800398BB9 /* CommonOSLog */ = {
isa = XCSwiftPackageProductDependency;
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
@@ -5614,11 +5570,6 @@
package = DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */;
productName = FPSIndicator;
};
- DBFEF07026A690E8006D7ED1 /* AlamofireNetworkActivityIndicator */ = {
- isa = XCSwiftPackageProductDependency;
- package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */;
- productName = AlamofireNetworkActivityIndicator;
- };
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index e79a0423f..fd09596bc 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
- 24
+ 22
Mastodon - ASDK.xcscheme_^#shared#^_
@@ -37,7 +37,7 @@
NotificationService.xcscheme_^#shared#^_
orderHint
- 22
+ 23
ShareActionExtension.xcscheme_^#shared#^_
diff --git a/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift b/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift
index b83bfe662..6d4461eab 100644
--- a/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift
+++ b/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift
@@ -17,6 +17,8 @@ final class SearchHistoryFetchedResultController: NSObject {
var disposeBag = Set()
let fetchedResultsController: NSFetchedResultsController
+ let domain = CurrentValueSubject(nil)
+ let userID = CurrentValueSubject(nil)
// output
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
@@ -38,6 +40,23 @@ final class SearchHistoryFetchedResultController: NSObject {
super.init()
fetchedResultsController.delegate = self
+
+ Publishers.CombineLatest(
+ self.domain.removeDuplicates(),
+ self.userID.removeDuplicates()
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] domain, userID in
+ guard let self = self else { return }
+ let predicates = [SearchHistory.predicate(domain: domain ?? "", userID: userID ?? "")]
+ self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
+ do {
+ try self.fetchedResultsController.performFetch()
+ } catch {
+ assertionFailure(error.localizedDescription)
+ }
+ }
+ .store(in: &disposeBag)
}
}
diff --git a/Mastodon/Diffiable/Item/SettingsItem.swift b/Mastodon/Diffiable/Item/SettingsItem.swift
index e23b3f36f..777df750d 100644
--- a/Mastodon/Diffiable/Item/SettingsItem.swift
+++ b/Mastodon/Diffiable/Item/SettingsItem.swift
@@ -8,12 +8,10 @@
import UIKit
import CoreData
-enum SettingsItem: Hashable {
+enum SettingsItem {
case appearance(settingObjectID: NSManagedObjectID)
case notification(settingObjectID: NSManagedObjectID, switchMode: NotificationSwitchMode)
- case preferenceDarkMode(settingObjectID: NSManagedObjectID)
- case preferenceDisableAvatarAnimation(settingObjectID: NSManagedObjectID)
- case preferenceUsingDefaultBrowser(settingObjectID: NSManagedObjectID)
+ case preference(settingObjectID: NSManagedObjectID, preferenceType: PreferenceType)
case boringZone(item: Link)
case spicyZone(item: Link)
}
@@ -26,7 +24,7 @@ extension SettingsItem {
case dark
}
- enum NotificationSwitchMode: CaseIterable {
+ enum NotificationSwitchMode: CaseIterable, Hashable {
case favorite
case follow
case reblog
@@ -41,8 +39,22 @@ extension SettingsItem {
}
}
}
+
+ enum PreferenceType: CaseIterable {
+ case darkMode
+ case disableAvatarAnimation
+ case useDefaultBrowser
+
+ var title: String {
+ switch self {
+ case .darkMode: return L10n.Scene.Settings.Section.AppearanceSettings.trueBlackDarkMode
+ case .disableAvatarAnimation: return L10n.Scene.Settings.Section.AppearanceSettings.disableAvatarAnimation
+ case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
+ }
+ }
+ }
- enum Link: CaseIterable {
+ enum Link: CaseIterable, Hashable {
case accountSettings
case termsOfService
case privacyPolicy
@@ -71,3 +83,27 @@ extension SettingsItem {
}
}
+
+extension SettingsItem: Hashable {
+ func hash(into hasher: inout Hasher) {
+ switch self {
+ case .appearance(let settingObjectID):
+ hasher.combine(String(describing: SettingsItem.AppearanceMode.self))
+ hasher.combine(settingObjectID)
+ case .notification(let settingObjectID, let switchMode):
+ hasher.combine(String(describing: SettingsItem.notification.self))
+ hasher.combine(settingObjectID)
+ hasher.combine(switchMode)
+ case .preference(let settingObjectID, let preferenceType):
+ hasher.combine(String(describing: SettingsItem.preference.self))
+ hasher.combine(settingObjectID)
+ hasher.combine(preferenceType)
+ case .boringZone(let link):
+ hasher.combine(String(describing: SettingsItem.boringZone.self))
+ hasher.combine(link)
+ case .spicyZone(let link):
+ hasher.combine(String(describing: SettingsItem.spicyZone.self))
+ hasher.combine(link)
+ }
+ }
+}
diff --git a/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift
index 57d7b6019..70de18c6b 100644
--- a/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift
+++ b/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift
@@ -6,7 +6,6 @@
//
import UIKit
-import Nuke
enum CustomEmojiPickerSection: Equatable, Hashable {
case emoji(name: String)
@@ -24,14 +23,9 @@ extension CustomEmojiPickerSection {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell
let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
.af.imageRounded(withCornerRadius: 4)
- cell.imageTask = Nuke.loadImage(
- with: attribute.emoji.url,
- options: .init(
- placeholder: placeholder,
- transition: .fadeIn(duration: 0.2)
- ),
- into: cell.emojiImageView
- )
+
+ let url = URL(string: attribute.emoji.url)
+ cell.emojiImageView.setImage(url: url, placeholder: placeholder, scaleToSize: CustomEmojiPickerItemCollectionViewCell.itemSize)
cell.accessibilityLabel = attribute.emoji.shortcode
return cell
}
diff --git a/Mastodon/Diffiable/Section/SettingsSection.swift b/Mastodon/Diffiable/Section/SettingsSection.swift
index 4d3c13d27..e329bd120 100644
--- a/Mastodon/Diffiable/Section/SettingsSection.swift
+++ b/Mastodon/Diffiable/Section/SettingsSection.swift
@@ -5,7 +5,9 @@
// Created by MainasuK Cirno on 2021-4-25.
//
-import Foundation
+import UIKit
+import CoreData
+import CoreDataStack
enum SettingsSection: Hashable {
case appearance
@@ -24,3 +26,125 @@ enum SettingsSection: Hashable {
}
}
}
+
+extension SettingsSection {
+ static func tableViewDiffableDataSource(
+ for tableView: UITableView,
+ managedObjectContext: NSManagedObjectContext,
+ settingsAppearanceTableViewCellDelegate: SettingsAppearanceTableViewCellDelegate,
+ settingsToggleCellDelegate: SettingsToggleCellDelegate
+ ) -> UITableViewDiffableDataSource {
+ UITableViewDiffableDataSource(tableView: tableView) { [
+ weak settingsAppearanceTableViewCellDelegate,
+ weak settingsToggleCellDelegate
+ ] tableView, indexPath, item -> UITableViewCell? in
+ switch item {
+ case .appearance(let objectID):
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
+ managedObjectContext.performAndWait {
+ let setting = managedObjectContext.object(with: objectID) as! Setting
+ cell.update(with: setting.appearance)
+ ManagedObjectObserver.observe(object: setting)
+ .receive(on: DispatchQueue.main)
+ .sink(receiveCompletion: { _ in
+ // do nothing
+ }, receiveValue: { [weak cell] change in
+ guard let cell = cell else { return }
+ guard case .update(let object) = change.changeType,
+ let setting = object as? Setting else { return }
+ cell.update(with: setting.appearance)
+ })
+ .store(in: &cell.disposeBag)
+ }
+ cell.delegate = settingsAppearanceTableViewCellDelegate
+ return cell
+ case .notification(let objectID, let switchMode):
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
+ managedObjectContext.performAndWait {
+ let setting = managedObjectContext.object(with: objectID) as! Setting
+ if let subscription = setting.activeSubscription {
+ SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
+ }
+ ManagedObjectObserver.observe(object: setting)
+ .sink(receiveCompletion: { _ in
+ // do nothing
+ }, receiveValue: { [weak cell] change in
+ guard let cell = cell else { return }
+ guard case .update(let object) = change.changeType,
+ let setting = object as? Setting else { return }
+ guard let subscription = setting.activeSubscription else { return }
+ SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
+ })
+ .store(in: &cell.disposeBag)
+ }
+ cell.delegate = settingsToggleCellDelegate
+ return cell
+ case .preference(let objectID, _):
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
+ cell.delegate = settingsToggleCellDelegate
+ managedObjectContext.performAndWait {
+ let setting = managedObjectContext.object(with: objectID) as! Setting
+ SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
+
+ ManagedObjectObserver.observe(object: setting)
+ .receive(on: DispatchQueue.main)
+ .sink(receiveCompletion: { _ in
+ // do nothing
+ }, receiveValue: { [weak cell] change in
+ guard let cell = cell else { return }
+ guard case .update(let object) = change.changeType,
+ let setting = object as? Setting else { return }
+ SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
+ })
+ .store(in: &cell.disposeBag)
+ }
+ return cell
+ case .boringZone(let item),
+ .spicyZone(let item):
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
+ cell.update(with: item)
+ return cell
+ }
+ }
+ }
+}
+
+extension SettingsSection {
+
+ static func configureSettingToggle(
+ cell: SettingsToggleTableViewCell,
+ item: SettingsItem,
+ setting: Setting
+ ) {
+ guard case let .preference(_, preferenceType) = item else { return }
+
+ cell.textLabel?.text = preferenceType.title
+
+ switch preferenceType {
+ case .darkMode:
+ cell.switchButton.isOn = setting.preferredTrueBlackDarkMode
+ case .disableAvatarAnimation:
+ cell.switchButton.isOn = setting.preferredStaticAvatar
+ case .useDefaultBrowser:
+ cell.switchButton.isOn = setting.preferredUsingDefaultBrowser
+ }
+ }
+
+ static func configureSettingToggle(
+ cell: SettingsToggleTableViewCell,
+ switchMode: SettingsItem.NotificationSwitchMode,
+ subscription: NotificationSubscription
+ ) {
+ cell.textLabel?.text = switchMode.title
+
+ let enabled: Bool?
+ switch switchMode {
+ case .favorite: enabled = subscription.alert.favourite
+ case .follow: enabled = subscription.alert.follow
+ case .reblog: enabled = subscription.alert.reblog
+ case .mention: enabled = subscription.alert.mention
+ }
+ cell.update(enabled: enabled)
+ }
+
+}
diff --git a/Mastodon/Diffiable/Section/Status/NotificationSection.swift b/Mastodon/Diffiable/Section/Status/NotificationSection.swift
index 5769c912d..01229235b 100644
--- a/Mastodon/Diffiable/Section/Status/NotificationSection.swift
+++ b/Mastodon/Diffiable/Section/Status/NotificationSection.swift
@@ -11,7 +11,6 @@ import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
-import Nuke
enum NotificationSection: Equatable, Hashable {
case main
@@ -56,17 +55,16 @@ extension NotificationSection {
.af.imageAspectScaled(toFit: CGSize(width: 14, height: 14))
}
- cell.actionImageView.image = createActionImage()
+ cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color
+ cell.avatarButton.badgeImageView.image = createActionImage()
cell.traitCollectionDidChange
.receive(on: DispatchQueue.main)
.sink { [weak cell] in
guard let cell = cell else { return }
- cell.actionImageView.image = createActionImage()
+ cell.avatarButton.badgeImageView.image = createActionImage()
}
.store(in: &cell.disposeBag)
- cell.actionImageView.backgroundColor = notification.notificationType.color
-
// configure author name, notification description, timestamp
cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict)
let createAt = notification.createAt
diff --git a/Mastodon/Diffiable/Section/Status/StatusSection.swift b/Mastodon/Diffiable/Section/Status/StatusSection.swift
index e1823851a..18406da66 100644
--- a/Mastodon/Diffiable/Section/Status/StatusSection.swift
+++ b/Mastodon/Diffiable/Section/Status/StatusSection.swift
@@ -688,12 +688,12 @@ extension StatusSection {
cell.statusView.usernameLabel.text = "@" + author.acct
// avatar
if let reblog = status.reblog {
- cell.statusView.avatarImageView.isHidden = true
+ cell.statusView.avatarButton.isHidden = true
cell.statusView.avatarStackedContainerButton.isHidden = false
cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL()))
cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
} else {
- cell.statusView.avatarImageView.isHidden = false
+ cell.statusView.avatarButton.isHidden = false
cell.statusView.avatarStackedContainerButton.isHidden = true
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
}
diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser+Property.swift b/Mastodon/Extension/CoreDataStack/MastodonUser+Property.swift
new file mode 100644
index 000000000..1e4e542f8
--- /dev/null
+++ b/Mastodon/Extension/CoreDataStack/MastodonUser+Property.swift
@@ -0,0 +1,74 @@
+//
+// MastodonUser+Property.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-20.
+//
+
+import Foundation
+import CoreDataStack
+
+extension MastodonUser {
+
+ var displayNameWithFallback: String {
+ return !displayName.isEmpty ? displayName : username
+ }
+
+ var acctWithDomain: String {
+ if !acct.contains("@") {
+ // Safe concat due to username cannot contains "@"
+ return username + "@" + domain
+ } else {
+ return acct
+ }
+ }
+
+ var domainFromAcct: String {
+ if !acct.contains("@") {
+ return domain
+ } else {
+ let domain = acct.split(separator: "@").last
+ return String(domain!)
+ }
+ }
+
+}
+
+extension MastodonUser {
+
+ public func headerImageURL() -> URL? {
+ return URL(string: header)
+ }
+
+ public func headerImageURLWithFallback(domain: String) -> URL {
+ return URL(string: header) ?? URL(string: "https://\(domain)/headers/original/missing.png")!
+ }
+
+ 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")!
+ }
+
+}
+
+extension MastodonUser {
+
+ var profileURL: URL {
+ if let urlString = self.url,
+ let url = URL(string: urlString) {
+ return url
+ } else {
+ return URL(string: "https://\(self.domain)/@\(username)")!
+ }
+ }
+
+ var activityItems: [Any] {
+ var items: [Any] = []
+ items.append(profileURL)
+ return items
+ }
+}
diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift
index b9be9165b..f914c8649 100644
--- a/Mastodon/Extension/CoreDataStack/MastodonUser.swift
+++ b/Mastodon/Extension/CoreDataStack/MastodonUser.swift
@@ -37,70 +37,5 @@ extension MastodonUser.Property {
}
}
-extension MastodonUser {
-
- var displayNameWithFallback: String {
- return !displayName.isEmpty ? displayName : username
- }
-
- var acctWithDomain: String {
- if !acct.contains("@") {
- // Safe concat due to username cannot contains "@"
- return username + "@" + domain
- } else {
- return acct
- }
- }
-
- var domainFromAcct: String {
- if !acct.contains("@") {
- return domain
- } else {
- let domain = acct.split(separator: "@").last
- return String(domain!)
- }
- }
-
-}
-
-extension MastodonUser {
-
- public func headerImageURL() -> URL? {
- return URL(string: header)
- }
-
- public func headerImageURLWithFallback(domain: String) -> URL {
- return URL(string: header) ?? URL(string: "https://\(domain)/headers/original/missing.png")!
- }
-
- 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")!
- }
-
-}
-
-extension MastodonUser {
-
- var profileURL: URL {
- if let urlString = self.url,
- let url = URL(string: urlString) {
- return url
- } else {
- return URL(string: "https://\(self.domain)/@\(username)")!
- }
- }
-
- var activityItems: [Any] {
- var items: [Any] = []
- items.append(profileURL)
- return items
- }
-}
-
extension MastodonUser: EmojiContainer { }
extension MastodonUser: FieldContainer { }
diff --git a/Mastodon/Extension/FLAnimatedImageView.swift b/Mastodon/Extension/FLAnimatedImageView.swift
new file mode 100644
index 000000000..1e6e62ad8
--- /dev/null
+++ b/Mastodon/Extension/FLAnimatedImageView.swift
@@ -0,0 +1,81 @@
+//
+// FLAnimatedImageView.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-21.
+//
+
+import Foundation
+import Combine
+import Alamofire
+import AlamofireImage
+import FLAnimatedImage
+
+private enum FLAnimatedImageViewAssociatedKeys {
+ static var activeAvatarRequestURL = "FLAnimatedImageViewAssociatedKeys.activeAvatarRequestURL"
+ static var avatarRequestCancellable = "FLAnimatedImageViewAssociatedKeys.avatarRequestCancellable"
+}
+
+extension FLAnimatedImageView {
+
+ var activeAvatarRequestURL: URL? {
+ get {
+ objc_getAssociatedObject(self, &FLAnimatedImageViewAssociatedKeys.activeAvatarRequestURL) as? URL
+ }
+ set {
+ objc_setAssociatedObject(self, &FLAnimatedImageViewAssociatedKeys.activeAvatarRequestURL, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ }
+ }
+
+ var avatarRequestCancellable: AnyCancellable? {
+ get {
+ objc_getAssociatedObject(self, &FLAnimatedImageViewAssociatedKeys.avatarRequestCancellable) as? AnyCancellable
+ }
+ set {
+ objc_setAssociatedObject(self, &FLAnimatedImageViewAssociatedKeys.avatarRequestCancellable, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ }
+ }
+
+ func setImage(url: URL?, placeholder: UIImage?, scaleToSize: CGSize?) {
+ // cancel task
+ activeAvatarRequestURL = nil
+ avatarRequestCancellable?.cancel()
+
+ // set placeholder
+ image = placeholder
+
+ // set image
+ guard let url = url else { return }
+ activeAvatarRequestURL = url
+ let avatarRequest = AF.request(url).publishData()
+ avatarRequestCancellable = avatarRequest
+ .sink { response in
+ switch response.result {
+ case .success(let data):
+ DispatchQueue.global().async {
+ let image: UIImage? = {
+ if let scaleToSize = scaleToSize {
+ return UIImage(data: data)?.af.imageScaled(to: scaleToSize, scale: UIScreen.main.scale)
+ } else {
+ return UIImage(data: data)
+ }
+ }()
+ let animatedImage = FLAnimatedImage(animatedGIFData: data)
+
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ if self.activeAvatarRequestURL == url {
+ if let animatedImage = animatedImage {
+ self.animatedImage = animatedImage
+ } else {
+ self.image = image
+ }
+ }
+ }
+ }
+ case .failure:
+ break
+ }
+ }
+ }
+}
diff --git a/Mastodon/Extension/ImageTask.swift b/Mastodon/Extension/ImageTask.swift
deleted file mode 100644
index 86be32d15..000000000
--- a/Mastodon/Extension/ImageTask.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// ImageTask.swift
-// Mastodon
-//
-// Created by Cirno MainasuK on 2021-6-24.
-//
-
-import Foundation
-import Nuke
-
-extension ImageTask {
- func store(in set: inout Set) {
- set.insert(self)
- }
-}
diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift
index 0f405ef05..0fe48777f 100644
--- a/Mastodon/Generated/Assets.swift
+++ b/Mastodon/Generated/Assets.swift
@@ -34,7 +34,6 @@ internal enum Asset {
internal enum Colors {
internal enum Border {
internal static let composePoll = ColorAsset(name: "Colors/Border/compose.poll")
- internal static let notificationStatus = ColorAsset(name: "Colors/Border/notification.status")
internal static let searchCard = ColorAsset(name: "Colors/Border/searchCard")
internal static let status = ColorAsset(name: "Colors/Border/status")
}
@@ -65,9 +64,6 @@ internal enum Asset {
internal enum Slider {
internal static let track = ColorAsset(name: "Colors/Slider/track")
}
- internal enum TabBar {
- internal static let itemInactive = ColorAsset(name: "Colors/TabBar/item.inactive")
- }
internal enum TextField {
internal static let background = ColorAsset(name: "Colors/TextField/background")
internal static let invalid = ColorAsset(name: "Colors/TextField/invalid")
@@ -135,6 +131,7 @@ internal enum Asset {
internal static let tableViewCellSelectionBackground = ColorAsset(name: "Theme/Mastodon/table.view.cell.selection.background")
internal static let tertiarySystemBackground = ColorAsset(name: "Theme/Mastodon/tertiary.system.background")
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Theme/Mastodon/tertiary.system.grouped.background")
+ internal static let notificationStatusBorderColor = ColorAsset(name: "Theme/Mastodon/notification.status.border.color")
internal static let separator = ColorAsset(name: "Theme/Mastodon/separator")
internal static let tabBarItemInactiveIconColor = ColorAsset(name: "Theme/Mastodon/tab.bar.item.inactive.icon.color")
}
@@ -153,6 +150,7 @@ internal enum Asset {
internal static let tableViewCellSelectionBackground = ColorAsset(name: "Theme/system/table.view.cell.selection.background")
internal static let tertiarySystemBackground = ColorAsset(name: "Theme/system/tertiary.system.background")
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Theme/system/tertiary.system.grouped.background")
+ internal static let notificationStatusBorderColor = ColorAsset(name: "Theme/system/notification.status.border.color")
internal static let separator = ColorAsset(name: "Theme/system/separator")
internal static let tabBarItemInactiveIconColor = ColorAsset(name: "Theme/system/tab.bar.item.inactive.icon.color")
}
diff --git a/Mastodon/Helper/MastodonStatusContent.swift b/Mastodon/Helper/MastodonStatusContent.swift
index 90e697daf..917c456d2 100755
--- a/Mastodon/Helper/MastodonStatusContent.swift
+++ b/Mastodon/Helper/MastodonStatusContent.swift
@@ -29,7 +29,7 @@ public enum MastodonStatusContent {
public static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult {
let document: String = {
- var content = content
+ var content = content.replacingOccurrences(of: "
", with: "\r\n")
for (shortcode, url) in emojiDict {
let emojiNode = "\(shortcode)"
let pattern = ":\(shortcode):"
diff --git a/Mastodon/Protocol/AvatarConfigurableView.swift b/Mastodon/Protocol/AvatarConfigurableView.swift
index 5807eed18..d771fa5a9 100644
--- a/Mastodon/Protocol/AvatarConfigurableView.swift
+++ b/Mastodon/Protocol/AvatarConfigurableView.swift
@@ -5,16 +5,16 @@
// Created by Cirno MainasuK on 2021-2-4.
//
+import Foundation
import UIKit
+import Combine
import AlamofireImage
import FLAnimatedImage
-import Nuke
protocol AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { get }
static var configurableAvatarImageCornerRadius: CGFloat { get }
- var configurableAvatarImageView: UIImageView? { get }
- var configurableAvatarButton: UIButton? { get }
+ var configurableAvatarImageView: FLAnimatedImageView? { get }
func configure(with configuration: AvatarConfigurableViewConfiguration)
func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration)
}
@@ -43,69 +43,31 @@ extension AvatarConfigurableView {
}
return placeholderImage
}()
-
- // reset layer attributes
- configurableAvatarImageView?.layer.masksToBounds = false
- configurableAvatarImageView?.layer.cornerRadius = 0
- configurableAvatarImageView?.layer.cornerCurve = .circular
-
- configurableAvatarButton?.layer.masksToBounds = false
- configurableAvatarButton?.layer.cornerRadius = 0
- configurableAvatarButton?.layer.cornerCurve = .circular
// accessibility
configurableAvatarImageView?.accessibilityIgnoresInvertColors = true
- configurableAvatarButton?.accessibilityIgnoresInvertColors = true
-
+
defer {
avatarConfigurableView(self, didFinishConfiguration: configuration)
}
- guard let imageDisplayingView: ImageDisplayingView = configurableAvatarImageView ?? configurableAvatarButton?.imageView else {
+ guard let configurableAvatarImageView = configurableAvatarImageView else {
return
}
// set corner radius (due to GIF won't crop)
- imageDisplayingView.layer.masksToBounds = true
- imageDisplayingView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
- imageDisplayingView.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular
+ configurableAvatarImageView.layer.masksToBounds = true
+ configurableAvatarImageView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
+ configurableAvatarImageView.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular
// set border
- configureLayerBorder(view: imageDisplayingView, configuration: configuration)
+ configureLayerBorder(view: configurableAvatarImageView, configuration: configuration)
-
- // set image
- let url = configuration.avatarImageURL
- let processors: [ImageProcessing] = [
- ImageProcessors.Resize(
- size: Self.configurableAvatarImageSize,
- unit: .points,
- contentMode: .aspectFill,
- crop: false
- ),
- ImageProcessors.RoundedCorners(
- radius: Self.configurableAvatarImageCornerRadius
- )
- ]
-
- let request = ImageRequest(url: url, processors: processors)
- let options = ImageLoadingOptions(
+ configurableAvatarImageView.setImage(
+ url: configuration.avatarImageURL,
placeholder: placeholderImage,
- transition: .fadeIn(duration: 0.2)
+ scaleToSize: Self.configurableAvatarImageSize
)
-
- Nuke.loadImage(
- with: request,
- options: options,
- into: imageDisplayingView
- ) { result in
- switch result {
- case .failure:
- break
- case .success:
- break
- }
- }
}
func configureLayerBorder(view: UIView, configuration: AvatarConfigurableViewConfiguration) {
diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift
index 77580296b..1abfcf70b 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift
@@ -168,9 +168,9 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
guard let self = self else { return }
self.attachment(of: status, index: i)
.setFailureType(to: Error.self)
- .compactMap { attachment -> AnyPublisher? in
+ .compactMap { attachment -> AnyPublisher? in
guard let attachment = attachment, let url = URL(string: attachment.url) else { return nil }
- return self.context.photoLibraryService.saveImage(url: url)
+ return self.context.photoLibraryService.save(imageSource: .url(url))
}
.switchToLatest()
.sink(receiveCompletion: { [weak self] completion in
@@ -197,9 +197,9 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
guard let self = self else { return }
self.attachment(of: status, index: i)
.setFailureType(to: Error.self)
- .compactMap { attachment -> AnyPublisher? in
+ .compactMap { attachment -> AnyPublisher? in
guard let attachment = attachment, let url = URL(string: attachment.url) else { return nil }
- return self.context.photoLibraryService.copyImage(url: url)
+ return self.context.photoLibraryService.copy(imageSource: .url(url))
}
.switchToLatest()
.sink(receiveCompletion: { completion in
diff --git a/Mastodon/Resources/Assets.xcassets/Colors/TabBar/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/TabBar/Contents.json
deleted file mode 100644
index 6e965652d..000000000
--- a/Mastodon/Resources/Assets.xcassets/Colors/TabBar/Contents.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- },
- "properties" : {
- "provides-namespace" : true
- }
-}
diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Border/notification.status.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/notification.status.border.color.colorset/Contents.json
similarity index 76%
rename from Mastodon/Resources/Assets.xcassets/Colors/Border/notification.status.colorset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Theme/Mastodon/notification.status.border.color.colorset/Contents.json
index 1067c15d9..c04af0902 100644
--- a/Mastodon/Resources/Assets.xcassets/Colors/Border/notification.status.colorset/Contents.json
+++ b/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/notification.status.border.color.colorset/Contents.json
@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
- "blue" : "232",
- "green" : "225",
- "red" : "217"
+ "blue" : "0.910",
+ "green" : "0.882",
+ "red" : "0.851"
}
},
"idiom" : "universal"
@@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
- "blue" : "110",
- "green" : "87",
- "red" : "79"
+ "blue" : "0.431",
+ "green" : "0.341",
+ "red" : "0.310"
}
},
"idiom" : "universal"
diff --git a/Mastodon/Resources/Assets.xcassets/Colors/TabBar/item.inactive.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/notification.status.border.color.colorset/Contents.json
similarity index 76%
rename from Mastodon/Resources/Assets.xcassets/Colors/TabBar/item.inactive.colorset/Contents.json
rename to Mastodon/Resources/Assets.xcassets/Theme/system/notification.status.border.color.colorset/Contents.json
index 48fc40b01..c04af0902 100644
--- a/Mastodon/Resources/Assets.xcassets/Colors/TabBar/item.inactive.colorset/Contents.json
+++ b/Mastodon/Resources/Assets.xcassets/Theme/system/notification.status.border.color.colorset/Contents.json
@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
- "blue" : "140",
- "green" : "130",
- "red" : "110"
+ "blue" : "0.910",
+ "green" : "0.882",
+ "red" : "0.851"
}
},
"idiom" : "universal"
@@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
- "blue" : "200",
- "green" : "174",
- "red" : "155"
+ "blue" : "0.431",
+ "green" : "0.341",
+ "red" : "0.310"
}
},
"idiom" : "universal"
diff --git a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift
index 65044f95f..8fa5d8644 100644
--- a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift
+++ b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift
@@ -6,6 +6,7 @@
//
import UIKit
+import FLAnimatedImage
final class AutoCompleteTableViewCell: UITableViewCell {
@@ -27,7 +28,7 @@ final class AutoCompleteTableViewCell: UITableViewCell {
return stackView
}()
- let avatarImageView = UIImageView()
+ let avatarImageView = FLAnimatedImageView()
let titleLabel: UILabel = {
let label = UILabel()
@@ -129,8 +130,7 @@ extension AutoCompleteTableViewCell {
extension AutoCompleteTableViewCell: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { avatarImageSize }
static var configurableAvatarImageCornerRadius: CGFloat { avatarImageCornerRadius }
- var configurableAvatarImageView: UIImageView? { avatarImageView }
- var configurableAvatarButton: UIButton? { nil }
+ var configurableAvatarImageView: FLAnimatedImageView? { avatarImageView }
}
#if canImport(SwiftUI) && DEBUG
diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift
index 7e305dbb0..d88b919f4 100644
--- a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift
+++ b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift
@@ -6,16 +6,14 @@
//
import UIKit
-import Nuke
+import FLAnimatedImage
final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
static let itemSize = CGSize(width: 44, height: 44)
- var imageTask: ImageTask?
-
- let emojiImageView: UIImageView = {
- let imageView = UIImageView()
+ let emojiImageView: FLAnimatedImageView = {
+ let imageView = FLAnimatedImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.masksToBounds = true
return imageView
@@ -29,8 +27,6 @@ final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
override func prepareForReuse() {
super.prepareForReuse()
- imageTask?.cancel()
- imageTask = nil
}
override init(frame: CGRect) {
diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift
index eb3b95c98..4ce020837 100644
--- a/Mastodon/Scene/Compose/ComposeViewController.swift
+++ b/Mastodon/Scene/Compose/ComposeViewController.swift
@@ -13,7 +13,6 @@ import MastodonSDK
import MetaTextView
import MastodonMeta
import Meta
-import Nuke
import MastodonUI
final class ComposeViewController: UIViewController, NeedsDependency {
diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift
index 760203a39..d421759b1 100644
--- a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift
+++ b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift
@@ -7,6 +7,7 @@
import UIKit
import Combine
+import MetaTextView
final class CustomEmojiPickerInputViewModel {
@@ -48,19 +49,26 @@ extension CustomEmojiPickerInputViewModel {
for reference in customEmojiReplaceableTextInputReferences {
guard let textInput = reference.value else { continue }
guard textInput.isFirstResponder == true else { continue }
+ guard let selectedTextRange = textInput.selectedTextRange else { continue }
- let selectedTextRange = textInput.selectedTextRange
textInput.insertText(text)
// due to insert text render as attachment
// the cursor reset logic not works
// hack with hard code +2 offset
assert(text.hasSuffix(": "))
- if text.hasPrefix(":") && text.hasSuffix(": "),
- let selectedTextRange = selectedTextRange,
- let newPosition = textInput.position(from: selectedTextRange.start, offset: 2) {
- let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
- textInput.selectedTextRange = newSelectedTextRange
+ guard text.hasPrefix(":") && text.hasSuffix(": ") else { continue }
+
+ if let _ = textInput as? MetaTextView {
+ if let newPosition = textInput.position(from: selectedTextRange.start, offset: 2) {
+ let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
+ textInput.selectedTextRange = newSelectedTextRange
+ }
+ } else {
+ if let newPosition = textInput.position(from: selectedTextRange.start, offset: text.length) {
+ let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
+ textInput.selectedTextRange = newSelectedTextRange
+ }
}
return reference
diff --git a/Mastodon/Scene/Compose/View/ReplicaStatusView.swift b/Mastodon/Scene/Compose/View/ReplicaStatusView.swift
index 9dde2f638..609e4bcc6 100644
--- a/Mastodon/Scene/Compose/View/ReplicaStatusView.swift
+++ b/Mastodon/Scene/Compose/View/ReplicaStatusView.swift
@@ -66,7 +66,7 @@ final class ReplicaStatusView: UIView {
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
return view
}()
- let avatarImageView: UIImageView = FLAnimatedImageView()
+ let avatarImageView = FLAnimatedImageView()
let nameLabel: ActiveLabel = {
let label = ActiveLabel(style: .statusName)
@@ -250,6 +250,5 @@ extension ReplicaStatusView {
extension ReplicaStatusView: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
static var configurableAvatarImageCornerRadius: CGFloat { return 4 }
- var configurableAvatarImageView: UIImageView? { avatarImageView }
- var configurableAvatarButton: UIButton? { nil }
+ var configurableAvatarImageView: FLAnimatedImageView? { avatarImageView }
}
diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift
index 0f2d06b9b..29ddc206a 100644
--- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift
+++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift
@@ -205,45 +205,51 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate {
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, contextMenuActionPerform action: MediaPreviewImageViewController.ContextMenuAction) {
switch action {
case .savePhoto:
- switch viewController.viewModel.item {
- case .status(let meta):
- context.photoLibraryService.saveImage(url: meta.url)
- .sink { [weak self] completion in
- guard let self = self else { return }
- switch completion {
- case .failure(let error):
- guard let error = error as? PhotoLibraryService.PhotoLibraryError,
- case .noPermission = error else { return }
- let alertController = SettingService.openSettingsAlertController(title: L10n.Common.Alerts.SavePhotoFailure.title, message: L10n.Common.Alerts.SavePhotoFailure.message)
- self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
- case .finished:
- break
- }
- } receiveValue: { _ in
- // do nothing
+ let savePublisher: AnyPublisher = {
+ switch viewController.viewModel.item {
+ case .status(let meta):
+ return context.photoLibraryService.save(imageSource: .url(meta.url))
+ case .local(let meta):
+ return context.photoLibraryService.save(imageSource: .image(meta.image))
+ }
+ }()
+ savePublisher
+ .sink { [weak self] completion in
+ guard let self = self else { return }
+ switch completion {
+ case .failure(let error):
+ guard let error = error as? PhotoLibraryService.PhotoLibraryError,
+ case .noPermission = error else { return }
+ let alertController = SettingService.openSettingsAlertController(title: L10n.Common.Alerts.SavePhotoFailure.title, message: L10n.Common.Alerts.SavePhotoFailure.message)
+ self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
+ case .finished:
+ break
}
- .store(in: &context.disposeBag)
- case .local(let meta):
- context.photoLibraryService.save(image: meta.image, withNotificationFeedback: true)
- }
+ } receiveValue: { _ in
+ // do nothing
+ }
+ .store(in: &context.disposeBag)
case .copyPhoto:
- switch viewController.viewModel.item {
- case .status(let meta):
- context.photoLibraryService.copyImage(url: meta.url)
- .sink { completion in
- switch completion {
- case .failure(let error):
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
- case .finished:
- break
- }
- } receiveValue: { _ in
- // do nothing
+ let copyPublisher: AnyPublisher = {
+ switch viewController.viewModel.item {
+ case .status(let meta):
+ return context.photoLibraryService.copy(imageSource: .url(meta.url))
+ case .local(let meta):
+ return context.photoLibraryService.copy(imageSource: .image(meta.image))
+ }
+ }()
+ copyPublisher
+ .sink { completion in
+ switch completion {
+ case .failure(let error):
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
+ case .finished:
+ break
}
- .store(in: &context.disposeBag)
- case .local(let meta):
- context.photoLibraryService.copy(image: meta.image, withNotificationFeedback: true)
- }
+ } receiveValue: { _ in
+ // do nothing
+ }
+ .store(in: &context.disposeBag)
case .share:
let applicationActivities: [UIActivity] = [
SafariActivity(sceneCoordinator: self.coordinator)
diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift
index 960746b0f..03004028e 100644
--- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift
+++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift
@@ -8,7 +8,6 @@
import os.log
import UIKit
import Combine
-import Nuke
protocol MediaPreviewImageViewControllerDelegate: AnyObject {
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer)
@@ -91,11 +90,14 @@ extension MediaPreviewImageViewController {
// }
viewModel.image
.receive(on: RunLoop.main) // use RunLoop prevent set image during zooming (TODO: handle transitioning state)
- .sink { [weak self] image in
+ .sink { [weak self] image, animatedImage in
guard let self = self else { return }
guard let image = image else { return }
self.previewImageView.imageView.image = image
self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true)
+ if let animatedImage = animatedImage {
+ self.previewImageView.imageView.animatedImage = animatedImage
+ }
self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText
}
.store(in: &disposeBag)
diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift
index a6163a8cf..f44a6a189 100644
--- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift
+++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift
@@ -8,7 +8,9 @@
import os.log
import UIKit
import Combine
-import Nuke
+import Alamofire
+import AlamofireImage
+import FLAnimatedImage
class MediaPreviewImageViewModel {
@@ -18,34 +20,35 @@ class MediaPreviewImageViewModel {
let item: ImagePreviewItem
// output
- let image: CurrentValueSubject
+ let image: CurrentValueSubject<(UIImage?, FLAnimatedImage?), Never>
let altText: String?
init(meta: RemoteImagePreviewMeta) {
self.item = .status(meta)
- self.image = CurrentValueSubject(meta.thumbnail)
+ self.image = CurrentValueSubject((meta.thumbnail, nil))
self.altText = meta.altText
let url = meta.url
-
- ImagePipeline.shared.imagePublisher(with: url)
- .sink { completion in
- switch completion {
+ AF.request(url).publishData()
+ .map { response in
+ switch response.result {
+ case .success(let data):
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription)
+ let image = UIImage(data: data, scale: UIScreen.main.scale)
+ let animatedImage = FLAnimatedImage(animatedGIFData: data)
+ return (image, animatedImage)
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)
- case .finished:
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription)
+ return (nil, nil)
}
- } receiveValue: { [weak self] response in
- guard let self = self else { return }
- self.image.value = response.image
}
+ .assign(to: \.value, on: image)
.store(in: &disposeBag)
}
init(meta: LocalImagePreviewMeta) {
self.item = .local(meta)
- self.image = CurrentValueSubject(meta.image)
+ self.image = CurrentValueSubject((meta.image, nil))
self.altText = nil
}
diff --git a/Mastodon/Scene/Notification/Button/NotificationAvatarButton.swift b/Mastodon/Scene/Notification/Button/NotificationAvatarButton.swift
new file mode 100644
index 000000000..6eafdd1dd
--- /dev/null
+++ b/Mastodon/Scene/Notification/Button/NotificationAvatarButton.swift
@@ -0,0 +1,85 @@
+//
+// NotificationAvatarButton.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-21.
+//
+
+import UIKit
+import FLAnimatedImage
+
+final class NotificationAvatarButton: AvatarButton {
+
+ // Size fixed
+ static let containerSize = CGSize(width: 35, height: 35)
+ static let badgeImageViewSize = CGSize(width: 24, height: 24)
+ static let badgeImageMaskSize = CGSize(width: badgeImageViewSize.width + 4, height: badgeImageViewSize.height + 4)
+
+ let badgeImageView: UIImageView = {
+ let imageView = RoundedImageView()
+ imageView.contentMode = .center
+ imageView.isOpaque = true
+ imageView.layer.shouldRasterize = true
+ imageView.layer.rasterizationScale = UIScreen.main.scale
+ return imageView
+ }()
+
+ override func _init() {
+ super._init()
+
+ avatarImageSize = CGSize(width: 35, height: 35)
+
+ let path: CGPath = {
+ let path = CGMutablePath()
+ path.addRect(CGRect(origin: .zero, size: NotificationAvatarButton.containerSize))
+ let x: CGFloat = {
+ if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft {
+ return -0.5 * NotificationAvatarButton.badgeImageMaskSize.width
+ } else {
+ return NotificationAvatarButton.containerSize.width - 0.5 * NotificationAvatarButton.badgeImageMaskSize.width
+ }
+ }()
+ path.addPath(UIBezierPath(
+ ovalIn: CGRect(
+ x: x,
+ y: NotificationAvatarButton.containerSize.height - 0.5 * NotificationAvatarButton.badgeImageMaskSize.width,
+ width: NotificationAvatarButton.badgeImageMaskSize.width,
+ height: NotificationAvatarButton.badgeImageMaskSize.height
+ )
+ ).cgPath)
+ return path
+ }()
+
+ let maskShapeLayer = CAShapeLayer()
+ maskShapeLayer.backgroundColor = UIColor.black.cgColor
+ maskShapeLayer.fillRule = .evenOdd
+ maskShapeLayer.path = path
+ avatarImageView.layer.mask = maskShapeLayer
+
+ badgeImageView.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(badgeImageView)
+ NSLayoutConstraint.activate([
+ badgeImageView.centerXAnchor.constraint(equalTo: trailingAnchor),
+ badgeImageView.centerYAnchor.constraint(equalTo: bottomAnchor),
+ badgeImageView.widthAnchor.constraint(equalToConstant: NotificationAvatarButton.badgeImageViewSize.width).priority(.required - 1),
+ badgeImageView.heightAnchor.constraint(equalToConstant: NotificationAvatarButton.badgeImageViewSize.height).priority(.required - 1),
+ ])
+ }
+
+ override func updateAppearance() {
+ super.updateAppearance()
+ badgeImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0
+ }
+
+}
+
+final class RoundedImageView: UIImageView {
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ layer.masksToBounds = true
+ layer.cornerRadius = bounds.width / 2
+ layer.cornerCurve = .circular
+ }
+}
diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift
index 6042c0bbd..684d9187c 100644
--- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift
+++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift
@@ -14,7 +14,6 @@ import ActiveLabel
import MetaTextView
import Meta
import FLAnimatedImage
-import Nuke
protocol NotificationTableViewCellDelegate: AnyObject {
var context: AppContext! { get }
@@ -46,31 +45,8 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
var containerStackViewBottomLayoutConstraint: NSLayoutConstraint!
let containerStackView = UIStackView()
- let avatarImageView: UIImageView = {
- let imageView = FLAnimatedImageView()
- return imageView
- }()
-
-
+ let avatarButton = NotificationAvatarButton()
let traitCollectionDidChange = PassthroughSubject()
-
- let actionImageView: UIImageView = {
- let imageView = UIImageView()
- imageView.contentMode = .center
- imageView.isOpaque = true
- imageView.layer.masksToBounds = true
- imageView.layer.cornerRadius = NotificationStatusTableViewCell.actionImageViewSize.width * 0.5
- imageView.layer.cornerCurve = .circular
- imageView.layer.borderWidth = NotificationStatusTableViewCell.actionImageBorderWidth
- imageView.layer.shouldRasterize = true
- imageView.layer.rasterizationScale = UIScreen.main.scale
- return imageView
- }()
-
- let avatarContainer: UIView = {
- let view = UIView()
- return view
- }()
let contentStackView = UIStackView()
@@ -114,7 +90,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
view.layer.cornerRadius = 6
view.layer.cornerCurve = .continuous
view.layer.borderWidth = 2
- view.layer.borderColor = Asset.Colors.Border.notificationStatus.color.cgColor
+ view.layer.borderColor = ThemeService.shared.currentTheme.value.notificationStatusBorderColor.cgColor
return view
}()
let statusView = StatusView()
@@ -181,25 +157,11 @@ extension NotificationStatusTableViewCell {
containerStackViewBottomLayoutConstraint.priority(.required - 1),
])
- containerStackView.addArrangedSubview(avatarContainer)
- avatarImageView.translatesAutoresizingMaskIntoConstraints = false
- avatarContainer.addSubview(avatarImageView)
+ avatarButton.translatesAutoresizingMaskIntoConstraints = false
+ containerStackView.addArrangedSubview(avatarButton)
NSLayoutConstraint.activate([
- avatarImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
- avatarImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor),
- avatarImageView.trailingAnchor.constraint(equalTo: avatarContainer.trailingAnchor),
- avatarImageView.bottomAnchor.constraint(equalTo: avatarContainer.bottomAnchor),
- avatarImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1),
- avatarImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1),
- ])
-
- actionImageView.translatesAutoresizingMaskIntoConstraints = false
- avatarContainer.addSubview(actionImageView)
- NSLayoutConstraint.activate([
- actionImageView.centerYAnchor.constraint(equalTo: avatarContainer.bottomAnchor),
- actionImageView.centerXAnchor.constraint(equalTo: avatarContainer.trailingAnchor),
- actionImageView.widthAnchor.constraint(equalToConstant: NotificationStatusTableViewCell.actionImageViewSize.width).priority(.required - 1),
- actionImageView.heightAnchor.constraint(equalTo: actionImageView.widthAnchor, multiplier: 1.0),
+ avatarButton.heightAnchor.constraint(equalToConstant: NotificationAvatarButton.containerSize.width).priority(.required - 1),
+ avatarButton.widthAnchor.constraint(equalToConstant: NotificationAvatarButton.containerSize.height).priority(.required - 1),
])
containerStackView.addArrangedSubview(contentStackView)
@@ -274,10 +236,8 @@ extension NotificationStatusTableViewCell {
filteredLabel.isHidden = true
statusView.delegate = self
-
- let avatarImageViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
- avatarImageViewTapGestureRecognizer.addTarget(self, action: #selector(NotificationStatusTableViewCell.avatarImageViewTapGestureRecognizerHandler(_:)))
- avatarImageView.addGestureRecognizer(avatarImageViewTapGestureRecognizer)
+
+ avatarButton.addTarget(self, action: #selector(NotificationStatusTableViewCell.avatarButtonDidPressed(_:)), for: .touchUpInside)
let authorNameLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
authorNameLabelTapGestureRecognizer.addTarget(self, action: #selector(NotificationStatusTableViewCell.authorNameLabelTapGestureRecognizerHandler(_:)))
nameLabel.addGestureRecognizer(authorNameLabelTapGestureRecognizer)
@@ -312,9 +272,7 @@ extension NotificationStatusTableViewCell {
extension NotificationStatusTableViewCell {
private func setupBackgroundColor(theme: Theme) {
- actionImageView.layer.borderColor = theme.systemBackgroundColor.cgColor
- avatarImageView.layer.borderColor = Asset.Theme.Mastodon.systemBackground.color.cgColor
- statusContainerView.layer.borderColor = Asset.Colors.Border.notificationStatus.color.cgColor
+ statusContainerView.layer.borderColor = theme.notificationStatusBorderColor.resolvedColor(with: traitCollection).cgColor
statusContainerView.backgroundColor = UIColor(dynamicProvider: { traitCollection in
return traitCollection.userInterfaceStyle == .light ? theme.systemBackgroundColor : theme.tertiarySystemGroupedBackgroundColor
})
@@ -323,9 +281,9 @@ extension NotificationStatusTableViewCell {
}
extension NotificationStatusTableViewCell {
- @objc private func avatarImageViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
+ @objc private func avatarButtonDidPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- delegate?.notificationStatusTableViewCell(self, avatarImageViewDidPressed: avatarImageView)
+ delegate?.notificationStatusTableViewCell(self, avatarImageViewDidPressed: avatarButton.avatarImageView)
}
@objc private func authorNameLabelTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
@@ -408,6 +366,5 @@ extension NotificationStatusTableViewCell {
extension NotificationStatusTableViewCell: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { CGSize(width: 35, height: 35) }
static var configurableAvatarImageCornerRadius: CGFloat { 4 }
- var configurableAvatarImageView: UIImageView? { avatarImageView }
- var configurableAvatarButton: UIButton? { nil }
+ var configurableAvatarImageView: FLAnimatedImageView? { avatarButton.avatarImageView }
}
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
index 86975a8de..95a91b994 100644
--- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
+++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
@@ -73,7 +73,7 @@ final class ProfileHeaderView: UIView {
return view
}()
- let avatarImageView: UIImageView = {
+ let avatarImageView: FLAnimatedImageView = {
let imageView = FLAnimatedImageView()
let placeholderImage = UIImage
.placeholder(size: ProfileHeaderView.avatarImageViewSize, color: Asset.Theme.Mastodon.systemGroupedBackground.color)
@@ -559,8 +559,7 @@ extension ProfileHeaderView: ProfileStatusDashboardViewDelegate {
extension ProfileHeaderView: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { avatarImageViewSize }
static var configurableAvatarImageCornerRadius: CGFloat { avatarImageViewCornerRadius }
- var configurableAvatarImageView: UIImageView? { return avatarImageView }
- var configurableAvatarButton: UIButton? { return nil }
+ var configurableAvatarImageView: FLAnimatedImageView? { return avatarImageView }
}
diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift
index acfd995ff..934c55b1b 100644
--- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift
+++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift
@@ -25,6 +25,15 @@ final class SearchHistoryViewModel {
self.context = context
self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext)
+ context.authenticationService.activeMastodonAuthenticationBox
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] box in
+ guard let self = self else { return }
+ self.searchHistoryFetchedResultController.domain.value = box?.domain
+ self.searchHistoryFetchedResultController.userID.value = box?.userID
+ }
+ .store(in: &disposeBag)
+
// may block main queue by large dataset
searchHistoryFetchedResultController.objectIDs
.removeDuplicates()
@@ -81,6 +90,9 @@ extension SearchHistoryViewModel {
extension SearchHistoryViewModel {
func persistSearchHistory(for item: SearchHistoryItem) {
+ guard let box = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
+ let property = SearchHistory.Property(domain: box.domain, userID: box.userID)
+
switch item {
case .account(let objectID):
let managedObjectContext = context.backgroundManagedObjectContext
@@ -89,7 +101,7 @@ extension SearchHistoryViewModel {
if let searchHistory = user.searchHistory {
searchHistory.update(updatedAt: Date())
} else {
- SearchHistory.insert(into: managedObjectContext, account: user)
+ SearchHistory.insert(into: managedObjectContext, property: property, account: user)
}
}
.sink { result in
@@ -104,7 +116,7 @@ extension SearchHistoryViewModel {
if let searchHistory = hashtag.searchHistory {
searchHistory.update(updatedAt: Date())
} else {
- SearchHistory.insert(into: managedObjectContext, hashtag: hashtag)
+ SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag)
}
}
.sink { result in
diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift
index 0ace96226..181302a24 100644
--- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift
+++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift
@@ -142,6 +142,7 @@ extension SearchResultViewModel {
extension SearchResultViewModel {
func persistSearchHistory(for item: SearchResultItem) {
guard let box = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
+ let property = SearchHistory.Property(domain: box.domain, userID: box.userID)
let domain = box.domain
switch item {
@@ -160,7 +161,7 @@ extension SearchResultViewModel {
if let searchHistory = user.searchHistory {
searchHistory.update(updatedAt: Date())
} else {
- SearchHistory.insert(into: managedObjectContext, account: user)
+ SearchHistory.insert(into: managedObjectContext, property: property, account: user)
}
}
.sink { result in
@@ -178,7 +179,7 @@ extension SearchResultViewModel {
if let searchHistory = hashtag.searchHistory {
searchHistory.update(updatedAt: Date())
} else {
- SearchHistory.insert(into: managedObjectContext, hashtag: hashtag)
+ SearchHistory.insert(into: managedObjectContext, property: property, hashtag: hashtag)
}
}
.sink { result in
diff --git a/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift
index 8223c6ddf..1b48d1d2d 100644
--- a/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift
+++ b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift
@@ -11,12 +11,11 @@ import Foundation
import MastodonSDK
import UIKit
import FLAnimatedImage
-import Nuke
final class SearchResultTableViewCell: UITableViewCell {
- let _imageView: UIImageView = {
- let imageView = FLAnimatedImageView()
+ let _imageView: AvatarImageView = {
+ let imageView = AvatarImageView()
imageView.tintColor = Asset.Colors.Label.primary.color
imageView.layer.cornerRadius = 4
imageView.clipsToBounds = true
@@ -48,7 +47,7 @@ final class SearchResultTableViewCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
- Nuke.cancelRequest(for: _imageView)
+ _imageView.af.cancelImageRequest()
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@@ -155,32 +154,19 @@ extension SearchResultTableViewCell {
extension SearchResultTableViewCell {
func config(with account: Mastodon.Entity.Account) {
- Nuke.loadImage(
- with: account.avatarImageURL(),
- options: ImageLoadingOptions(
- placeholder: UIImage.placeholder(color: .systemFill),
- transition: .fadeIn(duration: 0.2)
- ),
- into: _imageView
- )
+ configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL()))
_titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
_subTitleLabel.text = account.acct
}
func config(with account: MastodonUser) {
- Nuke.loadImage(
- with: account.avatarImageURL(),
- options: ImageLoadingOptions(
- placeholder: UIImage.placeholder(color: .systemFill),
- transition: .fadeIn(duration: 0.2)
- ),
- into: _imageView
- )
+ configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL()))
_titleLabel.text = account.displayNameWithFallback
_subTitleLabel.text = account.acct
}
func config(with tag: Mastodon.Entity.Tag) {
+ configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: nil))
let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate)
_imageView.image = image
_titleLabel.text = "#" + tag.name
@@ -195,6 +181,7 @@ extension SearchResultTableViewCell {
}
func config(with tag: Tag) {
+ configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: nil))
let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate)
_imageView.image = image
_titleLabel.text = "# " + tag.name
@@ -211,6 +198,13 @@ extension SearchResultTableViewCell {
}
}
+// MARK: - AvatarStackedImageView
+extension SearchResultTableViewCell: AvatarConfigurableView {
+ static var configurableAvatarImageSize: CGSize { CGSize(width: 42, height: 42) }
+ static var configurableAvatarImageCornerRadius: CGFloat { 4 }
+ var configurableAvatarImageView: FLAnimatedImageView? { _imageView }
+}
+
#if canImport(SwiftUI) && DEBUG
import SwiftUI
diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift
index a295272ee..744900bed 100644
--- a/Mastodon/Scene/Settings/SettingsViewController.swift
+++ b/Mastodon/Scene/Settings/SettingsViewController.swift
@@ -358,13 +358,10 @@ extension SettingsViewController: UITableViewDelegate {
case .appearance:
// do nothing
break
- case .preferenceDarkMode, .preferenceDisableAvatarAnimation:
- // do nothing
- break
case .notification:
// do nothing
break
- case .preferenceUsingDefaultBrowser:
+ case .preference:
// do nothing
break
case .boringZone(let link), .spicyZone(let link):
@@ -476,48 +473,30 @@ extension SettingsViewController: SettingsToggleCellDelegate {
// do nothing
}
.store(in: &disposeBag)
- case .preferenceDarkMode(let settingObjectID):
+ case .preference(let settingObjectID, let preferenceType):
let managedObjectContext = context.backgroundManagedObjectContext
managedObjectContext.performChanges {
let setting = managedObjectContext.object(with: settingObjectID) as! Setting
- setting.update(preferredTrueBlackDarkMode: isOn)
- }
- .sink { result in
- switch result {
- case .success:
- ThemeService.shared.set(themeName: isOn ? .system : .mastodon)
- case .failure(let error):
- assertionFailure(error.localizedDescription)
- break
+ switch preferenceType {
+ case .darkMode:
+ setting.update(preferredTrueBlackDarkMode: isOn)
+ case .disableAvatarAnimation:
+ setting.update(preferredStaticAvatar: isOn)
+ case .useDefaultBrowser:
+ setting.update(preferredUsingDefaultBrowser: isOn)
}
}
- .store(in: &disposeBag)
- case .preferenceDisableAvatarAnimation(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 .preferenceUsingDefaultBrowser(let settingObjectID):
- let managedObjectContext = context.backgroundManagedObjectContext
- managedObjectContext.performChanges {
- let setting = managedObjectContext.object(with: settingObjectID) as! Setting
- setting.update(preferredUsingDefaultBrowser: isOn)
- }
- .sink { result in
- switch result {
- case .success:
- UserDefaults.shared.preferredUsingDefaultBrowser = isOn
+ switch preferenceType {
+ case .darkMode:
+ ThemeService.shared.set(themeName: isOn ? .system : .mastodon)
+ case .disableAvatarAnimation:
+ UserDefaults.shared.preferredStaticAvatar = isOn
+ case .useDefaultBrowser:
+ UserDefaults.shared.preferredUsingDefaultBrowser = isOn
+ }
case .failure(let error):
assertionFailure(error.localizedDescription)
break
diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift
index 2d7fafde3..b586195e6 100644
--- a/Mastodon/Scene/Settings/SettingsViewModel.swift
+++ b/Mastodon/Scene/Settings/SettingsViewModel.swift
@@ -122,9 +122,9 @@ extension SettingsViewModel {
// preference
snapshot.appendSections([.preference])
let preferenceItems: [SettingsItem] = [
- .preferenceDarkMode(settingObjectID: setting.objectID),
- .preferenceDisableAvatarAnimation(settingObjectID: setting.objectID),
- .preferenceUsingDefaultBrowser(settingObjectID: setting.objectID),
+ .preference(settingObjectID: setting.objectID, preferenceType: .darkMode),
+ .preference(settingObjectID: setting.objectID, preferenceType: .disableAvatarAnimation),
+ .preference(settingObjectID: setting.objectID, preferenceType: .useDefaultBrowser),
]
snapshot.appendItems(preferenceItems,toSection: .preference)
@@ -163,123 +163,12 @@ extension SettingsViewModel {
settingsAppearanceTableViewCellDelegate: SettingsAppearanceTableViewCellDelegate,
settingsToggleCellDelegate: SettingsToggleCellDelegate
) {
- dataSource = UITableViewDiffableDataSource(tableView: tableView) { [
- weak self,
- weak settingsAppearanceTableViewCellDelegate,
- 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
- self.context.managedObjectContext.performAndWait {
- let setting = self.context.managedObjectContext.object(with: objectID) as! Setting
- cell.update(with: setting.appearance)
- ManagedObjectObserver.observe(object: setting)
- .receive(on: DispatchQueue.main)
- .sink(receiveCompletion: { _ in
- // do nothing
- }, receiveValue: { [weak cell] change in
- guard let cell = cell else { return }
- guard case .update(let object) = change.changeType,
- let setting = object as? Setting else { return }
- cell.update(with: setting.appearance)
- })
- .store(in: &cell.disposeBag)
- }
- cell.delegate = settingsAppearanceTableViewCellDelegate
- return cell
- case .preferenceDarkMode(let objectID),
- .preferenceDisableAvatarAnimation(let objectID),
- .preferenceUsingDefaultBrowser(let objectID):
- let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
- cell.delegate = settingsToggleCellDelegate
- self.context.managedObjectContext.performAndWait {
- let setting = self.context.managedObjectContext.object(with: objectID) as! Setting
- SettingsViewModel.configureSettingToggle(cell: cell, item: item, setting: setting)
-
- ManagedObjectObserver.observe(object: setting)
- .receive(on: DispatchQueue.main)
- .sink(receiveCompletion: { _ in
- // do nothing
- }, receiveValue: { [weak cell] change in
- guard let cell = cell else { return }
- guard case .update(let object) = change.changeType,
- let setting = object as? Setting else { return }
- SettingsViewModel.configureSettingToggle(cell: cell, item: item, setting: setting)
- })
- .store(in: &cell.disposeBag)
- }
- return cell
- case .notification(let objectID, let switchMode):
- let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
- self.context.managedObjectContext.performAndWait {
- let setting = self.context.managedObjectContext.object(with: objectID) as! Setting
- if let subscription = setting.activeSubscription {
- SettingsViewModel.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
- }
- ManagedObjectObserver.observe(object: setting)
- .sink(receiveCompletion: { _ in
- // do nothing
- }, receiveValue: { [weak cell] change in
- guard let cell = cell else { return }
- guard case .update(let object) = change.changeType,
- let setting = object as? Setting else { return }
- guard let subscription = setting.activeSubscription else { return }
- SettingsViewModel.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
- })
- .store(in: &cell.disposeBag)
- }
- cell.delegate = settingsToggleCellDelegate
- return cell
- case .boringZone(let item), .spicyZone(let item):
- let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
- cell.update(with: item)
- return cell
- }
- }
-
+ dataSource = SettingsSection.tableViewDiffableDataSource(
+ for: tableView,
+ managedObjectContext: context.managedObjectContext,
+ settingsAppearanceTableViewCellDelegate: settingsAppearanceTableViewCellDelegate,
+ settingsToggleCellDelegate: settingsToggleCellDelegate
+ )
processDataSource(self.setting.value)
}
}
-
-extension SettingsViewModel {
-
- static func configureSettingToggle(
- cell: SettingsToggleTableViewCell,
- item: SettingsItem,
- setting: Setting
- ) {
- switch item {
- case .preferenceDarkMode:
- cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.trueBlackDarkMode
- cell.switchButton.isOn = setting.preferredTrueBlackDarkMode
- case .preferenceDisableAvatarAnimation:
- cell.textLabel?.text = L10n.Scene.Settings.Section.AppearanceSettings.disableAvatarAnimation
- cell.switchButton.isOn = setting.preferredStaticAvatar
- case .preferenceUsingDefaultBrowser:
- cell.textLabel?.text = L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
- cell.switchButton.isOn = setting.preferredUsingDefaultBrowser
- default:
- assertionFailure()
- }
- }
-
- static func configureSettingToggle(
- cell: SettingsToggleTableViewCell,
- switchMode: SettingsItem.NotificationSwitchMode,
- subscription: NotificationSubscription
- ) {
- cell.textLabel?.text = switchMode.title
-
- let enabled: Bool?
- switch switchMode {
- case .favorite: enabled = subscription.alert.favourite
- case .follow: enabled = subscription.alert.follow
- case .reblog: enabled = subscription.alert.reblog
- case .mention: enabled = subscription.alert.mention
- }
- cell.update(enabled: enabled)
- }
-
-}
diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift b/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift
index 86698d840..2bc70e65e 100644
--- a/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift
+++ b/Mastodon/Scene/Settings/View/Cell/SettingsToggleTableViewCell.swift
@@ -22,6 +22,12 @@ class SettingsToggleTableViewCell: UITableViewCell {
}()
weak var delegate: SettingsToggleCellDelegate?
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+
+ disposeBag.removeAll()
+ }
// MARK: - Methods
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
diff --git a/Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift b/Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift
deleted file mode 100644
index 254403bde..000000000
--- a/Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// AvatarBarButtonItem.swift
-// Mastodon
-//
-// Created by Cirno MainasuK on 2021-2-4.
-//
-
-import UIKit
-
-final class AvatarBarButtonItem: UIBarButtonItem {
-
- static let avatarButtonSize = CGSize(width: 32, height: 32)
-
- let avatarButton: UIButton = {
- let button = UIButton(type: .custom)
- button.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- button.widthAnchor.constraint(equalToConstant: avatarButtonSize.width).priority(.defaultHigh),
- button.heightAnchor.constraint(equalToConstant: avatarButtonSize.height).priority(.defaultHigh),
- ])
- return button
- }()
-
- override init() {
- super.init()
- _init()
- }
-
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- _init()
- }
-
-}
-
-extension AvatarBarButtonItem {
-
- private func _init() {
- customView = avatarButton
- }
-
-}
-
-extension AvatarBarButtonItem: AvatarConfigurableView {
- static var configurableAvatarImageSize: CGSize { return avatarButtonSize }
- static var configurableAvatarImageCornerRadius: CGFloat { return 4 }
- var configurableAvatarImageView: UIImageView? { return nil }
- var configurableAvatarButton: UIButton? { return avatarButton }
- var configurableVerifiedBadgeImageView: UIImageView? { return nil }
-}
diff --git a/Mastodon/Scene/Share/View/Button/AvatarButton.swift b/Mastodon/Scene/Share/View/Button/AvatarButton.swift
new file mode 100644
index 000000000..a8f7212ae
--- /dev/null
+++ b/Mastodon/Scene/Share/View/Button/AvatarButton.swift
@@ -0,0 +1,129 @@
+//
+// AvatarButton.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-21.
+//
+
+import os.log
+import UIKit
+
+class AvatarButton: UIControl {
+
+ // UIControl.Event - Application: 0x0F000000
+ static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000
+ var primaryActionState: UIControl.State = .normal
+
+ var avatarImageSize = CGSize(width: 42, height: 42)
+ let avatarImageView = AvatarImageView()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ _init()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ _init()
+ }
+
+ func _init() {
+ avatarImageView.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(avatarImageView)
+ NSLayoutConstraint.activate([
+ avatarImageView.topAnchor.constraint(equalTo: topAnchor),
+ avatarImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
+ avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
+ avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+ }
+
+ func updateAppearance() {
+ avatarImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0
+ }
+
+}
+
+extension AvatarButton {
+
+ override var intrinsicContentSize: CGSize {
+ return avatarImageSize
+ }
+
+ override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
+ defer { updateAppearance() }
+
+ updateState(touch: touch, event: event)
+ return super.beginTracking(touch, with: event)
+ }
+
+ override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
+ defer { updateAppearance() }
+
+ updateState(touch: touch, event: event)
+ return super.continueTracking(touch, with: event)
+ }
+
+ override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
+ defer { updateAppearance() }
+ resetState()
+
+ if let touch = touch {
+ if AvatarButton.isTouching(touch, view: self, event: event) {
+ sendActions(for: AvatarButton.primaryAction)
+ } else {
+ // do nothing
+ }
+ }
+
+ super.endTracking(touch, with: event)
+ }
+
+ override func cancelTracking(with event: UIEvent?) {
+ defer { updateAppearance() }
+
+ resetState()
+ super.cancelTracking(with: event)
+ }
+
+}
+
+extension AvatarButton {
+
+ private static func isTouching(_ touch: UITouch, view: UIView, event: UIEvent?) -> Bool {
+ let location = touch.location(in: view)
+ return view.point(inside: location, with: event)
+ }
+
+ private func resetState() {
+ primaryActionState = .normal
+ }
+
+ private func updateState(touch: UITouch, event: UIEvent?) {
+ primaryActionState = AvatarButton.isTouching(touch, view: self, event: event) ? .highlighted : .normal
+ }
+
+}
+
+#if canImport(SwiftUI) && DEBUG
+import SwiftUI
+
+struct AvatarButton_Previews: PreviewProvider {
+
+ static var previews: some View {
+ UIViewPreview(width: 42) {
+ let avatarButton = AvatarButton()
+ avatarButton.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ avatarButton.widthAnchor.constraint(equalToConstant: 42),
+ avatarButton.heightAnchor.constraint(equalToConstant: 42),
+ ])
+ return avatarButton
+ }
+ .previewLayout(.fixed(width: 42, height: 42))
+ }
+
+}
+
+#endif
+
diff --git a/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift b/Mastodon/Scene/Share/View/Button/AvatarStackContainerButton.swift
similarity index 84%
rename from Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift
rename to Mastodon/Scene/Share/View/Button/AvatarStackContainerButton.swift
index 83b66454b..6c2d00e3c 100644
--- a/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift
+++ b/Mastodon/Scene/Share/View/Button/AvatarStackContainerButton.swift
@@ -9,19 +9,20 @@ import os.log
import UIKit
import FLAnimatedImage
-final class AvatarStackedImageView: FLAnimatedImageView { }
+final class AvatarStackedImageView: AvatarImageView { }
// MARK: - AvatarConfigurableView
extension AvatarStackedImageView: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { CGSize(width: 28, height: 28) }
static var configurableAvatarImageCornerRadius: CGFloat { 4 }
- var configurableAvatarImageView: UIImageView? { self }
- var configurableAvatarButton: UIButton? { nil }
+ var configurableAvatarImageView: FLAnimatedImageView? { self }
}
final class AvatarStackContainerButton: UIControl {
static let containerSize = CGSize(width: 42, height: 42)
+ static let avatarImageViewSize = CGSize(width: 28, height: 28)
+ static let avatarImageViewCornerRadius: CGFloat = 4
static let maskOffset: CGFloat = 2
// UIControl.Event - Application: 0x0F000000
@@ -46,13 +47,6 @@ final class AvatarStackContainerButton: UIControl {
extension AvatarStackContainerButton {
private func _init() {
- // GIF get worse when enable rasterize
-// topLeadingAvatarStackedImageView.layer.shouldRasterize = true
-// topLeadingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale
-//
-// bottomTrailingAvatarStackedImageView.layer.shouldRasterize = true
-// bottomTrailingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale
-
topLeadingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(topLeadingAvatarStackedImageView)
NSLayoutConstraint.activate([
@@ -75,16 +69,16 @@ extension AvatarStackContainerButton {
let offset: CGFloat = 2
let path: CGPath = {
let path = CGMutablePath()
- path.addRect(CGRect(origin: .zero, size: AvatarStackedImageView.configurableAvatarImageSize))
+ path.addRect(CGRect(origin: .zero, size: AvatarStackContainerButton.avatarImageViewSize))
let mirrorScale: CGFloat = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? -1 : 1
path.addPath(UIBezierPath(
roundedRect: CGRect(
- x: mirrorScale * (AvatarStackContainerButton.containerSize.width - AvatarStackedImageView.configurableAvatarImageSize.width - offset),
- y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset,
- width: AvatarStackedImageView.configurableAvatarImageSize.width,
- height: AvatarStackedImageView.configurableAvatarImageSize.height
+ x: mirrorScale * (AvatarStackContainerButton.containerSize.width - AvatarStackContainerButton.avatarImageViewSize.width - offset),
+ y: AvatarStackContainerButton.containerSize.height - AvatarStackContainerButton.avatarImageViewSize.height - offset,
+ width: AvatarStackContainerButton.avatarImageViewSize.width,
+ height: AvatarStackContainerButton.avatarImageViewSize.height
),
- cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius
+ cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius + 1 // 1pt overshoot
).cgPath)
return path
}()
@@ -93,9 +87,6 @@ extension AvatarStackContainerButton {
maskShapeLayer.fillRule = .evenOdd
maskShapeLayer.path = path
topLeadingAvatarStackedImageView.layer.mask = maskShapeLayer
-
- topLeadingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill)
- bottomTrailingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill)
}
override var intrinsicContentSize: CGSize {
diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift
index e596b39e4..a12a7bd28 100644
--- a/Mastodon/Scene/Share/View/Content/StatusView.swift
+++ b/Mastodon/Scene/Share/View/Content/StatusView.swift
@@ -95,10 +95,7 @@ final class StatusView: UIView {
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
return view
}()
- let avatarImageView: FLAnimatedImageView = {
- let imageView = FLAnimatedImageView()
- return imageView
- }()
+ let avatarButton = AvatarButton()
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
let nameLabel: ActiveLabel = {
@@ -317,13 +314,13 @@ extension StatusView {
avatarView.widthAnchor.constraint(equalToConstant: StatusView.avatarImageSize.width).priority(.required - 1),
avatarView.heightAnchor.constraint(equalToConstant: StatusView.avatarImageSize.height).priority(.required - 1),
])
- avatarImageView.translatesAutoresizingMaskIntoConstraints = false
- avatarView.addSubview(avatarImageView)
+ avatarButton.translatesAutoresizingMaskIntoConstraints = false
+ avatarView.addSubview(avatarButton)
NSLayoutConstraint.activate([
- avatarImageView.topAnchor.constraint(equalTo: avatarView.topAnchor),
- avatarImageView.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
- avatarImageView.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
- avatarImageView.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
+ avatarButton.topAnchor.constraint(equalTo: avatarView.topAnchor),
+ avatarButton.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
+ avatarButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
+ avatarButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
])
avatarStackedContainerButton.translatesAutoresizingMaskIntoConstraints = false
avatarView.addSubview(avatarStackedContainerButton)
@@ -473,11 +470,7 @@ extension StatusView {
headerInfoLabel.isUserInteractionEnabled = true
headerInfoLabel.addGestureRecognizer(headerInfoLabelTapGestureRecognizer)
- let avatarImageViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
- avatarImageViewTapGestureRecognizer.addTarget(self, action: #selector(StatusView.avatarImageViewDidPressed(_:)))
- avatarImageView.addGestureRecognizer(avatarImageViewTapGestureRecognizer)
- avatarImageView.isUserInteractionEnabled = true
-
+ avatarButton.addTarget(self, action: #selector(StatusView.avatarButtonDidPressed(_:)), for: .touchUpInside)
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)
@@ -544,9 +537,9 @@ extension StatusView {
delegate?.statusView(self, headerInfoLabelDidPressed: headerInfoLabel)
}
- @objc private func avatarImageViewDidPressed(_ sender: UITapGestureRecognizer) {
+ @objc private func avatarButtonDidPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- delegate?.statusView(self, avatarImageViewDidPressed: avatarImageView)
+ delegate?.statusView(self, avatarImageViewDidPressed: avatarButton.avatarImageView)
}
@objc private func avatarStackedContainerButtonDidPressed(_ sender: UIButton) {
@@ -633,8 +626,7 @@ extension StatusView: PlayerContainerViewDelegate {
extension StatusView: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
static var configurableAvatarImageCornerRadius: CGFloat { return 4 }
- var configurableAvatarImageView: UIImageView? { avatarImageView }
- var configurableAvatarButton: UIButton? { nil }
+ var configurableAvatarImageView: FLAnimatedImageView? { avatarButton.avatarImageView }
}
#if canImport(SwiftUI) && DEBUG
@@ -662,7 +654,7 @@ struct StatusView_Previews: PreviewProvider {
UIViewPreview(width: 375) {
let statusView = StatusView()
statusView.headerContainerView.isHidden = false
- statusView.avatarImageView.isHidden = true
+ statusView.avatarButton.isHidden = true
statusView.avatarStackedContainerButton.isHidden = false
statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(
with: AvatarConfigurableViewConfiguration(
diff --git a/Mastodon/Scene/Share/View/ImageView/AvatarImageView.swift b/Mastodon/Scene/Share/View/ImageView/AvatarImageView.swift
new file mode 100644
index 000000000..0b3f2a8f4
--- /dev/null
+++ b/Mastodon/Scene/Share/View/ImageView/AvatarImageView.swift
@@ -0,0 +1,11 @@
+//
+// AvatarImageView.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-7-21.
+//
+
+import UIKit
+import FLAnimatedImage
+
+class AvatarImageView: FLAnimatedImageView { }
diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift
index 2daeba041..9fbbbd888 100644
--- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift
+++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift
@@ -272,6 +272,10 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
transitionMaskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
transitionContext.containerView.addSubview(transitionMaskView)
transitionItem.interactiveTransitionMaskView = transitionMaskView
+
+ let transitionMaskViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
+ transitionMaskViewTapGestureRecognizer.addTarget(self, action: #selector(MediaHostToMediaPreviewViewControllerAnimatedTransitioning.transitionMaskViewTapGestureRecognizerHandler(_:)))
+ transitionMaskView.addGestureRecognizer(transitionMaskViewTapGestureRecognizer)
let maskLayer = CAShapeLayer()
maskLayer.frame = transitionMaskView.bounds
@@ -339,11 +343,33 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
}
extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
+
+ // app may freeze without response during transitioning
+ // patch it by tap the view to finish transitioning
+ @objc func transitionMaskViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
+ // not panning now but still in transitioning
+ guard panGestureRecognizer.state == .possible,
+ transitionContext.isAnimated, transitionContext.isInteractive else {
+ return
+ }
+
+ // finish or cancel current transitioning
+ let targetPosition = completionPosition()
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: target position: %s", ((#file as NSString).lastPathComponent), #line, #function, targetPosition == .end ? "end" : "start")
+ isTransitionContextFinish = true
+ animate(targetPosition)
+
+ targetPosition == .end ? transitionContext.finishInteractiveTransition() : transitionContext.cancelInteractiveTransition()
+ }
@objc func updatePanGestureInteractive(_ sender: UIPanGestureRecognizer) {
- guard !isTransitionContextFinish else { return } // do not accept transition abort
+ guard !isTransitionContextFinish else {
+ return
+ } // do not accept transition abort
switch sender.state {
+ case .possible:
+ return
case .began, .changed:
let translation = sender.translation(in: transitionContext.containerView)
let percent = popInteractiveTransitionAnimator.fractionComplete + progressStep(for: translation)
@@ -360,7 +386,10 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
animate(targetPosition)
targetPosition == .end ? transitionContext.finishInteractiveTransition() : transitionContext.cancelInteractiveTransition()
- default:
+ case .failed:
+ return
+ @unknown default:
+ assertionFailure()
return
}
}
diff --git a/Mastodon/Service/APIService/APIService.swift b/Mastodon/Service/APIService/APIService.swift
index 9d1468ce0..d9ae017d9 100644
--- a/Mastodon/Service/APIService/APIService.swift
+++ b/Mastodon/Service/APIService/APIService.swift
@@ -13,7 +13,6 @@ import CoreDataStack
import MastodonSDK
import AlamofireImage
import AlamofireNetworkActivityIndicator
-import Nuke
final class APIService {
@@ -34,10 +33,6 @@ 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/PhotoLibraryService.swift b/Mastodon/Service/PhotoLibraryService.swift
index 53e7529c0..99dcb61ff 100644
--- a/Mastodon/Service/PhotoLibraryService.swift
+++ b/Mastodon/Service/PhotoLibraryService.swift
@@ -9,7 +9,9 @@ import os.log
import UIKit
import Combine
import Photos
+import Alamofire
import AlamofireImage
+import FLAnimatedImage
final class PhotoLibraryService: NSObject {
@@ -19,88 +21,150 @@ extension PhotoLibraryService {
enum PhotoLibraryError: Error {
case noPermission
+ case badPayload
+ }
+
+ enum ImageSource {
+ case url(URL)
+ case image(UIImage)
}
}
extension PhotoLibraryService {
-
- func saveImage(url: URL) -> AnyPublisher {
+
+ func save(imageSource source: ImageSource) -> AnyPublisher {
+ let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
+ let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
+
+
+ let imageDataPublisher: AnyPublisher = {
+ switch source {
+ case .url(let url):
+ return PhotoLibraryService.fetchImageData(url: url)
+ case .image(let image):
+ return PhotoLibraryService.fetchImageData(image: image)
+ }
+ }()
+
+ return imageDataPublisher
+ .flatMap { data in
+ PhotoLibraryService.save(imageData: data)
+ }
+ .handleEvents(receiveSubscription: { _ in
+ impactFeedbackGenerator.impactOccurred()
+ }, receiveCompletion: { completion in
+ switch completion {
+ case .failure:
+ notificationFeedbackGenerator.notificationOccurred(.error)
+ case .finished:
+ notificationFeedbackGenerator.notificationOccurred(.success)
+ }
+ })
+ .eraseToAnyPublisher()
+ }
+
+}
+
+extension PhotoLibraryService {
+
+ func copy(imageSource source: ImageSource) -> AnyPublisher {
+
+ let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
+ let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
+
+ let imageDataPublisher: AnyPublisher = {
+ switch source {
+ case .url(let url):
+ return PhotoLibraryService.fetchImageData(url: url)
+ case .image(let image):
+ return PhotoLibraryService.fetchImageData(image: image)
+ }
+ }()
+
+ return imageDataPublisher
+ .flatMap { data in
+ PhotoLibraryService.copy(imageData: data)
+ }
+ .handleEvents(receiveSubscription: { _ in
+ impactFeedbackGenerator.impactOccurred()
+ }, receiveCompletion: { completion in
+ switch completion {
+ case .failure:
+ notificationFeedbackGenerator.notificationOccurred(.error)
+ case .finished:
+ notificationFeedbackGenerator.notificationOccurred(.success)
+ }
+ })
+ .eraseToAnyPublisher()
+ }
+}
+
+extension PhotoLibraryService {
+
+ static func fetchImageData(url: URL) -> AnyPublisher {
+ AF.request(url).publishData()
+ .tryMap { response in
+ switch response.result {
+ case .success(let data):
+ return data
+ case .failure(let error):
+ throw error
+ }
+ }
+ .eraseToAnyPublisher()
+ }
+
+ static func fetchImageData(image: UIImage) -> AnyPublisher {
+ return Future { promise in
+ DispatchQueue.global().async {
+ let imageData = image.pngData()
+ DispatchQueue.main.async {
+ if let imageData = imageData {
+ promise(.success(imageData))
+ } else {
+ promise(.failure(PhotoLibraryError.badPayload))
+ }
+ }
+ }
+ }
+ .eraseToAnyPublisher()
+ }
+
+ static func save(imageData: Data) -> AnyPublisher {
guard PHPhotoLibrary.authorizationStatus(for: .addOnly) != .denied else {
return Fail(error: PhotoLibraryError.noPermission).eraseToAnyPublisher()
}
- return processImage(url: url)
- .handleEvents(receiveOutput: { image in
- self.save(image: image)
- })
- .eraseToAnyPublisher()
- }
-
- func copyImage(url: URL) -> AnyPublisher {
- return processImage(url: url)
- .handleEvents(receiveOutput: { image in
- UIPasteboard.general.image = image
- })
- .eraseToAnyPublisher()
- }
-
- func processImage(url: URL) -> AnyPublisher {
- let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
- let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
-
- 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)
+ return Future { promise in
+ PHPhotoLibrary.shared().performChanges {
+ PHAssetCreationRequest.forAsset().addResource(with: .photo, data: imageData, options: nil)
+ } completionHandler: { isSuccess, error in
+ if let error = error {
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)
- promise(.success(image))
+ } else {
+ promise(.success(Void()))
}
- })
- }
- .handleEvents(receiveSubscription: { _ in
- impactFeedbackGenerator.impactOccurred()
- }, receiveCompletion: { completion in
- switch completion {
- case .failure:
- notificationFeedbackGenerator.notificationOccurred(.error)
- case .finished:
- notificationFeedbackGenerator.notificationOccurred(.success)
}
- })
+ }
.eraseToAnyPublisher()
}
-
- func save(image: UIImage, withNotificationFeedback: Bool = false) {
- UIImageWriteToSavedPhotosAlbum(
- image,
- self,
- #selector(PhotoLibraryService.image(_:didFinishSavingWithError:contextInfo:)),
- nil
- )
-
- // assert no error
- if withNotificationFeedback {
- let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
- notificationFeedbackGenerator.notificationOccurred(.success)
+
+ static func copy(imageData: Data) -> AnyPublisher {
+ Future { promise in
+ DispatchQueue.global().async {
+ let image = UIImage(data: imageData, scale: UIScreen.main.scale)
+ DispatchQueue.main.async {
+ if let image = image {
+ UIPasteboard.general.image = image
+ promise(.success(Void()))
+ } else {
+ promise(.failure(PhotoLibraryError.badPayload))
+ }
+ }
+ }
}
+ .eraseToAnyPublisher()
}
- func copy(image: UIImage, withNotificationFeedback: Bool = false) {
- UIPasteboard.general.image = image
-
- // assert no error
- if withNotificationFeedback {
- let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
- notificationFeedbackGenerator.notificationOccurred(.success)
- }
- }
-
- @objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
- // TODO: notify banner
- }
-
}
diff --git a/Mastodon/Service/ThemeService/MastodonTheme.swift b/Mastodon/Service/ThemeService/MastodonTheme.swift
index 2418877ff..60093f04d 100644
--- a/Mastodon/Service/ThemeService/MastodonTheme.swift
+++ b/Mastodon/Service/ThemeService/MastodonTheme.swift
@@ -34,4 +34,5 @@ struct MastodonTheme: Theme {
let contentWarningOverlayBackgroundColor = Asset.Theme.Mastodon.contentWarningOverlayBackground.color
let profileFieldCollectionViewBackgroundColor = Asset.Theme.Mastodon.profileFieldCollectionViewBackground.color
let composeToolbarBackgroundColor = Asset.Theme.Mastodon.composeToolbarBackground.color
+ let notificationStatusBorderColor = Asset.Theme.System.notificationStatusBorderColor.color
}
diff --git a/Mastodon/Service/ThemeService/SystemTheme.swift b/Mastodon/Service/ThemeService/SystemTheme.swift
index b91ba1caf..0be42c6ed 100644
--- a/Mastodon/Service/ThemeService/SystemTheme.swift
+++ b/Mastodon/Service/ThemeService/SystemTheme.swift
@@ -34,4 +34,5 @@ struct SystemTheme: Theme {
let contentWarningOverlayBackgroundColor = Asset.Theme.System.contentWarningOverlayBackground.color
let profileFieldCollectionViewBackgroundColor = Asset.Theme.System.profileFieldCollectionViewBackground.color
let composeToolbarBackgroundColor = Asset.Theme.System.composeToolbarBackground.color
+ let notificationStatusBorderColor = Asset.Theme.System.notificationStatusBorderColor.color
}
diff --git a/Mastodon/Service/ThemeService/Theme.swift b/Mastodon/Service/ThemeService/Theme.swift
index 29f90db8d..15def6f53 100644
--- a/Mastodon/Service/ThemeService/Theme.swift
+++ b/Mastodon/Service/ThemeService/Theme.swift
@@ -35,6 +35,7 @@ public protocol Theme {
var contentWarningOverlayBackgroundColor: UIColor { get }
var profileFieldCollectionViewBackgroundColor: UIColor { get }
var composeToolbarBackgroundColor: UIColor { get }
+ var notificationStatusBorderColor: UIColor { get }
}