diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index d0436bd2..617f5eaa 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -166,6 +166,8 @@ D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; }; D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; }; D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; }; + D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; }; + D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; }; D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; }; D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; }; D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; }; @@ -513,6 +515,8 @@ D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = ""; }; D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = ""; }; D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = ""; }; + D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = ""; }; + D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = ""; }; D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = ""; }; D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = ""; }; D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = ""; }; @@ -1253,6 +1257,7 @@ children = ( D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */, D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */, + D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */, D620483523D38075008A63EF /* ContentTextView.swift */, D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */, D6969E9F240C8384002843CE /* EmojiLabel.swift */, @@ -1293,6 +1298,7 @@ isa = PBXGroup; children = ( D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */, + D6895DC128D65274006341DA /* CustomAlertController.swift */, D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */, D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */, D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */, @@ -1770,6 +1776,7 @@ D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */, D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */, D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */, + D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, 0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */, D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */, @@ -1943,6 +1950,7 @@ D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */, D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */, D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */, + D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */, D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */, D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */, 04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */, diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index 57f2dbd6..53d95762 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -73,6 +73,7 @@ class ImageCache { } func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) { + // todo: this should integrate with the task cancellation mechanism somehow return await withCheckedContinuation { continuation in _ = get(url, loadOriginal: loadOriginal) { data, image in continuation.resume(returning: (data, image)) diff --git a/Tusker/Screens/Utilities/CustomAlertController.swift b/Tusker/Screens/Utilities/CustomAlertController.swift new file mode 100644 index 00000000..d96eb1b9 --- /dev/null +++ b/Tusker/Screens/Utilities/CustomAlertController.swift @@ -0,0 +1,393 @@ +// +// CustomAlertController.swift +// Tusker +// +// Created by Shadowfacts on 9/17/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import UIKit + +class CustomAlertController: UIViewController { + private let config: Configuration + + fileprivate var blurView: UIVisualEffectView! + fileprivate var dimmingView: UIView! + fileprivate var buttonsStack: UIStackView! + fileprivate var actionsView: CustomAlertActionsView! + private var separatorHeightConstraint: NSLayoutConstraint! + + init(config: Configuration) { + self.config = config + + super.init(nibName: nil, bundle: nil) + + transitioningDelegate = self + modalPresentationStyle = .custom + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .clear + + dimmingView = UIView() + dimmingView.backgroundColor = .black.withAlphaComponent(0.2) + dimmingView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(dimmingView) + NSLayoutConstraint.activate([ + dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + dimmingView.topAnchor.constraint(equalTo: view.topAnchor), + dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial)) + + blurView.backgroundColor = .systemBackground + blurView.layer.cornerRadius = 15 + blurView.layer.cornerCurve = .continuous + blurView.layer.masksToBounds = true + blurView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(blurView) + NSLayoutConstraint.activate([ + blurView.widthAnchor.constraint(equalToConstant: 270), + blurView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100), + blurView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + blurView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 0 + stack.alignment = .fill + stack.translatesAutoresizingMaskIntoConstraints = false + blurView.contentView.addSubview(stack) + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor), + blurView.contentView.trailingAnchor.constraint(equalTo: stack.trailingAnchor), + stack.topAnchor.constraint(equalTo: blurView.contentView.topAnchor, constant: 16), + stack.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor), + ]) + + let titleLabel = UILabel() + titleLabel.text = config.title + titleLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0) + titleLabel.numberOfLines = 0 + titleLabel.textAlignment = .center + stack.addArrangedSubview(titleLabel) + + stack.addSpacer(length: 8) + + stack.addArrangedSubview(config.content) + + stack.addSpacer(length: 16) + + let separator = UIView() + separator.backgroundColor = .separator + stack.addArrangedSubview(separator) + separatorHeightConstraint = separator.heightAnchor.constraint(equalToConstant: 0.5) + NSLayoutConstraint.activate([ + separator.widthAnchor.constraint(equalTo: stack.widthAnchor), + separatorHeightConstraint, + ]) + + actionsView = CustomAlertActionsView(config: config, dismiss: { [unowned self] in + self.dismiss(animated: true) + }) + stack.addArrangedSubview(actionsView) + NSLayoutConstraint.activate([ + actionsView.widthAnchor.constraint(equalTo: stack.widthAnchor), + ]) + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + separatorHeightConstraint.constant = 1 / view.window!.screen.scale + } + + struct Configuration { + var title: String + var content: UIView + var actions: [Action] + } + + struct Action { + let title: String + let style: UIAlertAction.Style + let handler: (() -> Void)? + } +} + +class CustomAlertActionsView: UIControl { + private let dismiss: () -> Void + + private let stack = UIStackView() + private var labels: [UIView] = [] + private var labelWrappers: [UIView] = [] + private var labelWrapperWidthConstraints: [NSLayoutConstraint] = [] + // the actions from the config but reordered to match labelWrappers order + private var reorderedActions: [CustomAlertController.Action] = [] + private var separators: [UIView] = [] + private var separatorSizeConstraints: [NSLayoutConstraint] = [] + + private let generator = UISelectionFeedbackGenerator() + private var currentSelectedActionIndex: Int? + + init(config: CustomAlertController.Configuration, dismiss: @escaping () -> Void) { + self.dismiss = dismiss + + super.init(frame: .zero) + + stack.alignment = .fill + stack.translatesAutoresizingMaskIntoConstraints = false + addSubview(stack) + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: leadingAnchor), + stack.trailingAnchor.constraint(equalTo: trailingAnchor), + stack.topAnchor.constraint(equalTo: topAnchor), + stack.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + for action in config.actions { + let labelWrapper = UIView() + labelWrapper.isAccessibilityElement = true + labelWrapper.accessibilityTraits = .button + labelWrapper.accessibilityRespondsToUserInteraction = true + labelWrapper.accessibilityLabel = action.title + + let label = UILabel() + labels.append(label) + label.text = action.title + label.textColor = .tintColor + switch action.style { + case .cancel: + label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0) + case .destructive: + label.textColor = .systemRed + default: + break + } + label.translatesAutoresizingMaskIntoConstraints = false + labelWrapper.addSubview(label) + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(greaterThanOrEqualTo: labelWrapper.leadingAnchor, constant: 4), + label.trailingAnchor.constraint(lessThanOrEqualTo: labelWrapper.trailingAnchor, constant: -4), + label.centerXAnchor.constraint(equalTo: labelWrapper.centerXAnchor), + label.centerYAnchor.constraint(equalTo: labelWrapper.centerYAnchor), + labelWrapper.heightAnchor.constraint(equalToConstant: 44), + ]) + + if action.style == .cancel { + labelWrappers.insert(labelWrapper, at: 0) + reorderedActions.insert(action, at: 0) + } else { + labelWrappers.append(labelWrapper) + reorderedActions.append(action) + } + } + + var first = true + for wrapper in labelWrappers { + if first { + first = false + } else { + let separator = UIView() + separator.backgroundColor = .separator + stack.addArrangedSubview(separator) + separators.append(separator) + } + stack.addArrangedSubview(wrapper) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + updateAxis() + super.layoutSubviews() + } + + private func updateAxis() { + if reorderedActions.count > 2 || labels.map({ $0.intrinsicContentSize.width }).contains(where: { $0 > (bounds.width - 16) / 2 }) { + stack.axis = .vertical + NSLayoutConstraint.deactivate(labelWrapperWidthConstraints) + labelWrapperWidthConstraints = [] + NSLayoutConstraint.deactivate(separatorSizeConstraints) + separatorSizeConstraints = separators.map { + $0.heightAnchor.constraint(equalToConstant: 0.5) + } + NSLayoutConstraint.activate(separatorSizeConstraints) + } else { + stack.axis = .horizontal + labelWrapperWidthConstraints = labelWrappers.map { + $0.widthAnchor.constraint(equalToConstant: (bounds.width - 0.5) / 2) + } + NSLayoutConstraint.activate(labelWrapperWidthConstraints) + NSLayoutConstraint.deactivate(separatorSizeConstraints) + separatorSizeConstraints = separators.map { + $0.widthAnchor.constraint(equalToConstant: 0.5) + } + NSLayoutConstraint.activate(separatorSizeConstraints) + } + } + + // MARK: - UIControl + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + // we want this view to handle all touches inside it + return self + } + + override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + for (index, labelWrapper) in labelWrappers.enumerated() { + if labelWrapper.point(inside: touch.location(in: labelWrapper), with: event) { + currentSelectedActionIndex = index + labelWrapper.backgroundColor = .secondarySystemFill + + generator.prepare() + + return true + } + } + return false + } + + override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + for (index, labelWrapper) in labelWrappers.enumerated() { + if labelWrapper.point(inside: touch.location(in: labelWrapper), with: event) { + if index != currentSelectedActionIndex { + if let currentSelectedActionIndex { + labelWrappers[currentSelectedActionIndex].backgroundColor = nil + } + generator.selectionChanged() + } + + currentSelectedActionIndex = index + labelWrapper.backgroundColor = .secondarySystemFill + + generator.prepare() + + return true + } + } + // didn't hit any button + if let currentSelectedActionIndex { + labelWrappers[currentSelectedActionIndex].backgroundColor = nil + self.currentSelectedActionIndex = nil + } + return true + } + + override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + super.endTracking(touch, with: event) + + if let currentSelectedActionIndex { + labelWrappers[currentSelectedActionIndex].backgroundColor = nil + reorderedActions[currentSelectedActionIndex].handler?() + dismiss() + } + } + + override func cancelTracking(with event: UIEvent?) { + super.cancelTracking(with: event) + + if let currentSelectedActionIndex { + labelWrappers[currentSelectedActionIndex].backgroundColor = nil + } + } + +} + +extension CustomAlertController { + override func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return CustomAlertPresentationAnimation() + } + + override func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return CustomAlertDismissAnimation() + } +} + +class CustomAlertPresentationAnimation: NSObject, UIViewControllerAnimatedTransitioning { + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.2 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let presenter = transitionContext.viewController(forKey: .from), + let alert = transitionContext.viewController(forKey: .to) as? CustomAlertController else { + transitionContext.completeTransition(false) + return + } + + let container = transitionContext.containerView + container.addSubview(alert.view) + + guard transitionContext.isAnimated else { + presenter.view.tintAdjustmentMode = .dimmed + transitionContext.completeTransition(true) + return + } + + alert.dimmingView.layer.opacity = 0 + alert.blurView.layer.opacity = 0 + alert.blurView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) + + UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseOut) { + presenter.view.tintAdjustmentMode = .dimmed + alert.dimmingView.layer.opacity = 1 + alert.blurView.layer.opacity = 1 + alert.blurView.transform = .identity + } completion: { _ in + transitionContext.completeTransition(true) + } + } +} + +class CustomAlertDismissAnimation: NSObject, UIViewControllerAnimatedTransitioning { + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.2 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let presenter = transitionContext.viewController(forKey: .to), + let alert = transitionContext.viewController(forKey: .from) as? CustomAlertController else { + transitionContext.completeTransition(false) + return + } + + guard transitionContext.isAnimated else { + presenter.view.tintAdjustmentMode = .dimmed + transitionContext.completeTransition(true) + return + } + + UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0) { + presenter.view.tintAdjustmentMode = .automatic + alert.view.layer.opacity = 0 + } completion: { _ in + transitionContext.completeTransition(true) + } + } +} + +fileprivate extension UIStackView { + func addSpacer(length: CGFloat) { + let spacer = UIView() + addArrangedSubview(spacer) + if axis == .vertical { + spacer.heightAnchor.constraint(equalToConstant: length).isActive = true + } else { + spacer.widthAnchor.constraint(equalToConstant: length).isActive = true + } + } +} + diff --git a/Tusker/Views/ConfirmReblogStatusPreviewView.swift b/Tusker/Views/ConfirmReblogStatusPreviewView.swift new file mode 100644 index 00000000..89028ebe --- /dev/null +++ b/Tusker/Views/ConfirmReblogStatusPreviewView.swift @@ -0,0 +1,86 @@ +// +// ConfirmReblogStatusPreviewView.swift +// Tusker +// +// Created by Shadowfacts on 9/17/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import UIKit + +class ConfirmReblogStatusPreviewView: UIView { + + private var avatarTask: Task? + + init(status: StatusMO) { + super.init(frame: .zero) + + let hStack = UIStackView() + hStack.axis = .horizontal + hStack.spacing = 8 + hStack.alignment = .leading + hStack.translatesAutoresizingMaskIntoConstraints = false + addSubview(hStack) + NSLayoutConstraint.activate([ + hStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), + hStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), + hStack.topAnchor.constraint(equalTo: topAnchor), + hStack.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + let avatarSize: CGFloat = 30 + let avatarImageView = UIImageView() + avatarImageView.layer.masksToBounds = true + avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarSize + hStack.addArrangedSubview(avatarImageView) + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: avatarSize), + avatarImageView.heightAnchor.constraint(equalToConstant: avatarSize), + ]) + if let avatar = status.account.avatar { + avatarTask = Task { + let (_, image) = await ImageCache.avatars.get(avatar) + try Task.checkCancellation() + avatarImageView.image = image + } + } + + let vStack = UIStackView() + vStack.axis = .vertical + vStack.spacing = 2 + vStack.alignment = .fill + vStack.setContentHuggingPriority(.defaultLow, for: .horizontal) + hStack.addArrangedSubview(vStack) + + let displayNameLabel = EmojiLabel() + displayNameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1).addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]]), size: 0) + displayNameLabel.adjustsFontSizeToFitWidth = true + displayNameLabel.updateForAccountDisplayName(account: status.account) + vStack.addArrangedSubview(displayNameLabel) + + let contentView = StatusContentTextView() + contentView.defaultFont = .preferredFont(forTextStyle: .caption2) + contentView.isUserInteractionEnabled = false + contentView.isScrollEnabled = false + contentView.backgroundColor = nil + contentView.textContainerInset = .zero + // remove the extra line spacing applied by StatusContentTextView because, since we're using a smaller font, the regular 2pt looks big + contentView.paragraphStyle = .default + // TODO: line limit + contentView.setTextFrom(status: status) + contentView.translatesAutoresizingMaskIntoConstraints = false + vStack.addArrangedSubview(contentView) + NSLayoutConstraint.activate([ + contentView.heightAnchor.constraint(lessThanOrEqualToConstant: 200), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + avatarTask?.cancel() + } + +} diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index a8d44f29..c485ed1b 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -23,6 +23,12 @@ class ContentTextView: LinkTextView, BaseEmojiLabel { var defaultFont: UIFont = .systemFont(ofSize: 17) var defaultColor: UIColor = .label + var paragraphStyle: NSParagraphStyle = { + let style = NSMutableParagraphStyle() + // 2 points is enough that it doesn't make things look weirdly spaced out, but leaves a slight gap when there are multiple lines of emojis + style.lineSpacing = 2 + return style + }() private(set) var hasEmojis = false @@ -36,9 +42,17 @@ class ContentTextView: LinkTextView, BaseEmojiLabel { // The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing. private weak var currentTargetedPreview: UITargetedPreview? - override func awakeFromNib() { - super.awakeFromNib() - + override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { delegate = self // Disable layer masking, otherwise the context menu opening animation @@ -78,10 +92,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel { mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines) mutAttrString.collapseWhitespace() - let style = NSMutableParagraphStyle() - // 2 points is enough that it doesn't make things look weirdly spaced out, but leaves a slight gap when there are multiple lines of emojis - style.lineSpacing = 2 - mutAttrString.addAttribute(.paragraphStyle, value: style, range: mutAttrString.fullRange) + mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: mutAttrString.fullRange) self.attributedText = mutAttrString } diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index 77202578..65a4410d 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -412,11 +412,14 @@ class BaseStatusTableViewCell: UITableViewCell { // if we are about to reblog and the user has confirmation enabled if !reblogged, Preferences.shared.confirmBeforeReblog { - let alert = UIAlertController(title: "Confirm Reblog", message: "Are you sure you want to reblog this post by @\(status.account.acct)?", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - alert.addAction(UIAlertAction(title: "Reblog", style: .default) { (_) in - self.toggleReblogInternal() - }) + let preview = ConfirmReblogStatusPreviewView(status: status) + let config = CustomAlertController.Configuration(title: "Are you sure you want to reblog this post?", content: preview, actions: [ + CustomAlertController.Action(title: "Cancel", style: .cancel, handler: nil), + CustomAlertController.Action(title: "Reblog", style: .default, handler: { [unowned self] in + self.toggleReblogInternal() + }), + ]) + let alert = CustomAlertController(config: config) delegate?.present(alert, animated: true) } else { toggleReblogInternal()