Skip to content

Commit fdc78f1

Browse files
Merge pull request #535 from PermanentOrg/feature/VSP-1635-IOS-Mobile-Old-Share-management-option-needs-to-be-replaced-for-private-workspace
VSP-1635[IOS][Mobile] Old Share management option needs to be replaced for private workspace
2 parents 6b72924 + 67d6b2d commit fdc78f1

File tree

17 files changed

+712
-176
lines changed

17 files changed

+712
-176
lines changed

Permanent/Common/Files/ViewModel/FilesViewModel.swift

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -354,25 +354,6 @@ class FilesViewModel: NSObject, ViewModelInterface {
354354
}
355355
)}
356356

357-
func download(file: FileModel, onDownloadStart: @escaping VoidAction, onFileDownloaded: @escaping DownloadResponse) {
358-
let downloadInfo = FileDownloadInfoVM(
359-
fileType: file.type,
360-
folderLinkId: file.folderLinkId,
361-
parentFolderLinkId: file.parentFolderLinkId
362-
)
363-
364-
downloader = DownloadManagerGCD()
365-
downloader?.download(
366-
downloadInfo,
367-
onDownloadStart: onDownloadStart,
368-
onFileDownloaded: onFileDownloaded,
369-
progressHandler: nil,
370-
completion: {
371-
self.downloader = nil
372-
self.downloadQueue.safeRemoveFirst()
373-
}
374-
)}
375-
376357
func delete(_ files: [FileModel]?, then handler: @escaping ServerResponse) {
377358
guard let files = files else {
378359
handler(.error(message: .errorMessage))

Permanent/Common/Managers/Download/DownloadManagerGCD.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class DownloadManagerGCD: Downloader {
1717
}
1818

1919
func fileVO(forRecordVO recordVO: RecordVO, fileType: FileType) -> FileVO? {
20-
if fileType == .video,
20+
if fileType == .video || fileType == .image,
2121
let fileVO = recordVO.recordVO?.fileVOS?.first(where: {$0.format == "file.format.converted"}) {
2222
return fileVO
2323
} else {
@@ -152,13 +152,29 @@ class DownloadManagerGCD: Downloader {
152152
return handler(nil, APIError.invalidResponse)
153153
}
154154

155-
// If the file was converted, then it most certainly is an mp4
156-
// Otherwise, the file was not converted, we use the original filename + extension
157155
let fileName: String
158156
if fileType == .video && fileVO.contentType == "video/mp4" {
159157
fileName = displayName + ".mp4"
160158
} else {
161-
fileName = uploadFileName
159+
var fileExtension = (uploadFileName as NSString).pathExtension
160+
161+
if fileVO.format == "file.format.converted" {
162+
if let contentType = fileVO.contentType {
163+
if contentType.contains("jpeg") {
164+
fileExtension = "JPG"
165+
} else if contentType.contains("png") {
166+
fileExtension = "PNG"
167+
} else if contentType.contains("heic") {
168+
fileExtension = "HEIC"
169+
}
170+
}
171+
}
172+
173+
if !fileExtension.isEmpty {
174+
fileName = displayName + "." + fileExtension
175+
} else {
176+
fileName = displayName
177+
}
162178
}
163179

164180
let apiOperation = APIOperation(FilesEndpoint.download(url: url, filename: fileName, progressHandler: progressHandler))

Permanent/Common/Network/Layer/APIRequestDispatcher.swift

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,19 @@ class APIRequestDispatcher: RequestDispatcherProtocol {
6161

6262
NetworkLogger.log(request: urlRequest)
6363

64+
let shouldIgnoreErrors = request.ignoreErrors
65+
6466
// Create a URLSessionTask to execute the URLRequest.
6567
var task: URLSessionTask?
6668
switch request.requestType {
6769
case .data:
6870
task = networkSession.dataTask(with: urlRequest, completionHandler: { data, urlResponse, error in
69-
self.handleJsonTaskResponse(data: data, urlResponse: urlResponse, error: error, completion: completion)
71+
self.handleJsonTaskResponse(data: data, urlResponse: urlResponse, error: error, ignoreErrors: shouldIgnoreErrors, completion: completion)
7072
})
7173

7274
case .upload:
7375
task = networkSession.uploadTask(with: urlRequest, progressHandler: request.progressHandler, completion: { data, urlResponse, error in
74-
self.handleJsonTaskResponse(data: data, urlResponse: urlResponse, error: error, completion: completion)
76+
self.handleJsonTaskResponse(data: data, urlResponse: urlResponse, error: error, ignoreErrors: shouldIgnoreErrors, completion: completion)
7577
})
7678

7779
case .download:
@@ -99,8 +101,9 @@ class APIRequestDispatcher: RequestDispatcherProtocol {
99101
/// - data: The `Data` instance to be serialized into a JSON object.
100102
/// - urlResponse: The received optional `URLResponse` instance.
101103
/// - error: The received optional `Error` instance.
104+
/// - ignoreErrors: If true, errors will be silently ignored without triggering session expiration
102105
/// - completion: Completion handler.
103-
private func handleJsonTaskResponse(data: Data?, urlResponse: URLResponse?, error: Error?, completion: @escaping (OperationResult) -> Void) {
106+
private func handleJsonTaskResponse(data: Data?, urlResponse: URLResponse?, error: Error?, ignoreErrors: Bool, completion: @escaping (OperationResult) -> Void) {
104107
// Check for errors
105108
if let apiError = APIError.error(withCode: (error as NSError?)?.code) {
106109
return completion(.error(apiError, nil))
@@ -113,6 +116,8 @@ class APIRequestDispatcher: RequestDispatcherProtocol {
113116
}
114117
NetworkLogger.log(response: urlResponse, data: data, error: error)
115118

119+
let shouldIgnoreAuthErrors = ignoreErrors || isNonCriticalEndpoint(urlResponse)
120+
116121
// Verify the HTTP status code.
117122
let result = verify(data: data, urlResponse: urlResponse, error: error)
118123
switch result {
@@ -124,7 +129,7 @@ class APIRequestDispatcher: RequestDispatcherProtocol {
124129
if let mfaError = json as? [String: Any],
125130
let results = mfaError["Results"] as? [[String: Any]],
126131
let message = (results[0]["message"] as? [String])?.first,
127-
(message == "warning.auth.mfaToken" && !ignoresMFAWarning) {
132+
(message == "warning.auth.mfaToken" && !ignoresMFAWarning && !shouldIgnoreAuthErrors) {
128133
DispatchQueue.main.async {
129134
NotificationCenter.default.post(name: Self.sessionExpiredNotificationName, object: self)
130135
}
@@ -137,7 +142,7 @@ class APIRequestDispatcher: RequestDispatcherProtocol {
137142
}
138143

139144
case .failure(let error):
140-
if error as? APIError == APIError.unauthorized {
145+
if error as? APIError == APIError.unauthorized && !shouldIgnoreAuthErrors {
141146
completion(OperationResult.error(error, urlResponse))
142147

143148
DispatchQueue.main.async {
@@ -241,4 +246,18 @@ class APIRequestDispatcher: RequestDispatcherProtocol {
241246
default: return nil
242247
}
243248
}
249+
250+
private func isNonCriticalEndpoint(_ urlResponse: HTTPURLResponse) -> Bool {
251+
guard let url = urlResponse.url?.absoluteString else { return false }
252+
253+
if url.contains("/api/v2/event") && !url.contains("/checklist") {
254+
return true
255+
}
256+
257+
if url.contains("/api/v2/share-links") {
258+
return true
259+
}
260+
261+
return false
262+
}
244263
}

Permanent/Common/Network/Layer/RequestProtocol.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ protocol RequestProtocol {
6262
var bodyData: Data? { get }
6363

6464
var customURL: String? { get }
65+
var ignoreErrors: Bool { get }
6566
}
6667

6768
extension RequestProtocol {
@@ -142,4 +143,8 @@ extension RequestProtocol {
142143
return nil
143144
}
144145
}
146+
147+
var ignoreErrors: Bool {
148+
return false
149+
}
145150
}

Permanent/Modules/FileOperations/ViewController/FileDetailsViewController.swift

Lines changed: 152 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import UIKit
9+
import SwiftUI
910

1011
class FileDetailsViewController: BaseViewController<FilePreviewViewModel> {
1112
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
@@ -112,24 +113,84 @@ class FileDetailsViewController: BaseViewController<FilePreviewViewModel> {
112113
}
113114

114115
@objc func showShareMenu(_ sender: Any) {
115-
var menuItems: [FileMenuViewController.MenuItem] = []
116+
var menuItems: [FileMenuViewModel.MenuItem] = []
116117

117-
menuItems.append(FileMenuViewController.MenuItem(type: .shareToAnotherApp, action: { [self] in
118-
shareWithOtherApps()
119-
}))
118+
// Share to Permanent - only for files with ownership
119+
if file.permissions.contains(.ownership) {
120+
menuItems.append(FileMenuViewModel.MenuItem(type: .shareToPermanent, action: nil))
121+
}
122+
123+
// Share to another app - for files with share permission (not folders)
124+
if file.permissions.contains(.share) && file.type.isFolder == false {
125+
menuItems.append(FileMenuViewModel.MenuItem(type: .shareToAnotherApp, action: { [self] in
126+
shareWithOtherApps()
127+
}))
128+
}
129+
130+
// Publish on the web - for files with delete permission
131+
if file.permissions.contains(.delete) {
132+
menuItems.append(FileMenuViewModel.MenuItem(type: .publish, action: { [self] in
133+
publishAction()
134+
}))
135+
}
120136

121-
if let publicURL = viewModel?.publicURL {
122-
menuItems.append(FileMenuViewController.MenuItem(type: .getLink, action: { [self] in
123-
share(url: publicURL)
137+
// Download - for files with read permission (not folders)
138+
if file.permissions.contains(.read) && file.type.isFolder == false {
139+
menuItems.append(FileMenuViewModel.MenuItem(type: .download, action: { [weak self] in
140+
guard let self = self else { return }
141+
// Store the file reference before dismissing
142+
let fileToDownload = self.file!
143+
144+
// First dismiss the menu
145+
if let menuHostingController = self.presentedViewController {
146+
menuHostingController.dismiss(animated: true) {
147+
// Then dismiss the preview and notify delegate
148+
self.navigationController?.dismiss(animated: true) {
149+
// Use delegate to request download from MainViewController
150+
self.delegate?.filePreviewNavigationControllerRequestsDownload(self, file: fileToDownload)
151+
}
152+
}
153+
}
124154
}))
125-
} else if self.file.permissions.contains(.ownership) {
126-
menuItems.append(FileMenuViewController.MenuItem(type: .shareToPermanent, action: nil))
127155
}
156+
157+
let swiftUIView = FileMoreMenuView(
158+
fileViewModel: file,
159+
menuItems: menuItems,
160+
onDismiss: { [weak self] in
161+
// Only dismiss the menu overlay, not the details view
162+
self?.presentedViewController?.dismiss(animated: true)
163+
},
164+
onShareManagementRequested: { [weak self] file in
165+
// Don't dismiss the details view - just dismiss the menu and present share management on top
166+
if let hostingController = self?.presentedViewController {
167+
hostingController.dismiss(animated: true) {
168+
self?.presentShareManagement(for: file)
169+
}
170+
}
171+
},
172+
downloadHandler: nil
173+
)
174+
175+
let hostingController = UIHostingController(rootView: swiftUIView)
176+
hostingController.modalPresentationStyle = UIModalPresentationStyle.overFullScreen
177+
hostingController.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
178+
hostingController.view.backgroundColor = UIColor.clear
179+
180+
present(hostingController, animated: true)
181+
}
128182

129-
let vc = FileMenuViewController()
130-
vc.fileViewModel = file
131-
vc.menuItems = menuItems
132-
present(vc, animated: true)
183+
private func presentShareManagement(for file: FileModel) {
184+
let shareContainerView = ShareContainerView(fileModel: file)
185+
let hostingController = UIHostingController(rootView: shareContainerView)
186+
hostingController.modalPresentationStyle = UIModalPresentationStyle.pageSheet
187+
if #available(iOS 15.0, *) {
188+
if let sheet = hostingController.sheetPresentationController {
189+
sheet.detents = [.large()]
190+
sheet.prefersGrabberVisible = true
191+
}
192+
}
193+
present(hostingController, animated: true)
133194
}
134195

135196
@objc func closeButtonAction(_ sender: Any) {
@@ -178,13 +239,16 @@ class FileDetailsViewController: BaseViewController<FilePreviewViewModel> {
178239
}
179240

180241
private func share(url: URL) {
181-
// For now, dismiss the menu in case another one opens so we avoid crash.
182-
documentInteractionController.dismissMenu(animated: true)
242+
let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
243+
244+
// For iPad support
245+
if let popover = activityViewController.popoverPresentationController {
246+
popover.sourceView = view
247+
popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
248+
popover.permittedArrowDirections = []
249+
}
183250

184-
documentInteractionController.url = url
185-
documentInteractionController.uti = url.typeIdentifier ?? "public.data, public.content"
186-
documentInteractionController.name = url.localizedName ?? url.lastPathComponent
187-
documentInteractionController.presentOptionsMenu(from: .zero, in: view, animated: true)
251+
present(activityViewController, animated: true)
188252
}
189253

190254
private func segmentedControlChangedAction() -> ((FileDetailsMenuCollectionViewCell) -> Void) {
@@ -216,8 +280,11 @@ class FileDetailsViewController: BaseViewController<FilePreviewViewModel> {
216280
}
217281

218282
func shareWithOtherApps() {
219-
if let fileName = self.viewModel?.fileName(),
220-
let localURL = fileHelper.url(forFileNamed: fileName) {
283+
// Check for file using displayName + extension (matches DownloadManagerGCD naming)
284+
let fileExtension = (file.uploadFileName as NSString).pathExtension
285+
let fileName = !fileExtension.isEmpty ? "\(file.name).\(fileExtension)" : file.name
286+
287+
if let localURL = fileHelper.url(forFileNamed: fileName) {
221288
share(url: localURL)
222289
} else {
223290
let preparingAlert = UIAlertController(title: "Preparing File..".localized(), message: nil, preferredStyle: .alert)
@@ -240,6 +307,70 @@ class FileDetailsViewController: BaseViewController<FilePreviewViewModel> {
240307
}
241308
}
242309
}
310+
311+
func publishAction() {
312+
let title = String(format: "\(String.publish) \"%@\"?", file.name)
313+
showActionDialog(
314+
styled: .simpleWithDescription,
315+
withTitle: title,
316+
description: .publishDescription,
317+
positiveButtonTitle: .publish,
318+
positiveAction: { [weak self] in
319+
self?.actionDialog?.dismiss()
320+
self?.publish()
321+
},
322+
overlayView: nil
323+
)
324+
}
325+
326+
private func publish() {
327+
showSpinner()
328+
329+
// Get the current archive number
330+
guard let archiveNbr = AuthenticationManager.shared.session?.selectedArchive?.archiveNbr else {
331+
hideSpinner()
332+
showErrorAlert(message: "Unable to publish file")
333+
return
334+
}
335+
336+
// First, get the public root folder
337+
let filesRepository = FilesRepository()
338+
filesRepository.getPublicRoot(archiveNbr: archiveNbr) { [weak self] (folder, error) in
339+
guard let self = self else { return }
340+
341+
if let error = error {
342+
self.hideSpinner()
343+
self.showErrorAlert(message: error.localizedDescription)
344+
return
345+
}
346+
347+
guard let publicRootFolder = folder else {
348+
self.hideSpinner()
349+
self.showErrorAlert(message: "Unable to get public folder")
350+
return
351+
}
352+
353+
// Now relocate (copy) the file to the public folder
354+
filesRepository.relocate(
355+
files: [self.file],
356+
folderLinkId: publicRootFolder.folderLinkId,
357+
isCopy: true
358+
) { error in
359+
self.hideSpinner()
360+
361+
if let error = error {
362+
self.showErrorAlert(message: error.localizedDescription)
363+
} else {
364+
if self.file.type.isFolder {
365+
self.view.showNotificationBanner(height: Constants.Design.bannerHeight, title: "Folder published successfully".localized())
366+
} else {
367+
self.view.showNotificationBanner(height: Constants.Design.bannerHeight, title: "File published successfully".localized())
368+
}
369+
}
370+
}
371+
}
372+
}
373+
243374
}
244375

245376
// MARK: - UICollectionViewDataSource

Permanent/Modules/FileOperations/ViewController/FilePreviewListViewController.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,9 @@ extension FilePreviewListViewController: FilePreviewNavigationControllerDelegate
204204
self.hasChanges = true
205205
}
206206
}
207+
208+
func filePreviewNavigationControllerRequestsDownload(_ filePreviewNavigationVC: UIViewController, file: FileModel) {
209+
// Forward the request through the navigation controller
210+
(navigationController as? FilePreviewNavigationController)?.filePreviewNavDelegate?.filePreviewNavigationControllerRequestsDownload(self, file: file)
211+
}
207212
}

0 commit comments

Comments
 (0)