Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: External links #1236

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9b1f099
WIP endpoints
adrien-coye Jul 26, 2024
b91e02f
chore: Merge branch 'master' into externalLinks
adrien-coye Aug 15, 2024
00271f3
feat: In memory realm for external share context
adrien-coye Aug 16, 2024
729fd5e
fix: Project builds
adrien-coye Aug 16, 2024
9c21f67
chore: Merge branch 'master' into externalLinks
adrien-coye Aug 16, 2024
2d38281
feat: Public share metadata WIP
adrien-coye Aug 19, 2024
e821a54
feat(Rights): Updated to be compatible with public share
adrien-coye Aug 20, 2024
254255e
feat(PublicShareMetadata): Parsing pass
adrien-coye Aug 20, 2024
72fe9cf
chore: Merge branch 'master' into externalLinks
adrien-coye Aug 22, 2024
30389e1
refactor(Endpoint): Split files into dedicated extensiions
adrien-coye Aug 22, 2024
4db1410
chore: Merge branch 'master' into externalLinks
adrien-coye Aug 28, 2024
4dfc177
feat: Fetch root folder for a public share
adrien-coye Aug 29, 2024
638b423
feat: Cursored public share children query
adrien-coye Sep 2, 2024
fd7dfab
feat: Can natively show a public share within the app
adrien-coye Sep 3, 2024
fb9442b
chore: Merge branch 'master' into externalLinks
adrien-coye Sep 16, 2024
d9bc2d3
fix(FilePresenter): Present public share in context
adrien-coye Sep 16, 2024
7c8c2c7
fix: Capabilities for Public Share
adrien-coye Sep 16, 2024
70d65a0
feat: Thumbnails on public share
adrien-coye Sep 17, 2024
d352554
feat: Can navigate hierarchy of public share folders
adrien-coye Sep 18, 2024
07e1c48
feat: Preview for public share
adrien-coye Sep 19, 2024
c26647f
feat: Download file from public share
adrien-coye Sep 19, 2024
76bf4e0
feat: Actions match spec on public share
adrien-coye Sep 20, 2024
c6a78e7
chore: Merge branch 'master' into externalLinks
adrien-coye Sep 23, 2024
14c2ca0
fix: Tuist config update to build
adrien-coye Sep 23, 2024
7224695
feat: Add to my drive button
adrien-coye Sep 23, 2024
98822b8
chore: Merge branch 'master' into externalLinks
adrien-coye Sep 24, 2024
a1bf2c4
WIP
adrien-coye Sep 24, 2024
31e6119
chore: Matomo for public share tasks
adrien-coye Sep 25, 2024
a4b8d70
chore: Align Matomo with android on what exists
adrien-coye Sep 26, 2024
e7d49dc
feat: Download all files from current folder as a ZIP within a public…
adrien-coye Sep 26, 2024
126df8e
chore: Merge branch 'master' into externalLinks
adrien-coye Sep 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions kDrive/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
application.registerForRemoteNotifications()

// swiftlint:disable force_try
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO remove

Task {
try! await Task.sleep(nanoseconds:5_000_000_000)
print("coucou")
let somePublicShare = URL(string: "")
//await UIApplication.shared.open(somePublicShare!) // opens safari

let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true)
await UniversalLinksHelper.handlePath(components!.path)
}


return true
}

Expand Down
20 changes: 20 additions & 0 deletions kDrive/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,26 @@ public struct AppRouter: AppNavigable {

// MARK: RouterFileNavigable

@MainActor public func presentPublicShare(
frozenRootFolder: File,
publicShareProxy: PublicShareProxy,
driveFileManager: DriveFileManager,
apiFetcher: PublicShareApiFetcher
) {
// TODO: Present on top of existing views
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Navigation

guard let window,
let rootViewController = window.rootViewController else {
fatalError("TODO: lazy load a rootViewController")
}

let filePresenter = FilePresenter(viewController: rootViewController)
filePresenter.presentPublicShareDirectory(publicShareProxy: publicShareProxy,
frozenRootFolder: frozenRootFolder,
rootViewController: rootViewController,
driveFileManager: driveFileManager,
apiFetcher: apiFetcher)
}

@MainActor public func present(file: File, driveFileManager: DriveFileManager) {
present(file: file, driveFileManager: driveFileManager, office: false)
}
Expand Down
14 changes: 8 additions & 6 deletions kDrive/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,15 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDel

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
Log.sceneDelegate("scene continue userActivity")
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
return
}
Task {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
return
}

UniversalLinksHelper.handlePath(components.path)
await UniversalLinksHelper.handlePath(components.path)
}
}

func scene(_ scene: UIScene, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
Expand Down
26 changes: 26 additions & 0 deletions kDrive/UI/Controller/Files/FilePresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,32 @@ final class FilePresenter {
}
}

public func presentPublicShareDirectory(
publicShareProxy: PublicShareProxy,
frozenRootFolder: File,
rootViewController: UIViewController,
driveFileManager: DriveFileManager,
apiFetcher: PublicShareApiFetcher
) {
let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy,
sortType: .nameAZ,
driveFileManager: driveFileManager,
currentDirectory: frozenRootFolder,
apiFetcher: apiFetcher)

// TODO: Fix access right
// guard !rootFolder.isDisabled else {
// return
// }

// TODO: Build clean context aware navigation
let nextVC = FileListViewController(viewModel: viewModel)
print("nextVC:\(nextVC) viewModel:\(viewModel) navigationController:\(navigationController)")
// navigationController?.pushViewController(nextVC, animated: true)

rootViewController.present(nextVC, animated: true)
}

public func presentDirectory(
for file: File,
driveFileManager: DriveFileManager,
Expand Down
82 changes: 82 additions & 0 deletions kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Infomaniak kDrive - iOS App
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import kDriveCore
import RealmSwift
import UIKit

/// Public share view model, loading content from memory realm
final class PublicShareViewModel: InMemoryFileListViewModel {
var publicShareProxy: PublicShareProxy?
let rootProxy: ProxyFile
var publicShareApiFetcher: PublicShareApiFetcher?

required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) {
guard let currentDirectory else {
fatalError("PublicShareViewModel requires a currentDirectory to work")
}

// TODO: i18n
let configuration = Configuration(selectAllSupported: false,
rootTitle: "public share",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: i18n

emptyViewType: .emptyFolder,
supportsDrop: false,
matomoViewPath: [MatomoUtils.Views.menu.displayName, "publicShare"])

rootProxy = currentDirectory.proxify()
super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory)
observedFiles = AnyRealmCollection(currentDirectory.children)
}

convenience init(
publicShareProxy: PublicShareProxy,
sortType: SortType,
driveFileManager: DriveFileManager,
currentDirectory: File,
apiFetcher: PublicShareApiFetcher
) {
self.init(driveFileManager: driveFileManager, currentDirectory: currentDirectory)
self.publicShareProxy = publicShareProxy
self.sortType = sortType
publicShareApiFetcher = apiFetcher
}

override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost the same as a ShareWithMe, I'll check if I can factorise some code.

guard !isLoading || cursor != nil,
let publicShareProxy,
let publicShareApiFetcher else {
return
}

// Only show loading indicator if we have nothing in cache
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guess I can remove this one

if !currentDirectory.canLoadChildrenFromCache {
startRefreshing(cursor: cursor)
}
defer {
endRefreshing()
}

let (_, nextCursor) = try await driveFileManager.publicShareFiles(rootProxy: rootProxy,
publicShareProxy: publicShareProxy,
publicShareApiFetcher: publicShareApiFetcher)
endRefreshing()
if let nextCursor {
try await loadFiles(cursor: nextCursor)
}
}
}
83 changes: 81 additions & 2 deletions kDrive/Utils/UniversalLinksHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,44 @@ enum UniversalLinksHelper {
regex: Regex(pattern: #"^/app/drive/([0-9]+)/redirect/([0-9]+)$"#)!,
displayMode: .file
)

/// Matches a public share link
static let publicShareLink = Link(
regex: Regex(pattern: #"^/app/share/([0-9]+)/([a-z0-9-]+)$"#)!,
displayMode: .file
)

/// Matches a directory list link
static let directoryLink = Link(regex: Regex(pattern: #"^/app/drive/([0-9]+)/files/([0-9]+)$"#)!, displayMode: .file)

/// Matches a file preview link
static let filePreview = Link(
regex: Regex(pattern: #"^/app/drive/([0-9]+)/files/([0-9]+/)?preview/[a-z]+/([0-9]+)$"#)!,
displayMode: .file
)

/// Matches an office file link
static let officeLink = Link(regex: Regex(pattern: #"^/app/office/([0-9]+)/([0-9]+)$"#)!, displayMode: .office)

static let all = [privateShareLink, directoryLink, filePreview, officeLink]
static let all = [privateShareLink, publicShareLink, directoryLink, filePreview, officeLink]
}

private enum DisplayMode {
case office, file
}

static func handlePath(_ path: String) -> Bool {
@discardableResult
static func handlePath(_ path: String) async -> Bool {
DDLogInfo("[UniversalLinksHelper] Trying to open link with path: \(path)")

// Public share link regex
let shareLink = Link.publicShareLink
let matches = shareLink.regex.matches(in: path)
if await processPublicShareLink(matches: matches, displayMode: shareLink.displayMode) {
return true
}

// Common regex
for link in Link.all {
let matches = link.regex.matches(in: path)
if processRegex(matches: matches, displayMode: link.displayMode) {
Expand All @@ -66,6 +84,33 @@ enum UniversalLinksHelper {
return false
}

private static func processPublicShareLink(matches: [[String]], displayMode: DisplayMode) async -> Bool {
@InjectService var accountManager: AccountManageable

guard let firstMatch = matches.first,
let driveId = firstMatch[safe: 1],
let driveIdInt = Int(driveId),
let shareLinkUid = firstMatch[safe: 2] else {
return false
}

// request metadata
let apiFetcher = PublicShareApiFetcher()
guard let metadata = try? await apiFetcher.getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid)
else {
return false
}

// get file ID from metadata
let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid)
openPublicShare(driveId: driveIdInt,
linkUuid: shareLinkUid,
fileId: metadata.fileId,
driveFileManager: publicShareDriveFileManager,
apiFetcher: apiFetcher)
return true
}

private static func processRegex(matches: [[String]], displayMode: DisplayMode) -> Bool {
@InjectService var accountManager: AccountManageable

Expand All @@ -83,6 +128,40 @@ enum UniversalLinksHelper {
return true
}

private static func openPublicShare(driveId: Int,
linkUuid: String,
fileId: Int,
driveFileManager: DriveFileManager,
apiFetcher: PublicShareApiFetcher) {
Task {
do {
let rootFolder = try await apiFetcher.getShareLinkFile(driveId: driveId,
linkUuid: linkUuid,
fileId: fileId)
// Root folder must be in database for the FileListViewModel to work
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this in a func ?

try driveFileManager.database.writeTransaction { writableRealm in
writableRealm.add(rootFolder)
}

let frozenRootFolder = rootFolder.freeze()

@InjectService var appNavigable: AppNavigable
let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: fileId, shareLinkUid: linkUuid)
await appNavigable.presentPublicShare(
frozenRootFolder: frozenRootFolder,
publicShareProxy: publicShareProxy,
driveFileManager: driveFileManager,
apiFetcher: apiFetcher
)
} catch {
DDLogError(
"[UniversalLinksHelper] Failed to get public folder [driveId:\(driveId) linkUuid:\(linkUuid) fileId:\(fileId)]: \(error)"
)
await UIConstants.showSnackBarIfNeeded(error: error)
}
}
}

private static func openFile(id: Int, driveFileManager: DriveFileManager, office: Bool) {
Task {
do {
Expand Down
48 changes: 48 additions & 0 deletions kDriveCore/Data/Api/DriveApiFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,54 @@ public class AuthenticatedImageRequestModifier: ImageDownloadRequestModifier {
}
}

public struct PublicShareMetadata: Decodable {
public let url: URL
public let fileId: Int
public let right: String

public let validUntil: TimeInterval?
public let capabilities: Rights

public let createdBy: TimeInterval
public let createdAt: TimeInterval
public let updatedAt: TimeInterval
public let accessBlocked: Bool

enum CodingKeys: String, CodingKey {
case url
case fileId
case right
case validUntil
case capabilities
case createdBy
case createdAt
case updatedAt
case accessBlocked
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

do {
url = try container.decode(URL.self, forKey: .url)
fileId = try container.decode(Int.self, forKey: .fileId)
right = try container.decode(String.self, forKey: .right)

validUntil = try container.decodeIfPresent(TimeInterval.self, forKey: .validUntil)
capabilities = try container.decode(Rights.self, forKey: .capabilities)

createdBy = try container.decode(TimeInterval.self, forKey: .createdBy)
createdAt = try container.decode(TimeInterval.self, forKey: .createdAt)
updatedAt = try container.decode(TimeInterval.self, forKey: .updatedAt)

accessBlocked = try container.decode(Bool.self, forKey: .accessBlocked)
} catch {
// TODO: remove
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Remove

fatalError("error:\(error)")
}
}
}

public class DriveApiFetcher: ApiFetcher {
@LazyInjectService var accountManager: AccountManageable
@LazyInjectService var tokenable: InfomaniakTokenable
Expand Down
Loading
Loading