-
Notifications
You must be signed in to change notification settings - Fork 9
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
feat: External links #1236
Changes from 14 commits
9b1f099
b91e02f
00271f3
729fd5e
9c21f67
2d38281
e821a54
254255e
72fe9cf
30389e1
4db1410
4dfc177
638b423
fd7dfab
fb9442b
d9bc2d3
7c8c2c7
70d65a0
d352554
07e1c48
c26647f
76bf4e0
c6a78e7
14c2ca0
7224695
98822b8
a1bf2c4
31e6119
a4b8d70
e7d49dc
126df8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
|
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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
|
@@ -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 | ||
|
||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO remove