From 773bfb6dd21e2a9fa9abb788923f410006a27e26 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 12 Apr 2021 16:31:53 +0800 Subject: [PATCH 01/23] feature: notification API and CoreData --- .../CoreData.xcdatamodel/contents | 18 ++- CoreDataStack/Entity/Notification.swift | 110 +++++++++++++++ Localization/app.json | 13 ++ Mastodon.xcodeproj/project.pbxproj | 40 ++++++ .../Diffiable/Item/NotificationItem.swift | 40 ++++++ .../Section/NotificationSection.swift | 75 +++++++++++ Mastodon/Generated/Strings.swift | 20 +++ .../Resources/en.lproj/Localizable.strings | 7 + .../NotificationViewController.swift | 126 +++++++++++++++++- ...otificationViewModel+LoadLatestState.swift | 96 +++++++++++++ .../NotificationViewModel+diffable.swift | 118 ++++++++++++++++ .../Notification/NotificationViewModel.swift | 87 ++++++++++++ .../NotificationTableViewCell.swift | 109 +++++++++++++++ .../APIService/APIService+Notification.swift | 65 +++++++++ .../API/Mastodon+API+Notifications.swift | 47 ++++--- .../Entity/Mastodon+Entity+Notification.swift | 1 + 16 files changed, 952 insertions(+), 20 deletions(-) create mode 100644 CoreDataStack/Entity/Notification.swift create mode 100644 Mastodon/Diffiable/Item/NotificationItem.swift create mode 100644 Mastodon/Diffiable/Section/NotificationSection.swift create mode 100644 Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift create mode 100644 Mastodon/Scene/Notification/NotificationViewModel+diffable.swift create mode 100644 Mastodon/Scene/Notification/NotificationViewModel.swift create mode 100644 Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift create mode 100644 Mastodon/Service/APIService/APIService+Notification.swift diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 5ed4021a..2569da5e 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -65,6 +65,21 @@ + + + + + + + + + + + + + + + @@ -208,6 +223,7 @@ + @@ -217,4 +233,4 @@ - + \ No newline at end of file diff --git a/CoreDataStack/Entity/Notification.swift b/CoreDataStack/Entity/Notification.swift new file mode 100644 index 00000000..f19f6898 --- /dev/null +++ b/CoreDataStack/Entity/Notification.swift @@ -0,0 +1,110 @@ +// +// MastodonNotification.swift +// CoreDataStack +// +// Created by sxiaojian on 2021/4/13. +// + +import Foundation +import CoreData + +public final class MastodonNotification: NSManagedObject { + public typealias ID = UUID + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var id: String + @NSManaged public private(set) var domain: String + @NSManaged public private(set) var createAt: Date + @NSManaged public private(set) var updatedAt: Date + @NSManaged public private(set) var type: String + @NSManaged public private(set) var account: MastodonUser + @NSManaged public private(set) var status: Status? + +} + +extension MastodonNotification { + public override func awakeFromInsert() { + super.awakeFromInsert() + setPrimitiveValue(UUID(), forKey: #keyPath(MastodonNotification.identifier)) + } + + public override func willSave() { + super.willSave() + setPrimitiveValue(Date(), forKey: #keyPath(MastodonNotification.updatedAt)) + } + +} + +public extension MastodonNotification { + @discardableResult + static func insert( + into context: NSManagedObjectContext, + domain: String, + property: Property + ) -> MastodonNotification { + let notification: MastodonNotification = context.insertObject() + notification.id = property.id + notification.createAt = property.createdAt + notification.updatedAt = property.createdAt + notification.type = property.type + notification.account = property.account + notification.status = property.status + notification.domain = domain + return notification + } +} + +public extension MastodonNotification { + struct Property { + public init(id: String, + type: String, + account: MastodonUser, + status: Status?, + createdAt: Date) { + self.id = id + self.type = type + self.account = account + self.status = status + self.createdAt = createdAt + } + + public let id: String + public let type: String + public let account: MastodonUser + public let status: Status? + public let createdAt: Date + } +} + +extension MastodonNotification { + static func predicate(domain: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.domain), domain) + } + + static func predicate(type: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.type), type) + } + + public static func predicate(domain: String, type: String) -> NSPredicate { + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + MastodonNotification.predicate(domain: domain), + MastodonNotification.predicate(type: type) + ]) + } + + static func predicate(types: [String]) -> NSPredicate { + return NSPredicate(format: "%K IN %@", #keyPath(MastodonNotification.type), types) + } + + public static func predicate(domain: String, types: [String]) -> NSPredicate { + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + MastodonNotification.predicate(domain: domain), + MastodonNotification.predicate(types: types) + ]) + } +} + +extension MastodonNotification: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \MastodonNotification.updatedAt, ascending: false)] + } +} diff --git a/Localization/app.json b/Localization/app.json index 120458f7..33f1abc2 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -321,6 +321,19 @@ }, "favorite": { "title": "Your Favorites" + }, + "notification": { + "title": { + "Everything": "Everything", + "Mentions": "Mentions" + }, + "action": { + "follow": "followed you", + "favourite": "favorited your post", + "reblog": "rebloged your post", + "poll": "Your poll has ended", + "mention": "mentioned you" + } } } } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b5213989..b2258c1d 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -30,6 +30,8 @@ 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; }; + 2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */; }; + 2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */; }; 2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0B7A1C261D839600B44727 /* SearchHistory.swift */; }; 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; }; 2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; }; @@ -49,6 +51,8 @@ 2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; }; 2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; }; 2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */; }; + 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35237926256D920031AF25 /* NotificationSection.swift */; }; + 2D35238126256F690031AF25 /* NotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35238026256F690031AF25 /* NotificationTableViewCell.swift */; }; 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; }; 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; }; 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; }; @@ -76,6 +80,9 @@ 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; }; + 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D607AD726242FC500B70763 /* NotificationViewModel.swift */; }; + 2D6125472625436B00299647 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6125462625436B00299647 /* Notification.swift */; }; + 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; }; 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; @@ -91,6 +98,7 @@ 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; }; 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; }; 2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; }; + 2D7867192625B77500211898 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7867182625B77500211898 /* NotificationItem.swift */; }; 2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */; }; 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; }; 2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; }; @@ -409,6 +417,8 @@ 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = ""; }; 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; + 2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+diffable.swift"; sourceTree = ""; }; + 2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+LoadLatestState.swift"; sourceTree = ""; }; 2D0B7A1C261D839600B44727 /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = ""; }; 2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; @@ -428,6 +438,8 @@ 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = ""; }; 2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = ""; }; 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = ""; }; + 2D35237926256D920031AF25 /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = ""; }; + 2D35238026256F690031AF25 /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = ""; }; 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = ""; }; 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = ""; }; 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = ""; }; @@ -453,6 +465,9 @@ 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = ""; }; 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = ""; }; 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; + 2D607AD726242FC500B70763 /* NotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = ""; }; + 2D6125462625436B00299647 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; + 2D61254C262547C200299647 /* APIService+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Notification.swift"; sourceTree = ""; }; 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Status.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = ""; }; @@ -467,6 +482,7 @@ 2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; 2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + 2D7867182625B77500211898 /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = ""; }; 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Tag.swift"; sourceTree = ""; }; 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = ""; }; 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = ""; }; @@ -878,6 +894,14 @@ path = CollectionViewCell; sourceTree = ""; }; + 2D35237F26256F470031AF25 /* TableViewCell */ = { + isa = PBXGroup; + children = ( + 2D35238026256F690031AF25 /* NotificationTableViewCell.swift */, + ); + path = TableViewCell; + sourceTree = ""; + }; 2D364F7025E66D5B00204FDC /* ResendEmail */ = { isa = PBXGroup; children = ( @@ -1032,6 +1056,7 @@ DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */, 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */, 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */, + 2D35237926256D920031AF25 /* NotificationSection.swift */, 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, @@ -1083,6 +1108,7 @@ children = ( 2D7631B225C159F700929FB9 /* Item.swift */, 2D198642261BF09500F0B013 /* SearchResultItem.swift */, + 2D7867182625B77500211898 /* NotificationItem.swift */, DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */, DB1E347725F519300079D7DF /* PickServerItem.swift */, DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */, @@ -1312,6 +1338,7 @@ DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */, DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */, DB9A488326034BD7008B817C /* APIService+Status.swift */, + 2D61254C262547C200299647 /* APIService+Notification.swift */, DB9A488F26035963008B817C /* APIService+Media.swift */, 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */, 2D34D9DA261494120081BFC0 /* APIService+Search.swift */, @@ -1487,6 +1514,7 @@ isa = PBXGroup; children = ( DB89BA2625C110B4008580ED /* Status.swift */, + 2D6125462625436B00299647 /* Notification.swift */, 2D0B7A1C261D839600B44727 /* SearchHistory.swift */, DB8AF52425C131D1002E6C99 /* MastodonUser.swift */, DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */, @@ -1642,6 +1670,10 @@ isa = PBXGroup; children = ( DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */, + 2D607AD726242FC500B70763 /* NotificationViewModel.swift */, + 2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */, + 2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */, + 2D35237F26256F470031AF25 /* TableViewCell */, ); path = Notification; sourceTree = ""; @@ -2210,6 +2242,7 @@ 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */, DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */, + 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */, DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */, DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */, DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */, @@ -2244,6 +2277,7 @@ DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */, 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, + 2D35238126256F690031AF25 /* NotificationTableViewCell.swift in Sources */, DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, 2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */, @@ -2263,6 +2297,7 @@ DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */, DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */, + 2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */, 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, @@ -2321,6 +2356,7 @@ DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, + 2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */, @@ -2329,6 +2365,7 @@ 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */, DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, + 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */, DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */, DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */, DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */, @@ -2358,6 +2395,7 @@ 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */, + 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, DB084B5725CBC56C00F898ED /* Status.swift in Sources */, DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */, DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */, @@ -2370,6 +2408,7 @@ DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */, DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, + 2D7867192625B77500211898 /* NotificationItem.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, 2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */, @@ -2472,6 +2511,7 @@ 2D152A9225C2980C009AA50C /* UIFont.swift in Sources */, DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */, DB89BA4425C1165F008580ED /* Managed.swift in Sources */, + 2D6125472625436B00299647 /* Notification.swift in Sources */, DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */, DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */, DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/NotificationItem.swift b/Mastodon/Diffiable/Item/NotificationItem.swift new file mode 100644 index 00000000..e4a53d2b --- /dev/null +++ b/Mastodon/Diffiable/Item/NotificationItem.swift @@ -0,0 +1,40 @@ +// +// NotificationItem.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/13. +// + +import Foundation +import CoreData + +enum NotificationItem { + + case notification(ObjectID: NSManagedObjectID) + + case bottomLoader +} + +extension NotificationItem: Equatable { + static func == (lhs: NotificationItem, rhs: NotificationItem) -> Bool { + switch (lhs, rhs) { + case (.bottomLoader, .bottomLoader): + return true + case (.notification(let idLeft),.notification(let idRight)): + return idLeft == idRight + default: + return false + } + } +} + +extension NotificationItem: Hashable { + func hash(into hasher: inout Hasher) { + switch self { + case .notification(let id): + hasher.combine(id) + case .bottomLoader: + hasher.combine(String(describing: NotificationItem.bottomLoader.self)) + } + } +} diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift new file mode 100644 index 00000000..d697d3ce --- /dev/null +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -0,0 +1,75 @@ +// +// NotificationSection.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/13. +// + +import CoreData +import CoreDataStack +import Foundation +import MastodonSDK +import UIKit +import Combine + +enum NotificationSection: Equatable, Hashable { + case main +} + +extension NotificationSection { + static func tableViewDiffableDataSource( + for tableView: UITableView, + timestampUpdatePublisher: AnyPublisher, + managedObjectContext: NSManagedObjectContext + ) -> UITableViewDiffableDataSource { + + return UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, notificationItem) -> UITableViewCell? in + switch notificationItem { + case .notification(let objectID): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell + let notification = managedObjectContext.object(with: objectID) as! MastodonNotification + let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.type) + + var actionText: String + var actionImageName: String + switch type { + case .follow: + actionText = L10n.Scene.Notification.Action.follow + actionImageName = "person.crop.circle.badge.checkmark" + case .favourite: + actionText = L10n.Scene.Notification.Action.favourite + actionImageName = "star.fill" + case .reblog: + actionText = L10n.Scene.Notification.Action.reblog + actionImageName = "arrow.2.squarepath" + case .mention: + actionText = L10n.Scene.Notification.Action.mention + actionImageName = "at" + case .poll: + actionText = L10n.Scene.Notification.Action.poll + actionImageName = "list.bullet" + default: + actionText = "" + actionImageName = "" + } + + timestampUpdatePublisher + .sink { _ in + let timeText = notification.createAt.shortTimeAgoSinceNow + cell.actionLabel.text = actionText + " · " + timeText + } + .store(in: &cell.disposeBag) + cell.nameLabel.text = notification.account.displayName + + if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { + cell.actionImageView.image = actionImage + } + return cell + case .bottomLoader: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader + cell.startAnimating() + return cell + } + } + } +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 14b99388..a94afa13 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -339,6 +339,26 @@ internal enum L10n { internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing") } } + internal enum Notification { + internal enum Action { + /// favorited your toot + internal static let favourite = L10n.tr("Localizable", "Scene.Notification.Action.Favourite") + /// followed you + internal static let follow = L10n.tr("Localizable", "Scene.Notification.Action.Follow") + /// mentioned you + internal static let mention = L10n.tr("Localizable", "Scene.Notification.Action.Mention") + /// Your poll has ended + internal static let poll = L10n.tr("Localizable", "Scene.Notification.Action.Poll") + /// boosted your toot + internal static let reblog = L10n.tr("Localizable", "Scene.Notification.Action.Reblog") + } + internal enum Title { + /// Everything + internal static let everything = L10n.tr("Localizable", "Scene.Notification.Title.Everything") + /// Mentions + internal static let mentions = L10n.tr("Localizable", "Scene.Notification.Title.Mentions") + } + } internal enum Profile { /// %@ posts internal static func subtitle(_ p1: Any) -> String { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 40000bef..e4b10c0c 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -114,6 +114,13 @@ tap the link to confirm your account."; "Scene.HomeTimeline.NavigationBarState.Published" = "Published!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post..."; "Scene.HomeTimeline.Title" = "Home"; +"Scene.Notification.Action.Favourite" = "favorited your toot"; +"Scene.Notification.Action.Follow" = "followed you"; +"Scene.Notification.Action.Mention" = "mentioned you"; +"Scene.Notification.Action.Poll" = "Your poll has ended"; +"Scene.Notification.Action.Reblog" = "boosted your toot"; +"Scene.Notification.Title.Everything" = "Everything"; +"Scene.Notification.Title.Mentions" = "Mentions"; "Scene.Profile.Dashboard.Followers" = "followers"; "Scene.Profile.Dashboard.Following" = "following"; "Scene.Profile.Dashboard.Posts" = "posts"; diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index f8b3ba81..51a94e89 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -2,23 +2,147 @@ // NotificationViewController.swift // Mastodon // -// Created by MainasuK Cirno on 2021-2-23. +// Created by sxiaojian on 2021/4/12. // import UIKit +import Combine +import OSLog final class NotificationViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + var disposeBag = Set() + private(set) lazy var viewModel = NotificationViewModel(context: context, coordinator: coordinator) + + let segmentControl: UISegmentedControl = { + let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything,L10n.Scene.Notification.Title.mentions]) + control.selectedSegmentIndex = 0 + control.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .touchUpInside) + return control + }() + + let tableView: UITableView = { + let tableView = ControlContainableTableView() + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) + return tableView + }() + + let refreshControl = UIRefreshControl() + } extension NotificationViewController { override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = Asset.Colors.Background.searchResult.color + navigationItem.titleView = segmentControl + view.addSubview(tableView) + tableView.constrain([ + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + tableView.refreshControl = refreshControl + refreshControl.addTarget(self, action: #selector(NotificationViewController.refreshControlValueChanged(_:)), for: .valueChanged) + + tableView.delegate = self + viewModel.tableView = tableView + viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self + viewModel.setupDiffableDataSource(for: tableView) + + // bind refresh control + viewModel.isFetchingLatestNotification + .receive(on: DispatchQueue.main) + .sink { [weak self] isFetching in + guard let self = self else { return } + if !isFetching { + UIView.animate(withDuration: 0.5) { [weak self] in + guard let self = self else { return } + self.refreshControl.endRefreshing() + } + } + } + .store(in: &disposeBag) } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // needs trigger manually after onboarding dismiss + setNeedsStatusBarAppearanceUpdate() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 { + self.viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) + } + } + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate { _ in + // do nothing + } completion: { _ in + self.tableView.reloadData() + } + } + } + +extension NotificationViewController { + @objc private func segmentedControlValueChanged(_ sender: UISegmentedControl) { + os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, sender.selectedSegmentIndex) + } + + @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { + guard viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) else { + sender.endRefreshing() + return + } + } +} + +// MARK: - UITableViewDelegate +extension NotificationViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + return 68 + } +} + +// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate +extension NotificationViewController: ContentOffsetAdjustableTimelineViewControllerDelegate { + func navigationBar() -> UINavigationBar? { + return navigationController?.navigationBar + } +} + +//// MARK: - UIScrollViewDelegate +//extension NotificationViewController { +// func scrollViewDidScroll(_ scrollView: UIScrollView) { +// handleScrollViewDidScroll(scrollView) +// } +//} +// +//extension NotificationViewController: LoadMoreConfigurableTableViewContainer { +// typealias BottomLoaderTableViewCell = SearchBottomLoader +// typealias LoadingState = NotificationViewController.LoadOldestState.Loading +// var loadMoreConfigurableTableView: UITableView { return tableView } +// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } +//} diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift new file mode 100644 index 00000000..364085c8 --- /dev/null +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift @@ -0,0 +1,96 @@ +// +// NotificationViewModel+LoadLatestState.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/13. +// + +import os.log +import func QuartzCore.CACurrentMediaTime +import Foundation +import CoreData +import CoreDataStack +import GameplayKit +import MastodonSDK + +extension NotificationViewModel { + class LoadLatestState: GKState { + weak var viewModel: NotificationViewModel? + + init(viewModel: NotificationViewModel) { + self.viewModel = viewModel + } + + override func didEnter(from previousState: GKState?) { + os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) + viewModel?.loadLatestStateMachinePublisher.send(self) + } + } +} + +extension NotificationViewModel.LoadLatestState { + class Initial: NotificationViewModel.LoadLatestState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Loading.self + } + } + + class Loading: NotificationViewModel.LoadLatestState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Fail.self || stateClass == Idle.self + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { + // sign out when loading will enter here + stateMachine.enter(Fail.self) + return + } + let query = Mastodon.API.Notifications.Query( + maxID: nil, + sinceID: nil, + minID: nil, + limit: nil, + excludeTypes: Mastodon.API.Notifications.allExcludeTypes(), + accountID: nil) + viewModel.context.apiService.allNotifications( + domain: activeMastodonAuthenticationBox.domain, + query: query, + mastodonAuthenticationBox: activeMastodonAuthenticationBox) + .sink { completion in + switch completion { + case .failure(let error): + viewModel.isFetchingLatestNotification.value = false + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + // handle isFetchingLatestTimeline in fetch controller delegate + break + } + + stateMachine.enter(Idle.self) + } receiveValue: { response in + if response.value.isEmpty { + viewModel.isFetchingLatestNotification.value = false + } + } + .store(in: &viewModel.disposeBag) + + + } + } + + class Fail: NotificationViewModel.LoadLatestState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Loading.self || stateClass == Idle.self + } + } + + class Idle: NotificationViewModel.LoadLatestState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Loading.self + } + } + +} diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift new file mode 100644 index 00000000..c68096c8 --- /dev/null +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -0,0 +1,118 @@ +// +// NotificationViewModel+diffable.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/13. +// + +import os.log +import UIKit +import CoreData +import CoreDataStack + +extension NotificationViewModel { + + func setupDiffableDataSource( + for tableView: UITableView + ) { + let timestampUpdatePublisher = Timer.publish(every: 30.0, on: .main, in: .common) + .autoconnect() + .share() + .eraseToAnyPublisher() + + diffableDataSource = NotificationSection.tableViewDiffableDataSource( + for: tableView, + timestampUpdatePublisher: timestampUpdatePublisher, + managedObjectContext: context.managedObjectContext + ) + } + +} + +extension NotificationViewModel: NSFetchedResultsControllerDelegate { + func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + + func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + guard let tableView = self.tableView else { return } + guard let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return } + + guard let diffableDataSource = self.diffableDataSource else { return } + let oldSnapshot = diffableDataSource.snapshot() + + let predicate = fetchedResultsController.fetchRequest.predicate + let parentManagedObjectContext = fetchedResultsController.managedObjectContext + let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + managedObjectContext.parent = parentManagedObjectContext + + managedObjectContext.perform { + + let notifications: [MastodonNotification] = { + let request = MastodonNotification.sortedFetchRequest + request.returnsObjectsAsFaults = false + request.predicate = predicate + do { + return try managedObjectContext.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return [] + } + }() + + var newSnapshot = NSDiffableDataSourceSnapshot() + newSnapshot.appendSections([.main]) + newSnapshot.appendItems(notifications.map({NotificationItem.notification(ObjectID: $0.objectID)}), toSection: .main) + newSnapshot.appendItems([.bottomLoader], toSection: .main) + + DispatchQueue.main.async { + guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else { + diffableDataSource.apply(newSnapshot) + self.isFetchingLatestNotification.value = false + return + } + + diffableDataSource.apply(newSnapshot, animatingDifferences: false) { + tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) + tableView.contentOffset.y = tableView.contentOffset.y - difference.offset + self.isFetchingLatestNotification.value = false + } + } + } + } + + private struct Difference { + let item: T + let sourceIndexPath: IndexPath + let targetIndexPath: IndexPath + let offset: CGFloat + } + + private func calculateReloadSnapshotDifference( + navigationBar: UINavigationBar, + tableView: UITableView, + oldSnapshot: NSDiffableDataSourceSnapshot, + newSnapshot: NSDiffableDataSourceSnapshot + ) -> Difference? { + guard oldSnapshot.numberOfItems != 0 else { return nil } + + // old snapshot not empty. set source index path to first item if not match + let sourceIndexPath = UIViewController.topVisibleTableViewCellIndexPath(in: tableView, navigationBar: navigationBar) ?? IndexPath(row: 0, section: 0) + + guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil } + + let timelineItem = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row] + guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: timelineItem) else { return nil } + let targetIndexPath = IndexPath(row: itemIndex, section: 0) + + let offset = UIViewController.tableViewCellOriginOffsetToWindowTop(in: tableView, at: sourceIndexPath, navigationBar: navigationBar) + return Difference( + item: timelineItem, + sourceIndexPath: sourceIndexPath, + targetIndexPath: targetIndexPath, + offset: offset + ) + } +} diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift new file mode 100644 index 00000000..4736785f --- /dev/null +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -0,0 +1,87 @@ +// +// NotificationViewModel.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/12. +// + +import Foundation +import Combine +import UIKit +import CoreData +import CoreDataStack +import GameplayKit + +final class NotificationViewModel: NSObject { + + var disposeBag = Set() + + // input + let context: AppContext + weak var coordinator: SceneCoordinator! + weak var tableView: UITableView! + weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? + + + let activeMastodonAuthenticationBox: CurrentValueSubject + let fetchedResultsController: NSFetchedResultsController! + let notificationPredicate = CurrentValueSubject(nil) + let cellFrameCache = NSCache() + + let isFetchingLatestNotification = CurrentValueSubject(false) + + //output + var diffableDataSource: UITableViewDiffableDataSource! + // top loader + private(set) lazy var loadLatestStateMachine: GKStateMachine = { + // exclude timeline middle fetcher state + let stateMachine = GKStateMachine(states: [ + LoadLatestState.Initial(viewModel: self), + LoadLatestState.Loading(viewModel: self), + LoadLatestState.Fail(viewModel: self), + LoadLatestState.Idle(viewModel: self), + ]) + stateMachine.enter(LoadLatestState.Initial.self) + return stateMachine + }() + + lazy var loadLatestStateMachinePublisher = CurrentValueSubject(nil) + + init(context: AppContext,coordinator: SceneCoordinator) { + self.coordinator = coordinator + self.context = context + self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) + self.fetchedResultsController = { + let fetchRequest = MastodonNotification.sortedFetchRequest + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(MastodonNotification.status),#keyPath(MastodonNotification.account)] + let controller = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: context.managedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + + return controller + }() + + super.init() + self.fetchedResultsController.delegate = self + context.authenticationService.activeMastodonAuthenticationBox + .assign(to: \.value, on: activeMastodonAuthenticationBox) + .store(in: &disposeBag) + + notificationPredicate + .compactMap{ $0 } + .sink { [weak self] predicate in + guard let self = self else { return } + self.fetchedResultsController.fetchRequest.predicate = predicate + do { + try self.fetchedResultsController.performFetch() + } catch { + assertionFailure(error.localizedDescription) + } + } + .store(in: &disposeBag) + } +} diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift new file mode 100644 index 00000000..8a1b3572 --- /dev/null +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -0,0 +1,109 @@ +// +// NotificationTableViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/13. +// + +import Foundation +import UIKit +import Combine + + +final class NotificationTableViewCell: UITableViewCell { + + static let actionImageBorderWidth: CGFloat = 3 + + var disposeBag = Set() + + let avatatImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 4 + imageView.layer.cornerCurve = .continuous + imageView.clipsToBounds = true + return imageView + }() + + let actionImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 4 + imageView.layer.cornerCurve = .continuous + imageView.clipsToBounds = true + imageView.layer.borderWidth = NotificationTableViewCell.actionImageBorderWidth + imageView.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + imageView.tintColor = Asset.Colors.Background.searchResult.color + return imageView + }() + + let actionLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.Label.secondary.color + label.font = UIFont.preferredFont(forTextStyle: .body) + label.lineBreakMode = .byTruncatingTail + return label + }() + + let nameLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.brandBlue.color + label.font = .systemFont(ofSize: 15, weight: .semibold) + label.lineBreakMode = .byTruncatingTail + return label + }() + + var nameLabelTop: NSLayoutConstraint! + + override func prepareForReuse() { + super.prepareForReuse() + avatatImageView.af.cancelImageRequest() + + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } +} + +extension NotificationTableViewCell { + + func configure() { + contentView.addSubview(avatatImageView) + avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) + avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil) + + contentView.addSubview(actionImageView) + actionImageView.pin(toSize: CGSize(width: 24, height: 24)) + actionImageView.pin(top: 33, left: 33, bottom: nil, right: nil) + + nameLabelTop = nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor) + nameLabel.constrain([ + nameLabelTop, + nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 61) + ]) + + actionLabel.constrain([ + actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), + actionLabel.topAnchor.constraint(equalTo: nameLabel.topAnchor), + contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4) + ]) + } + + public func nameLabelLayoutIn(center: Bool) { + if center { + nameLabelTop.constant = 24 + } else { + nameLabelTop.constant = 12 + } + } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + self.actionImageView.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + } +} diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift new file mode 100644 index 00000000..745a04fa --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Notification.swift @@ -0,0 +1,65 @@ +// +// APIService+Notification.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/13. +// + +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK +import OSLog + +extension APIService { + func allNotifications( + domain: String, + query: Mastodon.API.Notifications.Query, + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = mastodonAuthenticationBox.userAuthorization + return Mastodon.API.Notifications.getNotifications( + session: session, + domain: domain, + query: query, + authorization: authorization) + .flatMap { response -> AnyPublisher, Error> in + let log = OSLog.api + return self.backgroundManagedObjectContext.performChanges { + response.value.forEach { notification in + let (mastodonUser,isCreated) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log) + let flag = isCreated ? "+" : "-" + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)%s", (#file as NSString).lastPathComponent, #line, #function, flag, mastodonUser.id, mastodonUser.username) + var status: Status? + if let statusEntity = notification.status { + let (statusInCoreData,_,_) = APIService.CoreData.createOrMergeStatus( + into: self.backgroundManagedObjectContext, + for: nil, + domain: domain, + entity: statusEntity, + statusCache: nil, + userCache: nil, + networkDate: Date(), + log: log) + status = statusInCoreData + } + // use constrain to avoid repeated save + _ = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, property: MastodonNotification.Property(id: notification.id, type: notification.type.rawValue, account: mastodonUser, status: status, createdAt: Date())) + + } + } + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Notification]> in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift index cdee8292..b7fd0fb4 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift @@ -1,18 +1,19 @@ // // File.swift -// +// // // Created by BradGao on 2021/4/1. // -import Foundation import Combine +import Foundation -extension Mastodon.API.Notifications { - static func notificationsEndpointURL(domain: String) -> URL { - Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent("notifications") +public extension Mastodon.API.Notifications { + internal static func notificationsEndpointURL(domain: String) -> URL { + Mastodon.API.endpointURL(domain: domain).appendingPathComponent("notifications") } - static func getNotificationEndpointURL(domain: String, notificationID: String) -> URL { + + internal static func getNotificationEndpointURL(domain: String, notificationID: String) -> URL { notificationsEndpointURL(domain: domain).appendingPathComponent(notificationID) } @@ -27,15 +28,15 @@ extension Mastodon.API.Notifications { /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" - /// - query: `GetAllNotificationsQuery` with query parameters + /// - query: `NotificationsQuery` with query parameters /// - authorization: User token /// - Returns: `AnyPublisher` contains `Token` nested in the response - public static func getAll( + static func getNotifications( session: URLSession, domain: String, - query: GetAllNotificationsQuery, - authorization: Mastodon.API.OAuth.Authorization? - ) -> AnyPublisher, Error> { + query: Mastodon.API.Notifications.Query, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { let request = Mastodon.API.get( url: notificationsEndpointURL(domain: domain), query: query, @@ -63,12 +64,12 @@ extension Mastodon.API.Notifications { /// - notificationID: ID of the notification. /// - authorization: User token /// - Returns: `AnyPublisher` contains `Token` nested in the response - public static func get( + static func getNotification( session: URLSession, domain: String, notificationID: String, - authorization: Mastodon.API.OAuth.Authorization? - ) -> AnyPublisher, Error> { + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { let request = Mastodon.API.get( url: getNotificationEndpointURL(domain: domain, notificationID: notificationID), query: nil, @@ -82,12 +83,22 @@ extension Mastodon.API.Notifications { .eraseToAnyPublisher() } - public struct GetAllNotificationsQuery: Codable, PagedQueryType, GetQuery { + static func allExcludeTypes() -> [Mastodon.Entity.Notification.NotificationType] { + [.follow] + } + + static func mentionsExcludeTypes() -> [Mastodon.Entity.Notification.NotificationType] { + [.follow, .followRequest, .favourite, .reblog, .poll] + } +} + +public extension Mastodon.API.Notifications { + struct Query: Codable, PagedQueryType, GetQuery { public let maxID: Mastodon.Entity.Status.ID? public let sinceID: Mastodon.Entity.Status.ID? public let minID: Mastodon.Entity.Status.ID? public let limit: Int? - public let excludeTypes: [String]? + public let excludeTypes: [Mastodon.Entity.Notification.NotificationType]? public let accountID: String? public init( @@ -95,7 +106,7 @@ extension Mastodon.API.Notifications { sinceID: Mastodon.Entity.Status.ID? = nil, minID: Mastodon.Entity.Status.ID? = nil, limit: Int? = nil, - excludeTypes: [String]? = nil, + excludeTypes: [Mastodon.Entity.Notification.NotificationType]? = nil, accountID: String? = nil ) { self.maxID = maxID @@ -114,7 +125,7 @@ extension Mastodon.API.Notifications { limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } if let excludeTypes = excludeTypes { excludeTypes.forEach { - items.append(URLQueryItem(name: "exclude_types[]", value: $0)) + items.append(URLQueryItem(name: "exclude_types[]", value: $0.rawValue)) } } accountID.flatMap { items.append(URLQueryItem(name: "account_id", value: $0)) } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift index 413c89bd..0cdcc2e7 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift @@ -37,6 +37,7 @@ extension Mastodon.Entity { } extension Mastodon.Entity.Notification { + public typealias NotificationType = Type public enum `Type`: RawRepresentable, Codable { case follow case followRequest From 42628398e6891c34d2821262b1abd17c436c3140 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 13 Apr 2021 21:31:49 +0800 Subject: [PATCH 02/23] chore: display Notification Cell --- CoreDataStack/Entity/Notification.swift | 14 +------ .../Section/NotificationSection.swift | 20 ++++++++++ Mastodon/Generated/Assets.swift | 5 +++ Mastodon/Generated/Strings.swift | 4 +- .../Colors/Notification/Contents.json | 9 +++++ .../favourite.colorset/Contents.json | 20 ++++++++++ .../mention.colorset/Contents.json | 38 +++++++++++++++++++ .../reblog.colorset/Contents.json | 38 +++++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 4 +- .../NotificationViewController.swift | 11 ++++-- ...otificationViewModel+LoadLatestState.swift | 2 +- .../NotificationViewModel+diffable.swift | 4 +- .../Notification/NotificationViewModel.swift | 21 +++++++++- .../NotificationTableViewCell.swift | 31 ++++++++++----- .../APIService/APIService+Notification.swift | 7 ++-- .../API/Mastodon+API+Notifications.swift | 2 +- 16 files changed, 192 insertions(+), 38 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Notification/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Notification/favourite.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Notification/mention.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Notification/reblog.colorset/Contents.json diff --git a/CoreDataStack/Entity/Notification.swift b/CoreDataStack/Entity/Notification.swift index f19f6898..144ad9c2 100644 --- a/CoreDataStack/Entity/Notification.swift +++ b/CoreDataStack/Entity/Notification.swift @@ -76,7 +76,7 @@ public extension MastodonNotification { } extension MastodonNotification { - static func predicate(domain: String) -> NSPredicate { + public static func predicate(domain: String) -> NSPredicate { return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.domain), domain) } @@ -90,17 +90,7 @@ extension MastodonNotification { MastodonNotification.predicate(type: type) ]) } - - static func predicate(types: [String]) -> NSPredicate { - return NSPredicate(format: "%K IN %@", #keyPath(MastodonNotification.type), types) - } - - public static func predicate(domain: String, types: [String]) -> NSPredicate { - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - MastodonNotification.predicate(domain: domain), - MastodonNotification.predicate(types: types) - ]) - } + } extension MastodonNotification: Managed { diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index d697d3ce..277a40f5 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -32,25 +32,32 @@ extension NotificationSection { var actionText: String var actionImageName: String + var color: UIColor switch type { case .follow: actionText = L10n.Scene.Notification.Action.follow actionImageName = "person.crop.circle.badge.checkmark" + color = Asset.Colors.brandBlue.color case .favourite: actionText = L10n.Scene.Notification.Action.favourite actionImageName = "star.fill" + color = Asset.Colors.Notification.favourite.color case .reblog: actionText = L10n.Scene.Notification.Action.reblog actionImageName = "arrow.2.squarepath" + color = Asset.Colors.Notification.reblog.color case .mention: actionText = L10n.Scene.Notification.Action.mention actionImageName = "at" + color = Asset.Colors.Notification.mention.color case .poll: actionText = L10n.Scene.Notification.Action.poll actionImageName = "list.bullet" + color = Asset.Colors.brandBlue.color default: actionText = "" actionImageName = "" + color = .clear } timestampUpdatePublisher @@ -59,11 +66,24 @@ extension NotificationSection { cell.actionLabel.text = actionText + " · " + timeText } .store(in: &cell.disposeBag) + let timeText = notification.createAt.shortTimeAgoSinceNow + cell.actionImageBackground.backgroundColor = color + cell.actionLabel.text = actionText + " · " + timeText cell.nameLabel.text = notification.account.displayName + cell.avatatImageView.af.setImage( + withURL: URL(string: notification.account.avatar)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { cell.actionImageView.image = actionImage } + if let _ = notification.status { + cell.nameLabelLayoutIn(center: true) + } else { + cell.nameLabelLayoutIn(center: false) + } return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 843fce02..ef7ae929 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -70,6 +70,11 @@ internal enum Asset { internal static let primary = ColorAsset(name: "Colors/Label/primary") internal static let secondary = ColorAsset(name: "Colors/Label/secondary") } + internal enum Notification { + internal static let favourite = ColorAsset(name: "Colors/Notification/favourite") + internal static let mention = ColorAsset(name: "Colors/Notification/mention") + internal static let reblog = ColorAsset(name: "Colors/Notification/reblog") + } internal enum Shadow { internal static let searchCard = ColorAsset(name: "Colors/Shadow/SearchCard") } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index a94afa13..21a5053b 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -341,7 +341,7 @@ internal enum L10n { } internal enum Notification { internal enum Action { - /// favorited your toot + /// favorited your post internal static let favourite = L10n.tr("Localizable", "Scene.Notification.Action.Favourite") /// followed you internal static let follow = L10n.tr("Localizable", "Scene.Notification.Action.Follow") @@ -349,7 +349,7 @@ internal enum L10n { internal static let mention = L10n.tr("Localizable", "Scene.Notification.Action.Mention") /// Your poll has ended internal static let poll = L10n.tr("Localizable", "Scene.Notification.Action.Poll") - /// boosted your toot + /// rebloged your post internal static let reblog = L10n.tr("Localizable", "Scene.Notification.Action.Reblog") } internal enum Title { diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Notification/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Notification/Contents.json new file mode 100644 index 00000000..6e965652 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Notification/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Notification/favourite.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Notification/favourite.colorset/Contents.json new file mode 100644 index 00000000..36de2027 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Notification/favourite.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0", + "green" : "204", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Notification/mention.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Notification/mention.colorset/Contents.json new file mode 100644 index 00000000..9dff2f59 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Notification/mention.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "222", + "green" : "82", + "red" : "175" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "242", + "green" : "90", + "red" : "191" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Notification/reblog.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Notification/reblog.colorset/Contents.json new file mode 100644 index 00000000..ec427cca --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Notification/reblog.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "89", + "green" : "199", + "red" : "52" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "75", + "green" : "215", + "red" : "20" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index e4b10c0c..aa43ec64 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -114,11 +114,11 @@ tap the link to confirm your account."; "Scene.HomeTimeline.NavigationBarState.Published" = "Published!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post..."; "Scene.HomeTimeline.Title" = "Home"; -"Scene.Notification.Action.Favourite" = "favorited your toot"; +"Scene.Notification.Action.Favourite" = "favorited your post"; "Scene.Notification.Action.Follow" = "followed you"; "Scene.Notification.Action.Mention" = "mentioned you"; "Scene.Notification.Action.Poll" = "Your poll has ended"; -"Scene.Notification.Action.Reblog" = "boosted your toot"; +"Scene.Notification.Action.Reblog" = "rebloged your post"; "Scene.Notification.Title.Everything" = "Everything"; "Scene.Notification.Title.Mentions" = "Mentions"; "Scene.Profile.Dashboard.Followers" = "followers"; diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 51a94e89..b8717292 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -27,9 +27,10 @@ final class NotificationViewController: UIViewController, NeedsDependency { let tableView: UITableView = { let tableView = ControlContainableTableView() tableView.rowHeight = UITableView.automaticDimension - tableView.separatorStyle = .none - tableView.backgroundColor = .clear + tableView.separatorStyle = .singleLine + tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) + tableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self)) return tableView }() @@ -58,7 +59,7 @@ extension NotificationViewController { viewModel.tableView = tableView viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self viewModel.setupDiffableDataSource(for: tableView) - + viewModel.viewDidLoad.send() // bind refresh control viewModel.isFetchingLatestNotification .receive(on: DispatchQueue.main) @@ -124,6 +125,10 @@ extension NotificationViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { return 68 } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 68 + } } // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift index 364085c8..3e88de9a 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift @@ -43,7 +43,7 @@ extension NotificationViewModel.LoadLatestState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { + guard let activeMastodonAuthenticationBox = viewModel.activeMastodonAuthenticationBox.value else { // sign out when loading will enter here stateMachine.enter(Fail.self) return diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index c68096c8..1d77d41b 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -65,7 +65,9 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { var newSnapshot = NSDiffableDataSourceSnapshot() newSnapshot.appendSections([.main]) newSnapshot.appendItems(notifications.map({NotificationItem.notification(ObjectID: $0.objectID)}), toSection: .main) - newSnapshot.appendItems([.bottomLoader], toSection: .main) + if !notifications.isEmpty { + newSnapshot.appendItems([.bottomLoader], toSection: .main) + } DispatchQueue.main.async { guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else { diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 4736785f..1b489031 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -11,6 +11,7 @@ import UIKit import CoreData import CoreDataStack import GameplayKit +import MastodonSDK final class NotificationViewModel: NSObject { @@ -19,9 +20,10 @@ final class NotificationViewModel: NSObject { // input let context: AppContext weak var coordinator: SceneCoordinator! - weak var tableView: UITableView! + weak var tableView: UITableView? weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? + let viewDidLoad = PassthroughSubject() let activeMastodonAuthenticationBox: CurrentValueSubject let fetchedResultsController: NSFetchedResultsController! @@ -68,7 +70,13 @@ final class NotificationViewModel: NSObject { super.init() self.fetchedResultsController.delegate = self context.authenticationService.activeMastodonAuthenticationBox - .assign(to: \.value, on: activeMastodonAuthenticationBox) + .sink(receiveValue: { [weak self] box in + guard let self = self else { return } + self.activeMastodonAuthenticationBox.value = box + if let domain = box?.domain { + self.notificationPredicate.value = MastodonNotification.predicate(domain: domain) + } + }) .store(in: &disposeBag) notificationPredicate @@ -83,5 +91,14 @@ final class NotificationViewModel: NSObject { } } .store(in: &disposeBag) + + self.viewDidLoad + .sink { [weak self] in + + guard let domain = self?.activeMastodonAuthenticationBox.value?.domain else { return } + self?.notificationPredicate.value = MastodonNotification.predicate(domain: domain) + + } + .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 8a1b3572..b252b76d 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -12,7 +12,7 @@ import Combine final class NotificationTableViewCell: UITableViewCell { - static let actionImageBorderWidth: CGFloat = 3 + static let actionImageBorderWidth: CGFloat = 2 var disposeBag = Set() @@ -26,15 +26,21 @@ final class NotificationTableViewCell: UITableViewCell { let actionImageView: UIImageView = { let imageView = UIImageView() - imageView.layer.cornerRadius = 4 - imageView.layer.cornerCurve = .continuous - imageView.clipsToBounds = true - imageView.layer.borderWidth = NotificationTableViewCell.actionImageBorderWidth - imageView.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor imageView.tintColor = Asset.Colors.Background.searchResult.color return imageView }() + let actionImageBackground: UIView = { + let view = UIView() + view.layer.cornerRadius = (24 + NotificationTableViewCell.actionImageBorderWidth)/2 + view.layer.cornerCurve = .continuous + view.clipsToBounds = true + view.layer.borderWidth = NotificationTableViewCell.actionImageBorderWidth + view.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + view.tintColor = Asset.Colors.Background.searchResult.color + return view + }() + let actionLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.secondary.color @@ -77,16 +83,21 @@ extension NotificationTableViewCell { avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil) - contentView.addSubview(actionImageView) - actionImageView.pin(toSize: CGSize(width: 24, height: 24)) - actionImageView.pin(top: 33, left: 33, bottom: nil, right: nil) + contentView.addSubview(actionImageBackground) + actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationTableViewCell.actionImageBorderWidth, height: 24 + NotificationTableViewCell.actionImageBorderWidth)) + actionImageBackground.pin(top: 33, left: 33, bottom: nil, right: nil) + + actionImageBackground.addSubview(actionImageView) + actionImageView.constrainToCenter() nameLabelTop = nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor) + contentView.addSubview(nameLabel) nameLabel.constrain([ nameLabelTop, nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 61) ]) + contentView.addSubview(actionLabel) actionLabel.constrain([ actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), actionLabel.topAnchor.constraint(equalTo: nameLabel.topAnchor), @@ -104,6 +115,6 @@ extension NotificationTableViewCell { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - self.actionImageView.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + self.actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor } } diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift index 745a04fa..84cb6d3c 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/Mastodon/Service/APIService/APIService+Notification.swift @@ -28,9 +28,7 @@ extension APIService { let log = OSLog.api return self.backgroundManagedObjectContext.performChanges { response.value.forEach { notification in - let (mastodonUser,isCreated) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log) - let flag = isCreated ? "+" : "-" - os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)%s", (#file as NSString).lastPathComponent, #line, #function, flag, mastodonUser.id, mastodonUser.username) + let (mastodonUser,_) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log) var status: Status? if let statusEntity = notification.status { let (statusInCoreData,_,_) = APIService.CoreData.createOrMergeStatus( @@ -45,7 +43,8 @@ extension APIService { status = statusInCoreData } // use constrain to avoid repeated save - _ = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, property: MastodonNotification.Property(id: notification.id, type: notification.type.rawValue, account: mastodonUser, status: status, createdAt: Date())) + let notification = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, property: MastodonNotification.Property(id: notification.id, type: notification.type.rawValue, account: mastodonUser, status: status, createdAt: Date())) + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)", (#file as NSString).lastPathComponent, #line, #function, notification.type, notification.account.username) } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift index b7fd0fb4..1cc54add 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift @@ -84,7 +84,7 @@ public extension Mastodon.API.Notifications { } static func allExcludeTypes() -> [Mastodon.Entity.Notification.NotificationType] { - [.follow] + [.followRequest] } static func mentionsExcludeTypes() -> [Mastodon.Entity.Notification.NotificationType] { From 66b07f41dbb0f33724517d1f9236069250555950 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 14 Apr 2021 15:00:48 +0800 Subject: [PATCH 03/23] chore: display status Content --- .../Section/NotificationSection.swift | 377 +++++++++++++++++- Mastodon/Generated/Assets.swift | 1 + .../notification.colorset/Contents.json | 38 ++ .../NotificationViewController.swift | 14 +- .../NotificationViewModel+diffable.swift | 13 +- .../Notification/NotificationViewModel.swift | 5 + .../NotificationTableViewCell.swift | 71 +++- 7 files changed, 494 insertions(+), 25 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Border/notification.colorset/Contents.json diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 277a40f5..09f0f87a 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -20,13 +20,19 @@ extension NotificationSection { static func tableViewDiffableDataSource( for tableView: UITableView, timestampUpdatePublisher: AnyPublisher, - managedObjectContext: NSManagedObjectContext + managedObjectContext: NSManagedObjectContext, + delegate: NotificationTableViewCellDelegate, + dependency: NeedsDependency, + requestUserID: String ) -> UITableViewDiffableDataSource { - - return UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, notificationItem) -> UITableViewCell? in + return UITableViewDiffableDataSource(tableView: tableView) { + [weak delegate,weak dependency] + (tableView, indexPath, notificationItem) -> UITableViewCell? in + guard let dependency = dependency else { return nil } switch notificationItem { case .notification(let objectID): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell + cell.delegate = delegate let notification = managedObjectContext.object(with: objectID) as! MastodonNotification let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.type) @@ -69,7 +75,7 @@ extension NotificationSection { let timeText = notification.createAt.shortTimeAgoSinceNow cell.actionImageBackground.backgroundColor = color cell.actionLabel.text = actionText + " · " + timeText - cell.nameLabel.text = notification.account.displayName + cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName cell.avatatImageView.af.setImage( withURL: URL(string: notification.account.avatar)!, placeholderImage: UIImage.placeholder(color: .systemFill), @@ -79,10 +85,18 @@ extension NotificationSection { if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { cell.actionImageView.image = actionImage } - if let _ = notification.status { - cell.nameLabelLayoutIn(center: true) - } else { + if let status = notification.status { + let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - 61 - 2, height: tableView.readableContentGuide.layoutFrame.height) + NotificationSection.configure(cell: cell, + dependency: dependency, + readableLayoutFrame: frame, + timestampUpdatePublisher: timestampUpdatePublisher, + status: status, + requestUserID: "", + statusItemAttribute: Item.StatusAttribute(isStatusTextSensitive: false, isStatusSensitive: false)) cell.nameLabelLayoutIn(center: false) + } else { + cell.nameLabelLayoutIn(center: true) } return cell case .bottomLoader: @@ -93,3 +107,352 @@ extension NotificationSection { } } } + + +extension NotificationSection { + static func configure( + cell: NotificationTableViewCell, + dependency: NeedsDependency, + readableLayoutFrame: CGRect?, + timestampUpdatePublisher: AnyPublisher, + status: Status, + requestUserID: String, + statusItemAttribute: Item.StatusAttribute + ) { + // disable interaction + cell.statusView.isUserInteractionEnabled = false + // remove actionToolBar + cell.statusView.actionToolbarContainer.removeFromSuperview() + // setup attribute + statusItemAttribute.setupForStatus(status: status) + + // set header + NotificationSection.configureHeader(cell: cell, status: status) + ManagedObjectObserver.observe(object: status) + .receive(on: DispatchQueue.main) + .sink { _ in + // do nothing + } receiveValue: { change in + guard case .update(let object) = change.changeType, + let newStatus = object as? Status else { return } + NotificationSection.configureHeader(cell: cell, status: newStatus) + } + .store(in: &cell.disposeBag) + + // set name username + cell.statusView.nameLabel.text = { + let author = (status.reblog ?? status).author + return author.displayName.isEmpty ? author.username : author.displayName + }() + cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct + // set avatar + + cell.statusView.avatarButton.isHidden = false + cell.statusView.avatarStackedContainerButton.isHidden = true + cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) + + + // set text + cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content) + + // set status text content warning + let isStatusTextSensitive = statusItemAttribute.isStatusTextSensitive ?? false + let spoilerText = (status.reblog ?? status).spoilerText ?? "" + cell.statusView.isStatusTextSensitive = isStatusTextSensitive + cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive) + cell.statusView.contentWarningTitle.text = { + if spoilerText.isEmpty { + return L10n.Common.Controls.Status.statusContentWarning + } else { + return L10n.Common.Controls.Status.statusContentWarning + ": \(spoilerText)" + } + }() + + // prepare media attachments + let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } + + // set image + let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments) + let imageViewMaxSize: CGSize = { + let maxWidth: CGFloat = { + // use timelinePostView width as container width + // that width follows readable width and keep constant width after rotate + let containerFrame = readableLayoutFrame ?? cell.statusView.frame + var containerWidth = containerFrame.width + containerWidth -= 10 + containerWidth -= StatusView.avatarImageSize.width + return containerWidth + }() + let scale: CGFloat = { + switch mosiacImageViewModel.metas.count { + case 1: return 1.3 + default: return 0.7 + } + }() + return CGSize(width: maxWidth, height: maxWidth * scale) + }() + if mosiacImageViewModel.metas.count == 1 { + let meta = mosiacImageViewModel.metas[0] + let imageView = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) + imageView.af.setImage( + withURL: meta.url, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + } else { + let imageViews = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) + for (i, imageView) in imageViews.enumerated() { + let meta = mosiacImageViewModel.metas[i] + imageView.af.setImage( + withURL: meta.url, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + } + } + cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty + let isStatusSensitive = statusItemAttribute.isStatusSensitive ?? false + cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil + cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive + + // set audio + if let _ = mediaAttachments.filter({ $0.type == .audio }).first { + cell.statusView.audioView.isHidden = false + cell.statusView.audioView.playButton.isSelected = false + cell.statusView.audioView.slider.isEnabled = false + cell.statusView.audioView.slider.setValue(0, animated: false) + } else { + cell.statusView.audioView.isHidden = true + } + + // set GIF & video + let playerViewMaxSize: CGSize = { + let maxWidth: CGFloat = { + // use statusView width as container width + // that width follows readable width and keep constant width after rotate + let containerFrame = readableLayoutFrame ?? cell.statusView.frame + return containerFrame.width + }() + let scale: CGFloat = 1.3 + return CGSize(width: maxWidth, height: maxWidth * scale) + }() + + cell.statusView.playerContainerView.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil + cell.statusView.playerContainerView.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + cell.statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive + + if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first, + let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) + { + let parent = cell.delegate?.parent() + let playerContainerView = cell.statusView.playerContainerView + let playerViewController = playerContainerView.setupPlayer( + aspectRatio: videoPlayerViewModel.videoSize, + maxSize: playerViewMaxSize, + parent: parent + ) + playerViewController.player = videoPlayerViewModel.player + playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif + playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind) + if videoPlayerViewModel.videoKind == .gif { + playerContainerView.setMediaIndicator(isHidden: false) + } else { + videoPlayerViewModel.timeControlStatus.sink { timeControlStatus in + UIView.animate(withDuration: 0.33) { + switch timeControlStatus { + case .playing: + playerContainerView.setMediaIndicator(isHidden: true) + case .paused, .waitingToPlayAtSpecifiedRate: + playerContainerView.setMediaIndicator(isHidden: false) + @unknown default: + assertionFailure() + } + } + } + .store(in: &cell.disposeBag) + } + playerContainerView.isHidden = false + + } else { + cell.statusView.playerContainerView.playerViewController.player?.pause() + cell.statusView.playerContainerView.playerViewController.player = nil + } + // set poll + let poll = (status.reblog ?? status).poll + NotificationSection.configurePoll( + cell: cell, + poll: poll, + requestUserID: requestUserID, + updateProgressAnimated: false, + timestampUpdatePublisher: timestampUpdatePublisher + ) + if let poll = poll { + ManagedObjectObserver.observe(object: poll) + .sink { _ in + // do nothing + } receiveValue: { change in + guard case .update(let object) = change.changeType, + let newPoll = object as? Poll else { return } + NotificationSection.configurePoll( + cell: cell, + poll: newPoll, + requestUserID: requestUserID, + updateProgressAnimated: true, + timestampUpdatePublisher: timestampUpdatePublisher + ) + } + .store(in: &cell.disposeBag) + } + + // set date + let createdAt = (status.reblog ?? status).createdAt + cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow + timestampUpdatePublisher + .sink { _ in + cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow + } + .store(in: &cell.disposeBag) + + } + + static func configureHeader( + cell: NotificationTableViewCell, + status: Status + ) { + if status.reblog != nil { + cell.statusView.headerContainerStackView.isHidden = false + cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage) + cell.statusView.headerInfoLabel.text = { + let author = status.author + let name = author.displayName.isEmpty ? author.username : author.displayName + return L10n.Common.Controls.Status.userReblogged(name) + }() + } else if let replyTo = status.replyTo { + cell.statusView.headerContainerStackView.isHidden = false + cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) + cell.statusView.headerInfoLabel.text = { + let author = replyTo.author + let name = author.displayName.isEmpty ? author.username : author.displayName + return L10n.Common.Controls.Status.userRepliedTo(name) + }() + } else { + cell.statusView.headerContainerStackView.isHidden = true + } + } + + + static func configurePoll( + cell: NotificationTableViewCell, + poll: Poll?, + requestUserID: String, + updateProgressAnimated: Bool, + timestampUpdatePublisher: AnyPublisher + ) { + guard let poll = poll, + let managedObjectContext = poll.managedObjectContext + else { + cell.statusView.pollTableView.isHidden = true + cell.statusView.pollStatusStackView.isHidden = true + cell.statusView.pollVoteButton.isHidden = true + return + } + + cell.statusView.pollTableView.isHidden = false + cell.statusView.pollStatusStackView.isHidden = false + cell.statusView.pollVoteCountLabel.text = { + if poll.multiple { + let count = poll.votersCount?.intValue ?? 0 + if count > 1 { + return L10n.Common.Controls.Status.Poll.VoterCount.single(count) + } else { + return L10n.Common.Controls.Status.Poll.VoterCount.multiple(count) + } + } else { + let count = poll.votesCount.intValue + if count > 1 { + return L10n.Common.Controls.Status.Poll.VoteCount.single(count) + } else { + return L10n.Common.Controls.Status.Poll.VoteCount.multiple(count) + } + } + }() + + cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed + + + cell.statusView.pollTableView.allowsSelection = !poll.expired + + let votedOptions = poll.options.filter { option in + (option.votedBy ?? Set()).map(\.id).contains(requestUserID) + } + let didVotedLocal = !votedOptions.isEmpty + let didVotedRemote = (poll.votedBy ?? Set()).map(\.id).contains(requestUserID) + cell.statusView.pollVoteButton.isEnabled = didVotedLocal + cell.statusView.pollVoteButton.isHidden = !poll.multiple ? true : (didVotedRemote || poll.expired) + + cell.statusView.pollTableViewDataSource = PollSection.tableViewDiffableDataSource( + for: cell.statusView.pollTableView, + managedObjectContext: managedObjectContext + ) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + + let pollItems = poll.options + .sorted(by: { $0.index.intValue < $1.index.intValue }) + .map { option -> PollItem in + let attribute: PollItem.Attribute = { + let selectState: PollItem.Attribute.SelectState = { + // check didVotedRemote later to make the local change possible + if !votedOptions.isEmpty { + return votedOptions.contains(option) ? .on : .off + } else if poll.expired { + return .none + } else if didVotedRemote, votedOptions.isEmpty { + return .none + } else { + return .off + } + }() + let voteState: PollItem.Attribute.VoteState = { + var needsReveal: Bool + if poll.expired { + needsReveal = true + } else if didVotedRemote { + needsReveal = true + } else { + needsReveal = false + } + guard needsReveal else { return .hidden } + let percentage: Double = { + guard poll.votesCount.intValue > 0 else { return 0.0 } + return Double(option.votesCount?.intValue ?? 0) / Double(poll.votesCount.intValue) + }() + let voted = votedOptions.isEmpty ? true : votedOptions.contains(option) + return .reveal(voted: voted, percentage: percentage, animated: updateProgressAnimated) + }() + return PollItem.Attribute(selectState: selectState, voteState: voteState) + }() + let option = PollItem.opion(objectID: option.objectID, attribute: attribute) + return option + } + snapshot.appendItems(pollItems, toSection: .main) + cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) + } + + static func configureEmptyStateHeader( + cell: TimelineHeaderTableViewCell, + attribute: Item.EmptyStateHeaderAttribute + ) { + cell.timelineHeaderView.iconImageView.image = attribute.reason.iconImage + cell.timelineHeaderView.messageLabel.text = attribute.reason.message + } +} + +extension NotificationSection { + private static func formattedNumberTitleForActionButton(_ number: Int?) -> String { + guard let number = number, number > 0 else { return "" } + return String(number) + } +} diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index ef7ae929..a9963ce9 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -53,6 +53,7 @@ internal enum Asset { internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background") } internal enum Border { + internal static let notification = ColorAsset(name: "Colors/Border/notification") internal static let searchCard = ColorAsset(name: "Colors/Border/searchCard") } internal enum Button { diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Border/notification.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Border/notification.colorset/Contents.json new file mode 100644 index 00000000..afc18df1 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Border/notification.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE8", + "green" : "0xE1", + "red" : "0xD9" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "60", + "green" : "58", + "red" : "58" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index b8717292..5a700927 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -8,6 +8,8 @@ import UIKit import Combine import OSLog +import CoreData +import CoreDataStack final class NotificationViewController: UIViewController, NeedsDependency { @@ -58,7 +60,7 @@ extension NotificationViewController { tableView.delegate = self viewModel.tableView = tableView viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self - viewModel.setupDiffableDataSource(for: tableView) + viewModel.setupDiffableDataSource(for: tableView, delegate: self, dependency: self) viewModel.viewDidLoad.send() // bind refresh control viewModel.isFetchingLatestNotification @@ -125,10 +127,6 @@ extension NotificationViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { return 68 } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - 68 - } } // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate @@ -138,6 +136,12 @@ extension NotificationViewController: ContentOffsetAdjustableTimelineViewControl } } +extension NotificationViewController: NotificationTableViewCellDelegate { + func parent() -> UIViewController { + self + } +} + //// MARK: - UIScrollViewDelegate //extension NotificationViewController { // func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index 1d77d41b..c9c0dcf6 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -13,17 +13,24 @@ import CoreDataStack extension NotificationViewModel { func setupDiffableDataSource( - for tableView: UITableView + for tableView: UITableView, + delegate: NotificationTableViewCellDelegate, + dependency: NeedsDependency ) { let timestampUpdatePublisher = Timer.publish(every: 30.0, on: .main, in: .common) .autoconnect() .share() .eraseToAnyPublisher() - + guard let userid = activeMastodonAuthenticationBox.value?.userID else { + return + } diffableDataSource = NotificationSection.tableViewDiffableDataSource( for: tableView, timestampUpdatePublisher: timestampUpdatePublisher, - managedObjectContext: context.managedObjectContext + managedObjectContext: context.managedObjectContext, + delegate: delegate, + dependency: dependency, + requestUserID: userid ) } diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 1b489031..b8d74152 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -85,7 +85,12 @@ final class NotificationViewModel: NSObject { guard let self = self else { return } self.fetchedResultsController.fetchRequest.predicate = predicate do { + self.diffableDataSource?.defaultRowAnimation = .fade try self.fetchedResultsController.performFetch() + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in + guard let self = self else { return } + self.diffableDataSource?.defaultRowAnimation = .automatic + } } catch { assertionFailure(error.localizedDescription) } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index b252b76d..d7decfdd 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -5,17 +5,23 @@ // Created by sxiaojian on 2021/4/13. // +import Combine import Foundation import UIKit -import Combine +protocol NotificationTableViewCellDelegate: class { + var context: AppContext! { get } + + func parent() -> UIViewController +} final class NotificationTableViewCell: UITableViewCell { - static let actionImageBorderWidth: CGFloat = 2 var disposeBag = Set() + var delegate: NotificationTableViewCellDelegate? + let avatatImageView: UIImageView = { let imageView = UIImageView() imageView.layer.cornerRadius = 4 @@ -32,7 +38,7 @@ final class NotificationTableViewCell: UITableViewCell { let actionImageBackground: UIView = { let view = UIView() - view.layer.cornerRadius = (24 + NotificationTableViewCell.actionImageBorderWidth)/2 + view.layer.cornerRadius = (24 + NotificationTableViewCell.actionImageBorderWidth) / 2 view.layer.cornerCurve = .continuous view.clipsToBounds = true view.layer.borderWidth = NotificationTableViewCell.actionImageBorderWidth @@ -58,11 +64,30 @@ final class NotificationTableViewCell: UITableViewCell { }() var nameLabelTop: NSLayoutConstraint! + var nameLabelBottom: NSLayoutConstraint! + + let statusContainer: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.layer.cornerRadius = 6 + view.layer.borderWidth = 2 + view.layer.cornerCurve = .continuous + view.layer.borderColor = Asset.Colors.Border.notification.color.cgColor + view.clipsToBounds = true + return view + }() + + let statusView = StatusView() override func prepareForReuse() { super.prepareForReuse() avatatImageView.af.cancelImageRequest() - + statusView.isStatusTextSensitive = false + statusView.cleanUpContentWarning() + statusView.pollTableView.dataSource = nil + statusView.playerContainerView.reset() + statusView.playerContainerView.isHidden = true + disposeBag.removeAll() } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -74,11 +99,19 @@ final class NotificationTableViewCell: UITableViewCell { super.init(coder: coder) configure() } + + override func layoutSubviews() { + super.layoutSubviews() + DispatchQueue.main.async { + self.statusView.drawContentWarningImageView() + } + } } extension NotificationTableViewCell { - func configure() { + selectionStyle = .none + contentView.addSubview(avatatImageView) avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil) @@ -90,7 +123,8 @@ extension NotificationTableViewCell { actionImageBackground.addSubview(actionImageView) actionImageView.constrainToCenter() - nameLabelTop = nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor) + nameLabelTop = nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24) + nameLabelBottom = contentView.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24) contentView.addSubview(nameLabel) nameLabel.constrain([ nameLabelTop, @@ -100,21 +134,38 @@ extension NotificationTableViewCell { contentView.addSubview(actionLabel) actionLabel.constrain([ actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), - actionLabel.topAnchor.constraint(equalTo: nameLabel.topAnchor), - contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4) + actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), + contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) ]) + + statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color + } public func nameLabelLayoutIn(center: Bool) { if center { nameLabelTop.constant = 24 + NSLayoutConstraint.activate([nameLabelBottom]) + statusView.removeFromSuperview() + statusContainer.removeFromSuperview() } else { nameLabelTop.constant = 12 + NSLayoutConstraint.deactivate([nameLabelBottom]) + addStatusAndContainer() } } + + func addStatusAndContainer() { + contentView.addSubview(statusContainer) + statusContainer.pin(top: 40, left: 63, bottom: 14, right: 14) + + contentView.addSubview(statusView) + statusView.pin(top: 40 + 12, left: 63 + 12, bottom: 14 + 12, right: 14 + 12) + } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - - self.actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + statusContainer.layer.borderColor = Asset.Colors.Border.notification.color.cgColor + actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor } } From d99fb1af7610aa911eb7f397e11d4c40c84e5661 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 14 Apr 2021 15:56:06 +0800 Subject: [PATCH 04/23] chore: make segment work in notification --- .../Notification/NotificationViewController.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 5a700927..2c96c65d 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -10,6 +10,7 @@ import Combine import OSLog import CoreData import CoreDataStack +import MastodonSDK final class NotificationViewController: UIViewController, NeedsDependency { @@ -22,7 +23,6 @@ final class NotificationViewController: UIViewController, NeedsDependency { let segmentControl: UISegmentedControl = { let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything,L10n.Scene.Notification.Title.mentions]) control.selectedSegmentIndex = 0 - control.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .touchUpInside) return control }() @@ -33,6 +33,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) tableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self)) + tableView.tableFooterView = UIView() return tableView }() @@ -46,6 +47,7 @@ extension NotificationViewController { super.viewDidLoad() view.backgroundColor = Asset.Colors.Background.searchResult.color navigationItem.titleView = segmentControl + segmentControl.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .valueChanged) view.addSubview(tableView) tableView.constrain([ tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), @@ -111,6 +113,14 @@ extension NotificationViewController { extension NotificationViewController { @objc private func segmentedControlValueChanged(_ sender: UISegmentedControl) { os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, sender.selectedSegmentIndex) + guard let domain = viewModel.activeMastodonAuthenticationBox.value?.domain else { + return + } + if sender.selectedSegmentIndex == 0 { + viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain) + } else { + viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, type: Mastodon.Entity.Notification.NotificationType.mention.rawValue) + } } @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { From 288a8025ced0af0f78e1472067e819e039303b84 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 14 Apr 2021 16:24:40 +0800 Subject: [PATCH 05/23] chore: use NotificationStatusTableViewCell and NotificationTableViewCell --- Mastodon.xcodeproj/project.pbxproj | 4 + .../Section/NotificationSection.swift | 81 ++++++---- .../NotificationViewController.swift | 1 + .../NotificationStatusTableViewCell.swift | 147 ++++++++++++++++++ .../NotificationTableViewCell.swift | 59 +------ 5 files changed, 206 insertions(+), 86 deletions(-) create mode 100644 Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b2258c1d..150c29af 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; 2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; }; 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; }; + 2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */; }; 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; }; 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; }; 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; }; @@ -431,6 +432,7 @@ 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = ""; }; 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; + 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusTableViewCell.swift; sourceTree = ""; }; 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; @@ -898,6 +900,7 @@ isa = PBXGroup; children = ( 2D35238026256F690031AF25 /* NotificationTableViewCell.swift */, + 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */, ); path = TableViewCell; sourceTree = ""; @@ -2363,6 +2366,7 @@ DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, + 2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */, DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */, DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 09f0f87a..de7f5d6c 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -31,11 +31,12 @@ extension NotificationSection { guard let dependency = dependency else { return nil } switch notificationItem { case .notification(let objectID): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell - cell.delegate = delegate + let notification = managedObjectContext.object(with: objectID) as! MastodonNotification let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.type) + let timeText = notification.createAt.shortTimeAgoSinceNow + var actionText: String var actionImageName: String var color: UIColor @@ -66,39 +67,59 @@ extension NotificationSection { color = .clear } - timestampUpdatePublisher - .sink { _ in - let timeText = notification.createAt.shortTimeAgoSinceNow - cell.actionLabel.text = actionText + " · " + timeText - } - .store(in: &cell.disposeBag) - let timeText = notification.createAt.shortTimeAgoSinceNow - cell.actionImageBackground.backgroundColor = color - cell.actionLabel.text = actionText + " · " + timeText - cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName - cell.avatatImageView.af.setImage( - withURL: URL(string: notification.account.avatar)!, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) - - if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { - cell.actionImageView.image = actionImage - } if let status = notification.status { - let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - 61 - 2, height: tableView.readableContentGuide.layoutFrame.height) + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell + cell.delegate = delegate NotificationSection.configure(cell: cell, dependency: dependency, - readableLayoutFrame: frame, + readableLayoutFrame: nil, timestampUpdatePublisher: timestampUpdatePublisher, status: status, - requestUserID: "", + requestUserID: requestUserID, statusItemAttribute: Item.StatusAttribute(isStatusTextSensitive: false, isStatusSensitive: false)) - cell.nameLabelLayoutIn(center: false) + timestampUpdatePublisher + .sink { _ in + let timeText = notification.createAt.shortTimeAgoSinceNow + cell.actionLabel.text = actionText + " · " + timeText + } + .store(in: &cell.disposeBag) + cell.actionImageBackground.backgroundColor = color + cell.actionLabel.text = actionText + " · " + timeText + cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName + cell.avatatImageView.af.setImage( + withURL: URL(string: notification.account.avatar)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + + if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { + cell.actionImageView.image = actionImage + } + return cell + } else { - cell.nameLabelLayoutIn(center: true) + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell + cell.delegate = delegate + timestampUpdatePublisher + .sink { _ in + let timeText = notification.createAt.shortTimeAgoSinceNow + cell.actionLabel.text = actionText + " · " + timeText + } + .store(in: &cell.disposeBag) + cell.actionImageBackground.backgroundColor = color + cell.actionLabel.text = actionText + " · " + timeText + cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName + cell.avatatImageView.af.setImage( + withURL: URL(string: notification.account.avatar)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + + if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { + cell.actionImageView.image = actionImage + } + return cell } - return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader cell.startAnimating() @@ -111,7 +132,7 @@ extension NotificationSection { extension NotificationSection { static func configure( - cell: NotificationTableViewCell, + cell: NotificationStatusTableViewCell, dependency: NeedsDependency, readableLayoutFrame: CGRect?, timestampUpdatePublisher: AnyPublisher, @@ -317,7 +338,7 @@ extension NotificationSection { } static func configureHeader( - cell: NotificationTableViewCell, + cell: NotificationStatusTableViewCell, status: Status ) { if status.reblog != nil { @@ -343,7 +364,7 @@ extension NotificationSection { static func configurePoll( - cell: NotificationTableViewCell, + cell: NotificationStatusTableViewCell, poll: Poll?, requestUserID: String, updateProgressAnimated: Bool, diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 2c96c65d..d047a0a5 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -32,6 +32,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { tableView.separatorStyle = .singleLine tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) + tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self)) tableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self)) tableView.tableFooterView = UIView() return tableView diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift new file mode 100644 index 00000000..1a0df283 --- /dev/null +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -0,0 +1,147 @@ +// +// NotificationStatusTableViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/14. +// + +import Combine +import Foundation +import UIKit + +final class NotificationStatusTableViewCell: UITableViewCell { + static let actionImageBorderWidth: CGFloat = 2 + + var disposeBag = Set() + + var delegate: NotificationTableViewCellDelegate? + + let avatatImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 4 + imageView.layer.cornerCurve = .continuous + imageView.clipsToBounds = true + return imageView + }() + + let actionImageView: UIImageView = { + let imageView = UIImageView() + imageView.tintColor = Asset.Colors.Background.searchResult.color + return imageView + }() + + let actionImageBackground: UIView = { + let view = UIView() + view.layer.cornerRadius = (24 + NotificationStatusTableViewCell.actionImageBorderWidth) / 2 + view.layer.cornerCurve = .continuous + view.clipsToBounds = true + view.layer.borderWidth = NotificationStatusTableViewCell.actionImageBorderWidth + view.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + view.tintColor = Asset.Colors.Background.searchResult.color + return view + }() + + let actionLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.Label.secondary.color + label.font = UIFont.preferredFont(forTextStyle: .body) + label.lineBreakMode = .byTruncatingTail + return label + }() + + let nameLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.brandBlue.color + label.font = .systemFont(ofSize: 15, weight: .semibold) + label.lineBreakMode = .byTruncatingTail + return label + }() + + let statusContainer: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.layer.cornerRadius = 6 + view.layer.borderWidth = 2 + view.layer.cornerCurve = .continuous + view.layer.borderColor = Asset.Colors.Border.notification.color.cgColor + view.clipsToBounds = true + return view + }() + + let statusView = StatusView() + + override func prepareForReuse() { + super.prepareForReuse() + avatatImageView.af.cancelImageRequest() + statusView.isStatusTextSensitive = false + statusView.cleanUpContentWarning() + statusView.pollTableView.dataSource = nil + statusView.playerContainerView.reset() + statusView.playerContainerView.isHidden = true + disposeBag.removeAll() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + override func layoutSubviews() { + super.layoutSubviews() + DispatchQueue.main.async { + self.statusView.drawContentWarningImageView() + } + } +} + +extension NotificationStatusTableViewCell { + func configure() { + selectionStyle = .none + + contentView.addSubview(avatatImageView) + avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) + avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil) + + contentView.addSubview(actionImageBackground) + actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationStatusTableViewCell.actionImageBorderWidth, height: 24 + NotificationStatusTableViewCell.actionImageBorderWidth)) + actionImageBackground.pin(top: 33, left: 33, bottom: nil, right: nil) + + actionImageBackground.addSubview(actionImageView) + actionImageView.constrainToCenter() + + contentView.addSubview(nameLabel) + nameLabel.constrain([ + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 61) + ]) + + contentView.addSubview(actionLabel) + actionLabel.constrain([ + actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), + actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), + contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) + ]) + + statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color + addStatusAndContainer() + } + + func addStatusAndContainer() { + contentView.addSubview(statusContainer) + statusContainer.pin(top: 40, left: 63, bottom: 14, right: 14) + + contentView.addSubview(statusView) + statusView.pin(top: 40 + 12, left: 63 + 12, bottom: 14 + 12, right: 14 + 12) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + statusContainer.layer.borderColor = Asset.Colors.Border.notification.color.cgColor + actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + } +} diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index d7decfdd..5124dab6 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -63,30 +63,9 @@ final class NotificationTableViewCell: UITableViewCell { return label }() - var nameLabelTop: NSLayoutConstraint! - var nameLabelBottom: NSLayoutConstraint! - - let statusContainer: UIView = { - let view = UIView() - view.backgroundColor = .clear - view.layer.cornerRadius = 6 - view.layer.borderWidth = 2 - view.layer.cornerCurve = .continuous - view.layer.borderColor = Asset.Colors.Border.notification.color.cgColor - view.clipsToBounds = true - return view - }() - - let statusView = StatusView() - override func prepareForReuse() { super.prepareForReuse() avatatImageView.af.cancelImageRequest() - statusView.isStatusTextSensitive = false - statusView.cleanUpContentWarning() - statusView.pollTableView.dataSource = nil - statusView.playerContainerView.reset() - statusView.playerContainerView.isHidden = true disposeBag.removeAll() } @@ -99,13 +78,6 @@ final class NotificationTableViewCell: UITableViewCell { super.init(coder: coder) configure() } - - override func layoutSubviews() { - super.layoutSubviews() - DispatchQueue.main.async { - self.statusView.drawContentWarningImageView() - } - } } extension NotificationTableViewCell { @@ -122,12 +94,11 @@ extension NotificationTableViewCell { actionImageBackground.addSubview(actionImageView) actionImageView.constrainToCenter() - - nameLabelTop = nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24) - nameLabelBottom = contentView.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24) + contentView.addSubview(nameLabel) nameLabel.constrain([ - nameLabelTop, + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24), + contentView.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24), nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 61) ]) @@ -137,35 +108,11 @@ extension NotificationTableViewCell { actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) ]) - - statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color - } - public func nameLabelLayoutIn(center: Bool) { - if center { - nameLabelTop.constant = 24 - NSLayoutConstraint.activate([nameLabelBottom]) - statusView.removeFromSuperview() - statusContainer.removeFromSuperview() - } else { - nameLabelTop.constant = 12 - NSLayoutConstraint.deactivate([nameLabelBottom]) - addStatusAndContainer() - } - } - - func addStatusAndContainer() { - contentView.addSubview(statusContainer) - statusContainer.pin(top: 40, left: 63, bottom: 14, right: 14) - - contentView.addSubview(statusView) - statusView.pin(top: 40 + 12, left: 63 + 12, bottom: 14 + 12, right: 14 + 12) - } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - statusContainer.layer.borderColor = Asset.Colors.Border.notification.color.cgColor actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor } } From 2d71a48e36f7ff18953198361f7fed1aeee968e3 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 14 Apr 2021 17:13:23 +0800 Subject: [PATCH 06/23] chore: fix order of notification and layout issue --- CoreDataStack/Entity/Notification.swift | 2 +- Mastodon/Diffiable/Section/NotificationSection.swift | 3 ++- Mastodon/Scene/Notification/NotificationViewController.swift | 5 ++--- .../TableViewCell/NotificationStatusTableViewCell.swift | 3 ++- Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift | 3 +++ Mastodon/Service/APIService/APIService+Notification.swift | 2 +- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CoreDataStack/Entity/Notification.swift b/CoreDataStack/Entity/Notification.swift index 144ad9c2..8a0595f6 100644 --- a/CoreDataStack/Entity/Notification.swift +++ b/CoreDataStack/Entity/Notification.swift @@ -95,6 +95,6 @@ extension MastodonNotification { extension MastodonNotification: Managed { public static var defaultSortDescriptors: [NSSortDescriptor] { - return [NSSortDescriptor(keyPath: \MastodonNotification.updatedAt, ascending: false)] + return [NSSortDescriptor(keyPath: \MastodonNotification.createAt, ascending: false)] } } diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index de7f5d6c..77e7ab5a 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -70,9 +70,10 @@ extension NotificationSection { if let status = notification.status { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell cell.delegate = delegate + let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right, height: tableView.readableContentGuide.layoutFrame.height) NotificationSection.configure(cell: cell, dependency: dependency, - readableLayoutFrame: nil, + readableLayoutFrame: frame, timestampUpdatePublisher: timestampUpdatePublisher, status: status, requestUserID: requestUserID, diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index d047a0a5..0935c6e8 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -35,6 +35,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self)) tableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self)) tableView.tableFooterView = UIView() + tableView.rowHeight = UITableView.automaticDimension return tableView }() @@ -135,9 +136,7 @@ extension NotificationViewController { // MARK: - UITableViewDelegate extension NotificationViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - return 68 - } + } // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 1a0df283..2b3b6972 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -12,6 +12,7 @@ import UIKit final class NotificationStatusTableViewCell: UITableViewCell { static let actionImageBorderWidth: CGFloat = 2 + static let statusPadding: UIEdgeInsets = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24) var disposeBag = Set() var delegate: NotificationTableViewCellDelegate? @@ -136,7 +137,7 @@ extension NotificationStatusTableViewCell { statusContainer.pin(top: 40, left: 63, bottom: 14, right: 14) contentView.addSubview(statusView) - statusView.pin(top: 40 + 12, left: 63 + 12, bottom: 14 + 12, right: 14 + 12) + statusView.pin(top: NotificationStatusTableViewCell.statusPadding.top, left: NotificationStatusTableViewCell.statusPadding.left, bottom: NotificationStatusTableViewCell.statusPadding.bottom, right: NotificationStatusTableViewCell.statusPadding.right) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { diff --git a/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift b/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift index 7ab18bb0..923a0630 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift @@ -43,5 +43,8 @@ final class SearchBottomLoader: UITableViewCell { backgroundColor = Asset.Colors.Background.systemGroupedBackground.color contentView.addSubview(activityIndicatorView) activityIndicatorView.constrainToCenter() + NSLayoutConstraint.activate([ + contentView.heightAnchor.constraint(equalToConstant: 44) + ]) } } diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift index 84cb6d3c..53595fa9 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/Mastodon/Service/APIService/APIService+Notification.swift @@ -43,7 +43,7 @@ extension APIService { status = statusInCoreData } // use constrain to avoid repeated save - let notification = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, property: MastodonNotification.Property(id: notification.id, type: notification.type.rawValue, account: mastodonUser, status: status, createdAt: Date())) + let notification = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, property: MastodonNotification.Property(id: notification.id, type: notification.type.rawValue, account: mastodonUser, status: status, createdAt: notification.createdAt)) os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)", (#file as NSString).lastPathComponent, #line, #function, notification.type, notification.account.username) } From f3394ff38243db28dccf6705e58c0ef490491557 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 14 Apr 2021 17:37:58 +0800 Subject: [PATCH 07/23] feat: add navigation to Notification Cell --- Mastodon.xcodeproj/project.pbxproj | 4 + .../Diffiable/Item/NotificationItem.swift | 2 +- .../Section/NotificationSection.swift | 27 +++++- Mastodon/Extension/UIView+Gesture.swift | 93 +++++++++++++++++++ .../NotificationViewController.swift | 29 +++++- .../NotificationViewModel+diffable.swift | 2 +- .../Notification/NotificationViewModel.swift | 4 +- .../NotificationStatusTableViewCell.swift | 2 +- .../NotificationTableViewCell.swift | 3 + 9 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 Mastodon/Extension/UIView+Gesture.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 150c29af..f545b513 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; }; 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; }; 2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */; }; + 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; }; 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; }; 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; }; 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; }; @@ -433,6 +434,7 @@ 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = ""; }; 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusTableViewCell.swift; sourceTree = ""; }; + 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = ""; }; 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; @@ -1615,6 +1617,7 @@ 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, + 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */, 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, 2D84350425FF858100EECE90 /* UIScrollView.swift */, DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */, @@ -2264,6 +2267,7 @@ 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */, 2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */, DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */, + 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, 2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */, DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/NotificationItem.swift b/Mastodon/Diffiable/Item/NotificationItem.swift index e4a53d2b..6ef5b0c8 100644 --- a/Mastodon/Diffiable/Item/NotificationItem.swift +++ b/Mastodon/Diffiable/Item/NotificationItem.swift @@ -10,7 +10,7 @@ import CoreData enum NotificationItem { - case notification(ObjectID: NSManagedObjectID) + case notification(objectID: NSManagedObjectID) case bottomLoader } diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 77e7ab5a..74b5d6a4 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -92,7 +92,10 @@ extension NotificationSection { placeholderImage: UIImage.placeholder(color: .systemFill), imageTransition: .crossDissolve(0.2) ) - + cell.avatatImageView.gesture().sink { [weak cell] _ in + cell?.delegate?.userAvatarDidPressed(notification: notification) + } + .store(in: &cell.disposeBag) if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { cell.actionImageView.image = actionImage } @@ -115,7 +118,10 @@ extension NotificationSection { placeholderImage: UIImage.placeholder(color: .systemFill), imageTransition: .crossDissolve(0.2) ) - + cell.avatatImageView.gesture().sink { [weak cell] _ in + cell?.delegate?.userAvatarDidPressed(notification: notification) + } + .store(in: &cell.disposeBag) if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { cell.actionImageView.image = actionImage } @@ -399,9 +405,20 @@ extension NotificationSection { } } }() - - cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed - + if poll.expired { + cell.pollCountdownSubscription = nil + cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed + } else if let expiresAt = poll.expiresAt { + cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow) + cell.pollCountdownSubscription = timestampUpdatePublisher + .sink { _ in + cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow) + } + } else { + // assertionFailure() + cell.pollCountdownSubscription = nil + cell.statusView.pollCountdownLabel.text = "-" + } cell.statusView.pollTableView.allowsSelection = !poll.expired diff --git a/Mastodon/Extension/UIView+Gesture.swift b/Mastodon/Extension/UIView+Gesture.swift new file mode 100644 index 00000000..9c29c2c0 --- /dev/null +++ b/Mastodon/Extension/UIView+Gesture.swift @@ -0,0 +1,93 @@ +// +// UIView+Gesture.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/14. +// + +import Combine +import Foundation +import UIKit + +struct GesturePublisher: Publisher { + typealias Output = GestureType + typealias Failure = Never + private let view: UIView + private let gestureType: GestureType + init(view: UIView, gestureType: GestureType) { + self.view = view + self.gestureType = gestureType + } + + func receive(subscriber: S) where S: Subscriber, + GesturePublisher.Failure == S.Failure, GesturePublisher.Output + == S.Input + { + let subscription = GestureSubscription( + subscriber: subscriber, + view: view, + gestureType: gestureType + ) + subscriber.receive(subscription: subscription) + } +} + +enum GestureType { + case tap(UITapGestureRecognizer = .init()) + case swipe(UISwipeGestureRecognizer = .init()) + case longPress(UILongPressGestureRecognizer = .init()) + case pan(UIPanGestureRecognizer = .init()) + case pinch(UIPinchGestureRecognizer = .init()) + case edge(UIScreenEdgePanGestureRecognizer = .init()) + func get() -> UIGestureRecognizer { + switch self { + case let .tap(tapGesture): + return tapGesture + case let .swipe(swipeGesture): + return swipeGesture + case let .longPress(longPressGesture): + return longPressGesture + case let .pan(panGesture): + return panGesture + case let .pinch(pinchGesture): + return pinchGesture + case let .edge(edgePanGesture): + return edgePanGesture + } + } +} + +class GestureSubscription: Subscription where S.Input == GestureType, S.Failure == Never { + private var subscriber: S? + private var gestureType: GestureType + private var view: UIView + init(subscriber: S, view: UIView, gestureType: GestureType) { + self.subscriber = subscriber + self.view = view + self.gestureType = gestureType + configureGesture(gestureType) + } + + private func configureGesture(_ gestureType: GestureType) { + let gesture = gestureType.get() + gesture.addTarget(self, action: #selector(handler)) + view.addGestureRecognizer(gesture) + } + + func request(_ demand: Subscribers.Demand) {} + func cancel() { + subscriber = nil + } + + @objc + private func handler() { + _ = subscriber?.receive(gestureType) + } +} + +extension UIView { + func gesture(_ gestureType: GestureType = .tap()) -> GesturePublisher { + self.isUserInteractionEnabled = true + return GesturePublisher(view: self, gestureType: gestureType) + } +} diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 0935c6e8..86c21a2c 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -18,7 +18,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - private(set) lazy var viewModel = NotificationViewModel(context: context, coordinator: coordinator) + private(set) lazy var viewModel = NotificationViewModel(context: context) let segmentControl: UISegmentedControl = { let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything,L10n.Scene.Notification.Title.mentions]) @@ -136,6 +136,24 @@ extension NotificationViewController { // MARK: - UITableViewDelegate extension NotificationViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + switch item { + case .notification(let objectID): + let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification + if notification.status != nil { + // TODO goto status detail vc + } else { + let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: notification.account) + DispatchQueue.main.async { + self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show) + } + } + default: + break + } + } } @@ -147,9 +165,18 @@ extension NotificationViewController: ContentOffsetAdjustableTimelineViewControl } extension NotificationViewController: NotificationTableViewCellDelegate { + func userAvatarDidPressed(notification: MastodonNotification) { + let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: notification.account) + DispatchQueue.main.async { + self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show) + } + } + func parent() -> UIViewController { self } + + } //// MARK: - UIScrollViewDelegate diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index c9c0dcf6..2d68586d 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -71,7 +71,7 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { var newSnapshot = NSDiffableDataSourceSnapshot() newSnapshot.appendSections([.main]) - newSnapshot.appendItems(notifications.map({NotificationItem.notification(ObjectID: $0.objectID)}), toSection: .main) + newSnapshot.appendItems(notifications.map({NotificationItem.notification(objectID: $0.objectID)}), toSection: .main) if !notifications.isEmpty { newSnapshot.appendItems([.bottomLoader], toSection: .main) } diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index b8d74152..20bec715 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -19,7 +19,6 @@ final class NotificationViewModel: NSObject { // input let context: AppContext - weak var coordinator: SceneCoordinator! weak var tableView: UITableView? weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? @@ -49,8 +48,7 @@ final class NotificationViewModel: NSObject { lazy var loadLatestStateMachinePublisher = CurrentValueSubject(nil) - init(context: AppContext,coordinator: SceneCoordinator) { - self.coordinator = coordinator + init(context: AppContext) { self.context = context self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) self.fetchedResultsController = { diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 2b3b6972..4731b4e1 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -14,7 +14,7 @@ final class NotificationStatusTableViewCell: UITableViewCell { static let statusPadding: UIEdgeInsets = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24) var disposeBag = Set() - + var pollCountdownSubscription: AnyCancellable? var delegate: NotificationTableViewCellDelegate? let avatatImageView: UIImageView = { diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 5124dab6..1068e773 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -8,11 +8,14 @@ import Combine import Foundation import UIKit +import CoreDataStack protocol NotificationTableViewCellDelegate: class { var context: AppContext! { get } func parent() -> UIViewController + + func userAvatarDidPressed(notification:MastodonNotification) } final class NotificationTableViewCell: UITableViewCell { From bffb0a887b2b6a77752601676aeb36d62ecd9a3f Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 14 Apr 2021 18:00:43 +0800 Subject: [PATCH 08/23] chore: rename searchBottomLoader , rename pure color --- Mastodon.xcodeproj/project.pbxproj | 8 ++++---- Mastodon/Diffiable/Section/NotificationSection.swift | 7 +++++-- Mastodon/Diffiable/Section/SearchResultSection.swift | 2 +- Mastodon/Generated/Assets.swift | 2 +- .../Contents.json | 0 .../Scene/Notification/NotificationViewController.swift | 4 ++-- .../TableViewCell/NotificationStatusTableViewCell.swift | 8 ++++---- .../TableViewCell/NotificationTableViewCell.swift | 8 ++++---- .../Scene/Search/SearchViewController+Searching.swift | 2 +- Mastodon/Scene/Search/SearchViewController.swift | 4 ++-- ...{SearchBottomLoader.swift => CommonBottomLoader.swift} | 4 ++-- 11 files changed, 26 insertions(+), 23 deletions(-) rename Mastodon/Resources/Assets.xcassets/Colors/Background/{searchResult.colorset => pure.colorset}/Contents.json (100%) rename Mastodon/Scene/Search/TableViewCell/{SearchBottomLoader.swift => CommonBottomLoader.swift} (94%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f545b513..14de9fc4 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -37,7 +37,7 @@ 2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; - 2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */; }; + 2D19864F261C372A00F0B013 /* CommonBottomLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D19864E261C372A00F0B013 /* CommonBottomLoader.swift */; }; 2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */; }; 2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; }; 2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; }; @@ -426,7 +426,7 @@ 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; - 2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBottomLoader.swift; sourceTree = ""; }; + 2D19864E261C372A00F0B013 /* CommonBottomLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonBottomLoader.swift; sourceTree = ""; }; 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+LoadOldestState.swift"; sourceTree = ""; }; 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = ""; }; 2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; @@ -1152,7 +1152,7 @@ isa = PBXGroup; children = ( 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */, - 2D19864E261C372A00F0B013 /* SearchBottomLoader.swift */, + 2D19864E261C372A00F0B013 /* CommonBottomLoader.swift */, ); path = TableViewCell; sourceTree = ""; @@ -2269,7 +2269,7 @@ DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */, 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, - 2D19864F261C372A00F0B013 /* SearchBottomLoader.swift in Sources */, + 2D19864F261C372A00F0B013 /* CommonBottomLoader.swift in Sources */, DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */, 0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 74b5d6a4..01273e9f 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -128,7 +128,7 @@ extension NotificationSection { return cell } case .bottomLoader: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CommonBottomLoader.self)) as! CommonBottomLoader cell.startAnimating() return cell } @@ -149,8 +149,11 @@ extension NotificationSection { ) { // disable interaction cell.statusView.isUserInteractionEnabled = false - // remove actionToolBar + // remove item don't display cell.statusView.actionToolbarContainer.removeFromSuperview() + cell.statusView.avatarView.removeFromSuperview() + + // setup attribute statusItemAttribute.setupForStatus(status: status) diff --git a/Mastodon/Diffiable/Section/SearchResultSection.swift b/Mastodon/Diffiable/Section/SearchResultSection.swift index 50c56160..e01063c8 100644 --- a/Mastodon/Diffiable/Section/SearchResultSection.swift +++ b/Mastodon/Diffiable/Section/SearchResultSection.swift @@ -44,7 +44,7 @@ extension SearchResultSection { cell.config(with: user) return cell case .bottomLoader: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchBottomLoader.self)) as! SearchBottomLoader + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CommonBottomLoader.self)) as! CommonBottomLoader cell.startAnimating() return cell } diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index a9963ce9..40eda801 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -44,7 +44,7 @@ internal enum Asset { internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor") internal static let navigationBar = ColorAsset(name: "Colors/Background/navigationBar") internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background") - internal static let searchResult = ColorAsset(name: "Colors/Background/searchResult") + internal static let pure = ColorAsset(name: "Colors/Background/pure") internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background") internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background") internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/searchResult.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/pure.colorset/Contents.json similarity index 100% rename from Mastodon/Resources/Assets.xcassets/Colors/Background/searchResult.colorset/Contents.json rename to Mastodon/Resources/Assets.xcassets/Colors/Background/pure.colorset/Contents.json diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 86c21a2c..8d04674f 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -33,7 +33,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self)) - tableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self)) + tableView.register(CommonBottomLoader.self, forCellReuseIdentifier: String(describing: CommonBottomLoader.self)) tableView.tableFooterView = UIView() tableView.rowHeight = UITableView.automaticDimension return tableView @@ -47,7 +47,7 @@ extension NotificationViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = Asset.Colors.Background.searchResult.color + view.backgroundColor = Asset.Colors.Background.pure.color navigationItem.titleView = segmentControl segmentControl.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .valueChanged) view.addSubview(tableView) diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 4731b4e1..37b74599 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -27,7 +27,7 @@ final class NotificationStatusTableViewCell: UITableViewCell { let actionImageView: UIImageView = { let imageView = UIImageView() - imageView.tintColor = Asset.Colors.Background.searchResult.color + imageView.tintColor = Asset.Colors.Background.pure.color return imageView }() @@ -37,8 +37,8 @@ final class NotificationStatusTableViewCell: UITableViewCell { view.layer.cornerCurve = .continuous view.clipsToBounds = true view.layer.borderWidth = NotificationStatusTableViewCell.actionImageBorderWidth - view.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor - view.tintColor = Asset.Colors.Background.searchResult.color + view.layer.borderColor = Asset.Colors.Background.pure.color.cgColor + view.tintColor = Asset.Colors.Background.pure.color return view }() @@ -143,6 +143,6 @@ extension NotificationStatusTableViewCell { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) statusContainer.layer.borderColor = Asset.Colors.Border.notification.color.cgColor - actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + actionImageBackground.layer.borderColor = Asset.Colors.Background.pure.color.cgColor } } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 1068e773..a10ff01d 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -35,7 +35,7 @@ final class NotificationTableViewCell: UITableViewCell { let actionImageView: UIImageView = { let imageView = UIImageView() - imageView.tintColor = Asset.Colors.Background.searchResult.color + imageView.tintColor = Asset.Colors.Background.pure.color return imageView }() @@ -45,8 +45,8 @@ final class NotificationTableViewCell: UITableViewCell { view.layer.cornerCurve = .continuous view.clipsToBounds = true view.layer.borderWidth = NotificationTableViewCell.actionImageBorderWidth - view.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor - view.tintColor = Asset.Colors.Background.searchResult.color + view.layer.borderColor = Asset.Colors.Background.pure.color.cgColor + view.tintColor = Asset.Colors.Background.pure.color return view }() @@ -116,6 +116,6 @@ extension NotificationTableViewCell { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - actionImageBackground.layer.borderColor = Asset.Colors.Background.searchResult.color.cgColor + actionImageBackground.layer.borderColor = Asset.Colors.Background.pure.color.cgColor } } diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift index 3eb9793a..43e5d397 100644 --- a/Mastodon/Scene/Search/SearchViewController+Searching.swift +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -17,7 +17,7 @@ extension SearchViewController { func setupSearchingTableView() { searchingTableView.delegate = self searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self)) - searchingTableView.register(SearchBottomLoader.self, forCellReuseIdentifier: String(describing: SearchBottomLoader.self)) + searchingTableView.register(CommonBottomLoader.self, forCellReuseIdentifier: String(describing: CommonBottomLoader.self)) view.addSubview(searchingTableView) searchingTableView.constrain([ searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor), diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 5dcc47e0..dc941458 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -82,7 +82,7 @@ final class SearchViewController: UIViewController, NeedsDependency { // searching let searchingTableView: UITableView = { let tableView = UITableView() - tableView.backgroundColor = Asset.Colors.Background.searchResult.color + tableView.backgroundColor = Asset.Colors.Background.pure.color tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .singleLine tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) @@ -227,7 +227,7 @@ extension SearchViewController: UISearchBarDelegate { } extension SearchViewController: LoadMoreConfigurableTableViewContainer { - typealias BottomLoaderTableViewCell = SearchBottomLoader + typealias BottomLoaderTableViewCell = CommonBottomLoader typealias LoadingState = SearchViewModel.LoadOldestState.Loading var loadMoreConfigurableTableView: UITableView { searchingTableView } var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine } diff --git a/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift b/Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift similarity index 94% rename from Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift rename to Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift index 923a0630..bb2ae9ce 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchBottomLoader.swift +++ b/Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift @@ -1,5 +1,5 @@ // -// SearchBottomLoader.swift +// CommonBottomLoader.swift // Mastodon // // Created by sxiaojian on 2021/4/6. @@ -8,7 +8,7 @@ import Foundation import UIKit -final class SearchBottomLoader: UITableViewCell { +final class CommonBottomLoader: UITableViewCell { let activityIndicatorView: UIActivityIndicatorView = { let activityIndicatorView = UIActivityIndicatorView(style: .medium) activityIndicatorView.tintColor = Asset.Colors.Label.primary.color From 687614d43a93708064b9f05faa57d5a9503f5c1a Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 14 Apr 2021 19:04:11 +0800 Subject: [PATCH 09/23] feat: add bottom loader --- Mastodon.xcodeproj/project.pbxproj | 4 + .../NotificationViewController.swift | 28 ++-- ...otificationViewModel+LoadOldestState.swift | 128 ++++++++++++++++++ .../Notification/NotificationViewModel.swift | 16 +++ 4 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 14de9fc4..d088d317 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; }; 2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */; }; 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; }; + 2D24E12D2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E12C2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift */; }; 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; }; 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; }; 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; }; @@ -435,6 +436,7 @@ 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; 2D24E11C2626D8B100A59D4F /* NotificationStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusTableViewCell.swift; sourceTree = ""; }; 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = ""; }; + 2D24E12C2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+LoadOldestState.swift"; sourceTree = ""; }; 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; @@ -1679,6 +1681,7 @@ 2D607AD726242FC500B70763 /* NotificationViewModel.swift */, 2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */, 2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */, + 2D24E12C2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift */, 2D35237F26256F470031AF25 /* TableViewCell */, ); path = Notification; @@ -2401,6 +2404,7 @@ 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */, 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, + 2D24E12D2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 8d04674f..d5eb149a 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -11,6 +11,7 @@ import OSLog import CoreData import CoreDataStack import MastodonSDK +import GameplayKit final class NotificationViewController: UIViewController, NeedsDependency { @@ -123,6 +124,7 @@ extension NotificationViewController { } else { viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, type: Mastodon.Entity.Notification.NotificationType.mention.rawValue) } + viewModel.selectedIndex.value = sender.selectedSegmentIndex } @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { @@ -179,16 +181,16 @@ extension NotificationViewController: NotificationTableViewCellDelegate { } -//// MARK: - UIScrollViewDelegate -//extension NotificationViewController { -// func scrollViewDidScroll(_ scrollView: UIScrollView) { -// handleScrollViewDidScroll(scrollView) -// } -//} -// -//extension NotificationViewController: LoadMoreConfigurableTableViewContainer { -// typealias BottomLoaderTableViewCell = SearchBottomLoader -// typealias LoadingState = NotificationViewController.LoadOldestState.Loading -// var loadMoreConfigurableTableView: UITableView { return tableView } -// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } -//} +// MARK: - UIScrollViewDelegate +extension NotificationViewController { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + handleScrollViewDidScroll(scrollView) + } +} + +extension NotificationViewController: LoadMoreConfigurableTableViewContainer { + typealias BottomLoaderTableViewCell = CommonBottomLoader + typealias LoadingState = NotificationViewModel.LoadOldestState.Loading + var loadMoreConfigurableTableView: UITableView { return tableView } + var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } +} diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift new file mode 100644 index 00000000..13c82093 --- /dev/null +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift @@ -0,0 +1,128 @@ +// +// NotificationViewModel+LoadOldestState.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/14. +// + +import os.log +import Foundation +import GameplayKit +import MastodonSDK + +extension NotificationViewModel { + class LoadOldestState: GKState { + weak var viewModel: NotificationViewModel? + + init(viewModel: NotificationViewModel) { + self.viewModel = viewModel + } + + override func didEnter(from previousState: GKState?) { + os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) + viewModel?.loadOldestStateMachinePublisher.send(self) + } + } +} + +extension NotificationViewModel.LoadOldestState { + class Initial: NotificationViewModel.LoadOldestState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + guard let viewModel = viewModel else { return false } + guard !(viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty else { return false } + return stateClass == Loading.self + } + } + + class Loading: NotificationViewModel.LoadOldestState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Fail.self || stateClass == Idle.self || stateClass == NoMore.self + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { + assertionFailure() + stateMachine.enter(Fail.self) + return + } + + guard let last = viewModel.fetchedResultsController.fetchedObjects?.last else { + stateMachine.enter(Idle.self) + return + } + + let maxID = last.id + let query = Mastodon.API.Notifications.Query( + maxID: maxID, + sinceID: nil, + minID: nil, + limit: nil, + excludeTypes: Mastodon.API.Notifications.allExcludeTypes(), + accountID: nil) + viewModel.context.apiService.allNotifications( + domain: activeMastodonAuthenticationBox.domain, + query: query, + mastodonAuthenticationBox: activeMastodonAuthenticationBox) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + // handle isFetchingLatestTimeline in fetch controller delegate + break + } + + stateMachine.enter(Idle.self) + } receiveValue: { [weak viewModel] response in + guard let viewModel = viewModel else { return } + if viewModel.selectedIndex.value == 1 { + let list = response.value.filter { $0.type == Mastodon.Entity.Notification.NotificationType.mention } + if list.isEmpty { + stateMachine.enter(NoMore.self) + } else { + stateMachine.enter(Idle.self) + } + } else { + if response.value.isEmpty { + stateMachine.enter(NoMore.self) + } else { + stateMachine.enter(Idle.self) + } + } + } + .store(in: &viewModel.disposeBag) + } + } + + class Fail: NotificationViewModel.LoadOldestState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Loading.self || stateClass == Idle.self + } + } + + class Idle: NotificationViewModel.LoadOldestState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Loading.self + } + } + + class NoMore: NotificationViewModel.LoadOldestState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + // reset state if needs + return stateClass == Idle.self + } + + override func didEnter(from previousState: GKState?) { + guard let viewModel = viewModel else { return } + guard let diffableDataSource = viewModel.diffableDataSource else { + assertionFailure() + return + } + var snapshot = diffableDataSource.snapshot() + snapshot.deleteItems([.bottomLoader]) + diffableDataSource.apply(snapshot) + } + } +} diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 20bec715..26d21e79 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -23,6 +23,7 @@ final class NotificationViewModel: NSObject { weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? let viewDidLoad = PassthroughSubject() + let selectedIndex = CurrentValueSubject(0) let activeMastodonAuthenticationBox: CurrentValueSubject let fetchedResultsController: NSFetchedResultsController! @@ -48,6 +49,21 @@ final class NotificationViewModel: NSObject { lazy var loadLatestStateMachinePublisher = CurrentValueSubject(nil) + // bottom loader + private(set) lazy var loadoldestStateMachine: GKStateMachine = { + // exclude timeline middle fetcher state + let stateMachine = GKStateMachine(states: [ + LoadOldestState.Initial(viewModel: self), + LoadOldestState.Loading(viewModel: self), + LoadOldestState.Fail(viewModel: self), + LoadOldestState.Idle(viewModel: self), + LoadOldestState.NoMore(viewModel: self), + ]) + stateMachine.enter(LoadOldestState.Initial.self) + return stateMachine + }() + lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) + init(context: AppContext) { self.context = context self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) From 2e86449c41dd40d67c6b86ca72d48a190ed4525f Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 14 Apr 2021 20:02:41 +0800 Subject: [PATCH 10/23] fix: bottom loader display aways --- .../Section/NotificationSection.swift | 8 +------- .../NotificationViewController.swift | 13 +++++++++++++ ...otificationViewModel+LoadOldestState.swift | 19 ++++++++++++++++--- .../NotificationViewModel+diffable.swift | 5 ++++- .../Notification/NotificationViewModel.swift | 1 + .../NotificationStatusTableViewCell.swift | 7 +++++++ 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 01273e9f..9fbad362 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -147,13 +147,7 @@ extension NotificationSection { requestUserID: String, statusItemAttribute: Item.StatusAttribute ) { - // disable interaction - cell.statusView.isUserInteractionEnabled = false - // remove item don't display - cell.statusView.actionToolbarContainer.removeFromSuperview() - cell.statusView.avatarView.removeFromSuperview() - - + // setup attribute statusItemAttribute.setupForStatus(status: status) diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index d5eb149a..a1921558 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -156,6 +156,19 @@ extension NotificationViewController: UITableViewDelegate { break } } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + switch item { + case .bottomLoader: + if !tableView.isDragging && !tableView.isDecelerating { + viewModel.loadoldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self) + } + default: + break + } + } } diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift index 13c82093..bf049eac 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift @@ -9,6 +9,7 @@ import os.log import Foundation import GameplayKit import MastodonSDK +import CoreDataStack extension NotificationViewModel { class LoadOldestState: GKState { @@ -42,13 +43,24 @@ extension NotificationViewModel.LoadOldestState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { + guard let activeMastodonAuthenticationBox = viewModel.activeMastodonAuthenticationBox.value else { assertionFailure() stateMachine.enter(Fail.self) return } - - guard let last = viewModel.fetchedResultsController.fetchedObjects?.last else { + let notifications: [MastodonNotification]? = { + let request = MastodonNotification.sortedFetchRequest + request.predicate = MastodonNotification.predicate(domain: activeMastodonAuthenticationBox.domain) + request.returnsObjectsAsFaults = false + do { + return try self.viewModel?.context.managedObjectContext.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + + guard let last = notifications?.last else { stateMachine.enter(Idle.self) return } @@ -78,6 +90,7 @@ extension NotificationViewModel.LoadOldestState { } receiveValue: { [weak viewModel] response in guard let viewModel = viewModel else { return } if viewModel.selectedIndex.value == 1 { + viewModel.noMoreNotification.value = response.value.isEmpty let list = response.value.filter { $0.type == Mastodon.Entity.Notification.NotificationType.mention } if list.isEmpty { stateMachine.enter(NoMore.self) diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index 2d68586d..a4c86521 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -72,7 +72,7 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { var newSnapshot = NSDiffableDataSourceSnapshot() newSnapshot.appendSections([.main]) newSnapshot.appendItems(notifications.map({NotificationItem.notification(objectID: $0.objectID)}), toSection: .main) - if !notifications.isEmpty { + if !notifications.isEmpty && self.noMoreNotification.value == false { newSnapshot.appendItems([.bottomLoader], toSection: .main) } @@ -112,6 +112,9 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil } + if oldSnapshot.itemIdentifiers.elementsEqual(newSnapshot.itemIdentifiers) { + return nil + } let timelineItem = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row] guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: timelineItem) else { return nil } let targetIndexPath = IndexPath(row: itemIndex, section: 0) diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 26d21e79..85806acf 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -24,6 +24,7 @@ final class NotificationViewModel: NSObject { let viewDidLoad = PassthroughSubject() let selectedIndex = CurrentValueSubject(0) + let noMoreNotification = CurrentValueSubject(false) let activeMastodonAuthenticationBox: CurrentValueSubject let fetchedResultsController: NSFetchedResultsController! diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 37b74599..6bdf0fd1 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -79,6 +79,7 @@ final class NotificationStatusTableViewCell: UITableViewCell { statusView.pollTableView.dataSource = nil statusView.playerContainerView.reset() statusView.playerContainerView.isHidden = true + disposeBag.removeAll() } @@ -133,6 +134,12 @@ extension NotificationStatusTableViewCell { } func addStatusAndContainer() { + statusView.isUserInteractionEnabled = false + // remove item don't display + statusView.actionToolbarContainer.removeFromSuperview() + statusView.avatarView.removeFromSuperview() + statusView.usernameLabel.removeFromSuperview() + contentView.addSubview(statusContainer) statusContainer.pin(top: 40, left: 63, bottom: 14, right: 14) From 80d76acc27ef31dbfe94da47c18af2f8a236e472 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 15 Apr 2021 10:16:30 +0800 Subject: [PATCH 11/23] chore: format file --- .../Diffiable/Item/NotificationItem.swift | 5 ++- .../Section/NotificationSection.swift | 13 +++---- Mastodon/Extension/UIView+Gesture.swift | 2 +- .../NotificationViewController.swift | 36 ++++++++----------- ...otificationViewModel+LoadLatestState.swift | 21 +++++------ ...otificationViewModel+LoadOldestState.swift | 16 ++++----- .../NotificationViewModel+diffable.swift | 17 ++++----- .../Notification/NotificationViewModel.swift | 23 ++++++------ .../NotificationStatusTableViewCell.swift | 2 +- .../NotificationTableViewCell.swift | 7 ++-- .../APIService/APIService+Notification.swift | 11 +++--- 11 files changed, 66 insertions(+), 87 deletions(-) diff --git a/Mastodon/Diffiable/Item/NotificationItem.swift b/Mastodon/Diffiable/Item/NotificationItem.swift index 6ef5b0c8..c160eac5 100644 --- a/Mastodon/Diffiable/Item/NotificationItem.swift +++ b/Mastodon/Diffiable/Item/NotificationItem.swift @@ -5,11 +5,10 @@ // Created by sxiaojian on 2021/4/13. // -import Foundation import CoreData +import Foundation enum NotificationItem { - case notification(objectID: NSManagedObjectID) case bottomLoader @@ -20,7 +19,7 @@ extension NotificationItem: Equatable { switch (lhs, rhs) { case (.bottomLoader, .bottomLoader): return true - case (.notification(let idLeft),.notification(let idRight)): + case (.notification(let idLeft), .notification(let idRight)): return idLeft == idRight default: return false diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 9fbad362..5e3cd2d9 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -5,12 +5,12 @@ // Created by sxiaojian on 2021/4/13. // +import Combine import CoreData import CoreDataStack import Foundation import MastodonSDK import UIKit -import Combine enum NotificationSection: Equatable, Hashable { case main @@ -25,8 +25,8 @@ extension NotificationSection { dependency: NeedsDependency, requestUserID: String ) -> UITableViewDiffableDataSource { - return UITableViewDiffableDataSource(tableView: tableView) { - [weak delegate,weak dependency] + UITableViewDiffableDataSource(tableView: tableView) { + [weak delegate, weak dependency] (tableView, indexPath, notificationItem) -> UITableViewCell? in guard let dependency = dependency else { return nil } switch notificationItem { @@ -136,7 +136,6 @@ extension NotificationSection { } } - extension NotificationSection { static func configure( cell: NotificationStatusTableViewCell, @@ -147,7 +146,6 @@ extension NotificationSection { requestUserID: String, statusItemAttribute: Item.StatusAttribute ) { - // setup attribute statusItemAttribute.setupForStatus(status: status) @@ -176,7 +174,6 @@ extension NotificationSection { cell.statusView.avatarStackedContainerButton.isHidden = true cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) - // set text cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content) @@ -338,7 +335,6 @@ extension NotificationSection { cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow } .store(in: &cell.disposeBag) - } static func configureHeader( @@ -366,7 +362,6 @@ extension NotificationSection { } } - static func configurePoll( cell: NotificationStatusTableViewCell, poll: Poll?, @@ -486,7 +481,7 @@ extension NotificationSection { } } -extension NotificationSection { +extension NotificationSection { private static func formattedNumberTitleForActionButton(_ number: Int?) -> String { guard let number = number, number > 0 else { return "" } return String(number) diff --git a/Mastodon/Extension/UIView+Gesture.swift b/Mastodon/Extension/UIView+Gesture.swift index 9c29c2c0..a76843d8 100644 --- a/Mastodon/Extension/UIView+Gesture.swift +++ b/Mastodon/Extension/UIView+Gesture.swift @@ -87,7 +87,7 @@ class GestureSubscription: Subscription where S.Input == GestureT extension UIView { func gesture(_ gestureType: GestureType = .tap()) -> GesturePublisher { - self.isUserInteractionEnabled = true + isUserInteractionEnabled = true return GesturePublisher(view: self, gestureType: gestureType) } } diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index a1921558..becf8677 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -5,16 +5,15 @@ // Created by sxiaojian on 2021/4/12. // -import UIKit import Combine -import OSLog import CoreData import CoreDataStack -import MastodonSDK import GameplayKit +import MastodonSDK +import OSLog +import UIKit final class NotificationViewController: UIViewController, NeedsDependency { - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -22,7 +21,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { private(set) lazy var viewModel = NotificationViewModel(context: context) let segmentControl: UISegmentedControl = { - let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything,L10n.Scene.Notification.Title.mentions]) + let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything, L10n.Scene.Notification.Title.mentions]) control.selectedSegmentIndex = 0 return control }() @@ -41,11 +40,9 @@ final class NotificationViewController: UIViewController, NeedsDependency { }() let refreshControl = UIRefreshControl() - } extension NotificationViewController { - override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = Asset.Colors.Background.pure.color @@ -80,7 +77,6 @@ extension NotificationViewController { } } .store(in: &disposeBag) - } override func viewWillAppear(_ animated: Bool) { @@ -110,12 +106,11 @@ extension NotificationViewController { self.tableView.reloadData() } } - } extension NotificationViewController { @objc private func segmentedControlValueChanged(_ sender: UISegmentedControl) { - os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, sender.selectedSegmentIndex) + os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", (#file as NSString).lastPathComponent, #line, #function, sender.selectedSegmentIndex) guard let domain = viewModel.activeMastodonAuthenticationBox.value?.domain else { return } @@ -136,8 +131,8 @@ extension NotificationViewController { } // MARK: - UITableViewDelegate + extension NotificationViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } @@ -145,9 +140,9 @@ extension NotificationViewController: UITableViewDelegate { case .notification(let objectID): let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification if notification.status != nil { - // TODO goto status detail vc + // TODO: goto status detail vc } else { - let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: notification.account) + let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account) DispatchQueue.main.async { self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show) } @@ -162,26 +157,26 @@ extension NotificationViewController: UITableViewDelegate { guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { case .bottomLoader: - if !tableView.isDragging && !tableView.isDecelerating { + if !tableView.isDragging, !tableView.isDecelerating { viewModel.loadoldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self) } default: break } } - } // MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate + extension NotificationViewController: ContentOffsetAdjustableTimelineViewControllerDelegate { func navigationBar() -> UINavigationBar? { - return navigationController?.navigationBar + navigationController?.navigationBar } } extension NotificationViewController: NotificationTableViewCellDelegate { func userAvatarDidPressed(notification: MastodonNotification) { - let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: notification.account) + let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account) DispatchQueue.main.async { self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show) } @@ -190,11 +185,10 @@ extension NotificationViewController: NotificationTableViewCellDelegate { func parent() -> UIViewController { self } - - } // MARK: - UIScrollViewDelegate + extension NotificationViewController { func scrollViewDidScroll(_ scrollView: UIScrollView) { handleScrollViewDidScroll(scrollView) @@ -204,6 +198,6 @@ extension NotificationViewController { extension NotificationViewController: LoadMoreConfigurableTableViewContainer { typealias BottomLoaderTableViewCell = CommonBottomLoader typealias LoadingState = NotificationViewModel.LoadOldestState.Loading - var loadMoreConfigurableTableView: UITableView { return tableView } - var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine } + var loadMoreConfigurableTableView: UITableView { tableView } + var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine } } diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift index 3e88de9a..38f24c58 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift @@ -5,13 +5,13 @@ // Created by sxiaojian on 2021/4/13. // -import os.log -import func QuartzCore.CACurrentMediaTime -import Foundation import CoreData import CoreDataStack +import Foundation import GameplayKit import MastodonSDK +import os.log +import func QuartzCore.CACurrentMediaTime extension NotificationViewModel { class LoadLatestState: GKState { @@ -22,7 +22,7 @@ extension NotificationViewModel { } override func didEnter(from previousState: GKState?) { - os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) + os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, debugDescription, previousState.debugDescription) viewModel?.loadLatestStateMachinePublisher.send(self) } } @@ -31,13 +31,13 @@ extension NotificationViewModel { extension NotificationViewModel.LoadLatestState { class Initial: NotificationViewModel.LoadLatestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self + stateClass == Loading.self } } class Loading: NotificationViewModel.LoadLatestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Fail.self || stateClass == Idle.self + stateClass == Fail.self || stateClass == Idle.self } override func didEnter(from previousState: GKState?) { @@ -63,7 +63,7 @@ extension NotificationViewModel.LoadLatestState { switch completion { case .failure(let error): viewModel.isFetchingLatestNotification.value = false - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) case .finished: // handle isFetchingLatestTimeline in fetch controller delegate break @@ -76,21 +76,18 @@ extension NotificationViewModel.LoadLatestState { } } .store(in: &viewModel.disposeBag) - - } } class Fail: NotificationViewModel.LoadLatestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self || stateClass == Idle.self + stateClass == Loading.self || stateClass == Idle.self } } class Idle: NotificationViewModel.LoadLatestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self + stateClass == Loading.self } } - } diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift index bf049eac..4885d702 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift @@ -5,11 +5,11 @@ // Created by sxiaojian on 2021/4/14. // -import os.log +import CoreDataStack import Foundation import GameplayKit import MastodonSDK -import CoreDataStack +import os.log extension NotificationViewModel { class LoadOldestState: GKState { @@ -20,7 +20,7 @@ extension NotificationViewModel { } override func didEnter(from previousState: GKState?) { - os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) + os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, debugDescription, previousState.debugDescription) viewModel?.loadOldestStateMachinePublisher.send(self) } } @@ -37,7 +37,7 @@ extension NotificationViewModel.LoadOldestState { class Loading: NotificationViewModel.LoadOldestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Fail.self || stateClass == Idle.self || stateClass == NoMore.self + stateClass == Fail.self || stateClass == Idle.self || stateClass == NoMore.self } override func didEnter(from previousState: GKState?) { @@ -80,7 +80,7 @@ extension NotificationViewModel.LoadOldestState { .sink { completion in switch completion { case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) case .finished: // handle isFetchingLatestTimeline in fetch controller delegate break @@ -111,20 +111,20 @@ extension NotificationViewModel.LoadOldestState { class Fail: NotificationViewModel.LoadOldestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self || stateClass == Idle.self + stateClass == Loading.self || stateClass == Idle.self } } class Idle: NotificationViewModel.LoadOldestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self + stateClass == Loading.self } } class NoMore: NotificationViewModel.LoadOldestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { // reset state if needs - return stateClass == Idle.self + stateClass == Idle.self } override func didEnter(from previousState: GKState?) { diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index a4c86521..be516a50 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -5,13 +5,12 @@ // Created by sxiaojian on 2021/4/13. // -import os.log -import UIKit import CoreData import CoreDataStack +import os.log +import UIKit extension NotificationViewModel { - func setupDiffableDataSource( for tableView: UITableView, delegate: NotificationTableViewCellDelegate, @@ -33,19 +32,18 @@ extension NotificationViewModel { requestUserID: userid ) } - } extension NotificationViewModel: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) } func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) guard let tableView = self.tableView else { return } - guard let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return } + guard let navigationBar = contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return } guard let diffableDataSource = self.diffableDataSource else { return } let oldSnapshot = diffableDataSource.snapshot() @@ -56,7 +54,6 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { managedObjectContext.parent = parentManagedObjectContext managedObjectContext.perform { - let notifications: [MastodonNotification] = { let request = MastodonNotification.sortedFetchRequest request.returnsObjectsAsFaults = false @@ -71,8 +68,8 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { var newSnapshot = NSDiffableDataSourceSnapshot() newSnapshot.appendSections([.main]) - newSnapshot.appendItems(notifications.map({NotificationItem.notification(objectID: $0.objectID)}), toSection: .main) - if !notifications.isEmpty && self.noMoreNotification.value == false { + newSnapshot.appendItems(notifications.map { NotificationItem.notification(objectID: $0.objectID) }, toSection: .main) + if !notifications.isEmpty, self.noMoreNotification.value == false { newSnapshot.appendItems([.bottomLoader], toSection: .main) } diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 85806acf..f64c07fc 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -5,16 +5,15 @@ // Created by sxiaojian on 2021/4/12. // -import Foundation import Combine -import UIKit import CoreData import CoreDataStack +import Foundation import GameplayKit import MastodonSDK +import UIKit -final class NotificationViewModel: NSObject { - +final class NotificationViewModel: NSObject { var disposeBag = Set() // input @@ -23,8 +22,8 @@ final class NotificationViewModel: NSObject { weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? let viewDidLoad = PassthroughSubject() - let selectedIndex = CurrentValueSubject(0) - let noMoreNotification = CurrentValueSubject(false) + let selectedIndex = CurrentValueSubject(0) + let noMoreNotification = CurrentValueSubject(false) let activeMastodonAuthenticationBox: CurrentValueSubject let fetchedResultsController: NSFetchedResultsController! @@ -33,7 +32,7 @@ final class NotificationViewModel: NSObject { let isFetchingLatestNotification = CurrentValueSubject(false) - //output + // output var diffableDataSource: UITableViewDiffableDataSource! // top loader private(set) lazy var loadLatestStateMachine: GKStateMachine = { @@ -63,6 +62,7 @@ final class NotificationViewModel: NSObject { stateMachine.enter(LoadOldestState.Initial.self) return stateMachine }() + lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) init(context: AppContext) { @@ -71,7 +71,7 @@ final class NotificationViewModel: NSObject { self.fetchedResultsController = { let fetchRequest = MastodonNotification.sortedFetchRequest fetchRequest.returnsObjectsAsFaults = false - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(MastodonNotification.status),#keyPath(MastodonNotification.account)] + fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(MastodonNotification.status), #keyPath(MastodonNotification.account)] let controller = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: context.managedObjectContext, @@ -83,7 +83,7 @@ final class NotificationViewModel: NSObject { }() super.init() - self.fetchedResultsController.delegate = self + fetchedResultsController.delegate = self context.authenticationService.activeMastodonAuthenticationBox .sink(receiveValue: { [weak self] box in guard let self = self else { return } @@ -95,7 +95,7 @@ final class NotificationViewModel: NSObject { .store(in: &disposeBag) notificationPredicate - .compactMap{ $0 } + .compactMap { $0 } .sink { [weak self] predicate in guard let self = self else { return } self.fetchedResultsController.fetchRequest.predicate = predicate @@ -112,12 +112,11 @@ final class NotificationViewModel: NSObject { } .store(in: &disposeBag) - self.viewDidLoad + viewDidLoad .sink { [weak self] in guard let domain = self?.activeMastodonAuthenticationBox.value?.domain else { return } self?.notificationPredicate.value = MastodonNotification.predicate(domain: domain) - } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 6bdf0fd1..4766aaff 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -12,7 +12,7 @@ import UIKit final class NotificationStatusTableViewCell: UITableViewCell { static let actionImageBorderWidth: CGFloat = 2 - static let statusPadding: UIEdgeInsets = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24) + static let statusPadding = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24) var disposeBag = Set() var pollCountdownSubscription: AnyCancellable? var delegate: NotificationTableViewCellDelegate? diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index a10ff01d..ce8895ac 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -6,16 +6,16 @@ // import Combine +import CoreDataStack import Foundation import UIKit -import CoreDataStack -protocol NotificationTableViewCellDelegate: class { +protocol NotificationTableViewCellDelegate: AnyObject { var context: AppContext! { get } func parent() -> UIViewController - func userAvatarDidPressed(notification:MastodonNotification) + func userAvatarDidPressed(notification: MastodonNotification) } final class NotificationTableViewCell: UITableViewCell { @@ -113,7 +113,6 @@ extension NotificationTableViewCell { ]) } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) actionImageBackground.layer.borderColor = Asset.Colors.Background.pure.color.cgColor diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift index 53595fa9..e2b90ffd 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/Mastodon/Service/APIService/APIService+Notification.swift @@ -5,10 +5,10 @@ // Created by sxiaojian on 2021/4/13. // -import Foundation import Combine import CoreData import CoreDataStack +import Foundation import MastodonSDK import OSLog @@ -16,8 +16,8 @@ extension APIService { func allNotifications( domain: String, query: Mastodon.API.Notifications.Query, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox - ) -> AnyPublisher, Error> { + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> AnyPublisher, Error> + { let authorization = mastodonAuthenticationBox.userAuthorization return Mastodon.API.Notifications.getNotifications( session: session, @@ -28,10 +28,10 @@ extension APIService { let log = OSLog.api return self.backgroundManagedObjectContext.performChanges { response.value.forEach { notification in - let (mastodonUser,_) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log) + let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log) var status: Status? if let statusEntity = notification.status { - let (statusInCoreData,_,_) = APIService.CoreData.createOrMergeStatus( + let (statusInCoreData, _, _) = APIService.CoreData.createOrMergeStatus( into: self.backgroundManagedObjectContext, for: nil, domain: domain, @@ -45,7 +45,6 @@ extension APIService { // use constrain to avoid repeated save let notification = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, property: MastodonNotification.Property(id: notification.id, type: notification.type.rawValue, account: mastodonUser, status: status, createdAt: notification.createdAt)) os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)", (#file as NSString).lastPathComponent, #line, #function, notification.type, notification.account.username) - } } .setFailureType(to: Error.self) From 56d22e9c1c5e6b8df27af2e5300d846a4257cdfe Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 15 Apr 2021 10:37:46 +0800 Subject: [PATCH 12/23] chore: use readableContentGuide --- .../NotificationStatusTableViewCell.swift | 35 +++++++++++-------- .../NotificationTableViewCell.swift | 22 ++++++++---- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 4766aaff..f05f3d9a 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -105,24 +105,35 @@ extension NotificationStatusTableViewCell { func configure() { selectionStyle = .none - contentView.addSubview(avatatImageView) + let container = UIView() + container.backgroundColor = .clear + contentView.addSubview(container) + container.constrain([ + container.topAnchor.constraint(equalTo: contentView.topAnchor), + container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + + container.addSubview(avatatImageView) avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil) - contentView.addSubview(actionImageBackground) - actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationStatusTableViewCell.actionImageBorderWidth, height: 24 + NotificationStatusTableViewCell.actionImageBorderWidth)) + container.addSubview(actionImageBackground) + actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationTableViewCell.actionImageBorderWidth, height: 24 + NotificationTableViewCell.actionImageBorderWidth)) actionImageBackground.pin(top: 33, left: 33, bottom: nil, right: nil) actionImageBackground.addSubview(actionImageView) actionImageView.constrainToCenter() - - contentView.addSubview(nameLabel) + + container.addSubview(nameLabel) nameLabel.constrain([ - nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), - nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 61) + nameLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 12), + nameLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 61) + ]) - contentView.addSubview(actionLabel) + container.addSubview(actionLabel) actionLabel.constrain([ actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), @@ -130,20 +141,16 @@ extension NotificationStatusTableViewCell { ]) statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color - addStatusAndContainer() - } - - func addStatusAndContainer() { statusView.isUserInteractionEnabled = false // remove item don't display statusView.actionToolbarContainer.removeFromSuperview() statusView.avatarView.removeFromSuperview() statusView.usernameLabel.removeFromSuperview() - contentView.addSubview(statusContainer) + container.addSubview(statusContainer) statusContainer.pin(top: 40, left: 63, bottom: 14, right: 14) - contentView.addSubview(statusView) + container.addSubview(statusView) statusView.pin(top: NotificationStatusTableViewCell.statusPadding.top, left: NotificationStatusTableViewCell.statusPadding.left, bottom: NotificationStatusTableViewCell.statusPadding.bottom, right: NotificationStatusTableViewCell.statusPadding.right) } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index ce8895ac..e42b0811 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -87,25 +87,35 @@ extension NotificationTableViewCell { func configure() { selectionStyle = .none - contentView.addSubview(avatatImageView) + let container = UIView() + container.backgroundColor = .clear + contentView.addSubview(container) + container.constrain([ + container.topAnchor.constraint(equalTo: contentView.topAnchor), + container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + + container.addSubview(avatatImageView) avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil) - contentView.addSubview(actionImageBackground) + container.addSubview(actionImageBackground) actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationTableViewCell.actionImageBorderWidth, height: 24 + NotificationTableViewCell.actionImageBorderWidth)) actionImageBackground.pin(top: 33, left: 33, bottom: nil, right: nil) actionImageBackground.addSubview(actionImageView) actionImageView.constrainToCenter() - contentView.addSubview(nameLabel) + container.addSubview(nameLabel) nameLabel.constrain([ - nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24), + nameLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 24), contentView.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24), - nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 61) + nameLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 61) ]) - contentView.addSubview(actionLabel) + container.addSubview(actionLabel) actionLabel.constrain([ actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), From 845080a69dfda8ea6b9e423a8e5c4e5b8d9e09fb Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 15 Apr 2021 10:53:03 +0800 Subject: [PATCH 13/23] fix: NotificationCell nameLabel and actionLabel layout issue --- .../TableViewCell/NotificationStatusTableViewCell.swift | 2 +- .../TableViewCell/NotificationTableViewCell.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index f05f3d9a..0f7a8fbd 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -137,7 +137,7 @@ extension NotificationStatusTableViewCell { actionLabel.constrain([ actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), - contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) + container.trailingAnchor.constraint(greaterThanOrEqualTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) ]) statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index e42b0811..238d9c67 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -111,7 +111,7 @@ extension NotificationTableViewCell { container.addSubview(nameLabel) nameLabel.constrain([ nameLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 24), - contentView.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24), + container.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24), nameLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 61) ]) @@ -119,7 +119,7 @@ extension NotificationTableViewCell { actionLabel.constrain([ actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), - contentView.trailingAnchor.constraint(equalTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) + container.trailingAnchor.constraint(greaterThanOrEqualTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) ]) } From ecf622b86672167c84507d8c8861399a66e798bd Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 15 Apr 2021 12:35:40 +0800 Subject: [PATCH 14/23] fix: statusView layout issue --- ...tagTimelineViewModel+LoadOldestState.swift | 8 +++++--- ...omeTimelineViewModel+LoadOldestState.swift | 8 +++++--- ...otificationViewModel+LoadOldestState.swift | 8 +++++--- .../NotificationViewModel+diffable.swift | 19 ++++++++++--------- .../NotificationStatusTableViewCell.swift | 8 ++++---- .../SearchViewModel+LoadOldestState.swift | 8 +++++--- .../TableViewCell/CommonBottomLoader.swift | 3 --- 7 files changed, 34 insertions(+), 28 deletions(-) diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadOldestState.swift index e5c78f3d..13737364 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadOldestState.swift @@ -123,9 +123,11 @@ extension HashtagTimelineViewModel.LoadOldestState { assertionFailure() return } - var snapshot = diffableDataSource.snapshot() - snapshot.deleteItems([.bottomLoader]) - diffableDataSource.apply(snapshot) + DispatchQueue.main.async { + var snapshot = diffableDataSource.snapshot() + snapshot.deleteItems([.bottomLoader]) + diffableDataSource.apply(snapshot) + } } } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index aaabd7a8..a74d03a5 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -103,9 +103,11 @@ extension HomeTimelineViewModel.LoadOldestState { assertionFailure() return } - var snapshot = diffableDataSource.snapshot() - snapshot.deleteItems([.bottomLoader]) - diffableDataSource.apply(snapshot) + DispatchQueue.main.async { + var snapshot = diffableDataSource.snapshot() + snapshot.deleteItems([.bottomLoader]) + diffableDataSource.apply(snapshot) + } } } } diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift index 4885d702..a26dedee 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift @@ -133,9 +133,11 @@ extension NotificationViewModel.LoadOldestState { assertionFailure() return } - var snapshot = diffableDataSource.snapshot() - snapshot.deleteItems([.bottomLoader]) - diffableDataSource.apply(snapshot) + DispatchQueue.main.async { + var snapshot = diffableDataSource.snapshot() + snapshot.deleteItems([.bottomLoader]) + diffableDataSource.apply(snapshot) + } } } } diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index be516a50..774620f8 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -46,7 +46,6 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { guard let navigationBar = contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return } guard let diffableDataSource = self.diffableDataSource else { return } - let oldSnapshot = diffableDataSource.snapshot() let predicate = fetchedResultsController.fetchRequest.predicate let parentManagedObjectContext = fetchedResultsController.managedObjectContext @@ -66,16 +65,18 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { } }() - var newSnapshot = NSDiffableDataSourceSnapshot() - newSnapshot.appendSections([.main]) - newSnapshot.appendItems(notifications.map { NotificationItem.notification(objectID: $0.objectID) }, toSection: .main) - if !notifications.isEmpty, self.noMoreNotification.value == false { - newSnapshot.appendItems([.bottomLoader], toSection: .main) - } - DispatchQueue.main.async { + let oldSnapshot = diffableDataSource.snapshot() + var newSnapshot = NSDiffableDataSourceSnapshot() + newSnapshot.appendSections([.main]) + newSnapshot.appendItems(notifications.map { NotificationItem.notification(objectID: $0.objectID) }, toSection: .main) + if !notifications.isEmpty, self.noMoreNotification.value == false { + newSnapshot.appendItems([.bottomLoader], toSection: .main) + } guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else { - diffableDataSource.apply(newSnapshot) + diffableDataSource.apply(newSnapshot, animatingDifferences: false) { + tableView.reloadData() + } self.isFetchingLatestNotification.value = false return } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 0f7a8fbd..6bea35ea 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -58,7 +58,7 @@ final class NotificationStatusTableViewCell: UITableViewCell { return label }() - let statusContainer: UIView = { + let statusBorder: UIView = { let view = UIView() view.backgroundColor = .clear view.layer.cornerRadius = 6 @@ -147,8 +147,8 @@ extension NotificationStatusTableViewCell { statusView.avatarView.removeFromSuperview() statusView.usernameLabel.removeFromSuperview() - container.addSubview(statusContainer) - statusContainer.pin(top: 40, left: 63, bottom: 14, right: 14) + container.addSubview(statusBorder) + statusBorder.pin(top: 40, left: 63, bottom: 14, right: 14) container.addSubview(statusView) statusView.pin(top: NotificationStatusTableViewCell.statusPadding.top, left: NotificationStatusTableViewCell.statusPadding.left, bottom: NotificationStatusTableViewCell.statusPadding.bottom, right: NotificationStatusTableViewCell.statusPadding.right) @@ -156,7 +156,7 @@ extension NotificationStatusTableViewCell { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - statusContainer.layer.borderColor = Asset.Colors.Border.notification.color.cgColor + statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor actionImageBackground.layer.borderColor = Asset.Colors.Background.pure.color.cgColor } } diff --git a/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift index c76ab202..b486df77 100644 --- a/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Search/SearchViewModel+LoadOldestState.swift @@ -136,9 +136,11 @@ extension SearchViewModel.LoadOldestState { assertionFailure() return } - var snapshot = diffableDataSource.snapshot() - snapshot.deleteItems([.bottomLoader]) - diffableDataSource.apply(snapshot) + DispatchQueue.main.async { + var snapshot = diffableDataSource.snapshot() + snapshot.deleteItems([.bottomLoader]) + diffableDataSource.apply(snapshot) + } } } } diff --git a/Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift b/Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift index bb2ae9ce..2d529972 100644 --- a/Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift +++ b/Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift @@ -43,8 +43,5 @@ final class CommonBottomLoader: UITableViewCell { backgroundColor = Asset.Colors.Background.systemGroupedBackground.color contentView.addSubview(activityIndicatorView) activityIndicatorView.constrainToCenter() - NSLayoutConstraint.activate([ - contentView.heightAnchor.constraint(equalToConstant: 44) - ]) } } From ca7eb7bb1203ef0f912b3ac59b1453c76106186b Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 16 Apr 2021 13:45:54 +0800 Subject: [PATCH 15/23] chore: code format --- .../CoreData.xcdatamodel/contents | 5 +- CoreDataStack/Entity/Notification.swift | 55 +++++++++------ Mastodon.xcodeproj/project.pbxproj | 4 -- .../Diffiable/Item/NotificationItem.swift | 4 +- .../Section/NotificationSection.swift | 4 +- .../Section/SearchResultSection.swift | 2 +- Mastodon/Extension/UIView+Constraint.swift | 5 +- .../NotificationViewController.swift | 12 ++-- ...otificationViewModel+LoadLatestState.swift | 8 ++- ...otificationViewModel+LoadOldestState.swift | 20 +++--- .../Notification/NotificationViewModel.swift | 17 +++-- .../NotificationStatusTableViewCell.swift | 14 ++-- .../NotificationTableViewCell.swift | 5 +- .../SearchViewController+Searching.swift | 2 +- .../Scene/Search/SearchViewController.swift | 2 +- .../TableViewCell/CommonBottomLoader.swift | 47 ------------- .../APIService/APIService+Notification.swift | 70 ++++++++++--------- .../API/Mastodon+API+Notifications.swift | 18 ++--- 18 files changed, 130 insertions(+), 164 deletions(-) delete mode 100644 Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 2569da5e..bc7a20d5 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -70,8 +70,9 @@ - + + @@ -223,7 +224,7 @@ - + diff --git a/CoreDataStack/Entity/Notification.swift b/CoreDataStack/Entity/Notification.swift index 8a0595f6..31c361aa 100644 --- a/CoreDataStack/Entity/Notification.swift +++ b/CoreDataStack/Entity/Notification.swift @@ -12,13 +12,14 @@ public final class MastodonNotification: NSManagedObject { public typealias ID = UUID @NSManaged public private(set) var identifier: ID @NSManaged public private(set) var id: String - @NSManaged public private(set) var domain: String @NSManaged public private(set) var createAt: Date @NSManaged public private(set) var updatedAt: Date - @NSManaged public private(set) var type: String + @NSManaged public private(set) var typeRaw: String @NSManaged public private(set) var account: MastodonUser @NSManaged public private(set) var status: Status? + @NSManaged public private(set) var domain: String + @NSManaged public private(set) var userID: String } extension MastodonNotification { @@ -26,12 +27,6 @@ extension MastodonNotification { super.awakeFromInsert() setPrimitiveValue(UUID(), forKey: #keyPath(MastodonNotification.identifier)) } - - public override func willSave() { - super.willSave() - setPrimitiveValue(Date(), forKey: #keyPath(MastodonNotification.updatedAt)) - } - } public extension MastodonNotification { @@ -39,16 +34,19 @@ public extension MastodonNotification { static func insert( into context: NSManagedObjectContext, domain: String, + userID: String, + networkDate: Date, property: Property ) -> MastodonNotification { let notification: MastodonNotification = context.insertObject() notification.id = property.id notification.createAt = property.createdAt - notification.updatedAt = property.createdAt - notification.type = property.type + notification.updatedAt = networkDate + notification.typeRaw = property.typeRaw notification.account = property.account notification.status = property.status notification.domain = domain + notification.userID = userID return notification } } @@ -56,19 +54,20 @@ public extension MastodonNotification { public extension MastodonNotification { struct Property { public init(id: String, - type: String, + typeRaw: String, account: MastodonUser, status: Status?, - createdAt: Date) { + createdAt: Date + ) { self.id = id - self.type = type + self.typeRaw = typeRaw self.account = account self.status = status self.createdAt = createdAt } public let id: String - public let type: String + public let typeRaw: String public let account: MastodonUser public let status: Status? public let createdAt: Date @@ -76,19 +75,31 @@ public extension MastodonNotification { } extension MastodonNotification { - public static func predicate(domain: String) -> NSPredicate { + static func predicate(domain: String) -> NSPredicate { return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.domain), domain) } - static func predicate(type: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.type), type) + static func predicate(userID: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.userID), userID) } - public static func predicate(domain: String, type: String) -> NSPredicate { - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - MastodonNotification.predicate(domain: domain), - MastodonNotification.predicate(type: type) - ]) + static func predicate(typeRaw: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(MastodonNotification.typeRaw), typeRaw) + } + + public static func predicate(domain: String, userID: String, typeRaw: String? = nil) -> NSPredicate { + if let typeRaw = typeRaw { + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + MastodonNotification.predicate(domain: domain), + MastodonNotification.predicate(typeRaw: typeRaw), + MastodonNotification.predicate(userID: userID), + ]) + } else { + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + MastodonNotification.predicate(domain: domain), + MastodonNotification.predicate(userID: userID) + ]) + } } } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d088d317..3244dcd3 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -37,7 +37,6 @@ 2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; - 2D19864F261C372A00F0B013 /* CommonBottomLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D19864E261C372A00F0B013 /* CommonBottomLoader.swift */; }; 2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */; }; 2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; }; 2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; }; @@ -427,7 +426,6 @@ 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; - 2D19864E261C372A00F0B013 /* CommonBottomLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonBottomLoader.swift; sourceTree = ""; }; 2D198654261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+LoadOldestState.swift"; sourceTree = ""; }; 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = ""; }; 2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; @@ -1154,7 +1152,6 @@ isa = PBXGroup; children = ( 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */, - 2D19864E261C372A00F0B013 /* CommonBottomLoader.swift */, ); path = TableViewCell; sourceTree = ""; @@ -2272,7 +2269,6 @@ DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */, 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, - 2D19864F261C372A00F0B013 /* CommonBottomLoader.swift in Sources */, DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */, 0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/NotificationItem.swift b/Mastodon/Diffiable/Item/NotificationItem.swift index c160eac5..ba0d0c14 100644 --- a/Mastodon/Diffiable/Item/NotificationItem.swift +++ b/Mastodon/Diffiable/Item/NotificationItem.swift @@ -17,10 +17,10 @@ enum NotificationItem { extension NotificationItem: Equatable { static func == (lhs: NotificationItem, rhs: NotificationItem) -> Bool { switch (lhs, rhs) { - case (.bottomLoader, .bottomLoader): - return true case (.notification(let idLeft), .notification(let idRight)): return idLeft == idRight + case (.bottomLoader, .bottomLoader): + return true default: return false } diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 5e3cd2d9..0b63bb24 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -33,7 +33,7 @@ extension NotificationSection { case .notification(let objectID): let notification = managedObjectContext.object(with: objectID) as! MastodonNotification - let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.type) + let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) let timeText = notification.createAt.shortTimeAgoSinceNow @@ -128,7 +128,7 @@ extension NotificationSection { return cell } case .bottomLoader: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CommonBottomLoader.self)) as! CommonBottomLoader + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell cell.startAnimating() return cell } diff --git a/Mastodon/Diffiable/Section/SearchResultSection.swift b/Mastodon/Diffiable/Section/SearchResultSection.swift index e01063c8..1b9230ee 100644 --- a/Mastodon/Diffiable/Section/SearchResultSection.swift +++ b/Mastodon/Diffiable/Section/SearchResultSection.swift @@ -44,7 +44,7 @@ extension SearchResultSection { cell.config(with: user) return cell case .bottomLoader: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CommonBottomLoader.self)) as! CommonBottomLoader + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) as! TimelineBottomLoaderTableViewCell cell.startAnimating() return cell } diff --git a/Mastodon/Extension/UIView+Constraint.swift b/Mastodon/Extension/UIView+Constraint.swift index baa923ad..ded8846d 100644 --- a/Mastodon/Extension/UIView+Constraint.swift +++ b/Mastodon/Extension/UIView+Constraint.swift @@ -174,8 +174,9 @@ extension UIView { guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } translatesAutoresizingMaskIntoConstraints = false constrain([ - widthAnchor.constraint(equalToConstant: toSize.width), - heightAnchor.constraint(equalToConstant: toSize.height)]) + widthAnchor.constraint(equalToConstant: toSize.width).priority(.required - 1), + heightAnchor.constraint(equalToConstant: toSize.height).priority(.required - 1) + ]) } func pin(top: CGFloat?,left: CGFloat?,bottom: CGFloat?, right: CGFloat?) { diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index becf8677..ddc997a5 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -33,7 +33,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self)) - tableView.register(CommonBottomLoader.self, forCellReuseIdentifier: String(describing: CommonBottomLoader.self)) + tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.tableFooterView = UIView() tableView.rowHeight = UITableView.automaticDimension return tableView @@ -111,15 +111,15 @@ extension NotificationViewController { extension NotificationViewController { @objc private func segmentedControlValueChanged(_ sender: UISegmentedControl) { os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", (#file as NSString).lastPathComponent, #line, #function, sender.selectedSegmentIndex) - guard let domain = viewModel.activeMastodonAuthenticationBox.value?.domain else { + guard let domain = viewModel.activeMastodonAuthenticationBox.value?.domain, let userID = viewModel.activeMastodonAuthenticationBox.value?.userID else { return } if sender.selectedSegmentIndex == 0 { - viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain) + viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) } else { - viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, type: Mastodon.Entity.Notification.NotificationType.mention.rawValue) + viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain,userID: userID, typeRaw: Mastodon.Entity.Notification.NotificationType.mention.rawValue) } - viewModel.selectedIndex.value = sender.selectedSegmentIndex + viewModel.selectedIndex.value = NotificationViewModel.NotificationSegment.init(rawValue: sender.selectedSegmentIndex)! } @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { @@ -196,7 +196,7 @@ extension NotificationViewController { } extension NotificationViewController: LoadMoreConfigurableTableViewContainer { - typealias BottomLoaderTableViewCell = CommonBottomLoader + typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell typealias LoadingState = NotificationViewModel.LoadOldestState.Loading var loadMoreConfigurableTableView: UITableView { tableView } var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine } diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift index 38f24c58..0e6b0d62 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift @@ -53,12 +53,14 @@ extension NotificationViewModel.LoadLatestState { sinceID: nil, minID: nil, limit: nil, - excludeTypes: Mastodon.API.Notifications.allExcludeTypes(), - accountID: nil) + excludeTypes: [.followRequest], + accountID: nil + ) viewModel.context.apiService.allNotifications( domain: activeMastodonAuthenticationBox.domain, query: query, - mastodonAuthenticationBox: activeMastodonAuthenticationBox) + mastodonAuthenticationBox: activeMastodonAuthenticationBox + ) .sink { completion in switch completion { case .failure(let error): diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift index a26dedee..8075ce37 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift @@ -50,7 +50,7 @@ extension NotificationViewModel.LoadOldestState { } let notifications: [MastodonNotification]? = { let request = MastodonNotification.sortedFetchRequest - request.predicate = MastodonNotification.predicate(domain: activeMastodonAuthenticationBox.domain) + request.predicate = MastodonNotification.predicate(domain: activeMastodonAuthenticationBox.domain, userID: activeMastodonAuthenticationBox.userID) request.returnsObjectsAsFaults = false do { return try self.viewModel?.context.managedObjectContext.fetch(request) @@ -71,12 +71,13 @@ extension NotificationViewModel.LoadOldestState { sinceID: nil, minID: nil, limit: nil, - excludeTypes: Mastodon.API.Notifications.allExcludeTypes(), + excludeTypes: [.followRequest], accountID: nil) viewModel.context.apiService.allNotifications( domain: activeMastodonAuthenticationBox.domain, query: query, - mastodonAuthenticationBox: activeMastodonAuthenticationBox) + mastodonAuthenticationBox: activeMastodonAuthenticationBox + ) .sink { completion in switch completion { case .failure(let error): @@ -89,16 +90,17 @@ extension NotificationViewModel.LoadOldestState { stateMachine.enter(Idle.self) } receiveValue: { [weak viewModel] response in guard let viewModel = viewModel else { return } - if viewModel.selectedIndex.value == 1 { - viewModel.noMoreNotification.value = response.value.isEmpty - let list = response.value.filter { $0.type == Mastodon.Entity.Notification.NotificationType.mention } - if list.isEmpty { + switch viewModel.selectedIndex.value { + case .EveryThing: + if response.value.isEmpty { stateMachine.enter(NoMore.self) } else { stateMachine.enter(Idle.self) } - } else { - if response.value.isEmpty { + case .Mentions: + viewModel.noMoreNotification.value = response.value.isEmpty + let list = response.value.filter { $0.type == Mastodon.Entity.Notification.NotificationType.mention } + if list.isEmpty { stateMachine.enter(NoMore.self) } else { stateMachine.enter(Idle.self) diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index f64c07fc..e026af73 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -22,7 +22,7 @@ final class NotificationViewModel: NSObject { weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? let viewDidLoad = PassthroughSubject() - let selectedIndex = CurrentValueSubject(0) + let selectedIndex = CurrentValueSubject(.EveryThing) let noMoreNotification = CurrentValueSubject(false) let activeMastodonAuthenticationBox: CurrentValueSubject @@ -88,8 +88,8 @@ final class NotificationViewModel: NSObject { .sink(receiveValue: { [weak self] box in guard let self = self else { return } self.activeMastodonAuthenticationBox.value = box - if let domain = box?.domain { - self.notificationPredicate.value = MastodonNotification.predicate(domain: domain) + if let domain = box?.domain, let userID = box?.userID { + self.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) } }) .store(in: &disposeBag) @@ -115,9 +115,16 @@ final class NotificationViewModel: NSObject { viewDidLoad .sink { [weak self] in - guard let domain = self?.activeMastodonAuthenticationBox.value?.domain else { return } - self?.notificationPredicate.value = MastodonNotification.predicate(domain: domain) + guard let domain = self?.activeMastodonAuthenticationBox.value?.domain, let userID = self?.activeMastodonAuthenticationBox.value?.userID else { return } + self?.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) } .store(in: &disposeBag) } } + +extension NotificationViewModel { + enum NotificationSegment: Int { + case EveryThing + case Mentions + } +} diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 6bea35ea..dc3f49bb 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -103,7 +103,6 @@ final class NotificationStatusTableViewCell: UITableViewCell { extension NotificationStatusTableViewCell { func configure() { - selectionStyle = .none let container = UIView() container.backgroundColor = .clear @@ -117,11 +116,11 @@ extension NotificationStatusTableViewCell { container.addSubview(avatatImageView) avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) - avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil) + avatatImageView.pin(top: 12, left: 0, bottom: nil, right: nil) container.addSubview(actionImageBackground) actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationTableViewCell.actionImageBorderWidth, height: 24 + NotificationTableViewCell.actionImageBorderWidth)) - actionImageBackground.pin(top: 33, left: 33, bottom: nil, right: nil) + actionImageBackground.pin(top: 33, left: 21, bottom: nil, right: nil) actionImageBackground.addSubview(actionImageView) actionImageView.constrainToCenter() @@ -130,22 +129,21 @@ extension NotificationStatusTableViewCell { nameLabel.constrain([ nameLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 12), nameLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 61) - ]) container.addSubview(actionLabel) actionLabel.constrain([ actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), - container.trailingAnchor.constraint(greaterThanOrEqualTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) + container.trailingAnchor.constraint(greaterThanOrEqualTo: actionLabel.trailingAnchor, constant: 4) ]) statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color statusView.isUserInteractionEnabled = false // remove item don't display - statusView.actionToolbarContainer.removeFromSuperview() - statusView.avatarView.removeFromSuperview() - statusView.usernameLabel.removeFromSuperview() + statusView.actionToolbarContainer.isHidden = true + statusView.avatarView.isHidden = true + statusView.usernameLabel.isHidden = true container.addSubview(statusBorder) statusBorder.pin(top: 40, left: 63, bottom: 14, right: 14) diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 238d9c67..cda4d75d 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -85,7 +85,6 @@ final class NotificationTableViewCell: UITableViewCell { extension NotificationTableViewCell { func configure() { - selectionStyle = .none let container = UIView() container.backgroundColor = .clear @@ -99,7 +98,7 @@ extension NotificationTableViewCell { container.addSubview(avatatImageView) avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) - avatatImageView.pin(top: 12, left: 12, bottom: nil, right: nil) + avatatImageView.pin(top: 12, left: 0, bottom: nil, right: nil) container.addSubview(actionImageBackground) actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationTableViewCell.actionImageBorderWidth, height: 24 + NotificationTableViewCell.actionImageBorderWidth)) @@ -119,7 +118,7 @@ extension NotificationTableViewCell { actionLabel.constrain([ actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), - container.trailingAnchor.constraint(greaterThanOrEqualTo: actionLabel.trailingAnchor, constant: 4).priority(.defaultLow) + container.trailingAnchor.constraint(greaterThanOrEqualTo: actionLabel.trailingAnchor, constant: 4) ]) } diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift index 43e5d397..86a27e03 100644 --- a/Mastodon/Scene/Search/SearchViewController+Searching.swift +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -17,7 +17,7 @@ extension SearchViewController { func setupSearchingTableView() { searchingTableView.delegate = self searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self)) - searchingTableView.register(CommonBottomLoader.self, forCellReuseIdentifier: String(describing: CommonBottomLoader.self)) + searchingTableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) view.addSubview(searchingTableView) searchingTableView.constrain([ searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor), diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index dc941458..4fee226b 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -227,7 +227,7 @@ extension SearchViewController: UISearchBarDelegate { } extension SearchViewController: LoadMoreConfigurableTableViewContainer { - typealias BottomLoaderTableViewCell = CommonBottomLoader + typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell typealias LoadingState = SearchViewModel.LoadOldestState.Loading var loadMoreConfigurableTableView: UITableView { searchingTableView } var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine } diff --git a/Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift b/Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift deleted file mode 100644 index 2d529972..00000000 --- a/Mastodon/Scene/Search/TableViewCell/CommonBottomLoader.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// CommonBottomLoader.swift -// Mastodon -// -// Created by sxiaojian on 2021/4/6. -// - -import Foundation -import UIKit - -final class CommonBottomLoader: UITableViewCell { - let activityIndicatorView: UIActivityIndicatorView = { - let activityIndicatorView = UIActivityIndicatorView(style: .medium) - activityIndicatorView.tintColor = Asset.Colors.Label.primary.color - activityIndicatorView.hidesWhenStopped = true - return activityIndicatorView - }() - - override func prepareForReuse() { - super.prepareForReuse() - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - - func startAnimating() { - activityIndicatorView.startAnimating() - } - - func stopAnimating() { - activityIndicatorView.stopAnimating() - } - - func _init() { - selectionStyle = .none - backgroundColor = Asset.Colors.Background.systemGroupedBackground.color - contentView.addSubview(activityIndicatorView) - activityIndicatorView.constrainToCenter() - } -} diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift index e2b90ffd..ee8f5186 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/Mastodon/Service/APIService/APIService+Notification.swift @@ -16,48 +16,52 @@ extension APIService { func allNotifications( domain: String, query: Mastodon.API.Notifications.Query, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> AnyPublisher, Error> - { + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization + let userID = mastodonAuthenticationBox.userID return Mastodon.API.Notifications.getNotifications( session: session, domain: domain, query: query, - authorization: authorization) - .flatMap { response -> AnyPublisher, Error> in - let log = OSLog.api - return self.backgroundManagedObjectContext.performChanges { - response.value.forEach { notification in - let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log) - var status: Status? - if let statusEntity = notification.status { - let (statusInCoreData, _, _) = APIService.CoreData.createOrMergeStatus( - into: self.backgroundManagedObjectContext, - for: nil, - domain: domain, - entity: statusEntity, - statusCache: nil, - userCache: nil, - networkDate: Date(), - log: log) - status = statusInCoreData - } - // use constrain to avoid repeated save - let notification = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, property: MastodonNotification.Property(id: notification.id, type: notification.type.rawValue, account: mastodonUser, status: status, createdAt: notification.createdAt)) - os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)", (#file as NSString).lastPathComponent, #line, #function, notification.type, notification.account.username) + authorization: authorization + ) + .flatMap { response -> AnyPublisher, Error> in + let log = OSLog.api + return self.backgroundManagedObjectContext.performChanges { + response.value.forEach { notification in + let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log) + var status: Status? + if let statusEntity = notification.status { + let (statusInCoreData, _, _) = APIService.CoreData.createOrMergeStatus( + into: self.backgroundManagedObjectContext, + for: nil, + domain: domain, + entity: statusEntity, + statusCache: nil, + userCache: nil, + networkDate: Date(), + log: log + ) + status = statusInCoreData } + // use constrain to avoid repeated save + let property = MastodonNotification.Property(id: notification.id, typeRaw: notification.type.rawValue, account: mastodonUser, status: status, createdAt: notification.createdAt) + let notification = MastodonNotification.insert(into: self.backgroundManagedObjectContext, domain: domain, userID: userID, networkDate: response.networkDate, property: property) + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)", (#file as NSString).lastPathComponent, #line, #function, notification.typeRaw, notification.account.username) } - .setFailureType(to: Error.self) - .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Notification]> in - switch result { - case .success: - return response - case .failure(let error): - throw error - } + } + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Notification]> in + switch result { + case .success: + return response + case .failure(let error): + throw error } - .eraseToAnyPublisher() } .eraseToAnyPublisher() + } + .eraseToAnyPublisher() } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift index 1cc54add..b0ab13ed 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift @@ -8,7 +8,7 @@ import Combine import Foundation -public extension Mastodon.API.Notifications { +extension Mastodon.API.Notifications { internal static func notificationsEndpointURL(domain: String) -> URL { Mastodon.API.endpointURL(domain: domain).appendingPathComponent("notifications") } @@ -31,7 +31,7 @@ public extension Mastodon.API.Notifications { /// - query: `NotificationsQuery` with query parameters /// - authorization: User token /// - Returns: `AnyPublisher` contains `Token` nested in the response - static func getNotifications( + public static func getNotifications( session: URLSession, domain: String, query: Mastodon.API.Notifications.Query, @@ -64,7 +64,7 @@ public extension Mastodon.API.Notifications { /// - notificationID: ID of the notification. /// - authorization: User token /// - Returns: `AnyPublisher` contains `Token` nested in the response - static func getNotification( + public static func getNotification( session: URLSession, domain: String, notificationID: String, @@ -82,18 +82,10 @@ public extension Mastodon.API.Notifications { } .eraseToAnyPublisher() } - - static func allExcludeTypes() -> [Mastodon.Entity.Notification.NotificationType] { - [.followRequest] - } - - static func mentionsExcludeTypes() -> [Mastodon.Entity.Notification.NotificationType] { - [.follow, .followRequest, .favourite, .reblog, .poll] - } } -public extension Mastodon.API.Notifications { - struct Query: Codable, PagedQueryType, GetQuery { +extension Mastodon.API.Notifications { + public struct Query: PagedQueryType, GetQuery { public let maxID: Mastodon.Entity.Status.ID? public let sinceID: Mastodon.Entity.Status.ID? public let minID: Mastodon.Entity.Status.ID? From cfdd2ea6705c1ed07a3fd04895527375040d5387 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 16 Apr 2021 21:55:09 +0800 Subject: [PATCH 16/23] chore: use stackView --- Mastodon.xcodeproj/project.pbxproj | 4 + Mastodon/Extension/UIView+Remove.swift | 18 +++ .../NotificationViewController.swift | 2 +- .../NotificationViewModel+diffable.swift | 4 +- .../NotificationStatusTableViewCell.swift | 130 ++++++++++++------ .../NotificationTableViewCell.swift | 92 +++++++++---- 6 files changed, 176 insertions(+), 74 deletions(-) create mode 100644 Mastodon/Extension/UIView+Remove.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3244dcd3..19506bb9 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -136,6 +136,7 @@ 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; + 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */; }; 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; 5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; }; @@ -525,6 +526,7 @@ 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = ""; }; 5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; 5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = ""; }; + 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = ""; }; 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = ""; }; 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = ""; }; 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = ""; }; @@ -1614,6 +1616,7 @@ 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, + 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */, 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */, @@ -2329,6 +2332,7 @@ DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */, 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, + 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */, 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, 2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */, diff --git a/Mastodon/Extension/UIView+Remove.swift b/Mastodon/Extension/UIView+Remove.swift new file mode 100644 index 00000000..473b3c34 --- /dev/null +++ b/Mastodon/Extension/UIView+Remove.swift @@ -0,0 +1,18 @@ +// +// UIView+Remove.swift +// Mastodon +// +// Created by xiaojian sun on 2021/4/16. +// + +import Foundation +import UIKit + +extension UIView { + func removeFromStackView() { + if let stackView = self.superview as? UIStackView { + stackView.removeArrangedSubview(self) + } + self.removeFromSuperview() + } +} diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index ddc997a5..3fb86259 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -35,7 +35,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.tableFooterView = UIView() - tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = UITableView.automaticDimension return tableView }() diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index 774620f8..c65b5096 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -74,9 +74,7 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { newSnapshot.appendItems([.bottomLoader], toSection: .main) } guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else { - diffableDataSource.apply(newSnapshot, animatingDifferences: false) { - tableView.reloadData() - } + diffableDataSource.apply(newSnapshot, animatingDifferences: false) self.isFetchingLatestNotification.value = false return } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index dc3f49bb..5d1a14be 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -11,7 +11,6 @@ import UIKit final class NotificationStatusTableViewCell: UITableViewCell { static let actionImageBorderWidth: CGFloat = 2 - static let statusPadding = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24) var disposeBag = Set() var pollCountdownSubscription: AnyCancellable? @@ -42,6 +41,11 @@ final class NotificationStatusTableViewCell: UITableViewCell { return view }() + let avatarContainer: UIView = { + let view = UIView() + return view + }() + let actionLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.secondary.color @@ -103,53 +107,99 @@ final class NotificationStatusTableViewCell: UITableViewCell { extension NotificationStatusTableViewCell { func configure() { - - let container = UIView() - container.backgroundColor = .clear - contentView.addSubview(container) - container.constrain([ - container.topAnchor.constraint(equalTo: contentView.topAnchor), - container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + let containerStackView = UIStackView() + containerStackView.axis = .horizontal + containerStackView.alignment = .top + containerStackView.spacing = 4 + containerStackView.layoutMargins = UIEdgeInsets(top: 14, left: 0, bottom: 12, right: 0) + containerStackView.isLayoutMarginsRelativeArrangement = true + containerStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor), ]) - - container.addSubview(avatatImageView) - avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) - avatatImageView.pin(top: 12, left: 0, bottom: nil, right: nil) - - container.addSubview(actionImageBackground) - actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationTableViewCell.actionImageBorderWidth, height: 24 + NotificationTableViewCell.actionImageBorderWidth)) - actionImageBackground.pin(top: 33, left: 21, bottom: nil, right: nil) - - actionImageBackground.addSubview(actionImageView) - actionImageView.constrainToCenter() - container.addSubview(nameLabel) - nameLabel.constrain([ - nameLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 12), - nameLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 61) + containerStackView.addArrangedSubview(avatarContainer) + avatarContainer.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + avatarContainer.heightAnchor.constraint(equalToConstant: 47).priority(.required - 1), + avatarContainer.widthAnchor.constraint(equalToConstant: 47).priority(.required - 1) + ]) + + avatarContainer.addSubview(avatatImageView) + avatatImageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + avatatImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1), + avatatImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1), + avatatImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor), + avatatImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor) + ]) + + avatarContainer.addSubview(actionImageBackground) + actionImageBackground.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + actionImageBackground.heightAnchor.constraint(equalToConstant: 24 + NotificationTableViewCell.actionImageBorderWidth).priority(.required - 1), + actionImageBackground.widthAnchor.constraint(equalToConstant: 24 + NotificationTableViewCell.actionImageBorderWidth).priority(.required - 1), + actionImageBackground.bottomAnchor.constraint(equalTo: avatarContainer.bottomAnchor), + actionImageBackground.trailingAnchor.constraint(equalTo: avatarContainer.trailingAnchor) + ]) + + avatarContainer.addSubview(actionImageView) + actionImageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + actionImageView.centerXAnchor.constraint(equalTo: actionImageBackground.centerXAnchor), + actionImageView.centerYAnchor.constraint(equalTo: actionImageBackground.centerYAnchor) + ]) + + + let actionStackView = UIStackView() + actionStackView.axis = .horizontal + actionStackView.distribution = .fillProportionally + actionStackView.spacing = 4 + actionStackView.translatesAutoresizingMaskIntoConstraints = false + + nameLabel.translatesAutoresizingMaskIntoConstraints = false + actionStackView.addArrangedSubview(nameLabel) + actionLabel.translatesAutoresizingMaskIntoConstraints = false + actionStackView.addArrangedSubview(actionLabel) + nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) + + let statusStackView = UIStackView() + statusStackView.axis = .vertical + + statusStackView.distribution = .fill + statusStackView.spacing = 4 + statusStackView.translatesAutoresizingMaskIntoConstraints = false + statusView.translatesAutoresizingMaskIntoConstraints = false + statusStackView.addArrangedSubview(actionStackView) + + statusBorder.translatesAutoresizingMaskIntoConstraints = false + statusView.translatesAutoresizingMaskIntoConstraints = false + statusBorder.addSubview(statusView) + NSLayoutConstraint.activate([ + statusView.topAnchor.constraint(equalTo: statusBorder.topAnchor, constant: 12), + statusView.leadingAnchor.constraint(equalTo: statusBorder.leadingAnchor, constant: 12), + statusBorder.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 12), + statusBorder.trailingAnchor.constraint(equalTo: statusView.trailingAnchor, constant: 12), ]) - container.addSubview(actionLabel) - actionLabel.constrain([ - actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), - actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), - container.trailingAnchor.constraint(greaterThanOrEqualTo: actionLabel.trailingAnchor, constant: 4) - ]) + + statusStackView.addArrangedSubview(statusBorder) + + containerStackView.addArrangedSubview(statusStackView) statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color statusView.isUserInteractionEnabled = false // remove item don't display - statusView.actionToolbarContainer.isHidden = true - statusView.avatarView.isHidden = true - statusView.usernameLabel.isHidden = true - - container.addSubview(statusBorder) - statusBorder.pin(top: 40, left: 63, bottom: 14, right: 14) - - container.addSubview(statusView) - statusView.pin(top: NotificationStatusTableViewCell.statusPadding.top, left: NotificationStatusTableViewCell.statusPadding.left, bottom: NotificationStatusTableViewCell.statusPadding.bottom, right: NotificationStatusTableViewCell.statusPadding.right) + statusView.actionToolbarContainer.removeFromStackView() + // it affect stackView's height + statusView.avatarView.removeFromStackView() + statusView.usernameLabel.removeFromStackView() + statusView.nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) + statusView.activeTextLabel.setContentCompressionResistancePriority(.required, for: .vertical) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index cda4d75d..1b7560a7 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -50,6 +50,11 @@ final class NotificationTableViewCell: UITableViewCell { return view }() + let avatarContainer: UIView = { + let view = UIView() + return view + }() + let actionLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.secondary.color @@ -86,40 +91,67 @@ final class NotificationTableViewCell: UITableViewCell { extension NotificationTableViewCell { func configure() { - let container = UIView() - container.backgroundColor = .clear - contentView.addSubview(container) - container.constrain([ - container.topAnchor.constraint(equalTo: contentView.topAnchor), - container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + let containerStackView = UIStackView() + containerStackView.axis = .horizontal + containerStackView.alignment = .center + containerStackView.spacing = 4 + containerStackView.layoutMargins = UIEdgeInsets(top: 14, left: 0, bottom: 12, right: 0) + containerStackView.isLayoutMarginsRelativeArrangement = true + containerStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor), ]) - - container.addSubview(avatatImageView) - avatatImageView.pin(toSize: CGSize(width: 35, height: 35)) - avatatImageView.pin(top: 12, left: 0, bottom: nil, right: nil) - - container.addSubview(actionImageBackground) - actionImageBackground.pin(toSize: CGSize(width: 24 + NotificationTableViewCell.actionImageBorderWidth, height: 24 + NotificationTableViewCell.actionImageBorderWidth)) - actionImageBackground.pin(top: 33, left: 33, bottom: nil, right: nil) - - actionImageBackground.addSubview(actionImageView) - actionImageView.constrainToCenter() - container.addSubview(nameLabel) - nameLabel.constrain([ - nameLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 24), - container.bottomAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 24), - nameLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 61) + containerStackView.addArrangedSubview(avatarContainer) + avatarContainer.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + avatarContainer.heightAnchor.constraint(equalToConstant: 47).priority(.required - 1), + avatarContainer.widthAnchor.constraint(equalToConstant: 47).priority(.required - 1) ]) + + avatarContainer.addSubview(avatatImageView) + avatatImageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + avatatImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1), + avatatImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1), + avatatImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor), + avatatImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor) + ]) + + avatarContainer.addSubview(actionImageBackground) + actionImageBackground.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + actionImageBackground.heightAnchor.constraint(equalToConstant: 24 + NotificationTableViewCell.actionImageBorderWidth).priority(.required - 1), + actionImageBackground.widthAnchor.constraint(equalToConstant: 24 + NotificationTableViewCell.actionImageBorderWidth).priority(.required - 1), + actionImageBackground.bottomAnchor.constraint(equalTo: avatarContainer.bottomAnchor), + actionImageBackground.trailingAnchor.constraint(equalTo: avatarContainer.trailingAnchor) + ]) + + avatarContainer.addSubview(actionImageView) + actionImageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + actionImageView.centerXAnchor.constraint(equalTo: actionImageBackground.centerXAnchor), + actionImageView.centerYAnchor.constraint(equalTo: actionImageBackground.centerYAnchor) + ]) + + + let actionStackView = UIStackView() + actionStackView.axis = .horizontal + actionStackView.distribution = .fillProportionally + actionStackView.spacing = 4 + actionStackView.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(actionLabel) - actionLabel.constrain([ - actionLabel.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 4), - actionLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), - container.trailingAnchor.constraint(greaterThanOrEqualTo: actionLabel.trailingAnchor, constant: 4) - ]) + nameLabel.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(nameLabel) + actionLabel.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(actionLabel) + nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) + containerStackView.addArrangedSubview(actionStackView) + } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { From 780025a3ceee83cb5cbccf1d596a942982ae9100 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 16 Apr 2021 22:58:36 +0800 Subject: [PATCH 17/23] chore: use systemBackground color --- Mastodon/Generated/Assets.swift | 1 - .../Background/pure.colorset/Contents.json | 38 ------------------- .../NotificationViewController.swift | 2 +- .../NotificationStatusTableViewCell.swift | 13 ++++--- .../NotificationTableViewCell.swift | 18 +++------ .../Scene/Search/SearchViewController.swift | 2 +- 6 files changed, 15 insertions(+), 59 deletions(-) delete mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/pure.colorset/Contents.json diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 0e0869f0..2ffc882b 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -44,7 +44,6 @@ internal enum Asset { internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor") internal static let navigationBar = ColorAsset(name: "Colors/Background/navigationBar") internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background") - internal static let pure = ColorAsset(name: "Colors/Background/pure") internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background") internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background") internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/pure.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/pure.colorset/Contents.json deleted file mode 100644 index 82edd034..00000000 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/pure.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "30", - "green" : "28", - "red" : "28" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 3fb86259..7dccb423 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -45,7 +45,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { extension NotificationViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = Asset.Colors.Background.pure.color + view.backgroundColor = Asset.Colors.Background.systemBackground.color navigationItem.titleView = segmentControl segmentControl.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .valueChanged) view.addSubview(tableView) diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 5d1a14be..cdde9dbe 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -26,7 +26,7 @@ final class NotificationStatusTableViewCell: UITableViewCell { let actionImageView: UIImageView = { let imageView = UIImageView() - imageView.tintColor = Asset.Colors.Background.pure.color + imageView.tintColor = Asset.Colors.Background.systemBackground.color return imageView }() @@ -36,8 +36,8 @@ final class NotificationStatusTableViewCell: UITableViewCell { view.layer.cornerCurve = .continuous view.clipsToBounds = true view.layer.borderWidth = NotificationStatusTableViewCell.actionImageBorderWidth - view.layer.borderColor = Asset.Colors.Background.pure.color.cgColor - view.tintColor = Asset.Colors.Background.pure.color + view.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor + view.tintColor = Asset.Colors.Background.systemBackground.color return view }() @@ -157,7 +157,7 @@ extension NotificationStatusTableViewCell { let actionStackView = UIStackView() actionStackView.axis = .horizontal - actionStackView.distribution = .fillProportionally + actionStackView.distribution = .fill actionStackView.spacing = 4 actionStackView.translatesAutoresizingMaskIntoConstraints = false @@ -166,7 +166,8 @@ extension NotificationStatusTableViewCell { actionLabel.translatesAutoresizingMaskIntoConstraints = false actionStackView.addArrangedSubview(actionLabel) nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) - + nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + actionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) let statusStackView = UIStackView() statusStackView.axis = .vertical @@ -205,6 +206,6 @@ extension NotificationStatusTableViewCell { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor - actionImageBackground.layer.borderColor = Asset.Colors.Background.pure.color.cgColor + actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor } } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 1b7560a7..60b43ac3 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -35,7 +35,7 @@ final class NotificationTableViewCell: UITableViewCell { let actionImageView: UIImageView = { let imageView = UIImageView() - imageView.tintColor = Asset.Colors.Background.pure.color + imageView.tintColor = Asset.Colors.Background.systemBackground.color return imageView }() @@ -45,8 +45,8 @@ final class NotificationTableViewCell: UITableViewCell { view.layer.cornerCurve = .continuous view.clipsToBounds = true view.layer.borderWidth = NotificationTableViewCell.actionImageBorderWidth - view.layer.borderColor = Asset.Colors.Background.pure.color.cgColor - view.tintColor = Asset.Colors.Background.pure.color + view.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor + view.tintColor = Asset.Colors.Background.systemBackground.color return view }() @@ -137,25 +137,19 @@ extension NotificationTableViewCell { actionImageView.centerXAnchor.constraint(equalTo: actionImageBackground.centerXAnchor), actionImageView.centerYAnchor.constraint(equalTo: actionImageBackground.centerYAnchor) ]) - - - let actionStackView = UIStackView() - actionStackView.axis = .horizontal - actionStackView.distribution = .fillProportionally - actionStackView.spacing = 4 - actionStackView.translatesAutoresizingMaskIntoConstraints = false nameLabel.translatesAutoresizingMaskIntoConstraints = false containerStackView.addArrangedSubview(nameLabel) actionLabel.translatesAutoresizingMaskIntoConstraints = false containerStackView.addArrangedSubview(actionLabel) nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) - containerStackView.addArrangedSubview(actionStackView) + nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + actionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - actionImageBackground.layer.borderColor = Asset.Colors.Background.pure.color.cgColor + actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor } } diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 4fee226b..710808b5 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -82,7 +82,7 @@ final class SearchViewController: UIViewController, NeedsDependency { // searching let searchingTableView: UITableView = { let tableView = UITableView() - tableView.backgroundColor = Asset.Colors.Background.pure.color + tableView.backgroundColor = Asset.Colors.Background.systemBackground.color tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .singleLine tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) From 462fafe706090cfbd1a8526540c2e12e59e2d184 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Sun, 18 Apr 2021 22:00:19 +0800 Subject: [PATCH 18/23] chore: add navigation to ThreadScene --- .../NotificationViewController.swift | 38 +++++++++---------- .../NotificationViewModel+diffable.swift | 1 + 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 7dccb423..68e69b4b 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -16,16 +16,16 @@ import UIKit final class NotificationViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - + var disposeBag = Set() private(set) lazy var viewModel = NotificationViewModel(context: context) - + let segmentControl: UISegmentedControl = { let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything, L10n.Scene.Notification.Title.mentions]) control.selectedSegmentIndex = 0 return control }() - + let tableView: UITableView = { let tableView = ControlContainableTableView() tableView.rowHeight = UITableView.automaticDimension @@ -38,7 +38,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { tableView.estimatedRowHeight = UITableView.automaticDimension return tableView }() - + let refreshControl = UIRefreshControl() } @@ -55,10 +55,10 @@ extension NotificationViewController { tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - + tableView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(NotificationViewController.refreshControlValueChanged(_:)), for: .valueChanged) - + tableView.delegate = self viewModel.tableView = tableView viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self @@ -78,10 +78,10 @@ extension NotificationViewController { } .store(in: &disposeBag) } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + // needs trigger manually after onboarding dismiss setNeedsStatusBarAppearanceUpdate() } @@ -96,7 +96,7 @@ extension NotificationViewController { } } } - + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) @@ -117,11 +117,11 @@ extension NotificationViewController { if sender.selectedSegmentIndex == 0 { viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) } else { - viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain,userID: userID, typeRaw: Mastodon.Entity.Notification.NotificationType.mention.rawValue) + viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID, typeRaw: Mastodon.Entity.Notification.NotificationType.mention.rawValue) } - viewModel.selectedIndex.value = NotificationViewModel.NotificationSegment.init(rawValue: sender.selectedSegmentIndex)! + viewModel.selectedIndex.value = NotificationViewModel.NotificationSegment(rawValue: sender.selectedSegmentIndex)! } - + @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { guard viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) else { sender.endRefreshing() @@ -134,24 +134,24 @@ extension NotificationViewController { extension NotificationViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { case .notification(let objectID): let notification = context.managedObjectContext.object(with: objectID) as! MastodonNotification - if notification.status != nil { - // TODO: goto status detail vc + if let status = notification.status { + let viewModel = ThreadViewModel(context: context, optionalStatus: status) + coordinator.present(scene: .thread(viewModel: viewModel), from: self, transition: .show) } else { let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account) - DispatchQueue.main.async { - self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show) - } + coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show) } default: break } } - + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } @@ -181,7 +181,7 @@ extension NotificationViewController: NotificationTableViewCellDelegate { self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show) } } - + func parent() -> UIViewController { self } diff --git a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift index c65b5096..5bd2d92d 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+diffable.swift @@ -76,6 +76,7 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate { guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else { diffableDataSource.apply(newSnapshot, animatingDifferences: false) self.isFetchingLatestNotification.value = false + tableView.reloadData() return } From 5ae2c446422fc788f8b59a50b56693cb0762e42d Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 19 Apr 2021 10:10:46 +0800 Subject: [PATCH 19/23] chore: remove useless code --- .../TableViewCell/NotificationStatusTableViewCell.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index cdde9dbe..aae4c9e2 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -196,11 +196,9 @@ extension NotificationStatusTableViewCell { statusView.isUserInteractionEnabled = false // remove item don't display statusView.actionToolbarContainer.removeFromStackView() - // it affect stackView's height + // it affect stackView's height,need remove statusView.avatarView.removeFromStackView() statusView.usernameLabel.removeFromStackView() - statusView.nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) - statusView.activeTextLabel.setContentCompressionResistancePriority(.required, for: .vertical) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { From f6be51dd0fff8ba2b09b9f53da37235529e415b3 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 19 Apr 2021 11:41:50 +0800 Subject: [PATCH 20/23] chore: remove UIView+Contraint --- Mastodon.xcodeproj/project.pbxproj | 4 - Mastodon/Extension/UIView+Constraint.swift | 262 ------------------ .../NotificationViewController.swift | 3 +- ...hRecommendAccountsCollectionViewCell.swift | 77 +++-- ...earchRecommendTagsCollectionViewCell.swift | 59 +++- .../SearchViewController+Recommend.swift | 6 +- .../SearchViewController+Searching.swift | 31 ++- .../Scene/Search/SearchViewController.swift | 14 +- Mastodon/Scene/Search/SearchViewModel.swift | 4 +- .../SearchingTableViewCell.swift | 40 ++- .../SearchRecommendCollectionHeader.swift | 33 ++- 11 files changed, 191 insertions(+), 342 deletions(-) delete mode 100644 Mastodon/Extension/UIView+Constraint.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 427ffa0d..659d0d73 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -132,7 +132,6 @@ 2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; }; 2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */; }; 2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */; }; - 2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */; }; 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; @@ -531,7 +530,6 @@ 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = ""; }; 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Searching.swift"; sourceTree = ""; }; 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingTableViewCell.swift; sourceTree = ""; }; - 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraint.swift"; sourceTree = ""; }; 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1646,7 +1644,6 @@ DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */, - 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */, 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, @@ -2476,7 +2473,6 @@ 2D7867192625B77500211898 /* NotificationItem.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, - 2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, diff --git a/Mastodon/Extension/UIView+Constraint.swift b/Mastodon/Extension/UIView+Constraint.swift deleted file mode 100644 index ded8846d..00000000 --- a/Mastodon/Extension/UIView+Constraint.swift +++ /dev/null @@ -1,262 +0,0 @@ -// -// UIView+Constraint.swift -// Mastodon -// -// Created by sxiaojian on 2021/3/31. -// - -import UIKit - -enum Dimension { - case width - case height - - var layoutAttribute: NSLayoutConstraint.Attribute { - switch self { - case .width: - return .width - case .height: - return .height - } - } - -} - -extension UIView { - - func constrain(toSuperviewEdges: UIEdgeInsets?) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return} - translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - NSLayoutConstraint(item: self, - attribute: .leading, - relatedBy: .equal, - toItem: view, - attribute: .leading, - multiplier: 1.0, - constant: toSuperviewEdges?.left ?? 0.0), - NSLayoutConstraint(item: self, - attribute: .top, - relatedBy: .equal, - toItem: view, - attribute: .top, - multiplier: 1.0, - constant: toSuperviewEdges?.top ?? 0.0), - NSLayoutConstraint(item: view, - attribute: .trailing, - relatedBy: .equal, - toItem: self, - attribute: .trailing, - multiplier: 1.0, - constant: toSuperviewEdges?.right ?? 0.0), - NSLayoutConstraint(item: view, - attribute: .bottom, - relatedBy: .equal, - toItem: self, - attribute: .bottom, - multiplier: 1.0, - constant: toSuperviewEdges?.bottom ?? 0.0) - ]) - } - - func constrain(_ constraints: [NSLayoutConstraint?]) { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate(constraints.compactMap { $0 }) - } - - func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView, constant: CGFloat?) -> NSLayoutConstraint? { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil} - translatesAutoresizingMaskIntoConstraints = false - return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: constant ?? 0.0) - } - - func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView) -> NSLayoutConstraint? { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil} - translatesAutoresizingMaskIntoConstraints = false - return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: 0.0) - } - - func constraint(_ dimension: Dimension, constant: CGFloat) -> NSLayoutConstraint? { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } - translatesAutoresizingMaskIntoConstraints = false - return NSLayoutConstraint(item: self, - attribute: dimension.layoutAttribute, - relatedBy: .equal, - toItem: nil, - attribute: .notAnAttribute, - multiplier: 1.0, - constant: constant) - } - - func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat, topLayoutGuide: UILayoutSupport) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.leading, toView: view, constant: sidePadding), - NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: topLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: topPadding), - constraint(.trailing, toView: view, constant: -sidePadding) - ]) - } - - func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.leading, toView: view, constant: sidePadding), - constraint(.top, toView: view, constant: topPadding), - constraint(.trailing, toView: view, constant: -sidePadding) - ]) - } - - func constrainTopCorners(height: CGFloat) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.leading, toView: view), - constraint(.top, toView: view), - constraint(.trailing, toView: view), - constraint(.height, constant: height) - ]) - } - - func constrainBottomCorners(sidePadding: CGFloat, bottomPadding: CGFloat) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.leading, toView: view, constant: sidePadding), - constraint(.bottom, toView: view, constant: -bottomPadding), - constraint(.trailing, toView: view, constant: -sidePadding) - ]) - } - - func constrainBottomCorners(height: CGFloat) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.leading, toView: view), - constraint(.bottom, toView: view), - constraint(.trailing, toView: view), - constraint(.height, constant: height) - ]) - } - - func constrainLeadingCorners() { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.top, toView: view), - constraint(.leading, toView: view), - constraint(.bottom, toView: view) - ]) - } - - func constrainTrailingCorners() { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.top, toView: view), - constraint(.trailing, toView: view), - constraint(.bottom, toView: view) - ]) - } - - func constrainToCenter() { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - constraint(.centerX, toView: view), - constraint(.centerY, toView: view) - ]) - } - - func pin(toSize: CGSize) { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - widthAnchor.constraint(equalToConstant: toSize.width).priority(.required - 1), - heightAnchor.constraint(equalToConstant: toSize.height).priority(.required - 1) - ]) - } - - func pin(top: CGFloat?,left: CGFloat?,bottom: CGFloat?, right: CGFloat?) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - var constraints = [NSLayoutConstraint]() - if let topConstant = top { - constraints.append(topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant)) - } - if let leftConstant = left { - constraints.append(leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: leftConstant)) - } - if let bottomConstant = bottom { - constraints.append(view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottomConstant)) - } - if let rightConstant = right { - constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: rightConstant)) - } - constrain(constraints) - - } - func pinTopLeft(padding: CGFloat) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding), - topAnchor.constraint(equalTo: view.topAnchor, constant: padding)]) - } - - func pinTopLeft(top: CGFloat, left: CGFloat) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: left), - topAnchor.constraint(equalTo: view.topAnchor, constant: top)]) - } - - func pinTopRight(padding: CGFloat) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: padding), - topAnchor.constraint(equalTo: view.topAnchor, constant: padding)]) - } - - func pinTopRight(top: CGFloat, right: CGFloat) { - guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: right), - topAnchor.constraint(equalTo: view.topAnchor, constant: top)]) - } - - func pinTopLeft(toView: UIView, topPadding: CGFloat) { - guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } - translatesAutoresizingMaskIntoConstraints = false - constrain([ - leadingAnchor.constraint(equalTo: toView.leadingAnchor), - topAnchor.constraint(equalTo: toView.bottomAnchor, constant: topPadding)]) - } - - /// Cross-fades between two views by animating their alpha then setting one or the other hidden. - /// - parameters: - /// - lhs: left view - /// - rhs: right view - /// - toRight: fade to the right view if true, fade to the left view if false - /// - duration: animation duration - /// - static func crossfade(_ lhs: UIView, _ rhs: UIView, toRight: Bool, duration: TimeInterval) { - lhs.alpha = toRight ? 1.0 : 0.0 - rhs.alpha = toRight ? 0.0 : 1.0 - lhs.isHidden = false - rhs.isHidden = false - - UIView.animate(withDuration: duration, animations: { - lhs.alpha = toRight ? 0.0 : 1.0 - rhs.alpha = toRight ? 1.0 : 0.0 - }, completion: { _ in - lhs.isHidden = toRight - rhs.isHidden = !toRight - }) - } -} diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 68e69b4b..90d72afa 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -48,8 +48,9 @@ extension NotificationViewController { view.backgroundColor = Asset.Colors.Background.systemBackground.color navigationItem.titleView = segmentControl segmentControl.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .valueChanged) + tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) - tableView.constrain([ + NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index f4512467..fdb1af56 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -104,43 +104,60 @@ extension SearchRecommendAccountsCollectionViewCell { layer.cornerCurve = .continuous clipsToBounds = false applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0) + + headerImageView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(headerImageView) - headerImageView.pin(top: 16, left: 0, bottom: 0, right: 0) + NSLayoutConstraint.activate([ + headerImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + headerImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + headerImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + headerImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + let containerStackView = UIStackView() + containerStackView.axis = .vertical + containerStackView.distribution = .fill + containerStackView.alignment = .center + containerStackView.spacing = 6 + containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) + containerStackView.isLayoutMarginsRelativeArrangement = true + containerStackView.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + + avatarImageView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(avatarImageView) - avatarImageView.pin(toSize: CGSize(width: 88, height: 88)) - avatarImageView.constrain([ - avatarImageView.constraint(.top, toView: contentView), - avatarImageView.constraint(.centerX, toView: contentView) + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: 88), + avatarImageView.heightAnchor.constraint(equalToConstant: 88) ]) + containerStackView.addArrangedSubview(avatarImageView) + containerStackView.setCustomSpacing(20, after: avatarImageView) + displayNameLabel.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(displayNameLabel) + containerStackView.setCustomSpacing(0, after: displayNameLabel) - contentView.addSubview(displayNameLabel) - displayNameLabel.constrain([ - displayNameLabel.constraint(.top, toView: contentView, constant: 108), - displayNameLabel.constraint(.leading, toView: contentView), - displayNameLabel.constraint(.trailing, toView: contentView), - displayNameLabel.constraint(.centerX, toView: contentView) - ]) + acctLabel.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(acctLabel) + containerStackView.setCustomSpacing(7, after: acctLabel) - contentView.addSubview(acctLabel) - acctLabel.constrain([ - acctLabel.constraint(.top, toView: contentView, constant: 132), - acctLabel.constraint(.leading, toView: contentView), - acctLabel.constraint(.trailing, toView: contentView), - acctLabel.constraint(.centerX, toView: contentView) - ]) - - contentView.addSubview(followButton) - followButton.pin(toSize: CGSize(width: 76, height: 24)) - followButton.constrain([ - followButton.constraint(.top, toView: contentView, constant: 159), - followButton.constraint(.centerX, toView: contentView) + followButton.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(followButton) + NSLayoutConstraint.activate([ + followButton.widthAnchor.constraint(equalToConstant: 76), + followButton.heightAnchor.constraint(equalToConstant: 24) ]) + containerStackView.addArrangedSubview(followButton) } func config(with mastodonUser: MastodonUser) { displayNameLabel.text = mastodonUser.displayName.isEmpty ? mastodonUser.username : mastodonUser.displayName - acctLabel.text = mastodonUser.acct + acctLabel.text = "@" + mastodonUser.acct avatarImageView.af.setImage( withURL: URL(string: mastodonUser.avatar)!, placeholderImage: UIImage.placeholder(color: .systemFill), @@ -153,7 +170,13 @@ extension SearchRecommendAccountsCollectionViewCell { ) { [weak self] _ in guard let self = self else { return } self.headerImageView.addSubview(self.visualEffectView) - self.visualEffectView.pin(top: 0, left: 0, bottom: 0, right: 0) + self.visualEffectView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + self.visualEffectView.topAnchor.constraint(equalTo: self.headerImageView.topAnchor), + self.visualEffectView.leadingAnchor.constraint(equalTo: self.headerImageView.leadingAnchor), + self.visualEffectView.trailingAnchor.constraint(equalTo: self.headerImageView.trailingAnchor), + self.visualEffectView.bottomAnchor.constraint(equalTo: self.headerImageView.bottomAnchor) + ]) } delegate?.configFollowButton(with: mastodonUser, followButton: followButton) followButton.publisher(for: .touchUpInside) diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index d00cb050..81167ee6 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -12,7 +12,6 @@ import UIKit class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { let backgroundImageView: UIImageView = { let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() @@ -20,7 +19,6 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { let label = UILabel() label.textColor = .white label.font = .systemFont(ofSize: 20, weight: .semibold) - label.translatesAutoresizingMaskIntoConstraints = false label.lineBreakMode = .byTruncatingTail return label }() @@ -29,7 +27,6 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { let label = UILabel() label.textColor = .white label.font = .preferredFont(forTextStyle: .body) - label.translatesAutoresizingMaskIntoConstraints = false return label }() @@ -38,7 +35,6 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { let image = UIImage(systemName: "flame.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold))!.withRenderingMode(.alwaysTemplate) imageView.image = image imageView.tintColor = .white - imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() @@ -74,17 +70,58 @@ extension SearchRecommendTagsCollectionViewCell { layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0) + backgroundImageView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(backgroundImageView) - backgroundImageView.constrain(toSuperviewEdges: nil) + NSLayoutConstraint.activate([ + backgroundImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + backgroundImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + backgroundImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + backgroundImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) - contentView.addSubview(hashtagTitleLabel) - hashtagTitleLabel.pin(top: 16, left: 16, bottom: nil, right: 42) - contentView.addSubview(peopleLabel) - peopleLabel.pinTopLeft(top: 46, left: 16) + let containerStackView = UIStackView() + containerStackView.axis = .vertical + containerStackView.distribution = .fill + containerStackView.spacing = 6 + containerStackView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16) + containerStackView.isLayoutMarginsRelativeArrangement = true + containerStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) - contentView.addSubview(flameIconView) - flameIconView.pinTopRight(padding: 16) + + let horizontalStackView = UIStackView() + horizontalStackView.axis = .horizontal + horizontalStackView.translatesAutoresizingMaskIntoConstraints = false + horizontalStackView.distribution = .fill + + hashtagTitleLabel.translatesAutoresizingMaskIntoConstraints = false + hashtagTitleLabel.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) + horizontalStackView.addArrangedSubview(hashtagTitleLabel) + horizontalStackView.setContentHuggingPriority(.required - 1, for: .vertical) + + flameIconView.translatesAutoresizingMaskIntoConstraints = false + horizontalStackView.addArrangedSubview(flameIconView) + + + containerStackView.addArrangedSubview(horizontalStackView) + + let peopleHorizontalStackView = UIStackView() + peopleHorizontalStackView.axis = .horizontal + peopleHorizontalStackView.translatesAutoresizingMaskIntoConstraints = false + peopleHorizontalStackView.distribution = .fill + peopleHorizontalStackView.alignment = .top + peopleLabel.translatesAutoresizingMaskIntoConstraints = false + peopleLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical) + peopleHorizontalStackView.addArrangedSubview(peopleLabel) + + containerStackView.addArrangedSubview(peopleHorizontalStackView) } func config(with tag: Mastodon.Entity.Tag) { diff --git a/Mastodon/Scene/Search/SearchViewController+Recommend.swift b/Mastodon/Scene/Search/SearchViewController+Recommend.swift index e941fa84..f394f09f 100644 --- a/Mastodon/Scene/Search/SearchViewController+Recommend.swift +++ b/Mastodon/Scene/Search/SearchViewController+Recommend.swift @@ -23,8 +23,9 @@ extension SearchViewController { hashtagCollectionView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self)) hashtagCollectionView.delegate = self + hashtagCollectionView.translatesAutoresizingMaskIntoConstraints = false stackView.addArrangedSubview(hashtagCollectionView) - hashtagCollectionView.constrain([ + NSLayoutConstraint.activate([ hashtagCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 130) ]) } @@ -39,8 +40,9 @@ extension SearchViewController { accountsCollectionView.register(SearchRecommendAccountsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self)) accountsCollectionView.delegate = self + accountsCollectionView.translatesAutoresizingMaskIntoConstraints = false stackView.addArrangedSubview(accountsCollectionView) - accountsCollectionView.constrain([ + NSLayoutConstraint.activate([ accountsCollectionView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 202) ]) } diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift index 86a27e03..8eaf3632 100644 --- a/Mastodon/Scene/Search/SearchViewController+Searching.swift +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -19,7 +19,8 @@ extension SearchViewController { searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self)) searchingTableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) view.addSubview(searchingTableView) - searchingTableView.constrain([ + searchingTableView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor), searchingTableView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), searchingTableView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), @@ -50,18 +51,23 @@ extension SearchViewController { } func setupSearchHeader() { - searchHeader.addSubview(recentSearchesLabel) - recentSearchesLabel.constrain([ - recentSearchesLabel.constraint(.leading, toView: searchHeader, constant: 16), - recentSearchesLabel.constraint(.centerY, toView: searchHeader) + let containerStackView = UIStackView() + containerStackView.axis = .horizontal + containerStackView.distribution = .fill + containerStackView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) + containerStackView.isLayoutMarginsRelativeArrangement = true + searchHeader.addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: searchHeader.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: searchHeader.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: searchHeader.trailingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: searchHeader.bottomAnchor) ]) - - searchHeader.addSubview(clearSearchHistoryButton) - recentSearchesLabel.constrain([ - searchHeader.trailingAnchor.constraint(equalTo: clearSearchHistoryButton.trailingAnchor, constant: 16), - clearSearchHistoryButton.constraint(.centerY, toView: searchHeader) - ]) - + recentSearchesLabel.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(recentSearchesLabel) + clearSearchHistoryButton.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(clearSearchHistoryButton) clearSearchHistoryButton.addTarget(self, action: #selector(SearchViewController.clearAction(_:)), for: .touchUpInside) } } @@ -84,6 +90,7 @@ extension SearchViewController: UITableViewDelegate { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) guard let diffableDataSource = viewModel.searchResultDiffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } viewModel.searchResultItemDidSelected(item: item, from: self) diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 710808b5..770fb1da 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -135,14 +135,16 @@ extension SearchViewController { func setupSearchBar() { searchBar.delegate = self view.addSubview(searchBar) - searchBar.constrain([ + searchBar.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), searchBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), searchBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), ]) - view.addSubview(statusBar) - statusBar.constrain([ + statusBar.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(statusBar) + NSLayoutConstraint.activate([ statusBar.topAnchor.constraint(equalTo: view.topAnchor), statusBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), statusBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), @@ -151,8 +153,9 @@ extension SearchViewController { } func setupScrollView() { + scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) - scrollView.constrain([ + NSLayoutConstraint.activate([ scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor), scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), @@ -160,8 +163,9 @@ extension SearchViewController { scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor), ]) + stackView.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(stackView) - stackView.constrain([ + NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift index 1d87629b..27c322c8 100644 --- a/Mastodon/Scene/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -237,10 +237,10 @@ final class SearchViewModel: NSObject { .sink { completion in switch completion { case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendAccount request fail: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) promise(.failure(error)) case .finished: - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendHashTags request success", (#file as NSString).lastPathComponent, #line, #function) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: recommendAccount request success", (#file as NSString).lastPathComponent, #line, #function) promise(.success(())) } } receiveValue: { [weak self] accounts in diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift index 5a258d8a..9339e6f2 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -55,19 +55,39 @@ final class SearchingTableViewCell: UITableViewCell { extension SearchingTableViewCell { private func configure() { backgroundColor = .clear - selectionStyle = .none - contentView.addSubview(_imageView) - _imageView.pin(toSize: CGSize(width: 42, height: 42)) - _imageView.constrain([ - _imageView.constraint(.leading, toView: contentView, constant: 21), - _imageView.constraint(.centerY, toView: contentView) + + let containerStackView = UIStackView() + containerStackView.axis = .horizontal + containerStackView.distribution = .fill + containerStackView.spacing = 12 + containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 21, bottom: 12, right: 12) + containerStackView.isLayoutMarginsRelativeArrangement = true + containerStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) ]) - contentView.addSubview(_titleLabel) - _titleLabel.pin(top: 12, left: 75, bottom: nil, right: 0) + _imageView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(_imageView) + NSLayoutConstraint.activate([ + _imageView.widthAnchor.constraint(equalToConstant: 42), + _imageView.heightAnchor.constraint(equalToConstant: 42), + ]) - contentView.addSubview(_subTitleLabel) - _subTitleLabel.pin(top: 34, left: 75, bottom: nil, right: 0) + let textStackView = UIStackView() + textStackView.axis = .vertical + textStackView.distribution = .fill + textStackView.translatesAutoresizingMaskIntoConstraints = false + _titleLabel.translatesAutoresizingMaskIntoConstraints = false + textStackView.addArrangedSubview(_titleLabel) + _subTitleLabel.translatesAutoresizingMaskIntoConstraints = false + textStackView.addArrangedSubview(_subTitleLabel) + + containerStackView.addArrangedSubview(textStackView) } func config(with account: Mastodon.Entity.Account) { diff --git a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift index bc5bd766..3db8c280 100644 --- a/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/View/SearchRecommendCollectionHeader.swift @@ -47,14 +47,35 @@ extension SearchRecommendCollectionHeader { private func configure() { backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false - addSubview(titleLabel) - titleLabel.pinTopLeft(top: 31, left: 16) - addSubview(descriptionLabel) - descriptionLabel.constrain(toSuperviewEdges: UIEdgeInsets(top: 60, left: 16, bottom: 16, right: 16)) + let containerStackView = UIStackView() + containerStackView.axis = .vertical + containerStackView.layoutMargins = UIEdgeInsets(top: 31, left: 16, bottom: 16, right: 16) + containerStackView.isLayoutMarginsRelativeArrangement = true + containerStackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), + containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor) + ]) - addSubview(seeAllButton) - seeAllButton.pinTopRight(top: 26, right: 16) + let horizontalStackView = UIStackView() + horizontalStackView.axis = .horizontal + horizontalStackView.alignment = .center + horizontalStackView.translatesAutoresizingMaskIntoConstraints = false + horizontalStackView.distribution = .fill + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) + horizontalStackView.addArrangedSubview(titleLabel) + seeAllButton.translatesAutoresizingMaskIntoConstraints = false + horizontalStackView.addArrangedSubview(seeAllButton) + + containerStackView.addArrangedSubview(horizontalStackView) + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(descriptionLabel) + } } From c814065edb9583f55a56d668dd9be329d16049c0 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 19 Apr 2021 12:39:16 +0800 Subject: [PATCH 21/23] fix: Scroll view has ambiguous scrollable content height and ViewDebug warning --- .../SearchViewController+Searching.swift | 20 +++++++------------ .../SearchingTableViewCell.swift | 1 + 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Mastodon/Scene/Search/SearchViewController+Searching.swift b/Mastodon/Scene/Search/SearchViewController+Searching.swift index 8eaf3632..0602ac20 100644 --- a/Mastodon/Scene/Search/SearchViewController+Searching.swift +++ b/Mastodon/Scene/Search/SearchViewController+Searching.swift @@ -15,17 +15,18 @@ import UIKit extension SearchViewController { func setupSearchingTableView() { - searchingTableView.delegate = self searchingTableView.register(SearchingTableViewCell.self, forCellReuseIdentifier: String(describing: SearchingTableViewCell.self)) searchingTableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) + searchingTableView.estimatedRowHeight = 66 + searchingTableView.rowHeight = 66 view.addSubview(searchingTableView) + searchingTableView.delegate = self searchingTableView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - searchingTableView.frameLayoutGuide.topAnchor.constraint(equalTo: searchBar.bottomAnchor), - searchingTableView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), - searchingTableView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), - searchingTableView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - searchingTableView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor) + searchingTableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor), + searchingTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + searchingTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + searchingTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) searchingTableView.tableFooterView = UIView() viewModel.isSearching @@ -81,13 +82,6 @@ extension SearchViewController { // MARK: - UITableViewDelegate extension SearchViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - 66 - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - 66 - } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) diff --git a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift index 9339e6f2..a3a7b58a 100644 --- a/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift +++ b/Mastodon/Scene/Search/TableViewCell/SearchingTableViewCell.swift @@ -86,6 +86,7 @@ extension SearchingTableViewCell { textStackView.addArrangedSubview(_titleLabel) _subTitleLabel.translatesAutoresizingMaskIntoConstraints = false textStackView.addArrangedSubview(_subTitleLabel) + _subTitleLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical) containerStackView.addArrangedSubview(textStackView) } From bb03c10ef6a3a3486bba33db7c5ff550293a0b51 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 19 Apr 2021 17:00:51 +0800 Subject: [PATCH 22/23] chore: apply review suggestions --- Mastodon.xcodeproj/project.pbxproj | 4 + .../Section/NotificationSection.swift | 62 ++++++--------- .../Mastodon+Entity+Notification+Type.swift | 75 +++++++++++++++++++ .../NotificationViewController.swift | 6 +- ...hRecommendAccountsCollectionViewCell.swift | 10 ++- ...earchRecommendTagsCollectionViewCell.swift | 13 +--- 6 files changed, 112 insertions(+), 58 deletions(-) create mode 100644 Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 659d0d73..6c636983 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -120,6 +120,7 @@ 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; }; 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; }; 2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; }; + 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */; }; 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; }; 2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */; }; 2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */; }; @@ -518,6 +519,7 @@ 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = ""; }; 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Notification+Type.swift"; sourceTree = ""; }; 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = ""; }; 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendHashTagSection.swift; sourceTree = ""; }; 2DE0FAC72615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendAccountsCollectionViewCell.swift; sourceTree = ""; }; @@ -1450,6 +1452,7 @@ 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */, 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */, 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */, + 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */, ); path = MastodonSDK; sourceTree = ""; @@ -2455,6 +2458,7 @@ DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, 2D24E12D2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift in Sources */, + 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index e7c96139..81732c60 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -33,39 +33,15 @@ extension NotificationSection { case .notification(let objectID): let notification = managedObjectContext.object(with: objectID) as! MastodonNotification - let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) - + guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) else { + assertionFailure() + return nil + } let timeText = notification.createAt.shortTimeAgoSinceNow - var actionText: String - var actionImageName: String - var color: UIColor - switch type { - case .follow: - actionText = L10n.Scene.Notification.Action.follow - actionImageName = "person.crop.circle.badge.checkmark" - color = Asset.Colors.brandBlue.color - case .favourite: - actionText = L10n.Scene.Notification.Action.favourite - actionImageName = "star.fill" - color = Asset.Colors.Notification.favourite.color - case .reblog: - actionText = L10n.Scene.Notification.Action.reblog - actionImageName = "arrow.2.squarepath" - color = Asset.Colors.Notification.reblog.color - case .mention: - actionText = L10n.Scene.Notification.Action.mention - actionImageName = "at" - color = Asset.Colors.Notification.mention.color - case .poll: - actionText = L10n.Scene.Notification.Action.poll - actionImageName = "list.bullet" - color = Asset.Colors.brandBlue.color - default: - actionText = "" - actionImageName = "" - color = .clear - } + let actionText = type.actionText + let actionImageName = type.actionImageName + let color = type.color if let status = notification.status { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell @@ -87,11 +63,13 @@ extension NotificationSection { cell.actionImageBackground.backgroundColor = color cell.actionLabel.text = actionText + " · " + timeText cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName - cell.avatatImageView.af.setImage( - withURL: URL(string: notification.account.avatar)!, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) + if let url = notification.account.avatarImageURL() { + cell.avatatImageView.af.setImage( + withURL: url, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + } cell.avatatImageView.gesture().sink { [weak cell] _ in cell?.delegate?.userAvatarDidPressed(notification: notification) } @@ -113,11 +91,13 @@ extension NotificationSection { cell.actionImageBackground.backgroundColor = color cell.actionLabel.text = actionText + " · " + timeText cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName - cell.avatatImageView.af.setImage( - withURL: URL(string: notification.account.avatar)!, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) + if let url = notification.account.avatarImageURL() { + cell.avatatImageView.af.setImage( + withURL: url, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + } cell.avatatImageView.gesture().sink { [weak cell] _ in cell?.delegate?.userAvatarDidPressed(notification: notification) } diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift new file mode 100644 index 00000000..77a7b412 --- /dev/null +++ b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift @@ -0,0 +1,75 @@ +// +// Mastodon+Entity+Notification+Type.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/19. +// + +import Foundation +import MastodonSDK +import UIKit + +extension Mastodon.Entity.Notification.NotificationType { + public var color: UIColor { + get { + var color: UIColor + switch self { + case .follow: + color = Asset.Colors.brandBlue.color + case .favourite: + color = Asset.Colors.Notification.favourite.color + case .reblog: + color = Asset.Colors.Notification.reblog.color + case .mention: + color = Asset.Colors.Notification.mention.color + case .poll: + color = Asset.Colors.brandBlue.color + default: + color = .clear + } + return color + } + } + + public var actionText: String { + get { + var actionText: String + switch self { + case .follow: + actionText = L10n.Scene.Notification.Action.follow + case .favourite: + actionText = L10n.Scene.Notification.Action.favourite + case .reblog: + actionText = L10n.Scene.Notification.Action.reblog + case .mention: + actionText = L10n.Scene.Notification.Action.mention + case .poll: + actionText = L10n.Scene.Notification.Action.poll + default: + actionText = "" + } + return actionText + } + } + + public var actionImageName: String { + get { + var actionImageName: String + switch self { + case .follow: + actionImageName = "person.crop.circle.badge.checkmark" + case .favourite: + actionImageName = "star.fill" + case .reblog: + actionImageName = "arrow.2.squarepath" + case .mention: + actionImageName = "at" + case .poll: + actionImageName = "list.bullet" + default: + actionImageName = "" + } + return actionImageName + } + } +} diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 90d72afa..dd04118d 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -22,7 +22,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { let segmentControl: UISegmentedControl = { let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything, L10n.Scene.Notification.Title.mentions]) - control.selectedSegmentIndex = 0 + control.selectedSegmentIndex = NotificationViewModel.NotificationSegment.EveryThing.rawValue return control }() @@ -45,7 +45,7 @@ final class NotificationViewController: UIViewController, NeedsDependency { extension NotificationViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = Asset.Colors.Background.systemBackground.color + view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color navigationItem.titleView = segmentControl segmentControl.addTarget(self, action: #selector(NotificationViewController.segmentedControlValueChanged(_:)), for: .valueChanged) tableView.translatesAutoresizingMaskIntoConstraints = false @@ -115,7 +115,7 @@ extension NotificationViewController { guard let domain = viewModel.activeMastodonAuthenticationBox.value?.domain, let userID = viewModel.activeMastodonAuthenticationBox.value?.userID else { return } - if sender.selectedSegmentIndex == 0 { + if sender.selectedSegmentIndex == NotificationViewModel.NotificationSegment.EveryThing.rawValue { viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID) } else { viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID, typeRaw: Mastodon.Entity.Notification.NotificationType.mention.rawValue) diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index fdb1af56..9d6bbedc 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -62,6 +62,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { let followButton: HighlightDimmableButton = { let button = HighlightDimmableButton(type: .custom) + button.setInsets(forContentPadding: UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16), imageTitlePadding: 0) button.setTitleColor(.white, for: .normal) button.setTitle(L10n.Scene.Search.Recommend.Accounts.follow, for: .normal) button.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold) @@ -97,7 +98,10 @@ extension SearchRecommendAccountsCollectionViewCell { headerImageView.layer.borderColor = Asset.Colors.Border.searchCard.color.cgColor applyShadow(color: Asset.Colors.Shadow.searchCard.color, alpha: 0.1, x: 0, y: 3, blur: 12, spread: 0) } - + override open func layoutSubviews() { + super.layoutSubviews() + followButton.layer.cornerRadius = followButton.frame.height/2 + } private func configure() { headerImageView.backgroundColor = Asset.Colors.brandBlue.color layer.cornerRadius = 10 @@ -149,8 +153,8 @@ extension SearchRecommendAccountsCollectionViewCell { followButton.translatesAutoresizingMaskIntoConstraints = false containerStackView.addArrangedSubview(followButton) NSLayoutConstraint.activate([ - followButton.widthAnchor.constraint(equalToConstant: 76), - followButton.heightAnchor.constraint(equalToConstant: 24) + followButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 76), + followButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 24) ]) containerStackView.addArrangedSubview(followButton) } diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index 81167ee6..abcd9d08 100644 --- a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -91,8 +91,7 @@ extension SearchRecommendTagsCollectionViewCell { NSLayoutConstraint.activate([ containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) ]) @@ -111,17 +110,9 @@ extension SearchRecommendTagsCollectionViewCell { containerStackView.addArrangedSubview(horizontalStackView) - - let peopleHorizontalStackView = UIStackView() - peopleHorizontalStackView.axis = .horizontal - peopleHorizontalStackView.translatesAutoresizingMaskIntoConstraints = false - peopleHorizontalStackView.distribution = .fill - peopleHorizontalStackView.alignment = .top peopleLabel.translatesAutoresizingMaskIntoConstraints = false peopleLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical) - peopleHorizontalStackView.addArrangedSubview(peopleLabel) - - containerStackView.addArrangedSubview(peopleHorizontalStackView) + containerStackView.addArrangedSubview(peopleLabel) } func config(with tag: Mastodon.Entity.Tag) { From da19f8f6418f96145ddd1479e06f821b64766ec3 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 19 Apr 2021 18:06:02 +0800 Subject: [PATCH 23/23] chore: remove Redundant codes --- .../Section/NotificationSection.swift | 353 +----------------- .../Diffiable/Section/StatusSection.swift | 35 +- .../NotificationViewController.swift | 24 ++ .../NotificationStatusTableViewCell.swift | 2 +- .../TableviewCell/StatusTableViewCell.swift | 2 +- .../ViewModel/AudioContainerViewModel.swift | 4 +- 6 files changed, 57 insertions(+), 363 deletions(-) diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 81732c60..5ccab431 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -47,7 +47,7 @@ extension NotificationSection { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell cell.delegate = delegate let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right, height: tableView.readableContentGuide.layoutFrame.height) - NotificationSection.configure(cell: cell, + StatusSection.configure(cell: cell, dependency: dependency, readableLayoutFrame: frame, timestampUpdatePublisher: timestampUpdatePublisher, @@ -116,354 +116,3 @@ extension NotificationSection { } } -extension NotificationSection { - static func configure( - cell: NotificationStatusTableViewCell, - dependency: NeedsDependency, - readableLayoutFrame: CGRect?, - timestampUpdatePublisher: AnyPublisher, - status: Status, - requestUserID: String, - statusItemAttribute: Item.StatusAttribute - ) { - // setup attribute - statusItemAttribute.setupForStatus(status: status) - - // set header - NotificationSection.configureHeader(cell: cell, status: status) - ManagedObjectObserver.observe(object: status) - .receive(on: DispatchQueue.main) - .sink { _ in - // do nothing - } receiveValue: { change in - guard case .update(let object) = change.changeType, - let newStatus = object as? Status else { return } - NotificationSection.configureHeader(cell: cell, status: newStatus) - } - .store(in: &cell.disposeBag) - - // set name username - cell.statusView.nameLabel.text = { - let author = (status.reblog ?? status).author - return author.displayName.isEmpty ? author.username : author.displayName - }() - cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct - // set avatar - - cell.statusView.avatarButton.isHidden = false - cell.statusView.avatarStackedContainerButton.isHidden = true - cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) - - // set text - cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content) - - // set status text content warning - let isStatusTextSensitive = statusItemAttribute.isStatusTextSensitive ?? false - let spoilerText = (status.reblog ?? status).spoilerText ?? "" - cell.statusView.isStatusTextSensitive = isStatusTextSensitive - cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive) - cell.statusView.contentWarningTitle.text = { - if spoilerText.isEmpty { - return L10n.Common.Controls.Status.statusContentWarning - } else { - return L10n.Common.Controls.Status.statusContentWarning + ": \(spoilerText)" - } - }() - - // prepare media attachments - let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } - - // set image - let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments) - let imageViewMaxSize: CGSize = { - let maxWidth: CGFloat = { - // use timelinePostView width as container width - // that width follows readable width and keep constant width after rotate - let containerFrame = readableLayoutFrame ?? cell.statusView.frame - var containerWidth = containerFrame.width - containerWidth -= 10 - containerWidth -= StatusView.avatarImageSize.width - return containerWidth - }() - let scale: CGFloat = { - switch mosiacImageViewModel.metas.count { - case 1: return 1.3 - default: return 0.7 - } - }() - return CGSize(width: maxWidth, height: maxWidth * scale) - }() - if mosiacImageViewModel.metas.count == 1 { - let meta = mosiacImageViewModel.metas[0] - let imageView = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize) - imageView.af.setImage( - withURL: meta.url, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) - } else { - let imageViews = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height) - for (i, imageView) in imageViews.enumerated() { - let meta = mosiacImageViewModel.metas[i] - imageView.af.setImage( - withURL: meta.url, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) - } - } - cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty - let isStatusSensitive = statusItemAttribute.isStatusSensitive ?? false - cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil - cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 - cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive - - // set audio - if let _ = mediaAttachments.filter({ $0.type == .audio }).first { - cell.statusView.audioView.isHidden = false - cell.statusView.audioView.playButton.isSelected = false - cell.statusView.audioView.slider.isEnabled = false - cell.statusView.audioView.slider.setValue(0, animated: false) - } else { - cell.statusView.audioView.isHidden = true - } - - // set GIF & video - let playerViewMaxSize: CGSize = { - let maxWidth: CGFloat = { - // use statusView width as container width - // that width follows readable width and keep constant width after rotate - let containerFrame = readableLayoutFrame ?? cell.statusView.frame - return containerFrame.width - }() - let scale: CGFloat = 1.3 - return CGSize(width: maxWidth, height: maxWidth * scale) - }() - - cell.statusView.playerContainerView.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil - cell.statusView.playerContainerView.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 - cell.statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive - - if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first, - let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) - { - let parent = cell.delegate?.parent() - let playerContainerView = cell.statusView.playerContainerView - let playerViewController = playerContainerView.setupPlayer( - aspectRatio: videoPlayerViewModel.videoSize, - maxSize: playerViewMaxSize, - parent: parent - ) - playerViewController.player = videoPlayerViewModel.player - playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif - playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind) - if videoPlayerViewModel.videoKind == .gif { - playerContainerView.setMediaIndicator(isHidden: false) - } else { - videoPlayerViewModel.timeControlStatus.sink { timeControlStatus in - UIView.animate(withDuration: 0.33) { - switch timeControlStatus { - case .playing: - playerContainerView.setMediaIndicator(isHidden: true) - case .paused, .waitingToPlayAtSpecifiedRate: - playerContainerView.setMediaIndicator(isHidden: false) - @unknown default: - assertionFailure() - } - } - } - .store(in: &cell.disposeBag) - } - playerContainerView.isHidden = false - - } else { - cell.statusView.playerContainerView.playerViewController.player?.pause() - cell.statusView.playerContainerView.playerViewController.player = nil - } - // set poll - let poll = (status.reblog ?? status).poll - NotificationSection.configurePoll( - cell: cell, - poll: poll, - requestUserID: requestUserID, - updateProgressAnimated: false, - timestampUpdatePublisher: timestampUpdatePublisher - ) - if let poll = poll { - ManagedObjectObserver.observe(object: poll) - .sink { _ in - // do nothing - } receiveValue: { change in - guard case .update(let object) = change.changeType, - let newPoll = object as? Poll else { return } - NotificationSection.configurePoll( - cell: cell, - poll: newPoll, - requestUserID: requestUserID, - updateProgressAnimated: true, - timestampUpdatePublisher: timestampUpdatePublisher - ) - } - .store(in: &cell.disposeBag) - } - - // set date - let createdAt = (status.reblog ?? status).createdAt - cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow - timestampUpdatePublisher - .sink { _ in - cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow - } - .store(in: &cell.disposeBag) - } - - static func configureHeader( - cell: NotificationStatusTableViewCell, - status: Status - ) { - if status.reblog != nil { - cell.statusView.headerContainerStackView.isHidden = false - cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage) - cell.statusView.headerInfoLabel.text = { - let author = status.author - let name = author.displayName.isEmpty ? author.username : author.displayName - return L10n.Common.Controls.Status.userReblogged(name) - }() - } else if let replyTo = status.replyTo { - cell.statusView.headerContainerStackView.isHidden = false - cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) - cell.statusView.headerInfoLabel.text = { - let author = replyTo.author - let name = author.displayName.isEmpty ? author.username : author.displayName - return L10n.Common.Controls.Status.userRepliedTo(name) - }() - } else { - cell.statusView.headerContainerStackView.isHidden = true - } - } - - static func configurePoll( - cell: NotificationStatusTableViewCell, - poll: Poll?, - requestUserID: String, - updateProgressAnimated: Bool, - timestampUpdatePublisher: AnyPublisher - ) { - guard let poll = poll, - let managedObjectContext = poll.managedObjectContext - else { - cell.statusView.pollTableView.isHidden = true - cell.statusView.pollStatusStackView.isHidden = true - cell.statusView.pollVoteButton.isHidden = true - return - } - - cell.statusView.pollTableView.isHidden = false - cell.statusView.pollStatusStackView.isHidden = false - cell.statusView.pollVoteCountLabel.text = { - if poll.multiple { - let count = poll.votersCount?.intValue ?? 0 - if count > 1 { - return L10n.Common.Controls.Status.Poll.VoterCount.single(count) - } else { - return L10n.Common.Controls.Status.Poll.VoterCount.multiple(count) - } - } else { - let count = poll.votesCount.intValue - if count > 1 { - return L10n.Common.Controls.Status.Poll.VoteCount.single(count) - } else { - return L10n.Common.Controls.Status.Poll.VoteCount.multiple(count) - } - } - }() - if poll.expired { - cell.pollCountdownSubscription = nil - cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed - } else if let expiresAt = poll.expiresAt { - cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow) - cell.pollCountdownSubscription = timestampUpdatePublisher - .sink { _ in - cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow) - } - } else { - // assertionFailure() - cell.pollCountdownSubscription = nil - cell.statusView.pollCountdownLabel.text = "-" - } - - cell.statusView.pollTableView.allowsSelection = !poll.expired - - let votedOptions = poll.options.filter { option in - (option.votedBy ?? Set()).map(\.id).contains(requestUserID) - } - let didVotedLocal = !votedOptions.isEmpty - let didVotedRemote = (poll.votedBy ?? Set()).map(\.id).contains(requestUserID) - cell.statusView.pollVoteButton.isEnabled = didVotedLocal - cell.statusView.pollVoteButton.isHidden = !poll.multiple ? true : (didVotedRemote || poll.expired) - - cell.statusView.pollTableViewDataSource = PollSection.tableViewDiffableDataSource( - for: cell.statusView.pollTableView, - managedObjectContext: managedObjectContext - ) - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - - let pollItems = poll.options - .sorted(by: { $0.index.intValue < $1.index.intValue }) - .map { option -> PollItem in - let attribute: PollItem.Attribute = { - let selectState: PollItem.Attribute.SelectState = { - // check didVotedRemote later to make the local change possible - if !votedOptions.isEmpty { - return votedOptions.contains(option) ? .on : .off - } else if poll.expired { - return .none - } else if didVotedRemote, votedOptions.isEmpty { - return .none - } else { - return .off - } - }() - let voteState: PollItem.Attribute.VoteState = { - var needsReveal: Bool - if poll.expired { - needsReveal = true - } else if didVotedRemote { - needsReveal = true - } else { - needsReveal = false - } - guard needsReveal else { return .hidden } - let percentage: Double = { - guard poll.votesCount.intValue > 0 else { return 0.0 } - return Double(option.votesCount?.intValue ?? 0) / Double(poll.votesCount.intValue) - }() - let voted = votedOptions.isEmpty ? true : votedOptions.contains(option) - return .reveal(voted: voted, percentage: percentage, animated: updateProgressAnimated) - }() - return PollItem.Attribute(selectState: selectState, voteState: voteState) - }() - let option = PollItem.opion(objectID: option.objectID, attribute: attribute) - return option - } - snapshot.appendItems(pollItems, toSection: .main) - cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) - } - - static func configureEmptyStateHeader( - cell: TimelineHeaderTableViewCell, - attribute: Item.EmptyStateHeaderAttribute - ) { - cell.timelineHeaderView.iconImageView.image = attribute.reason.iconImage - cell.timelineHeaderView.messageLabel.text = attribute.reason.message - } -} - -extension NotificationSection { - private static func formattedNumberTitleForActionButton(_ number: Int?) -> String { - guard let number = number, number > 0 else { return "" } - return String(number) - } -} diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 36d4853a..e01276e8 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -10,6 +10,12 @@ import CoreData import CoreDataStack import os.log import UIKit +import AVKit + +protocol StatusCell : DisposeBagCollectable { + var statusView: StatusView { get } + var pollCountdownSubscription: AnyCancellable? { get set } +} enum StatusSection: Equatable, Hashable { case main @@ -127,7 +133,7 @@ extension StatusSection { extension StatusSection { static func configure( - cell: StatusTableViewCell, + cell: StatusCell, dependency: NeedsDependency, readableLayoutFrame: CGRect?, timestampUpdatePublisher: AnyPublisher, @@ -260,14 +266,27 @@ extension StatusSection { if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first, let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) { - let parent = cell.delegate?.parent() + var parent: UIViewController? + var playerViewControllerDelegate: AVPlayerViewControllerDelegate? = nil + switch cell { + case is StatusTableViewCell: + let statusTableViewCell = cell as! StatusTableViewCell + parent = statusTableViewCell.delegate?.parent() + playerViewControllerDelegate = statusTableViewCell.delegate?.playerViewControllerDelegate + case is NotificationTableViewCell: + let notificationTableViewCell = cell as! NotificationTableViewCell + parent = notificationTableViewCell.delegate?.parent() + default: + parent = nil + assertionFailure("unknown cell") + } let playerContainerView = cell.statusView.playerContainerView let playerViewController = playerContainerView.setupPlayer( aspectRatio: videoPlayerViewModel.videoSize, maxSize: playerViewMaxSize, parent: parent ) - playerViewController.delegate = cell.delegate?.playerViewControllerDelegate + playerViewController.delegate = playerViewControllerDelegate playerViewController.player = videoPlayerViewModel.player playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind) @@ -325,7 +344,9 @@ extension StatusSection { StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID) // separator line - cell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden + if let statusTableViewCell = cell as? StatusTableViewCell { + statusTableViewCell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden + } // set date let createdAt = (status.reblog ?? status).createdAt @@ -388,7 +409,7 @@ extension StatusSection { static func configureHeader( - cell: StatusTableViewCell, + cell: StatusCell, status: Status ) { if status.reblog != nil { @@ -416,7 +437,7 @@ extension StatusSection { } static func configureActionToolBar( - cell: StatusTableViewCell, + cell: StatusCell, status: Status, requestUserID: String ) { @@ -447,7 +468,7 @@ extension StatusSection { } static func configurePoll( - cell: StatusTableViewCell, + cell: StatusCell, poll: Poll?, requestUserID: String, updateProgressAnimated: Bool, diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index dd04118d..ad9a7472 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -131,9 +131,33 @@ extension NotificationViewController { } } +extension NotificationViewController { + func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + let key = item.hashValue + let frame = cell.frame + viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key)) + } + + func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + guard let diffableDataSource = viewModel.diffableDataSource else { return UITableView.automaticDimension } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension } + guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else { + if case .bottomLoader = item { + return TimelineLoaderTableViewCell.cellHeight + } else { + return UITableView.automaticDimension + } + } + + return ceil(frame.height) + } +} // MARK: - UITableViewDelegate extension NotificationViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) guard let diffableDataSource = viewModel.diffableDataSource else { return } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index aae4c9e2..871adcae 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -9,7 +9,7 @@ import Combine import Foundation import UIKit -final class NotificationStatusTableViewCell: UITableViewCell { +final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { static let actionImageBorderWidth: CGFloat = 2 static let statusPadding = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24) var disposeBag = Set() diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index afa044b6..88004afa 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -46,7 +46,7 @@ extension StatusTableViewCellDelegate { } } -final class StatusTableViewCell: UITableViewCell { +final class StatusTableViewCell: UITableViewCell, StatusCell { static let bottomPaddingHeight: CGFloat = 10 diff --git a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift index 56bf0cbc..2bc6db22 100644 --- a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift @@ -11,7 +11,7 @@ import UIKit class AudioContainerViewModel { static func configure( - cell: StatusTableViewCell, + cell: StatusCell, audioAttachment: Attachment, audioService: AudioPlaybackService ) { @@ -51,7 +51,7 @@ class AudioContainerViewModel { } static func observePlayer( - cell: StatusTableViewCell, + cell: StatusCell, audioAttachment: Attachment, audioService: AudioPlaybackService ) {