chore: display Notification Cell

This commit is contained in:
sunxiaojian 2021-04-13 21:31:49 +08:00
parent 773bfb6dd2
commit 42628398e6
16 changed files with 192 additions and 38 deletions

View File

@ -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 {

View File

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

View File

@ -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")
}

View File

@ -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 {

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

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

View File

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

View File

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

View File

@ -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";

View File

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

View File

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

View File

@ -65,7 +65,9 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate {
var newSnapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
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 {

View File

@ -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<Void, Never>()
let activeMastodonAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
let fetchedResultsController: NSFetchedResultsController<MastodonNotification>!
@ -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)
}
}

View File

@ -12,7 +12,7 @@ import Combine
final class NotificationTableViewCell: UITableViewCell {
static let actionImageBorderWidth: CGFloat = 3
static let actionImageBorderWidth: CGFloat = 2
var disposeBag = Set<AnyCancellable>()
@ -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
}
}

View File

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

View File

@ -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] {