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