diff --git a/DB/Sources/DB/Entities/CollectionItem.swift b/DB/Sources/DB/Entities/CollectionItem.swift index c8b4191..d0df88a 100644 --- a/DB/Sources/DB/Entities/CollectionItem.swift +++ b/DB/Sources/DB/Entities/CollectionItem.swift @@ -54,4 +54,13 @@ public extension CollectionItem { public extension CollectionItem.StatusConfiguration { static let `default` = Self(showContentToggled: false, showAttachmentsToggled: false) + + func reply() -> Self { + Self(showContentToggled: showContentToggled, + showAttachmentsToggled: showAttachmentsToggled, + isContextParent: false, + isPinned: false, + isReplyInContext: false, + hasReplyFollowing: true) + } } diff --git a/View Controllers/NewStatusViewController.swift b/View Controllers/NewStatusViewController.swift index 1172c5f..08240db 100644 --- a/View Controllers/NewStatusViewController.swift +++ b/View Controllers/NewStatusViewController.swift @@ -68,6 +68,18 @@ final class NewStatusViewController: UIViewController { self?.viewModel.post() } + #if !IS_SHARE_EXTENSION + if let inReplyToViewModel = viewModel.inReplyToViewModel { + let statusView = StatusView(configuration: .init(viewModel: inReplyToViewModel)) + + statusView.isUserInteractionEnabled = false + statusView.bodyView.alpha = 0.5 + statusView.buttonsStackView.isHidden = true + + stackView.addArrangedSubview(statusView) + } + #endif + setupViewModelBindings() } @@ -145,7 +157,9 @@ private extension NewStatusViewController { let compositionView = CompositionView( viewModel: compositionViewModel, parentViewModel: viewModel) - stackView.insertArrangedSubview(compositionView, at: index) + let adjustedIndex = viewModel.inReplyToViewModel == nil ? index : index + 1 + + stackView.insertArrangedSubview(compositionView, at: adjustedIndex) compositionView.textView.becomeFirstResponder() DispatchQueue.main.async { diff --git a/View Controllers/ProfileViewController.swift b/View Controllers/ProfileViewController.swift index 5b1bd63..0df55f0 100644 --- a/View Controllers/ProfileViewController.swift +++ b/View Controllers/ProfileViewController.swift @@ -9,10 +9,10 @@ final class ProfileViewController: TableViewController { private let viewModel: ProfileViewModel private var cancellables = Set() - required init(viewModel: ProfileViewModel, identification: Identification) { + required init(viewModel: ProfileViewModel, rootViewModel: RootViewModel, identification: Identification) { self.viewModel = viewModel - super.init(viewModel: viewModel, identification: identification) + super.init(viewModel: viewModel, rootViewModel: rootViewModel, identification: identification) } override func viewDidLoad() { @@ -108,7 +108,7 @@ private extension ProfileViewController { let reportViewModel = self.viewModel.accountViewModel?.reportViewModel() else { return } - self.report(viewModel: reportViewModel) + self.report(reportViewModel: reportViewModel) }) if relationship.blocking { diff --git a/View Controllers/TableViewController.swift b/View Controllers/TableViewController.swift index 7fe6850..bb247ed 100644 --- a/View Controllers/TableViewController.swift +++ b/View Controllers/TableViewController.swift @@ -6,10 +6,12 @@ import SafariServices import SwiftUI import ViewModels +// swiftlint:disable file_length class TableViewController: UITableViewController { var transitionViewTag = -1 private let viewModel: CollectionViewModel + private let rootViewModel: RootViewModel private let identification: Identification private let loadingTableFooterView = LoadingTableFooterView() private let webfingerIndicatorView = WebfingerIndicatorView() @@ -21,8 +23,9 @@ class TableViewController: UITableViewController { .init(tableView: tableView, viewModelProvider: viewModel.viewModel(indexPath:)) }() - init(viewModel: CollectionViewModel, identification: Identification) { + init(viewModel: CollectionViewModel, rootViewModel: RootViewModel, identification: Identification) { self.viewModel = viewModel + self.rootViewModel = rootViewModel self.identification = identification super.init(style: .plain) @@ -109,15 +112,6 @@ class TableViewController: UITableViewController { } } -extension TableViewController { - func report(viewModel: ReportViewModel) { - let reportViewController = ReportViewController(viewModel: viewModel) - let navigationController = UINavigationController(rootViewController: reportViewController) - - present(navigationController, animated: true) - } -} - extension TableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { guard @@ -132,6 +126,13 @@ extension TableViewController: UITableViewDataSourcePrefetching { } extension TableViewController { + func report(reportViewModel: ReportViewModel) { + let reportViewController = ReportViewController(viewModel: reportViewModel) + let navigationController = UINavigationController(rootViewController: reportViewController) + + present(navigationController, animated: true) + } + func sizeTableHeaderFooterViews() { // https://useyourloaf.com/blog/variable-height-table-view-header/ if let headerView = tableView.tableHeaderView { @@ -299,32 +300,40 @@ private extension TableViewController { case let .share(url): share(url: url) case let .navigation(navigation): - switch navigation { - case let .collection(collectionService): - show(TableViewController( - viewModel: CollectionItemsViewModel( - collectionService: collectionService, - identification: identification), - identification: identification), - sender: self) - case let .profile(profileService): - show(ProfileViewController( - viewModel: ProfileViewModel( - profileService: profileService, - identification: identification), - identification: identification), - sender: self) - case let .url(url): - present(SFSafariViewController(url: url), animated: true) - case .webfingerStart: - webfingerIndicatorView.startAnimating() - case .webfingerEnd: - webfingerIndicatorView.stopAnimating() - } + handle(navigation: navigation) case let .attachment(attachmentViewModel, statusViewModel): present(attachmentViewModel: attachmentViewModel, statusViewModel: statusViewModel) + case let .reply(statusViewModel): + reply(statusViewModel: statusViewModel) case let .report(reportViewModel): - report(viewModel: reportViewModel) + report(reportViewModel: reportViewModel) + } + } + + func handle(navigation: Navigation) { + switch navigation { + case let .collection(collectionService): + show(TableViewController( + viewModel: CollectionItemsViewModel( + collectionService: collectionService, + identification: identification), + rootViewModel: rootViewModel, + identification: identification), + sender: self) + case let .profile(profileService): + show(ProfileViewController( + viewModel: ProfileViewModel( + profileService: profileService, + identification: identification), + rootViewModel: rootViewModel, + identification: identification), + sender: self) + case let .url(url): + present(SFSafariViewController(url: url), animated: true) + case .webfingerStart: + webfingerIndicatorView.startAnimating() + case .webfingerEnd: + webfingerIndicatorView.stopAnimating() } } @@ -365,6 +374,18 @@ private extension TableViewController { } } + func reply(statusViewModel: StatusViewModel) { + let newStatusViewModel = rootViewModel.newStatusViewModel( + identification: identification, + inReplyTo: statusViewModel) + let newStatusViewController = UIHostingController(rootView: NewStatusView { newStatusViewModel }) + let navigationController = UINavigationController(rootViewController: newStatusViewController) + + navigationController.modalPresentationStyle = .overFullScreen + + present(navigationController, animated: true) + } + func set(expandAllState: ExpandAllState) { switch expandAllState { case .hidden: @@ -388,3 +409,4 @@ private extension TableViewController { present(activityViewController, animated: true, completion: nil) } } +// swiftlint:enable file_length diff --git a/ViewModels/Sources/ViewModels/Entities/CollectionItemEvent.swift b/ViewModels/Sources/ViewModels/Entities/CollectionItemEvent.swift index 8acd955..50f2620 100644 --- a/ViewModels/Sources/ViewModels/Entities/CollectionItemEvent.swift +++ b/ViewModels/Sources/ViewModels/Entities/CollectionItemEvent.swift @@ -7,6 +7,7 @@ public enum CollectionItemEvent { case ignorableOutput case navigation(Navigation) case attachment(AttachmentViewModel, StatusViewModel) + case reply(StatusViewModel) case report(ReportViewModel) case share(URL) } diff --git a/ViewModels/Sources/ViewModels/Entities/Navigation.swift b/ViewModels/Sources/ViewModels/Entities/Navigation.swift new file mode 100644 index 0000000..a3af2e9 --- /dev/null +++ b/ViewModels/Sources/ViewModels/Entities/Navigation.swift @@ -0,0 +1,5 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import ServiceLayer + +public typealias Navigation = ServiceLayer.Navigation diff --git a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift index 43097fa..1a35fcf 100644 --- a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift @@ -14,6 +14,7 @@ public final class NewStatusViewModel: ObservableObject { @Published public var canChangeIdentity = true @Published public var alertItem: AlertItem? @Published public private(set) var postingState = PostingState.composing + public let inReplyToViewModel: StatusViewModel? public let events: AnyPublisher private let allIdentitiesService: AllIdentitiesService @@ -24,10 +25,12 @@ public final class NewStatusViewModel: ObservableObject { public init(allIdentitiesService: AllIdentitiesService, identification: Identification, - environment: AppEnvironment) { + environment: AppEnvironment, + inReplyTo: StatusViewModel?) { self.allIdentitiesService = allIdentitiesService self.identification = identification self.environment = environment + inReplyToViewModel = inReplyTo compositionViewModels = [CompositionViewModel(eventsSubject: compositionEventsSubject)] events = eventsSubject.eraseToAnyPublisher() visibility = identification.identity.preferences.postingDefaultVisibility @@ -109,7 +112,7 @@ public extension NewStatusViewModel { func post() { guard let unposted = compositionViewModels.first(where: { !$0.isPosted }) else { return } - post(viewModel: unposted, inReplyToId: nil) + post(viewModel: unposted, inReplyToId: inReplyToViewModel?.id) } } diff --git a/ViewModels/Sources/ViewModels/RootViewModel.swift b/ViewModels/Sources/ViewModels/RootViewModel.swift index 6806a9d..031362a 100644 --- a/ViewModels/Sources/ViewModels/RootViewModel.swift +++ b/ViewModels/Sources/ViewModels/RootViewModel.swift @@ -58,11 +58,12 @@ public extension RootViewModel { instanceURLService: InstanceURLService(environment: environment)) } - func newStatusViewModel(identification: Identification) -> NewStatusViewModel { + func newStatusViewModel(identification: Identification, inReplyTo: StatusViewModel? = nil) -> NewStatusViewModel { NewStatusViewModel( allIdentitiesService: allIdentitiesService, identification: identification, - environment: environment) + environment: environment, + inReplyTo: inReplyTo) } } diff --git a/ViewModels/Sources/ViewModels/ShareExtensionNavigationViewModel.swift b/ViewModels/Sources/ViewModels/ShareExtensionNavigationViewModel.swift index 1f05754..ca1884b 100644 --- a/ViewModels/Sources/ViewModels/ShareExtensionNavigationViewModel.swift +++ b/ViewModels/Sources/ViewModels/ShareExtensionNavigationViewModel.swift @@ -36,6 +36,7 @@ public extension ShareExtensionNavigationViewModel { return NewStatusViewModel( allIdentitiesService: allIdentitiesService, identification: identification, - environment: environment) + environment: environment, + inReplyTo: nil) } } diff --git a/ViewModels/Sources/ViewModels/StatusViewModel.swift b/ViewModels/Sources/ViewModels/StatusViewModel.swift index ffb0800..384cbf2 100644 --- a/ViewModels/Sources/ViewModels/StatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/StatusViewModel.swift @@ -72,6 +72,8 @@ public extension StatusViewModel { sensitive || identification.identity.preferences.readingExpandMedia == .hideAll } + var id: Status.Id { statusService.status.displayStatus.id } + var accountName: String { "@".appending(statusService.status.displayStatus.account.acct) } var avatarURL: URL { @@ -200,6 +202,14 @@ public extension StatusViewModel { .eraseToAnyPublisher()) } + func reply() { + let replyViewModel = Self(statusService: statusService, identification: identification) + + replyViewModel.configuration = configuration.reply() + + eventsSubject.send(Just(.reply(replyViewModel)).setFailureType(to: Error.self).eraseToAnyPublisher()) + } + func toggleReblogged() { eventsSubject.send( statusService.toggleReblogged() diff --git a/Views/Status/StatusView.swift b/Views/Status/StatusView.swift index f181e45..1f8050f 100644 --- a/Views/Status/StatusView.swift +++ b/Views/Status/StatusView.swift @@ -198,6 +198,10 @@ private extension StatusView { interactionsStackView.addArrangedSubview(favoritedByButton) interactionsStackView.distribution = .fillEqually + replyButton.addAction( + UIAction { [weak self] _ in self?.statusConfiguration.viewModel.reply() }, + for: .touchUpInside) + reblogButton.addAction( UIAction { [weak self] _ in self?.statusConfiguration.viewModel.toggleReblogged() }, for: .touchUpInside) diff --git a/Views/TableView.swift b/Views/TableView.swift index 73960c0..12653b2 100644 --- a/Views/TableView.swift +++ b/Views/TableView.swift @@ -5,10 +5,11 @@ import ViewModels struct TableView: UIViewControllerRepresentable { @EnvironmentObject var identification: Identification + @EnvironmentObject var rootViewModel: RootViewModel let viewModelClosure: () -> CollectionViewModel func makeUIViewController(context: Context) -> TableViewController { - TableViewController(viewModel: viewModelClosure(), identification: identification) + TableViewController(viewModel: viewModelClosure(), rootViewModel: rootViewModel, identification: identification) } func updateUIViewController(_ uiViewController: TableViewController, context: Context) {