diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 2569da5e6..bc7a20d5d 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 8a0595f6c..31c361aa4 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 d088d3176..3244dcd33 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 c160eac5e..ba0d0c140 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 5e3cd2d9e..0b63bb241 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 e01063c86..1b9230ee0 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 baa923ada..ded8846d4 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 becf86771..ddc997a5d 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 38f24c586..0e6b0d622 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 a26dedeeb..8075ce375 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 f64c07fc9..e026af732 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 6bea35ead..dc3f49bb0 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 238d9c67f..cda4d75d7 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 43e5d397c..86a27e03d 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 dc9414585..4fee226bf 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 2d529972e..000000000 --- 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 e2b90ffd7..ee8f5186c 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 1cc54add5..b0ab13edb 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?