chore: display Notification Cell
This commit is contained in:
parent
773bfb6dd2
commit
42628398e6
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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] {
|
||||
|
Loading…
x
Reference in New Issue
Block a user