Skip to content

Commit

Permalink
Save user activity for open note items (#993)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvasilak authored Aug 14, 2024
1 parent 3522d09 commit 9569c02
Show file tree
Hide file tree
Showing 18 changed files with 380 additions and 152 deletions.
4 changes: 4 additions & 0 deletions Zotero.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
61ABA7512A6137D1002A4219 /* ShareableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61ABA7502A6137D1002A4219 /* ShareableImage.swift */; };
61BD13952A5831EF008A0704 /* TextKit1TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BD13942A5831EF008A0704 /* TextKit1TextView.swift */; };
61C817F22A49B5D30085B1E6 /* CollectionResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B1EDEE250242E700D8BC1E /* CollectionResponseSpec.swift */; };
61E24DCC2ABB385E00D75F50 /* OpenItemsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E24DCB2ABB385E00D75F50 /* OpenItemsController.swift */; };
61FA14CE2B05081D00E7D423 /* TextConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FA14CD2B05081D00E7D423 /* TextConverter.swift */; };
61FA14D02B08E24A00E7D423 /* ColorPickerStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FA14CF2B08E24A00E7D423 /* ColorPickerStackView.swift */; };
B300B33324291C8D00C1FE1E /* RTranslatorMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B300B33224291C8D00C1FE1E /* RTranslatorMetadata.swift */; };
Expand Down Expand Up @@ -1285,6 +1286,7 @@
61A0C8462B8F669B0048FF92 /* PSPDFKitUI+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PSPDFKitUI+Extensions.swift"; sourceTree = "<group>"; };
61ABA7502A6137D1002A4219 /* ShareableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareableImage.swift; sourceTree = "<group>"; };
61BD13942A5831EF008A0704 /* TextKit1TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextKit1TextView.swift; sourceTree = "<group>"; };
61E24DCB2ABB385E00D75F50 /* OpenItemsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenItemsController.swift; sourceTree = "<group>"; };
61FA14CD2B05081D00E7D423 /* TextConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextConverter.swift; sourceTree = "<group>"; };
61FA14CF2B08E24A00E7D423 /* ColorPickerStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerStackView.swift; sourceTree = "<group>"; };
B300B33224291C8D00C1FE1E /* RTranslatorMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTranslatorMetadata.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2314,6 +2316,7 @@
B305646C23FC051E003304F2 /* ObjectUserChangeObserver.swift */,
B34A9F6325BF1ABB007C9A4A /* PDFDocumentExporter.swift */,
B32B8A562B18A08900A9A741 /* PDFThumbnailController.swift */,
61E24DCB2ABB385E00D75F50 /* OpenItemsController.swift */,
B3C6D551261C9F2E0068B9FE /* PlaceholderTextViewDelegate.swift */,
B378F4CC242CD45700B88A05 /* RepoParserDelegate.swift */,
B305646A23FC051E003304F2 /* RItemLocaleController.swift */,
Expand Down Expand Up @@ -5042,6 +5045,7 @@
B36181EC24C96B0500B30D56 /* SearchableCollection.swift in Sources */,
B3830CDB255451AB00910FE0 /* TagPickerAction.swift in Sources */,
B3593F40241A61C700760E20 /* ItemCell.swift in Sources */,
61E24DCC2ABB385E00D75F50 /* OpenItemsController.swift in Sources */,
B305679023FC1D9B003304F2 /* CollectionDifference+Separated.swift in Sources */,
B3F6AA3A2AB30663005BC22E /* AnnotationTool.swift in Sources */,
B34ACC7A2514EAAB00040C17 /* AnnotationColorGenerator.swift in Sources */,
Expand Down
101 changes: 101 additions & 0 deletions Zotero/Controllers/OpenItemsController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// OpenItemsController.swift
// Zotero
//
// Created by Miltiadis Vasilakis on 20/9/23.
// Copyright © 2023 Corporation for Digital Scholarship. All rights reserved.
//

import UIKit

typealias OpenItem = OpenItemsController.Item
typealias ItemPresentation = OpenItemsController.Presentation

protocol OpenItemsPresenter: AnyObject {
func showItem(with presentation: ItemPresentation?)
}

final class OpenItemsController {
// MARK: Types
struct Item: Hashable, Equatable, Codable {
enum Kind: Hashable, Equatable, Codable {
case pdf(libraryId: LibraryIdentifier, key: String)
case note(libraryId: LibraryIdentifier, key: String)

// MARK: Properties
var libraryId: LibraryIdentifier {
switch self {
case .pdf(let libraryId, _), .note(let libraryId, _):
return libraryId
}
}

var key: String {
switch self {
case .pdf(_, let key), .note(_, let key):
return key
}
}

var icon: UIImage {
switch self {
case .pdf:
return Asset.Images.ItemTypes.pdf.image

case .note:
return Asset.Images.ItemTypes.note.image
}
}

// MARK: Codable
enum CodingKeys: CodingKey {
case pdfKind
case noteKind
case libraryId
case key
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .pdf:
try container.encode(true, forKey: .pdfKind)

case .note:
try container.encode(true, forKey: .noteKind)
}

try container.encode(libraryId, forKey: .libraryId)
try container.encode(key, forKey: .key)
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let libraryId = try container.decode(LibraryIdentifier.self, forKey: .libraryId)
let key = try container.decode(String.self, forKey: .key)
if (try? container.decode(Bool.self, forKey: .pdfKind)) == true {
self = .pdf(libraryId: libraryId, key: key)
} else if (try? container.decode(Bool.self, forKey: .noteKind)) == true {
self = .note(libraryId: libraryId, key: key)
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [CodingKeys.pdfKind, CodingKeys.noteKind], debugDescription: "Item kind key not found"))
}
}
}

let kind: Kind
var userIndex: Int
var lastOpened: Date

init(kind: Kind, userIndex: Int, lastOpened: Date = .now) {
self.kind = kind
self.userIndex = userIndex
self.lastOpened = lastOpened
}
}

enum Presentation {
case pdf(library: Library, key: String, parentKey: String?, url: URL)
case note(library: Library, key: String, text: String, tags: [Tag], parentTitleData: NoteEditorState.TitleData?, title: String)
}
}
127 changes: 80 additions & 47 deletions Zotero/Extensions/NSUserActivity+Activities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,78 +9,111 @@
import Foundation

struct RestoredStateData {
let key: String?
let libraryId: LibraryIdentifier
let collectionId: CollectionIdentifier
let openItems: [OpenItem]
let restoreMostRecentlyOpenedItem: Bool

static var myLibrary: Self = {
.init(key: nil, libraryId: .custom(.myLibrary), collectionId: .custom(.all))
}()
static func myLibrary() -> Self {
.init(libraryId: .custom(.myLibrary), collectionId: .custom(.all), openItems: [], restoreMostRecentlyOpenedItem: false)
}
}

extension NSUserActivity {
private static let pdfId = "org.zotero.PDFActivity"
static let mainId = "org.zotero.MainActivity"
public static let pdfId = "org.zotero.PDFActivity"
public static let mainId = "org.zotero.MainActivity"

static var mainActivity: NSUserActivity {
private static let libraryIdKey = "libraryId"
private static let collectionIdKey = "collectionId"
private static let openItemsKey = "openItems"
private static let restoreMostRecentlyOpenedItemKey = "restoreMostRecentlyOpenedItem"

static func mainActivity() -> NSUserActivity {
return NSUserActivity(activityType: self.mainId)
.addUserInfoEntries(openItems: [])
.addUserInfoEntries(restoreMostRecentlyOpened: false)
}

static func pdfActivity(for key: String, libraryId: LibraryIdentifier, collectionId: CollectionIdentifier) -> NSUserActivity {
let activity = NSUserActivity(activityType: self.pdfId)
var pdfUserInfo: [AnyHashable: Any] = ["key": key, "libraryId": libraryIdToString(libraryId)]
if let collectionIdData = try? JSONEncoder().encode(collectionId) {
pdfUserInfo["collectionId"] = collectionIdData
}
activity.addUserInfoEntries(from: pdfUserInfo)
return activity
static func pdfActivity(with openItems: [OpenItem], libraryId: LibraryIdentifier, collectionId: CollectionIdentifier) -> NSUserActivity {
return NSUserActivity(activityType: self.pdfId)
.addUserInfoEntries(openItems: openItems)
.addUserInfoEntries(libraryId: libraryId, collectionId: collectionId, restoreMostRecentlyOpened: true)
}

@discardableResult
func addUserInfoEntries(openItems: [OpenItem]) -> Self {
var userInfo: [AnyHashable: Any] = [:]
let encoder = JSONEncoder()
userInfo[Self.openItemsKey] = openItems.compactMap { try? encoder.encode($0) }
addUserInfoEntries(from: userInfo)
return self
}

@discardableResult
func set(title: String? = nil) -> NSUserActivity {
func set(title: String? = nil) -> Self {
self.title = title
return self
}

private static func libraryIdToString(_ libraryId: LibraryIdentifier) -> String {
switch libraryId {
case .custom:
return "myLibrary"
case .group(let groupId):
return "g:\(groupId)"
@discardableResult
func addUserInfoEntries(libraryId: LibraryIdentifier? = nil, collectionId: CollectionIdentifier? = nil, restoreMostRecentlyOpened: Bool = false) -> Self {
var userInfo: [AnyHashable: Any] = [:]
if let libraryId {
userInfo[Self.libraryIdKey] = libraryIdToString(libraryId)
}
}

private func stringToLibraryId(_ string: String) -> LibraryIdentifier? {
guard !string.isEmpty else { return nil }

if string == "myLibrary" {
return .custom(.myLibrary)
if let collectionId, let collectionIdData = try? JSONEncoder().encode(collectionId) {
userInfo[Self.collectionIdKey] = collectionIdData
}
userInfo[Self.restoreMostRecentlyOpenedItemKey] = restoreMostRecentlyOpened
addUserInfoEntries(from: userInfo)
return self

if string[string.startIndex..<string.index(string.startIndex, offsetBy: 1)] == "g" {
if let groupId = Int(String(string[string.index(string.startIndex, offsetBy: 2)..<string.endIndex])) {
return .group(groupId)
func libraryIdToString(_ libraryId: LibraryIdentifier) -> String {
switch libraryId {
case .custom:
return "myLibrary"
case .group(let groupId):
return "g:\(groupId)"
}
}

return nil
}

var restoredStateData: RestoredStateData? {
guard self.activityType == NSUserActivity.pdfId,
let userInfo,
let key = userInfo["key"] as? String,
let libraryString = userInfo["libraryId"] as? String,
let libraryId = stringToLibraryId(libraryString)
else { return nil }
var collectionId: CollectionIdentifier
if let collectionIdData = userInfo["collectionId"] as? Data,
let decodedCollectionId = try? JSONDecoder().decode(CollectionIdentifier.self, from: collectionIdData) {
collectionId = decodedCollectionId
} else {
collectionId = Defaults.shared.selectedCollectionId
guard let userInfo else { return nil }
var libraryId: LibraryIdentifier = Defaults.shared.selectedLibrary
var collectionId: CollectionIdentifier = Defaults.shared.selectedCollectionId
var openItems: [OpenItem] = []
var restoreMostRecentlyOpenedItem = false
if let libraryString = userInfo[Self.libraryIdKey] as? String, let _libraryId = stringToLibraryId(libraryString) {
libraryId = _libraryId
}
let decoder = JSONDecoder()
if let collectionIdData = userInfo[Self.collectionIdKey] as? Data, let _collectionId = try? decoder.decode(CollectionIdentifier.self, from: collectionIdData) {
collectionId = _collectionId
}
if let openItemsDataArray = userInfo[Self.openItemsKey] as? [Data] {
openItems = openItemsDataArray.compactMap { try? decoder.decode(OpenItem.self, from: $0) }
}
if let _restoreMostRecentlyOpenedItem = userInfo[Self.restoreMostRecentlyOpenedItemKey] as? Bool {
restoreMostRecentlyOpenedItem = _restoreMostRecentlyOpenedItem
}
// TODO: Migrate old pdf activity ("key", "libraryId") to "openItems"?
return RestoredStateData(libraryId: libraryId, collectionId: collectionId, openItems: openItems, restoreMostRecentlyOpenedItem: restoreMostRecentlyOpenedItem)

func stringToLibraryId(_ string: String) -> LibraryIdentifier? {
guard !string.isEmpty else { return nil }

if string == "myLibrary" {
return .custom(.myLibrary)
}

if string[string.startIndex..<string.index(string.startIndex, offsetBy: 1)] == "g" {
if let groupId = Int(String(string[string.index(string.startIndex, offsetBy: 2)..<string.endIndex])) {
return .group(groupId)
}
}

return nil
}
return RestoredStateData(key: key, libraryId: libraryId, collectionId: collectionId)
}
}
2 changes: 1 addition & 1 deletion Zotero/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {

func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
if shortcutItem.type == NSUserActivity.mainId {
completionHandler(coordinator.showMainScreen(with: .custom(.myLibrary), selectedCollection: .custom(.all)))
completionHandler(coordinator.showMainScreen(with: .myLibrary(), session: windowScene.session))
}
completionHandler(false)
}
Expand Down
Loading

0 comments on commit 9569c02

Please sign in to comment.