diff --git a/PocketKit/Sources/Analytics/Context/UIContext.swift b/PocketKit/Sources/Analytics/Context/UIContext.swift index a31f255df..a82580255 100644 --- a/PocketKit/Sources/Analytics/Context/UIContext.swift +++ b/PocketKit/Sources/Analytics/Context/UIContext.swift @@ -74,6 +74,7 @@ extension UIContext { case itemFavorite = "item_favorite" case itemUnfavorite = "item_unfavorite" case itemShare = "item_share" + case itemSave = "item_save" case slateDetail = "discover_topic" case recommendation = "recommendation" case reportItem = "report_item" diff --git a/PocketKit/Sources/PocketKit/Activity/PocketItemActivity.swift b/PocketKit/Sources/PocketKit/Activity/PocketItemActivity.swift index c6ea8627a..47b4e34f4 100644 --- a/PocketKit/Sources/PocketKit/Activity/PocketItemActivity.swift +++ b/PocketKit/Sources/PocketKit/Activity/PocketItemActivity.swift @@ -15,13 +15,9 @@ struct PocketItemActivity: PocketActivity { } let activityItems: [Any] - - init(item: SavedItem, additionalText: String? = nil) { - self.activityItems = Self.activityItems(for: item.readerURL, additionalText: additionalText) - } - - init(recommendation: Slate.Recommendation, additionalText: String? = nil) { - self.activityItems = Self.activityItems(for: recommendation.readerURL, additionalText: additionalText) + + init(url: URL?, additionalText: String? = nil) { + self.activityItems = Self.activityItems(for: url, additionalText: additionalText) } private static func activityItems(for url: URL?, additionalText: String?) -> [Any] { diff --git a/PocketKit/Sources/PocketKit/Article/ActivityItemSource.swift b/PocketKit/Sources/PocketKit/Article/ActivityItemSource.swift index 0b1f93711..04bd6213f 100644 --- a/PocketKit/Sources/PocketKit/Article/ActivityItemSource.swift +++ b/PocketKit/Sources/PocketKit/Article/ActivityItemSource.swift @@ -7,7 +7,7 @@ import UIKit /// Wrapping items in a `UIActivityItemSource` is a signal that tells /// the system that we want some separation between the items being shared. /// -/// See usages in ArticleViewController for more info. +/// See usages in ReadableViewController for more info. /// class ActivityItemSource: NSObject, UIActivityItemSource { private let item: Any diff --git a/PocketKit/Sources/PocketKit/Article/ArticleMetadataPresenter.swift b/PocketKit/Sources/PocketKit/Article/ArticleMetadataPresenter.swift index 839ca4556..fef299e81 100644 --- a/PocketKit/Sources/PocketKit/Article/ArticleMetadataPresenter.swift +++ b/PocketKit/Sources/PocketKit/Article/ArticleMetadataPresenter.swift @@ -37,16 +37,16 @@ private extension Style { } struct ArticleMetadataPresenter { - private let readable: Readable + private let readableViewModel: ReadableViewModel private let readerSettings: ReaderSettings - init(readable: Readable, readerSettings: ReaderSettings) { - self.readable = readable + init(readableViewModel: ReadableViewModel, readerSettings: ReaderSettings) { + self.readableViewModel = readableViewModel self.readerSettings = readerSettings } var attributedTitle: NSAttributedString? { - guard let title = readable.title else { + guard let title = readableViewModel.title else { return nil } @@ -57,7 +57,7 @@ struct ArticleMetadataPresenter { let byline = NSMutableAttributedString() let style = Style.byline(modifier: readerSettings) - if let authors = readable.authors, !authors.isEmpty { + if let authors = readableViewModel.authors, !authors.isEmpty { let authorNames = authors.compactMap { $0.name } let authorNamesString = ListFormatter.localizedString(byJoining: authorNames) as NSString let attributedAuthorNames = NSMutableAttributedString(string: authorNamesString as String, style: style) @@ -65,7 +65,7 @@ struct ArticleMetadataPresenter { byline.append(attributedAuthorNames) } - if let domain = readable.domain { + if let domain = readableViewModel.domain { if !byline.string.isEmpty { byline.append(NSAttributedString(string: " • ", style: style)) } @@ -77,7 +77,7 @@ struct ArticleMetadataPresenter { } var attributedPublishedDate: NSAttributedString? { - readable.publishDate + readableViewModel.publishDate .flatMap { $0.formatted(date: .long, time: .omitted) } .flatMap { NSAttributedString(string: $0, style: .publishedDate(modifier: readerSettings)) } } diff --git a/PocketKit/Sources/PocketKit/Article/Item+Readable.swift b/PocketKit/Sources/PocketKit/Article/Item+Readable.swift deleted file mode 100644 index c4ebeb604..000000000 --- a/PocketKit/Sources/PocketKit/Article/Item+Readable.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Sync -import Foundation - - -extension SavedItem: Readable { - var authors: [ReadableAuthor]? { - item?.authors?.compactMap { $0 as? Author } - } - - var title: String? { - item?.title - } - - var domain: String? { - item?.domainMetadata?.name ?? item?.domain - } - - var publishDate: Date? { - item?.datePublished - } - - var components: [ArticleComponent]? { - item?.article?.components - } - - var readerURL: URL? { - item?.resolvedURL ?? item?.givenURL ?? url - } - - func shareActivity(additionalText: String?) -> PocketActivity? { - PocketItemActivity(item: self, additionalText: additionalText) - } -} - -extension Author: ReadableAuthor { -} diff --git a/PocketKit/Sources/PocketKit/Article/Presenters/UnsupportedComponentPresenter.swift b/PocketKit/Sources/PocketKit/Article/Presenters/UnsupportedComponentPresenter.swift index 578c8a2cb..195d746d7 100644 --- a/PocketKit/Sources/PocketKit/Article/Presenters/UnsupportedComponentPresenter.swift +++ b/PocketKit/Sources/PocketKit/Article/Presenters/UnsupportedComponentPresenter.swift @@ -4,14 +4,14 @@ import UIKit class UnsupportedComponentPresenter: ArticleComponentPresenter { private let mainViewModel: MainViewModel - private let readable: Readable? + private let readableViewModel: ReadableViewModel? init( mainViewModel: MainViewModel, - readable: Readable? + readableViewModel: ReadableViewModel? ) { self.mainViewModel = mainViewModel - self.readable = readable + self.readableViewModel = readableViewModel } func cell(for indexPath: IndexPath, in collectionView: UICollectionView) -> UICollectionViewCell { @@ -31,6 +31,6 @@ class UnsupportedComponentPresenter: ArticleComponentPresenter { } private func handleShowInWebReaderButtonTap() { - mainViewModel.presentedWebReaderURL = readable?.readerURL + mainViewModel.presentedWebReaderURL = readableViewModel?.url } } diff --git a/PocketKit/Sources/PocketKit/Article/Presenters/VimeoComponentPresenter.swift b/PocketKit/Sources/PocketKit/Article/Presenters/VimeoComponentPresenter.swift index 20f7fd247..fd8491784 100644 --- a/PocketKit/Sources/PocketKit/Article/Presenters/VimeoComponentPresenter.swift +++ b/PocketKit/Sources/PocketKit/Article/Presenters/VimeoComponentPresenter.swift @@ -4,7 +4,7 @@ import UIKit class VimeoComponentPresenter: ArticleComponentPresenter { private let oEmbedService: OEmbedService - private let readable: Readable? + private let readableViewModel: ReadableViewModel? private let component: VideoComponent private let mainViewModel: MainViewModel @@ -14,13 +14,13 @@ class VimeoComponentPresenter: ArticleComponentPresenter { init( oEmbedService: OEmbedService, - readable: Readable?, + readableViewModel: ReadableViewModel?, component: VideoComponent, mainViewModel: MainViewModel, onContentLoaded: @escaping () -> Void ) { self.oEmbedService = oEmbedService - self.readable = readable + self.readableViewModel = readableViewModel self.component = component self.mainViewModel = mainViewModel self.onContentLoaded = onContentLoaded @@ -88,7 +88,7 @@ class VimeoComponentPresenter: ArticleComponentPresenter { extension VimeoComponentPresenter: VimeoComponentCellDelegate { func vimeoComponentCellDidTapOpenInWebView(_ cell: VimeoComponentCell) { - mainViewModel.presentedWebReaderURL = readable?.readerURL + mainViewModel.presentedWebReaderURL = readableViewModel?.url } func vimeoComponentCell(_ cell: VimeoComponentCell, didNavigateToURL url: URL) { diff --git a/PocketKit/Sources/PocketKit/Article/Presenters/YouTubeVideoComponentPresenter.swift b/PocketKit/Sources/PocketKit/Article/Presenters/YouTubeVideoComponentPresenter.swift index 87892ba7d..f8bfe1671 100644 --- a/PocketKit/Sources/PocketKit/Article/Presenters/YouTubeVideoComponentPresenter.swift +++ b/PocketKit/Sources/PocketKit/Article/Presenters/YouTubeVideoComponentPresenter.swift @@ -8,18 +8,18 @@ import UIKit class YouTubeVideoComponentPresenter: ArticleComponentPresenter { private let component: VideoComponent private let mainViewModel: MainViewModel - private let readable: Readable? + private let readableViewModel: ReadableViewModel? private var cancellable: AnyCancellable? = nil init( component: VideoComponent, mainViewModel: MainViewModel, - readable: Readable? + readableViewModel: ReadableViewModel? ) { self.component = component self.mainViewModel = mainViewModel - self.readable = readable + self.readableViewModel = readableViewModel } func size(for availableWidth: CGFloat) -> CGSize { @@ -66,6 +66,6 @@ class YouTubeVideoComponentPresenter: ArticleComponentPresenter { } private func handleShowInWebReaderButtonTap() { - mainViewModel.presentedWebReaderURL = readable?.readerURL + mainViewModel.presentedWebReaderURL = readableViewModel?.url } } diff --git a/PocketKit/Sources/PocketKit/Article/Readable.swift b/PocketKit/Sources/PocketKit/Article/Readable.swift deleted file mode 100644 index e042438be..000000000 --- a/PocketKit/Sources/PocketKit/Article/Readable.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Sync -import Foundation -import Textile - - -protocol Readable { - var components: [ArticleComponent]? { get } - var readerURL: URL? { get } - var textAlignment: TextAlignment { get } - - var title: String? { get } - var authors: [ReadableAuthor]? { get } - var domain: String? { get } - var publishDate: Date? { get } - - func shareActivity(additionalText: String?) -> PocketActivity? -} - -protocol ReadableAuthor { - var name: String? { get } -} diff --git a/PocketKit/Sources/PocketKit/Article/ArticleViewController.swift b/PocketKit/Sources/PocketKit/Article/ReadableViewController.swift similarity index 86% rename from PocketKit/Sources/PocketKit/Article/ArticleViewController.swift rename to PocketKit/Sources/PocketKit/Article/ReadableViewController.swift index 740826b47..5828780dd 100644 --- a/PocketKit/Sources/PocketKit/Article/ArticleViewController.swift +++ b/PocketKit/Sources/PocketKit/Article/ReadableViewController.swift @@ -6,31 +6,37 @@ import Analytics import Kingfisher +protocol ReadableViewControllerDelegate: AnyObject { + func readableViewController(_ controller: ReadableViewController, willOpenURL url: URL) + func readableViewControlled(_ controller: ReadableViewController, shareWithAdditionalText text: String?) +} -class ArticleViewController: UIViewController { +class ReadableViewController: UIViewController { private var metadata: ArticleMetadataPresenter? var presenters: [ArticleComponentPresenter]? - var item: Readable? { + var readableViewModel: ReadableViewModel? { didSet { - metadata = item.flatMap { item -> ArticleMetadataPresenter in + metadata = readableViewModel.flatMap { readableViewModel -> ArticleMetadataPresenter in ArticleMetadataPresenter( - readable: item, + readableViewModel: readableViewModel, readerSettings: readerSettings ) } - presenters = item?.components?.filter { !$0.isEmpty }.map { presenter(for: $0) } + presenters = readableViewModel?.components?.filter { !$0.isEmpty }.map { presenter(for: $0) } DispatchQueue.main.async { self.collectionView.reloadData() } } } + + weak var delegate: ReadableViewControllerDelegate? = nil private var contexts: [Context] { - guard let item = item, let url = item.readerURL else { + guard let viewModel = readableViewModel, let url = viewModel.url else { return [] } @@ -102,7 +108,7 @@ class ArticleViewController: UIViewController { } } -extension ArticleViewController: UICollectionViewDelegate { +extension ReadableViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { if let cell = cell as? YouTubeVideoComponentCell { cell.pause() @@ -110,7 +116,7 @@ extension ArticleViewController: UICollectionViewDelegate { } } -extension ArticleViewController: UICollectionViewDataSource { +extension ReadableViewController: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { guard let presenters = presenters else { return 0 @@ -125,7 +131,7 @@ extension ArticleViewController: UICollectionViewDataSource { ) -> Int { switch section { case 0: - return item == nil ? 0 : 1 + return readableViewModel == nil ? 0 : 1 default: return presenters?.count ?? 0 } @@ -158,31 +164,24 @@ extension ArticleViewController: UICollectionViewDataSource { } } -extension ArticleViewController: ArticleComponentTextCellDelegate { +extension ReadableViewController: ArticleComponentTextCellDelegate { func articleComponentTextCell( _ cell: ArticleComponentTextCell, didShareText selectedText: String? ) { - guard let item = item, let activity = item.shareActivity(additionalText: selectedText) else { - return - } - - viewModel.sharedActivity = activity + delegate?.readableViewControlled(self, shareWithAdditionalText: selectedText) } func articleComponentTextCell( _ cell: ArticleComponentTextCell, shouldOpenURL url: URL ) -> Bool { - let contentOpen = ContentOpenEvent(destination: .external, trigger: .click) - let link = UIContext.articleView.link - let contexts = contexts + [link] - tracker.track(event: contentOpen, contexts) + delegate?.readableViewController(self, willOpenURL: url) return true } } -extension ArticleViewController { +extension ReadableViewController { enum Constants { static let metaSectionContentInsets = NSDirectionalEdgeInsets( top: 16, @@ -258,7 +257,7 @@ extension ArticleViewController { } } -extension ArticleViewController { +extension ReadableViewController { func presenter(for component: ArticleComponent) -> ArticleComponentPresenter { switch component { case .text(let component): @@ -278,7 +277,7 @@ extension ArticleViewController { case .numberedList(let component): return ListComponentPresenter(component: component, readerSettings: readerSettings) case .table: - return UnsupportedComponentPresenter(mainViewModel: viewModel, readable: item) + return UnsupportedComponentPresenter(mainViewModel: viewModel, readableViewModel: readableViewModel) case .blockquote(let component): return BlockquoteComponentPresenter(component: component, readerSettings: readerSettings) case .video(let component): @@ -287,22 +286,22 @@ extension ArticleViewController { return YouTubeVideoComponentPresenter( component: component, mainViewModel: viewModel, - readable: item + readableViewModel: readableViewModel ) case .vimeoLink, .vimeoIframe, .vimeoMoogaloop: return VimeoComponentPresenter( oEmbedService: OEmbedService(session: URLSession.shared), - readable: item, + readableViewModel: readableViewModel, component: component, mainViewModel: viewModel ) { [weak self] in self?.layout.invalidateLayout() } default: - return UnsupportedComponentPresenter(mainViewModel: viewModel, readable: item) + return UnsupportedComponentPresenter(mainViewModel: viewModel, readableViewModel: readableViewModel) } default: - return UnsupportedComponentPresenter(mainViewModel: viewModel, readable: item) + return UnsupportedComponentPresenter(mainViewModel: viewModel, readableViewModel: readableViewModel) } } } diff --git a/PocketKit/Sources/PocketKit/Article/ReadableViewModel/ArchivedItemViewModel.swift b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/ArchivedItemViewModel.swift new file mode 100644 index 000000000..f7222a2a9 --- /dev/null +++ b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/ArchivedItemViewModel.swift @@ -0,0 +1,59 @@ +import Combine +import Sync +import Foundation +import Textile +import UIKit + + +class ArchivedItemViewModel: ReadableViewModel { + weak var delegate: ReadableViewModelDelegate? = nil + + @Published + private var _actions: [ReadableAction] = [] + var actions: Published<[ReadableAction]>.Publisher { $_actions } + + var components: [ArticleComponent]? { + item.item?.article?.components + } + + var textAlignment: Textile.TextAlignment { + TextAlignment(language: item.item?.language) + } + + var title: String? { + item.item?.title + } + + var authors: [ReadableAuthor]? { + item.item?.authors + } + + var domain: String? { + item.item?.domainMetadata?.name ?? item.item?.domain + } + + var publishDate: Date? { + item.item?.datePublished + } + + var url: URL? { + item.item?.resolvedURL ?? item.item?.givenURL ?? item.url + } + + private let item: ArchivedItem + + init(item: ArchivedItem) { + self.item = item + + _actions = [ + .save { self.delegate?.readableViewModelDidSave(self) }, + .favorite { self.delegate?.readableViewModelDidFavorite(self) } + ] + } + + func shareActivity(additionalText: String?) -> PocketItemActivity? { + PocketItemActivity(url: url, additionalText: additionalText) + } + + func delete() { } +} diff --git a/PocketKit/Sources/PocketKit/Article/ReadableViewModel/ReadableAction.swift b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/ReadableAction.swift new file mode 100644 index 000000000..d1b6db67c --- /dev/null +++ b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/ReadableAction.swift @@ -0,0 +1,58 @@ +import Foundation +import Textile +import UIKit + + +struct ReadableAction { + let title: String + let accessibilityIdentifier: String + let image: UIImage? + let handler: (() -> ())? +} + +extension ReadableAction { + static func save(_ handler: @escaping () -> ()) -> ReadableAction { + return ReadableAction( + title: "Save", + accessibilityIdentifier: "item-action-menu-save", + image: UIImage(asset: .save), + handler: handler + ) + } + + static func archive(_ handler: @escaping () -> ()) -> ReadableAction { + return ReadableAction( + title: "Archive", + accessibilityIdentifier: "item-action-menu-archive", + image: UIImage(systemName: "archivebox"), + handler: handler + ) + } + + static func delete(_ handler: @escaping () -> ()) -> ReadableAction { + return ReadableAction( + title: "Delete", + accessibilityIdentifier: "item-action-menu-delete", + image: UIImage(systemName: "trash"), + handler: handler + ) + } + + static func favorite(_ handler: @escaping () -> ()) -> ReadableAction { + return ReadableAction( + title: "Favorite", + accessibilityIdentifier: "item-action-menu-favorite", + image: UIImage(systemName: "star"), + handler: handler + ) + } + + static func unfavorite(_ handler: @escaping () -> ()) -> ReadableAction { + return ReadableAction( + title: "Unfavorite", + accessibilityIdentifier: "item-action-menu-unfavorite", + image: UIImage(systemName: "star.slash"), + handler: handler + ) + } +} diff --git a/PocketKit/Sources/PocketKit/Article/ReadableViewModel/ReadableViewModel.swift b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/ReadableViewModel.swift new file mode 100644 index 000000000..0ac9db656 --- /dev/null +++ b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/ReadableViewModel.swift @@ -0,0 +1,37 @@ +import Combine +import Sync +import Foundation +import Textile +import UIKit + + +protocol ReadableAuthor { + var name: String? { get } +} +extension Author: ReadableAuthor { } +extension UnmanagedItem.Author : ReadableAuthor { } + +protocol ReadableViewModelDelegate: AnyObject { + func readableViewModelDidFavorite(_ readableViewModel: ReadableViewModel) + func readableViewModelDidUnfavorite(_ readableViewModel: ReadableViewModel) + func readableViewModelDidArchive(_ readableViewModel: ReadableViewModel) + func readableViewModelDidDelete(_ readableViewModel: ReadableViewModel) + func readableViewModelDidSave(_ readableViewModel: ReadableViewModel) +} + +protocol ReadableViewModel: AnyObject { + var delegate: ReadableViewModelDelegate? { get set } + + var actions: Published<[ReadableAction]>.Publisher { get } + + var components: [ArticleComponent]? { get } + var textAlignment: TextAlignment { get } + var title: String? { get } + var authors: [ReadableAuthor]? { get } + var domain: String? { get } + var publishDate: Date? { get } + var url: URL? { get } + + func shareActivity(additionalText: String?) -> PocketItemActivity? + func delete() +} diff --git a/PocketKit/Sources/PocketKit/Article/ReadableViewModel/RecommendationViewModel.swift b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/RecommendationViewModel.swift new file mode 100644 index 000000000..146c939fd --- /dev/null +++ b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/RecommendationViewModel.swift @@ -0,0 +1,59 @@ +import Combine +import Sync +import Foundation +import Textile +import UIKit + + +class RecommendationViewModel: ReadableViewModel { + weak var delegate: ReadableViewModelDelegate? = nil + + @Published + private var _actions: [ReadableAction] = [] + var actions: Published<[ReadableAction]>.Publisher { $_actions } + + var components: [ArticleComponent]? { + recommendation.item.article?.components + } + + var textAlignment: Textile.TextAlignment { + TextAlignment(language: recommendation.item.language) + } + + var title: String? { + recommendation.item.title + } + + var authors: [ReadableAuthor]? { + recommendation.item.authors + } + + var domain: String? { + recommendation.item.domainMetadata?.name ?? recommendation.item.domain + } + + var publishDate: Date? { + recommendation.item.datePublished + } + + var url: URL? { + recommendation.item.resolvedURL ?? recommendation.item.givenURL + } + + private let recommendation: Slate.Recommendation + + init(recommendation: Slate.Recommendation) { + self.recommendation = recommendation + + _actions = [ + .save { self.delegate?.readableViewModelDidSave(self) }, + .favorite { self.delegate?.readableViewModelDidFavorite(self) } + ] + } + + func shareActivity(additionalText: String?) -> PocketItemActivity? { + PocketItemActivity(url: url, additionalText: additionalText) + } + + func delete() { } +} diff --git a/PocketKit/Sources/PocketKit/Article/ReadableViewModel/SavedItemViewModel.swift b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/SavedItemViewModel.swift new file mode 100644 index 000000000..0efb78d51 --- /dev/null +++ b/PocketKit/Sources/PocketKit/Article/ReadableViewModel/SavedItemViewModel.swift @@ -0,0 +1,98 @@ +import Sync +import Combine +import Foundation +import Textile + + +class SavedItemViewModel: ReadableViewModel { + weak var delegate: ReadableViewModelDelegate? = nil + + @Published + private var _actions: [ReadableAction] = [] + var actions: Published<[ReadableAction]>.Publisher { $_actions } + + var components: [ArticleComponent]? { + item.item?.article?.components + } + + var textAlignment: Textile.TextAlignment { + item.textAlignment + } + + var title: String? { + item.item?.title + } + + var authors: [ReadableAuthor]? { + item.item?.authors?.compactMap { $0 as? Author } + } + + var domain: String? { + item.item?.domainMetadata?.name ?? item.item?.domain + } + + var publishDate: Date? { + item.item?.datePublished + } + + var url: URL? { + item.item?.resolvedURL ?? item.item?.givenURL ?? item.url + } + + private var source: Source + private var favoriteSubscription: AnyCancellable? = nil + + private let item: SavedItem + + init(item: SavedItem, source: Source) { + self.item = item + self.source = source + + favoriteSubscription = item.publisher(for: \.isFavorite).sink { [weak self] _ in + self?.buildActions() + } + + buildActions() + } + + func favorite() { + source.favorite(item: item) + delegate?.readableViewModelDidFavorite(self) + } + + func unfavorite() { + source.unfavorite(item: item) + delegate?.readableViewModelDidUnfavorite(self) + } + + func archive() { + source.archive(item: item) + delegate?.readableViewModelDidArchive(self) + } + + func delete() { + source.delete(item: item) + delegate?.readableViewModelDidDelete(self) + } + + func shareActivity(additionalText: String?) -> PocketItemActivity? { + PocketItemActivity(url: url, additionalText: additionalText) + } + + private func buildActions() { + if item.isFavorite { + _actions = [ + .unfavorite { [weak self] in self?.unfavorite() }, + .archive { [weak self] in self?.archive() }, + .delete { [weak self] in self?.delete() } + ] + } else { + _actions = [ + .favorite { [weak self] in self?.favorite() }, + .archive { [weak self] in self?.archive() }, + .delete { [weak self] in self?.delete() } + ] + } + } +} + diff --git a/PocketKit/Sources/PocketKit/Article/Recommendation+Readable.swift b/PocketKit/Sources/PocketKit/Article/Recommendation+Readable.swift deleted file mode 100644 index 58a229473..000000000 --- a/PocketKit/Sources/PocketKit/Article/Recommendation+Readable.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation -import Textile -import Sync - - -extension Slate.Recommendation: Readable { - var title: String? { - item.title - } - - var authors: [ReadableAuthor]? { - item.authors - } - - var domain: String? { - item.domainMetadata?.name ?? item.domain - } - - var publishDate: Date? { - item.datePublished - } - - var components: [ArticleComponent]? { - item.article?.components - } - - var readerURL: URL? { - item.resolvedURL ?? item.givenURL - } - - var textAlignment: TextAlignment { - TextAlignment(language: item.language) - } - - func shareActivity(additionalText: String?) -> PocketActivity? { - PocketItemActivity(recommendation: self, additionalText: additionalText) - } -} - -extension UnmanagedItem.Author: ReadableAuthor { - -} diff --git a/PocketKit/Sources/PocketKit/Home/HomeViewController.swift b/PocketKit/Sources/PocketKit/Home/HomeViewController.swift index 5060e7a10..32110023a 100644 --- a/PocketKit/Sources/PocketKit/Home/HomeViewController.swift +++ b/PocketKit/Sources/PocketKit/Home/HomeViewController.swift @@ -350,7 +350,9 @@ extension HomeViewController: UICollectionViewDelegate { let engagement = SnowplowEngagement(type: .general, value: nil) tracker.track(event: engagement, contexts(for: indexPath)) - model.selectedRecommendation = slates[indexPath.section - 1].recommendations[indexPath.item] + let recommendation = slates[indexPath.section - 1].recommendations[indexPath.item] + let viewModel = RecommendationViewModel(recommendation: recommendation) + model.selectedHomeReadableViewModel = viewModel let open = ContentOpenEvent(destination: .internal, trigger: .click) tracker.track(event: open, contexts(for: indexPath)) diff --git a/PocketKit/Sources/PocketKit/Home/SlateDetailViewController.swift b/PocketKit/Sources/PocketKit/Home/SlateDetailViewController.swift index d0ec65d64..fda2ac466 100644 --- a/PocketKit/Sources/PocketKit/Home/SlateDetailViewController.swift +++ b/PocketKit/Sources/PocketKit/Home/SlateDetailViewController.swift @@ -266,7 +266,8 @@ extension SlateDetailViewController: UICollectionViewDelegate { let engagement = SnowplowEngagement(type: .general, value: nil) tracker.track(event: engagement, contexts(for: indexPath)) - model.selectedRecommendation = recommendation + let viewModel = RecommendationViewModel(recommendation: recommendation) + model.selectedHomeReadableViewModel = viewModel let contentOpen = ContentOpenEvent(destination: .internal, trigger: .click) tracker.track(event: contentOpen, contexts(for: indexPath)) @@ -292,7 +293,7 @@ extension SlateDetailViewController { } let snowplowRecommendation = RecommendationContext(id: recommendationID, index: UIIndex(indexPath.item)) - guard let url = recommendation.readerURL else { + guard let url = url(for: recommendation) else { return [] } let content = ContentContext(url: url) @@ -301,6 +302,10 @@ extension SlateDetailViewController { return [context, content, snowplowSlate, snowplowRecommendation] } + + private func url(for recommendation: Slate.Recommendation) -> URL? { + recommendation.item.resolvedURL ?? recommendation.item.givenURL + } } private extension Style { diff --git a/PocketKit/Sources/PocketKit/Item/ItemViewController.swift b/PocketKit/Sources/PocketKit/Item/ItemViewController.swift deleted file mode 100644 index 6f0dcc9fe..000000000 --- a/PocketKit/Sources/PocketKit/Item/ItemViewController.swift +++ /dev/null @@ -1,251 +0,0 @@ -import UIKit -import Combine -import Analytics -import Sync - - -protocol ItemViewControllerDelegate: AnyObject { - func itemViewControllerDidDeleteItem(_ itemViewController: ItemViewController) - func itemViewControllerDidArchiveItem(_ itemViewController: ItemViewController) -} - -class ItemViewController: UIViewController { - private let itemHost: ArticleViewController - private let source: Source - private let tracker: Tracker - private let moreButtonItem: UIBarButtonItem - private var subscriptions: [AnyCancellable] = [] - private var observer: NSKeyValueObservation? - private let model: MainViewModel - - var savedItem: SavedItem? { - didSet { - itemHost.item = savedItem - observer = savedItem?.observe(\.isFavorite, options: [.initial]) { [weak self] _, _ in - self?.buildOverflowMenu() - } - } - } - - weak var delegate: ItemViewControllerDelegate? - - init( - model: MainViewModel, - tracker: Tracker, - source: Source - ) { - self.source = source - self.tracker = tracker - self.itemHost = ArticleViewController( - readerSettings: model.readerSettings, - tracker: tracker, - viewModel: model - ) - self.model = model - self.moreButtonItem = UIBarButtonItem( - image: UIImage(systemName: "ellipsis"), - menu: nil - ) - - super.init(nibName: nil, bundle: nil) - - title = nil - navigationItem.largeTitleDisplayMode = .never - - navigationItem.rightBarButtonItems = [ - moreButtonItem, - UIBarButtonItem( - image: UIImage(systemName: "safari"), - style: .plain, - target: self, - action: #selector(showWebView) - ) - ] - } - - override func loadView() { - view = UIView() - - itemHost.willMove(toParent: self) - addChild(itemHost) - view.addSubview(itemHost.view) - itemHost.didMove(toParent: self) - - itemHost.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - itemHost.view.topAnchor.constraint(equalTo: view.topAnchor), - itemHost.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - itemHost.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - itemHost.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - } - - func buildOverflowMenu() { - let deleteAction = UIAction( - title: "Delete", - image: UIImage(systemName: "trash"), - handler: { [weak self] _ in - self?.delete() - } - ) - deleteAction.accessibilityIdentifier = "item-action-menu-delete" - - let archiveAction = UIAction( - title: "Archive", - image: UIImage(systemName: "archivebox"), - handler: { [weak self] _ in - self?.archive() - } - ) - archiveAction.accessibilityIdentifier = "item-action-menu-archive" - - - moreButtonItem.menu = UIMenu( - image: nil, - identifier: nil, - options: [], - children: [ - UIAction( - title: "Display Settings", - image: UIImage(systemName: "textformat.size"), - handler: { [weak self] _ in - self?.showReaderSettings() - } - ), - { - if model.selectedItem?.isFavorite == true { - return UIAction( - title: "Unfavorite", - image: UIImage(systemName: "star.slash"), - handler: { [weak self] _ in - self?.unfavorite() - } - ) - } else { - return UIAction( - title: "Favorite", - image: UIImage(systemName: "star"), - handler: { [weak self] _ in - self?.favorite() - } - ) - } - }(), - archiveAction, - deleteAction, - UIAction( - title: "Share", - image: UIImage(systemName: "square.and.arrow.up"), - handler: { [weak self] _ in - self?.share() - } - ), - ] - ) - } - - required init?(coder: NSCoder) { - fatalError("\(Self.self) cannot be instantiated from a xib/storyboard") - } - - @objc - private func showWebView() { - model.presentedWebReaderURL = model.selectedItem?.url - } - - @objc - private func showReaderSettings() { - model.isPresentingReaderSettings = true - } - - var popoverAnchor: UIBarButtonItem? { - navigationItem.rightBarButtonItems?[0] - } -} - -// MARK: - Item Actions - -extension ItemViewController { - private func favorite() { - guard let item = model.selectedItem else { - return - } - - source.favorite(item: item) - track(identifier: .itemFavorite, item: item) - } - - private func unfavorite() { - guard let item = model.selectedItem else { - return - } - - source.unfavorite(item: item) - track(identifier: .itemUnfavorite, item: item) - } - - private func archive() { - guard let item = model.selectedItem else { - return - } - - source.archive(item: item) - delegate?.itemViewControllerDidArchiveItem(self) - track(identifier: .itemArchive, item: item) - } - - private func delete() { - guard let item = model.selectedItem else { - return - } - - let actions = [ - UIAlertAction(title: "No", style: .default) { [weak self] _ in - self?.model.presentedAlert = nil - }, - UIAlertAction(title: "Yes", style: .destructive) { [weak self] _ in - self?.model.presentedAlert = nil - - guard let self = self else { - return - } - - self.source.delete(item: item) - self.delegate?.itemViewControllerDidDeleteItem(self) - self.track(identifier: .itemDelete, item: item) - } - ] - - let alert = PocketAlert( - title: "Are you sure you want to delete this item?", - message: nil, - preferredStyle: .alert, - actions: actions, - preferredAction: nil - ) - model.presentedAlert = alert - } - - private func share() { - guard let item = model.selectedItem else { - return - } - - model.sharedActivity = PocketItemActivity(item: item, additionalText: nil) - track(identifier: .itemShare, item: item) - } - - private func track(identifier: UIContext.Identifier, item: SavedItem) { - guard let url = item.url else { - return - } - - let contexts: [Context] = [ - UIContext.button(identifier: identifier), - ContentContext(url: url) - ] - - let event = SnowplowEngagement(type: .general, value: nil) - tracker.track(event: event, contexts) - } -} diff --git a/PocketKit/Sources/PocketKit/Item/ReadableHostViewController.swift b/PocketKit/Sources/PocketKit/Item/ReadableHostViewController.swift new file mode 100644 index 000000000..6fe4961ce --- /dev/null +++ b/PocketKit/Sources/PocketKit/Item/ReadableHostViewController.swift @@ -0,0 +1,222 @@ +import UIKit +import Combine +import Analytics +import Sync +import Textile + + +protocol ReadableHostViewControllerDelegate: AnyObject { + func readableHostViewControllerDidDeleteItem() + func readableHostViewControllerDidArchiveItem() +} + +class ReadableHostViewController: UIViewController { + private let tracker: Tracker + private let moreButtonItem: UIBarButtonItem + private var subscriptions: [AnyCancellable] = [] + + private let mainViewModel: MainViewModel + private var readableViewModel: ReadableViewModel + + weak var delegate: ReadableHostViewControllerDelegate? + + init( + mainViewModel: MainViewModel, + readableViewModel: ReadableViewModel, + tracker: Tracker, + source: Source + ) { + self.mainViewModel = mainViewModel + self.readableViewModel = readableViewModel + self.tracker = tracker + self.moreButtonItem = UIBarButtonItem( + image: UIImage(systemName: "ellipsis"), + menu: nil + ) + + super.init(nibName: nil, bundle: nil) + + title = nil + navigationItem.largeTitleDisplayMode = .never + hidesBottomBarWhenPushed = true + + navigationItem.rightBarButtonItems = [ + moreButtonItem, + UIBarButtonItem( + image: UIImage(systemName: "safari"), + style: .plain, + target: self, + action: #selector(showWebView) + ) + ] + + readableViewModel.delegate = self + readableViewModel.actions.sink { [weak self] actions in + self?.buildOverflowMenu(from: actions) + }.store(in: &subscriptions) + } + + override func loadView() { + view = UIView() + + let readableViewController = ReadableViewController( + readerSettings: mainViewModel.readerSettings, + tracker: tracker, + viewModel: mainViewModel + ) + readableViewController.readableViewModel = readableViewModel + readableViewController.delegate = self + + readableViewController.willMove(toParent: self) + addChild(readableViewController) + view.addSubview(readableViewController.view) + readableViewController.didMove(toParent: self) + + readableViewController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + readableViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + readableViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + readableViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + readableViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + func buildOverflowMenu(from actions: [ReadableAction]) { + var menuActions: [UIAction] = [] + + menuActions.append( + UIAction( + title: "Display Settings", + image: UIImage(systemName: "textformat.size"), + handler: { [weak self] _ in + self?.showReaderSettings() + } + ) + ) + + actions.forEach { action in + let uiAction = UIAction(title: action.title,image: action.image) { _ in action.handler?() } + uiAction.accessibilityIdentifier = action.accessibilityIdentifier + menuActions.append(uiAction) + } + + menuActions.append( + UIAction( + title: "Share", + image: UIImage(systemName: "square.and.arrow.up"), + handler: { [weak self] _ in + self?.share() + } + ) + ) + + + moreButtonItem.menu = UIMenu( + image: nil, + identifier: nil, + options: [], + children: menuActions + ) + } + + required init?(coder: NSCoder) { + fatalError("\(Self.self) cannot be instantiated from a xib/storyboard") + } + + @objc + private func showWebView() { + mainViewModel.presentedWebReaderURL = readableViewModel.url + } + + @objc + private func showReaderSettings() { + mainViewModel.isPresentingReaderSettings = true + } + + var popoverAnchor: UIBarButtonItem? { + navigationItem.rightBarButtonItems?[0] + } +} + +// MARK: - Item Actions + +extension ReadableHostViewController: ReadableViewModelDelegate { + func readableViewModelDidFavorite(_ readableViewModel: ReadableViewModel) { + track(identifier: .itemFavorite, url: readableViewModel.url) + } + + func readableViewModelDidUnfavorite(_ readableViewModel: ReadableViewModel) { + track(identifier: .itemUnfavorite, url: readableViewModel.url) + } + + func readableViewModelDidArchive(_ readableViewModel: ReadableViewModel) { + delegate?.readableHostViewControllerDidArchiveItem() + track(identifier: .itemArchive, url: readableViewModel.url) + } + + func readableViewModelDidDelete(_ readableViewModel: ReadableViewModel) { + let actions = [ + UIAlertAction(title: "No", style: .default) { [weak self] _ in + self?.mainViewModel.presentedAlert = nil + }, + UIAlertAction(title: "Yes", style: .destructive) { [weak self] _ in + self?.mainViewModel.presentedAlert = nil + + guard let self = self else { + return + } + + self.readableViewModel.delete() + self.delegate?.readableHostViewControllerDidDeleteItem() + self.track(identifier: .itemDelete, url: self.readableViewModel.url) + } + ] + + let alert = PocketAlert( + title: "Are you sure you want to delete this item?", + message: nil, + preferredStyle: .alert, + actions: actions, + preferredAction: nil + ) + mainViewModel.presentedAlert = alert + } + + func readableViewModelDidSave(_ readableViewModel: ReadableViewModel) { + track(identifier: .itemSave, url: readableViewModel.url) + } + + private func share(additionalText: String? = nil) { + mainViewModel.sharedActivity = readableViewModel.shareActivity(additionalText: additionalText) + track(identifier: .itemShare, url: readableViewModel.url) + } + + private func track(identifier: UIContext.Identifier, url: URL?) { + guard let url = url else { + return + } + + let contexts: [Context] = [ + UIContext.button(identifier: identifier), + ContentContext(url: url) + ] + + let event = SnowplowEngagement(type: .general, value: nil) + tracker.track(event: event, contexts) + } +} + +extension ReadableHostViewController: ReadableViewControllerDelegate { + func readableViewController(_ controller: ReadableViewController, willOpenURL url: URL) { + let additionalContexts: [Context] = [ContentContext(url: url)] + + let contentOpen = ContentOpenEvent(destination: .external, trigger: .click) + let link = UIContext.articleView.link + let contexts = additionalContexts + [link] + tracker.track(event: contentOpen, contexts) + } + + func readableViewControlled(_ controller: ReadableViewController, shareWithAdditionalText text: String?) { + share(additionalText: text) + } +} diff --git a/PocketKit/Sources/PocketKit/Main/CompactMainCoordinator.swift b/PocketKit/Sources/PocketKit/Main/CompactMainCoordinator.swift index 83fc2b1e0..b04f2b082 100644 --- a/PocketKit/Sources/PocketKit/Main/CompactMainCoordinator.swift +++ b/PocketKit/Sources/PocketKit/Main/CompactMainCoordinator.swift @@ -44,7 +44,7 @@ class CompactMainCoordinator: NSObject { ) ), ItemsListViewController( - model: ArchivedItemsListViewModel(source: source) + model: ArchivedItemsListViewModel(source: source, mainViewModel: model) ) ] ) @@ -115,28 +115,17 @@ class CompactMainCoordinator: NSObject { } func show(item: SavedItem, animated: Bool) { - let itemVC = ItemViewController( - model: model, + let viewModel = SavedItemViewModel(item: item, source: source) + let readableHost = ReadableHostViewController( + mainViewModel: model, + readableViewModel: viewModel, tracker: tracker.childTracker(hosting: .articleView.screen), source: source ) - itemVC.savedItem = item - itemVC.delegate = self - itemVC.hidesBottomBarWhenPushed = true - - myList.pushViewController(itemVC, animated: animated) - } - - func show(recommendation: Slate.Recommendation, animated: Bool) { - let article = ArticleViewController( - readerSettings: model.readerSettings, - tracker: tracker.childTracker(hosting: .articleView.screen), - viewModel: model - ) - article.hidesBottomBarWhenPushed = true - article.item = recommendation + readableHost.delegate = self + readableHost.hidesBottomBarWhenPushed = true - home.pushViewController(article, animated: animated) + myList.pushViewController(readableHost, animated: animated) } func showSlate(withID slateID: String, animated: Bool) { @@ -149,6 +138,30 @@ class CompactMainCoordinator: NSObject { home.pushViewController(slateDetail, animated: animated) } + + func showHome(viewModel: ReadableViewModel, animated: Bool) { + let viewController = ReadableHostViewController( + mainViewModel: model, + readableViewModel: viewModel, + tracker: tracker.childTracker(hosting: .articleView.screen), + source: source + ) + + home.pushViewController(viewController, animated: animated) + } + + func showMyList(viewModel: ReadableViewModel, animated: Bool) { + let viewController = ReadableHostViewController( + mainViewModel: model, + readableViewModel: viewModel, + tracker: tracker.childTracker(hosting: .articleView.screen), + source: source + ) + viewController.delegate = self + viewController.hidesBottomBarWhenPushed = true + + myList.pushViewController(viewController, animated: animated) + } func subscribeToModelChanges() { var isResetting = true @@ -166,28 +179,28 @@ class CompactMainCoordinator: NSObject { } }.store(in: &subscriptions) - model.$selectedItem.receive(on: DispatchQueue.main).sink { [weak self] item in - guard let item = item else { + model.$selectedMyListReadableViewModel.receive(on: DispatchQueue.main).sink { [weak self] viewModel in + guard let viewModel = viewModel else { return } - - self?.show(item: item, animated: !isResetting) + + self?.showMyList(viewModel: viewModel, animated: !isResetting) }.store(in: &subscriptions) - - model.$selectedSlateID.receive(on: DispatchQueue.main).sink { [weak self] slateID in - guard let slateID = slateID else { + + model.$selectedHomeReadableViewModel.receive(on: DispatchQueue.main).sink { [weak self] viewModel in + guard let viewModel = viewModel else { return } - - self?.showSlate(withID: slateID, animated: !isResetting) + + self?.showHome(viewModel: viewModel, animated: !isResetting) }.store(in: &subscriptions) - model.$selectedRecommendation.receive(on: DispatchQueue.main).sink { [weak self] recommendation in - guard let recommendation = recommendation else { + model.$selectedSlateID.receive(on: DispatchQueue.main).sink { [weak self] slateID in + guard let slateID = slateID else { return } - self?.show(recommendation: recommendation, animated: !isResetting) + self?.showSlate(withID: slateID, animated: !isResetting) }.store(in: &subscriptions) model.refreshTasks.receive(on: DispatchQueue.main).sink { [weak self] task in @@ -200,17 +213,17 @@ class CompactMainCoordinator: NSObject { } } -extension CompactMainCoordinator: ItemViewControllerDelegate { - func itemViewControllerDidDeleteItem(_ itemViewController: ItemViewController) { +extension CompactMainCoordinator: ReadableHostViewControllerDelegate { + func readableHostViewControllerDidDeleteItem() { popReader() } - func itemViewControllerDidArchiveItem(_ itemViewController: ItemViewController) { + func readableHostViewControllerDidArchiveItem() { popReader() } private func popReader() { - model.selectedItem = nil + model.selectedMyListReadableViewModel = nil myList.popViewController(animated: true) } } @@ -220,18 +233,18 @@ extension CompactMainCoordinator: UINavigationControllerDelegate { switch navigationController { case myList: if viewController === myList.viewControllers.first { - model.selectedItem = nil + model.selectedMyListReadableViewModel = nil return } case home: if viewController === home.viewControllers.first { model.selectedSlateID = nil - model.selectedRecommendation = nil + model.selectedHomeReadableViewModel = nil return } if viewController is SlateDetailViewController { - model.selectedRecommendation = nil + model.selectedHomeReadableViewModel = nil return } default: diff --git a/PocketKit/Sources/PocketKit/Main/MainViewModel.swift b/PocketKit/Sources/PocketKit/Main/MainViewModel.swift index c338b9251..9f1529d9e 100644 --- a/PocketKit/Sources/PocketKit/Main/MainViewModel.swift +++ b/PocketKit/Sources/PocketKit/Main/MainViewModel.swift @@ -7,15 +7,15 @@ import BackgroundTasks class MainViewModel: ObservableObject { @Published var selectedSection: AppSection = .home - + @Published - var selectedItem: SavedItem? - + var selectedRecommendationToReport: Slate.Recommendation? + @Published - var selectedRecommendation: Slate.Recommendation? + var selectedMyListReadableViewModel: ReadableViewModel? @Published - var selectedRecommendationToReport: Slate.Recommendation? + var selectedHomeReadableViewModel: ReadableViewModel? @Published var selectedSlateID: String? diff --git a/PocketKit/Sources/PocketKit/Main/RegularMainCoordinator.swift b/PocketKit/Sources/PocketKit/Main/RegularMainCoordinator.swift index 482715108..87ae6f65c 100644 --- a/PocketKit/Sources/PocketKit/Main/RegularMainCoordinator.swift +++ b/PocketKit/Sources/PocketKit/Main/RegularMainCoordinator.swift @@ -18,8 +18,7 @@ class RegularMainCoordinator: NSObject { private let settings: SettingsViewController private let readerRoot: UINavigationController - private let itemVC: ItemViewController - private let recommendationVC: ArticleViewController +// private let readableViewController: ReadableViewController private let tracker: Tracker private let source: Source @@ -53,7 +52,7 @@ class RegularMainCoordinator: NSObject { ) ), ItemsListViewController( - model: ArchivedItemsListViewModel(source: source) + model: ArchivedItemsListViewModel(source: source, mainViewModel: model) ) ] ) @@ -68,20 +67,20 @@ class RegularMainCoordinator: NSObject { settings = SettingsViewController(model: model.settings) settings.view.backgroundColor = UIColor(.ui.white1) - itemVC = ItemViewController( - model: model, - tracker: tracker.childTracker(hosting: .articleView.screen), - source: source - ) - itemVC.view.backgroundColor = UIColor(.ui.white1) - - recommendationVC = ArticleViewController( - readerSettings: model.readerSettings, - tracker: tracker.childTracker(hosting: .articleView.screen), - viewModel: model - ) - - readerRoot = UINavigationController(rootViewController: itemVC) +// readableHost = ReadableHostViewController( +// model: model, +// tracker: tracker.childTracker(hosting: .articleView.screen), +// source: source +// ) +// readableHost.view.backgroundColor = UIColor(.ui.white1) +// +// readableViewController = ReadableViewController( +// readerSettings: model.readerSettings, +// tracker: tracker.childTracker(hosting: .articleView.screen), +// viewModel: model +// ) +// + readerRoot = UINavigationController() super.init() @@ -103,11 +102,11 @@ class RegularMainCoordinator: NSObject { home.navigationController?.navigationBar.barTintColor = UIColor(.ui.white1) home.navigationController?.navigationBar.tintColor = UIColor(.ui.grey1) - itemVC.navigationController?.navigationBar.prefersLargeTitles = true - itemVC.navigationController?.navigationBar.barTintColor = UIColor(.ui.white1) - itemVC.navigationController?.navigationBar.tintColor = UIColor(.ui.grey1) +// readableHost.navigationController?.navigationBar.prefersLargeTitles = true +// readableHost.navigationController?.navigationBar.barTintColor = UIColor(.ui.white1) +// readableHost.navigationController?.navigationBar.tintColor = UIColor(.ui.grey1) - itemVC.delegate = self +// readableHost.delegate = self splitController.delegate = self home.navigationController?.delegate = self @@ -169,17 +168,27 @@ class RegularMainCoordinator: NSObject { func showSettings() { splitController.setViewController(settings, for: .supplementary) } - - func show(item: SavedItem) { - itemVC.savedItem = item - readerRoot.viewControllers = [itemVC] + + func showMyList(viewModel: ReadableViewModel) { + let viewController = ReadableHostViewController( + mainViewModel: model, + readableViewModel: viewModel, + tracker: tracker.childTracker(hosting: .articleView.screen), + source: source + ) + readerRoot.viewControllers = [viewController] splitController.show(.secondary) } - - func show(recommendation: Slate.Recommendation) { - recommendationVC.item = recommendation - readerRoot.viewControllers = [recommendationVC] + + func showHome(viewModel: ReadableViewModel) { + let viewController = ReadableHostViewController( + mainViewModel: model, + readableViewModel: viewModel, + tracker: tracker.childTracker(hosting: .articleView.screen), + source: source + ) + readerRoot.viewControllers = [viewController] splitController.show(.secondary) } @@ -214,6 +223,19 @@ class RegularMainCoordinator: NSObject { splitController.show(.supplementary) } + + func show(archivedItem: ArchivedItem) { + let viewModel = ArchivedItemViewModel(item: archivedItem) + let viewController = ReadableHostViewController( + mainViewModel: model, + readableViewModel: viewModel, + tracker: tracker.childTracker(hosting: .articleView.screen), + source: source + ) + readerRoot.viewControllers = [viewController] + + splitController.show(.secondary) + } func subscribeToModelChanges() { var isResetting = true @@ -229,29 +251,29 @@ class RegularMainCoordinator: NSObject { self.showSettings() } }.store(in: &subscriptions) - - model.$selectedItem.receive(on: DispatchQueue.main).sink { [weak self] item in - guard let item = item else { + + model.$selectedMyListReadableViewModel.receive(on: DispatchQueue.main).sink { [weak self] viewModel in + guard let viewModel = viewModel else { return } - - self?.show(item: item) + + self?.showMyList(viewModel: viewModel) }.store(in: &subscriptions) - - model.$selectedSlateID.receive(on: DispatchQueue.main).sink { [weak self] slateID in - guard let slateID = slateID else { + + model.$selectedHomeReadableViewModel.receive(on: DispatchQueue.main).sink { [weak self] viewModel in + guard let viewModel = viewModel else { return } - - self?.showSlate(withID: slateID, animated: !isResetting) + + self?.showHome(viewModel: viewModel) }.store(in: &subscriptions) - model.$selectedRecommendation.receive(on: DispatchQueue.main).sink { [weak self] recommendation in - guard let recommendation = recommendation else { + model.$selectedSlateID.receive(on: DispatchQueue.main).sink { [weak self] slateID in + guard let slateID = slateID else { return } - self?.show(recommendation: recommendation) + self?.showSlate(withID: slateID, animated: !isResetting) }.store(in: &subscriptions) model.refreshTasks.receive(on: DispatchQueue.main).sink { [weak self] task in @@ -314,7 +336,7 @@ class RegularMainCoordinator: NSObject { modal.modalPresentationStyle = .popover } - modal.popoverPresentationController?.barButtonItem = itemVC.popoverAnchor +// modal.popoverPresentationController?.barButtonItem = readableHost.popoverAnchor splitController.present(modal, animated: true) } @@ -332,17 +354,17 @@ class RegularMainCoordinator: NSObject { } } -extension RegularMainCoordinator: ItemViewControllerDelegate { - func itemViewControllerDidDeleteItem(_ itemViewController: ItemViewController) { +extension RegularMainCoordinator: ReadableHostViewControllerDelegate { + func readableHostViewControllerDidDeleteItem() { popReader() } - func itemViewControllerDidArchiveItem(_ itemViewController: ItemViewController) { + func readableHostViewControllerDidArchiveItem() { popReader() } private func popReader() { - model.selectedItem = nil + model.selectedMyListReadableViewModel = nil } } @@ -350,7 +372,7 @@ extension RegularMainCoordinator: UISplitViewControllerDelegate { func splitViewControllerDidExpand(_ svc: UISplitViewController) { model.isCollapsed = false - if model.selectedItem == nil { + if model.selectedMyListReadableViewModel == nil { splitController.show(.supplementary) } } diff --git a/PocketKit/Sources/PocketKit/MyList/ArchivedItemsList/ArchivedItemsListViewModel.swift b/PocketKit/Sources/PocketKit/MyList/ArchivedItemsList/ArchivedItemsListViewModel.swift index 3ef86e82c..e4758d632 100644 --- a/PocketKit/Sources/PocketKit/MyList/ArchivedItemsList/ArchivedItemsListViewModel.swift +++ b/PocketKit/Sources/PocketKit/MyList/ArchivedItemsList/ArchivedItemsListViewModel.swift @@ -12,14 +12,16 @@ class ArchivedItemsListViewModel: ItemsListViewModel { let selectionItem: SelectionItem = SelectionItem(title: "Archive", image: .init(asset: .archive)) private let source: Source + private let mainViewModel: MainViewModel private var archivedItems: [String: ArchivedItem] = [:] @Published private var selectedFilters: Set = .init() private let availableFilters: [ItemsListFilter] = ItemsListFilter.allCases - init(source: Source) { + init(source: Source, mainViewModel: MainViewModel) { self.source = source + self.mainViewModel = mainViewModel self.events = .init() } @@ -62,8 +64,14 @@ class ArchivedItemsListViewModel: ItemsListViewModel { // TODO: Support pull to refresh } - func selectCell(with: ItemsListCell) { - // TODO: show the item in reader + func selectCell(with cell: ItemsListCell) { + guard case .item(let archivedItemID) = cell, + let archivedItem = archivedItems[archivedItemID] else { + return + } + + let viewModel = ArchivedItemViewModel(item: archivedItem) + mainViewModel.selectedMyListReadableViewModel = viewModel } func shareItem(with: ItemsListCell) { diff --git a/PocketKit/Sources/PocketKit/MyList/SavedItemsList/SavedItem+ItemsListItem.swift b/PocketKit/Sources/PocketKit/MyList/SavedItemsList/SavedItem+ItemsListItem.swift index 5dc3ef1de..a184f964e 100644 --- a/PocketKit/Sources/PocketKit/MyList/SavedItemsList/SavedItem+ItemsListItem.swift +++ b/PocketKit/Sources/PocketKit/MyList/SavedItemsList/SavedItem+ItemsListItem.swift @@ -3,6 +3,14 @@ import Foundation extension SavedItem: ItemsListItem { + var domain: String? { + item?.domain + } + + var title: String? { + item?.title + } + var topImageURL: URL? { item?.topImageURL } diff --git a/PocketKit/Sources/PocketKit/MyList/SavedItemsList/SavedItemsListViewModel.swift b/PocketKit/Sources/PocketKit/MyList/SavedItemsList/SavedItemsListViewModel.swift index 0e37dbfd2..0062bbb8b 100644 --- a/PocketKit/Sources/PocketKit/MyList/SavedItemsList/SavedItemsListViewModel.swift +++ b/PocketKit/Sources/PocketKit/MyList/SavedItemsList/SavedItemsListViewModel.swift @@ -42,7 +42,7 @@ class SavedItemsListViewModel: NSObject, ItemsListViewModel { itemsController.delegate = self - self.main.$selectedItem.sink { _ in + self.main.$selectedMyListReadableViewModel.sink { _ in // TODO: Handle deselection here }.store(in: &subscriptions) } @@ -92,7 +92,11 @@ class SavedItemsListViewModel: NSObject, ItemsListViewModel { func selectCell(with cellID: ItemsListCell) { switch cellID { case .item(let objectID): - main.selectedItem = bareItem(with: objectID) + guard let item = bareItem(with: objectID) else { + return + } + let viewModel = SavedItemViewModel(item: item, source: source) + main.selectedMyListReadableViewModel = viewModel case .filterButton(let filterID): if selectedFilters.contains(filterID) { selectedFilters.remove(filterID) @@ -111,7 +115,7 @@ class SavedItemsListViewModel: NSObject, ItemsListViewModel { return } - main.sharedActivity = bareItem(with: objectID).flatMap { PocketItemActivity(item: $0) } + main.sharedActivity = bareItem(with: objectID).flatMap { PocketItemActivity(url: $0.url) } } private func bareItem(with id: NSManagedObjectID) -> SavedItem? { diff --git a/PocketKit/Sources/PocketKit/Report/ReportRecommendationView.swift b/PocketKit/Sources/PocketKit/Report/ReportRecommendationView.swift index bc6081062..bb8aa51ec 100644 --- a/PocketKit/Sources/PocketKit/Report/ReportRecommendationView.swift +++ b/PocketKit/Sources/PocketKit/Report/ReportRecommendationView.swift @@ -99,7 +99,7 @@ struct ReportRecommendationView: View { } private func report(_ reason: ReportEvent.Reason) { - guard let url = recommendation.readerURL else { + guard let url = url(for: recommendation) else { return } @@ -117,6 +117,10 @@ struct ReportRecommendationView: View { } } + private func url(for recommendation: Slate.Recommendation) -> URL? { + recommendation.item.resolvedURL ?? recommendation.item.givenURL + } + private func selectionColor(for reason: ReportEvent.Reason) -> Color { return reason == selectedReason ? Constants.reasonRowSelectedColor : Constants.reasonRowDeselectedColor } diff --git a/Tests iOS/MyList/ArchiveTests.swift b/Tests iOS/MyList/ArchiveTests.swift index 0e4802722..33228fb4d 100644 --- a/Tests iOS/MyList/ArchiveTests.swift +++ b/Tests iOS/MyList/ArchiveTests.swift @@ -52,6 +52,25 @@ class ArchiveTests: XCTestCase { XCTAssertFalse(myList.itemView(matching: "Item 1").exists) XCTAssertFalse(myList.itemView(matching: "Item 2").exists) } + + func test_tappingItem_displaysNativeReaderView() { + app.launch().tabBar.myListButton.wait().tap() + app.myListView.selectionSwitcher.archiveButton.wait().tap() + app.myListView.itemView(at: 0).wait().tap() + + let expectedContent = [ + "Archived Item 1", + "Socrates", + "January 1, 2001", + ] + + for expectedString in expectedContent { + app + .readerView + .cell(containing: expectedString) + .wait() + } + } } private func requestIsForArchivedContent(_ request: Request) -> Bool {