mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2024-12-15 10:24:32 +01:00
284 lines
12 KiB
Swift
284 lines
12 KiB
Swift
//
|
|
// NotificationView+ViewModel.swift
|
|
//
|
|
//
|
|
// Created by MainasuK on 2022-1-21.
|
|
//
|
|
|
|
import os.log
|
|
import UIKit
|
|
import Combine
|
|
import Meta
|
|
import MastodonSDK
|
|
import MastodonAsset
|
|
import MastodonLocalization
|
|
import MastodonExtension
|
|
import MastodonCore
|
|
import CoreData
|
|
import CoreDataStack
|
|
|
|
extension NotificationView {
|
|
public final class ViewModel: ObservableObject {
|
|
public var disposeBag = Set<AnyCancellable>()
|
|
public var objects = Set<NSManagedObject>()
|
|
|
|
let logger = Logger(subsystem: "NotificationView", category: "ViewModel")
|
|
|
|
@Published public var authContext: AuthContext?
|
|
|
|
@Published public var type: MastodonNotificationType?
|
|
@Published public var notificationIndicatorText: MetaContent?
|
|
|
|
@Published public var authorAvatarImage: UIImage?
|
|
@Published public var authorAvatarImageURL: URL?
|
|
@Published public var authorName: MetaContent?
|
|
@Published public var authorUsername: String?
|
|
|
|
@Published public var isMyself = false
|
|
@Published public var isMuting = false
|
|
@Published public var isBlocking = false
|
|
|
|
@Published public var timestamp: Date?
|
|
|
|
@Published public var followRequestState = MastodonFollowRequestState(state: .none)
|
|
@Published public var transientFollowRequestState = MastodonFollowRequestState(state: .none)
|
|
|
|
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
|
.autoconnect()
|
|
.share()
|
|
.eraseToAnyPublisher()
|
|
}
|
|
}
|
|
|
|
extension NotificationView.ViewModel {
|
|
func bind(notificationView: NotificationView) {
|
|
bindAuthor(notificationView: notificationView)
|
|
bindAuthorMenu(notificationView: notificationView)
|
|
bindFollowRequest(notificationView: notificationView)
|
|
|
|
$authContext
|
|
.assign(to: \.authContext, on: notificationView.statusView.viewModel)
|
|
.store(in: &disposeBag)
|
|
$authContext
|
|
.assign(to: \.authContext, on: notificationView.quoteStatusView.viewModel)
|
|
.store(in: &disposeBag)
|
|
}
|
|
|
|
private func bindAuthor(notificationView: NotificationView) {
|
|
// avatar
|
|
Publishers.CombineLatest(
|
|
$authorAvatarImage,
|
|
$authorAvatarImageURL
|
|
)
|
|
.sink { image, url in
|
|
let configuration: AvatarImageView.Configuration = {
|
|
if let image = image {
|
|
return AvatarImageView.Configuration(image: image)
|
|
} else {
|
|
return AvatarImageView.Configuration(url: url)
|
|
}
|
|
}()
|
|
notificationView.avatarButton.avatarImageView.configure(configuration: configuration)
|
|
notificationView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12)))
|
|
}
|
|
.store(in: &disposeBag)
|
|
// name
|
|
$authorName
|
|
.sink { metaContent in
|
|
let metaContent = metaContent ?? PlaintextMetaContent(string: " ")
|
|
notificationView.authorNameLabel.configure(content: metaContent)
|
|
}
|
|
.store(in: &disposeBag)
|
|
// username
|
|
$authorUsername
|
|
.map { text -> String in
|
|
guard let text = text else { return "" }
|
|
return "@\(text)"
|
|
}
|
|
.sink { username in
|
|
let metaContent = PlaintextMetaContent(string: username)
|
|
notificationView.authorUsernameLabel.configure(content: metaContent)
|
|
}
|
|
.store(in: &disposeBag)
|
|
// timestamp
|
|
let formattedTimestamp = Publishers.CombineLatest(
|
|
$timestamp,
|
|
timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher()
|
|
)
|
|
.map { timestamp, _ in
|
|
timestamp?.localizedTimeAgoSinceNow ?? ""
|
|
}
|
|
|
|
formattedTimestamp
|
|
.sink { timestamp in
|
|
notificationView.dateLabel.configure(content: PlaintextMetaContent(string: timestamp))
|
|
}
|
|
.store(in: &disposeBag)
|
|
|
|
// notification type indicator
|
|
$notificationIndicatorText
|
|
.sink { text in
|
|
if let text = text {
|
|
notificationView.notificationTypeIndicatorLabel.configure(content: text)
|
|
} else {
|
|
notificationView.notificationTypeIndicatorLabel.reset()
|
|
}
|
|
}
|
|
.store(in: &disposeBag)
|
|
|
|
Publishers.CombineLatest4(
|
|
$authorName,
|
|
$authorUsername,
|
|
$notificationIndicatorText,
|
|
formattedTimestamp
|
|
)
|
|
.sink { name, username, type, timestamp in
|
|
notificationView.accessibilityLabel = [
|
|
"\(name?.string ?? "") \(type?.string ?? "")",
|
|
username.map { "@\($0)" } ?? "",
|
|
timestamp
|
|
].joined(separator: ", ")
|
|
if !notificationView.statusView.isHidden {
|
|
notificationView.accessibilityLabel! += ", " + (notificationView.statusView.accessibilityLabel ?? "")
|
|
}
|
|
if !notificationView.quoteStatusViewContainerView.isHidden {
|
|
notificationView.accessibilityLabel! += ", " + (notificationView.quoteStatusView.accessibilityLabel ?? "")
|
|
}
|
|
}
|
|
.store(in: &disposeBag)
|
|
|
|
Publishers.CombineLatest(
|
|
$authorAvatarImage,
|
|
$type
|
|
)
|
|
.sink { avatarImage, type in
|
|
var actions = [UIAccessibilityCustomAction]()
|
|
|
|
// these notifications can be directly actioned to view the profile
|
|
if type != .follow, type != .followRequest {
|
|
actions.append(
|
|
UIAccessibilityCustomAction(
|
|
name: L10n.Common.Controls.Status.showUserProfile,
|
|
image: avatarImage
|
|
) { [weak notificationView] _ in
|
|
guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false }
|
|
delegate.notificationView(notificationView, authorAvatarButtonDidPressed: notificationView.avatarButton)
|
|
return true
|
|
}
|
|
)
|
|
}
|
|
|
|
if type == .followRequest {
|
|
actions.append(
|
|
UIAccessibilityCustomAction(
|
|
name: L10n.Common.Controls.Actions.confirm,
|
|
image: Asset.Editing.checkmark20.image
|
|
) { [weak notificationView] _ in
|
|
guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false }
|
|
delegate.notificationView(notificationView, acceptFollowRequestButtonDidPressed: notificationView.acceptFollowRequestButton)
|
|
return true
|
|
}
|
|
)
|
|
|
|
actions.append(
|
|
UIAccessibilityCustomAction(
|
|
name: L10n.Common.Controls.Actions.delete,
|
|
image: Asset.Circles.forbidden20.image
|
|
) { [weak notificationView] _ in
|
|
guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false }
|
|
delegate.notificationView(notificationView, rejectFollowRequestButtonDidPressed: notificationView.rejectFollowRequestButton)
|
|
return true
|
|
}
|
|
)
|
|
}
|
|
|
|
notificationView.notificationActions = actions
|
|
}
|
|
.store(in: &disposeBag)
|
|
}
|
|
|
|
private func bindAuthorMenu(notificationView: NotificationView) {
|
|
Publishers.CombineLatest4(
|
|
$authorName,
|
|
$isMuting,
|
|
$isBlocking,
|
|
$isMyself
|
|
)
|
|
.sink { authorName, isMuting, isBlocking, isMyself in
|
|
guard let name = authorName?.string else {
|
|
notificationView.menuButton.menu = nil
|
|
return
|
|
}
|
|
|
|
let menuContext = NotificationView.AuthorMenuContext(
|
|
name: name,
|
|
isMuting: isMuting,
|
|
isBlocking: isBlocking,
|
|
isMyself: isMyself,
|
|
isBookmarking: false // no bookmark action display for notification item
|
|
)
|
|
let (menu, actions) = notificationView.setupAuthorMenu(menuContext: menuContext)
|
|
notificationView.menuButton.menu = menu
|
|
notificationView.authorActions = actions
|
|
notificationView.menuButton.showsMenuAsPrimaryAction = true
|
|
|
|
notificationView.menuButton.isHidden = menuContext.isMyself
|
|
}
|
|
.store(in: &disposeBag)
|
|
}
|
|
|
|
private func bindFollowRequest(notificationView: NotificationView) {
|
|
Publishers.CombineLatest(
|
|
$followRequestState,
|
|
$transientFollowRequestState
|
|
)
|
|
.sink { followRequestState, transientFollowRequestState in
|
|
switch followRequestState.state {
|
|
case .isAccept:
|
|
notificationView.rejectFollowRequestButtonShadowBackgroundContainer.isHidden = true
|
|
notificationView.acceptFollowRequestButton.isUserInteractionEnabled = false
|
|
notificationView.acceptFollowRequestButton.setImage(nil, for: .normal)
|
|
notificationView.acceptFollowRequestButton.setTitle(L10n.Scene.Notification.FollowRequest.accepted, for: .normal)
|
|
case .isReject:
|
|
notificationView.acceptFollowRequestButtonShadowBackgroundContainer.isHidden = true
|
|
notificationView.rejectFollowRequestButton.isUserInteractionEnabled = false
|
|
notificationView.rejectFollowRequestButton.setImage(nil, for: .normal)
|
|
notificationView.rejectFollowRequestButton.setTitle(L10n.Scene.Notification.FollowRequest.rejected, for: .normal)
|
|
default:
|
|
break
|
|
}
|
|
|
|
let state = transientFollowRequestState.state
|
|
if state == .isAccepting {
|
|
notificationView.acceptFollowRequestActivityIndicatorView.startAnimating()
|
|
notificationView.acceptFollowRequestButton.tintColor = .clear
|
|
notificationView.acceptFollowRequestButton.setTitleColor(.clear, for: .normal)
|
|
} else {
|
|
notificationView.acceptFollowRequestActivityIndicatorView.stopAnimating()
|
|
notificationView.acceptFollowRequestButton.tintColor = .white
|
|
notificationView.acceptFollowRequestButton.setTitleColor(.white, for: .normal)
|
|
}
|
|
if state == .isRejecting {
|
|
notificationView.rejectFollowRequestActivityIndicatorView.startAnimating()
|
|
notificationView.rejectFollowRequestButton.tintColor = .clear
|
|
notificationView.rejectFollowRequestButton.setTitleColor(.clear, for: .normal)
|
|
} else {
|
|
notificationView.rejectFollowRequestActivityIndicatorView.stopAnimating()
|
|
notificationView.rejectFollowRequestButton.tintColor = .black
|
|
notificationView.rejectFollowRequestButton.setTitleColor(.black, for: .normal)
|
|
}
|
|
|
|
UIView.animate(withDuration: 0.3) {
|
|
if state == .isAccept {
|
|
notificationView.rejectFollowRequestButtonShadowBackgroundContainer.isHidden = true
|
|
}
|
|
if state == .isReject {
|
|
notificationView.acceptFollowRequestButtonShadowBackgroundContainer.isHidden = true
|
|
}
|
|
}
|
|
}
|
|
.store(in: &disposeBag)
|
|
}
|
|
|
|
}
|