Skip to content

Commit

Permalink
Add support for different open items per session
Browse files Browse the repository at this point in the history
  • Loading branch information
mvasilak committed Jan 3, 2024
1 parent d10fbae commit b220861
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 81 deletions.
104 changes: 60 additions & 44 deletions Zotero/Controllers/OpenItemsController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,40 +96,44 @@ final class OpenItemsController {
private unowned let dbStorage: DbStorage
private unowned let fileStorage: FileStorage
// TODO: Use a better data structure, such as an ordered set
private(set) var items: [Item] = []
private var itemsToken: NotificationToken?
public var itemsSortedByUserOrder: [Item] {
items.sorted(by: { $0.userIndex < $1.userIndex })
}
public var itemsSortedByLastOpen: [Item] {
items.sorted(by: { $0.lastOpened > $1.lastOpened })
}
let observable: PublishSubject<[Item]>
private var itemsBySessionIdentifier: [String: [Item]] = [:]
private var itemsTokenBySessionIdentifier: [String: NotificationToken] = [:]
private var observableBySessionIdentifier: [String: PublishSubject<[Item]>] = [:]
private let disposeBag: DisposeBag

// MARK: Object Lifecycle
init(dbStorage: DbStorage, fileStorage: FileStorage) {
self.dbStorage = dbStorage
self.fileStorage = fileStorage
observable = PublishSubject()
disposeBag = DisposeBag()
}

// MARK: Actions
func setItems(_ items: [Item], validate: Bool) {
DDLogInfo("OpenItemsController: setting items \(items)")
var finalItems = items
if validate {
finalItems = filterValidItems(items)
func observable(for sessionIdentifier: String) -> PublishSubject<[Item]> {
if let observable = observableBySessionIdentifier[sessionIdentifier] {
return observable
}
guard finalItems != self.items else { return }
let observable = PublishSubject<[Item]>()
observableBySessionIdentifier[sessionIdentifier] = observable
return observable
}

func getItems(for sessionIdentifier: String) -> [Item] {
itemsBySessionIdentifier[sessionIdentifier, default: []]
}

func setItems(_ items: [Item], for sessionIdentifier: String, validate: Bool) {
DDLogInfo("OpenItemsController: setting items \(items) for \(sessionIdentifier)")
let existingItems = getItems(for: sessionIdentifier)
let newItems = validate ? filterValidItems(items) : items
guard newItems != existingItems else { return }
// Invalidate previous observer first.
itemsToken?.invalidate()
itemsToken = nil
self.items = finalItems
itemsTokenBySessionIdentifier[sessionIdentifier]?.invalidate()
itemsTokenBySessionIdentifier[sessionIdentifier] = nil
itemsBySessionIdentifier[sessionIdentifier] = newItems
// Register observer for newly set items.
itemsToken = registerObserver(for: finalItems)
observable.on(.next(finalItems))
itemsTokenBySessionIdentifier[sessionIdentifier] = registerObserver(for: newItems)
observable(for: sessionIdentifier).on(.next(newItems))

func registerObserver(for items: [Item]) -> NotificationToken? {
var token: NotificationToken?
Expand All @@ -152,7 +156,8 @@ final class OpenItemsController {
case .update(_, let deletions, _, _):
if !deletions.isEmpty, let self {
// Observed items have been deleted, call setItems to validate and register new observer.
self.setItems(self.items, validate: true)
let existingItems = getItems(for: sessionIdentifier)
setItems(existingItems, for: sessionIdentifier, validate: true)
}

case .error(let error):
Expand All @@ -161,24 +166,26 @@ final class OpenItemsController {
}
}
} catch let error {
DDLogError("OpenItemsController: can't setup items observer - \(error)")
DDLogError("OpenItemsController: can't register items observer - \(error)")
}
return token
}
}

func open(_ kind: Item.Kind) {
DDLogInfo("OpenItemsController: opened item \(kind)")
if let index = items.firstIndex(where: { $0.kind == kind }) {
items[index].lastOpened = .now
DDLogInfo("OpenItemsController: already opened item \(kind) became most recent")
observable.on(.next(items))
func open(_ kind: Item.Kind, for sessionIdentifier: String) {
DDLogInfo("OpenItemsController: opened item \(kind) for \(sessionIdentifier)")
var existingItems = getItems(for: sessionIdentifier)
if let index = existingItems.firstIndex(where: { $0.kind == kind }) {
existingItems[index].lastOpened = .now
itemsBySessionIdentifier[sessionIdentifier] = existingItems
DDLogInfo("OpenItemsController: already opened item \(kind) became most recent for \(sessionIdentifier)")
observable(for: sessionIdentifier).on(.next(existingItems))
} else {
DDLogInfo("OpenItemsController: newly opened item \(kind) set as most recent")
let item = Item(kind: kind, userIndex: items.count)
let newItems = items + [item]
DDLogInfo("OpenItemsController: newly opened item \(kind) set as most recent for \(sessionIdentifier)")
let item = Item(kind: kind, userIndex: existingItems.count)
let newItems = existingItems + [item]
// setItems will produce next observable event
setItems(newItems, validate: false)
setItems(newItems, for: sessionIdentifier, validate: false)
}
}

Expand All @@ -190,43 +197,44 @@ final class OpenItemsController {
}

@discardableResult
func restoreMostRecentlyOpenedItem(using presenter: OpenItemsPresenter) -> Item? {
func restoreMostRecentlyOpenedItem(using presenter: OpenItemsPresenter, sessionIdentifier: String) -> Item? {
// Will restore most recent opened item still present, or none if all fail
DDLogInfo("OpenItemsController: restoring most recently opened item using presenter \(presenter)")
var existingItems = getItems(for: sessionIdentifier)
DDLogInfo("OpenItemsController: restoring most recently opened item using presenter \(presenter) for \(sessionIdentifier)")
var itemsChanged: Bool = false
defer {
if itemsChanged {
observable.on(.next(items))
observable(for: sessionIdentifier).on(.next(existingItems))
}
}
var item: Item?
var presentation: Presentation?
let itemsSortedByLastOpen = itemsSortedByLastOpen
for _item in itemsSortedByLastOpen {
let existingItemsSortedByLastOpen = itemsSortedByLastOpen(for: sessionIdentifier)
for _item in existingItemsSortedByLastOpen {
if let _presentation = loadPresentation(for: _item) {
item = _item
presentation = _presentation
break
}
DDLogWarn("OpenItemsController: removing not loaded item \(_item)")
items.removeAll(where: { $0 == _item })
DDLogWarn("OpenItemsController: removing not loaded item \(_item) for \(sessionIdentifier)")
existingItems.removeAll(where: { $0 == _item })
itemsChanged = true
}
guard let item, let presentation else { return nil }
presentItem(with: presentation, using: presenter)
return item
}

func deferredOpenItemsMenuElement(disableOpenItem: Bool, itemActionCallback: @escaping (Item, UIAction) -> Void) -> UIDeferredMenuElement {
func deferredOpenItemsMenuElement(for sessionIdentifier: String, disableOpenItem: Bool, itemActionCallback: @escaping (Item, UIAction) -> Void) -> UIDeferredMenuElement {
UIDeferredMenuElement { [weak self] elementProvider in
guard let self else {
elementProvider([])
return
}
var actions: [UIAction] = []
let openItem: Item? = disableOpenItem ? itemsSortedByLastOpen.first : nil
let itemsSortedByUserOrder = itemsSortedByUserOrder
var itemTuples: [(Item, RItem)] = filterValidItemsWithRItem(itemsSortedByUserOrder)
let openItem: Item? = disableOpenItem ? itemsSortedByLastOpen(for: sessionIdentifier).first : nil
let existingItemsSortedByLastOpen = itemsSortedByUserOrder(for: sessionIdentifier)
var itemTuples: [(Item, RItem)] = filterValidItemsWithRItem(existingItemsSortedByLastOpen)
for (item, rItem) in itemTuples {
var attributes: UIMenuElement.Attributes = []
var state: UIMenuElement.State = .off
Expand All @@ -244,6 +252,14 @@ final class OpenItemsController {
}

// MARK: Helper Methods
private func itemsSortedByUserOrder(for sessionIdentifier: String) -> [Item] {
getItems(for: sessionIdentifier).sorted(by: { $0.userIndex < $1.userIndex })
}

private func itemsSortedByLastOpen(for sessionIdentifier: String) -> [Item] {
getItems(for: sessionIdentifier).sorted(by: { $0.lastOpened > $1.lastOpened })
}

private func filterValidItemsWithRItem(_ items: [Item]) -> [(Item, RItem)] {
var itemTuples: [(Item, RItem)] = []
do {
Expand Down
14 changes: 7 additions & 7 deletions Zotero/Scenes/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {
conflictAlertQueueController = nil
controllers.userControllers?.syncScheduler.syncController.set(coordinator: nil)
} else {
let controller = MainViewController(controllers: controllers)
(urlContext, data) = preprocess(connectionOptions: options, session: session)
let controller = MainViewController(sessionIdentifier: session.persistentIdentifier, controllers: controllers)
viewController = controller

conflictReceiverAlertController = ConflictReceiverAlertController(viewController: controller)
Expand All @@ -137,7 +137,7 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {

DDLogInfo("AppCoordinator: show main screen logged \(isLoggedIn ? "in" : "out"); animated=\(animated)")
show(viewController: viewController, in: window, animated: animated) {
process(urlContext: urlContext, data: data)
process(urlContext: urlContext, data: data, sessionIdentifier: session.persistentIdentifier)
}

func show(viewController: UIViewController?, in window: UIWindow, animated: Bool = false, completion: @escaping () -> Void) {
Expand All @@ -160,12 +160,12 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {
DDLogInfo("AppCoordinator: Preprocessing restored state - \(data)")
Defaults.shared.selectedLibrary = data.libraryId
Defaults.shared.selectedCollectionId = data.collectionId
controllers.userControllers?.openItemsController.setItems(data.openItems, validate: true)
controllers.userControllers?.openItemsController.setItems(data.openItems, for: session.persistentIdentifier, validate: true)
}
return (urlContext, data)
}

func process(urlContext: UIOpenURLContext?, data: RestoredStateData?) {
func process(urlContext: UIOpenURLContext?, data: RestoredStateData?, sessionIdentifier: String) {
if let urlContext, let urlController = controllers.userControllers?.customUrlController {
// If scene was started from custom URL
let sourceApp = urlContext.options.sourceApplication ?? "unknown"
Expand All @@ -180,10 +180,10 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {
if let data, data.restoreMostRecentlyOpenedItem {
DDLogInfo("AppCoordinator: Processing restored state - \(data)")
// If scene had state stored, restore state
showRestoredState(for: data)
showRestoredState(for: data, sessionIdentifier: sessionIdentifier)
}

func showRestoredState(for data: RestoredStateData) {
func showRestoredState(for data: RestoredStateData, sessionIdentifier: String) {
guard let openItemsController = controllers.userControllers?.openItemsController else { return }
DDLogInfo("AppCoordinator: show restored state")
guard let mainController = window.rootViewController as? MainViewController else {
Expand All @@ -205,7 +205,7 @@ extension AppCoordinator: AppDelegateCoordinatorDelegate {
collection = Collection(custom: .all)
}
mainController.showItems(for: collection, in: library)
openItemsController.restoreMostRecentlyOpenedItem(using: self)
openItemsController.restoreMostRecentlyOpenedItem(using: self, sessionIdentifier: sessionIdentifier)

func loadRestoredStateData(libraryId: LibraryIdentifier, collectionId: CollectionIdentifier) -> (Library, Collection?)? {
guard let dbStorage = controllers.userControllers?.dbStorage else { return nil }
Expand Down
23 changes: 18 additions & 5 deletions Zotero/Scenes/Detail/DetailCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,27 @@ final class DetailCoordinator: Coordinator {
let collection: Collection
let library: Library
let searchItemKeys: [String]?
let sessionIdentifier: String
private unowned let controllers: Controllers
private let disposeBag: DisposeBag

private weak var citationNavigationController: UINavigationController?

init(library: Library, collection: Collection, searchItemKeys: [String]?, navigationController: UINavigationController, itemsTagFilterDelegate: ItemsTagFilterDelegate?, controllers: Controllers) {
init(
library: Library,
collection: Collection,
searchItemKeys: [String]?,
navigationController: UINavigationController,
itemsTagFilterDelegate: ItemsTagFilterDelegate?,
sessionIdentifier: String,
controllers: Controllers
) {
self.library = library
self.collection = collection
self.searchItemKeys = searchItemKeys
self.navigationController = navigationController
self.itemsTagFilterDelegate = itemsTagFilterDelegate
self.sessionIdentifier = sessionIdentifier
self.controllers = controllers
self.childCoordinators = []
self.disposeBag = DisposeBag()
Expand Down Expand Up @@ -155,7 +165,7 @@ final class DetailCoordinator: Coordinator {
remoteDownloadBatchData: remoteDownloadBatchData,
identifierLookupBatchData: identifierLookupBatchData,
error: nil,
openItemsCount: openItemsController.items.count
openItemsCount: openItemsController.getItems(for: sessionIdentifier).count
)
let handler = ItemsActionHandler(
dbStorage: dbStorage,
Expand Down Expand Up @@ -300,7 +310,7 @@ final class DetailCoordinator: Coordinator {

private func showPDF(at url: URL, key: String, library: Library) {
guard let navigationController else { return }
controllers.userControllers?.openItemsController.open(.pdf(libraryId: library.identifier, key: key))
controllers.userControllers?.openItemsController.open(.pdf(libraryId: library.identifier, key: key), for: sessionIdentifier)

let viewControllerProvider: () -> UIViewController = {
self.createPDFController(key: key, library: library, url: url)
Expand Down Expand Up @@ -471,6 +481,7 @@ extension DetailCoordinator: DetailItemsCoordinatorDelegate {
title: title,
saveCallback: saveCallback,
navigationController: navigationController,
sessionIdentifier: sessionIdentifier,
controllers: controllers
)
coordinator.parentCoordinator = self
Expand Down Expand Up @@ -921,7 +932,9 @@ extension DetailCoordinator: DetailNoteEditorCoordinatorDelegate {
switch result {
case .success(let note):
// If indeed a new note is created inform open items controller about it.
self?.controllers.userControllers?.openItemsController.open(.note(libraryId: library.identifier, key: note.key))
if let self {
controllers.userControllers?.openItemsController.open(.note(libraryId: library.identifier, key: note.key), for: sessionIdentifier)
}

case .failure:
break
Expand All @@ -932,7 +945,7 @@ extension DetailCoordinator: DetailNoteEditorCoordinatorDelegate {

case .edit(let key), .readOnly(let key):
DDLogInfo("DetailCoordinator: show note \(key)")
controllers.userControllers?.openItemsController.open(.note(libraryId: library.identifier, key: key))
controllers.userControllers?.openItemsController.open(.note(libraryId: library.identifier, key: key), for: sessionIdentifier)
}

let viewControllerProvider: () -> UIViewController = {
Expand Down
8 changes: 4 additions & 4 deletions Zotero/Scenes/Detail/Items/Views/ItemsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -653,8 +653,8 @@ final class ItemsViewController: UIViewController {
image = UIImage(systemName: "0.square")
accessibilityLabel = L10n.Items.restoreOpen
action = { [weak self] _ in
guard let self, let presenter, let controller = controllers.userControllers?.openItemsController else { return }
controller.restoreMostRecentlyOpenedItem(using: presenter)
guard let self, let presenter, let controller = controllers.userControllers?.openItemsController, let sessionIdentifier = view.scene?.session.persistentIdentifier else { return }
controller.restoreMostRecentlyOpenedItem(using: presenter, sessionIdentifier: sessionIdentifier)
}
}

Expand Down Expand Up @@ -718,8 +718,8 @@ final class ItemsViewController: UIViewController {
}

private func setupOpenItemsObserving() {
guard let controller = controllers.userControllers?.openItemsController else { return }
controller.observable
guard let controller = controllers.userControllers?.openItemsController, let sessionIdentifier = view.scene?.session.persistentIdentifier else { return }
controller.observable(for: sessionIdentifier)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] items in
self?.viewModel.process(action: .updateOpenItems(items: items))
Expand Down
Loading

0 comments on commit b220861

Please sign in to comment.