Kurdtvs-Live-Kurdish-TV-Kur.../Mastodon/Diffiable/Section/StatusSection.swift

806 lines
38 KiB
Swift
Raw Normal View History

2021-01-28 09:10:30 +01:00
//
// TimelineSection.swift
// Mastodon
//
// Created by sxiaojian on 2021/1/27.
//
import Combine
import CoreData
import CoreDataStack
import os.log
import UIKit
2021-04-19 12:06:02 +02:00
import AVKit
2021-04-30 13:28:06 +02:00
protocol StatusCell: DisposeBagCollectable {
2021-04-19 12:06:02 +02:00
var statusView: StatusView { get }
var pollCountdownSubscription: AnyCancellable? { get set }
}
2021-01-28 09:10:30 +01:00
enum StatusSection: Equatable, Hashable {
2021-01-28 09:10:30 +01:00
case main
}
extension StatusSection {
2021-01-28 09:10:30 +01:00
static func tableViewDiffableDataSource(
for tableView: UITableView,
dependency: NeedsDependency,
managedObjectContext: NSManagedObjectContext,
timestampUpdatePublisher: AnyPublisher<Date, Never>,
statusTableViewCellDelegate: StatusTableViewCellDelegate,
2021-04-13 13:46:42 +02:00
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?,
threadReplyLoaderTableViewCellDelegate: ThreadReplyLoaderTableViewCellDelegate?
) -> UITableViewDiffableDataSource<StatusSection, Item> {
2021-04-13 13:46:42 +02:00
UITableViewDiffableDataSource(tableView: tableView) { [
weak dependency,
weak statusTableViewCellDelegate,
weak timelineMiddleLoaderTableViewCellDelegate,
weak threadReplyLoaderTableViewCellDelegate
] tableView, indexPath, item -> UITableViewCell? in
guard let dependency = dependency else { return UITableViewCell() }
guard let statusTableViewCellDelegate = statusTableViewCellDelegate else { return UITableViewCell() }
2021-01-28 09:10:30 +01:00
switch item {
case .homeTimelineIndex(objectID: let objectID, let attribute):
2021-02-23 08:16:55 +01:00
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
2021-02-07 07:42:50 +01:00
// configure cell
managedObjectContext.performAndWait {
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
2021-03-10 07:36:28 +01:00
StatusSection.configure(
cell: cell,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
2021-04-01 08:39:15 +02:00
status: timelineIndex.status,
requestUserID: timelineIndex.userID,
statusItemAttribute: attribute
2021-03-10 07:36:28 +01:00
)
2021-02-07 07:42:50 +01:00
}
cell.delegate = statusTableViewCellDelegate
2021-02-07 07:42:50 +01:00
return cell
2021-04-13 13:46:42 +02:00
case .status(let objectID, let attribute),
.root(let objectID, let attribute),
.reply(let objectID, let attribute),
.leaf(let objectID, let attribute):
2021-02-23 08:16:55 +01:00
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
2021-02-08 11:29:27 +01:00
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
2021-01-28 09:10:30 +01:00
// configure cell
managedObjectContext.performAndWait {
2021-04-01 08:39:15 +02:00
let status = managedObjectContext.object(with: objectID) as! Status
2021-03-10 07:36:28 +01:00
StatusSection.configure(
cell: cell,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
2021-04-01 08:39:15 +02:00
status: status,
requestUserID: requestUserID,
statusItemAttribute: attribute
2021-03-10 07:36:28 +01:00
)
2021-04-13 13:46:42 +02:00
switch item {
case .root:
StatusSection.configureThreadMeta(cell: cell, status: status)
ManagedObjectObserver.observe(object: status.reblog ?? status)
.receive(on: DispatchQueue.main)
.sink { _ in
// do nothing
} receiveValue: { change in
guard case .update(let object) = change.changeType,
let status = object as? Status else { return }
StatusSection.configureThreadMeta(cell: cell, status: status)
}
.store(in: &cell.disposeBag)
default:
break
}
2021-01-28 09:10:30 +01:00
}
cell.delegate = statusTableViewCellDelegate
2021-04-13 13:46:42 +02:00
return cell
case .leafBottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ThreadReplyLoaderTableViewCell.self), for: indexPath) as! ThreadReplyLoaderTableViewCell
cell.delegate = threadReplyLoaderTableViewCellDelegate
2021-01-28 09:10:30 +01:00
return cell
2021-04-01 08:39:15 +02:00
case .publicMiddleLoader(let upperTimelineStatusID):
2021-02-07 07:42:50 +01:00
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
2021-04-01 08:39:15 +02:00
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineStatusID: upperTimelineStatusID, timelineIndexobjectID: nil)
2021-02-07 07:42:50 +01:00
return cell
case .homeMiddleLoader(let upperTimelineIndexObjectID):
2021-02-04 08:09:58 +01:00
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
2021-04-01 08:39:15 +02:00
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineStatusID: nil, timelineIndexobjectID: upperTimelineIndexObjectID)
2021-02-04 08:09:58 +01:00
return cell
2021-04-13 13:46:42 +02:00
case .topLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
cell.startAnimating()
return cell
2021-02-03 06:01:50 +01:00
case .bottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
cell.startAnimating()
2021-02-03 06:01:50 +01:00
return cell
case .emptyStateHeader(let attribute):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineHeaderTableViewCell.self), for: indexPath) as! TimelineHeaderTableViewCell
StatusSection.configureEmptyStateHeader(cell: cell, attribute: attribute)
return cell
case .reportStatus:
return UITableViewCell()
2021-01-28 09:10:30 +01:00
}
}
}
}
extension StatusSection {
2021-01-28 09:10:30 +01:00
static func configure(
2021-04-19 12:06:02 +02:00
cell: StatusCell,
2021-03-10 07:36:28 +01:00
dependency: NeedsDependency,
readableLayoutFrame: CGRect?,
timestampUpdatePublisher: AnyPublisher<Date, Never>,
2021-04-01 08:39:15 +02:00
status: Status,
requestUserID: String,
2021-03-05 06:41:48 +01:00
statusItemAttribute: Item.StatusAttribute
2021-04-30 13:28:06 +02:00
) {
2021-02-23 08:16:55 +01:00
// set header
2021-04-01 08:39:15 +02:00
StatusSection.configureHeader(cell: cell, status: status)
ManagedObjectObserver.observe(object: status)
2021-03-10 12:12:53 +01:00
.receive(on: DispatchQueue.main)
.sink { _ in
// do nothing
2021-04-26 11:41:24 +02:00
} receiveValue: { [weak cell] change in
guard let cell = cell else { return }
2021-03-10 12:12:53 +01:00
guard case .update(let object) = change.changeType,
2021-04-01 08:39:15 +02:00
let newStatus = object as? Status else { return }
StatusSection.configureHeader(cell: cell, status: newStatus)
2021-03-10 12:12:53 +01:00
}
.store(in: &cell.disposeBag)
2021-02-23 08:16:55 +01:00
// set name username
cell.statusView.nameLabel.text = {
2021-04-01 08:39:15 +02:00
let author = (status.reblog ?? status).author
return author.displayName.isEmpty ? author.username : author.displayName
}()
2021-04-01 08:39:15 +02:00
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
// set avatar
2021-04-01 08:39:15 +02:00
if let reblog = status.reblog {
cell.statusView.avatarButton.isHidden = true
cell.statusView.avatarStackedContainerButton.isHidden = false
cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL()))
2021-04-01 08:39:15 +02:00
cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
} else {
cell.statusView.avatarButton.isHidden = false
cell.statusView.avatarStackedContainerButton.isHidden = true
2021-04-01 08:39:15 +02:00
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
}
2021-02-23 08:16:55 +01:00
// set text
2021-04-01 08:39:15 +02:00
cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content)
// prepare media attachments
2021-04-01 08:39:15 +02:00
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 {
2021-03-08 04:42:10 +01:00
case 1: return 1.3
default: return 0.7
}
}()
return CGSize(width: maxWidth, height: maxWidth * scale)
}()
let blurhashImageCache = dependency.context.documentStore.blurhashImageCache
let mosaics: [MosaicImageViewContainer.ConfigurableMosaic] = {
if mosiacImageViewModel.metas.count == 1 {
let meta = mosiacImageViewModel.metas[0]
let mosaic = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize)
return [mosaic]
} else {
let mosaics = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height)
return mosaics
}
}()
for (i, mosiac) in mosaics.enumerated() {
let (imageView, blurhashOverlayImageView) = mosiac
let meta = mosiacImageViewModel.metas[i]
let blurhashImageDataKey = meta.url.absoluteString as NSString
if let blurhashImageData = blurhashImageCache.object(forKey: meta.url.absoluteString as NSString),
let image = UIImage(data: blurhashImageData as Data) {
blurhashOverlayImageView.image = image
} else {
meta.blurhashImagePublisher()
.receive(on: DispatchQueue.main)
2021-04-26 11:41:24 +02:00
.sink { [weak cell] image in
blurhashOverlayImageView.image = image
image?.pngData().flatMap {
blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey)
}
}
.store(in: &cell.disposeBag)
}
imageView.af.setImage(
withURL: meta.url,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
) { response in
switch response.result {
case .success:
statusItemAttribute.isImageLoaded.value = true
case .failure:
break
}
}
Publishers.CombineLatest(
statusItemAttribute.isImageLoaded,
statusItemAttribute.isRevealing
)
.receive(on: DispatchQueue.main)
2021-04-26 11:41:24 +02:00
.sink { [weak cell] isImageLoaded, isMediaRevealing in
guard let cell = cell else { return }
guard isImageLoaded else {
blurhashOverlayImageView.alpha = 1
blurhashOverlayImageView.isHidden = false
return
}
2021-04-16 14:29:08 +02:00
blurhashOverlayImageView.alpha = isMediaRevealing ? 0 : 1
if isMediaRevealing {
let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
animator.addAnimations {
blurhashOverlayImageView.alpha = isMediaRevealing ? 0 : 1
}
animator.startAnimation()
} else {
cell.statusView.drawContentWarningImageView()
}
}
.store(in: &cell.disposeBag)
}
2021-03-02 12:33:33 +01:00
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
2021-03-08 04:42:10 +01:00
// set audio
if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first {
cell.statusView.audioView.isHidden = false
AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment, audioService: dependency.context.audioPlaybackService)
2021-03-08 04:42:10 +01:00
} else {
cell.statusView.audioView.isHidden = true
}
2021-03-10 07:36:28 +01:00
// 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)
}()
2021-03-10 07:36:28 +01:00
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
{
2021-04-19 12:06:02 +02:00
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 NotificationStatusTableViewCell:
let notificationTableViewCell = cell as! NotificationStatusTableViewCell
2021-04-19 12:06:02 +02:00
parent = notificationTableViewCell.delegate?.parent()
2021-04-26 09:58:49 +02:00
case is ReportedStatusTableViewCell:
let reportTableViewCell = cell as! ReportedStatusTableViewCell
parent = reportTableViewCell.dependency
2021-04-19 12:06:02 +02:00
default:
parent = nil
assertionFailure("unknown cell")
}
2021-03-11 12:23:44 +01:00
let playerContainerView = cell.statusView.playerContainerView
let playerViewController = playerContainerView.setupPlayer(
2021-03-10 07:36:28 +01:00
aspectRatio: videoPlayerViewModel.videoSize,
maxSize: playerViewMaxSize,
parent: parent
)
2021-04-19 12:06:02 +02:00
playerViewController.delegate = playerViewControllerDelegate
2021-03-10 07:36:28 +01:00
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)
}
2021-03-11 12:23:44 +01:00
playerContainerView.isHidden = false
2021-03-10 07:36:28 +01:00
} else {
2021-03-11 12:06:15 +01:00
cell.statusView.playerContainerView.playerViewController.player?.pause()
cell.statusView.playerContainerView.playerViewController.player = nil
2021-03-10 07:36:28 +01:00
}
// set text content warning
StatusSection.configureContentWarningOverlay(
statusView: cell.statusView,
status: status,
attribute: statusItemAttribute,
documentStore: dependency.context.documentStore,
animated: false
)
// observe model change
ManagedObjectObserver.observe(object: status)
.receive(on: DispatchQueue.main)
.sink { _ in
// do nothing
2021-04-26 11:41:24 +02:00
} receiveValue: { [weak dependency, weak cell] change in
guard let cell = cell else { return }
guard let dependency = dependency else { return }
guard case .update(let object) = change.changeType,
let status = object as? Status else { return }
StatusSection.configureContentWarningOverlay(
statusView: cell.statusView,
status: status,
attribute: statusItemAttribute,
documentStore: dependency.context.documentStore,
animated: true
)
}
.store(in: &cell.disposeBag)
// set poll
2021-04-01 08:39:15 +02:00
let poll = (status.reblog ?? status).poll
2021-03-09 08:18:43 +01:00
StatusSection.configurePoll(
2021-03-05 06:41:48 +01:00
cell: cell,
poll: poll,
requestUserID: requestUserID,
updateProgressAnimated: false,
timestampUpdatePublisher: timestampUpdatePublisher
)
if let poll = poll {
ManagedObjectObserver.observe(object: poll)
.sink { _ in
// do nothing
2021-04-26 11:41:24 +02:00
} receiveValue: { [weak cell] change in
guard let cell = cell else { return }
2021-03-08 04:42:10 +01:00
guard case .update(let object) = change.changeType,
let newPoll = object as? Poll else { return }
2021-03-09 08:18:43 +01:00
StatusSection.configurePoll(
2021-03-05 06:41:48 +01:00
cell: cell,
poll: newPoll,
requestUserID: requestUserID,
updateProgressAnimated: true,
timestampUpdatePublisher: timestampUpdatePublisher
)
2021-03-02 12:10:45 +01:00
}
.store(in: &cell.disposeBag)
2021-03-02 12:10:45 +01:00
}
2021-04-19 12:06:02 +02:00
if let statusTableViewCell = cell as? StatusTableViewCell {
2021-04-28 13:56:30 +02:00
// toolbar
StatusSection.configureActionToolBar(
cell: statusTableViewCell,
dependency: dependency,
status: status,
requestUserID: requestUserID
)
// separator line
2021-04-19 12:06:02 +02:00
statusTableViewCell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden
}
2021-04-13 13:46:42 +02:00
// set date
2021-04-01 08:39:15 +02:00
let createdAt = (status.reblog ?? status).createdAt
2021-02-23 08:16:55 +01:00
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
timestampUpdatePublisher
2021-04-26 11:41:24 +02:00
.sink { [weak cell] _ in
guard let cell = cell else { return }
2021-02-23 08:16:55 +01:00
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
}
.store(in: &cell.disposeBag)
2021-02-08 11:29:27 +01:00
// observe model change
2021-04-01 08:39:15 +02:00
ManagedObjectObserver.observe(object: status.reblog ?? status)
2021-02-08 11:29:27 +01:00
.receive(on: DispatchQueue.main)
.sink { _ in
// do nothing
2021-04-26 11:41:24 +02:00
} receiveValue: { [weak dependency, weak cell] change in
guard let dependency = dependency else { return }
guard case .update(let object) = change.changeType,
2021-04-01 08:39:15 +02:00
let status = object as? Status else { return }
guard let statusTableViewCell = cell as? StatusTableViewCell else { return }
2021-04-26 09:58:49 +02:00
StatusSection.configureActionToolBar(
cell: statusTableViewCell,
2021-04-26 09:58:49 +02:00
dependency: dependency,
status: status,
requestUserID: requestUserID
)
2021-03-09 08:18:43 +01:00
2021-04-01 08:39:15 +02:00
os_log("%{public}s[%{public}ld], %{public}s: reblog count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.reblogsCount.intValue)
os_log("%{public}s[%{public}ld], %{public}s: like count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.favouritesCount.intValue)
2021-02-08 11:29:27 +01:00
}
.store(in: &cell.disposeBag)
2021-01-28 09:10:30 +01:00
}
2021-04-13 13:46:42 +02:00
static func configureContentWarningOverlay(
statusView: StatusView,
status: Status,
attribute: Item.StatusAttribute,
documentStore: DocumentStore,
animated: Bool
) {
statusView.contentWarningOverlayView.blurContentWarningTitleLabel.text = {
2021-04-16 14:29:08 +02:00
let spoilerText = (status.reblog ?? status).spoilerText ?? ""
if spoilerText.isEmpty {
return L10n.Common.Controls.Status.contentWarning
} else {
return L10n.Common.Controls.Status.contentWarningText(spoilerText)
}
}()
let appStartUpTimestamp = documentStore.appStartUpTimestamp
switch (status.reblog ?? status).sensitiveType {
case .none:
statusView.revealContentWarningButton.isHidden = true
statusView.contentWarningOverlayView.isHidden = true
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = true
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
case .all:
statusView.revealContentWarningButton.isHidden = false
statusView.contentWarningOverlayView.isHidden = false
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = true
statusView.playerContainerView.contentWarningOverlayView.isHidden = true
if let revealedAt = status.revealedAt, revealedAt > appStartUpTimestamp {
statusView.updateRevealContentWarningButton(isRevealing: true)
statusView.updateContentWarningDisplay(isHidden: true, animated: animated)
attribute.isRevealing.value = true
} else {
statusView.updateRevealContentWarningButton(isRevealing: false)
statusView.updateContentWarningDisplay(isHidden: false, animated: animated)
attribute.isRevealing.value = false
}
case .media(let isSensitive):
if !isSensitive, documentStore.defaultRevealStatusDict[status.id] == nil {
documentStore.defaultRevealStatusDict[status.id] = true
}
statusView.revealContentWarningButton.isHidden = false
statusView.contentWarningOverlayView.isHidden = true
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isHidden = false
statusView.playerContainerView.contentWarningOverlayView.isHidden = false
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
func updateContentOverlay() {
let needsReveal: Bool = {
if documentStore.defaultRevealStatusDict[status.id] == true {
return true
}
if let revealedAt = status.revealedAt, revealedAt > appStartUpTimestamp {
return true
}
return false
}()
attribute.isRevealing.value = needsReveal
if needsReveal {
statusView.updateRevealContentWarningButton(isRevealing: true)
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.update(isRevealing: true, style: .visualEffectView)
statusView.playerContainerView.contentWarningOverlayView.update(isRevealing: true, style: .visualEffectView)
} else {
statusView.updateRevealContentWarningButton(isRevealing: false)
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.update(isRevealing: false, style: .visualEffectView)
statusView.playerContainerView.contentWarningOverlayView.update(isRevealing: false, style: .visualEffectView)
}
}
if animated {
UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) {
updateContentOverlay()
} completion: { _ in
// do nothing
}
} else {
updateContentOverlay()
}
}
}
2021-04-13 13:46:42 +02:00
static func configureThreadMeta(
cell: StatusTableViewCell,
status: Status
) {
cell.selectionStyle = .none
cell.threadMetaView.dateLabel.text = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: status.createdAt)
}()
let reblogCountTitle: String = {
let count = status.reblogsCount.intValue
if count > 1 {
return L10n.Scene.Thread.Reblog.multiple(String(count))
} else {
return L10n.Scene.Thread.Reblog.single(String(count))
}
}()
cell.threadMetaView.reblogButton.setTitle(reblogCountTitle, for: .normal)
let favoriteCountTitle: String = {
let count = status.favouritesCount.intValue
if count > 1 {
return L10n.Scene.Thread.Favorite.multiple(String(count))
} else {
return L10n.Scene.Thread.Favorite.single(String(count))
}
}()
cell.threadMetaView.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
cell.threadMetaView.isHidden = false
}
2021-03-10 12:12:53 +01:00
static func configureHeader(
2021-04-19 12:06:02 +02:00
cell: StatusCell,
2021-04-01 08:39:15 +02:00
status: Status
2021-03-10 12:12:53 +01:00
) {
2021-04-01 08:39:15 +02:00
if status.reblog != nil {
cell.statusView.headerContainerView.isHidden = false
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
2021-03-10 12:12:53 +01:00
cell.statusView.headerInfoLabel.text = {
2021-04-01 08:39:15 +02:00
let author = status.author
2021-03-10 12:12:53 +01:00
let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Common.Controls.Status.userReblogged(name)
2021-03-10 12:12:53 +01:00
}()
2021-04-13 13:46:42 +02:00
} else if status.inReplyToID != nil {
cell.statusView.headerContainerView.isHidden = false
2021-03-16 04:48:33 +01:00
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
2021-03-10 12:12:53 +01:00
cell.statusView.headerInfoLabel.text = {
2021-04-13 13:46:42 +02:00
guard let replyTo = status.replyTo else {
return L10n.Common.Controls.Status.userRepliedTo("-")
}
2021-03-10 12:12:53 +01:00
let author = replyTo.author
let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Common.Controls.Status.userRepliedTo(name)
}()
} else {
cell.statusView.headerContainerView.isHidden = true
2021-03-10 12:12:53 +01:00
}
}
2021-03-09 08:18:43 +01:00
static func configureActionToolBar(
2021-04-28 13:56:30 +02:00
cell: StatusTableViewCell,
2021-04-26 09:58:49 +02:00
dependency: NeedsDependency,
2021-04-01 08:39:15 +02:00
status: Status,
2021-03-09 08:18:43 +01:00
requestUserID: String
) {
2021-04-01 08:39:15 +02:00
let status = status.reblog ?? status
2021-03-09 08:18:43 +01:00
// set reply
let replyCountTitle: String = {
2021-04-01 08:39:15 +02:00
let count = status.repliesCount?.intValue ?? 0
2021-03-09 08:18:43 +01:00
return StatusSection.formattedNumberTitleForActionButton(count)
}()
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
2021-03-15 11:19:45 +01:00
// set reblog
2021-04-01 08:39:15 +02:00
let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
2021-03-15 11:19:45 +01:00
let reblogCountTitle: String = {
2021-04-01 08:39:15 +02:00
let count = status.reblogsCount.intValue
2021-03-09 08:18:43 +01:00
return StatusSection.formattedNumberTitleForActionButton(count)
}()
2021-03-15 11:19:45 +01:00
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged
2021-03-09 08:18:43 +01:00
// set like
2021-04-01 08:39:15 +02:00
let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
2021-03-09 08:18:43 +01:00
let favoriteCountTitle: String = {
2021-04-01 08:39:15 +02:00
let count = status.favouritesCount.intValue
2021-03-09 08:18:43 +01:00
return StatusSection.formattedNumberTitleForActionButton(count)
}()
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike
2021-04-26 09:58:49 +02:00
2021-04-30 08:55:02 +02:00
Publishers.CombineLatest(
dependency.context.blockDomainService.blockedDomains,
ManagedObjectObserver.observe(object: status.authorForUserProvider)
.assertNoFailure()
)
.receive(on: DispatchQueue.main)
2021-04-30 09:38:15 +02:00
.sink { [weak dependency, weak cell] _,change in
2021-04-30 08:55:02 +02:00
guard let cell = cell else { return }
guard let dependency = dependency else { return }
2021-04-30 09:38:15 +02:00
switch change.changeType {
case .delete:
return
case .update(_):
break
case .none:
break
}
2021-05-06 12:03:58 +02:00
StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
2021-04-30 08:55:02 +02:00
}
.store(in: &cell.disposeBag)
2021-05-06 12:03:58 +02:00
self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
2021-03-09 08:18:43 +01:00
}
static func configurePoll(
2021-04-19 12:06:02 +02:00
cell: StatusCell,
poll: Poll?,
2021-03-05 06:41:48 +01:00
requestUserID: String,
updateProgressAnimated: Bool,
timestampUpdatePublisher: AnyPublisher<Date, Never>
) {
guard let poll = poll,
2021-03-10 07:36:28 +01:00
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
2021-03-05 08:53:36 +01:00
let votedOptions = poll.options.filter { option in
2021-03-10 07:36:28 +01:00
(option.votedBy ?? Set()).map(\.id).contains(requestUserID)
2021-03-05 08:53:36 +01:00
}
let didVotedLocal = !votedOptions.isEmpty
2021-03-10 07:36:28 +01:00
let didVotedRemote = (poll.votedBy ?? Set()).map(\.id).contains(requestUserID)
2021-03-05 08:53:36 +01:00
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<PollSection, PollItem>()
snapshot.appendSections([.main])
2021-03-05 08:53:36 +01:00
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 = {
2021-03-05 08:53:36 +01:00
// check didVotedRemote later to make the local change possible
if !votedOptions.isEmpty {
return votedOptions.contains(option) ? .on : .off
} else if poll.expired {
return .none
2021-03-05 08:53:36 +01:00
} else if didVotedRemote, votedOptions.isEmpty {
return .none
} else {
return .off
}
}()
let voteState: PollItem.Attribute.VoteState = {
2021-03-05 07:23:26 +01:00
var needsReveal: Bool
if poll.expired {
needsReveal = true
2021-03-05 08:53:36 +01:00
} else if didVotedRemote {
2021-03-05 07:23:26 +01:00
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)
2021-03-05 06:41:48 +01:00
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
}
2021-01-28 09:10:30 +01:00
}
extension StatusSection {
2021-01-28 09:10:30 +01:00
private static func formattedNumberTitleForActionButton(_ number: Int?) -> String {
guard let number = number, number > 0 else { return "" }
return String(number)
}
2021-04-26 09:58:49 +02:00
private static func setupStatusMoreButtonMenu(
2021-04-28 13:56:30 +02:00
cell: StatusTableViewCell,
2021-04-26 09:58:49 +02:00
dependency: NeedsDependency,
status: Status) {
2021-04-28 13:56:30 +02:00
guard let userProvider = dependency as? UserProvider else { fatalError() }
2021-04-26 09:58:49 +02:00
guard let authenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
2021-04-29 04:50:10 +02:00
let author = status.authorForUserProvider
let isMyself = authenticationBox.userID == author.id
let canReport = !isMyself
let isInSameDomain = authenticationBox.domain == author.domainFromAcct
2021-04-28 13:56:30 +02:00
let isMuting = (author.mutingBy ?? Set()).map(\.id).contains(authenticationBox.userID)
let isBlocking = (author.blockingBy ?? Set()).map(\.id).contains(authenticationBox.userID)
2021-04-30 08:55:02 +02:00
let isDomainBlocking = dependency.context.blockDomainService.blockedDomains.value.contains(author.domainFromAcct)
2021-04-26 09:58:49 +02:00
cell.statusView.actionToolbarContainer.moreButton.showsMenuAsPrimaryAction = true
2021-04-30 08:55:02 +02:00
cell.statusView.actionToolbarContainer.moreButton.menu = UserProviderFacade.createProfileActionMenu(
for: author,
isMyself: isMyself,
2021-04-30 08:55:02 +02:00
isMuting: isMuting,
isBlocking: isBlocking,
isInSameDomain: isInSameDomain,
isDomainBlocking: isDomainBlocking,
provider: userProvider,
cell: cell,
sourceView: cell.statusView.actionToolbarContainer.moreButton,
barButtonItem: nil,
shareUser: nil,
shareStatus: status
)
2021-04-26 09:58:49 +02:00
}
2021-01-28 09:10:30 +01:00
}